peaks-loop 4.0.25 → 4.0.27

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 (63) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/commands/code-runtime-commands.js +7 -1
  3. package/dist/cli/commands/core/memory-command.js +1 -3
  4. package/dist/cli/commands/dispatch-commands.js +14 -67
  5. package/dist/cli/commands/memory-commands.d.ts +0 -2
  6. package/dist/cli/commands/memory-commands.js +4 -7
  7. package/dist/cli/commands/preferences-commands.js +0 -1
  8. package/dist/cli/commands/qa-commands.js +5 -5
  9. package/dist/cli/commands/sub-agent-shared.d.ts +0 -4
  10. package/dist/cli/commands/sub-agent-shared.js +0 -14
  11. package/dist/cli/commands/workflow-commands.js +5 -5
  12. package/dist/services/code/auto-compact-orchestrator.js +3 -0
  13. package/dist/services/context/auto-compact-reader.d.ts +45 -49
  14. package/dist/services/context/auto-compact-reader.js +21 -148
  15. package/dist/services/context/auto-compact-types.d.ts +17 -2
  16. package/dist/services/context/build-dispatch-system-prompt.d.ts +4 -4
  17. package/dist/services/context/build-dispatch-system-prompt.js +6 -6
  18. package/dist/services/context/context-guard.d.ts +2 -4
  19. package/dist/services/context/context-guard.js +4 -6
  20. package/dist/services/context/{headroom-fetcher.d.ts → doc-cache-fetcher.d.ts} +2 -2
  21. package/dist/services/context/{headroom-fetcher.js → doc-cache-fetcher.js} +4 -3
  22. package/dist/services/context/memory-preflight-service.js +6 -17
  23. package/dist/services/context/threshold.d.ts +0 -3
  24. package/dist/services/context/threshold.js +0 -3
  25. package/dist/services/dispatch/batch-counter.js +1 -1
  26. package/dist/services/dispatch/leak-detector.js +1 -1
  27. package/dist/services/fuzzy-matching/fzf-pick-service.d.ts +1 -1
  28. package/dist/services/fuzzy-matching/fzf-pick-service.js +1 -1
  29. package/dist/services/ide/adapters/claude-code-adapter.d.ts +12 -12
  30. package/dist/services/ide/adapters/claude-code-adapter.js +290 -1
  31. package/dist/services/ide/ide-types.d.ts +39 -0
  32. package/dist/services/memory/llm-reranker.d.ts +4 -5
  33. package/dist/services/memory/llm-reranker.js +6 -8
  34. package/dist/services/memory/memory-search-service.d.ts +0 -32
  35. package/dist/services/memory/memory-search-service.js +0 -37
  36. package/dist/services/preferences/preferences-service.d.ts +5 -8
  37. package/dist/services/preferences/preferences-service.js +0 -30
  38. package/dist/services/preferences/preferences-types.d.ts +0 -22
  39. package/dist/services/preferences/preferences-types.js +0 -12
  40. package/dist/services/retrospective/retrospective-search-service.d.ts +0 -18
  41. package/dist/services/retrospective/retrospective-search-service.js +0 -32
  42. package/dist/services/session/binding-status-service.d.ts +11 -0
  43. package/dist/services/session/binding-status-service.js +25 -4
  44. package/dist/services/skill/skill-search-service.d.ts +1 -1
  45. package/dist/services/slice/slice-benchmark-service.d.ts +1 -1
  46. package/dist/services/slice/slice-benchmark-service.js +1 -1
  47. package/dist/services/slice/slice-pick-service.d.ts +1 -1
  48. package/dist/services/slice/slice-pick-service.js +1 -1
  49. package/package.json +5 -6
  50. package/skills/bee/peaks-qa/references/qa-context-governance.md +1 -1
  51. package/skills/bee/peaks-rd/SKILL.md +2 -2
  52. package/skills/bee/peaks-rd/references/rd-context-governance.md +2 -2
  53. package/skills/bee/peaks-txt/SKILL.md +1 -1
  54. package/skills/bee/peaks-ui/SKILL.md +1 -1
  55. package/skills/peaks-code/SKILL.md +1 -1
  56. package/skills/peaks-code/references/context-governance.md +7 -34
  57. package/skills/peaks-code/references/envelope-contract.md +2 -7
  58. package/skills/peaks-code/references/sub-agent-dispatch.md +2 -4
  59. package/dist/services/context/headroom-client.d.ts +0 -34
  60. package/dist/services/context/headroom-client.js +0 -117
  61. package/dist/services/context/headroom-prefs.d.ts +0 -46
  62. package/dist/services/context/headroom-prefs.js +0 -34
  63. package/skills/peaks-code/references/headroom-integration.md +0 -107
