@maxiaochao/pi-toolkit 0.11.1 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 2026-10-12 - v0.11.4
2
+
3
+ - bundle herdr-subagents v0.4.13: fix stale-idle child startup race and require real Herdr E2E release smoke
4
+
5
+ ## 2026-10-12 - v0.11.3
6
+
7
+ - harden coordinated publishing and make local release checks authoritative
8
+
9
+ ## Unreleased
10
+
11
+ - Fix Herdr Subagents startup failures caused by stale idle observations and require real child-Agent smoke tests before releasing that package.
12
+
13
+ - Replace split child/root publishing with one idempotent root-tag workflow, make final-version local checks authoritative, keep CI publication-only, and remove timing-sensitive Herdr lifecycle assertions.
14
+
15
+ ## 2026-10-12 - v0.11.2
16
+
17
+ - bundle herdr-subagents v0.4.12: enable codemode for child agents while preserving persona tool boundaries
18
+ - Enable `codemode` for bundled Herdr Subagents children while preserving persona tool boundaries.
19
+
1
20
  ## 2026-10-12 - v0.11.1
2
21
 
3
22
  - bundle herdr-subagents v0.4.11: add architecture documentation and clear-output guidance evaluation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.11.1",
3
+ "version": "0.11.4",
4
4
  "description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,3 +1,15 @@
1
+ ## 2026-10-12 - v0.4.13
2
+
3
+ - fix stale-idle child startup race and require real Herdr E2E release smoke
4
+ - Make lifecycle tests accept both valid report-first and observation-first execution orderings.
5
+ - Fix child startup being mistaken for completion when `before_agent_start` records the attempt as working before Herdr leaves its stale idle state.
6
+ - Add mandatory real-Herdr single-Agent and concurrent batch smoke tests to the Herdr Subagents release gate.
7
+
8
+ ## 2026-10-12 - v0.4.12
9
+
10
+ - enable codemode for child agents while preserving persona tool boundaries
11
+ - Enable `codemode` for every child Pi while preserving each persona's existing tool allowlist and read/write boundary.
12
+
1
13
  ## 2026-10-12 - v0.4.11
2
14
 
3
15
  - add architecture documentation and clear-output guidance evaluation
@@ -172,6 +172,8 @@ Each `personas.<name>` object supports exactly these overrides:
172
172
 
173
173
  JSON persona overrides cannot change `name`, `description`, `access`, `tools`, or the system prompt; define those in persona Markdown.
174
174
 
175
+ Every child Pi explicitly loads and enables `codemode`. Built-in personas include it in their tool allowlists. Custom personas may omit `tools` and inherit Pi's defaults plus `codemode`, or list `codemode` explicitly alongside their allowed tools. A read-only persona remains read-only because codemode can call only the tools enabled for that child session.
176
+
175
177
  A global configuration can establish shared defaults:
176
178
 
