peaks-loop 4.0.46 → 4.0.48

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 (207) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/agents/karpathy-reviewer.md +11 -10
  5. package/dist/cli/cli-helpers.d.ts +34 -0
  6. package/dist/cli/cli-helpers.js +57 -0
  7. package/dist/cli/commands/code-job-shape-commands.js +8 -0
  8. package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
  9. package/dist/cli/commands/code-runtime-commands.js +139 -16
  10. package/dist/cli/commands/compact-command.js +241 -1
  11. package/dist/cli/commands/config-commands.js +15 -9
  12. package/dist/cli/commands/container-commands.js +3 -3
  13. package/dist/cli/commands/core/skill-command.js +45 -10
  14. package/dist/cli/commands/cron-commands.js +2 -1
  15. package/dist/cli/commands/dashboard-long-run.js +6 -0
  16. package/dist/cli/commands/dispatch-commands.js +11 -1
  17. package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
  18. package/dist/cli/commands/e2e-verify.js +3 -3
  19. package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
  20. package/dist/cli/commands/hooks-commands.js +14 -5
  21. package/dist/cli/commands/job-commands.js +8 -0
  22. package/dist/cli/commands/loop-commands.js +1 -0
  23. package/dist/cli/commands/loop-eval-commands.js +15 -0
  24. package/dist/cli/commands/perf-audit-commands.js +2 -0
  25. package/dist/cli/commands/playwright-commands.js +14 -1
  26. package/dist/cli/commands/prd-commands.js +1 -1
  27. package/dist/cli/commands/qa-commands.js +22 -0
  28. package/dist/cli/commands/reinject-command.d.ts +72 -0
  29. package/dist/cli/commands/reinject-command.js +174 -0
  30. package/dist/cli/commands/request-commands.js +14 -3
  31. package/dist/cli/commands/scan-commands.js +1 -1
  32. package/dist/cli/commands/security-audit-commands.js +2 -0
  33. package/dist/cli/commands/shadcn-commands.js +1 -0
  34. package/dist/cli/commands/slice-integrate-commands.js +5 -0
  35. package/dist/cli/commands/statusline-commands.js +44 -4
  36. package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
  37. package/dist/cli/commands/sub-agent/detached.js +47 -22
  38. package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
  39. package/dist/cli/commands/test-commands.js +2 -1
  40. package/dist/cli/commands/verdict-aggregate-command.js +95 -13
  41. package/dist/cli/commands/vm-commands.js +7 -7
  42. package/dist/cli/commands/workflow-commands.js +1 -1
  43. package/dist/cli/commands/workspace/init-command.js +24 -2
  44. package/dist/cli/commands/worktree-lease-commands.js +4 -4
  45. package/dist/cli/index.js +10 -3
  46. package/dist/cli/program.js +5 -0
  47. package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
  48. package/dist/services/adapter/adapter-registry.js +1 -1
  49. package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
  50. package/dist/services/artifacts/artifact-prerequisites.js +130 -65
  51. package/dist/services/artifacts/artifact-service.js +1 -1
  52. package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
  53. package/dist/services/artifacts/request-artifact-service.js +18 -8
  54. package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
  55. package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
  56. package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
  57. package/dist/services/audit-independent/perf-audit-service.js +27 -5
  58. package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
  59. package/dist/services/audit-independent/security-audit-service.js +28 -6
  60. package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
  61. package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
  62. package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
  63. package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
  64. package/dist/services/code/auto-compact-lifecycle.d.ts +130 -1
  65. package/dist/services/code/auto-compact-lifecycle.js +180 -4
  66. package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
  67. package/dist/services/code/auto-compact-orchestrator.js +166 -34
  68. package/dist/services/code/compact-event-settle.d.ts +122 -0
  69. package/dist/services/code/compact-event-settle.js +219 -0
  70. package/dist/services/code/orchestrator-can-do.d.ts +4 -2
  71. package/dist/services/code/orchestrator-can-do.js +37 -5
  72. package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
  73. package/dist/services/codegraph/codegraph-process-runner.js +3 -2
  74. package/dist/services/compact/request-transition-hook.js +5 -2
  75. package/dist/services/compact-history/compact-history-service.d.ts +75 -0
  76. package/dist/services/compact-history/compact-history-service.js +49 -0
  77. package/dist/services/config/config-restore.d.ts +12 -1
  78. package/dist/services/config/config-restore.js +35 -4
  79. package/dist/services/config/config-rollback.js +6 -1
  80. package/dist/services/config/config-safety.d.ts +52 -0
  81. package/dist/services/config/config-safety.js +75 -1
  82. package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
  83. package/dist/services/context/auto-compact-dispatcher.js +113 -40
  84. package/dist/services/context/auto-compact-reader.d.ts +68 -28
  85. package/dist/services/context/auto-compact-reader.js +155 -1
  86. package/dist/services/context/auto-compact-types.d.ts +89 -12
  87. package/dist/services/context/auto-compact-types.js +16 -32
  88. package/dist/services/context/harness-context-witness.d.ts +310 -0
  89. package/dist/services/context/harness-context-witness.js +606 -0
  90. package/dist/services/context/harness-window-config.d.ts +412 -0
  91. package/dist/services/context/harness-window-config.js +607 -0
  92. package/dist/services/context/main-session-monitor.d.ts +27 -0
  93. package/dist/services/context/main-session-monitor.js +32 -1
  94. package/dist/services/context/post-compact-reinjection.d.ts +221 -0
  95. package/dist/services/context/post-compact-reinjection.js +491 -0
  96. package/dist/services/dispatch/merge-back-runner.js +5 -5
  97. package/dist/services/dispatch/service-shutdown.js +3 -3
  98. package/dist/services/doc/doc-generator.js +2 -1
  99. package/dist/services/env/shell-probe.js +1 -1
  100. package/dist/services/evidence/evidence-generator.js +86 -49
  101. package/dist/services/final-review/final-review-service.d.ts +9 -0
  102. package/dist/services/final-review/final-review-service.js +36 -12
  103. package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
  104. package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
  105. package/dist/services/hooks/auto-compact-hook-install.js +8 -0
  106. package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
  107. package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
  108. package/dist/services/ide/ide-registry.d.ts +31 -0
  109. package/dist/services/ide/ide-registry.js +35 -0
  110. package/dist/services/ide/ide-types.d.ts +59 -0
  111. package/dist/services/job/job-state-store.js +7 -0
  112. package/dist/services/lint/detect-eslint.js +2 -2
  113. package/dist/services/lint/eslint-runner.js +3 -1
  114. package/dist/services/loop/evaluator-dispatcher.js +2 -1
  115. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
  116. package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
  117. package/dist/services/memory/project-memory-service/store/paths.js +15 -6
  118. package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
  119. package/dist/services/prd/best-practice-auto-trigger.js +1 -0
  120. package/dist/services/prd/handoff-auto-regen.js +31 -27
  121. package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
  122. package/dist/services/prd/handoff-frontmatter.js +75 -0
  123. package/dist/services/prd/handoff-service.d.ts +41 -2
  124. package/dist/services/prd/handoff-service.js +81 -8
  125. package/dist/services/prd/handoff-types.d.ts +3 -2
  126. package/dist/services/prd/handoff-types.js +3 -2
  127. package/dist/services/qa/qa-business-review-state.js +9 -0
  128. package/dist/services/release/version-precheck-service.d.ts +2 -1
  129. package/dist/services/release/version-precheck-service.js +82 -12
  130. package/dist/services/runtime/vendor-adapter.d.ts +29 -4
  131. package/dist/services/runtime/vendors/claude-code.js +1 -1
  132. package/dist/services/runtime/vendors/codex.js +1 -1
  133. package/dist/services/runtime/vendors/copilot.js +1 -1
  134. package/dist/services/sc/sc-service.js +1 -1
  135. package/dist/services/scan/diff-scope-service.js +2 -2
  136. package/dist/services/scan/file-size-scan.js +2 -2
  137. package/dist/services/scan/karpathy-service.js +2 -2
  138. package/dist/services/scan/orphan-service.js +2 -1
  139. package/dist/services/scan/type-sanity-service.js +2 -2
  140. package/dist/services/session/session-checkpoint-service.js +8 -0
  141. package/dist/services/skill/resume-detector.js +29 -11
  142. package/dist/services/skillhub/tar-runtime.js +1 -0
  143. package/dist/services/skills/hooks-codegate-superpowers.d.ts +14 -0
  144. package/dist/services/skills/hooks-codegate-superpowers.js +99 -3
  145. package/dist/services/skills/hooks-settings-service.d.ts +12 -0
  146. package/dist/services/skills/hooks-settings-service.js +91 -14
  147. package/dist/services/skills/session-start-hook-constants.d.ts +86 -0
  148. package/dist/services/skills/session-start-hook-constants.js +86 -0
  149. package/dist/services/skills/skill-presence-service.js +9 -0
  150. package/dist/services/skills/skill-statusline-service.d.ts +14 -0
  151. package/dist/services/slice/slice-check-service.js +31 -12
  152. package/dist/services/slice/slice-decompose-runners.js +2 -1
  153. package/dist/services/slice/slice-review-state.js +8 -0
  154. package/dist/services/upgrade/upgrade-service.js +1 -0
  155. package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
  156. package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
  157. package/dist/services/workflow/pipeline-verify-service.js +24 -23
  158. package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
  159. package/dist/services/workflow/workflow-skip-service.js +2 -1
  160. package/dist/services/workspace/claude-settings-template.d.ts +56 -8
  161. package/dist/services/workspace/claude-settings-template.js +98 -20
  162. package/dist/services/workspace/migrate-service.js +1 -1
  163. package/dist/services/workspace/workspace-claude-settings-materializer.js +124 -9
  164. package/dist/services/workspace/workspace-service.js +8 -0
  165. package/dist/services/worktree/host-worktree-reconciler.js +1 -0
  166. package/dist/services/worktree/long-path-cleanup.js +3 -2
  167. package/dist/shared/process.js +1 -1
  168. package/package.json +6 -6
  169. package/scripts/install-skills.mjs +1 -0
  170. package/scripts/watch.mjs +3 -1
  171. package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
  172. package/skills/bee/peaks-prd/SKILL.md +8 -6
  173. package/skills/bee/peaks-qa/SKILL.md +7 -7
  174. package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
  175. package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
  176. package/skills/bee/peaks-rd/SKILL.md +10 -8
  177. package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
  178. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
  179. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
  180. package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
  181. package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
  182. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
  183. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  184. package/skills/bee/peaks-sc/SKILL.md +1 -1
  185. package/skills/bee/peaks-security-audit/SKILL.md +1 -1
  186. package/skills/bee/peaks-txt/SKILL.md +1 -1
  187. package/skills/bee/peaks-ui/SKILL.md +1 -1
  188. package/skills/peaks-audit/SKILL.md +1 -1
  189. package/skills/peaks-code/SKILL.md +3 -3
  190. package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
  191. package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
  192. package/skills/peaks-code/references/resume-detection.md +13 -7
  193. package/skills/peaks-code/references/runbook.md +3 -2
  194. package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
  195. package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
  196. package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
  197. package/skills/peaks-content/SKILL.md +1 -1
  198. package/skills/peaks-doctor/SKILL.md +1 -1
  199. package/skills/peaks-final-review/SKILL.md +1 -1
  200. package/skills/peaks-ide/SKILL.md +1 -1
  201. package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
  202. package/skills/peaks-resume/SKILL.md +1 -1
  203. package/skills/peaks-slice-decompose/SKILL.md +1 -1
  204. package/skills/peaks-solo/SKILL.md +1 -1
  205. package/skills/peaks-sop/SKILL.md +1 -1
  206. package/skills/peaks-status/SKILL.md +1 -1
  207. package/skills/peaks-test/SKILL.md +1 -1