@@ -1,3 +1,4 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
1
2
  import { homedir } from 'node:os';
2
3
  import { join, resolve } from 'node:path';
3
4
  import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
@@ -17,6 +18,293 @@ import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatche
17
18
  *
18
19
  * 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
19
20
  */
21
+ /**
22
+ * Read Claude Code's statusline state file
23
+ * (`~/.claude/statusline-state.json`) and parse a context-percent key.
24
+ * Moved from the generic reader in slice
25
+ * 2026-09-02-vendor-neutral-context-probe — Claude-specific paths now live
26
+ * only in the Claude Code adapter.
27
+ */
28
+ function readClaudeStatuslinePercent() {
29
+ const path = join(homedir(), '.claude', 'statusline-state.json');
30
+ if (!existsSync(path))
31
+ return null;
32
+ try {
33
+ const json = JSON.parse(readFileSync(path, 'utf8'));
34
+ const candidates = ['contextPercent', 'context_usage_percent', 'contextPercentUsed'];
35
+ for (const key of candidates) {
36
+ const raw = json[key];
37
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
38
+ return raw > 1.5 ? raw / 100 : Math.max(0, Math.min(1, raw));
39
+ }
40
+ }
41
+ }
42
+ catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
43
+ if (err instanceof ReferenceError)
44
+ throw err; // surface module-load bugs
45
+ if (err instanceof SyntaxError)
46
+ throw err; // surface parse bugs (e.g. broken statusline JSON)
47
+ return null; // only swallow IO errors
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Recursive search for `<outerSessionId>.jsonl` under `projectsDir`. The
53
+ * Mac layout encodes the cwd as a single hash directory; on Mac Claude Code
54
+ * nests the transcript under that hash with an extra level of subdirectory we
55
+ * cannot predict ahead of time. A flat readdir misses that branch and returns
56
+ * null — the silent-failure mode this recursion closes.
57
+ *
58
+ * Moved from the generic reader in slice
59
+ * 2026-09-02-vendor-neutral-context-probe. The lookup key is the OUTER
60
+ * session id (Claude Code names its transcript by the outer session UUID),
61
+ * NOT the peaks session id.
62
+ */
63
+ function findTranscriptJsonl(projectsDir, outerSessionId) {
64
+ if (!existsSync(projectsDir))
65
+ return null;
66
+ try {
67
+ const stack = [projectsDir];
68
+ while (stack.length > 0) {
69
+ const dir = stack.pop();
70
+ if (dir === undefined)
71
+ break;
72
+ const entries = readdirSync(dir, { withFileTypes: true });
73
+ for (const entry of entries) {
74
+ const full = join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ stack.push(full);
77
+ }
78
+ else if (entry.isFile() && entry.name === `${outerSessionId}.jsonl`) {
79
+ return full;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
85
+ if (err instanceof ReferenceError)
86
+ throw err; // surface module-load bugs
87
+ if (err instanceof SyntaxError)
88
+ throw err; // surface parse bugs
89
+ return null; // only swallow IO errors
90
+ }
91
+ return null;
92
+ }
93
+ /** 1M-context window size in tokens (documented single choice: 1,000,000). */
94
+ const ONE_MILLION_CONTEXT_TOKENS = 1_000_000;
95
+ /** Safe-default (non-1M) context window size in tokens. */
96
+ const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
97
+ /** Reverse-scan chunk size in bytes (keeps memory bounded on multi-MB transcripts). */
98
+ const TRANSCRIPT_SCAN_CHUNK_BYTES = 64 * 1024;
99
+ /**
100
+ * Known 1M-context Claude model id prefixes whose ids do NOT carry a `1m`
101
+ * suffix (e.g. `claude-sonnet-4-5-20250929`). The substring match is
102
+ * intentionally generous — every `claude-sonnet-4*` / `claude-opus-4*`
103
+ * variant is 1M-context.
104
+ */
105
+ const ONE_MILLION_CONTEXT_MODELS = ['claude-opus-4', 'claude-sonnet-4'];
106
+ /**
107
+ * Model-aware context-window size in tokens.
108
+ *
109
+ * Detection rule (documented):
110
+ * 1. Empty / unknown model → DEFAULT_CONTEXT_WINDOW_TOKENS (200_000).
111
+ * 2. Suffix heuristic — a model id containing `1m` (case-insensitive) is
112
+ * treated as 1M-context.
113
+ * 3. Explicit allowlist — known 1M Claude model ids
114
+ * (ONE_MILLION_CONTEXT_MODELS).
115
+ * 4. Everything else → 200_000 (the safe default).
116
+ *
117
+ * Callers MAY additionally infer ≥1M from the observed token count: if
118
+ * `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
119
+ * 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
120
+ */
121
+ export function modelContextWindowTokens(model) {
122
+ const m = model.trim().toLowerCase();
123
+ if (m.length === 0)
124
+ return DEFAULT_CONTEXT_WINDOW_TOKENS;
125
+ if (m.includes('1m'))
126
+ return ONE_MILLION_CONTEXT_TOKENS;
127
+ for (const known of ONE_MILLION_CONTEXT_MODELS) {
128
+ if (m.includes(known))
129
+ return ONE_MILLION_CONTEXT_TOKENS;
130
+ }
131
+ return DEFAULT_CONTEXT_WINDOW_TOKENS;
132
+ }
133
+ /** A non-negative finite number, or null when the value is not numeric. */
134
+ function numericTokenCount(value) {
135
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
136
+ return value;
137
+ return null;
138
+ }
139
+ /**
140
+ * Parse a single jsonl line into its token count + model id. Returns null
141
+ * when the line has no `message.usage` object with numeric token fields.
142
+ */
143
+ function parseTranscriptUsageLine(line) {
144
+ if (line.length === 0)
145
+ return null;
146
+ let json;
147
+ try {
148
+ json = JSON.parse(line);
149
+ }
150
+ catch {
151
+ return null; // non-JSON line (blank / corrupt) — skip
152
+ }
153
+ if (typeof json !== 'object' || json === null)
154
+ return null;
155
+ const record = json;
156
+ const message = record.message;
157
+ if (typeof message !== 'object' || message === null)
158
+ return null;
159
+ const msg = message;
160
+ const usage = msg.usage;
161
+ if (typeof usage !== 'object' || usage === null)
162
+ return null;
163
+ const u = usage;
164
+ const inputTokens = numericTokenCount(u.input_tokens);
165
+ const cacheRead = numericTokenCount(u.cache_read_input_tokens);
166
+ const cacheCreation = numericTokenCount(u.cache_creation_input_tokens);
167
+ if (inputTokens === null && cacheRead === null && cacheCreation === null)
168
+ return null;
169
+ const contextTokens = (inputTokens ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0);
170
+ // Model id lives at `message.model`, falling back to a top-level `model`.
171
+ const model = typeof msg.model === 'string'
172
+ ? msg.model
173
+ : typeof record.model === 'string' ? record.model : '';
174
+ return { contextTokens, model };
175
+ }
176
+ /**
177
+ * Reverse-scan the transcript jsonl (from the END) for the LATEST entry that
178
+ * carries a numeric `message.usage`. The file can be many MB; it is read in
179
+ * backward chunks of TRANSCRIPT_SCAN_CHUNK_BYTES — never fully into memory —
180
+ * and stops at the first (newest) usable entry.
181
+ */
182
+ function findLatestTranscriptUsage(filePath) {
183
+ let fd = null;
184
+ try {
185
+ const size = statSync(filePath).size;
186
+ if (size === 0)
187
+ return null;
188
+ fd = openSync(filePath, 'r');
189
+ let position = size;
190
+ let carry = ''; // partial line head carried into the next (older) chunk
191
+ while (position > 0) {
192
+ const readLen = Math.min(TRANSCRIPT_SCAN_CHUNK_BYTES, position);
193
+ position -= readLen;
194
+ const buf = Buffer.alloc(readLen);
195
+ const bytesRead = readSync(fd, buf, 0, readLen, position);
196
+ if (bytesRead <= 0)
197
+ break;
198
+ const lines = (buf.toString('utf8', 0, bytesRead) + carry).split('\n');
199
+ carry = lines[0] ?? '';
200
+ for (let i = lines.length - 1; i >= 1; i--) {
201
+ const line = lines[i];
202
+ if (line === undefined)
203
+ continue;
204
+ const parsed = parseTranscriptUsageLine(line);
205
+ if (parsed !== null)
206
+ return parsed;
207
+ }
208
+ }
209
+ // The final carry is the first line of the file (complete, since it starts at byte 0).
210
+ if (carry.length > 0) {
211
+ const parsed = parseTranscriptUsageLine(carry);
212
+ if (parsed !== null)
213
+ return parsed;
214
+ }
215
+ return null;
216
+ }
217
+ catch (err) {
218
+ // Narrow: surface module-load / parse bugs, swallow IO errors only
219
+ // (mirrors the other adapter read helpers' catch discipline).
220
+ if (err instanceof ReferenceError)
221
+ throw err;
222
+ if (err instanceof SyntaxError)
223
+ throw err;
224
+ return null;
225
+ }
226
+ finally {
227
+ if (fd !== null) {
228
+ try {
229
+ closeSync(fd);
230
+ }
231
+ catch { /* best-effort */ }
232
+ }
233
+ }
234
+ }
235
+ /**
236
+ * Resolve the context window for a transcript usage entry. Prefers the
237
+ * explicit `modelContextWindowTokens(model)` mapping; when the observed token
238
+ * count contradicts it (tokens exceed the mapped window), the model must be
239
+ * ≥1M, so bump to the 1M window.
240
+ */
241
+ function resolveContextWindowTokens(model, contextTokens) {
242
+ const window = modelContextWindowTokens(model);
243
+ return contextTokens > window ? ONE_MILLION_CONTEXT_TOKENS : window;
244
+ }
245
+ /**
246
+ * Conservative transcript-estimate fallback. Recursively searches
247
+ * `~/.claude/projects/<hash>/<outerSessionId-or-nested>.jsonl` (Mac may nest
248
+ * the jsonl under an extra directory we cannot predict ahead of time) and
249
+ * estimates `ratio = contextTokens / contextWindowTokens` from the LATEST
250
+ * `message.usage` entry — token-based + model-aware, NOT the old
251
+ * `bytes / 256KB` (which over-fired because the transcript grows unboundedly).
252
+ * Tagged `'transcript-estimate'` (v2.14.0) so callers know it is a real
253
+ * signal, NOT a hard gate.
254
+ */
255
+ function readClaudeTranscriptEstimate(outerSessionId) {
256
+ const projectsDir = join(homedir(), '.claude', 'projects');
257
+ const path = findTranscriptJsonl(projectsDir, outerSessionId);
258
+ if (path === null)
259
+ return null;
260
+ const latest = findLatestTranscriptUsage(path);
261
+ if (latest === null)
262
+ return null;
263
+ const contextWindowTokens = resolveContextWindowTokens(latest.model, latest.contextTokens);
264
+ const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
265
+ return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
266
+ }
267
+ /**
268
+ * Claude Code's vendor-specific context-percent fallback, exposed as
269
+ * `IdeCompactProfile.readContextPercentFallback`. The generic reader calls
270
+ * this only when the primary env-var probe misses; only the adapter knows the
271
+ * Claude-specific statusline + transcript paths. Resolution order:
272
+ * 1. statusline poll (`~/.claude/statusline-state.json`)
273
+ * 2. transcript estimate — looks up `<outerSessionId>.jsonl` under
274
+ * `~/.claude/projects/<hash>/...` using the OUTER session id (Claude
275
+ * names its transcript by the outer session UUID, not the peaks sid),
276
+ * and estimates `contextTokens / contextWindowTokens` from the LATEST
277
+ * `message.usage` entry (token-based + model-aware).
278
+ * Returns `null` when neither yields a signal → the reader emits
279
+ * `conservative-fallback`.
280
+ */
281
+ function readContextPercentFallback(input) {
282
+ const capturedAt = new Date().toISOString();
283
+ // Byte-based capacity is carried only for the percent path (statusline-poll
284
+ // returns a 0..1 ratio; capacityBytes is metadata there). The
285
+ // transcript-estimate path is token-based and surfaces `capacityTokens`
286
+ // (the model window) instead — see readClaudeTranscriptEstimate.
287
+ const capacityBytes = 256 * 1024;
288
+ const ide = 'claude-code';
289
+ const statusline = readClaudeStatuslinePercent();
290
+ if (statusline !== null) {
291
+ return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide, capturedAt };
292
+ }
293
+ if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
294
+ const estimate = readClaudeTranscriptEstimate(input.outerSessionId);
295
+ if (estimate !== null) {
296
+ return {
297
+ ratio: estimate.ratio,
298
+ source: 'transcript-estimate',
299
+ rawTokens: estimate.contextTokens,
300
+ capacityTokens: estimate.contextWindowTokens,
301
+ ide,
302
+ capturedAt
303
+ };
304
+ }
305
+ }
306
+ return null;
307
+ }
20
308
  export const CLAUDE_CODE_ADAPTER = {
21
309
  id: 'claude-code',
22
310
  displayName: 'Claude Code',
@@ -71,7 +359,8 @@ export const CLAUDE_CODE_ADAPTER = {
71
359
  envVarForContextPercent: 'CLAUDE_CONTEXT_USAGE_PERCENT',
72
360
  compactCommand: 'claude --compact',
73
361
  compactPathway: 'ide-native',
74
- postCompactDetectCommand: 'peaks compact auto --json'
362
+ postCompactDetectCommand: 'peaks compact auto --json',
363
+ readContextPercentFallback
75
364
  },
76
365
  // Slice #011: standards profile. Claude Code reads its constitution at
77
366
  // CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
@@ -12,6 +12,7 @@
12
12
  * 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。
13
13
  */
14
14
  import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js';
15
+ import type { ContextPercentProbe } from '../context/auto-compact-types.js';
15
16
  export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma' | 'hermes' | 'openclaw' | 'zcode';
16
17
  export interface IdeCapabilities {
17
18
  /** peaks gate enforce 是否适用该 IDE(必备) */
@@ -200,6 +201,44 @@ export interface IdeCompactProfile {
200
201
  * the orchestrator polls `envVarForContextPercent` directly.
201
202
  */
202
203
  readonly postCompactDetectCommand?: string;
204
+ /**
205
+ * Optional vendor-specific fallback the generic `readContextPercent`
206
+ * reader calls when the primary env-var probe misses. Returns a
207
+ * completed `ContextPercentProbe` (e.g. `statusline-poll` /
208
+ * `transcript-estimate`) or `null` when the adapter has no signal —
209
+ * the reader then falls through to `conservative-fallback`.
210
+ *
211
+ * Keeps IDE-specific filesystem / env knowledge (Claude Code's
212
+ * `~/.claude/statusline-state.json` + `~/.claude/projects` transcript
213
+ * layout, etc.) inside the adapter, not the generic reader. Adapters
214
+ * that do not opt in simply omit the field; new IDEs are addable
215
+ * without touching the generic reader.
216
+ *
217
+ * Added in slice 2026-09-02-vendor-neutral-context-probe.
218
+ */
219
+ readonly readContextPercentFallback?: (input: ContextPercentFallbackInput) => ContextPercentProbe | null;
220
+ }
221
+ /**
222
+ * Input the generic `readContextPercent` reader passes to an adapter's
223
+ * optional `IdeCompactProfile.readContextPercentFallback` hook. The adapter
224
+ * owns the vendor-specific fallback logic (statusline poll, transcript
225
+ * lookup, etc.); the generic reader stays IDE-agnostic.
226
+ *
227
+ * Added in slice 2026-09-02-vendor-neutral-context-probe.
228
+ */
229
+ export interface ContextPercentFallbackInput {
230
+ /** Project root (the probe's `--project` anchor). */
231
+ readonly projectRoot: string;
232
+ /** Peaks session id (NOT the harness / IDE transcript id). */
233
+ readonly sessionId: string;
234
+ /**
235
+ * Outer (harness / IDE) session id — the id the IDE uses to name its
236
+ * transcript / session files (e.g. a UUID). Optional: when unresolved,
237
+ * adapters whose fallback depends on it should return `null`.
238
+ */
239
+ readonly outerSessionId?: string | undefined;
240
+ /** Injectable env (defaults to process.env in the reader). */
241
+ readonly env?: NodeJS.ProcessEnv | undefined;
203
242
  }
204
243
  /**
205
244
  * Per-IDE standards-file location + format profile. Used by the
@@ -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
  /**
@@ -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
  *
@@ -49,9 +48,8 @@
49
48
  * - No integration with `memory-search-service.ts` (Z-A is a spike;
50
49
  * the wiring into `peaks memory search` is Z-B).
51
50
  */
52
- // Approximate 1 token = 4 bytes for English text. Matches
53
- // `headroom-client.ts:60` so the AC-ZA-5 benchmark can compare
54
- // rerank input cost against headroom compressed cost directly.
51
+ // Approximate 1 token = 4 bytes for English text. Standard rough
52
+ // approximation used across the pipeline (AC-ZA-5 benchmark).
55
53
  const BYTES_PER_TOKEN = 4;
56
54
  // Hard caps so a pathological query / 60-candidate list cannot blow
57
55
  // up the LLM prompt. These are conservative defaults; Z-B may tune.
@@ -62,7 +60,7 @@ const MAX_DESCRIPTION_CHARS = 240;
62
60
  const DEFAULT_CHAT_TIMEOUT_MS = 5_000;
63
61
  /**
64
62
  * Estimate token count for a string. `1 token ≈ 4 bytes` for English
65
- * text — same approximation as `headroom-client.ts:60`.
63
+ * text (standard rough approximation).
66
64
  */
67
65
  export function estimateTokens(text) {
68
66
  return Math.ceil(Buffer.byteLength(text, 'utf8') / BYTES_PER_TOKEN);
@@ -59,35 +59,3 @@ export declare function loadMemoryIndex(projectRoot: string): MemoryIndexSnapsho
59
59
  * spec §Component Details).
60
60
  */
61
61
  export declare function searchMemory(input: MemorySearchInput): MemorySearchResult[];
62
- /**
63
- * Envelope for the optional headroom-compressed version of the search
64
- * result text. The compressed text is intended for the LLM-side prompt
65
- * (it preserves the entry names + descriptions in a more token-efficient
66
- * form); the structured `matches` array is unchanged and remains the
67
- * machine-readable form. `null` when compression was not requested,
68
- * did not run, or failed (see `warning` for the reason).
69
- */
70
- export interface CompressedResultsEnvelope {
71
- readonly mode: 'balanced' | 'aggressive' | 'conservative';
72
- readonly originalSize: number;
73
- readonly compressedSize: number;
74
- readonly compressionRatio: number;
75
- readonly compressedText: string;
76
- readonly warning: string | null;
77
- }
78
- export interface MemorySearchWithResults {
79
- readonly matches: MemorySearchResult[];
80
- readonly compressedResults: CompressedResultsEnvelope | null;
81
- }
82
- export interface MemorySearchOptions {
83
- /** When true, read preferences.headroom.perTouchpoint.memorySearch and
84
- * compress joined match text via headroom-ai. Falls back silently to
85
- * the structured `matches` array when compression is unavailable. */
86
- readonly compressResults?: boolean;
87
- }
88
- /**
89
- * Run the search + optionally compress the joined match text via headroom.
90
- * The structured `matches` array is the canonical output. The
91
- * `compressedResults` is a derived view for LLM-side prompt assembly.
92
- */
93
- export declare function searchMemoryWithResults(input: MemorySearchInput, options?: MemorySearchOptions): Promise<MemorySearchWithResults>;
@@ -1,9 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fuzzyMatchWithKey } from '../fuzzy-matching/fuzzy-match-service.js';
4
- import { compressPrompt } from '../context/headroom-client.js';
5
- import { loadPreferences } from '../preferences/preferences-service.js';
6
- import { shouldCompressResults } from '../context/headroom-prefs.js';
7
4
  const DEFAULT_LIMIT = 6;
8
5
  /**
9
6
  * Read `.peaks/memory/index.json` and flatten the on-disk `hot[<kind>][]`
@@ -81,37 +78,3 @@ export function searchMemory(input) {
81
78
  };
82
79
  });
83
80
  }
84
- /**
85
- * Run the search + optionally compress the joined match text via headroom.
86
- * The structured `matches` array is the canonical output. The
87
- * `compressedResults` is a derived view for LLM-side prompt assembly.
88
- */
89
- export async function searchMemoryWithResults(input, options = {}) {
90
- const matches = searchMemory(input);
91
- if (options.compressResults !== true) {
92
- return { matches, compressedResults: null };
93
- }
94
- const projectRoot = input.projectRoot ?? process.cwd();
95
- const prefs = loadPreferences(projectRoot).headroom;
96
- const joinedText = matches.map((m) => `${m.name}: ${m.description}`).join('\n');
97
- const joinedBytes = Buffer.byteLength(joinedText, 'utf8');
98
- const decision = shouldCompressResults(prefs, joinedBytes, 'memorySearch');
99
- if (!decision.compress) {
100
- return { matches, compressedResults: null };
101
- }
102
- const result = await compressPrompt(joinedText, decision.mode);
103
- if (result.warning !== null || result.compressedPrompt === null) {
104
- return { matches, compressedResults: null };
105
- }
106
- return {
107
- matches,
108
- compressedResults: {
109
- mode: decision.mode,
110
- originalSize: result.originalSize,
111
- compressedSize: result.compressedSize,
112
- compressionRatio: result.compressionRatio,
113
- compressedText: result.compressedPrompt,
114
- warning: result.warning
115
- }
116
- };
117
- }
@@ -16,14 +16,11 @@ export declare function writeAtomic(filePath: string, prefs: ProjectPreferences)
16
16
  * current shape. Returns the migrated object + a list of changes
17
17
  * applied (for surfacing in the CLI envelope and the migration log).
18
18
  *
19
- * The v1 → v2 mapping fills in the `headroom.perTouchpoint` sub-keys
20
- * that v1 didn't track: each touchpoint gets the v1 `headroom.defaultMode`
21
- * (or 'balanced' if absent), and `compressMinBytes` is preserved
22
- * verbatim. v1's `agentShieldPrompt` and `loopAutonomousEnabled` were
23
- * already present, so they carry through unchanged. The `fanout` field
24
- * is new in v2 — it defaults to `{ defaultMode: 'fan-out' }` so the
25
- * pre-v2 behavior (peak-code SKILL instructed fan-out when ≥ 2 leaves)
26
- * is preserved.
19
+ * v1's `agentShieldPrompt` and `loopAutonomousEnabled` were already
20
+ * present, so they carry through unchanged. The `fanout` field is new
21
+ * in v2 — it defaults to `{ defaultMode: 'fan-out' }` so the pre-v2
22
+ * behavior (peak-code SKILL instructed fan-out when ≥ 2 leaves) is
23
+ * preserved.
27
24
  */
28
25
  export type MigrateResult = {
29
26
  readonly fromVersion: string;
@@ -135,25 +135,6 @@ export function migratePreferences(projectRoot, opts = {}) {
135
135
  };
136
136
  }
137
137
  const changes = [];
138
- // v1 → v2: headroom.perTouchpoint fill-in (v1 only had defaultMode).
139
- const headroomRaw = (raw.headroom ?? {});
140
- const defaultMode = isHeadroomModeString(headroomRaw.defaultMode)
141
- ? headroomRaw.defaultMode
142
- : 'balanced';
143
- const perTouchpoint = {
144
- subAgentDispatch: defaultMode,
145
- memorySearch: defaultMode,
146
- retrospectiveSearch: defaultMode,
147
- doctorScan: defaultMode,
148
- doctorRoute: 'conservative'
149
- };
150
- changes.push(`headroom.perTouchpoint filled in with defaultMode='${defaultMode}' (v1 lacked per-touchpoint overrides)`);
151
- if (typeof headroomRaw.compressMinBytes === 'number') {
152
- changes.push(`headroom.compressMinBytes preserved at ${headroomRaw.compressMinBytes}`);
153
- }
154
- else {
155
- changes.push(`headroom.compressMinBytes defaulted to 4096`);
156
- }
157
138
  // v1 → v2: fanout block was absent; v2 needs it. Default to the
158
139
  // pre-v2 behavior (fan-out when ≥ 2 leaves).
159
140
  if (raw.fanout === undefined) {
@@ -176,14 +157,6 @@ export function migratePreferences(projectRoot, opts = {}) {
176
157
  // fully-valid v2 ProjectPreferences.
177
158
  const migrated = mergePreferences(DEFAULT_PREFERENCES, {
178
159
  ...raw,
179
- headroom: {
180
- enabled: typeof headroomRaw.enabled === 'boolean' ? headroomRaw.enabled : true,
181
- defaultMode,
182
- perTouchpoint,
183
- compressMinBytes: typeof headroomRaw.compressMinBytes === 'number'
184
- ? headroomRaw.compressMinBytes
185
- : 4096
186
- },
187
160
  fanout: typeof raw.fanout === 'object' && raw.fanout !== null && !Array.isArray(raw.fanout)
188
161
  ? (raw.fanout.defaultMode === 'serial'
189
162
  ? { defaultMode: 'fan-out' }
@@ -203,6 +176,3 @@ export function migratePreferences(projectRoot, opts = {}) {
203
176
  written
204
177
  };
205
178
  }
206
- function isHeadroomModeString(value) {
207
- return value === 'balanced' || value === 'aggressive' || value === 'conservative';
208
- }
@@ -22,27 +22,6 @@ export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
22
22
  * - 'lax' — always downgrade to previous level (faster, riskier)
23
23
  */
24
24
  export type ClassifyConservatism = 'default' | 'strict' | 'lax';
25
- /**
26
- * Per-touchpoint headroom-AI mode override.
27
- * Spec §7.4 — default 'balanced'.
28
- */
29
- export type HeadroomMode = 'balanced' | 'aggressive' | 'conservative';
30
- export interface HeadroomPreferences {
31
- /** Whether headroom integration is enabled globally. Default: true */
32
- readonly enabled: boolean;
33
- /** Default mode if a touchpoint doesn't override. Default: 'balanced' */
34
- readonly defaultMode: HeadroomMode;
35
- /** Per-touchpoint mode overrides */
36
- readonly perTouchpoint: {
37
- subAgentDispatch: HeadroomMode;
38
- memorySearch: HeadroomMode;
39
- retrospectiveSearch: HeadroomMode;
40
- doctorScan: HeadroomMode;
41
- doctorRoute: HeadroomMode;
42
- };
43
- /** Minimum joined-result byte count before search-touchpoint compression runs. Default: 4096. */
44
- readonly compressMinBytes: number;
45
- }
46
25
  export interface ClassifyRuleOverrides {
47
26
  /** File count threshold above which a task is promoted to 'feature' */
48
27
  readonly feature_threshold_files?: number;
@@ -101,7 +80,6 @@ export interface ProjectPreferences {
101
80
  readonly agentShieldPrompt: UaPromptDecision;
102
81
  readonly classifyConservatism: ClassifyConservatism;
103
82
  readonly classifyRules: ClassifyRuleOverrides;
104
- readonly headroom: HeadroomPreferences;
105
83
  readonly swarmSpeculative: SwarmSpeculativePreferences;
106
84
  /** Loop Autonomous (L4 14.5) toggle. Default: false — never auto-enable. */
107
85
  readonly loopAutonomousEnabled: boolean;
@@ -28,18 +28,6 @@ export const DEFAULT_PREFERENCES = {
28
28
  feature_threshold_lines: 100,
29
29
  runtime_clean_grace_hours: 24,
30
30
  },
31
- headroom: {
32
- enabled: true,
33
- defaultMode: 'balanced',
34
- perTouchpoint: {
35
- subAgentDispatch: 'balanced',
36
- memorySearch: 'balanced',
37
- retrospectiveSearch: 'balanced',
38
- doctorScan: 'balanced',
39
- doctorRoute: 'conservative',
40
- },
41
- compressMinBytes: 4096,
42
- },
43
31
  swarmSpeculative: {
44
32
  enabled: true,
45
33
  maxConcurrent: 3,
@@ -35,21 +35,3 @@ export interface RetrospectiveSearchResult {
35
35
  * runs (cheaper to filter, then fuzzy on a smaller set).
36
36
  */
37
37
  export declare function searchRetrospective(input: RetrospectiveSearchInput): RetrospectiveSearchResult[];
38
- export interface CompressedRetrospectiveResultsEnvelope {
39
- readonly mode: 'balanced' | 'aggressive' | 'conservative';
40
- readonly originalSize: number;
41
- readonly compressedSize: number;
42
- readonly compressionRatio: number;
43
- readonly compressedText: string;
44
- readonly warning: string | null;
45
- }
46
- export interface RetrospectiveSearchWithResults {
47
- readonly matches: RetrospectiveSearchResult[];
48
- readonly compressedResults: CompressedRetrospectiveResultsEnvelope | null;
49
- }
50
- export interface RetrospectiveSearchOptions {
51
- /** When true, compress joined match text via headroom-ai. Falls back
52
- * silently to the structured `matches` array on failure. */
53
- readonly compressResults?: boolean;
54
- }
55
- export declare function searchRetrospectiveWithResults(input: RetrospectiveSearchInput, options?: RetrospectiveSearchOptions): Promise<RetrospectiveSearchWithResults>;