peaks-loop 4.0.35 → 4.0.37
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 +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- 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/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- 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/scan-commands.js +1 -1
- 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/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- 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-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- 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/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- 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/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- 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/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- 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 +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-three-fixes (Slice 2) — proactive context-consumer hint.
|
|
3
|
+
*
|
|
4
|
+
* `peaks code context-audit` (Slice A, same day) already reports WHAT fills
|
|
5
|
+
* the window, but only when someone remembers to run it. The Step 0.8
|
|
6
|
+
* PreToolUse gate (`peaks code gate-step-08`) runs before every Bash call, so
|
|
7
|
+
* it is the natural place to surface the single largest consumer — provided
|
|
8
|
+
* that costs nothing measurable.
|
|
9
|
+
*
|
|
10
|
+
* Cost contract (the whole point of this module):
|
|
11
|
+
* - The ratio probe is the cheap, adapter-driven `readContextPercent`
|
|
12
|
+
* (env-var → statusline file read; the transcript fallback is a
|
|
13
|
+
* bounded backwards scan, never a whole-file read).
|
|
14
|
+
* - The expensive part — the transcript scan inside `auditContext` — runs
|
|
15
|
+
* at most ONCE per TTL window. Its result is cached under
|
|
16
|
+
* `.peaks/_runtime/<sid>/context-audit-hint.json`; a fresh entry
|
|
17
|
+
* short-circuits the scan entirely.
|
|
18
|
+
* - Every failure is fail-soft: a missing/corrupt cache, an unavailable
|
|
19
|
+
* transcript, an adapter that cannot locate one — all yield `null`
|
|
20
|
+
* (emit nothing). The gate never blocks and never gains more than ONE
|
|
21
|
+
* line.
|
|
22
|
+
*
|
|
23
|
+
* The cache deliberately records FAILED audits too (with `available:false`),
|
|
24
|
+
* so a broken transcript cannot turn every Bash call back into a scan.
|
|
25
|
+
*/
|
|
26
|
+
import { type ContextAuditInput, type ContextAuditResult } from './context-audit.js';
|
|
27
|
+
/** Only surface a hint once the window is this full. */
|
|
28
|
+
export declare const CONTEXT_HINT_RATIO_THRESHOLD = 0.7;
|
|
29
|
+
/** Minimum cache TTL — a fresh entry must skip the transcript scan. */
|
|
30
|
+
export declare const CONTEXT_HINT_CACHE_TTL_MS: number;
|
|
31
|
+
/** Per-session cache file name (under `.peaks/_runtime/<sid>/`). */
|
|
32
|
+
export declare const CONTEXT_HINT_CACHE_FILE_NAME = "context-audit-hint.json";
|
|
33
|
+
/** Cached top-consumer snapshot — one audit result, no transcript content. */
|
|
34
|
+
export interface ContextHintCacheEntry {
|
|
35
|
+
/** Epoch ms when the audit ran (TTL anchor). */
|
|
36
|
+
readonly cachedAt: number;
|
|
37
|
+
/** False when the audit could not read the transcript. */
|
|
38
|
+
readonly available: boolean;
|
|
39
|
+
/** Ratio observed when the audit ran (informational). */
|
|
40
|
+
readonly ratio: number;
|
|
41
|
+
readonly tool: string | null;
|
|
42
|
+
readonly key: string | null;
|
|
43
|
+
readonly bytes: number;
|
|
44
|
+
readonly pctOfTotal: number;
|
|
45
|
+
readonly count: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ContextAuditHintInput {
|
|
48
|
+
readonly projectRoot: string;
|
|
49
|
+
readonly sessionId: string;
|
|
50
|
+
readonly outerSessionId?: string | null | undefined;
|
|
51
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
52
|
+
/** Clock override (test seam). */
|
|
53
|
+
readonly nowMs?: number | undefined;
|
|
54
|
+
/** Cache path override (test seam; default is per-session under `_runtime`). */
|
|
55
|
+
readonly cachePath?: string | undefined;
|
|
56
|
+
/** Ratio probe override (test seam). Returns null when unknown. */
|
|
57
|
+
readonly probeRatio?: (() => number | null) | undefined;
|
|
58
|
+
/** Audit runner override (test seam; lets a test count scans). */
|
|
59
|
+
readonly runAudit?: ((input: ContextAuditInput) => ContextAuditResult) | undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `<projectRoot>/.peaks/_runtime/<sid>/context/context-audit-hint.json`
|
|
63
|
+
* (gitignored). The `context/` bucket mirrors the existing `<sid>/txt/`
|
|
64
|
+
* convention so session-id artifacts stay under `.peaks/_runtime/<sid>/`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function contextHintCachePath(projectRoot: string, sessionId: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* The single line the gate may append. Names ONE consumer — the largest by
|
|
69
|
+
* tool-result bytes — plus the share and call count that justify the number.
|
|
70
|
+
*/
|
|
71
|
+
export declare function formatContextHintLine(entry: ContextHintCacheEntry): string;
|
|
72
|
+
/**
|
|
73
|
+
* Build the ONE optional hint line for the Step 0.8 gate.
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` when: the ratio is unknown or below 0.70, the cache (fresh)
|
|
76
|
+
* says the audit was unavailable, the fresh audit found no groups, or
|
|
77
|
+
* anything throws. Never throws, never blocks.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildContextAuditHint(input: ContextAuditHintInput): string | null;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-three-fixes (Slice 2) — proactive context-consumer hint.
|
|
3
|
+
*
|
|
4
|
+
* `peaks code context-audit` (Slice A, same day) already reports WHAT fills
|
|
5
|
+
* the window, but only when someone remembers to run it. The Step 0.8
|
|
6
|
+
* PreToolUse gate (`peaks code gate-step-08`) runs before every Bash call, so
|
|
7
|
+
* it is the natural place to surface the single largest consumer — provided
|
|
8
|
+
* that costs nothing measurable.
|
|
9
|
+
*
|
|
10
|
+
* Cost contract (the whole point of this module):
|
|
11
|
+
* - The ratio probe is the cheap, adapter-driven `readContextPercent`
|
|
12
|
+
* (env-var → statusline file read; the transcript fallback is a
|
|
13
|
+
* bounded backwards scan, never a whole-file read).
|
|
14
|
+
* - The expensive part — the transcript scan inside `auditContext` — runs
|
|
15
|
+
* at most ONCE per TTL window. Its result is cached under
|
|
16
|
+
* `.peaks/_runtime/<sid>/context-audit-hint.json`; a fresh entry
|
|
17
|
+
* short-circuits the scan entirely.
|
|
18
|
+
* - Every failure is fail-soft: a missing/corrupt cache, an unavailable
|
|
19
|
+
* transcript, an adapter that cannot locate one — all yield `null`
|
|
20
|
+
* (emit nothing). The gate never blocks and never gains more than ONE
|
|
21
|
+
* line.
|
|
22
|
+
*
|
|
23
|
+
* The cache deliberately records FAILED audits too (with `available:false`),
|
|
24
|
+
* so a broken transcript cannot turn every Bash call back into a scan.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { atomicWriteJson } from '../ide/shared/atomic-json.js';
|
|
29
|
+
import { readContextPercent } from './auto-compact-reader.js';
|
|
30
|
+
import { auditContext } from './context-audit.js';
|
|
31
|
+
/** Only surface a hint once the window is this full. */
|
|
32
|
+
export const CONTEXT_HINT_RATIO_THRESHOLD = 0.7;
|
|
33
|
+
/** Minimum cache TTL — a fresh entry must skip the transcript scan. */
|
|
34
|
+
export const CONTEXT_HINT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
35
|
+
/** Per-session cache file name (under `.peaks/_runtime/<sid>/`). */
|
|
36
|
+
export const CONTEXT_HINT_CACHE_FILE_NAME = 'context-audit-hint.json';
|
|
37
|
+
/**
|
|
38
|
+
* `<projectRoot>/.peaks/_runtime/<sid>/context/context-audit-hint.json`
|
|
39
|
+
* (gitignored). The `context/` bucket mirrors the existing `<sid>/txt/`
|
|
40
|
+
* convention so session-id artifacts stay under `.peaks/_runtime/<sid>/`.
|
|
41
|
+
*/
|
|
42
|
+
export function contextHintCachePath(projectRoot, sessionId) {
|
|
43
|
+
const safeSessionId = sessionId.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\.+/, '');
|
|
44
|
+
return join(projectRoot, '.peaks', '_runtime', safeSessionId.length > 0 ? safeSessionId : 'unknown', 'context', CONTEXT_HINT_CACHE_FILE_NAME);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The single line the gate may append. Names ONE consumer — the largest by
|
|
48
|
+
* tool-result bytes — plus the share and call count that justify the number.
|
|
49
|
+
*/
|
|
50
|
+
export function formatContextHintLine(entry) {
|
|
51
|
+
const ratioPct = (entry.ratio * 100).toFixed(1);
|
|
52
|
+
const sharePct = entry.pctOfTotal.toFixed(1);
|
|
53
|
+
const calls = entry.count === 1 ? '1 call' : `${entry.count} calls`;
|
|
54
|
+
return `Context ${ratioPct}% used — top consumer: ${entry.tool} \`${entry.key}\` (${sharePct}% of tool-result bytes, ${calls}). Run \`peaks code context-audit\` for the full breakdown.`;
|
|
55
|
+
}
|
|
56
|
+
function readCache(cachePath) {
|
|
57
|
+
if (!existsSync(cachePath))
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
61
|
+
if (typeof parsed.cachedAt !== 'number' || !Number.isFinite(parsed.cachedAt))
|
|
62
|
+
return null;
|
|
63
|
+
if (typeof parsed.available !== 'boolean')
|
|
64
|
+
return null;
|
|
65
|
+
return {
|
|
66
|
+
cachedAt: parsed.cachedAt,
|
|
67
|
+
available: parsed.available,
|
|
68
|
+
ratio: typeof parsed.ratio === 'number' && Number.isFinite(parsed.ratio) ? parsed.ratio : 0,
|
|
69
|
+
tool: typeof parsed.tool === 'string' ? parsed.tool : null,
|
|
70
|
+
key: typeof parsed.key === 'string' ? parsed.key : null,
|
|
71
|
+
bytes: typeof parsed.bytes === 'number' && Number.isFinite(parsed.bytes) ? parsed.bytes : 0,
|
|
72
|
+
pctOfTotal: typeof parsed.pctOfTotal === 'number' && Number.isFinite(parsed.pctOfTotal) ? parsed.pctOfTotal : 0,
|
|
73
|
+
count: typeof parsed.count === 'number' && Number.isFinite(parsed.count) ? parsed.count : 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Never throws: an unavailable ratio probe degrades to "no hint". */
|
|
81
|
+
function probeRatioSafe(input) {
|
|
82
|
+
if (input.probeRatio !== undefined) {
|
|
83
|
+
try {
|
|
84
|
+
const value = input.probeRatio();
|
|
85
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const probe = readContextPercent({
|
|
93
|
+
projectRoot: input.projectRoot,
|
|
94
|
+
sessionId: input.sessionId,
|
|
95
|
+
outerSessionId: input.outerSessionId ?? undefined,
|
|
96
|
+
env: input.env ?? process.env,
|
|
97
|
+
});
|
|
98
|
+
return Number.isFinite(probe.ratio) ? probe.ratio : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the ONE optional hint line for the Step 0.8 gate.
|
|
106
|
+
*
|
|
107
|
+
* Returns `null` when: the ratio is unknown or below 0.70, the cache (fresh)
|
|
108
|
+
* says the audit was unavailable, the fresh audit found no groups, or
|
|
109
|
+
* anything throws. Never throws, never blocks.
|
|
110
|
+
*/
|
|
111
|
+
export function buildContextAuditHint(input) {
|
|
112
|
+
try {
|
|
113
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
114
|
+
const ratio = probeRatioSafe(input);
|
|
115
|
+
if (ratio === null || ratio < CONTEXT_HINT_RATIO_THRESHOLD)
|
|
116
|
+
return null;
|
|
117
|
+
const cachePath = input.cachePath ?? contextHintCachePath(input.projectRoot, input.sessionId);
|
|
118
|
+
const cached = readCache(cachePath);
|
|
119
|
+
// Fresh entry → the transcript scan is skipped entirely (cost guard).
|
|
120
|
+
if (cached !== null && nowMs - cached.cachedAt < CONTEXT_HINT_CACHE_TTL_MS) {
|
|
121
|
+
return cached.available && cached.tool !== null ? formatContextHintLine(cached) : null;
|
|
122
|
+
}
|
|
123
|
+
// Cache miss / stale → at most one scan per TTL window.
|
|
124
|
+
const result = (input.runAudit ?? auditContext)({
|
|
125
|
+
outerSessionId: input.outerSessionId ?? null,
|
|
126
|
+
topN: 1,
|
|
127
|
+
});
|
|
128
|
+
const top = result.available ? (result.entries[0] ?? null) : null;
|
|
129
|
+
const entry = {
|
|
130
|
+
cachedAt: nowMs,
|
|
131
|
+
available: result.available,
|
|
132
|
+
ratio,
|
|
133
|
+
tool: top?.tool ?? null,
|
|
134
|
+
key: top?.key ?? null,
|
|
135
|
+
bytes: top?.bytes ?? 0,
|
|
136
|
+
pctOfTotal: top?.pctOfTotal ?? 0,
|
|
137
|
+
count: top?.count ?? 0,
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
atomicWriteJson(cachePath, entry);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fail-soft: an unwritable cache must not lose this turn's hint.
|
|
144
|
+
}
|
|
145
|
+
return top !== null ? formatContextHintLine(entry) : null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -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;
|