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.
- package/CHANGELOG.md +16 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +57 -2
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/package.json +5 -5
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +8 -0
- package/skills/peaks-code/references/context-governance.md +29 -0
- 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\
|
|
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
|
|
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
|
|
123
|
-
*
|
|
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
|
|
127
|
-
* `formatTestToolDetection()\n\n
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10)
|
|
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
|
-
*
|
|
78
|
-
*
|
|
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
|
|
82
|
-
* `formatTestToolDetection()\n\n
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|