Merge remote-tracking branch 'origin/main' into kennylopez-agent-catalog-sharing

This commit is contained in:
kenny lopez
2026-07-24 09:49:47 -07:00
15 changed files with 1383 additions and 169 deletions
@@ -681,3 +681,114 @@ test("parseSystemPromptSections splits on the LAST occurrence of the canonical d
},
]);
});
// ── Modern [Team Instructions] bracket-header extraction ─────────────────────
test("parseSystemPromptSections (modern) extracts bracket Team Instructions after Base+System", () => {
// with_team() emits "\n\n[Team Instructions]\n{instructions}" as a top-level
// bracket section — the same framing used by with_core() and with_canvas().
const framed = [
"[Base]",
"You are a helpful assistant.",
"",
"[System]",
"You are Agent X.",
"",
"[Team Instructions]",
"Always respond in markdown.",
].join("\n");
const sections = parseSystemPromptSections(framed);
assert.deepEqual(sections, [
{ title: "Base", body: "You are a helpful assistant." },
{ title: "System", body: "You are Agent X." },
{ title: "Team Instructions", body: "Always respond in markdown." },
]);
});
test("parseSystemPromptSections (modern) extracts bracket Team Instructions after System-only", () => {
// [Base] absent; [Team Instructions] is a direct top-level bracket section.
const framed = [
"[System]",
"You are Agent Y.",
"",
"[Team Instructions]",
"Keep responses concise.",
].join("\n");
const sections = parseSystemPromptSections(framed);
assert.deepEqual(sections, [
{ title: "System", body: "You are Agent Y." },
{ title: "Team Instructions", body: "Keep responses concise." },
]);
});
test("parseSystemPromptSections (modern) handles bracket Team Instructions as start-of-string", () => {
// with_team() also handles prompt=None → "[Team Instructions]\n{instructions}".
const framed = ["[Team Instructions]", "Instructions only."].join("\n");
const sections = parseSystemPromptSections(framed);
assert.deepEqual(sections, [
{ title: "Team Instructions", body: "Instructions only." },
]);
});
test("parseSystemPromptSections (modern) pins full 5-section shape: Base+System+Team+Core+Canvas", () => {
// Production shape from with_team() + with_core() + with_canvas(): all five sections present.
const framed = [
"[Base]",
"You are a helpful AI assistant running in Buzz.",
"",
"[System]",
"You are Observer Agent. You coordinate multi-agent workflows.",
"",
"[Team Instructions]",
"Always tag on handoff.",
"Never expand scope without approval.",
"",
"[Agent Memory — core]",
"I am Observer Agent.",
"## Lessons Learned",
"Always tag on handoff.",
"",
"[Channel Canvas]",
"Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"Last modified: 2026-07-11T10:00:00Z",
"Fetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301",
].join("\n");
const sections = parseSystemPromptSections(framed);
assert.deepEqual(sections, [
{ title: "Base", body: "You are a helpful AI assistant running in Buzz." },
{
title: "System",
body: "You are Observer Agent. You coordinate multi-agent workflows.",
},
{
title: "Team Instructions",
body: "Always tag on handoff.\nNever expand scope without approval.",
},
{
title: "Core Memory",
body: "I am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.",
},
{
title: "Channel Canvas",
body: "Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301",
},
]);
});
test("parseSystemPromptSections (modern) does NOT split on bracket [Team Instructions] preceded by only a single newline", () => {
// The inline marker is "\n\n[Team Instructions]\n" — a single preceding newline
// must be kept literal inside System, same guard as canvas/core.
const framed = [
"[System]",
"Persona preamble.",
"[Team Instructions]",
"This is persona text, not a real team block.",
].join("\n");
const sections = parseSystemPromptSections(framed);
assert.deepEqual(sections, [
{
title: "System",
body: "Persona preamble.\n[Team Instructions]\nThis is persona text, not a real team block.",
},
]);
});
@@ -61,15 +61,12 @@ export function parsePromptText(text: string): {
* deterministically.
*
* The harness composes the value in order:
* `[Base]\n{base}\n\n[System]\n{persona}\n\n[Agent Memory — core]\n{core}\n\n[Channel Canvas]\n{canvas}`
* with any section omitted when absent. For team-pack agents the persona body
* already contains the pack-level instructions appended by `compose_prompt()`
* in `buzz-persona/src/resolve.rs`:
* `{persona_body}\n\n---\n# Team Instructions\n{pack_instructions}`
* Extraction runs in reverse producer order so that each `lastIndexOf` search
* operates on the full input and each extraction boundary is unambiguous.
* `[Base]\n{base}\n\n[System]\n{persona}\n\n[Team Instructions]\n{team}\n\n[Agent Memory — core]\n{core}\n\n[Channel Canvas]\n{canvas}`
* with any section omitted when absent. Extraction runs in reverse producer
* order so that each `lastIndexOf` search operates on the full input and each
* extraction boundary is unambiguous.
*
* Four extraction passes before Base/System parsing:
* Five extraction passes:
*
* 1. **Canvas** (`[Channel Canvas]`): appended last by `with_canvas()`.
* - Start-of-string: canvas-only input.
@@ -80,18 +77,23 @@ export function parsePromptText(text: string): {
* 2. **Core** (`[Agent Memory — core]`): appended before canvas by `with_core()`.
* Same two cases, same last-occurrence guard.
*
* 3. **Base/System**: remainder after canvas and core extraction.
* 3. **Team Instructions** (`[Team Instructions]`): appended before core by
* `with_team()` in `buzz-acp/src/pool.rs`. Same two cases (start-of-string
* or `\n\n[Team Instructions]\n` inline), same last-occurrence guard. Output
* position: after System, before Core Memory.
*
* 4. **Base/System**: remainder after the three top-level section extractions.
* Split on the first `\n[System]\n` boundary; no embedded `[...]` line
* inside a body can start a new section.
*
* 4. **Team Instructions**: if the `System` body contains the exact canonical
* delimiter `\n\n---\n# Team Instructions\n` (produced by `compose_prompt()`),
* the body is split at the **last** occurrence of that boundary (same
* last-occurrence guard as canvas and core). The text before becomes the
* `System` body; the text after becomes a `Team Instructions` section
* inserted immediately after `System`. Non-canonical lookalikes (bare `---`
* without the heading, a `# Team Instructions` on a different line, or only
* a single preceding newline) are kept literal inside `System`.
* 5. **Legacy Team Instructions** (backward compat): if the `System` body
* contains the exact canonical delimiter `\n\n---\n# Team Instructions\n`
* (produced by the now-removed `compose_prompt()` in buzz-persona), the body
* is split at the **last** occurrence of that boundary. The text before
* becomes the `System` body; the text after becomes a `Team Instructions`
* section inserted immediately after `System`. Non-canonical lookalikes
* (bare `---` without the heading, a `# Team Instructions` on a different
* line, or only a single preceding newline) are kept literal inside `System`.
*/
export function parseSystemPromptSections(
systemPrompt: string,
@@ -133,7 +135,29 @@ export function parseSystemPromptSections(
}
}
// ── 3. Parse Base/System from the remaining prefix ────────────────────────
// ── 3. Extract [Team Instructions] (modern runtime framing) ─────────────
// with_team() in buzz-acp/src/pool.rs appends "\n\n[Team Instructions]\n{instructions}"
// after [System] and before core/canvas. Same two cases as canvas/core:
// start-of-string (team-only input) or the inline double-newline marker
// (last occurrence guards against embedded lookalikes preceded by a single \n).
const TEAM_HEADER = "[Team Instructions]";
const TEAM_MARKER_INLINE = `\n\n${TEAM_HEADER}\n`;
let modernTeamBody: string | null = null;
if (remainder.startsWith(`${TEAM_HEADER}\n`)) {
modernTeamBody = remainder.slice(`${TEAM_HEADER}\n`.length).trim();
remainder = "";
} else {
const lastTeam = remainder.lastIndexOf(TEAM_MARKER_INLINE);
if (lastTeam !== -1) {
modernTeamBody = remainder
.slice(lastTeam + TEAM_MARKER_INLINE.length)
.trim();
remainder = remainder.slice(0, lastTeam);
}
}
// ── 4. Parse Base/System from the remaining prefix ────────────────────────
// The canonical team-instructions delimiter produced by compose_prompt() in
// buzz-persona/src/resolve.rs:
// format!("{persona_prompt}\n\n---\n# Team Instructions\n{instructions}")
@@ -181,7 +205,9 @@ export function parseSystemPromptSections(
}
}
// ── 4. Append core and canvas sections in producer order ──────────────────
// ── 5. Append team (modern), core, and canvas sections in producer order ──
if (modernTeamBody)
sections.push({ title: "Team Instructions", body: modernTeamBody });
if (coreBody) sections.push({ title: "Core Memory", body: coreBody });
if (canvasBody) sections.push({ title: "Channel Canvas", body: canvasBody });
@@ -42,7 +42,9 @@ export function RuntimeErrorTooltip({
side="bottom"
sideOffset={12}
>
<span className="leading-4">{detail}</span>
<span className="block max-h-48 overflow-y-auto overflow-x-hidden break-words whitespace-pre-line leading-4">
{detail}
</span>
</TooltipContent>
</Tooltip>
);
@@ -463,19 +463,56 @@ function RuntimeAuthError({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
}
function RuntimeCard({
installError,
isInstalling,
onInstall,
installResults,
onInstallResultsChange,
runtime,
}: {
installError: string | null;
isInstalling: boolean;
onInstall: () => void;
installResults: InstallResultsState;
onInstallResultsChange: React.Dispatch<
React.SetStateAction<InstallResultsState>
>;
runtime: AcpRuntimeCatalogEntry;
}) {
// Each card owns its own mutation instance so concurrent installs on
// different cards each track their own isPending state and callbacks
// independently (react-query v5 per-mutate callbacks only fire for the
// latest mutate() call on a shared instance, silently dropping earlier ones).
const installMutation = useInstallAcpRuntimeMutation();
const installError = installResults[runtime.id]?.error ?? null;
const isInstalling = installMutation.isPending;
const isAvailable = runtime.availability === "available";
const isReady = runtimeIsReadyForOnboarding(runtime);
function handleInstall() {
onInstallResultsChange((current) => ({
...current,
[runtime.id]: { error: null, success: false },
}));
installMutation.mutate(runtime.id, {
onSuccess: (result) => {
onInstallResultsChange((current) => ({
...current,
[runtime.id]: result.success
? { error: null, success: true }
: {
error: getInstallErrorMessage(result.steps),
success: false,
},
}));
},
onError: (error) => {
onInstallResultsChange((current) => ({
...current,
[runtime.id]: {
error: error instanceof Error ? error.message : "Install failed.",
success: false,
},
}));
},
});
}
return (
<Card
className={cn(
@@ -499,7 +536,7 @@ function RuntimeCard({
<RuntimeStatus
installError={installError}
isInstalling={isInstalling}
onInstall={onInstall}
onInstall={handleInstall}
runtime={runtime}
/>
{!isAvailable && runtimeDetailText(runtime) ? (
@@ -517,7 +554,7 @@ function RuntimeCard({
{installError ? (
<RuntimeErrorTooltip
className="absolute inset-x-3 bottom-2 flex min-w-0 items-center justify-center gap-1.5 overflow-hidden whitespace-nowrap text-xs leading-4 text-destructive"
detail="Installation couldn’t be completed. Try again."
detail={installError}
label="Installation failed"
showIcon
testId={`onboarding-runtime-error-${runtime.id}`}
@@ -560,34 +597,6 @@ function RuntimeProvidersSection({
}) {
const { errorMessage, isChecking, items } = runtimeProviders;
const orderedItems = getVisibleOnboardingRuntimes(items);
const installMutation = useInstallAcpRuntimeMutation();
function handleInstall(runtimeId: string) {
onInstallResultsChange((current) => ({
...current,
[runtimeId]: { error: null, success: false },
}));
installMutation.mutate(runtimeId, {
onSuccess: (result) => {
onInstallResultsChange((current) => ({
...current,
[runtimeId]: result.success
? { error: null, success: true }
: { error: getInstallErrorMessage(result.steps), success: false },
}));
},
onError: (error) => {
onInstallResultsChange((current) => ({
...current,
[runtimeId]: {
error: error instanceof Error ? error.message : "Install failed.",
success: false,
},
}));
},
});
}
return (
<section className="flex min-h-full w-full flex-col items-center">
@@ -606,13 +615,9 @@ function RuntimeProvidersSection({
<div className="grid min-w-0 w-full max-w-[592px] grid-cols-1 gap-4 md:grid-cols-2">
{orderedItems.map((runtime) => (
<RuntimeCard
installError={installResults[runtime.id]?.error ?? null}
isInstalling={
installMutation.isPending &&
installMutation.variables === runtime.id
}
installResults={installResults}
key={runtime.id}
onInstall={() => handleInstall(runtime.id)}
onInstallResultsChange={onInstallResultsChange}
runtime={runtime}
/>
))}
@@ -279,22 +279,56 @@ function RuntimeHeader({
}
function RuntimeRow({
installError,
installSuccess,
isInstalling,
onInstall,
resetEpoch,
runtime,
}: {
installError: string | null;
installSuccess: boolean;
isInstalling: boolean;
onInstall: () => void;
resetEpoch: number;
runtime: AcpRuntimeCatalogEntry;
}) {
const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState<
string | null
>(null);
const [isUpdateWarningOpen, setIsUpdateWarningOpen] = React.useState(false);
// Each row owns its mutation instance so concurrent installs each track
// their own isPending / result state independently.
const installMutation = useInstallAcpRuntimeMutation();
const [installResult, setInstallResult] = React.useState<{
success: boolean;
error: string | null;
} | null>(null);
// Clear stale install results when the parent triggers a catalog refresh
// (Check again) — the runtime may now be healthy and stale failure state
// would linger because keyed rows don't remount on refetch.
// biome-ignore lint/correctness/useExhaustiveDependencies: resetEpoch is an intentional trigger only; its value is not consumed in the effect body
React.useEffect(() => {
setInstallResult(null);
}, [resetEpoch]);
const isInstalling = installMutation.isPending;
const installError = installResult?.error ?? null;
const installSuccess = installResult?.success ?? false;
function handleInstall() {
setInstallResult(null);
installMutation.mutate(runtime.id, {
onSuccess: (result) => {
if (result.success) {
setInstallResult({ success: true, error: null });
} else {
setInstallResult({
success: false,
error: getInstallErrorMessage(result.steps),
});
}
},
onError: (error) => {
setInstallResult({
success: false,
error: error instanceof Error ? error.message : "Install failed.",
});
},
});
}
const canConnectAccount =
runtime.availability === "available" &&
runtime.authStatus.status === "logged_out";
@@ -352,7 +386,7 @@ function RuntimeRow({
setIsUpdateWarningOpen(true);
return;
}
onInstall();
handleInstall();
}}
runtime={runtime}
/>
@@ -372,7 +406,10 @@ function RuntimeRow({
</p>
) : null}
{installError ? (
<p className="mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive">
<p
className="mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive"
data-testid={`doctor-runtime-install-error-${runtime.id}`}
>
{installError}
</p>
) : null}
@@ -410,7 +447,7 @@ function RuntimeRow({
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onInstall}
onClick={handleInstall}
data-testid={`doctor-runtime-confirm-update-${runtime.id}`}
>
Update
@@ -491,59 +528,9 @@ export function DoctorSettingsPanel() {
[runtimesQuery.data],
);
const isRefreshing = runtimesQuery.isFetching;
const installMutation = useInstallAcpRuntimeMutation();
const [installResults, setInstallResults] = React.useState<
Record<string, { success: boolean; error: string | null }>
>({});
// Per-runtime installing state: tracks which runtime IDs have an in-flight
// install so concurrent installs each show their own spinner correctly.
const [installingIds, setInstallingIds] = React.useState<Set<string>>(
new Set(),
);
function handleInstall(runtimeId: string) {
// Clear any previous result for this runtime before retrying.
setInstallResults((prev) => ({
...prev,
[runtimeId]: { success: false, error: null },
}));
setInstallingIds((prev) => new Set(prev).add(runtimeId));
installMutation.mutate(runtimeId, {
onSuccess: (result) => {
if (result.success) {
setInstallResults((prev) => ({
...prev,
[runtimeId]: { success: true, error: null },
}));
} else {
setInstallResults((prev) => ({
...prev,
[runtimeId]: {
success: false,
error: getInstallErrorMessage(result.steps),
},
}));
}
},
onError: (error) => {
setInstallResults((prev) => ({
...prev,
[runtimeId]: {
success: false,
error: error instanceof Error ? error.message : "Install failed.",
},
}));
},
onSettled: () => {
setInstallingIds((prev) => {
const next = new Set(prev);
next.delete(runtimeId);
return next;
});
},
});
}
// Incremented each time the user clicks "Check again" so RuntimeRow
// useEffect clears stale install results from before the refresh.
const [resetEpoch, setResetEpoch] = React.useState(0);
return (
<section
@@ -558,7 +545,7 @@ export function DoctorSettingsPanel() {
<Button
disabled={isRefreshing}
onClick={() => {
setInstallResults({});
setResetEpoch((e) => e + 1);
void runtimesQuery.refetch();
void gitBashQuery.refetch();
}}
@@ -598,11 +585,8 @@ export function DoctorSettingsPanel() {
<div className="space-y-3" data-testid="doctor-runtime-list">
{runtimes.map((runtime) => (
<RuntimeRow
installError={installResults[runtime.id]?.error ?? null}
installSuccess={installResults[runtime.id]?.success ?? false}
isInstalling={installingIds.has(runtime.id)}
key={runtime.id}
onInstall={() => handleInstall(runtime.id)}
resetEpoch={resetEpoch}
runtime={runtime}
/>
))}
@@ -67,3 +67,47 @@ test("getInstallErrorMessage: failed step with empty stderr falls back to stdout
]);
assert.match(message, /some stdout output/);
});
test("getInstallErrorMessage: hint and step detail are separated by double newline for whitespace-pre-line rendering", () => {
const hint = "Git Bash is required. Install it from git-scm.com.";
const message = getInstallErrorMessage([
{
step: "shell",
command: "bash -l -c 'npm install'",
success: false,
stdout: "",
stderr: "bash: command not found",
exitCode: 127,
hint,
},
]);
assert.ok(
message.includes("\n\n"),
"hint and step detail should be separated by a blank line",
);
assert.ok(message.startsWith(hint));
});
test("getInstallErrorMessage: only reports the last (failing) step when multiple steps present", () => {
const message = getInstallErrorMessage([
{
step: "node",
command: "node --version",
success: true,
stdout: "v20.0.0",
stderr: "",
exitCode: 0,
},
{
step: "adapter",
command: "npm install -g @agentclientprotocol/claude-code-acp",
success: false,
stdout: "",
stderr: "npm ERR! code E404",
exitCode: 1,
},
]);
assert.match(message, /Step "adapter" failed:/);
assert.match(message, /npm ERR! code E404/);
assert.doesNotMatch(message, /Step "node"/);
});
+39
View File
@@ -187,6 +187,18 @@ type E2eConfig = {
* Call N returns results[N]; when exhausted the last entry repeats.
* Takes precedence over `installAcpRuntimeResult`. */
installAcpRuntimeResults?: RawInstallRuntimeResult[];
/** Per-runtime install configuration keyed by runtimeId.
* When a runtimeId matches, its entry overrides the global
* installAcpRuntime* fields for that specific runtime. */
installAcpRuntimeByRuntime?: Record<
string,
{
delayMs?: number;
result?: RawInstallRuntimeResult;
/** Call-order sequence — same semantics as installAcpRuntimeResults. */
results?: RawInstallRuntimeResult[];
}
>;
managedAgentPrereqs?: {
acp?: MockCommandAvailability;
mcp?: MockCommandAvailability;
@@ -7075,6 +7087,8 @@ async function handleConnectAcpRuntime(
// Per-page install call counter. Reset each test run because this module is
// re-evaluated via addInitScript, so the counter starts at 0 for every test.
let installCallCount = 0;
/** Per-runtime call counters for `installAcpRuntimeByRuntime` sequences. */
const installCallCountByRuntime: Record<string, number> = {};
let addChannelMembersCallCount = 0;
let mockGlobalAgentConfig: {
env_vars: Record<string, string>;
@@ -7095,6 +7109,31 @@ async function handleInstallAcpRuntime(
},
config: E2eConfig | undefined,
): Promise<RawInstallRuntimeResult> {
const runtimeId = args.runtimeId ?? "";
const perRuntime = config?.mock?.installAcpRuntimeByRuntime?.[runtimeId];
if (perRuntime) {
const delayMs = perRuntime.delayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const seq = perRuntime.results;
if (seq && seq.length > 0) {
const idx = Math.min(
installCallCountByRuntime[runtimeId] ?? 0,
seq.length - 1,
);
installCallCountByRuntime[runtimeId] = idx + 1;
const result = seq[idx];
if (result.success) mockInstallCompleted = true;
return result;
}
if (perRuntime.result) {
if (perRuntime.result.success) mockInstallCompleted = true;
return perRuntime.result;
}
}
const delayMs = config?.mock?.installAcpRuntimeDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));