peaks-loop 4.0.25 → 4.0.26

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 (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli/commands/core/memory-command.js +1 -3
  3. package/dist/cli/commands/dispatch-commands.js +11 -67
  4. package/dist/cli/commands/memory-commands.d.ts +0 -2
  5. package/dist/cli/commands/memory-commands.js +4 -7
  6. package/dist/cli/commands/preferences-commands.js +0 -1
  7. package/dist/cli/commands/qa-commands.js +5 -5
  8. package/dist/cli/commands/sub-agent-shared.d.ts +0 -4
  9. package/dist/cli/commands/sub-agent-shared.js +0 -14
  10. package/dist/cli/commands/workflow-commands.js +5 -5
  11. package/dist/services/context/build-dispatch-system-prompt.d.ts +4 -4
  12. package/dist/services/context/build-dispatch-system-prompt.js +6 -6
  13. package/dist/services/context/context-guard.d.ts +2 -4
  14. package/dist/services/context/context-guard.js +4 -6
  15. package/dist/services/context/{headroom-fetcher.d.ts → doc-cache-fetcher.d.ts} +2 -2
  16. package/dist/services/context/{headroom-fetcher.js → doc-cache-fetcher.js} +4 -3
  17. package/dist/services/context/memory-preflight-service.js +6 -17
  18. package/dist/services/context/threshold.d.ts +0 -3
  19. package/dist/services/context/threshold.js +0 -3
  20. package/dist/services/dispatch/batch-counter.js +1 -1
  21. package/dist/services/dispatch/leak-detector.js +1 -1
  22. package/dist/services/fuzzy-matching/fzf-pick-service.d.ts +1 -1
  23. package/dist/services/fuzzy-matching/fzf-pick-service.js +1 -1
  24. package/dist/services/memory/llm-reranker.d.ts +4 -5
  25. package/dist/services/memory/llm-reranker.js +6 -8
  26. package/dist/services/memory/memory-search-service.d.ts +0 -32
  27. package/dist/services/memory/memory-search-service.js +0 -37
  28. package/dist/services/preferences/preferences-service.d.ts +5 -8
  29. package/dist/services/preferences/preferences-service.js +0 -30
  30. package/dist/services/preferences/preferences-types.d.ts +0 -22
  31. package/dist/services/preferences/preferences-types.js +0 -12
  32. package/dist/services/retrospective/retrospective-search-service.d.ts +0 -18
  33. package/dist/services/retrospective/retrospective-search-service.js +0 -32
  34. package/dist/services/slice/slice-benchmark-service.d.ts +1 -1
  35. package/dist/services/slice/slice-benchmark-service.js +1 -1
  36. package/dist/services/slice/slice-pick-service.d.ts +1 -1
  37. package/dist/services/slice/slice-pick-service.js +1 -1
  38. package/package.json +5 -6
  39. package/skills/bee/peaks-qa/references/qa-context-governance.md +1 -1
  40. package/skills/bee/peaks-rd/SKILL.md +2 -2
  41. package/skills/bee/peaks-rd/references/rd-context-governance.md +2 -2
  42. package/skills/bee/peaks-txt/SKILL.md +1 -1
  43. package/skills/bee/peaks-ui/SKILL.md +1 -1
  44. package/skills/peaks-code/SKILL.md +1 -1
  45. package/skills/peaks-code/references/context-governance.md +7 -34
  46. package/skills/peaks-code/references/envelope-contract.md +2 -7
  47. package/skills/peaks-code/references/sub-agent-dispatch.md +2 -4
  48. package/dist/services/context/headroom-client.d.ts +0 -34
  49. package/dist/services/context/headroom-client.js +0 -117
  50. package/dist/services/context/headroom-prefs.d.ts +0 -46
  51. package/dist/services/context/headroom-prefs.js +0 -34
  52. package/skills/peaks-code/references/headroom-integration.md +0 -107
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.26 — 2026-09-01 (headroom removal + prompt-cache prefix alignment)
4
+
5
+ **2 atomic commits from session 2026-09-01-session-fdd7aa**:
6
+
7
+ - `3d8c911a` refactor(context): remove headroom-ai half-baked integration
8
+ - `2086074b` perf(dispatch): stabilize sub-agent prompt prefix for prompt-cache alignment
9
+
10
+ **Highlights**:
11
+
12
+ 1. **移除 headroom-ai 半成品集成** — headroom-ai 的 TS SDK 已接但 Python proxy 后端(N-7)从未集成,`--use-headroom` / `--compress-results` 是静默 no-op。删除依赖 + headroom-client/prefs + G7.7 opt-in 路径 + HeadroomPreferences;保留 G7 metadata-only + auto-compact + prompt-cache 对齐作为省 token 主杠杆。通用 DocFetcher 保留为 doc-cache-fetcher.ts。42 文件,+100/−909。
13
+
14
+ 2. **dispatch 前缀稳定化(prompt-cache 对齐)** — 把 LIFECYCLE_RULES 从 prompt 尾部挪到 L1 worktree 块之后,让所有稳定 boilerplate 连续排在最前,最大化 Anthropic prompt-cache 前缀复用(缓存读价约 90% 折扣)。变量块(context % / project memory / task)留在最后。
15
+
3
16
  ## 4.0.25 — 2026-09-01 (codegraph hint fix + post-slice re-index + frontend ACL)
4
17
 
5
18
  **1 atomic commit from session 2026-09-01-session-fdd7aa** (user feedback batch):
@@ -74,10 +74,9 @@ export function registerMemoryCommand(program, io) {
74
74
  });
