peaks-loop 4.0.33 → 4.0.35
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 +38 -0
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -3
- package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
- package/dist/cli/commands/code-mode-status-command.js +51 -0
- package/dist/cli/commands/code-run-command.js +8 -1
- package/dist/cli/commands/code-runtime-commands.js +13 -5
- package/dist/cli/commands/core/memory-command.js +61 -3
- package/dist/cli/commands/core/skill-command.js +1 -1
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/memory-commands.d.ts +35 -0
- package/dist/cli/commands/memory-commands.js +119 -10
- package/dist/cli/commands/request-commands.js +3 -7
- package/dist/cli/commands/session-24h-mode.js +9 -1
- package/dist/services/24h-mode/auto-engage.d.ts +39 -0
- package/dist/services/24h-mode/auto-engage.js +46 -0
- package/dist/services/24h-mode/decider.d.ts +11 -5
- package/dist/services/24h-mode/decider.js +13 -13
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
- package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
- package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
- package/dist/services/code/auto-compact-lifecycle.js +1 -1
- package/dist/services/code/auto-compact-modes.d.ts +1 -1
- package/dist/services/code/auto-compact-modes.js +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +18 -22
- package/dist/services/code/mode-gate.d.ts +6 -2
- package/dist/services/code/mode-gate.js +9 -5
- package/dist/services/code/orchestrator-can-do.js +2 -2
- package/dist/services/config/config-service.d.ts +17 -0
- package/dist/services/config/config-service.js +34 -0
- package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
- package/dist/services/context/auto-compact-dispatcher.js +4 -4
- package/dist/services/context/auto-compact-reader.js +8 -1
- package/dist/services/context/auto-compact-types.d.ts +13 -1
- package/dist/services/context/build-dispatch-system-prompt.js +2 -2
- package/dist/services/context/memory-index-reader.d.ts +26 -0
- package/dist/services/context/memory-index-reader.js +62 -30
- package/dist/services/context/memory-preflight-config.d.ts +33 -0
- package/dist/services/context/memory-preflight-config.js +32 -2
- package/dist/services/context/memory-preflight-service.d.ts +20 -1
- package/dist/services/context/memory-preflight-service.js +198 -31
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
- package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
- package/dist/services/hooks/auto-compact-hook-install.js +2 -2
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
- package/dist/services/ide/ide-types.d.ts +10 -1
- package/dist/services/job/job-types.d.ts +3 -3
- package/dist/services/memory/memory-ingest-service.d.ts +79 -0
- package/dist/services/memory/memory-ingest-service.js +225 -0
- package/dist/services/memory/memory-rotate-service.d.ts +88 -0
- package/dist/services/memory/memory-rotate-service.js +373 -0
- package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
- package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
- package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
- package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
- package/dist/services/memory/project-memory-service/index/search.js +14 -24
- package/dist/services/memory/project-memory-service/index.d.ts +7 -3
- package/dist/services/memory/project-memory-service/index.js +6 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
- package/dist/services/memory/project-memory-service/types.d.ts +31 -1
- package/dist/services/memory/project-memory-service/types.js +76 -1
- package/dist/services/mode/mode-enforcement.d.ts +4 -1
- package/dist/services/mode/mode-enforcement.js +26 -28
- package/dist/services/mode/mode-status-service.d.ts +81 -0
- package/dist/services/mode/mode-status-service.js +108 -0
- package/dist/services/preferences/preferences-types.d.ts +14 -0
- package/dist/services/preferences/preferences-types.js +8 -0
- package/dist/services/share/run-state-contract.d.ts +1 -1
- package/dist/services/skills/presence-lease-service.d.ts +21 -0
- package/dist/services/skills/presence-lease-service.js +26 -0
- package/dist/services/skills/skill-presence-service.d.ts +10 -1
- package/dist/services/skills/skill-presence-service.js +32 -4
- package/dist/services/skills/skill-statusline-service.js +6 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +10 -10
- package/skills/peaks-code/references/mode-selection.md +7 -3
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/runbook.md +16 -5
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
- package/skills/peaks-code/references/standards-preflight.md +1 -1
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
- package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`L3:l3-memory-health`).
|
|
2
|
+
* Checks for `.peaks/memory/` health (`L3:l3-memory-health` and siblings).
|
|
4
3
|
*
|
|
5
4
|
* Slice 2026-06-13-repair-pre-existing-test-failures: the
|
|
6
5
|
* production MemoryIndex schema (see
|
|
@@ -11,12 +10,122 @@
|
|
|
11
10
|
*
|
|
12
11
|
* When no `.peaks/memory/index.json` exists yet, the check passes
|
|
13
12
|
* (fresh project — no memories have been extracted).
|
|
13
|
+
*
|
|
14
|
+
* Slice 2026-09-09-memory-system-overhaul (D) extends the check with the
|
|
15
|
+
* drift findings the original version could not see. It used to report
|
|
16
|
+
* `ok: true` for "index.json is well-formed JSON; 100 hot + 131 warm" and
|
|
17
|
+
* never looked at coverage, orphans, or unclassified files. It now emits,
|
|
18
|
+
* in addition to the unchanged well-formed-JSON assertion:
|
|
19
|
+
*
|
|
20
|
+
* - `L3:l3-memory-coverage` — disk files vs indexed entries (warning
|
|
21
|
+
* when the gap exceeds a small threshold)
|
|
22
|
+
* - `L3:l3-memory-orphans` — index entries whose `sourcePath` is gone
|
|
23
|
+
* (error) + disk files absent from the
|
|
24
|
+
* index (warning)
|
|
25
|
+
* - `L3:l3-memory-unclassified` — files with no resolvable kind (warning,
|
|
26
|
+
* count + first N names)
|
|
27
|
+
*
|
|
28
|
+
* All three are read-only and fail-soft: an inspection error degrades to a
|
|
29
|
+
* single warning instead of throwing, and none of them change the id or the
|
|
30
|
+
* `ok` semantics of the original `L3:l3-memory-health` assertion.
|
|
14
31
|
*/
|
|
15
32
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
33
|
+
import { basename, join } from 'node:path';
|
|
17
34
|
import { getErrorMessage } from 'peaks-loop-shared/result';
|
|
35
|
+
import { listMarkdownFiles, MEMORY_MD_FILENAME, parseMemoryFrontmatter } from '../../../memory/project-memory-service/index.js';
|
|
36
|
+
/** Warn when |disk - indexed| exceeds this. Small enough to catch real drift. */
|
|
37
|
+
const COVERAGE_GAP_WARN_THRESHOLD = 2;
|
|
38
|
+
/** How many offending names to inline before truncating the message. */
|
|
39
|
+
const MAX_NAMES_IN_MESSAGE = 5;
|
|
40
|
+
function countEntries(bucket) {
|
|
41
|
+
return Object.values(bucket ?? {}).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0);
|
|
42
|
+
}
|
|
43
|
+
function previewNames(names) {
|
|
44
|
+
const shown = names.slice(0, MAX_NAMES_IN_MESSAGE);
|
|
45
|
+
const suffix = names.length > shown.length ? ` (+${names.length - shown.length} more)` : '';
|
|
46
|
+
return shown.join(', ') + suffix;
|
|
47
|
+
}
|
|
48
|
+
function readIndexSourcePaths(indexPath) {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
50
|
+
const fromBucket = (bucket) => Object.values(bucket ?? {}).flatMap((arr) => (Array.isArray(arr) ? arr : []));
|
|
51
|
+
return [...fromBucket(parsed.hot), ...fromBucket(parsed.warm), ...(parsed.cold ?? [])];
|
|
52
|
+
}
|
|
53
|
+
function inspectDrift(memoryDir, memoryIndexPath, indexedCount) {
|
|
54
|
+
const checks = [];
|
|
55
|
+
const diskFiles = listMarkdownFiles(memoryDir).filter((filePath) => basename(filePath) !== MEMORY_MD_FILENAME);
|
|
56
|
+
// --- coverage ---------------------------------------------------------
|
|
57
|
+
const gap = diskFiles.length - indexedCount;
|
|
58
|
+
if (Math.abs(gap) > COVERAGE_GAP_WARN_THRESHOLD) {
|
|
59
|
+
const direction = gap > 0
|
|
60
|
+
? `${gap} file(s) on disk are not in the index`
|
|
61
|
+
: `${-gap} index entr(ies) have no matching file`;
|
|
62
|
+
checks.push({
|
|
63
|
+
id: 'L3:l3-memory-coverage',
|
|
64
|
+
ok: false,
|
|
65
|
+
severity: 'warning',
|
|
66
|
+
message: `Memory index coverage gap: ${diskFiles.length} file(s) on disk vs ${indexedCount} indexed — ${direction}. Run \`peaks memory reindex\` for the full drift report.`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
checks.push({
|
|
71
|
+
id: 'L3:l3-memory-coverage',
|
|
72
|
+
ok: true,
|
|
73
|
+
message: `Memory index coverage: ${diskFiles.length} file(s) on disk, ${indexedCount} indexed (within threshold ${COVERAGE_GAP_WARN_THRESHOLD})`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// --- orphans (both directions) ---------------------------------------
|
|
77
|
+
const missingSources = readIndexSourcePaths(memoryIndexPath)
|
|
78
|
+
.filter((entry) => typeof entry.sourcePath !== 'string' || entry.sourcePath.length === 0 || !existsSync(entry.sourcePath))
|
|
79
|
+
.map((entry) => entry.name ?? entry.sourcePath ?? '<unnamed>')
|
|
80
|
+
.sort((left, right) => left.localeCompare(right));
|
|
81
|
+
if (missingSources.length > 0) {
|
|
82
|
+
checks.push({
|
|
83
|
+
id: 'L3:l3-memory-orphans',
|
|
84
|
+
ok: false,
|
|
85
|
+
severity: 'error',
|
|
86
|
+
message: `${missingSources.length} index entr(ies) point at a missing sourcePath: ${previewNames(missingSources)}. Run \`peaks memory reindex\` to rebuild the index.`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
checks.push({
|
|
91
|
+
id: 'L3:l3-memory-orphans',
|
|
92
|
+
ok: true,
|
|
93
|
+
message: 'No memory index entries point at missing files'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// --- unclassified -----------------------------------------------------
|
|
97
|
+
const unclassified = [];
|
|
98
|
+
for (const filePath of diskFiles) {
|
|
99
|
+
try {
|
|
100
|
+
if (parseMemoryFrontmatter(readFileSync(filePath, 'utf8')).kind.kind === null) {
|
|
101
|
+
unclassified.push(basename(filePath, '.md'));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
unclassified.push(`${basename(filePath, '.md')} (unreadable)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
unclassified.sort((left, right) => left.localeCompare(right));
|
|
109
|
+
if (unclassified.length > 0) {
|
|
110
|
+
checks.push({
|
|
111
|
+
id: 'L3:l3-memory-unclassified',
|
|
112
|
+
ok: false,
|
|
113
|
+
severity: 'warning',
|
|
114
|
+
message: `${unclassified.length} memory file(s) have no resolvable kind (no metadata.type / kind / type): ${previewNames(unclassified)}. Add \`metadata.type\` then run \`peaks memory reindex\`.`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
checks.push({
|
|
119
|
+
id: 'L3:l3-memory-unclassified',
|
|
120
|
+
ok: true,
|
|
121
|
+
message: 'Every memory file on disk has a resolvable kind'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return checks;
|
|
125
|
+
}
|
|
18
126
|
function run({ resolvedL3Root }) {
|
|
19
|
-
const
|
|
127
|
+
const memoryDir = join(resolvedL3Root, '.peaks/memory');
|
|
128
|
+
const memoryIndexPath = join(memoryDir, 'index.json');
|
|
20
129
|
if (!existsSync(memoryIndexPath)) {
|
|
21
130
|
return [{
|
|
22
131
|
id: 'L3:l3-memory-health',
|
|
@@ -24,32 +133,47 @@ function run({ resolvedL3Root }) {
|
|
|
24
133
|
message: 'No .peaks/memory/index.json yet (no memories extracted)'
|
|
25
134
|
}];
|
|
26
135
|
}
|
|
136
|
+
let parsed;
|
|
27
137
|
try {
|
|
28
138
|
const raw = readFileSync(memoryIndexPath, 'utf8');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return [{
|
|
33
|
-
id: 'L3:l3-memory-health',
|
|
34
|
-
ok: false,
|
|
35
|
-
message: '.peaks/memory/index.json missing schema_version / version field'
|
|
36
|
-
}];
|
|
37
|
-
}
|
|
38
|
-
const hotCount = Object.values(parsed.hot ?? {}).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0);
|
|
39
|
-
const warmCount = Object.values(parsed.warm ?? {}).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0);
|
|
139
|
+
parsed = JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch (parseError) {
|
|
40
142
|
return [{
|
|
41
143
|
id: 'L3:l3-memory-health',
|
|
42
|
-
ok:
|
|
43
|
-
message: `.peaks/memory/index.json is
|
|
144
|
+
ok: false,
|
|
145
|
+
message: `.peaks/memory/index.json is not valid JSON: ${getErrorMessage(parseError)}`
|
|
44
146
|
}];
|
|
45
147
|
}
|
|
46
|
-
|
|
148
|
+
const schemaMarker = parsed.schema_version ?? parsed.version;
|
|
149
|
+
if (schemaMarker === undefined) {
|
|
47
150
|
return [{
|
|
48
151
|
id: 'L3:l3-memory-health',
|
|
49
152
|
ok: false,
|
|
50
|
-
message:
|
|
153
|
+
message: '.peaks/memory/index.json missing schema_version / version field'
|
|
51
154
|
}];
|
|
52
155
|
}
|
|
156
|
+
const hotCount = countEntries(parsed.hot);
|
|
157
|
+
const warmCount = countEntries(parsed.warm);
|
|
158
|
+
const checks = [{
|
|
159
|
+
id: 'L3:l3-memory-health',
|
|
160
|
+
ok: true,
|
|
161
|
+
message: `.peaks/memory/index.json is well-formed JSON; version=${schemaMarker}; ${hotCount} hot + ${warmCount} warm memory entries`
|
|
162
|
+
}];
|
|
163
|
+
// Drift inspection is best-effort: a scan failure must not turn a
|
|
164
|
+
// well-formed index into a hard failure.
|
|
165
|
+
try {
|
|
166
|
+
checks.push(...inspectDrift(memoryDir, memoryIndexPath, hotCount + warmCount));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
checks.push({
|
|
170
|
+
id: 'L3:l3-memory-coverage',
|
|
171
|
+
ok: true,
|
|
172
|
+
severity: 'warning',
|
|
173
|
+
message: `Memory drift inspection skipped: ${getErrorMessage(error)}`
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return checks;
|
|
53
177
|
}
|
|
54
178
|
export const check = {
|
|
55
179
|
name: 'l3-memory-health',
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const AUTO_COMPACT_HOOK_MATCHER = "Bash|Task";
|
|
39
39
|
/**
|
|
40
|
-
* Stable command the hook fires. Wraps `peaks
|
|
40
|
+
* Stable command the hook fires. Wraps `peaks code auto-compact` so the
|
|
41
41
|
* heavy lifting (ratio probe + in-band `claude --compact` spawn)
|
|
42
42
|
* lives in the CLI surface, not inlined into the hook.
|
|
43
43
|
*
|
|
@@ -46,7 +46,7 @@ export declare const AUTO_COMPACT_HOOK_MATCHER = "Bash|Task";
|
|
|
46
46
|
* so the unqualified command works in the same shell the runner
|
|
47
47
|
* is using.
|
|
48
48
|
*/
|
|
49
|
-
export declare const AUTO_COMPACT_HOOK_COMMAND = "peaks
|
|
49
|
+
export declare const AUTO_COMPACT_HOOK_COMMAND = "peaks code auto-compact";
|
|
50
50
|
/**
|
|
51
51
|
* Magic comment marker that fences the auto-compatchook block.
|
|
52
52
|
* Both install (writes the marker) and remove (matches on it) use
|
|
@@ -39,7 +39,7 @@ import { dirname, join } from 'node:path';
|
|
|
39
39
|
*/
|
|
40
40
|
export const AUTO_COMPACT_HOOK_MATCHER = 'Bash|Task';
|
|
41
41
|
/**
|
|
42
|
-
* Stable command the hook fires. Wraps `peaks
|
|
42
|
+
* Stable command the hook fires. Wraps `peaks code auto-compact` so the
|
|
43
43
|
* heavy lifting (ratio probe + in-band `claude --compact` spawn)
|
|
44
44
|
* lives in the CLI surface, not inlined into the hook.
|
|
45
45
|
*
|
|
@@ -48,7 +48,7 @@ export const AUTO_COMPACT_HOOK_MATCHER = 'Bash|Task';
|
|
|
48
48
|
* so the unqualified command works in the same shell the runner
|
|
49
49
|
* is using.
|
|
50
50
|
*/
|
|
51
|
-
export const AUTO_COMPACT_HOOK_COMMAND = 'peaks
|
|
51
|
+
export const AUTO_COMPACT_HOOK_COMMAND = 'peaks code auto-compact';
|
|
52
52
|
/**
|
|
53
53
|
* Magic comment marker that fences the auto-compatchook block.
|
|
54
54
|
* Both install (writes the marker) and remove (matches on it) use
|
|
@@ -20,6 +20,57 @@ export declare function resolveClaudeModelFromEnv(env: NodeJS.ProcessEnv | undef
|
|
|
20
20
|
* Callers MAY additionally infer ≥1M from the observed token count: if
|
|
21
21
|
* `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
|
|
22
22
|
* 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
|
|
23
|
+
*
|
|
24
|
+
* This is the HEURISTIC layer only. Explicit user overrides sit ABOVE it —
|
|
25
|
+
* see `resolveContextWindow`, which is what the probe actually calls.
|
|
23
26
|
*/
|
|
24
27
|
export declare function modelContextWindowTokens(model: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* Slice 2026-09-09-context-window-override: vendor-neutral escape hatch for
|
|
30
|
+
* the context-window size in tokens. Third-party / proxied models whose id
|
|
31
|
+
* carries no `[1M]` suffix (and is absent from the hardcoded allowlist) are
|
|
32
|
+
* otherwise stuck at the 200K default, which inflates `ratio` up to 5×.
|
|
33
|
+
* Same spirit as `PEAKS_CALLER_ID`: an env var the user can export, plus a
|
|
34
|
+
* machine-wide `peaks config set --key context.windowTokens` twin.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CONTEXT_WINDOW_TOKENS_ENV_VAR = "PEAKS_CONTEXT_WINDOW_TOKENS";
|
|
37
|
+
/**
|
|
38
|
+
* Which layer produced a resolved context window:
|
|
39
|
+
* - `env-override` — `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
40
|
+
* - `config` — `context.windowTokens` (`peaks config set`)
|
|
41
|
+
* - `model-heuristic` — `[1M]` suffix / `ONE_MILLION_CONTEXT_MODELS`
|
|
42
|
+
* - `default` — 200K safe default
|
|
43
|
+
*/
|
|
44
|
+
export type ContextWindowSource = 'env-override' | 'config' | 'model-heuristic' | 'default';
|
|
45
|
+
export interface ContextWindowResolution {
|
|
46
|
+
readonly tokens: number;
|
|
47
|
+
readonly source: ContextWindowSource;
|
|
48
|
+
}
|
|
49
|
+
/** Explicit override inputs (both optional; env wins over config). */
|
|
50
|
+
export interface ContextWindowOverrides {
|
|
51
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
52
|
+
/** Raw `context.windowTokens` value (unvalidated — validated here). */
|
|
53
|
+
readonly configWindowTokens?: unknown;
|
|
54
|
+
/** Warning sink for an invalid override (defaults to `console.warn`). */
|
|
55
|
+
readonly onInvalidOverride?: ((message: string) => void) | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse an explicit context-window override. Accepts a positive finite
|
|
59
|
+
* integer only (number, or a numeric string so an env var works); anything
|
|
60
|
+
* else — `0`, negative, `NaN`, `Infinity`, `1.5`, `'abc'`, `''` — returns
|
|
61
|
+
* null so the caller can ignore it and fall through to the next layer.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseContextWindowOverride(raw: unknown): number | null;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the context window in tokens AND report which layer won
|
|
66
|
+
* (first hit wins):
|
|
67
|
+
* 1. env `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
68
|
+
* 2. config `context.windowTokens`
|
|
69
|
+
* 3. model-name heuristic (`modelContextWindowTokens`)
|
|
70
|
+
* 4. `DEFAULT_CONTEXT_WINDOW_TOKENS` (200_000)
|
|
71
|
+
*
|
|
72
|
+
* An invalid explicit override is ignored with a warning and falls through
|
|
73
|
+
* to the next layer — a typo must never crash or silently win the probe.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveContextWindow(model: string, overrides?: ContextWindowOverrides): ContextWindowResolution;
|
|
25
76
|
export declare const CLAUDE_CODE_ADAPTER: IdeAdapter;
|
|
@@ -161,6 +161,9 @@ export function resolveClaudeModelFromEnv(env) {
|
|
|
161
161
|
* Callers MAY additionally infer ≥1M from the observed token count: if
|
|
162
162
|
* `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
|
|
163
163
|
* 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
|
|
164
|
+
*
|
|
165
|
+
* This is the HEURISTIC layer only. Explicit user overrides sit ABOVE it —
|
|
166
|
+
* see `resolveContextWindow`, which is what the probe actually calls.
|
|
164
167
|
*/
|
|
165
168
|
export function modelContextWindowTokens(model) {
|
|
166
169
|
const m = model.trim().toLowerCase();
|
|
@@ -174,6 +177,60 @@ export function modelContextWindowTokens(model) {
|
|
|
174
177
|
}
|
|
175
178
|
return DEFAULT_CONTEXT_WINDOW_TOKENS;
|
|
176
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Slice 2026-09-09-context-window-override: vendor-neutral escape hatch for
|
|
182
|
+
* the context-window size in tokens. Third-party / proxied models whose id
|
|
183
|
+
* carries no `[1M]` suffix (and is absent from the hardcoded allowlist) are
|
|
184
|
+
* otherwise stuck at the 200K default, which inflates `ratio` up to 5×.
|
|
185
|
+
* Same spirit as `PEAKS_CALLER_ID`: an env var the user can export, plus a
|
|
186
|
+
* machine-wide `peaks config set --key context.windowTokens` twin.
|
|
187
|
+
*/
|
|
188
|
+
export const CONTEXT_WINDOW_TOKENS_ENV_VAR = 'PEAKS_CONTEXT_WINDOW_TOKENS';
|
|
189
|
+
/**
|
|
190
|
+
* Parse an explicit context-window override. Accepts a positive finite
|
|
191
|
+
* integer only (number, or a numeric string so an env var works); anything
|
|
192
|
+
* else — `0`, negative, `NaN`, `Infinity`, `1.5`, `'abc'`, `''` — returns
|
|
193
|
+
* null so the caller can ignore it and fall through to the next layer.
|
|
194
|
+
*/
|
|
195
|
+
export function parseContextWindowOverride(raw) {
|
|
196
|
+
if (typeof raw !== 'number' && typeof raw !== 'string')
|
|
197
|
+
return null;
|
|
198
|
+
if (typeof raw === 'string' && raw.trim().length === 0)
|
|
199
|
+
return null;
|
|
200
|
+
const parsed = typeof raw === 'number' ? raw : Number(raw.trim());
|
|
201
|
+
return Number.isFinite(parsed) && Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Resolve the context window in tokens AND report which layer won
|
|
205
|
+
* (first hit wins):
|
|
206
|
+
* 1. env `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
207
|
+
* 2. config `context.windowTokens`
|
|
208
|
+
* 3. model-name heuristic (`modelContextWindowTokens`)
|
|
209
|
+
* 4. `DEFAULT_CONTEXT_WINDOW_TOKENS` (200_000)
|
|
210
|
+
*
|
|
211
|
+
* An invalid explicit override is ignored with a warning and falls through
|
|
212
|
+
* to the next layer — a typo must never crash or silently win the probe.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveContextWindow(model, overrides = {}) {
|
|
215
|
+
const warn = overrides.onInvalidOverride ?? ((message) => console.warn(message));
|
|
216
|
+
const envRaw = overrides.env?.[CONTEXT_WINDOW_TOKENS_ENV_VAR];
|
|
217
|
+
if (envRaw !== undefined) {
|
|
218
|
+
const parsed = parseContextWindowOverride(envRaw);
|
|
219
|
+
if (parsed !== null)
|
|
220
|
+
return { tokens: parsed, source: 'env-override' };
|
|
221
|
+
warn(`[peaks] ${CONTEXT_WINDOW_TOKENS_ENV_VAR}="${String(envRaw)}" is not a positive integer — ignoring the override`);
|
|
222
|
+
}
|
|
223
|
+
if (overrides.configWindowTokens !== undefined) {
|
|
224
|
+
const parsed = parseContextWindowOverride(overrides.configWindowTokens);
|
|
225
|
+
if (parsed !== null)
|
|
226
|
+
return { tokens: parsed, source: 'config' };
|
|
227
|
+
warn(`[peaks] config context.windowTokens=${JSON.stringify(overrides.configWindowTokens)} is not a positive integer — ignoring the override`);
|
|
228
|
+
}
|
|
229
|
+
const heuristic = modelContextWindowTokens(model);
|
|
230
|
+
return heuristic === DEFAULT_CONTEXT_WINDOW_TOKENS
|
|
231
|
+
? { tokens: heuristic, source: 'default' }
|
|
232
|
+
: { tokens: heuristic, source: 'model-heuristic' };
|
|
233
|
+
}
|
|
177
234
|
/** A non-negative finite number, or null when the value is not numeric. */
|
|
178
235
|
function numericTokenCount(value) {
|
|
179
236
|
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
@@ -277,14 +334,20 @@ function findLatestTranscriptUsage(filePath) {
|
|
|
277
334
|
}
|
|
278
335
|
}
|
|
279
336
|
/**
|
|
280
|
-
* Resolve the context window for a transcript usage entry.
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
337
|
+
* Resolve the context window for a transcript usage entry. Explicit user
|
|
338
|
+
* overrides (`resolveContextWindow`) win outright — the user pinned the
|
|
339
|
+
* number, so nothing else may contradict it. Without an explicit override,
|
|
340
|
+
* when the observed token count contradicts the heuristic window (tokens
|
|
341
|
+
* exceed it), the model must be ≥1M, so bump to the 1M window (the late
|
|
342
|
+
* rescue; it keeps the heuristic source tag, only the tokens change).
|
|
284
343
|
*/
|
|
285
|
-
function resolveContextWindowTokens(model, contextTokens) {
|
|
286
|
-
const
|
|
287
|
-
|
|
344
|
+
function resolveContextWindowTokens(model, contextTokens, overrides = {}) {
|
|
345
|
+
const resolved = resolveContextWindow(model, overrides);
|
|
346
|
+
if (resolved.source === 'env-override' || resolved.source === 'config')
|
|
347
|
+
return resolved;
|
|
348
|
+
return contextTokens > resolved.tokens
|
|
349
|
+
? { tokens: ONE_MILLION_CONTEXT_TOKENS, source: resolved.source }
|
|
350
|
+
: resolved;
|
|
288
351
|
}
|
|
289
352
|
/**
|
|
290
353
|
* Conservative transcript-estimate fallback. Recursively searches
|
|
@@ -298,9 +361,11 @@ function resolveContextWindowTokens(model, contextTokens) {
|
|
|
298
361
|
*
|
|
299
362
|
* Window model resolution is env-first: when `envModel` is present, its id
|
|
300
363
|
* (which Claude Code stamps with the `[1M]` / `[200K]` suffix) drives the
|
|
301
|
-
* window; otherwise the transcript `message.model` is used.
|
|
364
|
+
* window; otherwise the transcript `message.model` is used. Explicit
|
|
365
|
+
* overrides (env `PEAKS_CONTEXT_WINDOW_TOKENS` / config `context.windowTokens`)
|
|
366
|
+
* sit above both and are reported via `capacitySource`.
|
|
302
367
|
*/
|
|
303
|
-
function readClaudeTranscriptEstimate(outerSessionId, envModel) {
|
|
368
|
+
function readClaudeTranscriptEstimate(outerSessionId, envModel, overrides = {}) {
|
|
304
369
|
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
305
370
|
const path = findTranscriptJsonl(projectsDir, outerSessionId);
|
|
306
371
|
if (path === null)
|
|
@@ -309,9 +374,10 @@ function readClaudeTranscriptEstimate(outerSessionId, envModel) {
|
|
|
309
374
|
if (latest === null)
|
|
310
375
|
return null;
|
|
311
376
|
const model = envModel !== undefined && envModel.trim().length > 0 ? envModel : latest.model;
|
|
312
|
-
const
|
|
377
|
+
const resolved = resolveContextWindowTokens(model, latest.contextTokens, overrides);
|
|
378
|
+
const contextWindowTokens = resolved.tokens;
|
|
313
379
|
const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
|
|
314
|
-
return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
|
|
380
|
+
return { ratio, contextTokens: latest.contextTokens, contextWindowTokens, capacitySource: resolved.source };
|
|
315
381
|
}
|
|
316
382
|
/**
|
|
317
383
|
* Claude Code's vendor-specific context-percent fallback, exposed as
|
|
@@ -344,13 +410,17 @@ function readContextPercentFallback(input) {
|
|
|
344
410
|
}
|
|
345
411
|
if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
|
|
346
412
|
const envModel = resolveClaudeModelFromEnv(input.env);
|
|
347
|
-
const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel
|
|
413
|
+
const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel, {
|
|
414
|
+
env: input.env,
|
|
415
|
+
configWindowTokens: input.configWindowTokens
|
|
416
|
+
});
|
|
348
417
|
if (estimate !== null) {
|
|
349
418
|
return {
|
|
350
419
|
ratio: estimate.ratio,
|
|
351
420
|
source: 'transcript-estimate',
|
|
352
421
|
rawTokens: estimate.contextTokens,
|
|
353
422
|
capacityTokens: estimate.contextWindowTokens,
|
|
423
|
+
capacitySource: estimate.capacitySource,
|
|
354
424
|
ide,
|
|
355
425
|
capturedAt
|
|
356
426
|
};
|
|
@@ -401,7 +471,7 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
401
471
|
// Pathway = 'ide-native' (not 'shell-exec') so the dispatcher
|
|
402
472
|
// routes main-session compacts through the PreToolUse hook in
|
|
403
473
|
// `.claude/settings.local.json`. The hook fires
|
|
404
|
-
// `peaks
|
|
474
|
+
// `peaks code auto-compact` on the NEXT Bash/Task tool
|
|
405
475
|
// call from the runner, which in-band spawns `claude --compact`
|
|
406
476
|
// against the CURRENT runner — not a child process (the
|
|
407
477
|
// shell-exec spawn-new-claude bug documented in
|
|
@@ -412,7 +482,7 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
412
482
|
envVarForContextPercent: 'CLAUDE_CONTEXT_USAGE_PERCENT',
|
|
413
483
|
compactCommand: 'claude --compact',
|
|
414
484
|
compactPathway: 'ide-native',
|
|
415
|
-
postCompactDetectCommand: 'peaks
|
|
485
|
+
postCompactDetectCommand: 'peaks code auto-compact --json',
|
|
416
486
|
readContextPercentFallback
|
|
417
487
|
},
|
|
418
488
|
// Slice #011: standards profile. Claude Code reads its constitution at
|
|
@@ -197,7 +197,7 @@ export interface IdeCompactProfile {
|
|
|
197
197
|
readonly compactPathway: 'shell-exec' | 'ide-native' | 'llm-self-compress' | 'noop';
|
|
198
198
|
/**
|
|
199
199
|
* Optional command the runner invokes post-compact to confirm
|
|
200
|
-
* ratio dropped (e.g. `peaks
|
|
200
|
+
* ratio dropped (e.g. `peaks code auto-compact --json`). When omitted,
|
|
201
201
|
* the orchestrator polls `envVarForContextPercent` directly.
|
|
202
202
|
*/
|
|
203
203
|
readonly postCompactDetectCommand?: string;
|
|
@@ -239,6 +239,15 @@ export interface ContextPercentFallbackInput {
|
|
|
239
239
|
readonly outerSessionId?: string | undefined;
|
|
240
240
|
/** Injectable env (defaults to process.env in the reader). */
|
|
241
241
|
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
242
|
+
/**
|
|
243
|
+
* Raw `context.windowTokens` value from the merged config (project layer
|
|
244
|
+
* over user layer), read by the generic reader so the adapter stays free
|
|
245
|
+
* of config-path knowledge. UNVALIDATED — the adapter validates it via
|
|
246
|
+
* `parseContextWindowOverride` and warns on a bad value.
|
|
247
|
+
*
|
|
248
|
+
* Slice 2026-09-09-context-window-override.
|
|
249
|
+
*/
|
|
250
|
+
readonly configWindowTokens?: unknown;
|
|
242
251
|
}
|
|
243
252
|
/**
|
|
244
253
|
* Per-IDE standards-file location + format profile. Used by the
|
|
@@ -6,8 +6,8 @@ export declare const SliceStateSchema: z.ZodObject<{
|
|
|
6
6
|
pending: "pending";
|
|
7
7
|
blocked: "blocked";
|
|
8
8
|
failed: "failed";
|
|
9
|
-
skipped: "skipped";
|
|
10
9
|
done: "done";
|
|
10
|
+
skipped: "skipped";
|
|
11
11
|
"in-progress": "in-progress";
|
|
12
12
|
}>;
|
|
13
13
|
commitSha: z.ZodOptional<z.ZodString>;
|
|
@@ -48,8 +48,8 @@ export declare const JobStateSchema: z.ZodObject<{
|
|
|
48
48
|
pending: "pending";
|
|
49
49
|
blocked: "blocked";
|
|
50
50
|
failed: "failed";
|
|
51
|
-
skipped: "skipped";
|
|
52
51
|
done: "done";
|
|
52
|
+
skipped: "skipped";
|
|
53
53
|
"in-progress": "in-progress";
|
|
54
54
|
}>;
|
|
55
55
|
commitSha: z.ZodOptional<z.ZodString>;
|
|
@@ -118,8 +118,8 @@ export declare const JobCheckpointInputSchema: z.ZodObject<{
|
|
|
118
118
|
sliceId: z.ZodString;
|
|
119
119
|
state: z.ZodEnum<{
|
|
120
120
|
failed: "failed";
|
|
121
|
-
skipped: "skipped";
|
|
122
121
|
done: "done";
|
|
122
|
+
skipped: "skipped";
|
|
123
123
|
}>;
|
|
124
124
|
commitSha: z.ZodOptional<z.ZodString>;
|
|
125
125
|
reason: z.ZodOptional<z.ZodString>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ProjectMemoryKind } from './project-memory-service/types.js';
|
|
2
|
+
export interface MemoryIngestOptions {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
/** Override the IDE-side source dir (defaults to `~/.claude/projects/<encoded>/memory`). */
|
|
5
|
+
sourceDir?: string;
|
|
6
|
+
/** Injectable home dir (tests); defaults to `os.homedir()`. */
|
|
7
|
+
homeDir?: string;
|
|
8
|
+
apply?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface MemoryIngestImported {
|
|
11
|
+
name: string;
|
|
12
|
+
kind: ProjectMemoryKind;
|
|
13
|
+
sourcePath: string;
|
|
14
|
+
targetPath: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MemoryIngestSkipped {
|
|
17
|
+
name: string;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
targetPath: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MemoryIngestConflict {
|
|
22
|
+
name: string;
|
|
23
|
+
sourcePath: string;
|
|
24
|
+
targetPath: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
export interface MemoryIngestNeedsClassification {
|
|
28
|
+
name: string;
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
rawKind: string | null;
|
|
31
|
+
reason: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MemoryIngestRefused {
|
|
34
|
+
name: string;
|
|
35
|
+
sourcePath: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export interface MemoryIngestReport {
|
|
39
|
+
apply: boolean;
|
|
40
|
+
projectRoot: string;
|
|
41
|
+
sourceDir: string;
|
|
42
|
+
sourceExists: boolean;
|
|
43
|
+
memoryDir: string;
|
|
44
|
+
scannedFiles: number;
|
|
45
|
+
imported: MemoryIngestImported[];
|
|
46
|
+
skippedIdentical: MemoryIngestSkipped[];
|
|
47
|
+
conflicts: MemoryIngestConflict[];
|
|
48
|
+
needsClassification: MemoryIngestNeedsClassification[];
|
|
49
|
+
refused: MemoryIngestRefused[];
|
|
50
|
+
writtenFiles: string[];
|
|
51
|
+
warnings: string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Claude Code encodes a project cwd into its `~/.claude/projects/<name>/`
|
|
55
|
+
* directory by replacing every non-alphanumeric character with `-`
|
|
56
|
+
* (`D:\peaks-loop` → `D--peaks-loop`). Separator-agnostic, so the same
|
|
57
|
+
* encoding holds for POSIX paths.
|
|
58
|
+
*/
|
|
59
|
+
export declare function encodeIdeProjectDir(projectRoot: string): string;
|
|
60
|
+
/** Default IDE-side memory dir for a project: `~/.claude/projects/<encoded>/memory`. */
|
|
61
|
+
export declare function defaultIdeMemoryDir(projectRoot: string, homeDir?: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Rewrite a source file's frontmatter to the peaks contract: `name` pinned to
|
|
64
|
+
* the destination filename stem, and `metadata.type` set to the resolved
|
|
65
|
+
* kind. Non-contract keys (e.g. `originSessionId`, `modified`, `node_type`)
|
|
66
|
+
* are preserved verbatim so provenance survives the import; `type` / `kind`
|
|
67
|
+
* are consumed by the normalization and not duplicated.
|
|
68
|
+
*/
|
|
69
|
+
export declare function renderNormalizedMemory(input: {
|
|
70
|
+
stem: string;
|
|
71
|
+
kind: ProjectMemoryKind;
|
|
72
|
+
frontmatter: string;
|
|
73
|
+
body: string;
|
|
74
|
+
}): string;
|
|
75
|
+
/**
|
|
76
|
+
* Import IDE-side memories into `.peaks/memory/`. Always returns the full
|
|
77
|
+
* envelope; `apply` only controls whether files are actually written.
|
|
78
|
+
*/
|
|
79
|
+
export declare function executeMemoryIngest(options: MemoryIngestOptions): MemoryIngestReport;
|