@maxiaochao/pi-toolkit 0.11.0 → 0.11.3

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,20 @@
1
+ ## 2026-10-12 - v0.11.3
2
+
3
+ - harden coordinated publishing and make local release checks authoritative
4
+
5
+ ## Unreleased
6
+
7
+ - 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.
8
+
9
+ ## 2026-10-12 - v0.11.2
10
+
11
+ - bundle herdr-subagents v0.4.12: enable codemode for child agents while preserving persona tool boundaries
12
+ - Enable `codemode` for bundled Herdr Subagents children while preserving persona tool boundaries.
13
+
14
+ ## 2026-10-12 - v0.11.1
15
+
16
+ - bundle herdr-subagents v0.4.11: add architecture documentation and clear-output guidance evaluation
17
+
1
18
  ## 2026-10-12 - v0.11.0
2
19
 
3
20
  - bundle worktree v0.4.0: Replace CLI start and prepare with the create-only command
@@ -0,0 +1,19 @@
1
+ # AGENTS.md Clear Output Evaluation
2
+
3
+ This A/B evaluation measures the effect of the `Clear Output` guidance. The `before` variant uses a minimal coding-assistant prompt. The `after` variant adds only the two `Clear Output` rules.
4
+
5
+ Run the deterministic scorer tests:
6
+
7
+ ```bash
8
+ npm run eval:agents:test
9
+ ```
10
+
11
+ Run the model evaluation:
12
+
13
+ ```bash
14
+ npm run eval:agents -- --runs 2
15
+ ```
16
+
17
+ Set `PI_EVAL_MODEL` or pass `--model <provider/model>` to pin a model. Results are written to `results/latest.json` and `results/latest.md`.
18
+
19
+ The scorer measures sentence length, conjunction density, response language, terminology changes, and use of a visual for the branching-flow case. These are proxies. Review the saved outputs for correctness and completeness before accepting a prompt change.
@@ -0,0 +1,217 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const CLEAR_OUTPUT = `## Clear Output
6
+
7
+ - Make outputs easy to understand and verify. Use ASD-STE100-inspired principles: short sentences, one idea per sentence, and consistent terms. Apply its formal language rules only to English.
8
+ - When it improves understanding, use a diagram, HTML artifact, or video instead of long prose.`;
9
+
10
+ const BASE_SYSTEM = "You are an expert coding assistant. Give a direct and accurate answer. Reply in the same language as the user.";
11
+
12
+ export const variants = {
13
+ before: BASE_SYSTEM,
14
+ after: `${BASE_SYSTEM}\n\n${CLEAR_OUTPUT}`,
15
+ };
16
+
17
+ export const cases = [
18
+ {
19
+ id: "english-explanation",
20
+ language: "en",
21
+ prompt: "Explain how optimistic concurrency control prevents lost updates. Include the read, write, conflict, and retry stages. Write for an application developer.",
22
+ },
23
+ {
24
+ id: "chinese-explanation",
25
+ language: "zh",
26
+ prompt: "向应用开发者解释乐观并发控制如何防止更新丢失。需要涵盖读取、写入、冲突和重试阶段。",
27
+ },
28
+ {
29
+ id: "term-consistency",
30
+ language: "en",
31
+ aliases: ["work item", "job", "task"],
32
+ prompt: "A queue API uses job, task, and work item for the same object. Explain its lifecycle from creation through processing to completion. Resolve the naming ambiguity in your answer.",
33
+ },
34
+ {
35
+ id: "visual-flow",
36
+ language: "en",
37
+ expectVisual: true,
38
+ prompt: "Explain this request flow: gateway to authentication; failed authentication to rejection; successful authentication to cache; cache miss to database; transient database failure retries twice, then returns an error. Make the branching and retry path easy to verify.",
39
+ },
40
+ ];
41
+
42
+ function splitSentences(text) {
43
+ return [...new Intl.Segmenter(undefined, { granularity: "sentence" }).segment(text)]
44
+ .map(({ segment }) => segment.trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ function tokenCount(sentence, language) {
49
+ if (language === "zh") {
50
+ return (sentence.match(/[\p{Script=Han}]|[A-Za-z0-9]+/gu) ?? []).length;
51
+ }
52
+ return (sentence.match(/[\p{L}\p{N}]+(?:[-'][\p{L}\p{N}]+)*/gu) ?? []).length;
53
+ }
54
+
55
+ export function scoreOutput(text, testCase) {
56
+ const sentences = splitSentences(text);
57
+ const lengths = sentences.map((sentence) => tokenCount(sentence, testCase.language));
58
+ const limit = testCase.language === "zh" ? 35 : 25;
59
+ const conjunctionPattern = testCase.language === "zh"
60
+ ? /(?:而且|但是|同时|以及|并且|然而|其中)/g
61
+ : /\b(?:and|but|while|whereas|which|although|however)\b/gi;
62
+ const conjunctions = sentences.reduce(
63
+ (sum, sentence) => sum + (sentence.match(conjunctionPattern) ?? []).length,
64
+ 0,
65
+ );
66
+ const firstParagraphEnd = text.indexOf("\n\n");
67
+ const bodyAfterNaming = firstParagraphEnd >= 0
68
+ ? text.slice(firstParagraphEnd + 2)
69
+ : sentences.slice(1).join(" ");
70
+ const usedAliasesAfterNaming = (testCase.aliases ?? []).filter((alias) =>
71
+ new RegExp(`\\b${alias.replaceAll(" ", "\\s+")}s?\\b`, "i").test(bodyAfterNaming),
72
+ );
73
+ const hasVisual = /```(?:mermaid)?|(?:-->|->|→|↓|├|└)|<svg\b|<html\b|\|[^\n]+\|[^\n]+\|/i.test(text);
74
+ const hanCharacters = (text.match(/[\p{Script=Han}]/gu) ?? []).length;
75
+ const latinWords = (text.match(/[A-Za-z]+/g) ?? []).length;
76
+ const languageMatches = testCase.language === "zh"
77
+ ? hanCharacters >= 20
78
+ : hanCharacters <= Math.max(5, latinWords * 0.05);
79
+
80
+ return {
81
+ characters: text.length,
82
+ sentences: sentences.length,
83
+ averageSentenceTokens: lengths.length
84
+ ? Number((lengths.reduce((sum, value) => sum + value, 0) / lengths.length).toFixed(1))
85
+ : 0,
86
+ longSentenceRate: lengths.length
87
+ ? Number((lengths.filter((value) => value > limit).length / lengths.length).toFixed(2))
88
+ : 0,
89
+ conjunctionsPerSentence: lengths.length
90
+ ? Number((conjunctions / lengths.length).toFixed(2))
91
+ : 0,
92
+ extraAliasesAfterNaming: Math.max(0, usedAliasesAfterNaming.length - 1),
93
+ languageMatches,
94
+ hasVisual,
95
+ };
96
+ }
97
+
98
+ function parseArguments(argv) {
99
+ const options = { runs: 1, model: process.env.PI_EVAL_MODEL };
100
+ for (let index = 0; index < argv.length; index += 1) {
101
+ if (argv[index] === "--runs") options.runs = Number(argv[++index]);
102
+ else if (argv[index] === "--model") options.model = argv[++index];
103
+ else throw new Error(`Unknown argument: ${argv[index]}`);
104
+ }
105
+ if (!Number.isInteger(options.runs) || options.runs < 1) {
106
+ throw new Error("--runs must be a positive integer");
107
+ }
108
+ return options;
109
+ }
110
+
111
+ function runPi(systemPrompt, prompt, model) {
112
+ const args = [
113
+ "--print",
114
+ "--no-session",
115
+ "--no-tools",
116
+ "--no-extensions",
117
+ "--no-skills",
118
+ "--no-prompt-templates",
119
+ "--no-themes",
120
+ "--no-context-files",
121
+ "--system-prompt",
122
+ systemPrompt,
123
+ ];
124
+ if (model) args.push("--model", model);
125
+ args.push("--", prompt);
126
+ return execFileSync("pi", args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }).trim();
127
+ }
128
+
129
+ function average(rows, key) {
130
+ if (rows.length === 0) return 0;
131
+ return Number((rows.reduce((sum, row) => sum + Number(row.score[key]), 0) / rows.length).toFixed(2));
132
+ }
133
+
134
+ export function summarize(results) {
135
+ return Object.fromEntries(Object.keys(variants).map((variant) => {
136
+ const rows = results.filter((result) => result.variant === variant);
137
+ const visualRows = rows.filter((result) => cases.find(({ id }) => id === result.caseId).expectVisual);
138
+ const terminologyRows = rows.filter((result) => cases.find(({ id }) => id === result.caseId).aliases);
139
+ return [variant, {
140
+ averageCharacters: average(rows, "characters"),
141
+ averageSentenceTokens: average(rows, "averageSentenceTokens"),
142
+ longSentenceRate: average(rows, "longSentenceRate"),
143
+ conjunctionsPerSentence: average(rows, "conjunctionsPerSentence"),
144
+ languageMatchRate: average(rows, "languageMatches"),
145
+ visualFlowRate: average(visualRows, "hasVisual"),
146
+ extraAliasesAfterNaming: average(terminologyRows, "extraAliasesAfterNaming"),
147
+ }];
148
+ }));
149
+ }
150
+
151
+ export function markdownReport(payload) {
152
+ const { summary } = payload;
153
+ const lines = [
154
+ "# AGENTS.md Clear Output A/B Evaluation",
155
+ "",
156
+ `Runs per case: ${payload.options.runs}`,
157
+ `Model override: ${payload.options.model ?? "Pi default"}`,
158
+ "",
159
+ "| Variant | Mean characters | Mean sentence tokens | Long sentence rate | Conjunctions / sentence | Language match | Visual-flow rate | Extra aliases |",
160
+ "|---|---:|---:|---:|---:|---:|---:|---:|",
161
+ ...Object.entries(summary).map(([name, score]) =>
162
+ `| ${name} | ${score.averageCharacters} | ${score.averageSentenceTokens} | ${score.longSentenceRate} | ${score.conjunctionsPerSentence} | ${score.languageMatchRate} | ${score.visualFlowRate} | ${score.extraAliasesAfterNaming} |`,
163
+ ),
164
+ "",
165
+ "Lower is better for long sentence rate, conjunctions, and extra aliases after naming. Higher is better for language match and visual-flow rate. Character count is descriptive, not a quality score.",
166
+ "",
167
+ "## Per-case output",
168
+ "",
169
+ ...payload.results.flatMap((result) => [
170
+ `### ${result.caseId} · ${result.variant} · run ${result.run}`,
171
+ "",
172
+ `Score: \`${JSON.stringify(result.score)}\``,
173
+ "",
174
+ result.output,
175
+ "",
176
+ ]),
177
+ ];
178
+ return `${lines.join("\n")}\n`;
179
+ }
180
+
181
+ async function main() {
182
+ const options = parseArguments(process.argv.slice(2));
183
+ const results = [];
184
+ for (let run = 1; run <= options.runs; run += 1) {
185
+ for (const testCase of cases) {
186
+ const order = run % 2 === 1 ? ["before", "after"] : ["after", "before"];
187
+ for (const variant of order) {
188
+ process.stderr.write(`Running ${testCase.id} / ${variant} / ${run}\n`);
189
+ const output = runPi(variants[variant], testCase.prompt, options.model);
190
+ results.push({
191
+ caseId: testCase.id,
192
+ variant,
193
+ run,
194
+ output,
195
+ score: scoreOutput(output, testCase),
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ const payload = {
202
+ generatedAt: new Date().toISOString(),
203
+ options,
204
+ summary: summarize(results),
205
+ results,
206
+ };
207
+ const resultDirectory = fileURLToPath(new URL("./results", import.meta.url));
208
+ mkdirSync(resultDirectory, { recursive: true });
209
+ writeFileSync(`${resultDirectory}/latest.json`, `${JSON.stringify(payload, null, 2)}\n`);
210
+ writeFileSync(`${resultDirectory}/latest.md`, markdownReport(payload));
211
+ console.log(JSON.stringify(payload.summary, null, 2));
212
+ console.log(`\nFull report: ${resultDirectory}/latest.md`);
213
+ }
214
+
215
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
216
+ await main();
217
+ }
@@ -0,0 +1,52 @@
1
+ import assert from "node:assert/strict";
2
+ import { scoreOutput } from "../run.mjs";
3
+
4
+ const concise = scoreOutput(
5
+ "Read the record. Compare its version. Reject a conflict. Retry the operation.",
6
+ { language: "en" },
7
+ );
8
+ assert.equal(concise.sentences, 4);
9
+ assert.equal(concise.longSentenceRate, 0);
10
+ assert.equal(concise.conjunctionsPerSentence, 0);
11
+ assert.equal(concise.languageMatches, true);
12
+
13
+ const dense = scoreOutput(
14
+ "Read the record and compare its version, but reject the write when another client changed it, while preserving the new value and returning a conflict which the caller can retry after loading the latest record.",
15
+ { language: "en" },
16
+ );
17
+ assert.equal(dense.sentences, 1);
18
+ assert.equal(dense.longSentenceRate, 1);
19
+ assert.ok(dense.conjunctionsPerSentence >= 4);
20
+
21
+ const chinese = scoreOutput(
22
+ "客户端读取记录。客户端比较版本。版本冲突时,服务拒绝写入。客户端重新读取后重试。",
23
+ { language: "zh" },
24
+ );
25
+ assert.equal(chinese.sentences, 4);
26
+ assert.equal(chinese.longSentenceRate, 0);
27
+ assert.equal(chinese.languageMatches, true);
28
+
29
+ const terms = scoreOutput(
30
+ "Create one work item. The worker processes the job and completes the task.",
31
+ { language: "en", aliases: ["work item", "job", "task"] },
32
+ );
33
+ assert.equal(terms.extraAliasesAfterNaming, 1);
34
+
35
+ const canonicalTerm = scoreOutput(
36
+ "Use work item as the canonical term for job, task, and work item. Create the work item. Process the work item. Complete the work item.",
37
+ { language: "en", aliases: ["work item", "job", "task"] },
38
+ );
39
+ assert.equal(canonicalTerm.extraAliasesAfterNaming, 0);
40
+
41
+ const wrongLanguage = scoreOutput("这是一个中文回答,包含足够多的中文字符,因此不应匹配英文问题。", {
42
+ language: "en",
43
+ });
44
+ assert.equal(wrongLanguage.languageMatches, false);
45
+
46
+ const visual = scoreOutput("gateway --> auth --> cache\n ↓\n reject", {
47
+ language: "en",
48
+ expectVisual: true,
49
+ });
50
+ assert.equal(visual.hasVisual, true);
51
+
52
+ console.log("AGENTS.md evaluation scorer tests passed");
package/global/AGENTS.md CHANGED
@@ -2,13 +2,18 @@
2
2
 
3
3
  Behavioral guidelines to reduce common LLM coding mistakes. Project-specific instructions override this file when they conflict.
4
4
 
5
- ## 1. Think Before Coding
5
+ ## 1. Clear Output
6
+
7
+ - Make outputs easy to understand and verify. Use ASD-STE100-inspired principles: short sentences, one idea per sentence, and consistent terms. Apply its formal language rules only to English.
8
+ - When it improves understanding, use a diagram, HTML artifact, or video instead of long prose.
9
+
10
+ ## 2. Think Before Coding
6
11
 
7
12
  - Surface material assumptions, interpretations, and tradeoffs.
8
13
  - If unresolved ambiguity would materially change the result, ask. When no human is reachable, choose a defensible interpretation, state it, and proceed with verifiable criteria.
9
14
  - Prefer the simpler approach. Name conflicting requirements rather than silently compromising between them.
10
15
 
11
- ## 2. Minimal Solution
16
+ ## 3. Minimal Solution
12
17
 
13
18
  Understand the problem and relevant code flow before choosing the first rung that works:
14
19
 
@@ -24,11 +29,11 @@ Understand the problem and relevant code flow before choosing the first rung tha
24
29
  - For states excluded by enforced invariants, fail visibly instead of inventing fallback behavior.
25
30
  - State the ceiling and upgrade trigger of any deliberate simplification that cuts a real corner.
26
31
 
27
- ## 3. Skill Boundaries
32
+ ## 4. Skill Boundaries
28
33
 
29
34
  After loading a skill, treat its documented interface as sufficient for normal use. Do not inspect the skill's implementation, installed executable, or package source unless the user asks to review, debug, or modify it; its documented behavior conflicts with observed behavior; or required usage details are missing from the skill.
30
35
 
31
- ## 4. Read, Then Change Surgically
36
+ ## 5. Read, Then Change Surgically
32
37
 
33
38
  Before editing, read the affected public API and its immediate callers. For a bug, find every statically discoverable caller and relevant implementation.
34
39
 
@@ -38,23 +43,23 @@ Every changed line traces to the user's request or to a defect that request reve
38
43
  - Remove imports, variables, and functions that your changes make unused. Leave pre-existing dead code in place and mention it.
39
44
  - Fix a faulty shared function once and name affected sibling callers; if only one caller is wrong, fix that caller instead of guarding a correct shared function.
40
45
 
41
- ## 5. Goal-Driven Execution
46
+ ## 6. Goal-Driven Execution
42
47
 
43
48
  - Turn work into checkable outcomes: reproduce bugs before fixing them, preserve behavior during refactors, and optimize against a tested reference implementation.
44
49
  - For multi-step or materially ambiguous work, state a brief plan and give every step its own verification.
45
50
  - Leave the smallest relevant runnable check for new or changed non-trivial logic. Do not introduce a test framework solely for that check.
46
51
 
47
- ## 6. Fail Visibly
52
+ ## 7. Fail Visibly
48
53
 
49
54
  - "Completed" is wrong if anything required was silently skipped.
50
55
  - "Tests pass" is wrong if any required test was skipped or not run.
51
56
  - If a required input, file, or tool is missing, or a step is impossible under the stated constraints, report what is missing and what you need. Never fabricate it to make a check pass.
52
57
 
53
- ## 7. Frontend Verification
58
+ ## 8. Frontend Verification
54
59
 
55
60
  For frontend feature development, load the `web-browser` skill and verify the result in a real Chrome browser through CDP. Exercise the relevant user flow with real clicks, capture screenshots of the resulting UI, and test the actual behavior before reporting completion. Static inspection or non-browser tests do not replace this verification.
56
61
 
57
- ## 8. Worktree Isolation
62
+ ## 9. Worktree Isolation
58
63
 
59
64
  For a Git-repository task that coordinates substantial changes across multiple files or modules, or implements a feature intended for release, ask before editing whether to isolate it in a worktree. Skip the question when already in a linked worktree.
60
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.11.0",
3
+ "version": "0.11.3",
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": [
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "files": [
14
14
  "extensions",
15
+ "evaluations/agents-md",
15
16
  "packages/cache-export",
16
17
  "packages/codex-edit",
17
18
  "packages/herdr-subagents",
@@ -0,0 +1,407 @@
1
+ # Herdr Subagents architecture and decision-pause design
2
+
3
+ This document describes the package's current architecture, the problems exposed by failure and blocking semantics, and the target decision-pause model. Sections marked **Current** describe implemented behavior. Sections marked **Target** are an implementation plan, not the current contract.
4
+
5
+ ## 1. Product boundary
6
+
7
+ Herdr Subagents runs ordinary interactive Pi Agents in visible Herdr panes and keeps their complete sessions and results in project-local durable records. The parent receives compact status, questions, summaries, and document paths.
8
+
9
+ A batch is a resource-bounded collection of **independent tasks**:
10
+
11
+ - array order controls FIFO scheduling, pane numbering, and result order;
12
+ - `concurrency` controls how many tasks may be active at once;
13
+ - a batch containing a write-capable persona is forced to concurrency one;
14
+ - concurrency one does **not** express `A -> B` dependency semantics;
15
+ - tasks that consume another task's result must be orchestrated by the parent as separate runs.
16
+
17
+ The package does not currently need a DAG scheduler. If dependency workflows become a first-class product requirement, they should be introduced explicitly with dependency and skip-propagation semantics instead of inferred from array order.
18
+
19
+ ## 2. Architecture overview
20
+
21
+ ```text
22
+ Parent Pi tool call
23
+ |
24
+ v
25
+ Extension lifecycle and foreground handoff
26
+ | extensions/herdr-subagents.ts
27
+ v
28
+ Global run dispatcher ---- persisted background queue
29
+ | src/dispatcher.ts, src/queued-runs.ts
30
+ v
31
+ Batch runner and bounded workers
32
+ | src/batch-runner.ts
33
+ +---- pane allocation / Herdr Agent lifecycle
34
+ | src/run-pane-allocator.ts, src/live-agent.ts, src/herdr.ts
35
+ +---- attempt-scoped structured report
36
+ | src/attempts.ts, src/protocol.ts, src/records.ts
37
+ +---- durable run/task/session records
38
+ | .pi/herdr-subagents/runs/<run>/...
39
+ v
40
+ Aggregate status, monitor, control, recovery and cleanup
41
+ src/run-status.ts, src/status.ts, src/fleet*.ts,
42
+ src/blocking.ts, src/historical-resume.ts,
43
+ src/reconcile.ts, src/tab-cleanup.ts
44
+ ```
45
+
46
+ ### Responsibilities
47
+
48
+ | Layer | Responsibility |
49
+ | --- | --- |
50
+ | Extension | Register tools, bind parent-session lifetime, return foreground results, send final background follow-ups. |
51
+ | Dispatcher | Give one run ownership of the global submission lane and queue later submissions FIFO. |
52
+ | Batch runner | Persist the run, start bounded workers, allocate one pane per task, and aggregate task results. |
53
+ | Live Agent adapter | Start or continue an interactive Pi Agent and distinguish runtime telemetry from business reports. |
54
+ | Records | Atomically persist run, task, turn, report, attempt, ownership, and resume state. |
55
+ | Control | Inspect, answer, retry/continue saved work, cancel, resume history, and clean archives. |
56
+ | Recovery | Reconcile stale owners and processes, settle abandoned work, and clean identifiable tabs. |
57
+ | Fleet | Project durable business state plus transient runtime telemetry into the parent UI. |
58
+
59
+ ## 3. Authoritative state and invariants
60
+
61
+ 1. A matching attempt-scoped `subagent_report` is authoritative for business `completed`, `blocked`, or `failed`.
62
+ 2. Herdr `working`, `blocked`, `idle`, and `done` are transport/runtime telemetry. Herdr UI `blocked` projects to `waiting_interaction`; it does not make the business task blocked.
63
+ 3. Each task owns one pane and one Pi session. A pane is never reused by another task.
64
+ 4. A task session has at most one writer. Task resume locks enforce that invariant.
65
+ 5. Shared `run.json` changes occur under the short-lived run-state lock. An Agent turn never holds that lock.
66
+ 6. Every new prompt has a unique attempt ID. A late report from an older attempt cannot settle a newer attempt.
67
+ 7. Task and report state is persisted before it is exposed as an actionable parent result.
68
+ 8. Running work is not forcibly interrupted merely because a sibling needs a decision.
69
+
70
+ ## 4. Current execution flow
71
+
72
+ ### Submission
73
+
74
+ 1. Tool parameters are validated.
75
+ 2. Configuration and personas are loaded and resolved.
76
+ 3. A stable run ID is allocated.
77
+ 4. Background runs are durably queued before dispatch; foreground runs wait for the dispatcher lane.
78
+ 5. The batch runner creates `run.json`, task directories, task records, prompts, and session paths.
79
+ 6. Effective concurrency is bounded by settings and forced to one if any persona is write-capable.
80
+
81
+ ### Task execution
82
+
83
+ Each worker repeatedly claims the next FIFO task, allocates a pane, starts an interactive Pi Agent, waits for the matching structured report, persists the result, and claims another task. A failed task does not currently stop that worker. A blocked task stops only its worker lane.
84
+
85
+ ### Current foreground outcomes
86
+
87
+ | Event | Current foreground behavior | Queued siblings |
88
+ | --- | --- | --- |
89
+ | All tasks complete | Return after the whole batch settles. | Completed normally. |
90
+ | Failure with concurrency one | Continue later tasks, then return terminal `failed` or `partial`. | Continue automatically. |
91
+ | Failure with concurrency greater than one and unfinished siblings | Persist the failure, return early, detach the batch, and let it finish in the background. | Continue automatically. |
92
+ | Business block | Wait for the other worker lanes to settle, then return the question and retain the dispatcher lane. | A blocked worker stops, but other workers keep claiming tasks. |
93
+ | Herdr UI block | Keep waiting for an authoritative report while showing `waiting_interaction`. | Unchanged. |
94
+
95
+ The failure and block rows are asymmetric: parallel failure gives control back promptly, while block may withhold its question until unrelated work finishes. Serial failure also assumes later tasks are independent. The target design removes both ambiguities.
96
+
97
+ ## 5. Dispatcher ownership in plain language
98
+
99
+ The dispatcher is the global submission lane, not the task-concurrency limiter.
100
+
101
+ | Operation | Meaning |
102
+ | --- | --- |
103
+ | `detach` | The run continues in the background but gives up the submission lane, so another run may start. |
104
+ | `retain` | The run is waiting for a parent decision and keeps the lane; a new foreground run is rejected and a background run stays queued. |
105
+ | `release` | A retained run is resolved or cancelled and gives up the lane. |
106
+
107
+ Current parallel failure uses `detach`; current business block eventually uses `retain`.
108
+
109
+ ## 6. Target decision-pause model
110
+
111
+ ### Goal
112
+
113
+ Any actionable business problem returns control to the parent immediately and stops new task dispatch until the parent decides what to do. Already-running siblings are allowed to finish safely.
114
+
115
+ ```text
116
+ A reports failed or blocked
117
+ |
118
+ +---- persist task/report/attempt state
119
+ +---- pause new queue claims
120
+ +---- return pause details to the parent immediately
121
+ +---- retain the dispatcher lane
122
+ |
123
+ B and C were already running ---- continue to a terminal report
124
+ D and E were queued ------------- remain queued
125
+ ```
126
+
127
+ "Pause" means pausing **dispatch**, not freezing an Agent process at an arbitrary instruction. Forcibly suspending or cancelling running siblings could leave tools, file writes, or external operations in an unknown state.
128
+
129
+ ### Run control state must be separate from task outcome
130
+
131
+ The current `RunStatus` reuses task statuses and aggregates them into `running`, `blocked`, `partial`, and terminal values. That combines two different questions:
132
+
133
+ 1. May the scheduler dispatch more work?
134
+ 2. What outcomes have tasks produced?
135
+
136
+ The target model separates them conceptually:
137
+
138
+ ```ts
139
+ type RunPhase = "queued" | "running" | "paused" | "cancelling" | "settled";
140
+ type RunOutcome = "completed" | "partial" | "failed" | "cancelled";
141
+ ```
142
+
143
+ The public status projection may continue exposing `status: "paused"` for convenience, but durable control logic should use the phase. While a run is paused, some task activities may still be `running` as already-started siblings settle.
144
+
145
+ ### Durable pause reasons
146
+
147
+ A pause is tied to a specific task attempt so stale answers cannot resolve newer work:
148
+
149
+ ```ts
150
+ interface PauseReason {
151
+ taskId: string;
152
+ taskOrder: number;
153
+ attemptId: string;
154
+ kind: "blocked" | "failed";
155
+ resolution: "pending" | "responded" | "retried" | "continued" | "cancelled";
156
+ createdAt: string;
157
+ resolvedAt?: string;
158
+ }
159
+ ```
160
+
161
+ The durable reason should reference authoritative task/report records instead of duplicating mutable summary, question, and error text. Status and tool responses project a detailed view:
162
+
163
+ ```json
164
+ {
165
+ "status": "paused",
166
+ "pauseReasons": [
167
+ {
168
+ "task": 1,
169
+ "kind": "failed",
170
+ "summary": "Authentication review failed.",
171
+ "error": "Provider request failed.",
172
+ "allowedActions": ["retry", "continue", "cancel"]
173
+ }
174
+ ]
175
+ }
176
+ ```
177
+
178
+ Multiple already-running siblings may subsequently fail or block. Their reasons accumulate. No queued task starts while any pending reason remains.
179
+
180
+ ### Decision actions
181
+
182
+ | Reason | Action | Effect |
183
+ | --- | --- | --- |
184
+ | `blocked` | `respond` | Continue the same live/saved session with the answer. |
185
+ | `failed` | `retry` | Retry or continue the selected task explicitly; never infer a task. |
186
+ | `failed` | `continue` | Accept that task's failure and allow the run to proceed without retrying it. |
187
+ | Any | `cancel` | Cancel running and queued work and settle the run as cancelled. |
188
+
189
+ `respond` should no longer be overloaded for failed tasks once `retry` exists. This makes control intent and audit history explicit.
190
+
191
+ The queue resumes only when every pending pause reason is resolved. A retry or response that blocks/fails again creates a reason for the new attempt.
192
+
193
+ ### Failure classification
194
+
195
+ Both child-reported failures and generated terminal failures can pause a run:
196
+
197
+ - valid `subagent_report(status="failed")`;
198
+ - child process or prompt API terminal error without a valid report;
199
+ - Agent settled without publishing a report after the grace period;
200
+ - pane or Agent startup failure.
201
+
202
+ Ambiguous prompt delivery remains `delivery_unknown` while the child is alive and must not be converted into a retryable failure. Retry safety should be projected explicitly when the implementation can prove whether a prompt was submitted; the system must never automatically replay an attempt with uncertain side effects.
203
+
204
+ ### Foreground handoff
205
+
206
+ Failure and block should share one foreground-handoff mechanism:
207
+
208
+ 1. observe an actionable, durably persisted task attempt;
209
+ 2. atomically pause dispatch and register its reason;
210
+ 3. detach the parent tool-call abort signal without cancelling the run;
211
+ 4. retain dispatcher ownership;
212
+ 5. return the projected pause details immediately;
213
+ 6. keep already-running siblings and final background lifecycle tracking alive;
214
+ 7. send at most one terminal follow-up after the run eventually settles.
215
+
216
+ The event-specific policy only determines allowed decisions and response rendering. It should not duplicate lifecycle, persistence, or notification machinery.
217
+
218
+ ## 7. Target flows
219
+
220
+ ### Independent parallel tasks
221
+
222
+ ```text
223
+ Submit A, B, C, D with concurrency 3
224
+ A/B/C start; D queued
225
+ A fails
226
+ -> run phase paused
227
+ -> D remains queued
228
+ -> parent receives A failure immediately
229
+ B/C continue safely
230
+ Parent retries A, continues past A, or cancels
231
+ -> once all pending reasons are resolved, dispatch may resume
232
+ ```
233
+
234
+ ### Serial tasks
235
+
236
+ ```text
237
+ Submit A, B, C with concurrency 1
238
+ A starts; B/C queued
239
+ A fails or blocks
240
+ -> run phase paused
241
+ -> B/C do not start
242
+ -> parent decides
243
+ ```
244
+
245
+ This makes serial execution safer, but still does not make B consume A's result. A real `A -> B` workflow remains parent-orchestrated as separate runs.
246
+
247
+ ### Runtime-only Herdr block
248
+
249
+ ```text
250
+ Herdr reports blocked
251
+ -> task remains business running
252
+ -> Fleet shows waiting_interaction
253
+ -> no decision pause yet
254
+ Child publishes matching structured blocked report
255
+ -> create pause reason
256
+ -> return exact question
257
+ ```
258
+
259
+ ## 8. Configuration: keep fewer concepts, not necessarily fewer files
260
+
261
+ ### Current user-facing sources
262
+
263
+ | Scope | Runtime settings and persona overrides | Persona definitions and prompts |
264
+ | --- | --- | --- |
265
+ | Project | `.pi/herdr-subagents.json` | `.pi/herdr-subagents/agents/*.md` |
266
+ | Global | `~/.pi/agent/herdr-subagents.json` | `~/.pi/agent/herdr-subagents/agents/*.md` |
267
+ | Package | Built-in defaults | Built-in `agents/*.md` |
268
+
269
+ The two formats have distinct jobs:
270
+
271
+ - JSON is suitable for environment policy and small overrides.
272
+ - Markdown frontmatter plus body is suitable for a named persona and its system prompt.
273
+
274
+ Merging them into one physical file would either put long prompts into JSON or require runtime policy to be parsed from Markdown. It would also force users to copy an entire built-in persona merely to override its model. Therefore the two physical forms should remain.
275
+
276
+ ### Sources of confusion to remove
277
+
278
+ 1. Persona Markdown and JSON both expose `model`, `thinking`, and `skills`; this is intentional override layering but must be described as one resolution pipeline.
279
+ 2. Call-level task model overrides are resolved later in `tool.ts`, after configuration loading.
280
+ 3. TypeBox validates shape while `validation.ts` validates action-dependent requirements; these are complementary but currently look like duplicated validation.
281
+ 4. Downstream modules receive partially resolved values instead of one canonical run plan.
282
+
283
+ ### Canonical normalization boundary
284
+
285
+ All sources should be normalized once before persistence or dispatch:
286
+
287
+ ```text
288
+ Built-in defaults
289
+ <- global JSON
290
+ <- project JSON
291
+ <- effective persona Markdown
292
+ <- global persona override
293
+ <- project persona override
294
+ <- call/task override
295
+ |
296
+ v
297
+ ResolvedRunPlan + diagnostics + source metadata
298
+ ```
299
+
300
+ A target canonical plan can contain only execution-ready values:
301
+
302
+ ```ts
303
+ interface ResolvedRunPlan {
304
+ label: string;
305
+ requestedConcurrency: number;
306
+ effectiveConcurrency: number;
307
+ tasks: Array<{
308
+ name: string;
309
+ prompt: string;
310
+ persona: string;
311
+ access: "read" | "write";
312
+ model?: string;
313
+ thinking?: string;
314
+ tools?: string[];
315
+ skills: string[];
316
+ systemPrompt: string;
317
+ }>;
318
+ provenance: Record<string, string>;
319
+ diagnostics: string[];
320
+ }
321
+ ```
322
+
323
+ After this boundary, the batch runner, persistence, recovery, and UI should not know about global/project/package precedence.
324
+
325
+ ### Configuration reductions
326
+
327
+ Do:
328
+
329
+ - retain one JSON settings file per scope and one persona directory per scope;
330
+ - centralize precedence and call overrides in a single `resolveRunPlan` step;
331
+ - expose resolved values and provenance through `list/settings` diagnostics;
332
+ - state prominently that batch tasks are independent;
333
+ - make decision pause the safe fixed behavior rather than adding a new failure-policy option;
334
+ - keep runtime-only Herdr states out of business configuration.
335
+
336
+ Do not add now:
337
+
338
+ - DAG or `dependsOn` configuration;
339
+ - configurable `failurePolicy` variants;
340
+ - separate block/failure handoff settings;
341
+ - automatic retry counts;
342
+ - configuration for dispatcher `detach`/`retain` behavior.
343
+
344
+ These options would multiply state-machine branches before a demonstrated need. The safe decision gate should be the invariant.
345
+
346
+ ### Validation ownership
347
+
348
+ - `parameters.ts`: public schema, field types, basic bounds, and model-visible descriptions.
349
+ - `validation.ts`: cross-field and action-specific requirements.
350
+ - `resolveRunPlan`: persona existence, precedence, capability constraints, effective concurrency, and canonical values.
351
+ - runtime state machine: whether an action is legal for the selected run/task/attempt.
352
+
353
+ This division avoids both duplicated validation and an oversized configuration loader.
354
+
355
+ ## 9. Implementation seams for the target model
356
+
357
+ | Area | Required change |
358
+ | --- | --- |
359
+ | `records.ts` | Separate run phase/outcome, persist attempt-scoped pause reasons, and preserve backward-compatible reads. |
360
+ | `run-status.ts` | Aggregate task outcomes independently from scheduler phase. |
361
+ | `batch-runner.ts` | Add a shared dispatch gate; stop queue claims immediately when a reason is registered. |
362
+ | `extensions/herdr-subagents.ts` | Replace failure-only early return with shared actionable-event handoff. |
363
+ | `dispatcher.ts` | Retain paused runs and release only after resolution/cancellation; keep ownership transitions explicit. |
364
+ | `blocking.ts` / control | Split blocked `respond` from failed `retry`; add explicit `continue`. |
365
+ | `continue-run.ts` | Resume queued dispatch only when no pending reason remains. |
366
+ | `reconcile.ts` | Recover paused runs and cancel them when their owning parent session disappears. |
367
+ | `status.ts` / Fleet | Show pause reasons, allowed actions, and still-running sibling activity together. |
368
+ | `parameters.ts` / `validation.ts` | Add `retry` and `continue`, clarify independent-task semantics. |
369
+ | tests | Cover immediate handoff, dispatch pause, multiple reasons, resolution races, restart recovery, and exactly-once final notification. |
370
+
371
+ ## 10. Required tests
372
+
373
+ 1. Serial failure pauses before the next queued task starts.
374
+ 2. Serial block pauses before the next queued task starts.
375
+ 3. Parallel failure returns immediately while already-running siblings continue.
376
+ 4. Parallel block returns immediately while already-running siblings continue.
377
+ 5. Queued siblings never start while a reason is pending.
378
+ 6. A second already-running sibling can add another pause reason without losing the first.
379
+ 7. Resolving only one of multiple reasons does not resume dispatch.
380
+ 8. `continue` preserves the failed task result and eventually produces `partial` when other tasks succeed.
381
+ 9. Retry/response is scoped to task and attempt; stale resolution is rejected.
382
+ 10. Herdr UI `blocked` alone never pauses business dispatch.
383
+ 11. `delivery_unknown` never causes automatic replay.
384
+ 12. A paused run retains the dispatcher lane; foreground submissions fail fast and background submissions remain queued.
385
+ 13. Cancellation settles running/queued tasks and releases the retained lane.
386
+ 14. Parent-session shutdown and startup reconciliation do not leave ownerless paused work.
387
+ 15. Foreground handoff and final follow-up are each delivered at most once.
388
+
389
+ ## 11. Non-goals
390
+
391
+ - Inferring dependencies from task order or prompt text.
392
+ - Freezing arbitrary live Agent processes.
393
+ - Automatically retrying failed or ambiguously delivered prompts.
394
+ - Treating Herdr UI state as business truth.
395
+ - Loading complete child results into parent context.
396
+ - Adding policy configuration for every state transition.
397
+
398
+ ## 12. Summary
399
+
400
+ The architecture becomes clearer when it separates four concerns:
401
+
402
+ 1. **Task outcome:** what each child authoritatively reported.
403
+ 2. **Run phase:** whether new work may be dispatched or a parent decision is required.
404
+ 3. **Runtime telemetry:** what Herdr currently observes without changing business truth.
405
+ 4. **Dispatcher ownership:** whether another run may enter the global submission lane.
406
+
407
+ The immediate next design step is a shared decision-pause and foreground-handoff mechanism for business failure and block. Configuration should be simplified through one canonical resolution boundary and fewer policy choices, not by forcing settings and persona prompts into one file.
@@ -1,3 +1,16 @@
1
+ ## Unreleased
2
+
3
+ - Make lifecycle tests accept both valid report-first and observation-first execution orderings.
4
+
5
+ ## 2026-10-12 - v0.4.12
6
+
7
+ - enable codemode for child agents while preserving persona tool boundaries
8
+ - Enable `codemode` for every child Pi while preserving each persona's existing tool allowlist and read/write boundary.
9
+
10
+ ## 2026-10-12 - v0.4.11
11
+
12
+ - add architecture documentation and clear-output guidance evaluation
13
+
1
14
  ## 2026-10-12 - v0.4.10
2
15
 
3
16
  - Stabilize concurrency checks and cancellation synchronization on slower CI runners.
@@ -32,6 +32,8 @@ Inside a Herdr workspace, the package registers `herdr_subagents`, `herdr_subage
32
32
 
33
33
  The design replaces hidden print-mode subprocesses with ordinary interactive Pi Agents managed through Herdr, so delegated work stays visible, inspectable, resumable, and durably recorded.
34
34
 
35
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for the current execution architecture, the proposed decision-pause state machine, and configuration-normalization boundaries.
36
+
35
37
  ## Usage
36
38
 
37
39
  Discover the effective personas first:
@@ -170,6 +172,8 @@ Each `personas.<name>` object supports exactly these overrides:
170
172
 
171
173
  JSON persona overrides cannot change `name`, `description`, `access`, `tools`, or the system prompt; define those in persona Markdown.
172
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
+
173
177
  A global configuration can establish shared defaults:
174
178
 
175
179
  ```json
@@ -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.10",
3
+ "version": "0.4.12",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -15,6 +15,7 @@
15
15
  "tests",
16
16
  "docs",
17
17
  "README.md",
18
+ "ARCHITECTURE.md",
18
19
  "TESTING.md",
19
20
  "CHANGELOG.md",
20
21
  "package.json"
@@ -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
  }
@@ -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 {
@@ -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"));
@@ -570,6 +571,10 @@ try {
570
571
 
571
572
  const definition = parseAgentMarkdown(`---\nname: reviewer\ndescription: Review code\nmodel: fake/model\ntools: read, grep\n---\nReview carefully.`, "reviewer.md");
572
573
  check("persona frontmatter parsing", definition.name === "reviewer" && definition.model === "fake/model" && definition.tools?.join(",") === "read,grep" && definition.systemPrompt === "Review carefully.");
574
+ const inheritedToolArgs = childPiArgs({ id: "inherited", sessionFile: "/tmp/inherited.jsonl" }, "/tmp/system.md");
575
+ check("child Pi enables codemode when a persona inherits default tools", inheritedToolArgs.includes("builtin:codemode") && inheritedToolArgs[inheritedToolArgs.indexOf("--tools") + 1] === "+codemode");
576
+ const explicitToolArgs = childPiArgs({ id: "explicit", sessionFile: "/tmp/explicit.jsonl", tools: ["read"] }, "/tmp/system.md");
577
+ check("child Pi enables codemode with an explicit tool allowlist", explicitToolArgs.includes("builtin:codemode") && explicitToolArgs.includes("read,codemode,subagent_report"));
573
578
  const agentsDir = join(root, "agents");
574
579
  await import("node:fs/promises").then(({ mkdir }) => mkdir(agentsDir));
575
580
  writeFileSync(join(agentsDir, "worker.md"), `---\nname: worker\ndescription: Worker\n---\nDo work.`);
@@ -578,7 +583,7 @@ try {
578
583
  check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
579
584
  const packageDiscovery = discoverPackageAgents(join(process.cwd(), "agents"));
580
585
  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));
586
+ 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
587
 
583
588
  const configRoot = join(root, "configuration");
584
589
  const packageAgents = join(configRoot, "package-agents");