75
75
  addJsonOption(memory
76
76
  .command('search <query>')
77
- .description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6. Pass --compress-results to also emit a headroom-compressed text view of the matches for LLM-side prompt assembly.')
77
+ .description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.')
78
78
  .option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
79
79
  .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
80
- .option('--compress-results', 'compress joined match text via headroom-ai (uses preferences.headroom.perTouchpoint.memorySearch mode)')
81
80
  .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
82
81
  // Lazy import avoids a top-of-file import cycle (memory-commands.ts
83
82
  // imports services that the rest of this file may also touch).
@@ -86,7 +85,6 @@ export function registerMemoryCommand(program, io) {
86
85
  query,
87
86
  ...(options.kind !== undefined ? { kind: options.kind } : {}),
88
87
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
89
- ...(options.compressResults === true ? { compressResults: true } : {}),
90
88
  ...(options.project !== undefined ? { project: options.project } : {}),
91
89
  ...(options.json !== undefined ? { json: options.json } : {}),
92
90
  });
@@ -29,13 +29,11 @@ import { evaluatePromptSize } from '../../services/context/context-guard.js';
29
29
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
30
30
  import { buildArtifactMeta, buildContextImpact } from '../../services/context/artifact-meta.js';
31
31
  import { assertSafeArtifactPath } from 'peaks-loop-shared-channel';
32
- import { compressPrompt } from '../../services/context/headroom-client.js';
33
- import { resolveHeadroomOptions } from '../../services/context/headroom-prefs.js';
34
32
  import { playwrightProfilePaths } from '../../services/worktree/playwright-profile.js';
35
33
  import { loadPreferences } from '../../services/preferences/preferences-service.js';
36
34
  import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-types.js';
37
35
  import { writeLogEntry } from '../../services/log/logger.js';