@@ -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. model-name heuristic (`modelContextWindowTokens`)
223
- * 4. `DEFAULT_CONTEXT_WINDOW_TOKENS` (200_000)
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` / config `context.windowTokens`)
380
- * sit above both and are reported via `capacitySource`.
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,8 +1,39 @@
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[];
18
+ /**
19
+ * Help text for every `--ide <id>` option, derived from the registry.
20
+ *
21
+ * It used to be a hand-written literal — `"target adapter id (claude-code |
22
+ * trae); default: auto-detect from env/cwd"` — copy-pasted into six option
23
+ * declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
24
+ * time anyone read it the registry had nine adapters, so the help named two of
25
+ * the nine values the option accepts and stayed silent about the other seven.
26
+ * A help string that enumerates a set it does not own cannot be kept true by
27
+ * discipline; deriving it here is the only form that cannot drift.
28
+ *
29
+ * Note for whoever is tempted to point the help at `peaks adapter list`
30
+ * instead: that command lists USER-REGISTERED vendor adapters from
31
+ * `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
32
+ * project). It is not this set. The nine ids below have no CLI surface of
33
+ * their own; `peaks ide model --current` is the closest read-only viewer and
34
+ * it reports one adapter, not the list.
35
+ */
36
+ export declare function resolveIdeOptionHelp(): string;
6
37
  /** All registered adapters (insertion order). */
7
38
  export declare function listAdapters(): readonly IdeAdapter[];
8
39
  /**
@@ -39,10 +39,45 @@ 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());
45
59
  }
60
+ /**
61
+ * Help text for every `--ide <id>` option, derived from the registry.
62
+ *
63
+ * It used to be a hand-written literal — `"target adapter id (claude-code |
64
+ * trae); default: auto-detect from env/cwd"` — copy-pasted into six option
65
+ * declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
66
+ * time anyone read it the registry had nine adapters, so the help named two of
67
+ * the nine values the option accepts and stayed silent about the other seven.
68
+ * A help string that enumerates a set it does not own cannot be kept true by
69
+ * discipline; deriving it here is the only form that cannot drift.
70
+ *
71
+ * Note for whoever is tempted to point the help at `peaks adapter list`
72
+ * instead: that command lists USER-REGISTERED vendor adapters from
73
+ * `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
74
+ * project). It is not this set. The nine ids below have no CLI surface of
75
+ * their own; `peaks ide model --current` is the closest read-only viewer and
76
+ * it reports one adapter, not the list.
77
+ */
78
+ export function resolveIdeOptionHelp() {
79
+ return `target adapter id (${listAdapterIds().join(' | ')}); default: auto-detect from env/cwd`;
80
+ }
46
81
  /** All registered adapters (insertion order). */
47
82
  export function listAdapters() {
48
83
  return Array.from(ADAPTERS.values());
@@ -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
@@ -1,12 +1,19 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { JobStateSchema } from './job-types.js';
4
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
4
5
  export class JobStateStore {
5
6
  rootDir;
6
7
  constructor(rootDir) {
7
8
  this.rootDir = rootDir;
8
9
  }
9
10
  jobDir(jobId) {
11
+ // JobId axis. This is the only join the store performs, so one guard here
12
+ // covers every `peaks job *` subcommand — ten CLI call sites hand the
13
+ // store a caller-supplied `--job-id` and none of them checked it.
14
+ if (isUnsafePathInput(jobId)) {
15
+ throw new Error(`Invalid job id: ${jobId} (must be a single path segment)`);
16
+ }
10
17
  return join(this.rootDir, jobId);
11
18
  }
12
19
  init(input) {
@@ -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
- export declare function realPathOrThrow(path: string, errorMessage: string): string;
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 so callers can distinguish "missing" from "escape
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
- export function realPathOrThrow(path, errorMessage) {
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(errorMessage);
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
  }
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
27
27
  import { dirname, join } from 'node:path';
28
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
28
29
  /** Compute the child-side path where the artifact should be mirrored.
29
30
  * Mirrors the parent shape exactly: `.peaks/_runtime/<sid>/<role>/`. */
30
31
  function childArtifactPath(childRoot, sid, role, sourcePath) {
@@ -37,6 +38,16 @@ function ensureDirFor(file) {
37
38
  export function dispatchArtifact(opts) {
38
39
  const warnings = [];
39
40
  const perChild = [];
41
+ // `sid` becomes a path segment in every child (`childArtifactPath`). It has no
42
+ // pinned format (`--sid` is a free-form override), so it gets the repo's
43
+ // segment check — the same `isUnsafePathInput` `peaks evidence generate` and
44
+ // `peaks verdict aggregate` apply to their session id. Guard before the first
45
+ // mkdir so a rejected dispatch leaves nothing behind. Measured before this
46
+ // line existed: `--sid '../../../../../../PWNED-SID-ESCAPE'` wrote
47
+ // `<parent-of-fixture>/PWNED-SID-ESCAPE/prd/src.md`, above every project root.
48
+ if (isUnsafePathInput(opts.sid)) {
49
+ throw new Error(`Invalid session id: ${opts.sid} (must be a single path segment)`);
50
+ }
40
51
  // Validate targets against the manifest.
41
52
  const knownIds = new Set(opts.manifest.children.map((c) => c.id));
42
53
  const validTargets = [];
@@ -105,6 +105,7 @@ export async function triggerBestPracticeScan(opts) {
105
105
  cwd: opts.projectRoot,
106
106
  stdio: 'ignore',
107
107
  detached: true,
108
+ windowsHide: true,
108
109
  env: { ...process.env, PEAKS_BEST_PRACTICE_STDIN: '' }
109
110
  });
110
111
  child.once('error', (err) => {