peaks-loop 4.0.46 → 4.0.47
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 +30 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +99 -16
- package/dist/cli/commands/compact-command.js +129 -1
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/playwright-commands.js +2 -1
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +6 -3
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +49 -2
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +11 -1
- package/dist/services/code/auto-compact-lifecycle.js +11 -4
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +153 -32
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +61 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +12 -0
- package/dist/services/ide/ide-registry.js +14 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +8 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +40 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +77 -10
- package/dist/services/skills/session-start-hook-constants.d.ts +41 -0
- package/dist/services/skills/session-start-hook-constants.js +41 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/slice/slice-check-service.js +2 -1
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +51 -7
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +5 -5
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -2
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +2 -2
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -47,11 +47,16 @@ export declare const CONTEXT_WINDOW_TOKENS_ENV_VAR = "PEAKS_CONTEXT_WINDOW_TOKEN
|
|
|
47
47
|
/**
|
|
48
48
|
* Which layer produced a resolved context window:
|
|
49
49
|
* - `env-override` — `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
50
|
+
* - `harness-env` — the window peaks-loop itself wrote into the
|
|
51
|
+
* harness's machine-local settings
|
|
52
|
+
* (`CLAUDE_CODE_AUTO_COMPACT_WINDOW`, declared by
|
|
53
|
+
* the adapter as `autoCompactWindowEnvVar`); see
|
|
54
|
+
* `harness-window-config.ts`
|
|
50
55
|
* - `config` — `context.windowTokens` (`peaks config set`)
|
|
51
56
|
* - `model-heuristic` — `[1M]` suffix / `ONE_MILLION_CONTEXT_MODELS`
|
|
52
57
|
* - `default` — 200K safe default
|
|
53
58
|
*/
|
|
54
|
-
export type ContextWindowSource = 'env-override' | 'config' | 'model-heuristic' | 'default';
|
|
59
|
+
export type ContextWindowSource = 'env-override' | 'harness-env' | 'config' | 'model-heuristic' | 'default';
|
|
55
60
|
export interface ContextWindowResolution {
|
|
56
61
|
readonly tokens: number;
|
|
57
62
|
readonly source: ContextWindowSource;
|
|
@@ -61,9 +66,78 @@ export interface ContextWindowOverrides {
|
|
|
61
66
|
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
62
67
|
/** Raw `context.windowTokens` value (unvalidated — validated here). */
|
|
63
68
|
readonly configWindowTokens?: unknown;
|
|
69
|
+
/**
|
|
70
|
+
* The window peaks-loop itself configured for the harness — raw value read
|
|
71
|
+
* from the harness's machine-local settings `env` block (or the process env
|
|
72
|
+
* the harness populated from it). Resolved by the caller
|
|
73
|
+
* (`auto-compact-reader.ts` via `harness-window-config.ts`), NOT here: the
|
|
74
|
+
* settings path and the key name are per-IDE declarations.
|
|
75
|
+
*
|
|
76
|
+
* Slice 2026-09-13-auto-compact-trigger-ownership: this layer is what makes
|
|
77
|
+
* the ratio peaks-loop computes and the window the harness compacts against
|
|
78
|
+
* THE SAME NUMBER. Without it they are two independent resolutions that can
|
|
79
|
+
* drift 5× apart on a 1M-window model the model-name heuristic misses.
|
|
80
|
+
*/
|
|
81
|
+
readonly harnessWindowTokens?: unknown;
|
|
82
|
+
/**
|
|
83
|
+
* Provenance of `harnessWindowTokens`: true when peaks-loop wrote that value
|
|
84
|
+
* itself, false/omitted when a human set it. Only a peaks-written value may
|
|
85
|
+
* be overruled by the late 1M rescue — see `resolveContextWindowTokens`.
|
|
86
|
+
*/
|
|
87
|
+
readonly harnessWindowPeakWritten?: boolean;
|
|
64
88
|
/** Warning sink for an invalid override (defaults to `console.warn`). */
|
|
65
89
|
readonly onInvalidOverride?: ((message: string) => void) | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Warning sink for the E2 notice: a HUMAN pin (`env-override` / `config`)
|
|
92
|
+
* larger than peaks-loop's own model-window estimate. Deliberately separate
|
|
93
|
+
* from `onInvalidOverride` — that sink means "your value could not be read",
|
|
94
|
+
* this one means "your value was read, and peaks-loop cannot promise the
|
|
95
|
+
* harness will use it". Two meanings, two sinks. Defaults to `console.warn`.
|
|
96
|
+
*
|
|
97
|
+
* See `describeWindowAboveModelEstimate` for what the notice does and does
|
|
98
|
+
* not claim, and why it is a notice rather than a refusal.
|
|
99
|
+
*/
|
|
100
|
+
readonly onAboveModelEstimate?: ((message: string) => void) | undefined;
|
|
66
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* E2 (rid 2026-09-13-defects-e) — the notice for a pin larger than
|
|
104
|
+
* peaks-loop's model-window estimate. `null` when there is nothing to say.
|
|
105
|
+
*
|
|
106
|
+
* WHY THIS EXISTS. The harness does not take its auto-compact window on faith:
|
|
107
|
+
* it reduces the configured value with `Math.min(native, override)`, where
|
|
108
|
+
* `native` is the model's own context size. A pin ABOVE `native` is therefore
|
|
109
|
+
* accepted by peaks-loop, written to the harness's settings, and silently
|
|
110
|
+
* reduced by the harness — so peaks-loop's ratio divides by a window the
|
|
111
|
+
* harness is not compacting on. That is the A1 drift (two independent
|
|
112
|
+
* resolutions, one number meaning two things), arriving from the other side
|
|
113
|
+
* from E1: E1 is a value the harness refuses, E2 a value it quietly lowers.
|
|
114
|
+
*
|
|
115
|
+
* CAN WE READ THE NATIVE WINDOW? No, and the notice does not pretend otherwise.
|
|
116
|
+
* There is no API for it and no environment variable that carries it; the only
|
|
117
|
+
* local knowledge is `modelContextWindowTokens`'s NAME HEURISTIC (200_000, or
|
|
118
|
+
* 1_000_000 when the id says `1m` or matches the allowlist) — the very layer
|
|
119
|
+
* this slice demoted, because it is wrong for precisely the proxied /
|
|
120
|
+
* third-party models the pin exists for. It is used here ONLY as a disagreement
|
|
121
|
+
* DETECTOR, never as an authority, and the uncertainty that buys is one-sided:
|
|
122
|
+
* this notice means "peaks-loop's best guess of your model is smaller than the
|
|
123
|
+
* window you pinned", which a genuinely larger model ALSO produces. It cannot
|
|
124
|
+
* mean "the harness will definitely cap you" — only the harness knows that.
|
|
125
|
+
*
|
|
126
|
+
* WHY A NOTICE AND NOT A REFUSAL. The pin is the documented escape hatch for a
|
|
127
|
+
* model id the heuristic cannot see, so refusing would delete the feature. The
|
|
128
|
+
* notice keeps it and states the risk.
|
|
129
|
+
*
|
|
130
|
+
* WHY ONLY THE HUMAN PINS. The `harness-env` layer is a value peaks-loop FOUND
|
|
131
|
+
* in the harness's own file, not one this call is setting; its disagreements are
|
|
132
|
+
* already reported by the writer (`harnessWindowSyncWarning`), and warning here
|
|
133
|
+
* would fire on every probe of a project whose file legitimately says 1000000.
|
|
134
|
+
*/
|
|
135
|
+
export declare function describeWindowAboveModelEstimate(input: {
|
|
136
|
+
readonly model: string;
|
|
137
|
+
readonly tokens: number;
|
|
138
|
+
/** Human-readable name of the pin, e.g. `PEAKS_CONTEXT_WINDOW_TOKENS="500000"`. */
|
|
139
|
+
readonly pin: string;
|
|
140
|
+
}): string | null;
|
|
67
141
|
/**
|
|
68
142
|
* Parse an explicit context-window override. Accepts a positive finite
|
|
69
143
|
* integer only (number, or a numeric string so an env var works); anything
|
|
@@ -76,11 +150,41 @@ export declare function parseContextWindowOverride(raw: unknown): number | null;
|
|
|
76
150
|
* (first hit wins):
|
|
77
151
|
* 1. env `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
78
152
|
* 2. config `context.windowTokens`
|
|
79
|
-
* 3.
|
|
80
|
-
*
|
|
153
|
+
* 3. `harnessWindowTokens` — the window peaks-loop configured for the
|
|
154
|
+
* harness (`autoCompactWindowEnvVar`)
|
|
155
|
+
* 4. model-name heuristic (`modelContextWindowTokens`)
|
|
156
|
+
* 5. `DEFAULT_CONTEXT_WINDOW_TOKENS` (200_000)
|
|
81
157
|
*
|
|
82
158
|
* An invalid explicit override is ignored with a warning and falls through
|
|
83
159
|
* to the next layer — a typo must never crash or silently win the probe.
|
|
160
|
+
*
|
|
161
|
+
* Slice 2026-09-13-auto-compact-trigger-ownership — why layer 3 exists:
|
|
162
|
+
* The hard constraint is that the window peaks-loop divides by must BE the
|
|
163
|
+
* window it configured for the harness. Layer 3 is peaks-loop's own output
|
|
164
|
+
* read back from the harness's settings file, so the two sides reference one
|
|
165
|
+
* artifact rather than two independent resolutions. It sits ABOVE the model
|
|
166
|
+
* heuristic because a heuristic contradicting the number both sides already
|
|
167
|
+
* use is precisely how the 5× drift arose (a 1M-window model measured
|
|
168
|
+
* against a 200K guess).
|
|
169
|
+
*
|
|
170
|
+
* Why it sits BELOW the two explicit pins. Both are human declarations of
|
|
171
|
+
* intent, and a pin that loses to a value peaks-loop wrote earlier is a
|
|
172
|
+
* silently ignored setting: the user changes `context.windowTokens`, the
|
|
173
|
+
* stale harness key shadows it, and nothing appears to happen. Conflict is
|
|
174
|
+
* instead resolved by PROPAGATION — the probe that resolves the pin also
|
|
175
|
+
* syncs it into the harness (`syncHarnessWindowForProject`), so peaks-loop's
|
|
176
|
+
* ratio and the harness's window are back on one number before that command
|
|
177
|
+
* returns. The conflict is therefore transient and self-healing rather than
|
|
178
|
+
* either silent shadowing or permanent drift.
|
|
179
|
+
*
|
|
180
|
+
* Why layer 3 is not simply authoritative (the ratchet). Reading back a value
|
|
181
|
+
* peaks-loop wrote makes peaks-loop trust its own earlier resolution, so a
|
|
182
|
+
* first-time mis-resolution (an unrecognised model defaulting to 200_000)
|
|
183
|
+
* would be written to disk and then believed forever: the ratio stays
|
|
184
|
+
* saturated at 1.0 while the session grows past the pin, and the correction
|
|
185
|
+
* that exists for exactly that case — the late 1M rescue — was disabled for
|
|
186
|
+
* this layer. Self-locking. See `resolveContextWindowTokens` for how the
|
|
187
|
+
* rescue is let back in without letting it overwrite a human's own setting.
|
|
84
188
|
*/
|
|
85
189
|
export declare function resolveContextWindow(model: string, overrides?: ContextWindowOverrides): ContextWindowResolution;
|
|
86
190
|
export declare const CLAUDE_CODE_ADAPTER: IdeAdapter;
|
|
@@ -200,6 +200,50 @@ export function modelContextWindowTokens(model) {
|
|
|
200
200
|
* machine-wide `peaks config set --key context.windowTokens` twin.
|
|
201
201
|
*/
|
|
202
202
|
export const CONTEXT_WINDOW_TOKENS_ENV_VAR = 'PEAKS_CONTEXT_WINDOW_TOKENS';
|
|
203
|
+
/**
|
|
204
|
+
* E2 (rid 2026-09-13-defects-e) — the notice for a pin larger than
|
|
205
|
+
* peaks-loop's model-window estimate. `null` when there is nothing to say.
|
|
206
|
+
*
|
|
207
|
+
* WHY THIS EXISTS. The harness does not take its auto-compact window on faith:
|
|
208
|
+
* it reduces the configured value with `Math.min(native, override)`, where
|
|
209
|
+
* `native` is the model's own context size. A pin ABOVE `native` is therefore
|
|
210
|
+
* accepted by peaks-loop, written to the harness's settings, and silently
|
|
211
|
+
* reduced by the harness — so peaks-loop's ratio divides by a window the
|
|
212
|
+
* harness is not compacting on. That is the A1 drift (two independent
|
|
213
|
+
* resolutions, one number meaning two things), arriving from the other side
|
|
214
|
+
* from E1: E1 is a value the harness refuses, E2 a value it quietly lowers.
|
|
215
|
+
*
|
|
216
|
+
* CAN WE READ THE NATIVE WINDOW? No, and the notice does not pretend otherwise.
|
|
217
|
+
* There is no API for it and no environment variable that carries it; the only
|
|
218
|
+
* local knowledge is `modelContextWindowTokens`'s NAME HEURISTIC (200_000, or
|
|
219
|
+
* 1_000_000 when the id says `1m` or matches the allowlist) — the very layer
|
|
220
|
+
* this slice demoted, because it is wrong for precisely the proxied /
|
|
221
|
+
* third-party models the pin exists for. It is used here ONLY as a disagreement
|
|
222
|
+
* DETECTOR, never as an authority, and the uncertainty that buys is one-sided:
|
|
223
|
+
* this notice means "peaks-loop's best guess of your model is smaller than the
|
|
224
|
+
* window you pinned", which a genuinely larger model ALSO produces. It cannot
|
|
225
|
+
* mean "the harness will definitely cap you" — only the harness knows that.
|
|
226
|
+
*
|
|
227
|
+
* WHY A NOTICE AND NOT A REFUSAL. The pin is the documented escape hatch for a
|
|
228
|
+
* model id the heuristic cannot see, so refusing would delete the feature. The
|
|
229
|
+
* notice keeps it and states the risk.
|
|
230
|
+
*
|
|
231
|
+
* WHY ONLY THE HUMAN PINS. The `harness-env` layer is a value peaks-loop FOUND
|
|
232
|
+
* in the harness's own file, not one this call is setting; its disagreements are
|
|
233
|
+
* already reported by the writer (`harnessWindowSyncWarning`), and warning here
|
|
234
|
+
* would fire on every probe of a project whose file legitimately says 1000000.
|
|
235
|
+
*/
|
|
236
|
+
export function describeWindowAboveModelEstimate(input) {
|
|
237
|
+
const estimate = modelContextWindowTokens(input.model);
|
|
238
|
+
if (input.tokens <= estimate)
|
|
239
|
+
return null;
|
|
240
|
+
return (`[peaks] ${input.pin} is larger than peaks-loop's model-window estimate for ` +
|
|
241
|
+
`"${input.model}" (${estimate} tokens). peaks-loop cannot read a model's native context window — this ` +
|
|
242
|
+
`estimate is a name heuristic and may be wrong — and the harness caps its auto-compact window at that ` +
|
|
243
|
+
`native size, so if the estimate is right the harness will compact at ${estimate} while this ratio ` +
|
|
244
|
+
`divides by ${input.tokens}. If your model really does have the larger window this is expected; ` +
|
|
245
|
+
`otherwise set the value to the model's real window so the two stay in step.`);
|
|
246
|
+
}
|
|
203
247
|
/**
|
|
204
248
|
* Parse an explicit context-window override. Accepts a positive finite
|
|
205
249
|
* integer only (number, or a numeric string so an env var works); anything
|
|
@@ -219,27 +263,82 @@ export function parseContextWindowOverride(raw) {
|
|
|
219
263
|
* (first hit wins):
|
|
220
264
|
* 1. env `PEAKS_CONTEXT_WINDOW_TOKENS`
|
|
221
265
|
* 2. config `context.windowTokens`
|
|
222
|
-
* 3.
|
|
223
|
-
*
|
|
266
|
+
* 3. `harnessWindowTokens` — the window peaks-loop configured for the
|
|
267
|
+
* harness (`autoCompactWindowEnvVar`)
|
|
268
|
+
* 4. model-name heuristic (`modelContextWindowTokens`)
|
|
269
|
+
* 5. `DEFAULT_CONTEXT_WINDOW_TOKENS` (200_000)
|
|
224
270
|
*
|
|
225
271
|
* An invalid explicit override is ignored with a warning and falls through
|
|
226
272
|
* to the next layer — a typo must never crash or silently win the probe.
|
|
273
|
+
*
|
|
274
|
+
* Slice 2026-09-13-auto-compact-trigger-ownership — why layer 3 exists:
|
|
275
|
+
* The hard constraint is that the window peaks-loop divides by must BE the
|
|
276
|
+
* window it configured for the harness. Layer 3 is peaks-loop's own output
|
|
277
|
+
* read back from the harness's settings file, so the two sides reference one
|
|
278
|
+
* artifact rather than two independent resolutions. It sits ABOVE the model
|
|
279
|
+
* heuristic because a heuristic contradicting the number both sides already
|
|
280
|
+
* use is precisely how the 5× drift arose (a 1M-window model measured
|
|
281
|
+
* against a 200K guess).
|
|
282
|
+
*
|
|
283
|
+
* Why it sits BELOW the two explicit pins. Both are human declarations of
|
|
284
|
+
* intent, and a pin that loses to a value peaks-loop wrote earlier is a
|
|
285
|
+
* silently ignored setting: the user changes `context.windowTokens`, the
|
|
286
|
+
* stale harness key shadows it, and nothing appears to happen. Conflict is
|
|
287
|
+
* instead resolved by PROPAGATION — the probe that resolves the pin also
|
|
288
|
+
* syncs it into the harness (`syncHarnessWindowForProject`), so peaks-loop's
|
|
289
|
+
* ratio and the harness's window are back on one number before that command
|
|
290
|
+
* returns. The conflict is therefore transient and self-healing rather than
|
|
291
|
+
* either silent shadowing or permanent drift.
|
|
292
|
+
*
|
|
293
|
+
* Why layer 3 is not simply authoritative (the ratchet). Reading back a value
|
|
294
|
+
* peaks-loop wrote makes peaks-loop trust its own earlier resolution, so a
|
|
295
|
+
* first-time mis-resolution (an unrecognised model defaulting to 200_000)
|
|
296
|
+
* would be written to disk and then believed forever: the ratio stays
|
|
297
|
+
* saturated at 1.0 while the session grows past the pin, and the correction
|
|
298
|
+
* that exists for exactly that case — the late 1M rescue — was disabled for
|
|
299
|
+
* this layer. Self-locking. See `resolveContextWindowTokens` for how the
|
|
300
|
+
* rescue is let back in without letting it overwrite a human's own setting.
|
|
227
301
|
*/
|
|
228
302
|
export function resolveContextWindow(model, overrides = {}) {
|
|
229
303
|
const warn = overrides.onInvalidOverride ?? ((message) => console.warn(message));
|
|
304
|
+
// E2: the second sink, for a value that IS valid but that the harness may
|
|
305
|
+
// reduce. Kept separate so neither notice can be mistaken for the other.
|
|
306
|
+
const warnAboveEstimate = overrides.onAboveModelEstimate ?? ((message) => console.warn(message));
|
|
230
307
|
const envRaw = overrides.env?.[CONTEXT_WINDOW_TOKENS_ENV_VAR];
|
|
231
308
|
if (envRaw !== undefined) {
|
|
232
309
|
const parsed = parseContextWindowOverride(envRaw);
|
|
233
|
-
if (parsed !== null)
|
|
310
|
+
if (parsed !== null) {
|
|
311
|
+
const above = describeWindowAboveModelEstimate({
|
|
312
|
+
model,
|
|
313
|
+
tokens: parsed,
|
|
314
|
+
pin: `${CONTEXT_WINDOW_TOKENS_ENV_VAR}="${String(envRaw)}"`
|
|
315
|
+
});
|
|
316
|
+
if (above !== null)
|
|
317
|
+
warnAboveEstimate(above);
|
|
234
318
|
return { tokens: parsed, source: 'env-override' };
|
|
319
|
+
}
|
|
235
320
|
warn(`[peaks] ${CONTEXT_WINDOW_TOKENS_ENV_VAR}="${String(envRaw)}" is not a positive integer — ignoring the override`);
|
|
236
321
|
}
|
|
237
322
|
if (overrides.configWindowTokens !== undefined) {
|
|
238
323
|
const parsed = parseContextWindowOverride(overrides.configWindowTokens);
|
|
239
|
-
if (parsed !== null)
|
|
324
|
+
if (parsed !== null) {
|
|
325
|
+
const above = describeWindowAboveModelEstimate({
|
|
326
|
+
model,
|
|
327
|
+
tokens: parsed,
|
|
328
|
+
pin: `config context.windowTokens=${parsed}`
|
|
329
|
+
});
|
|
330
|
+
if (above !== null)
|
|
331
|
+
warnAboveEstimate(above);
|
|
240
332
|
return { tokens: parsed, source: 'config' };
|
|
333
|
+
}
|
|
241
334
|
warn(`[peaks] config context.windowTokens=${JSON.stringify(overrides.configWindowTokens)} is not a positive integer — ignoring the override`);
|
|
242
335
|
}
|
|
336
|
+
if (overrides.harnessWindowTokens !== undefined) {
|
|
337
|
+
const parsed = parseContextWindowOverride(overrides.harnessWindowTokens);
|
|
338
|
+
if (parsed !== null)
|
|
339
|
+
return { tokens: parsed, source: 'harness-env' };
|
|
340
|
+
warn(`[peaks] harness auto-compact window ${JSON.stringify(overrides.harnessWindowTokens)} is not a positive integer — ignoring the override`);
|
|
341
|
+
}
|
|
243
342
|
const heuristic = modelContextWindowTokens(model);
|
|
244
343
|
return heuristic === DEFAULT_CONTEXT_WINDOW_TOKENS
|
|
245
344
|
? { tokens: heuristic, source: 'default' }
|
|
@@ -354,11 +453,37 @@ function findLatestTranscriptUsage(filePath) {
|
|
|
354
453
|
* when the observed token count contradicts the heuristic window (tokens
|
|
355
454
|
* exceed it), the model must be ≥1M, so bump to the 1M window (the late
|
|
356
455
|
* rescue; it keeps the heuristic source tag, only the tokens change).
|
|
456
|
+
*
|
|
457
|
+
* `env-override` and `config` are in the no-bump list because a human pinned
|
|
458
|
+
* them: a pin the probe silently overrules is a setting that does not work.
|
|
459
|
+
*
|
|
460
|
+
* `harness-env` is the interesting one, and is decided by PROVENANCE:
|
|
461
|
+
*
|
|
462
|
+
* - peaks-written (`harnessWindowPeakWritten === true`): may be bumped. This
|
|
463
|
+
* is the ratchet fix. The value is peaks-loop's own earlier resolution, so
|
|
464
|
+
* believing it forever makes a first-time mis-resolution PERMANENT — a
|
|
465
|
+
* 200_000 default written to disk, re-read as the window, and exempt from
|
|
466
|
+
* the very rescue that exists to correct it, leaving the ratio saturated at
|
|
467
|
+
* 1.0 while the session grows past the pin. Evidence must be able to
|
|
468
|
+
* overrule peaks-loop's own output.
|
|
469
|
+
* - human-set (false/omitted): NOT bumpable. Here the key is a person's
|
|
470
|
+
* explicit declaration — the documented Claude Code variable, hand-edited.
|
|
471
|
+
* Overruling it would not be self-correction, it would be peaks-loop
|
|
472
|
+
* silently rewriting a setting the user chose (and persisting the rewrite).
|
|
473
|
+
*
|
|
474
|
+
* Both branches keep the single-source rule: the resolution this function
|
|
475
|
+
* returns becomes `probe.capacityTokens`, and the caller syncs exactly that
|
|
476
|
+
* back into the harness key (`syncHarnessWindowForProject`). When the rescue
|
|
477
|
+
* fires, the key is refreshed to the bumped number in the same command, so
|
|
478
|
+
* peaks-loop's ratio and the harness window are one number again — the rescue
|
|
479
|
+
* never leaves the two sides apart.
|
|
357
480
|
*/
|
|
358
481
|
function resolveContextWindowTokens(model, contextTokens, overrides = {}) {
|
|
359
482
|
const resolved = resolveContextWindow(model, overrides);
|
|
360
483
|
if (resolved.source === 'env-override' || resolved.source === 'config')
|
|
361
484
|
return resolved;
|
|
485
|
+
if (resolved.source === 'harness-env' && overrides.harnessWindowPeakWritten !== true)
|
|
486
|
+
return resolved;
|
|
362
487
|
return contextTokens > resolved.tokens
|
|
363
488
|
? { tokens: ONE_MILLION_CONTEXT_TOKENS, source: resolved.source }
|
|
364
489
|
: resolved;
|
|
@@ -376,8 +501,9 @@ function resolveContextWindowTokens(model, contextTokens, overrides = {}) {
|
|
|
376
501
|
* Window model resolution is env-first: when `envModel` is present, its id
|
|
377
502
|
* (which Claude Code stamps with the `[1M]` / `[200K]` suffix) drives the
|
|
378
503
|
* window; otherwise the transcript `message.model` is used. Explicit
|
|
379
|
-
* overrides (env `PEAKS_CONTEXT_WINDOW_TOKENS` /
|
|
380
|
-
* sit above both and are reported via
|
|
504
|
+
* overrides (env `PEAKS_CONTEXT_WINDOW_TOKENS` / the harness key peaks-loop
|
|
505
|
+
* wrote / config `context.windowTokens`) sit above both and are reported via
|
|
506
|
+
* `capacitySource`.
|
|
381
507
|
*/
|
|
382
508
|
function readClaudeTranscriptEstimate(outerSessionId, envModel, overrides = {}) {
|
|
383
509
|
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
@@ -426,7 +552,15 @@ function readContextPercentFallback(input) {
|
|
|
426
552
|
const envModel = resolveClaudeModelFromEnv(input.env);
|
|
427
553
|
const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel, {
|
|
428
554
|
env: input.env,
|
|
429
|
-
configWindowTokens: input.configWindowTokens
|
|
555
|
+
configWindowTokens: input.configWindowTokens,
|
|
556
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership: the window
|
|
557
|
+
// peaks-loop configured for the harness outranks config + heuristic
|
|
558
|
+
// (see `resolveContextWindow`). The generic reader resolved it from the
|
|
559
|
+
// adapter's own declarations — this module never names the key.
|
|
560
|
+
...(input.harnessWindowTokens !== undefined ? { harnessWindowTokens: input.harnessWindowTokens } : {}),
|
|
561
|
+
// ...and whether that value is peaks-loop's own output (bumpable) or a
|
|
562
|
+
// human's pin (not) — see `resolveContextWindowTokens`.
|
|
563
|
+
...(input.harnessWindowPeakWritten !== undefined ? { harnessWindowPeakWritten: input.harnessWindowPeakWritten } : {})
|
|
430
564
|
});
|
|
431
565
|
if (estimate !== null) {
|
|
432
566
|
return {
|
|
@@ -448,6 +582,11 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
448
582
|
settings: {
|
|
449
583
|
dirName: '.claude',
|
|
450
584
|
settingsFileName: 'settings.json',
|
|
585
|
+
// The machine-local layer Claude Code merges on top of settings.json.
|
|
586
|
+
// Declared here so the auto-compact `ide-native` hook writer reads the
|
|
587
|
+
// path off the adapter instead of assuming a Claude-specific filename —
|
|
588
|
+
// see `IdeSettingsLocation.localSettingsFileName`.
|
|
589
|
+
localSettingsFileName: 'settings.local.json',
|
|
451
590
|
resolveSettingsFile: (scope, projectRoot) => {
|
|
452
591
|
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
453
592
|
return join(root, '.claude', 'settings.json');
|
|
@@ -497,6 +636,14 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
497
636
|
compactCommand: 'claude --compact',
|
|
498
637
|
compactPathway: 'ide-native',
|
|
499
638
|
postCompactDetectCommand: 'peaks code auto-compact --json',
|
|
639
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership: the key Claude Code
|
|
640
|
+
// reads its auto-compact WINDOW from, in the machine-local `env` block.
|
|
641
|
+
// Claude Code documents it as taking precedence "over the command, the
|
|
642
|
+
// flag, and the setting", and as accepting the plain token count only
|
|
643
|
+
// (no `500k` suffix). peaks-loop writes the window it computes the ratio
|
|
644
|
+
// against here so the two sides cannot drift; see
|
|
645
|
+
// `harness-window-config.ts`.
|
|
646
|
+
autoCompactWindowEnvVar: 'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
|
500
647
|
readContextPercentFallback,
|
|
501
648
|
// Slice 2026-09-10-context-audit-and-discipline (Slice A): the vendor
|
|
502
649
|
// layout knowledge (`~/.claude/projects/**/<outerSessionId>.jsonl`)
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { IdeAdapter, IdeId } from './ide-types.js';
|
|
2
2
|
/** Get the adapter for a given IDE id. Throws on unsupported IDE. */
|
|
3
3
|
export declare function getAdapter(ide: IdeId): IdeAdapter;
|
|
4
|
+
/**
|
|
5
|
+
* Non-throwing twin of `getAdapter`. Returns `undefined` for an id with
|
|
6
|
+
* no registered adapter instead of raising.
|
|
7
|
+
*
|
|
8
|
+
* For callers that hold an IDE-shaped string which is NOT guaranteed to
|
|
9
|
+
* be an `IdeId`: `detectIdeFromEnv` returns `IdeKind` (`'claude-code' |
|
|
10
|
+
* 'trae' | 'opencode' | 'unknown'`), and `'opencode'` has no adapter
|
|
11
|
+
* registered. Casting to `IdeId` and calling `getAdapter` would throw
|
|
12
|
+
* inside a pure formatting/threshold path — the caller wants "no
|
|
13
|
+
* adapter → fall back", not an exception.
|
|
14
|
+
*/
|
|
15
|
+
export declare function tryGetAdapter(ide: string): IdeAdapter | undefined;
|
|
4
16
|
/** All registered adapter ids (insertion order). */
|
|
5
17
|
export declare function listAdapterIds(): readonly IdeId[];
|
|
6
18
|
/** All registered adapters (insertion order). */
|
|
@@ -39,6 +39,20 @@ export function getAdapter(ide) {
|
|
|
39
39
|
}
|
|
40
40
|
return adapter;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Non-throwing twin of `getAdapter`. Returns `undefined` for an id with
|
|
44
|
+
* no registered adapter instead of raising.
|
|
45
|
+
*
|
|
46
|
+
* For callers that hold an IDE-shaped string which is NOT guaranteed to
|
|
47
|
+
* be an `IdeId`: `detectIdeFromEnv` returns `IdeKind` (`'claude-code' |
|
|
48
|
+
* 'trae' | 'opencode' | 'unknown'`), and `'opencode'` has no adapter
|
|
49
|
+
* registered. Casting to `IdeId` and calling `getAdapter` would throw
|
|
50
|
+
* inside a pure formatting/threshold path — the caller wants "no
|
|
51
|
+
* adapter → fall back", not an exception.
|
|
52
|
+
*/
|
|
53
|
+
export function tryGetAdapter(ide) {
|
|
54
|
+
return ADAPTERS.get(ide);
|
|
55
|
+
}
|
|
42
56
|
/** All registered adapter ids (insertion order). */
|
|
43
57
|
export function listAdapterIds() {
|
|
44
58
|
return Array.from(ADAPTERS.keys());
|
|
@@ -25,6 +25,20 @@ export interface IdeSettingsLocation {
|
|
|
25
25
|
readonly dirName: string;
|
|
26
26
|
/** settings 文件名(部分 IDE 叫 settings.json / mcp.json) */
|
|
27
27
|
readonly settingsFileName: string;
|
|
28
|
+
/**
|
|
29
|
+
* 该 IDE 的**机器本地** settings 层文件名(Claude Code = `settings.local.json`),
|
|
30
|
+
* 与 `settingsFileName` 同目录、通常被 gitignore,IDE 读取时叠加在其上。
|
|
31
|
+
* `undefined` = 该 IDE 没有这一层;调用方此时回落到自己的默认值。
|
|
32
|
+
*
|
|
33
|
+
* peaks-loop 只在"写进去的东西是机器相关"时用这一层 —— 例如 per-OS 的
|
|
34
|
+
* hook shell shim —— 绝不写进共享的 `settingsFileName`。写 hook 的调用方
|
|
35
|
+
* (auto-compact 的 `ide-native` 通路)因此不需要知道任何 IDE 的目录/文件名,
|
|
36
|
+
* 只读适配器声明的这两个字段。
|
|
37
|
+
*
|
|
38
|
+
* 可选:既有适配器无需改动;声明了 `compactPathway: 'ide-native'` 的适配器
|
|
39
|
+
* 应当一并声明此字段,否则会落到调用方的 claude-code 默认路径。
|
|
40
|
+
*/
|
|
41
|
+
readonly localSettingsFileName?: string;
|
|
28
42
|
/** 解析出 settings.json 绝对路径 */
|
|
29
43
|
resolveSettingsFile(scope: 'project' | 'global', projectRoot: string | undefined): string;
|
|
30
44
|
/** 该 IDE 是否支持此 scope(用于清晰报错) */
|
|
@@ -178,6 +192,13 @@ export interface IdeCompactProfile {
|
|
|
178
192
|
* Slash command or shell-call to invoke compact. The orchestrator
|
|
179
193
|
* spawns this via `child_process.spawn` (shell-exec pathway) or
|
|
180
194
|
* writes it to an IDE hook file (ide-native pathway).
|
|
195
|
+
*
|
|
196
|
+
* ZERO EXECUTOR as of slice 2026-09-13-auto-compact-trigger-ownership: every
|
|
197
|
+
* `shell-exec` branch now only REPORTS that it will not spawn (see
|
|
198
|
+
* `auto-compact-dispatcher.ts`), and the `ide-native` branch registers a
|
|
199
|
+
* harness-side trigger instead. The field is retained because the sibling
|
|
200
|
+
* work (A2, harness-side state re-injection) still needs it; nothing in this
|
|
201
|
+
* slice deletes it. Do not read it as a capability peaks-loop has.
|
|
181
202
|
*/
|
|
182
203
|
readonly compactCommand: string;
|
|
183
204
|
/**
|
|
@@ -195,6 +216,19 @@ export interface IdeCompactProfile {
|
|
|
195
216
|
* adapters still on the v2.11.x model).
|
|
196
217
|
*/
|
|
197
218
|
readonly compactPathway: 'shell-exec' | 'ide-native' | 'llm-self-compress' | 'noop';
|
|
219
|
+
/**
|
|
220
|
+
* Optional settings key / env var the IDE reads its AUTO-COMPACT WINDOW
|
|
221
|
+
* from — i.e. the window the harness itself compacts against. When
|
|
222
|
+
* declared, peaks-loop writes the window it computes its ratio against
|
|
223
|
+
* into the adapter's machine-local settings `env` block, under this key,
|
|
224
|
+
* so both sides reference one number instead of two independent
|
|
225
|
+
* resolutions (slice 2026-09-13-auto-compact-trigger-ownership).
|
|
226
|
+
*
|
|
227
|
+
* `undefined` = this IDE exposes no such knob. peaks-loop then cannot
|
|
228
|
+
* close the drift; `peaks compact harness-window` reports that instead of
|
|
229
|
+
* pretending. Adapters that do not opt in are unchanged.
|
|
230
|
+
*/
|
|
231
|
+
readonly autoCompactWindowEnvVar?: string;
|
|
198
232
|
/**
|
|
199
233
|
* Optional command the runner invokes post-compact to confirm
|
|
200
234
|
* ratio dropped (e.g. `peaks code auto-compact --json`). When omitted,
|
|
@@ -263,6 +297,31 @@ export interface ContextPercentFallbackInput {
|
|
|
263
297
|
* Slice 2026-09-09-context-window-override.
|
|
264
298
|
*/
|
|
265
299
|
readonly configWindowTokens?: unknown;
|
|
300
|
+
/**
|
|
301
|
+
* Raw value of the adapter's `autoCompactWindowEnvVar`, resolved by the
|
|
302
|
+
* generic reader from the adapter's own declarations (settings path +
|
|
303
|
+
* key name) — the window peaks-loop configured for the harness. Feeds the
|
|
304
|
+
* `harness-env` layer of `resolveContextWindow`.
|
|
305
|
+
*
|
|
306
|
+
* Slice 2026-09-13-auto-compact-trigger-ownership.
|
|
307
|
+
*/
|
|
308
|
+
readonly harnessWindowTokens?: unknown;
|
|
309
|
+
/**
|
|
310
|
+
* True when `harnessWindowTokens` is a value peaks-loop ITSELF wrote (the
|
|
311
|
+
* harness settings carry the provenance marker matching it), false when a
|
|
312
|
+
* human set the key by hand.
|
|
313
|
+
*
|
|
314
|
+
* The `harness-env` layer outranks the model heuristic either way — that is
|
|
315
|
+
* what keeps peaks-loop's ratio and the harness's trigger on one number. The
|
|
316
|
+
* flag decides only whether the late 1M rescue may OVERRULE that layer when
|
|
317
|
+
* the observed context proves it too small: peaks-loop's own earlier
|
|
318
|
+
* resolution is self-correcting, whereas a human's explicit pin must not be
|
|
319
|
+
* silently rewritten (and, since it is persisted, permanently so).
|
|
320
|
+
*
|
|
321
|
+
* Omitted = treated as not peaks-written (the safe direction: do not touch
|
|
322
|
+
* what you did not write).
|
|
323
|
+
*/
|
|
324
|
+
readonly harnessWindowPeakWritten?: boolean;
|
|
266
325
|
}
|
|
267
326
|
/**
|
|
268
327
|
* Per-IDE standards-file location + format profile. Used by the
|
|
@@ -20,7 +20,7 @@ function packageNameFor(key) {
|
|
|
20
20
|
}
|
|
21
21
|
function probeNpx() {
|
|
22
22
|
const { command, args, baseEnv } = resolveNpxInvocation(['--version']);
|
|
23
|
-
const probe = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
|
|
23
|
+
const probe = spawnSync(command, args, { encoding: 'utf8', env: baseEnv, windowsHide: true });
|
|
24
24
|
return probe.status === 0;
|
|
25
25
|
}
|
|
26
26
|
/** Named code for "npm itself could not be launched" — distinct from a registry miss. */
|
|
@@ -35,7 +35,7 @@ function probePackage(key) {
|
|
|
35
35
|
// own JS entry and this runs it through `process.execPath`. Same shape as the
|
|
36
36
|
// npx probe above and as `eslint-runner.ts`.
|
|
37
37
|
const { command, args, baseEnv } = resolveNpmInvocation(['view', `${pkg}@${pin}`, 'version']);
|
|
38
|
-
const result = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
|
|
38
|
+
const result = spawnSync(command, args, { encoding: 'utf8', env: baseEnv, windowsHide: true });
|
|
39
39
|
return {
|
|
40
40
|
ok: result.status === 0,
|
|
41
41
|
error: result.error === undefined || result.error === null ? null : result.error.message
|
|
@@ -91,7 +91,8 @@ function loadDiffRanges(cwd) {
|
|
|
91
91
|
const result = spawnSync('git', ['diff', 'HEAD', '--unified=0', '--no-color'], {
|
|
92
92
|
cwd,
|
|
93
93
|
encoding: 'utf8',
|
|
94
|
-
maxBuffer: DIFF_BUFFER_BYTES
|
|
94
|
+
maxBuffer: DIFF_BUFFER_BYTES,
|
|
95
|
+
windowsHide: true
|
|
95
96
|
});
|
|
96
97
|
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
97
98
|
return [];
|
|
@@ -260,6 +261,7 @@ export function runEslint(options) {
|
|
|
260
261
|
const spawnOptions = {
|
|
261
262
|
cwd: projectRoot,
|
|
262
263
|
encoding: 'utf8',
|
|
264
|
+
windowsHide: true,
|
|
263
265
|
timeout: options.timeoutMs ?? ESLINT_DEFAULT_TIMEOUT_MS,
|
|
264
266
|
maxBuffer: OUTPUT_BUFFER_BYTES
|
|
265
267
|
};
|
|
@@ -60,7 +60,8 @@ function execPeaks(args, cwd, peaksBin) {
|
|
|
60
60
|
const stdout = execFileSync(cmd[0], [...cmd.slice(1), ...args], {
|
|
61
61
|
cwd,
|
|
62
62
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
|
-
encoding: 'utf8'
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
windowsHide: true
|
|
64
65
|
});
|
|
65
66
|
return { stdout, exitCode: 0 };
|
|
66
67
|
}
|
|
@@ -136,7 +136,7 @@ export function executeProjectMemoryBackup(options) {
|
|
|
136
136
|
const safeMemoryDir = assertSafeProjectMemoryDir(plan.projectRoot);
|
|
137
137
|
mkdirSync(plan.backupMemoryDir, { recursive: true });
|
|
138
138
|
for (const copy of plan.plannedCopies) {
|
|
139
|
-
const sourcePath = realPathOrThrow(copy.sourcePath, 'Project memory source must stay inside the project memory directory');
|
|
139
|
+
const sourcePath = realPathOrThrow(copy.sourcePath, 'Project memory source must stay inside the project memory directory', 'Project memory source does not exist');
|
|
140
140
|
if (!isInsidePath(sourcePath, stableRealPath(safeMemoryDir))) {
|
|
141
141
|
throw new Error('Project memory source must stay inside the project memory directory');
|
|
142
142
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export declare function normalizeRoot(path: string): string;
|
|
2
2
|
export declare function normalizeRealRoot(path: string): string;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Resolve `path` to its realpath, refusing missing paths and symlinks.
|
|
5
|
+
*
|
|
6
|
+
* `missingPathMessage` is required because the two refusals mean different
|
|
7
|
+
* things to the user: "there is nothing at this path" is a caller typo, while
|
|
8
|
+
* "this path is a symlink" is an escape attempt. A single shared message made
|
|
9
|
+
* `peaks memory extract --artifact <typo>` report a sandbox-escape warning.
|
|
10
|
+
*/
|
|
11
|
+
export declare function realPathOrThrow(path: string, errorMessage: string, missingPathMessage: string): string;
|
|
4
12
|
export declare function resolveProjectPath(path: string, projectRoot: string): string;
|
|
5
13
|
export declare function assertInsideProject(path: string, projectRoot: string): string;
|
|
6
14
|
export declare function assertSafeProjectMemoryDir(projectRoot: string): string;
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// canonical form the rest of the codebase expects (handles input-path
|
|
9
9
|
// quirks from the path-utils layer).
|
|
10
10
|
// - `realPathOrThrow` — refuses symlinks and missing paths. Throws with
|
|
11
|
-
// a stable message
|
|
12
|
-
// attempt"
|
|
11
|
+
// a stable, path-free message, and takes ONE MESSAGE PER REASON, so
|
|
12
|
+
// callers can distinguish "missing" from "escape attempt" by the
|
|
13
|
+
// message they supplied.
|
|
13
14
|
// - `assertInsideProject` — confirms an artifact path resolves inside
|
|
14
15
|
// the project root after realpath, throwing the same kind of error
|
|
15
16
|
// message realPathOrThrow uses.
|
|
@@ -32,9 +33,17 @@ export function normalizeRoot(path) {
|
|
|
32
33
|
export function normalizeRealRoot(path) {
|
|
33
34
|
return stableRealPath(path);
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `path` to its realpath, refusing missing paths and symlinks.
|
|
38
|
+
*
|
|
39
|
+
* `missingPathMessage` is required because the two refusals mean different
|
|
40
|
+
* things to the user: "there is nothing at this path" is a caller typo, while
|
|
41
|
+
* "this path is a symlink" is an escape attempt. A single shared message made
|
|
42
|
+
* `peaks memory extract --artifact <typo>` report a sandbox-escape warning.
|
|
43
|
+
*/
|
|
44
|
+
export function realPathOrThrow(path, errorMessage, missingPathMessage) {
|
|
36
45
|
if (!existsSync(path)) {
|
|
37
|
-
throw new Error(
|
|
46
|
+
throw new Error(missingPathMessage);
|
|
38
47
|
}
|
|
39
48
|
const stats = lstatSync(path);
|
|
40
49
|
if (stats.isSymbolicLink()) {
|
|
@@ -53,8 +62,8 @@ export function resolveProjectPath(path, projectRoot) {
|
|
|
53
62
|
export function assertInsideProject(path, projectRoot) {
|
|
54
63
|
const resolvedRoot = normalizeRoot(projectRoot);
|
|
55
64
|
const resolvedPath = resolveProjectPath(path, resolvedRoot);
|
|
56
|
-
const realProjectRoot = realPathOrThrow(resolvedRoot, 'Project root is not accessible');
|
|
57
|
-
const realArtifactPath = realPathOrThrow(resolvedPath, 'Artifact path must stay inside the project root');
|
|
65
|
+
const realProjectRoot = realPathOrThrow(resolvedRoot, 'Project root is not accessible', 'Project root does not exist');
|
|
66
|
+
const realArtifactPath = realPathOrThrow(resolvedPath, 'Artifact path must stay inside the project root', 'Artifact path does not exist');
|
|
58
67
|
if (!isInsidePath(realArtifactPath, realProjectRoot)) {
|
|
59
68
|
throw new Error('Artifact path must stay inside the project root');
|
|
60
69
|
}
|
|
@@ -34,7 +34,8 @@ export interface PrecheckOptions {
|
|
|
34
34
|
export interface PrecheckEnvelope {
|
|
35
35
|
readonly ok: boolean;
|
|
36
36
|
readonly overall: LayerStatus;
|
|
37
|
-
|
|
37
|
+
/** `null` when the root `package.json` is missing or unreadable — see `rootVsShared`. */
|
|
38
|
+
readonly rootVersion: string | null;
|
|
38
39
|
readonly strict: boolean;
|
|
39
40
|
readonly snapshotAt: string;
|
|
40
41
|
readonly layers: {
|