38
- import { HEADROOM_MODES, PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
36
+ import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
39
37
  import { runDispatchFromDag } from './dispatch-from-dag.js';
40
38
  import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
41
39
  import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
@@ -46,7 +44,7 @@ export function registerDispatchCommand(parent, io) {
46
44
  .command('dispatch')
47
45
  .description('Build an IDE-specific tool-call descriptor for a sub-agent dispatch. ' +
48
46
  'Dry-run by design; the LLM executes the returned toolCall in its own ' +
49
- 'environment. Flags: --write-artifact (G7), --use-headroom (G7.7), ' +
47
+ 'environment. Flags: --write-artifact (G7), ' +
50
48
  '--force (G9 CLI 兜底). ' +
51
49
  'See skills/peaks-code/references/sub-agent-dispatch.md for the ' +
52
50
  'orchestrator contract.')
@@ -63,8 +61,6 @@ export function registerDispatchCommand(parent, io) {
63
61
  .option('--project <path>', 'target project root (defaults to cwd)')
64
62
  .option('--batch-id <uuid>', 'batch id for the dispatch (default: auto-generated UUID)')
65
63
  .option('--write-artifact <path>', 'G7: register an artifact file at <path>; CLI computes sha256 + size + writes ArtifactMeta to the dispatch record')
66
- .option('--use-headroom', 'G7.7/G9: compress the prompt via headroom-ai before dispatch (opt-in; falls back to G7 metadata-only if headroom unavailable)')
67
- .option('--headroom-mode <mode>', `G7.7: headroom mode (${HEADROOM_MODES.join(' | ')}); default balanced`)
68
64
  .option('--force', 'G9: override the 80% hard reject threshold at CLI (NOT allowed at hook layer per RL-30 strict)')
69
65
  .option('--from-dag <file>', '2.7.0 slice-dag-dispatcher MVP: read a SliceDag JSON file, dispatch one sub-agent per node in topological order; --batch-id overrides the auto-generated batch id (mutually exclusive with <role>)')
70
66
  .option('--isolation <mode>', 'slice 2026-07-29-worktree-l2-extended Part 2.C: isolation mode for the sub-agent. Accepts "worktree" (Part 2.C + Part 12 L2 surface), "container" (Part 8 contract + Part 12 L4 docker runtime), or "vm" (Part 25 contract; the VM runtime is a follow-up rid and fail-fasts with ISOLATION_VM_NOT_YET_IMPLEMENTED). Auto-spawns a lease + injects PEAKS_<MODE>_LEASE_ID into the dispatch envelope so the sub-agent can write to the isolated surface without a separate auth grant.')
@@ -351,35 +347,14 @@ export function registerDispatchCommand(parent, io) {
351
347
  return;
352
348
  }
353
349
  }
354
- // G7.7 / G9: resolve headroom options from preferences + CLI overrides.
355
- // Preferences hard-block when headroom.enabled=false (returns HEADROOM_DISABLED_BY_PREFERENCE).
356
- // loadPreferences can throw on schema mismatch; we fall back to defaults to avoid
357
- // breaking the dispatch on a stale preferences.json file.
350
+ // loadPreferences can throw on schema mismatch; we fall back to defaults
351
+ // to avoid breaking the dispatch on a stale preferences.json file.
358
352
  let projectPrefs = DEFAULT_PREFERENCES;
359
- let headroomPrefs = DEFAULT_PREFERENCES.headroom;
360
353
  try {
361
354
  projectPrefs = loadPreferences(projectRoot);
362
- headroomPrefs = projectPrefs.headroom;
363
355
  }
364
356
  catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
365
- // Keep default preferences; the user can re-run with explicit --headroom-mode
366
- // if they want to override the fallback.
367
- }
368
- const headroomResolved = resolveHeadroomOptions(headroomPrefs, {
369
- useHeadroom: options.useHeadroom === true,
370
- ...(options.headroomMode !== undefined ? { headroomMode: options.headroomMode } : {})
371
- });
372
- if (headroomResolved.blocked !== null) {
373
- printResult(io, fail('sub-agent.dispatch', headroomResolved.blocked, `headroom integration is disabled in preferences (headroom.enabled=false); pass --headroom-mode and update preferences first, or run without --use-headroom`, {
374
- role,
375
- toolCall: null,
376
- dispatchRecordPath: null
377
- }, [
378
- 'Edit .peaks/preferences.json: set headroom.enabled = true (per-touchpoint mode is headroom.perTouchpoint.subAgentDispatch).',
379
- 'Or re-run without --use-headroom to dispatch without compression.'
380
- ]), asJson);
381
- process.exitCode = 1;
382
- return;
357
+ // Keep default preferences.
383
358
  }
384
359
  const ide = detectInstalledIde(projectRoot) ?? 'claude-code';
385
360
  const adapter = getAdapter(ide);
@@ -390,12 +365,9 @@ export function registerDispatchCommand(parent, io) {
390
365
  process.exitCode = 1;
391
366
  return;
392
367
  }
393
- // G7.7 headroom compress (opt-in). If headroom fails or is unavailable,
394
- // fall back to the original prompt + emit warning.
395
368
  // Slice 2026-07-22-orchestrator-memory-preflight (Task 5): prepend the
396
369
  // memory preflight block (or silently skip when unavailable) via the
397
- // pure-function builder, BEFORE the headroom-ai compress step so the
398
- // compressor sees the augmented payload.
370
+ // pure-function builder.
399
371
  const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
400
372
  const memoryBlock = await preflightService.fetchBlock(role);
401
373
  // Slice 2026-07-29-context-evaluation-accuracy: capture the
@@ -428,9 +400,8 @@ export function registerDispatchCommand(parent, io) {
428
400
  contextProbe
429
401
  });
430
402
  // Part 2.C: when --isolation worktree, prepend an isolation envelope
431
- // block so the sub-agent sees the lease id + worktree path BEFORE
432
- // headroom-ai compress. The block is short (a few lines) and the
433
- // compressor is expected to keep it. We deliberately do NOT set
403
+ // block so the sub-agent sees the lease id + worktree path. The block
404
+ // is short (a few lines). We deliberately do NOT set
434
405
  // process.env.PEAKS_WORKTREE_LEASE_ID here — sub-agents are spawned
435
406
  // by the LLM in its own environment, not as children of this CLI;
436
407
  // the lease id travels through the dispatch record + prompt body.
@@ -465,20 +436,8 @@ export function registerDispatchCommand(parent, io) {
465
436
  `confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
466
437
  `if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
467
438
  }
468
- let effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
469
- let headroomCompressed = false;
470
- let headroomResult = null;
439
+ const effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
471
440
  const warnings = [...decision.warnings];
