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.
- package/CHANGELOG.md +23 -0
- package/dist/cli/commands/code-runtime-commands.js +7 -1
- package/dist/cli/commands/core/memory-command.js +1 -3
- package/dist/cli/commands/dispatch-commands.js +14 -67
- package/dist/cli/commands/memory-commands.d.ts +0 -2
- package/dist/cli/commands/memory-commands.js +4 -7
- package/dist/cli/commands/preferences-commands.js +0 -1
- package/dist/cli/commands/qa-commands.js +5 -5
- package/dist/cli/commands/sub-agent-shared.d.ts +0 -4
- package/dist/cli/commands/sub-agent-shared.js +0 -14
- package/dist/cli/commands/workflow-commands.js +5 -5
- package/dist/services/code/auto-compact-orchestrator.js +3 -0
- package/dist/services/context/auto-compact-reader.d.ts +45 -49
- package/dist/services/context/auto-compact-reader.js +21 -148
- package/dist/services/context/auto-compact-types.d.ts +17 -2
- package/dist/services/context/build-dispatch-system-prompt.d.ts +4 -4
- package/dist/services/context/build-dispatch-system-prompt.js +6 -6
- package/dist/services/context/context-guard.d.ts +2 -4
- package/dist/services/context/context-guard.js +4 -6
- package/dist/services/context/{headroom-fetcher.d.ts → doc-cache-fetcher.d.ts} +2 -2
- package/dist/services/context/{headroom-fetcher.js → doc-cache-fetcher.js} +4 -3
- package/dist/services/context/memory-preflight-service.js +6 -17
- package/dist/services/context/threshold.d.ts +0 -3
- package/dist/services/context/threshold.js +0 -3
- package/dist/services/dispatch/batch-counter.js +1 -1
- package/dist/services/dispatch/leak-detector.js +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.d.ts +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.js +1 -1
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +12 -12
- package/dist/services/ide/adapters/claude-code-adapter.js +290 -1
- package/dist/services/ide/ide-types.d.ts +39 -0
- package/dist/services/memory/llm-reranker.d.ts +4 -5
- package/dist/services/memory/llm-reranker.js +6 -8
- package/dist/services/memory/memory-search-service.d.ts +0 -32
- package/dist/services/memory/memory-search-service.js +0 -37
- package/dist/services/preferences/preferences-service.d.ts +5 -8
- package/dist/services/preferences/preferences-service.js +0 -30
- package/dist/services/preferences/preferences-types.d.ts +0 -22
- package/dist/services/preferences/preferences-types.js +0 -12
- package/dist/services/retrospective/retrospective-search-service.d.ts +0 -18
- package/dist/services/retrospective/retrospective-search-service.js +0 -32
- package/dist/services/session/binding-status-service.d.ts +11 -0
- package/dist/services/session/binding-status-service.js +25 -4
- package/dist/services/skill/skill-search-service.d.ts +1 -1
- package/dist/services/slice/slice-benchmark-service.d.ts +1 -1
- package/dist/services/slice/slice-benchmark-service.js +1 -1
- package/dist/services/slice/slice-pick-service.d.ts +1 -1
- package/dist/services/slice/slice-pick-service.js +1 -1
- package/package.json +5 -6
- package/skills/bee/peaks-qa/references/qa-context-governance.md +1 -1
- package/skills/bee/peaks-rd/SKILL.md +2 -2
- package/skills/bee/peaks-rd/references/rd-context-governance.md +2 -2
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/context-governance.md +7 -34
- package/skills/peaks-code/references/envelope-contract.md +2 -7
- package/skills/peaks-code/references/sub-agent-dispatch.md +2 -4
- package/dist/services/context/headroom-client.d.ts +0 -34
- package/dist/services/context/headroom-client.js +0 -117
- package/dist/services/context/headroom-prefs.d.ts +0 -46
- package/dist/services/context/headroom-prefs.js +0 -34
- 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
|
-
* - `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
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
|
-
* - `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
53
|
-
//
|
|
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
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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>;
|