peaks-loop 4.0.35 → 4.0.36

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
  3. package/dist/cli/commands/code-runtime-commands.js +57 -2
  4. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  5. package/dist/cli/commands/core/doctor-command.js +44 -2
  6. package/dist/cli/commands/core/memory-command.js +5 -1
  7. package/dist/cli/commands/dispatch-commands.js +15 -3
  8. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  9. package/dist/cli/commands/memory-commands.d.ts +24 -0
  10. package/dist/cli/commands/memory-commands.js +77 -10
  11. package/dist/cli/commands/request-commands.d.ts +8 -0
  12. package/dist/cli/commands/request-commands.js +23 -2
  13. package/dist/cli/commands/sub-agent-commands.js +2 -0
  14. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  15. package/dist/cli/commands/wave-plan-commands.js +93 -0
  16. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  17. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  18. package/dist/services/context/context-audit.d.ts +100 -0
  19. package/dist/services/context/context-audit.js +322 -0
  20. package/dist/services/context/summary-view.d.ts +54 -0
  21. package/dist/services/context/summary-view.js +114 -0
  22. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  23. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  24. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  25. package/dist/services/dispatch/session-capsule.js +56 -0
  26. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  27. package/dist/services/dispatch/slice-dag.js +9 -1
  28. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  29. package/dist/services/dispatch/test-tool-detection.js +14 -13
  30. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  31. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  32. package/dist/services/ide/ide-types.d.ts +15 -0
  33. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
  34. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
  35. package/package.json +5 -5
  36. package/skills/bee/peaks-qa/SKILL.md +2 -0
  37. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  38. package/skills/bee/peaks-rd/SKILL.md +2 -0
  39. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  40. package/skills/bee/peaks-txt/SKILL.md +2 -0
  41. package/skills/bee/peaks-ui/SKILL.md +2 -0
  42. package/skills/peaks-code/SKILL.md +8 -0
  43. package/skills/peaks-code/references/context-governance.md +29 -0
  44. package/skills/peaks-doctor/SKILL.md +2 -0
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { getErrorMessage, ok, fail } from 'peaks-loop-shared/result';
3
+ import { addJsonOption, printResult } from '../cli-helpers.js';
4
+ import { planFileOverlapWaves } from '../../services/dispatch/file-overlap-wave-planner.js';
5
+ export function registerWavePlanCommand(parent, io) {
6
+ addJsonOption(parent
7
+ .command('wave-plan')
8
+ .description('§3 file-overlap-aware scheduling: read slice descriptors ' +
9
+ '({slices:[{id,files:[]}]}) and emit a wave plan where every wave is ' +
10
+ 'pairwise file-disjoint. Overlapping slices are deferred to later ' +
11
+ 'waves with the colliding file named. Machine-readable envelope; the ' +
12
+ 'LLM runs this, users never type it.')
13
+ .option('--slices <file>', 'path to a JSON file: { "slices": [{ "id": "s1", "files": ["src/a.ts"] }] }')
14
+ .option('--slices-json <json>', 'inline JSON with the same shape as --slices')).action((options) => {
15
+ const asJson = options.json === true;
16
+ const source = typeof options.slicesJson === 'string' && options.slicesJson.length > 0
17
+ ? { text: options.slicesJson }
18
+ : typeof options.slices === 'string' && options.slices.length > 0
19
+ ? readSlicesFile(options.slices)
20
+ : null;
21
+ if (source === null) {
22
+ printResult(io, fail('sub-agent.wave-plan', 'MISSING_INPUT', 'pass --slices <file> or --slices-json <json>', { ok: false, waves: [] }, ['Provide slice descriptors as { "slices": [{ "id": "s1", "files": ["src/a.ts"] }] }.']), asJson);
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ if (source.error !== undefined) {
27
+ printResult(io, fail('sub-agent.wave-plan', 'INVALID_INPUT', source.error, { ok: false, waves: [] }, ['Check the JSON shape: { "slices": [{ "id": string, "files": string[] }] }.']), asJson);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(source.text);
34
+ }
35
+ catch (err) {
36
+ printResult(io, fail('sub-agent.wave-plan', 'INVALID_JSON', `input is not valid JSON: ${getErrorMessage(err)}`, { ok: false, waves: [] }, ['Fix the JSON syntax and re-run.']), asJson);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ const descriptors = coerceDescriptors(parsed);
41
+ if (descriptors === null) {
42
+ printResult(io, fail('sub-agent.wave-plan', 'INVALID_SHAPE', 'expected { "slices": [{ "id": string, "files": string[] }] }', { ok: false, waves: [] }, ['Each entry needs a non-empty string id and an array of file paths.']), asJson);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const plan = planFileOverlapWaves(descriptors);
47
+ const warnings = plan.duplicateIds.length > 0
48
+ ? [`DUPLICATE_SLICE_IDS: ${plan.duplicateIds.join(', ')} (first descriptor wins; the rest were not scheduled)`]
49
+ : [];
50
+ printResult(io, ok('sub-agent.wave-plan', {
51
+ envelopeVersion: '2.1.0',
52
+ ok: true,
53
+ sliceCount: plan.sliceCount,
54
+ waveCount: plan.waves.length,
55
+ waves: plan.waves,
56
+ duplicateIds: plan.duplicateIds,
57
+ maxParallelism: plan.waves.reduce((max, w) => Math.max(max, w.slices.length), 0)
58
+ }, warnings, [
59
+ plan.waves.length <= 1
60
+ ? 'All slices are file-disjoint: dispatch them in a single wave.'
61
+ : `Dispatch wave 0 first, then each later wave after its predecessors finish; the deferred[] entries name the blocking file.`
62
+ ]), asJson);
63
+ });
64
+ }
65
+ function readSlicesFile(path) {
66
+ try {
67
+ return { text: readFileSync(path, 'utf8') };
68
+ }
69
+ catch (err) {
70
+ return { text: '', error: `cannot read --slices file ${path}: ${getErrorMessage(err)}` };
71
+ }
72
+ }
73
+ function coerceDescriptors(parsed) {
74
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
75
+ return null;
76
+ const raw = parsed.slices;
77
+ if (!Array.isArray(raw))
78
+ return null;
79
+ const out = [];
80
+ for (const item of raw) {
81
+ if (item === null || typeof item !== 'object' || Array.isArray(item))
82
+ return null;
83
+ const id = item.id;
84
+ const files = item.files;
85
+ if (typeof id !== 'string' || id.length === 0)
86
+ return null;
87
+ if (files !== undefined && (!Array.isArray(files) || files.some((f) => typeof f !== 'string'))) {
88
+ return null;
89
+ }
90
+ out.push({ id, files: files ?? [] });
91
+ }
92
+ return out;
93
+ }
@@ -68,6 +68,22 @@ export interface DispatchPromptInput {
68
68
  * and before the memory/task content.
69
69
  */
70
70
  freshContextBlock?: string | null;
71
+ /**
72
+ * Slice 2026-09-10-dispatch-token-and-swarm §4: session capsule
73
+ * published by the orchestrator through `peaks sub-agent share`.
74
+ *
75
+ * - `undefined` / `null` → no capsule pointer, no precedence line
76
+ * (byte-identical legacy prompt).
77
+ * - `{ batchId, key, bytes }` → a `shared-read` pointer plus the
78
+ * mandatory precedence line: the capsule is ADVISORY BACKGROUND only
79
+ * and the task spec wins on conflict. Nothing the sub-agent must act
80
+ * on may live only in the capsule.
81
+ */
82
+ capsule?: {
83
+ readonly batchId: string;
84
+ readonly key: string;
85
+ readonly bytes: number;
86
+ } | null;
71
87
  }
72
88
  /**
73
89
  * Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
@@ -91,7 +107,7 @@ export interface DispatchPromptInput {
91
107
  * fallback path is `peaks worktree auth grant --rid <id> --reason <text>
92
108
  * --ttl <5m>` (already shipped). Update the prose once `spawn` lands.
93
109
  */
94
- export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\nThis chain ends with `git worktree add` (superpowers:using-git-worktrees SKILL.md line 96). It bypasses peaks-loop's L2 hook gate and L3 IDE `permissions.deny`. Even when L3 denies the terminal Skill, the chain has already taught you to use raw `git worktree add`, so L3 is not sufficient.\n\nFor worktree operations, use ONLY:\n\n- `peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>` (after rid-L2-extended ships)\n- until then: `peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>`\n\nThe superpowers skills remain available as REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`) are authoritative.\n\nIf the upstream superpowers chain suggests raw `git worktree add`:\n\n1. STOP\n2. Re-author the plan as a peaks-rd artifact\n3. Continue with `peaks worktree spawn` (or the auth-grant fallback)\n";
110
+ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\nThat chain ends with `git worktree add` (superpowers:using-git-worktrees SKILL.md line 96), which bypasses peaks-loop's L2 hook gate and L3 IDE `permissions.deny`. L3 denial is NOT sufficient \u2014 by then the chain has already taught raw `git worktree add`.\n\nFor worktree operations, use ONLY:\n\n- `peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>` (after rid-L2-extended ships)\n- until then: `peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>`\n\nSuperpowers skills remain REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`) are authoritative.\n\nIf the upstream superpowers chain suggests raw `git worktree add`:\n\n1. STOP\n2. Re-author the plan as a peaks-rd artifact\n3. Continue with `peaks worktree spawn` (or the auth-grant fallback)\n";
95
111
  /**
96
112
  * Slice 2026-08-01-subagent-merge-and-e2e (Task 8): the dispatch
97
113
  * system prompt gains three lifecycle rules. The sub-agent must:
@@ -117,17 +133,38 @@ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusa
117
133
  * start, maximizing Anthropic prompt-cache prefix reuse (stable-first
118
134
  * ordering).
119
135
  */
120
- export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit. The parent session will best-effort-kill it before merge-back.\n- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10). Your E2E work is duplicate effort.\n- Do NOT call `git merge`, `git pull`, `git rebase`, or `peaks worktree release`. The parent session owns the merge-back step.\n";
136
+ export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit; the parent session best-effort-kills it before merge-back.\n- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10); your E2E work is duplicate effort.\n- Do NOT call `git merge`, `git pull`, `git rebase`, or `peaks worktree release`. The parent session owns the merge-back step.\n";
137
+ /**
138
+ * Slice 2026-09-10-context-audit-and-discipline (Slice C): cap the sub-agent's
139
+ * FINAL report.
140
+ *
141
+ * Why (measured, session 2026-09-07-session-245530): 20 sub-agent final
142
+ * reports cost ≈ 60 KB ≈ 15K tokens of the ORCHESTRATOR's window in one
143
+ * session — the reports, not the dispatch boilerplate, were the second-largest
144
+ * consumer. The sub-agent already writes a full artifact to disk; the report
145
+ * only needs to be the index into it.
146
+ *
147
+ * QUALITY GUARD (binding): the cap removes no information. Everything the
148
+ * parent needs to ACT on stays in the report; everything longer lives in the
149
+ * artifact the parent can `Read`. The five mandatory fields below are exactly
150
+ * the ones the orchestrator must have to decide the next gate.
151
+ */
152
+ export declare const REPORT_CAP_BLOCK = "## Final report cap (mandatory)\n\nYour FINAL report to the parent MUST be \u2264 40 lines and \u2264 2 KB. Write any longer detail into the artifact file you already own \u2014 the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry: changed files (one line each), the exact commands you ran, pass/fail counts, tsc status, and any blocker. Do NOT paste file contents, full tool output, or logs into the report.\n";
121
153
  /**
122
- * Compose the system-prompt body that the dispatch site prepends to
123
- * `formatTestToolDetection()\n\n`.
154
+ * Compose the system-prompt body for a sub-agent dispatch.
155
+ *
156
+ * 2026-09-10-dispatch-block-d (Option D): the composer owns the Test Tool
157
+ * Detection injection — ONE unified block for every role, prepended first.
158
+ * Callers MUST NOT prepend `formatTestToolDetection()` themselves or the
159
+ * block is injected twice.
124
160
  *
125
161
  * Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
126
- * controller brief): when the memory block is unavailable, the caller does
127
- * `formatTestToolDetection()\n\n${taskBody}` — i.e. the final prompt is exactly
128
- * `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
129
- * produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
130
- * so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
162
+ * controller brief): when the memory block is unavailable, the composed body is
163
+ * exactly `formatTestToolDetection() + "\n\n" + L1 + "\n" + LIFECYCLE +
164
+ * "\n" + REPORT_CAP + "\n" + contextBlock + taskBody`, so the unavailable
165
+ * branch MUST return `taskBody` unwrapped (NOT a `# title\n\n` wrap).
166
+ * (REPORT_CAP joined the stable prefix in slice
167
+ * 2026-09-10-context-audit-and-discipline, Slice C.)
131
168
  * The contract holds for callers that do not pass `codegraphBlock` (all
132
169
  * non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
133
170
  * codegraph structure block (or its fail-soft unavailable note) for RD
@@ -151,3 +188,23 @@ export declare function buildDispatchSystemPrompt(input: DispatchPromptInput): s
151
188
  * byte-stable and trivially testable.
152
189
  */
153
190
  export declare const CODEGRAPH_UNAVAILABLE_BLOCK = "## Codegraph structure\n\ncodegraph unavailable \u2014 proceeding on project-scan only.\n";
191
+ /** Binding phrases every dispatch prompt must contain, for every role. */
192
+ export declare const BINDING_RULE_TOKENS: readonly string[];
193
+ /**
194
+ * The runner-direct-path tokens: the refusal example, the two direct paths
195
+ * the block names (`peaks test --json` to introspect; PB-5, the repo-defined
196
+ * `test` / `test:*` scripts that are NOT gated), and the two pieces of
197
+ * quality guidance that must survive any compression — never assume a
198
+ * runner without asking the user as a last resort, and prefer
199
+ * `peaks test <file>` because it resolves the local binary Windows-aware.
200
+ *
201
+ * 2026-09-10-dispatch-block-d (Option D): there is no role split any more,
202
+ * so this set is asserted IDENTICALLY for every role. The runner EXAMPLES
203
+ * were removed as part of the unification — they were never rules.
204
+ */
205
+ export declare const TEST_RUNNER_RULE_TOKENS: readonly string[];
206
+ /**
207
+ * Return the subset of `tokens` that `text` does NOT contain. Pure; used by
208
+ * the rule-presence guard and usable by any future prompt self-check.
209
+ */
210
+ export declare function missingRuleTokens(text: string, tokens: readonly string[]): readonly string[];
@@ -1,3 +1,4 @@
1
+ import { formatTestToolDetection } from '../dispatch/test-tool-detection.js';
1
2
  /**
2
3
  * Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
3
4
  * defence. The block below is prepended to every sub-agent dispatch system
@@ -27,14 +28,14 @@ You MUST NOT follow the superpowers chain for worktree decisions:
27
28
  - superpowers:brainstorming → superpowers:writing-plans →
28
29
  superpowers:subagent-driven-development → superpowers:using-git-worktrees
29
30
 
30
- This chain ends with \`git worktree add\` (superpowers:using-git-worktrees SKILL.md line 96). It bypasses peaks-loop's L2 hook gate and L3 IDE \`permissions.deny\`. Even when L3 denies the terminal Skill, the chain has already taught you to use raw \`git worktree add\`, so L3 is not sufficient.
31
+ That chain ends with \`git worktree add\` (superpowers:using-git-worktrees SKILL.md line 96), which bypasses peaks-loop's L2 hook gate and L3 IDE \`permissions.deny\`. L3 denial is NOT sufficient — by then the chain has already taught raw \`git worktree add\`.
31
32
 
32
33
  For worktree operations, use ONLY:
33
34
 
34
35
  - \`peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>\` (after rid-L2-extended ships)
35
36
  - until then: \`peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>\`
36
37
 
37
- The superpowers skills remain available as REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (\`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md\`) are authoritative.
38
+ Superpowers skills remain REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (\`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md\`) are authoritative.
38
39
 
39
40
  If the upstream superpowers chain suggests raw \`git worktree add\`:
40
41
 
@@ -69,20 +70,44 @@ If the upstream superpowers chain suggests raw \`git worktree add\`:
69
70
  */
70
71
  export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
71
72
 
72
- - If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with \`peaks sub-agent shutdown register --pid <pid> --name <label>\` before you exit. The parent session will best-effort-kill it before merge-back.
73
- - Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10). Your E2E work is duplicate effort.
73
+ - If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with \`peaks sub-agent shutdown register --pid <pid> --name <label>\` before you exit; the parent session best-effort-kills it before merge-back.
74
+ - Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10); your E2E work is duplicate effort.
74
75
  - Do NOT call \`git merge\`, \`git pull\`, \`git rebase\`, or \`peaks worktree release\`. The parent session owns the merge-back step.
75
76
  `;
76
77
  /**
77
- * Compose the system-prompt body that the dispatch site prepends to
78
- * `formatTestToolDetection()\n\n`.
78
+ * Slice 2026-09-10-context-audit-and-discipline (Slice C): cap the sub-agent's
79
+ * FINAL report.
80
+ *
81
+ * Why (measured, session 2026-09-07-session-245530): 20 sub-agent final
82
+ * reports cost ≈ 60 KB ≈ 15K tokens of the ORCHESTRATOR's window in one
83
+ * session — the reports, not the dispatch boilerplate, were the second-largest
84
+ * consumer. The sub-agent already writes a full artifact to disk; the report
85
+ * only needs to be the index into it.
86
+ *
87
+ * QUALITY GUARD (binding): the cap removes no information. Everything the
88
+ * parent needs to ACT on stays in the report; everything longer lives in the
89
+ * artifact the parent can `Read`. The five mandatory fields below are exactly
90
+ * the ones the orchestrator must have to decide the next gate.
91
+ */
92
+ export const REPORT_CAP_BLOCK = `## Final report cap (mandatory)
93
+
94
+ Your FINAL report to the parent MUST be ≤ 40 lines and ≤ 2 KB. Write any longer detail into the artifact file you already own — the parent can \`Read\` that file for the full detail, so nothing is lost. The report itself MUST still carry: changed files (one line each), the exact commands you ran, pass/fail counts, tsc status, and any blocker. Do NOT paste file contents, full tool output, or logs into the report.
95
+ `;
96
+ /**
97
+ * Compose the system-prompt body for a sub-agent dispatch.
98
+ *
99
+ * 2026-09-10-dispatch-block-d (Option D): the composer owns the Test Tool
100
+ * Detection injection — ONE unified block for every role, prepended first.
101
+ * Callers MUST NOT prepend `formatTestToolDetection()` themselves or the
102
+ * block is injected twice.
79
103
  *
80
104
  * Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
81
- * controller brief): when the memory block is unavailable, the caller does
82
- * `formatTestToolDetection()\n\n${taskBody}` — i.e. the final prompt is exactly
83
- * `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
84
- * produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
85
- * so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
105
+ * controller brief): when the memory block is unavailable, the composed body is
106
+ * exactly `formatTestToolDetection() + "\n\n" + L1 + "\n" + LIFECYCLE +
107
+ * "\n" + REPORT_CAP + "\n" + contextBlock + taskBody`, so the unavailable
108
+ * branch MUST return `taskBody` unwrapped (NOT a `# title\n\n` wrap).
109
+ * (REPORT_CAP joined the stable prefix in slice
110
+ * 2026-09-10-context-audit-and-discipline, Slice C.)
86
111
  * The contract holds for callers that do not pass `codegraphBlock` (all
87
112
  * non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
88
113
  * codegraph structure block (or its fail-soft unavailable note) for RD
@@ -98,15 +123,33 @@ export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
98
123
  * refusal is in scope before any task-specific prose arrives.
99
124
  */
100
125
  export function buildDispatchSystemPrompt(input) {
101
- const { taskBody, memoryBlock, contextProbe, codegraphBlock, projectStackBlock, freshContextBlock } = input;
126
+ const { taskBody, memoryBlock, contextProbe, codegraphBlock, projectStackBlock, freshContextBlock, capsule } = input;
127
+ // 2026-09-10-dispatch-block-d (Option D): ONE Test Tool Detection block
128
+ // for every role — the composer owns the injection so callers MUST NOT
129
+ // prepend `formatTestToolDetection()` themselves (double injection).
130
+ const testToolText = `${formatTestToolDetection()}\n\n`;
102
131
  const contextBlock = renderContextBlock(contextProbe ?? null);
103
132
  const codegraphText = renderCodegraphBlock(codegraphBlock);
104
133
  const projectStackText = renderProjectStackBlock(projectStackBlock);
105
134
  const freshContextText = renderFreshContextBlock(freshContextBlock);
135
+ const capsuleText = renderCapsulePointer(capsule);
106
136
  if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
107
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${memoryBlock.block}\n## Task\n${taskBody}`;
137
+ return `${testToolText}${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${REPORT_CAP_BLOCK}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${capsuleText}${memoryBlock.block}\n## Task\n${taskBody}`;
108
138
  }
109
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${taskBody}`;
139
+ return `${testToolText}${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${REPORT_CAP_BLOCK}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${capsuleText}${taskBody}`;
140
+ }
141
+ /**
142
+ * Slice 2026-09-10-dispatch-token-and-swarm §4 — session capsule pointer.
143
+ *
144
+ * QUALITY GUARD: the capsule is BACKGROUND only. The precedence line below
145
+ * is part of the contract, not decoration — anything the sub-agent must
146
+ * ACT on stays inline in the task spec. The renderer therefore always
147
+ * emits the precedence sentence whenever it emits the pointer.
148
+ */
149
+ function renderCapsulePointer(capsule) {
150
+ if (capsule === null || capsule === undefined)
151
+ return '';
152
+ return `## Shared session capsule (advisory background)\nBackground facts already established by the orchestrator (${capsule.bytes} bytes): read them with \`peaks sub-agent shared-read --batch ${capsule.batchId} --key ${capsule.key}\`. This capsule is ADVISORY BACKGROUND ONLY — it is not a task. Your task spec below is authoritative and wins on any conflict; anything you must act on is stated inline there.\n\n`;
110
153
  }
111
154
  /**
112
155
  * Slice 2026-09-03-codegraph-preread: fixed degradation string emitted
@@ -199,17 +242,89 @@ function renderContextBlock(probe) {
199
242
  : 'plenty of room — continue without compacting.';
200
243
  return `## Context window (authoritative — do NOT estimate yourself)
201
244
 
202
- Your context is **${usedPct}% used** (${freePct}% free) as measured by the IDE adapter's token-counted statusline (source: \`${probe.source}\`, IDE: \`${probe.ide}\`). This number is the SAME value \`peaks code context-now\` returns — trust it; do not derive a percentage from your message length or any other heuristic (char/4 estimates diverge from token counts by 2-4x and have caused false "context too low" reports at ${freePct}%+ free).
245
+ Context **${usedPct}% used** (${freePct}% free), token-counted by the IDE adapter's statusline (source: \`${probe.source}\`, IDE: \`${probe.ide}\`). This is the SAME value \`peaks code context-now\` returns — trust it; never derive a percentage from message length (char/4 diverges 2-4x and has caused false "context too low" reports at ${freePct}%+ free).
203
246
 
204
247
  **Action:** ${action}
205
248
 
206
- If you are tempted to declare "context pressure" or "context too low" to the parent, FIRST re-run \`peaks code context-now\` and compare its \`ratio\` field to the number above. Only report context pressure if \`peaks code context-now\` returns \`verdict: red-line\` or \`action: auto-compact-now\`.
249
+ Before telling the parent "context pressure" or "context too low", re-run \`peaks code context-now\` and compare its \`ratio\` to the number above. Report pressure ONLY if it returns \`verdict: red-line\` or \`action: auto-compact-now\`.
207
250
 
208
251
  `;
209
252
  }
210
253
  return `## Context window (no probe available)
211
254
 
212
- The orchestrator did not capture a context-fill probe before this dispatch. If you need to evaluate context pressure, run \`peaks code context-now --project <root>\` and trust its \`ratio\` field. Do not estimate from message length.
255
+ No context-fill probe was captured before this dispatch. To evaluate context pressure, run \`peaks code context-now --project <root>\` and trust its \`ratio\` field. Do not estimate from message length.
213
256
 
214
257
  `;
215
258
  }
259
+ /* ──────────────────────────────────────────────────────────────────────────
260
+ * Slice 2026-09-10-dispatch-token-and-swarm §1 — rule-presence guard.
261
+ *
262
+ * The compression + role-scoping in this file is allowed to shorten prose.
263
+ * It is NOT allowed to drop a binding rule. These token sets are the
264
+ * machine-checkable definition of "binding rule": each entry is a phrase
265
+ * that carries an obligation (MUST / MUST NOT / refused / a command the
266
+ * sub-agent is told to use or avoid). The guard test asserts that EVERY
267
+ * role's composed prompt contains EVERY token — so a future compression
268
+ * that deletes a rule fails CI instead of silently weakening the contract.
269
+ * ────────────────────────────────────────────────────────────────────────── */
270
+ /** Binding phrases every dispatch prompt must contain, for every role. */
271
+ export const BINDING_RULE_TOKENS = [
272
+ // L1 worktree governance
273
+ 'MUST NOT follow the superpowers chain',
274
+ 'superpowers:using-git-worktrees',
275
+ '`git worktree add`',
276
+ '`peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>`',
277
+ '`peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>`',
278
+ 'MUST NOT be used as a workflow',
279
+ 'STOP',
280
+ 'Re-author the plan as a peaks-rd artifact',
281
+ // lifecycle rules
282
+ '`peaks sub-agent shutdown register --pid <pid> --name <label>`',
283
+ 'Do NOT run E2E',
284
+ 'Do NOT call `git merge`, `git pull`, `git rebase`',
285
+ '`peaks worktree release`',
286
+ // context window
287
+ 'do NOT estimate yourself',
288
+ '`peaks code context-now`',
289
+ '`verdict: red-line`',
290
+ // final report cap (Slice 2026-09-10-context-audit-and-discipline, Slice C)
291
+ '## Final report cap (mandatory)',
292
+ '≤ 40 lines and ≤ 2 KB',
293
+ 'the parent can `Read` that file for the full detail',
294
+ 'changed files (one line each)',
295
+ 'pass/fail counts',
296
+ 'tsc status',
297
+ // test scope — ONE unified block, byte-identical for EVERY role
298
+ '## Test Tool Detection (mandatory)',
299
+ '`package.json#scripts.test`',
300
+ 'do NOT invoke `npx <runner>`',
301
+ '## Test Scope (mandatory)',
302
+ 'PEAKS_FULL_TEST=1',
303
+ 'refused',
304
+ ];
305
+ /**
306
+ * The runner-direct-path tokens: the refusal example, the two direct paths
307
+ * the block names (`peaks test --json` to introspect; PB-5, the repo-defined
308
+ * `test` / `test:*` scripts that are NOT gated), and the two pieces of
309
+ * quality guidance that must survive any compression — never assume a
310
+ * runner without asking the user as a last resort, and prefer
311
+ * `peaks test <file>` because it resolves the local binary Windows-aware.
312
+ *
313
+ * 2026-09-10-dispatch-block-d (Option D): there is no role split any more,
314
+ * so this set is asserted IDENTICALLY for every role. The runner EXAMPLES
315
+ * were removed as part of the unification — they were never rules.
316
+ */
317
+ export const TEST_RUNNER_RULE_TOKENS = [
318
+ '`./node_modules/.bin/vitest run`',
319
+ 'PB-5',
320
+ '`peaks test --json`',
321
+ 'ask the user before assuming a runner',
322
+ '(Windows-aware)',
323
+ ];
324
+ /**
325
+ * Return the subset of `tokens` that `text` does NOT contain. Pure; used by
326
+ * the rule-presence guard and usable by any future prompt self-check.
327
+ */
328
+ export function missingRuleTokens(text, tokens) {
329
+ return tokens.filter((t) => !text.includes(t));
330
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `peaks code context-audit` — what actually fills the orchestrator's window.
3
+ *
4
+ * Slice 2026-09-10-context-audit-and-discipline (Slice A).
5
+ *
6
+ * Why this exists: `peaks code context-now` reports a RATIO only. Nothing
7
+ * reported WHAT occupies the window, so the same 40K-token mistake (dumping a
8
+ * full `peaks memory reindex --json` array four times in one session) was
9
+ * invisible until the window was 68% gone. The IDE transcript already holds
10
+ * per-message tool results, so the breakdown is derivable locally, with zero
11
+ * tokens spent asking a model.
12
+ *
13
+ * Contract:
14
+ * - READ-ONLY. The transcript is never modified.
15
+ * - FAIL-SOFT. A missing / oversized / corrupt transcript yields
16
+ * `available: false` plus a machine-readable `reason`. Never throws,
17
+ * never blocks a workflow, never exits non-zero on its own.
18
+ * - NO CONTENT. The envelope carries tool names, short command/path keys
19
+ * and byte counts — never the tool result text itself (dumping it would
20
+ * re-create the very problem this command measures).
21
+ * - BOUNDED MEMORY. The transcript can be tens of MB; it is streamed in
22
+ * fixed-size chunks with a carried partial line, never read whole.
23
+ *
24
+ * Grouping key = `(tool name, short input key)`. The key is a *stable
25
+ * summary* of the tool input — the Bash command line, the file path tail, the
26
+ * grep pattern — so "4 × the same 40KB reindex dump" collapses into ONE row
27
+ * with `count: 4` instead of four anonymous entries.
28
+ */
29
+ /** Default number of top entries emitted. */
30
+ export declare const CONTEXT_AUDIT_DEFAULT_TOP = 15;
31
+ /** Hard ceiling for `--top` — the envelope must stay small by construction. */
32
+ export declare const CONTEXT_AUDIT_MAX_TOP = 100;
33
+ /** Transcripts larger than this are reported `available:false` (fail-soft). */
34
+ export declare const CONTEXT_AUDIT_MAX_TRANSCRIPT_BYTES: number;
35
+ export interface ContextAuditEntry {
36
+ /** Tool name (`Bash`, `Read`, `Grep`, …), or `unknown` when unmatched. */
37
+ readonly tool: string;
38
+ /** Short, stable summary of the tool input (command line / path tail / pattern). */
39
+ readonly key: string;
40
+ /** Total UTF-8 bytes of every tool result in this group. */
41
+ readonly bytes: number;
42
+ /**
43
+ * Share of the session's tool-result bytes, as a PERCENTAGE in `[0, 100]`
44
+ * with one decimal (e.g. `4.2` — not the `0.042` ratio). The name and the
45
+ * value agree: `pct` means percent.
46
+ */
47
+ readonly pctOfTotal: number;
48
+ /** How many tool results landed in this group. */
49
+ readonly count: number;
50
+ }
51
+ export interface ContextAuditResult {
52
+ /** False when the transcript could not be read; see `reason`. */
53
+ readonly available: boolean;
54
+ /** Machine-readable unavailability reason (`null` when available). */
55
+ readonly reason: string | null;
56
+ /** Absolute transcript path, or `null` when unresolved. */
57
+ readonly transcriptPath: string | null;
58
+ /** Total UTF-8 bytes of all tool results seen. */
59
+ readonly totalBytes: number;
60
+ /** Number of tool-result entries seen. */
61
+ readonly entryCount: number;
62
+ /** Distinct `(tool, key)` groups — always ≥ `entries.length`. */
63
+ readonly groupCount: number;
64
+ /** Number of top entries requested. */
65
+ readonly topN: number;
66
+ /** Top-N groups, sorted by bytes descending. */
67
+ readonly entries: readonly ContextAuditEntry[];
68
+ }
69
+ export interface ContextAuditInput {
70
+ /** Outer (harness) session id — the transcript is named by it. */
71
+ readonly outerSessionId?: string | null;
72
+ /** How many top entries to emit. Clamped to `[1, CONTEXT_AUDIT_MAX_TOP]`. */
73
+ readonly topN?: number;
74
+ /** Explicit transcript path override (test seam; skips the locator). */
75
+ readonly transcriptPath?: string | null;
76
+ /** Override the too-large threshold (test seam; default 256 MB). */
77
+ readonly maxTranscriptBytes?: number;
78
+ /** Env used to detect the active IDE (default `process.env`). */
79
+ readonly env?: NodeJS.ProcessEnv;
80
+ }
81
+ /** Clamp a caller-supplied `--top` into the documented range. */
82
+ export declare function normalizeTopN(value: unknown): number;
83
+ /**
84
+ * Build the stable group key for one tool call. Unknown tools fall back to a
85
+ * clipped JSON rendering of their input so the group is still recognizable.
86
+ */
87
+ export declare function contextAuditKey(tool: string, input: unknown): string;
88
+ /**
89
+ * Audit the CURRENT session's transcript. Never throws.
90
+ *
91
+ * Unavailability reasons (all return `available: false`, exit code stays 0):
92
+ * - `no-outer-session-id` — the peaks session has no bound outer id
93
+ * - `transcript-locator-unavailable` — the active IDE adapter does not
94
+ * declare `compact.resolveTranscriptPath`
95
+ * - `transcript-not-found` — the adapter locator returned null
96
+ * - `transcript-too-large` — above `CONTEXT_AUDIT_MAX_TRANSCRIPT_BYTES`
97
+ * - `transcript-unreadable`— stat/open failed
98
+ * - `audit-failed` — any unexpected internal error
99
+ */
100
+ export declare function auditContext(input?: ContextAuditInput): ContextAuditResult;