472
- if (headroomResolved.mode !== null) {
473
- headroomResult = await compressPrompt(effectivePrompt, headroomResolved.mode);
474
- if (headroomResult.warning !== null) {
475
- warnings.push(headroomResult.warning);
476
- }
477
- if (headroomResult.compressed && headroomResult.compressedPrompt !== null) {
478
- effectivePrompt = headroomResult.compressedPrompt;
479
- headroomCompressed = true;
480
- }
481
- }
482
441
  let toolCall;
483
442
  try {
484
443
  toolCall = adapter.subAgentDispatcher.buildToolCall({ role, prompt: effectivePrompt, requestId: rid, sessionId: sid });
@@ -554,8 +513,7 @@ export function registerDispatchCommand(parent, io) {
554
513
  detail: {
555
514
  requestId: rid,
556
515
  ide: adapter.subAgentDispatcher.label,
557
- promptBytes: effectivePrompt.length,
558
- headroomCompressed
516
+ promptBytes: effectivePrompt.length
559
517
  }
560
518
  }, { projectRoot });
561
519
  }
@@ -628,9 +586,6 @@ export function registerDispatchCommand(parent, io) {
628
586
  if (counter.warning) {
629
587
  nextActions.push(`Batch is over the RL-1 limit (${BATCH_LIMIT}); consider splitting into multiple batches.`);
630
588
  }
631
- if (headroomResult && headroomResult.warning === 'HEADROOM_UNAVAILABLE') {
632
- nextActions.push('Headroom daemon unavailable; dispatched with G7 metadata-only fallback.');
633
- }
634
589
  const expectedCompletionSeconds = 45;
635
590
  const artifactsPublicPaths = typeof options.writeArtifact === 'string' && options.writeArtifact.length > 0
636
591
  ? [options.writeArtifact]
@@ -650,7 +605,7 @@ export function registerDispatchCommand(parent, io) {
650
605
  // disk (gitignored under .peaks/_sub_agents/) keeps the prompt
651
606
  // for the sub-agent to read; CLI stdout stays metadata-only.
652
607
  // Surface promptSize + originalPromptSize so the LLM-side
653
- // runner can still reason about headroom without seeing the
608
+ // runner can reason about the size delta without seeing the
654
609
  // content.
655
610
  originalPromptSize: options.prompt.length,
656
611
  promptSize: effectivePrompt.length,
@@ -658,16 +613,6 @@ export function registerDispatchCommand(parent, io) {
658
613
  dispatchRecordPath,
659
614
  batchId,
660
615
  dispatchedInBatch: counter.count,
661
- headroomCompressed,
662
- headroomResult: headroomResult
663
- ? {
664
- mode: headroomResult.mode,
665
- compressed: headroomResult.compressed,
666
- compressionRatio: headroomResult.compressionRatio,
667
- tokensSaved: headroomResult.tokensSaved,
668
- warning: headroomResult.warning
669
- }
670
- : null,
671
616
  forcedAt: decision.forcedAt,
672
617
  contextImpact,
673
618
  artifactMetas: artifactMeta ? [artifactMeta] : [],
@@ -706,7 +651,6 @@ export function registerDispatchCommand(parent, io) {
706
651
  role,
707
652
  batchId,
708
653
  dispatchedInBatch: counter.count,
709
- headroomCompressed,
710
654
  forcedAt: decision.forcedAt
711
655
  }
712
656
  });
@@ -5,8 +5,6 @@ export interface MemorySearchCommandOptions {
5
5
  limit?: number;
6
6
  project?: string;
7
7
  json?: boolean;
8
- /** When true, call headroom-ai to compress joined match text for LLM-side prompt assembly. */
9
- compressResults?: boolean;
10
8
  }
11
9
  export interface MemoryListCommandOptions {
12
10
  kind?: string;
@@ -1,6 +1,6 @@
1
1
  import { findProjectRoot } from '../../services/config/config-safety.js';
2
2
  import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
3
- import { loadMemoryIndex, searchMemoryWithResults } from '../../services/memory/memory-search-service.js';
3
+ import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
4
4
  import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
5
5
  import { fail, ok } from 'peaks-loop-shared/result';
6
6
  import { getErrorMessage, printResult } from '../cli-helpers.js';
@@ -97,19 +97,16 @@ export async function runMemorySearch(io, options) {
97
97
  ? options.kind
98
98
  : undefined;
99
99
  try {
100
- const out = await searchMemoryWithResults({
100
+ const matches = searchMemory({
101
101
  query: options.query,
102
102
  projectRoot,
103
103
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
104
104
  ...(kindFilter !== undefined ? { kind: kindFilter } : {}),
105
- }, {
106
- ...(options.compressResults === true ? { compressResults: true } : {})
107
105
  });
108
106
  printResult(io, ok('memory.search', {
109
107
  query: options.query,
110
- total: out.matches.length,
111
- matches: out.matches,
112
- ...(out.compressedResults !== null ? { compressedResults: out.compressedResults } : {}),
108
+ total: matches.length,
109
+ matches,
113
110
  warnings: [],
114
111
  }, []), options.json);
115
112
  }
@@ -8,7 +8,6 @@ const ALLOWED_KEYS = new Set([
8
8
  'agentShieldPrompt',
9
9
  'classifyConservatism',
10
10
  'classifyRules',
11
- 'headroom',
12
11
  'swarmSpeculative',
13
12
  'loopAutonomousEnabled',
14
13
  ]);
@@ -36,13 +36,13 @@ import { BROWSER_REUSE_HINT } from '../../services/qa/browser-reuse-hint.js';
36
36
  // Plan 1 / Task 9 — auto-build peaks-context before peaks-qa runs.
37
37
  import { buildContext } from '../../services/context/context-builder.js';
38
38
  // Plan 1 / Task 10 — production fetcher (replaces mockFetcher).
39
- import { createHeadroomFetcher } from '../../services/context/headroom-fetcher.js';
39
+ import { createDocCacheFetcher } from '../../services/context/doc-cache-fetcher.js';
40
40
  // Plan 2 / Task 8 — consume MUT.sig from peaks-mut into verdict envelope.
41
41
  import { loadMutReport, mutReportPath } from 'peaks-loop-mut';
42
- function buildHeadroomFetcher(sid) {
43
- return createHeadroomFetcher({
42
+ function buildDocFetcher(sid) {
43
+ return createDocCacheFetcher({
44
44
  cacheDir: `.peaks/_runtime/${sid}/doc-cache`,
45
- // remoteFetcher wired in a future slice (headroom-ai programmatic API).
45
+ // remoteFetcher wired in a future slice.
46
46
  });
47
47
  }
48
48
  async function ensureContextForQa(goal, project, sid) {
@@ -55,7 +55,7 @@ async function ensureContextForQa(goal, project, sid) {
55
55
  depsMode: 'locked',
56
56
  docBudgetTokens: 8000,
57
57
  out,
58
- fetcher: buildHeadroomFetcher(sid),
58
+ fetcher: buildDocFetcher(sid),
59
59
  });
60
60
  }
61
61
  catch (error) {
@@ -12,12 +12,10 @@
12
12
  * Everything else is internal to the `peaks sub-agent` group.
13
13
  */
14
14
  import type { SubAgentBatchResult } from '../../services/dispatch/sub-agent-dispatcher.js';
15
- import type { HeadroomMode } from '../../services/context/headroom-client.js';
16
15
  import type { HeartbeatStatus } from '../../services/dispatch/dispatch-record-writer.js';
17
16
  export { probeShell, type ShellProbeReport, type ShellProbeOptions } from '../../services/env/shell-probe.js';
18
17
  export declare const RECOMMENDED_ROLES = "rd | qa | ui | txt | qa-business | qa-perf | qa-security | qa-business-<*> | general-purpose";
19
18
  export declare const HEARTBEAT_STATUSES: readonly HeartbeatStatus[];
20
- export declare const HEADROOM_MODES: readonly HeadroomMode[];
21
19
  export declare const PROMPT_LIMIT_BYTES: number;
22
20
  export type DispatchOptions = {
23
21
  prompt?: string;
@@ -27,8 +25,6 @@ export type DispatchOptions = {
27
25
  project?: string;
28
26
  batchId?: string;
29
27
  writeArtifact?: string;
30
- useHeadroom?: boolean;
31
- headroomMode?: string;
32
28
  force?: boolean;
33
29
  fromDag?: string;
34
30
  /**
@@ -25,7 +25,6 @@ export const HEARTBEAT_STATUSES = [
25
25
  'never-started',
26
26
  'unreadable'
27
27
  ];
28
- export const HEADROOM_MODES = ['balanced', 'aggressive', 'conservative'];
29
28
  export const PROMPT_LIMIT_BYTES = 256 * 1024;
30
29
  /**
31
30
  * Validate a role string. Returns `null` when valid, otherwise the
@@ -75,16 +74,3 @@ export function summarizeBatchResults(results) {
75
74
  }
76
75
  return { total: results.length, done, failed, cancelled, timeout };
77
76
  }
78
- // Note: `isHeadroomMode` and `RegisterSubCommand` used to live here as
79
- // duplicate exports. Removed in slice 2026-06-23-audit-p0-cleanup:
80
- // - `isHeadroomMode` is exported by `src/services/context/headroom-prefs.ts`
81
- // and that is the canonical source — dispatch consumer imports from
82
- // there directly.
83
- // - `RegisterSubCommand` was never used as a type anywhere; the entry
84
- // point (`sub-agent-commands.ts`) calls each register function with
85
- // `(program, io)` directly.
86
- // - `deriveProjectRoot` (audit-p0-reaudit) was removed in slice
87
- // 2026-06-23-audit-3rd: it trusted the record path's `.peaks` segment,
88
- // letting a caller point `--record` at any project's record tree. The
89
- // heartbeat command now trusts `--project` (or `process.cwd()`) and
90
- // leaves the relative() backstop to the R-2 guard.
@@ -36,11 +36,11 @@ import { registerWorkflowLifecycleCommand } from './workflow-lifecycle-commands.
36
36
  // Plan 1 / Task 9 — auto-build peaks-context before peaks-rd runs.
37
37
  import { buildContext } from '../../services/context/context-builder.js';
38
38
  // Plan 1 / Task 10 — production fetcher (replaces mockFetcher).
39
- import { createHeadroomFetcher } from '../../services/context/headroom-fetcher.js';
40
- function buildHeadroomFetcher(sid) {
41
- return createHeadroomFetcher({
39
+ import { createDocCacheFetcher } from '../../services/context/doc-cache-fetcher.js';
40
+ function buildDocFetcher(sid) {
41
+ return createDocCacheFetcher({
42
42
  cacheDir: `.peaks/_runtime/${sid}/doc-cache`,
43
- // remoteFetcher wired in a future slice (headroom-ai programmatic API).
43
+ // remoteFetcher wired in a future slice.
44
44
  });
45
45
  }
46
46
  async function ensureContextForRd(goal, project, sid) {
@@ -53,7 +53,7 @@ async function ensureContextForRd(goal, project, sid) {
53
53
  depsMode: 'locked',
54
54
  docBudgetTokens: 8000,
55
55
  out,
56
- fetcher: buildHeadroomFetcher(sid),
56
+ fetcher: buildDocFetcher(sid),
57
57
  });
58
58
  }
59
59
  catch (error) {
@@ -72,10 +72,10 @@ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusa
72
72
  * index, race the worktree release, and can corrupt the
73
73
  * caller's working branch.
74
74
  *
75
- * These rules are appended to the system prompt on top of the L1
76
- * worktree governance block so the sub-agent sees them last (i.e.
77
- * most-recently-read), which is the strongest prompt position in
78
- * transformer attention.
75
+ * These rules are placed immediately after the L1 worktree governance
76
+ * block so every stable boilerplate block is contiguous at the prompt
77
+ * start, maximizing Anthropic prompt-cache prefix reuse (stable-first
78
+ * ordering).
79
79
  */
80
80
  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";
81
81
  /**
@@ -62,10 +62,10 @@ If the upstream superpowers chain suggests raw \`git worktree add\`:
62
62
  * index, race the worktree release, and can corrupt the
63
63
  * caller's working branch.
64
64
  *
65
- * These rules are appended to the system prompt on top of the L1
66
- * worktree governance block so the sub-agent sees them last (i.e.
67
- * most-recently-read), which is the strongest prompt position in
68
- * transformer attention.
65
+ * These rules are placed immediately after the L1 worktree governance
66
+ * block so every stable boilerplate block is contiguous at the prompt
67
+ * start, maximizing Anthropic prompt-cache prefix reuse (stable-first
68
+ * ordering).
69
69
  */
70
70
  export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
71
71
 
@@ -97,9 +97,9 @@ export function buildDispatchSystemPrompt(input) {
97
97
  const { taskBody, memoryBlock, contextProbe } = input;
98
98
  const contextBlock = renderContextBlock(contextProbe ?? null);
99
99
  if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
100
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${contextBlock}${memoryBlock.block}\n## Task\n${taskBody}\n${LIFECYCLE_RULES}`;
100
+ return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${memoryBlock.block}\n## Task\n${taskBody}`;
101
101
  }
102
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${contextBlock}${taskBody}\n${LIFECYCLE_RULES}`;
102
+ return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${taskBody}`;
103
103
  }
104
104
  /**
105
105
  * Slice 2026-07-29-context-evaluation-accuracy: emit a
@@ -8,13 +8,11 @@
8
8
  *
9
9
  * Decision codes:
10
10
  * - `OK` — under 50%
11
- * - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
12
- * - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
11
+ * - `CONTEXT_SOFT_WARN` — 50-75%, prompt is large
12
+ * - `CONTEXT_NEAR_LIMIT` — 75-80%, prompt is near the limit
13
13
  * - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
14
14
  * - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
15
15
  * - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
16
- *
17
- * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
18
16
  */
19
17
  import { tierToCode, type ThresholdEvaluation } from './threshold.js';
20
18
  export type ContextGuardCode = 'OK' | 'CONTEXT_SOFT_WARN' | 'CONTEXT_NEAR_LIMIT' | 'PROMPT_TOO_LARGE' | 'PROMPT_EMERGENCY' | 'FORCED_OVER_THRESHOLD';
@@ -8,17 +8,15 @@
8
8
  *
9
9
  * Decision codes:
10
10
  * - `OK` — under 50%
11
- * - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
12
- * - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
11
+ * - `CONTEXT_SOFT_WARN` — 50-75%, prompt is large
12
+ * - `CONTEXT_NEAR_LIMIT` — 75-80%, prompt is near the limit
13
13
  * - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
14
14
  * - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
15
15
  * - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
16
- *
17
- * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
18
16
  */
19
17
  import { CONTEXT_CAPACITY_DEFAULT_BYTES, evaluateThresholdTier, tierToCode } from './threshold.js';
20
- const NEAR_LIMIT_SUGGEST = 'Consider --use-headroom to compress prompt.';
21
- const SOFT_WARN_SUGGEST = 'Use --use-headroom to compress prompt proactively.';
18
+ const NEAR_LIMIT_SUGGEST = 'Prompt is near the context limit; trim the prompt or split into multiple dispatches.';
19
+ const SOFT_WARN_SUGGEST = 'Prompt is large; trim or split into multiple dispatches to stay within context budget.';
22
20
  const HARD_REJECT_SUGGEST = 'Trim prompt to < 80% of context capacity. Pass --force at CLI to override (NOT allowed at hook layer).';
23
21
  const EMERGENCY_SUGGEST = 'Prompt exceeds 90% of context. Trim aggressively or split into multiple dispatches.';
24
22
  /**
@@ -1,6 +1,6 @@
1
1
  import type { DocFetcher } from './doc-retriever.js';
2
- export interface HeadroomFetcherOptions {
2
+ export interface DocCacheFetcherOptions {
3
3
  readonly cacheDir: string;
4
4
  readonly remoteFetcher?: DocFetcher;
5
5
  }
6
- export declare function createHeadroomFetcher(opts: HeadroomFetcherOptions): DocFetcher;
6
+ export declare function createDocCacheFetcher(opts: DocCacheFetcherOptions): DocFetcher;
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Production DocFetcher. Uses existing headroom-ai dependency for remote
3
- * fetch; local cache (per-session, per-dep markdown) preferred when version matches.
2
+ * Production DocFetcher. Local cache (per-session, per-dep markdown)
3
+ * preferred when version matches; otherwise delegates to an injected
4
+ * remote fetcher.
4
5
  *
5
6
  * Hard constraint H2 (locked version): never returns a doc whose version
6
7
  * differs from the requested locked version.
@@ -12,7 +13,7 @@
12
13
  */
13
14
  import { readFile } from 'node:fs/promises';
14
15
  import { join } from 'node:path';
15
- export function createHeadroomFetcher(opts) {
16
+ export function createDocCacheFetcher(opts) {
16
17
  return async (dep, version) => {
17
18
  const cachePath = join(opts.cacheDir, `${dep}@${version}.md`);
18
19
  try {
@@ -8,7 +8,7 @@
8
8
  * - MemoryIndexReader (Task 3) — reads .peaks/memory/index.json + layer=A filter
9
9
  * - MemoryLruCache (Task 2) — constructed for capacity parity, but NOT used
10
10
  * for memo content (see deviation note below)
11
- * - compressPrompt (existing) — headroom-ai wrapper for hard-cap compression
11
+ * - truncateToCap — byte-cap truncation of the composed block
12
12
  *
13
13
  * Deviation from brief — controller-accepted (pre-task 4):
14
14
  * The brief mandated use of `MemoryLruCache` for memo content caching. The
@@ -20,25 +20,14 @@
20
20
  * `MemoryLruCache` class remains as a separate, reusable LRU primitive
21
21
  * (Task 2) and is not consumed by this service.
22
22
  */
23
- import { compressPrompt } from './headroom-client.js';
24
23
  import { MemoryIndexReader } from './memory-index-reader.js';
25
24
  import { resolveMemoryPreflightConfig, } from './memory-preflight-config.js';
26
- async function compressToCap(text, capBytes, mode) {
27
- try {
28
- const result = await compressPrompt(text, mode);
29
- if (result.warning !== null || result.compressedPrompt === null) {
30
- return { text, truncated: false };
31
- }
32
- const compressed = result.compressedPrompt;
33
- if (Buffer.byteLength(compressed, 'utf8') > capBytes) {
34
- const sliced = compressed.slice(0, Math.max(0, capBytes - 64)) + '\n…[truncated]';
35
- return { text: sliced, truncated: true };
36
- }
37
- return { text: compressed, truncated: false };
38
- }
39
- catch {
25
+ function truncateToCap(text, capBytes) {
26
+ if (Buffer.byteLength(text, 'utf8') <= capBytes) {
40
27
  return { text, truncated: false };
41
28
  }
29
+ const sliced = text.slice(0, Math.max(0, capBytes - 64)) + '\n…[truncated]';
30
+ return { text: sliced, truncated: true };
42
31
  }
43
32
  export class MemoryPreflightService {
44
33
  reader;
@@ -85,7 +74,7 @@ export class MemoryPreflightService {
85
74
  const header = '## Project memory relevant to this task\n';
86
75
  const composed = `${header}${listLines}${tail}`;
87
76
  const capBytes = Math.max(64, this.config.maxTokens * 4);
88
- const { text, truncated } = await compressToCap(composed, capBytes, 'balanced');
77
+ const { text, truncated } = truncateToCap(composed, capBytes);
89
78
  const droppedCount = truncated ? selected.length - countItemsInBlock(text) : 0;
90
79
  return {
91
80
  available: true,
@@ -4,9 +4,6 @@
4
4
  * 256K default context capacity is a conservative proxy. The LLM's real
5
5
  * capacity is its private business (R-1 / R-8 / R-10 / R-13 boundary
6
6
  * inherited from slice #009); we use prompt size as the gate signal.
7
- *
8
- * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`
9
- * for the full G9 rule (RL-27..RL-32, AC-50..AC-65).
10
7
  */
11
8
  export declare const CONTEXT_CAPACITY_DEFAULT_BYTES: number;
12
9
  export declare const THRESHOLD_SOFT_WARN_RATIO = 0.5;
@@ -4,9 +4,6 @@
4
4
  * 256K default context capacity is a conservative proxy. The LLM's real
5
5
  * capacity is its private business (R-1 / R-8 / R-10 / R-13 boundary
6
6
  * inherited from slice #009); we use prompt size as the gate signal.
7
- *
8
- * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`
9
- * for the full G9 rule (RL-27..RL-32, AC-50..AC-65).
10
7
  */
11
8
  export const CONTEXT_CAPACITY_DEFAULT_BYTES = 256 * 1024; // 256K
12
9
  export const THRESHOLD_SOFT_WARN_RATIO = 0.5; // 50%
@@ -5,7 +5,7 @@
5
5
  * Above 6, the LLM / human is encouraged to split into multiple
6
6
  * batches with an explicit reducer step in between. peaks-code's
7
7
  * swarm phase dispatches 3; peaks-rd's 4-way fan-out dispatches 4;
8
- * peaks-qa's 3-way fan-out dispatches 3. The 6 limit leaves headroom
8
+ * peaks-qa's 3-way fan-out dispatches 3. The 6 limit leaves margin
9
9
  * for "qa-business-api" / "qa-business-frontend" / "qa-business-regression"
10
10
  * subdivisions (3-way + 3-way = 6) without crossing the line.
11
11
  *
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Threshold: 1h (configurable). Rationale: the longest empirical
12
12
  * peaks-rd / peaks-qa fan-out + reducer cycle is < 60s; the threshold
13
- * gives slow slices headroom without hiding leaks for the next session.
13
+ * gives slow slices margin without hiding leaks for the next session.
14
14
  */
15
15
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generic fzf binary picker (slice 2026-06-14-fzf-headroom-rollout).
2
+ * Generic fzf binary picker (slice 2026-06-14-fzf-rollout).
3
3
  *
4
4
  * Promoted from `src/services/slice/slice-pick-service.ts` (which is
5
5
  * the only prior caller). Encapsulates the canonical fzf integration
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generic fzf binary picker (slice 2026-06-14-fzf-headroom-rollout).
2
+ * Generic fzf binary picker (slice 2026-06-14-fzf-rollout).
3
3
  *
4
4
  * Promoted from `src/services/slice/slice-pick-service.ts` (which is
5
5
  * the only prior caller). Encapsulates the canonical fzf integration
@@ -35,10 +35,9 @@
35
35
  *
36
36
  * Why 4-bytes-per-token:
37
37
  *
38
- * - `headroom-client.ts:60` already uses `BYTES_PER_TOKEN = 4` as
39
- * its rough English-text approximation. Reusing the same constant
40
- * keeps token estimates comparable across the headroom + rerank
41
- * pipelines in the AC-ZA-5 benchmark.
38
+ * - `BYTES_PER_TOKEN = 4` is the standard rough English-text
39
+ * approximation (1 token ≈ 4 bytes). It keeps token estimates
40
+ * stable and comparable in the AC-ZA-5 benchmark.
42
41
  *
43
42
  * Out of scope (YAGNI per Karpathy #2 Simplicity First):
44
43
  *
@@ -112,7 +111,7 @@ export interface RerankResult {
112
111
  }
113
112
  /**
114
113
  * Estimate token count for a string. `1 token ≈ 4 bytes` for English
115
- * text — same approximation as `headroom-client.ts:60`.
114
+ * text (standard rough approximation).
116
115
  */
117
116
  export declare function estimateTokens(text: string): number;
118
117
  /**