177
179
  ```json
@@ -4,9 +4,10 @@
4
4
 
5
5
  1. `npm --prefix packages/herdr-subagents test` runs deterministic scheduler, recovery, and persona tests.
6
6
  2. `pi -ne -ns --no-session -e ./packages/herdr-subagents/extensions/herdr-subagents.ts` verifies extension loading.
7
- 3. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
8
- 4. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
9
- 5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
7
+ 3. From a Pi session inside Herdr, run `npm --prefix packages/herdr-subagents run test:real-herdr`. This is a mandatory child-package and root-bundle release gate. It starts one real child Pi and then two concurrent read-only child Pi Agents, requires assistant events and matching attempt-scoped reports, and verifies final tab cleanup. `HERDR_SUBAGENTS_E2E_MODEL=provider/model` optionally pins the smoke model. Failed cleanup preserves the temporary durable records and fails the gate.
8
+ 4. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
9
+ 5. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
10
+ 6. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
10
11
 
11
12
  ## Cases
12
13
 
@@ -45,4 +46,4 @@
45
46
 
46
47
  ## Latest baseline
47
48
 
48
- All 246 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, lifecycle, attempt-isolation, and persona cases plus the extension-contract lifecycle simulations must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed a read-only package inspection with matching durable attempt/report IDs and automatic tab cleanup.
49
+ All 246 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, lifecycle, attempt-isolation, and persona cases plus the extension-contract lifecycle simulations must pass. The startup regression test reproduces the real ordering where `before_agent_start` records the attempt as working while Herdr can still report stale idle. Every release also runs the real single-Agent and concurrent read-only Herdr smoke; a skipped smoke is not a passing release check.
@@ -2,7 +2,7 @@
2
2
  name: analyst
3
3
  description: Neutral read-only analyst for dynamically assigned roles
4
4
  access: read
5
- tools: read, bash, grep, find, ls
5
+ tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
8
  Act as an independent analyst and follow the role and evaluation method in the task prompt. Do not modify project files. Separate direct evidence, inference, and unknowns, and report concrete evidence with useful paths.
@@ -2,7 +2,7 @@
2
2
  name: explorer
3
3
  description: Read-only explorer for fast codebase investigation
4
4
  access: read
5
- tools: read, bash, grep, find, ls
5
+ tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
8
  Investigate the assigned question without modifying files. Report concrete evidence and useful paths.
@@ -2,7 +2,7 @@
2
2
  name: reviewer
3
3
  description: Read-only reviewer for correctness and maintainability risks
4
4
  access: read
5
- tools: read, bash, grep, find, ls
5
+ tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
8
  Review the assigned work without modifying files. Report actionable findings with concrete evidence.
@@ -2,7 +2,7 @@
2
2
  name: worker
3
3
  description: General-purpose worker for one focused task
4
4
  access: write
5
- tools: read, bash, edit, write, grep, find, ls
5
+ tools: read, bash, edit, write, grep, find, ls, codemode
6
6
  ---
7
7
 
8
8
  You are a focused worker subagent.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "node --experimental-strip-types tests/run-tests.mjs",
41
+ "test:real-herdr": "node --experimental-strip-types tests/real-herdr-smoke.mjs",
41
42
  "load-test": "env -u PI_HERDR_SUBAGENTS_CHILD pi -ne -ns --no-session -e ./tests/extension-contract.ts && env -u PI_HERDR_SUBAGENTS_CHILD pi -ne -ns --no-session -e ./extensions/herdr-subagents.ts"
42
43
  },
43
44
  "publishConfig": {
@@ -36,13 +36,15 @@ export function childPiArgs(task: LiveTaskRecord, systemPromptFile: string): str
36
36
  "--session", task.sessionFile,
37
37
  "--name", `Subagent: ${task.name ?? task.agent ?? "worker"}`,
38
38
  "--no-extensions",
39
+ "--extension", "builtin:codemode",
39
40
  "--extension", fileURLToPath(new URL("../extensions/subagent-report.ts", import.meta.url)),
40
41
  "--no-skills",
41
42
  "--append-system-prompt", systemPromptFile,
42
43
  ];
43
44
  if (task.model) args.push("--model", task.model);
44
45
  if (task.thinking) args.push("--thinking", task.thinking);
45
- if (task.tools?.length) args.push("--tools", [...new Set([...task.tools, "subagent_report"])].join(","));
46
+ if (task.tools?.length) args.push("--tools", [...new Set([...task.tools, "codemode", "subagent_report"])].join(","));
47
+ else args.push("--tools", "+codemode");
46
48
  for (const skill of task.skills ?? []) args.push("--skill", skill);
47
49
  return args;
48
50
  }
@@ -132,7 +134,6 @@ export async function promptLiveAgent(options: {
132
134
  });
133
135
  const stalledTimer = options.stalledWarningMs ? setTimeout(() => options.onStalled?.(), options.stalledWarningMs) : undefined;
134
136
  let terminalObservedAt: number | undefined;
135
- let turnObserved = false;
136
137
  try {
137
138
  await persistRuntime("working");
138
139
  while (!existsSync(attemptReportFile)) {
@@ -142,7 +143,6 @@ export async function promptLiveAgent(options: {
142
143
  try { agentState = await options.herdr.getAgentState(agentName, options.signal); }
143
144
  catch (error) { observationDiagnostic = `Could not observe Herdr agent state: ${error instanceof Error ? error.message : String(error)}`; }
144
145
  const attemptState = await readAttemptState();
145
- if (agentState === "working" || agentState === "blocked" || attemptState === "working") turnObserved = true;
146
146
  const promptDetail = promptError instanceof Error ? promptError.message : promptError === undefined ? "" : String(promptError);
147
147
  if (agentState === "blocked" || /agent_blocked/i.test(promptDetail)) {
148
148
  await persistRuntime("waiting_interaction", observationDiagnostic ?? "Herdr is waiting for an approval or interaction; no business input request has been reported.");
@@ -158,7 +158,7 @@ export async function promptLiveAgent(options: {
158
158
  } else if (promptError && promptSettled) {
159
159
  terminalObservedAt ??= Date.now();
160
160
  if (Date.now() - terminalObservedAt >= 50) throw promptError;
161
- } else if ((promptSettled || turnObserved || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
161
+ } else if ((promptSettled || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
162
162
  if (attemptState === "working") {
163
163
  let live = false;
164
164
  try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
@@ -10,6 +10,7 @@ export const BUILTIN_TOOLS = new Set([
10
10
  "find",
11
11
  "ls",
12
12
  "powershell",
13
+ "codemode",
13
14
  ]);
14
15
 
15
16
  export interface AgentDefinition {
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { executeHerdrSubagents } from "../src/tool.ts";
7
+ import { CliHerdrAutomation } from "../src/herdr.ts";
8
+
9
+ if (process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) {
10
+ throw new Error("The real Herdr smoke test must run from a Pi session inside Herdr.");
11
+ }
12
+
13
+ const packageDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
14
+ const root = mkdtempSync(join(tmpdir(), "pi-herdr-subagents-real-"));
15
+ const herdr = new CliHerdrAutomation();
16
+ const tabIds = new Set();
17
+ const model = process.env.HERDR_SUBAGENTS_E2E_MODEL?.trim();
18
+ const configurationPaths = {
19
+ globalConfig: join(root, "global-config.json"),
20
+ projectConfig: join(root, "project-config.json"),
21
+ globalAgents: join(root, "global-agents"),
22
+ projectAgents: join(root, "project-agents"),
23
+ };
24
+ const timeout = AbortSignal.timeout(120_000);
25
+
26
+ function hasAssistantMessage(path) {
27
+ return readFileSync(path, "utf8").split("\n").filter(Boolean).some((line) => {
28
+ try { return JSON.parse(line)?.message?.role === "assistant"; }
29
+ catch { return false; }
30
+ });
31
+ }
32
+
33
+ async function runCase(label, names, concurrency) {
34
+ const response = await executeHerdrSubagents({
35
+ label,
36
+ concurrency,
37
+ tasks: names.map((name) => ({
38
+ name,
39
+ agent: "analyst",
40
+ ...(model ? { model } : {}),
41
+ prompt: `Do not inspect files. Call subagent_report with status completed, summary '${name} completed', and result 'hello'.`,
42
+ })),
43
+ }, timeout, (update) => {
44
+ if (update.details?.tabId) tabIds.add(update.details.tabId);
45
+ }, { cwd: root }, {
46
+ agentsDirectory: join(packageDirectory, "agents"),
47
+ configurationPaths,
48
+ herdr,
49
+ });
50
+
51
+ const result = response.details;
52
+ if (result?.tabId) tabIds.add(result.tabId);
53
+ assert.equal(response.isError, undefined, response.content?.[0]?.text);
54
+ assert.equal(result.status, "completed");
55
+ assert.equal(result.effectiveConcurrency, concurrency);
56
+ assert.equal(result.tasks.length, names.length);
57
+ assert.equal(new Set(result.tasks.map((task) => task.paneId)).size, names.length);
58
+
59
+ for (const taskResult of result.tasks) {
60
+ const task = JSON.parse(readFileSync(join(taskResult.recordDirectory, "task.json"), "utf8"));
61
+ const report = JSON.parse(readFileSync(join(taskResult.recordDirectory, "report.json"), "utf8"));
62
+ assert.equal(task.status, "completed");
63
+ assert.equal(report.status, "completed");
64
+ assert.equal(report.attemptId, task.currentAttemptId);
65
+ assert.equal(hasAssistantMessage(task.sessionFile), true, `${taskResult.name} produced no assistant event`);
66
+ assert.equal(await herdr.paneExists(task.paneId), false, `${taskResult.name} pane survived tab cleanup`);
67
+ }
68
+ console.log(`PASS ${label}: ${names.length} real child Agent${names.length === 1 ? "" : "s"} completed and cleaned up`);
69
+ }
70
+
71
+ try {
72
+ await runCase("real-single", ["hello-single"], 1);
73
+ await runCase("real-concurrent", ["hello-one", "hello-two"], 2);
74
+ } finally {
75
+ for (const tabId of tabIds) await herdr.closeTab(tabId).catch(() => undefined);
76
+ rmSync(root, { recursive: true, force: true });
77
+ }
@@ -24,6 +24,7 @@ import { RunPaneAllocator } from "../src/run-pane-allocator.ts";
24
24
  import { aggregateTaskStatus } from "../src/run-status.ts";
25
25
  import { isAbortError } from "../src/errors.ts";
26
26
  import { acquireDirectoryLock, withRunStateLock } from "../src/run-state.ts";
27
+ import { childPiArgs } from "../src/live-agent.ts";
27
28
  import registerSubagentReport from "../extensions/subagent-report.ts";
28
29
 
29
30
  /** Drives one task through the batch runner and projects its single result like a task-scoped run. */
@@ -457,13 +458,13 @@ try {
457
458
  herdr,
458
459
  });
459
460
  check("compact successful report", success.ok && success.status === "completed" && success.summary === "Authentication review completed." && success.documents.length === 1 && !success.output.includes("FULL RESULT"));
460
- check("Herdr lifecycle", herdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab");
461
+ check("Herdr lifecycle", herdr.calls.map(([name]) => name).filter((name) => name !== "getAgentState").join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab");
461
462
  const createRequest = herdr.calls.find(([name]) => name === "createTab")[1];
462
463
  const startRequest = herdr.calls.find(([name]) => name === "startAgent")[1];
463
464
  check("named unfocused tab in parent workspace", createRequest.workspaceId === "w1" && createRequest.cwd === root && createRequest.label === "SA · auth-review" && createRequest.focus === false);
464
465
  check("ordinary execution never steals pane focus", !herdr.calls.some(([name]) => name === "focusPane"));
465
466
  check("child environment points at durable task and parent pane", createRequest.env.PI_HERDR_SUBAGENTS_CHILD === "1" && createRequest.env.PI_HERDR_SUBAGENTS_TASK_DIR.includes(join(".pi", "herdr-subagents", "runs")) && createRequest.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID === "w1:p-parent");
466
- check("persistent isolated child Pi", startRequest.kind === "pi" && startRequest.paneId === "w1:p2" && startRequest.args.includes("--session") && startRequest.args.includes("--no-extensions") && startRequest.args.includes("--extension") && startRequest.args.includes("--no-skills") && startRequest.args.includes("--model") && startRequest.args.includes("fake/model") && startRequest.args.includes("--tools") && startRequest.args.includes("read,grep,subagent_report"));
467
+ check("persistent isolated child Pi", startRequest.kind === "pi" && startRequest.paneId === "w1:p2" && startRequest.args.includes("--session") && startRequest.args.includes("--no-extensions") && startRequest.args.includes("builtin:codemode") && startRequest.args.includes("--extension") && startRequest.args.includes("--no-skills") && startRequest.args.includes("--model") && startRequest.args.includes("fake/model") && startRequest.args.includes("--tools") && startRequest.args.includes("read,grep,codemode,subagent_report"));
467
468
  const promptRequest = herdr.calls.find(([name]) => name === "promptAgent")[1];
468
469
  check("prompt submitted through Herdr", promptRequest.prompt === "Review authentication" && promptRequest.target === startRequest.name);
469
470
 
@@ -480,7 +481,7 @@ try {
480
481
  model: { provider: "fake", id: "model" },
481
482
  thinkingLevel: "medium",
482
483
  }, { agentsDirectory: join(process.cwd(), "agents"), herdr: extensionHerdr });
483
- check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
484
+ check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).filter((name) => name !== "getAgentState").join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
484
485
  let singleStalledWarning = "";
485
486
  await runSingleTask("single slow task", { cwd: root, herdr: fakeHerdr(), stalledWarningSeconds: 0.001, onStalled: (message) => { singleStalledWarning = message; } });
486
487
  check("single-task execution emits configured stalled warning", singleStalledWarning.includes("appears stalled"));
@@ -494,8 +495,18 @@ try {
494
495
  check("missing report is visible failure", !silent.ok && silent.errorMessage?.includes("did not submit") && silentHerdr.calls.at(-1)[0] === "closeTab");
495
496
  const initialIdleHerdr = fakeHerdr();
496
497
  const initialIdlePrompt = initialIdleHerdr.promptAgent.bind(initialIdleHerdr);
498
+ const initialIdleStart = initialIdleHerdr.startAgent.bind(initialIdleHerdr);
499
+ let initialIdleDirectory;
500
+ initialIdleHerdr.startAgent = async (request) => {
501
+ const sessionIndex = request.args.indexOf("--session");
502
+ initialIdleDirectory = join(request.args[sessionIndex + 1], "..");
503
+ return initialIdleStart(request);
504
+ };
497
505
  let initialIdleAccepted = false;
498
506
  initialIdleHerdr.promptAgent = async (request) => {
507
+ const task = JSON.parse(readFileSync(join(initialIdleDirectory, "task.json"), "utf8"));
508
+ mkdirSync(join(initialIdleDirectory, "attempts"), { recursive: true });
509
+ writeFileSync(join(initialIdleDirectory, "attempts", `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
499
510
  await new Promise((resolve) => setTimeout(resolve, 150));
