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.
Files changed (88) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/commands/code-commands.js +2 -0
  3. package/dist/cli/commands/code-mode-gate-commands.js +3 -3
  4. package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
  5. package/dist/cli/commands/code-mode-status-command.js +51 -0
  6. package/dist/cli/commands/code-run-command.js +8 -1
  7. package/dist/cli/commands/code-runtime-commands.js +13 -5
  8. package/dist/cli/commands/core/memory-command.js +61 -3
  9. package/dist/cli/commands/core/skill-command.js +1 -1
  10. package/dist/cli/commands/dispatch-commands.js +4 -2
  11. package/dist/cli/commands/memory-commands.d.ts +35 -0
  12. package/dist/cli/commands/memory-commands.js +119 -10
  13. package/dist/cli/commands/request-commands.js +3 -7
  14. package/dist/cli/commands/session-24h-mode.js +9 -1
  15. package/dist/services/24h-mode/auto-engage.d.ts +39 -0
  16. package/dist/services/24h-mode/auto-engage.js +46 -0
  17. package/dist/services/24h-mode/decider.d.ts +11 -5
  18. package/dist/services/24h-mode/decider.js +13 -13
  19. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
  20. package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
  21. package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
  22. package/dist/services/code/auto-compact-lifecycle.js +1 -1
  23. package/dist/services/code/auto-compact-modes.d.ts +1 -1
  24. package/dist/services/code/auto-compact-modes.js +1 -1
  25. package/dist/services/code/auto-compact-orchestrator.js +18 -22
  26. package/dist/services/code/mode-gate.d.ts +6 -2
  27. package/dist/services/code/mode-gate.js +9 -5
  28. package/dist/services/code/orchestrator-can-do.js +2 -2
  29. package/dist/services/config/config-service.d.ts +17 -0
  30. package/dist/services/config/config-service.js +34 -0
  31. package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
  32. package/dist/services/context/auto-compact-dispatcher.js +4 -4
  33. package/dist/services/context/auto-compact-reader.js +8 -1
  34. package/dist/services/context/auto-compact-types.d.ts +13 -1
  35. package/dist/services/context/build-dispatch-system-prompt.js +2 -2
  36. package/dist/services/context/memory-index-reader.d.ts +26 -0
  37. package/dist/services/context/memory-index-reader.js +62 -30
  38. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  39. package/dist/services/context/memory-preflight-config.js +32 -2
  40. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  41. package/dist/services/context/memory-preflight-service.js +198 -31
  42. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  43. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  44. package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
  45. package/dist/services/hooks/auto-compact-hook-install.js +2 -2
  46. package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
  47. package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
  48. package/dist/services/ide/ide-types.d.ts +10 -1
  49. package/dist/services/job/job-types.d.ts +3 -3
  50. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  51. package/dist/services/memory/memory-ingest-service.js +225 -0
  52. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  53. package/dist/services/memory/memory-rotate-service.js +373 -0
  54. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  55. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  56. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  57. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  58. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  59. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  60. package/dist/services/memory/project-memory-service/index.js +6 -2
  61. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
  62. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
  63. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  64. package/dist/services/memory/project-memory-service/types.js +76 -1
  65. package/dist/services/mode/mode-enforcement.d.ts +4 -1
  66. package/dist/services/mode/mode-enforcement.js +26 -28
  67. package/dist/services/mode/mode-status-service.d.ts +81 -0
  68. package/dist/services/mode/mode-status-service.js +108 -0
  69. package/dist/services/preferences/preferences-types.d.ts +14 -0
  70. package/dist/services/preferences/preferences-types.js +8 -0
  71. package/dist/services/share/run-state-contract.d.ts +1 -1
  72. package/dist/services/skills/presence-lease-service.d.ts +21 -0
  73. package/dist/services/skills/presence-lease-service.js +26 -0
  74. package/dist/services/skills/skill-presence-service.d.ts +10 -1
  75. package/dist/services/skills/skill-presence-service.js +32 -4
  76. package/dist/services/skills/skill-statusline-service.js +6 -3
  77. package/package.json +5 -5
  78. package/skills/peaks-code/SKILL.md +10 -10
  79. package/skills/peaks-code/references/mode-selection.md +7 -3
  80. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  81. package/skills/peaks-code/references/runbook.md +16 -5
  82. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  83. package/skills/peaks-code/references/standards-preflight.md +1 -1
  84. package/skills/peaks-code/references/startup-sequence.md +1 -1
  85. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  86. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
  87. package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
  88. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Check: `.peaks/memory/index.json` is well-formed JSON
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 memoryIndexPath = join(resolvedL3Root, '.peaks/memory/index.json');
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
- const parsed = JSON.parse(raw);
30
- const schemaMarker = parsed.schema_version ?? parsed.version;
31
- if (schemaMarker === undefined) {
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: true,
43
- message: `.peaks/memory/index.json is well-formed JSON; version=${schemaMarker}; ${hotCount} hot + ${warmCount} warm memory entries`
144
+ ok: false,
145
+ message: `.peaks/memory/index.json is not valid JSON: ${getErrorMessage(parseError)}`
44
146
  }];
45
147
  }
46
- catch (parseError) {
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: `.peaks/memory/index.json is not valid JSON: ${getErrorMessage(parseError)}`
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 compact auto` so the
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 compact auto";
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 compact auto` so the
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 compact auto';
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. Prefers the
281
- * explicit `modelContextWindowTokens(model)` mapping; when the observed token
282
- * count contradicts it (tokens exceed the mapped window), the model must be
283
- * ≥1M, so bump to the 1M window.
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 window = modelContextWindowTokens(model);
287
- return contextTokens > window ? ONE_MILLION_CONTEXT_TOKENS : window;
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 contextWindowTokens = resolveContextWindowTokens(model, latest.contextTokens);
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 compact auto` on the NEXT Bash/Task tool
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 compact auto --json',
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 compact auto --json`). When omitted,
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;