500
511
  initialIdleAccepted = true;
501
512
  return initialIdlePrompt(request);
@@ -570,6 +581,10 @@ try {
570
581
 
571
582
  const definition = parseAgentMarkdown(`---\nname: reviewer\ndescription: Review code\nmodel: fake/model\ntools: read, grep\n---\nReview carefully.`, "reviewer.md");
572
583
  check("persona frontmatter parsing", definition.name === "reviewer" && definition.model === "fake/model" && definition.tools?.join(",") === "read,grep" && definition.systemPrompt === "Review carefully.");
584
+ const inheritedToolArgs = childPiArgs({ id: "inherited", sessionFile: "/tmp/inherited.jsonl" }, "/tmp/system.md");
585
+ check("child Pi enables codemode when a persona inherits default tools", inheritedToolArgs.includes("builtin:codemode") && inheritedToolArgs[inheritedToolArgs.indexOf("--tools") + 1] === "+codemode");
586
+ const explicitToolArgs = childPiArgs({ id: "explicit", sessionFile: "/tmp/explicit.jsonl", tools: ["read"] }, "/tmp/system.md");
587
+ check("child Pi enables codemode with an explicit tool allowlist", explicitToolArgs.includes("builtin:codemode") && explicitToolArgs.includes("read,codemode,subagent_report"));
573
588
  const agentsDir = join(root, "agents");
574
589
  await import("node:fs/promises").then(({ mkdir }) => mkdir(agentsDir));
575
590
  writeFileSync(join(agentsDir, "worker.md"), `---\nname: worker\ndescription: Worker\n---\nDo work.`);
@@ -578,7 +593,7 @@ try {
578
593
  check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
579
594
  const packageDiscovery = discoverPackageAgents(join(process.cwd(), "agents"));
580
595
  const analystPersona = packageDiscovery.agents.find((persona) => persona.name === "analyst");
581
- check("built-in analyst is neutral and read-only", packageDiscovery.diagnostics.length === 0 && analystPersona?.access === "read" && analystPersona.model === undefined && analystPersona.tools?.join(",") === "read,bash,grep,find,ls" && /independent analyst/.test(analystPersona.systemPrompt) && /direct evidence, inference, and unknowns/.test(analystPersona.systemPrompt) && !/auditor|replicator|adversary/.test(analystPersona.systemPrompt));
596
+ check("built-in analyst is neutral and read-only", packageDiscovery.diagnostics.length === 0 && analystPersona?.access === "read" && analystPersona.model === undefined && analystPersona.tools?.join(",") === "read,bash,grep,find,ls,codemode" && /independent analyst/.test(analystPersona.systemPrompt) && /direct evidence, inference, and unknowns/.test(analystPersona.systemPrompt) && !/auditor|replicator|adversary/.test(analystPersona.systemPrompt));
582
597
 
583
598
  const configRoot = join(root, "configuration");
584
599
  const packageAgents = join(configRoot, "package-agents");