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
@@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { dirname, isAbsolute, relative, resolve } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
+ import { projectRootsMatch } from '../../shared/path-utils.js';
6
7
  export function getUserConfigPath() {
7
8
  return resolve(homedir(), '.peaks', 'config.json');
8
9
  }
@@ -202,7 +203,8 @@ function resolveProjectRootFromGit(startPath) {
202
203
  const stdout = execFileSync('git', ['rev-parse', '--show-toplevel'], {
203
204
  cwd: startPath,
204
205
  encoding: 'utf8',
205
- stdio: ['ignore', 'pipe', 'ignore']
206
+ stdio: ['ignore', 'pipe', 'ignore'],
207
+ windowsHide: true
206
208
  });
207
209
  const trimmed = stdout.trim();
208
210
  if (trimmed.length === 0)
@@ -227,6 +229,78 @@ function resolveProjectRootFromGit(startPath) {
227
229
  return rawRoot;
228
230
  }
229
231
  }
232
+ /**
233
+ * True when `projectRoot` IS the user's home directory itself.
234
+ *
235
+ * Exact match, NOT `isInsidePath`: `~/my-project` is an ordinary project and
236
+ * must keep working — the guard exists for the one directory that is never a
237
+ * project.
238
+ *
239
+ * `projectRootsMatch` rather than `===` because the two strings reach this
240
+ * function from different sources: a `--project .` argument arrives in the
241
+ * caller's spelling while `homedir()` yields the OS form (`C:\Users\x` vs
242
+ * `C:/Users/x`, and differing case on win32/darwin).
243
+ *
244
+ * This is the ONE definition of "the user's home" in the codebase; the
245
+ * harness-window writer's own guard delegates here so the two cannot drift.
246
+ */
247
+ export function isUserHomeProjectRoot(projectRoot) {
248
+ try {
249
+ return projectRootsMatch(projectRoot, homedir());
250
+ }
251
+ catch {
252
+ // A path that cannot be resolved is not a claim about the home directory.
253
+ return false;
254
+ }
255
+ }
256
+ /**
257
+ * Thrown by `assertWritableProjectRoot` / `resolveWritableProjectRoot` when a
258
+ * write would land in the user's home directory. Distinct from
259
+ * `InvalidProjectRootError` (which is about a malformed/absent path): this path
260
+ * is perfectly valid — it is just not a project.
261
+ */
262
+ export class UnsafeProjectRootError extends Error {
263
+ projectRoot;
264
+ constructor(projectRoot) {
265
+ super(`Refusing to write into the user's home directory: "${projectRoot}" is not a project root.`);
266
+ this.projectRoot = projectRoot;
267
+ this.name = 'UnsafeProjectRootError';
268
+ }
269
+ }
270
+ /**
271
+ * Refuse a project root that IS the user's home directory.
272
+ *
273
+ * The hazard this closes: a fresh terminal starts in `$HOME`, and
274
+ * `peaks workspace init --project .` there resolved to `$HOME` (via
275
+ * `resolveCanonicalProjectRoot`'s fail-open fallback, which returns the start
276
+ * path when it can find no project marker) and then materialized a whole
277
+ * project tree INTO the user's home — `.claude/settings.local.json` above all,
278
+ * which is a live config file shared with every other project on the machine.
279
+ *
280
+ * Deliberately NOT enforced inside `resolveCanonicalProjectRoot`. That helper
281
+ * is called from 85 sites across 32 files, most of them READS (`peaks status`,
282
+ * `peaks dashboard`, `peaks memory`, the observability commands). Refusing
283
+ * there would break every one of them the moment a user runs a read command
284
+ * from their home directory, and its fail-open contract is documented and
285
+ * depended on. The hazard is a WRITE to `$HOME/.claude/`, so the guard belongs
286
+ * on write paths — this function, plus `resolveWritableProjectRoot` so a
287
+ * writer only has to make one call.
288
+ */
289
+ export function assertWritableProjectRoot(projectRoot) {
290
+ if (isUserHomeProjectRoot(projectRoot)) {
291
+ throw new UnsafeProjectRootError(projectRoot);
292
+ }
293
+ }
294
+ /**
295
+ * `resolveCanonicalProjectRoot` for a path that is about to be WRITTEN to:
296
+ * same git-root-then-heuristic promotion, plus the home-directory refusal.
297
+ * Read paths keep using the fail-open helper.
298
+ */
299
+ export function resolveWritableProjectRoot(startPath) {
300
+ const root = resolveCanonicalProjectRoot(startPath);
301
+ assertWritableProjectRoot(root);
302
+ return root;
303
+ }
230
304
  export function getProjectConfigPath(projectRoot) {
231
305
  if (!projectRoot)
232
306
  return null;
@@ -1,37 +1,3 @@
1
- /**
2
- * AC-3 — IDE-aware compact dispatcher.
3
- *
4
- * Reads the active IDE's `IdeAdapter.compact` profile and dispatches
5
- * compact via the adapter-declared pathway. No hard-coded IDE names
6
- * — Claude Code is the MVP that fills `compact`; other adapters
7
- * register their own `compact` profile as L2-dogfood verifies
8
- * each IDE's actual compact surface.
9
- *
10
- * Pathway dispatch:
11
- *
12
- * - `ide-native` — peaks-loop writes the compact intent to
13
- * the IDE's hook file (per
14
- * `IdeSettingsLocation`). Used when the IDE
15
- * requires a registered hook rather than a
16
- * runtime command.
17
- * - `llm-self-compress` — peaks-loop records the intent and
18
- * returns `ok: true` with `pathway` echoed.
19
- * The LLM does its own context summarization
20
- * on the next turn. Always available; least
21
- * precise fallback.
22
- * - `noop` — adapter explicitly opted out. peaks-loop
23
- * returns `ok: false` with `message: 'noop'`.
24
- * Used by legacy / unverified adapters.
25
- *
26
- * rid-031 (2026-07-28): `shell-exec` pathway is DEPRECATED. The
27
- * `case 'shell-exec':` branch remains reachable only to keep the
28
- * 2 currently-passing tests in `tests/unit/context/auto-compact-main-target.test.ts:58`
29
- * + `tests/unit/services/context/auto-compact-dispatcher-ide-native.test.ts:102`
30
- * passing (they assert `pathway: 'shell-exec'`). Real callers must
31
- * use `ide-native` (main session) or `llm-self-compress`. No host
32
- * CLI spawn occurs; the case logs a deprecation warning and returns
33
- * the same envelope shape.
34
- */
35
1
  import type { CompactDispatchResult } from './auto-compact-types.js';
36
2
  export type CompactTarget = 'main' | 'sub-agent';
37
3
  export interface DispatchIdeCompactInput {
@@ -48,14 +14,18 @@ export interface DispatchIdeCompactInput {
48
14
  * shells that spawn their own `peaks code auto-compact` flow pass
49
15
  * `'sub-agent'` to preserve the legacy shell-spawn behaviour.
50
16
  *
51
- * Behaviour matrix (claude-code MVP):
17
+ * Behaviour matrix, keyed on the ADAPTER'S DECLARED pathway only
18
+ * — never on the adapter's name (slice
19
+ * 2026-09-12-auto-compact-vendor-neutrality):
52
20
  * - target='main' → llm-self-compress (write intent; main LLM
53
- * fires `/compact` on its next turn).
21
+ * fires its compact command on its next turn).
54
22
  * - target='sub-agent'→ shell-exec stub (DEPRECATED — no host
55
23
  * CLI spawn; returns envelope with
56
24
  * `pathway: 'shell-exec'` for legacy
57
25
  * contract only — see rid-031).
58
- * Non-claude-code IDEs + target='main' return noop + warning.
26
+ * An adapter with no registered `compact` profile returns noop for
27
+ * BOTH targets; an adapter whose profile serves the main session is
28
+ * dispatched regardless of which IDE it is.
59
29
  */
60
30
  readonly target?: CompactTarget | undefined;
61
31
  }
@@ -1,3 +1,48 @@
1
+ /**
2
+ * AC-3 — IDE-aware compact dispatcher.
3
+ *
4
+ * Reads the active IDE's `IdeAdapter.compact` profile and dispatches
5
+ * compact via the adapter-declared pathway. No hard-coded IDE names
6
+ * — Claude Code is the MVP that fills `compact`; other adapters
7
+ * register their own `compact` profile as L2-dogfood verifies
8
+ * each IDE's actual compact surface.
9
+ *
10
+ * Pathway dispatch:
11
+ *
12
+ * - `ide-native` — peaks-loop writes the compact intent to
13
+ * the IDE's hook file (per
14
+ * `IdeSettingsLocation`). Used when the IDE
15
+ * requires a registered hook rather than a
16
+ * runtime command.
17
+ * - `llm-self-compress` — peaks-loop records the intent and
18
+ * returns `ok: true` with `pathway` echoed.
19
+ * The LLM does its own context summarization
20
+ * on the next turn. Always available; least
21
+ * precise fallback.
22
+ * - `noop` — adapter explicitly opted out. peaks-loop
23
+ * returns `ok: false` with `message: 'noop'`.
24
+ * Used by legacy / unverified adapters.
25
+ *
26
+ * rid-031 (2026-07-28): `shell-exec` pathway is DEPRECATED. Real
27
+ * callers must use `ide-native` (main session) or
28
+ * `llm-self-compress`. No host CLI spawn occurs; the `case
29
+ * 'shell-exec':` branch logs a deprecation warning and returns the
30
+ * same envelope shape.
31
+ *
32
+ * Slice 2026-09-12-auto-compact-vendor-neutrality (defect #2 of the QA
33
+ * pass): the paragraph above used to justify keeping that branch with
34
+ * "the 2 currently-passing tests" it named by path and line (`:58` of
35
+ * one file, `:102` of another, asserting `pathway: 'shell-exec'`).
36
+ * That justification ROTTED and is false as of today: both files were
37
+ * deleted by `f17aa377 test(rebuild): delete 559 legacy unit tests and
38
+ * reset vitest config`, and `grep -rn "shell-exec" tests/` now returns
39
+ * nothing — no test in this repo asserts `pathway: 'shell-exec'`. The
40
+ * branch is retained for the rid-031 ENVELOPE CONTRACT itself
41
+ * (`pathway` is part of `CompactDispatchResult` and is echoed to
42
+ * `peaks code auto-compact` callers), not for a test. Deleting it is a
43
+ * contract change, deliberately out of this slice.
44
+ */
45
+ import { join } from 'node:path';
1
46
  import { detectIdeFromEnv } from './ide-detect.js';
2
47
  import { getAdapter } from '../ide/ide-registry.js';
3
48
  /**
@@ -21,21 +66,28 @@ export async function dispatchIdeCompact(input) {
21
66
  // the main-session context. The orchestrator passes `'sub-agent'`
22
67
  // explicitly when a sub-agent shell dispatches the call.
23
68
  const target = input.target ?? 'main';
24
- // Slice 2026-06-28: when targeting the MAIN session, refuse
25
- // up-front for adapters we cannot dispatch a main-session compact
26
- // against. We DO this before the `!adapter.compact` short-circuit
27
- // so the test message reflects the operational cause (target vs
28
- // adapter capability). Even non-claude-code adapters without a
29
- // registered `compact` profile should report "main-session target
30
- // unsupported" rather than the generic "no compact profile" line.
31
- if (target === 'main' && ideId !== 'claude-code') {
32
- return {
33
- ok: false,
34
- ide: ideId,
35
- pathway: 'noop',
36
- message: `main-session target unsupported on adapter '${ideId}'; only claude-code supports in-band main-session compact.`
37
- };
38
- }
69
+ // Slice 2026-09-12-auto-compact-vendor-neutrality — the up-front
70
+ // `target === 'main' && ideId !== 'claude-code'` refusal is GONE.
71
+ //
72
+ // It decided by adapter NAME what the adapter's own profile already
73
+ // declares, contradicting this file's contract ("No hard-coded IDE
74
+ // names", :5-8): any adapter that filled in `compact` was still
75
+ // rejected for not being called `claude-code`.
76
+ //
77
+ // It was also redundant. Every pathway below owns its own
78
+ // main-session semantics — `ide-native` installs the hook against the
79
+ // caller's own window; `shell-exec` is explicitly degraded to
80
+ // `llm-self-compress` for a main target; `llm-self-compress` and
81
+ // `noop` behave identically for both targets (so `noop` still returns
82
+ // `ok: false` on main); an UNKNOWN pathway falls through to the
83
+ // default noop. An adapter with no `compact` profile keeps its own
84
+ // explicit refusal immediately below. No remaining capability test
85
+ // would have been anything but a duplicate of one of those branches,
86
+ // so the check was deleted rather than re-keyed. The two candidate
87
+ // re-keyings were both rejected: `pathway !== 'ide-native'` would
88
+ // refuse `shell-exec` and `llm-self-compress` adapters that the
89
+ // switch already serves, and `pathway === 'noop'` IS the branch
90
+ // immediately below.
39
91
  // Adapters that don't declare `compact` (legacy / unverified) →
40
92
  // explicit noop so the caller can distinguish "IDE doesn't support
41
93
  // compact" from "IDE supports but dispatch failed".
@@ -53,12 +105,13 @@ export async function dispatchIdeCompact(input) {
53
105
  switch (pathway) {
54
106
  case 'shell-exec':
55
107
  // rid-031 (2026-07-28): `shell-exec` pathway is DEPRECATED.
56
- // No host CLI spawn occurs. The case marker is preserved so
57
- // 2 currently-passing tests in
58
- // tests/unit/context/auto-compact-main-target.test.ts:58
59
- // tests/unit/services/context/auto-compact-dispatcher-ide-native.test.ts:102
60
- // continue to assert `pathway: 'shell-exec'`. Real callers must
61
- // use `ide-native` (main session) or `llm-self-compress`.
108
+ // No host CLI spawn occurs. The case marker is preserved for the
109
+ // rid-031 envelope contract (callers read `pathway`), NOT for a
110
+ // test — see this file's header, defect #2 of the 2026-09-12 QA
111
+ // pass: the tests the previous comment named were deleted by
112
+ // f17aa377 and no test asserts `pathway: 'shell-exec'` now.
113
+ // Real callers must use `ide-native` (main session) or
114
+ // `llm-self-compress`.
62
115
  console.warn(`compact: shell-exec pathway is deprecated on adapter '${ideId}' (target='${target}'); no host CLI spawn — use ide-native instead.`);
63
116
  if (target === 'main') {
64
117
  return {
@@ -76,12 +129,14 @@ export async function dispatchIdeCompact(input) {
76
129
  };
77
130
  case 'ide-native':
78
131
  // Slice 2026-07-02-auto-compact-zero-pause: write the auto-compact
79
- // PreToolUse hook into `.claude/settings.local.json`. The hook
80
- // command (`peaks code auto-compact`) reads
81
- // `CLAUDE_CONTEXT_USAGE_PERCENT` on every subsequent Bash/Task
82
- // tool call from the runner and, at ratio ≥ 0.95, in-band spawns
83
- // `claude --compact` against the CURRENT runner (not a child
84
- // process — the bug documented in
132
+ // PreToolUse hook into the IDE's MACHINE-LOCAL settings layer,
133
+ // resolved from the adapter (`settings.dirName` +
134
+ // `settings.localSettingsFileName`) — never from a path literal
135
+ // here. The hook command (`peaks code auto-compact`) reads the
136
+ // adapter-declared `envVarForContextPercent` on every subsequent
137
+ // Bash/Task tool call from the runner and, at ratio ≥ 0.95,
138
+ // in-band spawns the adapter-declared `compactCommand` against the
139
+ // CURRENT runner (not a child process — the bug documented in
85
140
  // `.peaks/memory/2026-06-27-auto-compact-design.md:139-152`).
86
141
  //
87
142
  // `ide-native` is ONLY for the main-session runner. When the
@@ -93,10 +148,11 @@ export async function dispatchIdeCompact(input) {
93
148
  if (target === 'sub-agent') {
94
149
  // rid-031 (2026-07-28): legacy shell-spawn fallback is
95
150
  // DEPRECATED. No host CLI spawn occurs. The envelope is
96
- // returned with `pathway: 'shell-exec'` to preserve the
97
- // contract asserted by
98
- // tests/unit/services/context/auto-compact-dispatcher-ide-native.test.ts:102
99
- // (sub-agent shells historically relied on this path).
151
+ // returned with `pathway: 'shell-exec'` to preserve the rid-031
152
+ // contract for callers that read `pathway` (sub-agent shells
153
+ // historically relied on this path). The test the previous
154
+ // comment named was deleted by f17aa377 — see this file's
155
+ // header, defect #2 of the 2026-09-12 QA pass.
100
156
  console.warn(`compact: sub-agent shell-exec fallback is deprecated on adapter '${ideId}'; no host CLI spawn for '${profile.compactCommand}'.`);
101
157
  return {
102
158
  ok: true,
@@ -111,7 +167,9 @@ export async function dispatchIdeCompact(input) {
111
167
  return await dispatchIdeNativeHook({
112
168
  projectRoot: input.projectRoot,
113
169
  sessionId: input.sessionId,
114
- target
170
+ target,
171
+ adapter,
172
+ profile
115
173
  });
116
174
  case 'llm-self-compress':
117
175
  return {
@@ -141,11 +199,18 @@ export async function dispatchIdeCompact(input) {
141
199
  /**
142
200
  * Slice 2026-07-02-auto-compact-zero-pause: implement the
143
201
  * `ide-native` pathway. Writes the auto-compact PreToolUse hook
144
- * into `.claude/settings.local.json` (idempotent; the install
145
- * service is a no-op if the hook is already present). On the next
146
- * Bash/Task tool call from the runner, the hook fires
147
- * `peaks code auto-compact` which in-band spawns
148
- * `claude --compact` against the CURRENT runner session.
202
+ * into the IDE's MACHINE-LOCAL settings file (idempotent; the
203
+ * install service is a no-op if the hook is already present). On
204
+ * the next Bash/Task tool call from the runner, the hook fires
205
+ * `peaks code auto-compact` which in-band spawns the adapter's
206
+ * declared compact command against the CURRENT runner session.
207
+ *
208
+ * Slice 2026-09-12-auto-compact-vendor-neutrality: the settings path
209
+ * AND the echoed `ide` are read off the adapter, so this function no
210
+ * longer names a vendor. An adapter that declares
211
+ * `compactPathway: 'ide-native'` and its own `settings` location gets
212
+ * its own hook file — the `.claude/settings.local.json` value survives
213
+ * only as the installer's default for callers with no adapter in hand.
149
214
  *
150
215
  * Returns `ok: true, pathway: 'ide-native'` regardless of install
151
216
  * action (`installed` vs `already-installed`) — both states
@@ -158,13 +223,21 @@ async function dispatchIdeNativeHook(input) {
158
223
  // avoids a static cycle if future slices add cross-imports
159
224
  // between dispatcher and hook-install.
160
225
  const { installAutoCompactHook } = await import('../hooks/auto-compact-hook-install.js');
161
- const result = installAutoCompactHook({ projectRoot: input.projectRoot });
226
+ // Resolve the local settings file from the adapter's declared
227
+ // location. `undefined` means "this IDE declares no machine-local
228
+ // layer" → the installer's documented default applies.
229
+ const localFileName = input.adapter.settings.localSettingsFileName;
230
+ const settingsPath = localFileName === undefined
231
+ ? undefined
232
+ : join(input.projectRoot, input.adapter.settings.dirName, localFileName);
233
+ const result = installAutoCompactHook({ projectRoot: input.projectRoot, settingsPath });
234
+ const envVar = input.profile.envVarForContextPercent;
162
235
  return {
163
236
  ok: true,
164
- ide: 'claude-code',
237
+ ide: input.adapter.id,
165
238
  pathway: 'ide-native',
166
239
  message: result.action === 'installed'
167
- ? `Auto-compact PreToolUse hook installed at ${result.settingsPath}. Next Bash/Task tool call will read CLAUDE_CONTEXT_USAGE_PERCENT and compact in-band at ratio ≥ 95%.`
240
+ ? `Auto-compact PreToolUse hook installed at ${result.settingsPath}. Next Bash/Task tool call will read ${envVar} and compact in-band at ratio ≥ 95%.`
168
241
  : result.action === 'updated'
169
242
  ? `Auto-compact PreToolUse hook REPAIRED at ${result.settingsPath} — the installed entry carried a stale command and was rewritten.`
170
243
  : `Auto-compact PreToolUse hook already installed at ${result.settingsPath}; next Bash/Task tool call will trigger compact in-band at ratio ≥ 95%.`
@@ -1,32 +1,5 @@
1
- /**
2
- * AC-1 — auto context-percent probe.
3
- *
4
- * Reads the current AI CLI context-fill ratio without requiring the
5
- * LLM to pass `--prompt-size <bytes>` manually. Strategy: ask the
6
- * registered `IdeAdapter.compact` profile which env-var to read and,
7
- * when that misses, ask the adapter for a vendor-specific fallback —
8
- * no hard-coded IDE names. Per-adapter:
9
- *
10
- * - claude-code: its adapter-declared env-var (MVP) + a
11
- * `readContextPercentFallback` that polls the statusline /
12
- * transcript (see claude-code-adapter.ts).
13
- * - trae / codex / cursor / qoder / tongyi-lingma / hermes /
14
- * openclaw / zcode: each adapter fills its own env-var; until
15
- * L2-dogfood verifies each surface, adapters may omit `compact`
16
- * and the probe returns `source: 'conservative-fallback'`.
17
- *
18
- * Resolution order (user-overridden → env-var → adapter fallback →
19
- * conservative-fallback):
20
- * 1. `promptSizeBytes` (P0 `--prompt-size <bytes>` escape hatch) →
21
- * `source: 'user-overridden'`.
22
- * 2. `adapter.compact.envVarForContextPercent` env-var →
23
- * `source: '<ideId>-env'`.
24
- * 3. `adapter.compact.readContextPercentFallback?.(input)` — the
25
- * adapter owns any vendor-specific statusline / transcript probe.
26
- * 4. `ratio: 0` with `source: 'conservative-fallback'` — the
27
- * orchestrator MUST NOT auto-fire compact on this signal.
28
- */
29
1
  import type { ContextPercentProbe } from './auto-compact-types.js';
2
+ import { type HarnessWindowLocation, type HarnessWindowReadResult, type HarnessWindowSyncResult } from './harness-window-config.js';
30
3
  export interface ReadContextPercentInput {
31
4
  readonly projectRoot: string;
32
5
  readonly sessionId: string;
@@ -50,6 +23,73 @@ export interface ReadContextPercentInput {
50
23
  */
51
24
  readonly promptSizeBytes?: number | undefined;
52
25
  }
26
+ /**
27
+ * Resolve WHERE the harness auto-compact window lives, for the IDE the
28
+ * current env selects. Returns `null` when either half is undeclared:
29
+ * - the adapter exposes no `compact` profile, or
30
+ * - the adapter's `compact` profile declares no
31
+ * `autoCompactWindowEnvVar` (the IDE has no such knob), or
32
+ * - the adapter declares no machine-local settings layer.
33
+ *
34
+ * Both the path and the key name come from the adapter's OWN declarations,
35
+ * so this module names no IDE and no settings literal — the vendor-neutrality
36
+ * guard's shape-3 rule applies to every registry consumer, and this is one.
37
+ *
38
+ * Slice 2026-09-13-auto-compact-trigger-ownership.
39
+ */
40
+ export declare function resolveHarnessWindowLocation(input: {
41
+ readonly projectRoot: string;
42
+ readonly env?: NodeJS.ProcessEnv | undefined;
43
+ }): HarnessWindowLocation | null;
44
+ /**
45
+ * Read the window peaks-loop last configured for the harness, plus whether
46
+ * the user has opted out of peaks-loop managing it. Returns `null` when the
47
+ * active adapter exposes no window knob at all.
48
+ */
49
+ export declare function readHarnessWindowState(input: {
50
+ readonly projectRoot: string;
51
+ readonly env?: NodeJS.ProcessEnv | undefined;
52
+ }): HarnessWindowReadResult | null;
53
+ /**
54
+ * The RAW window peaks-loop's context ratio must divide by — T1's number.
55
+ *
56
+ * The SETTINGS FILE is the source of truth, so its value wins whenever it has
57
+ * one; the env-first read is consulted only when the file is silent (no key
58
+ * yet, or the IDE injects the value from an outer layer).
59
+ *
60
+ * Why not "whatever the read returned" (the round-2 shape): the process env is
61
+ * a snapshot the harness froze at session start, so it is stale by exactly one
62
+ * write. Trusting it over the file is what let a stale env revert a human's
63
+ * hand-edited window (B1) and downgrade a 1M file to a 200K one (a second
64
+ * session's frozen env), and it is also what made T1's consistency depend on
65
+ * *rewriting the file* — the same unconditional write that destroyed the human's
66
+ * value. Dividing by the file instead makes peaks-loop's ratio and the harness's
67
+ * configured window one number BY CONSTRUCTION, with no write required. The
68
+ * write is then free to be gated on provenance (see `syncHarnessWindow`).
69
+ *
70
+ * Falls back to `undefined` (no harness layer at all) when the adapter
71
+ * declares no knob or neither side carries the key — the adapter then resolves
72
+ * its own window exactly as it did before this slice.
73
+ */
74
+ export declare function resolveHarnessRatioWindow(state: HarnessWindowReadResult | null): unknown;
75
+ /**
76
+ * Materialize a window into the harness's own settings, so the number
77
+ * peaks-loop divides by and the number the harness compacts against are one
78
+ * value rather than two resolutions (slice
79
+ * 2026-09-13-auto-compact-trigger-ownership).
80
+ *
81
+ * The caller passes the denominator it JUST USED (`probe.capacityTokens`), not
82
+ * a re-derived one — that is what makes the two sides structurally identical.
83
+ * `tokens: null` (a percent-only probe carries no token window) is a
84
+ * deliberate no-op: peaks-loop never invents a window it did not measure.
85
+ *
86
+ * Returns `null` when the active adapter declares no window knob at all.
87
+ */
88
+ export declare function syncHarnessWindowForProject(input: {
89
+ readonly projectRoot: string;
90
+ readonly env?: NodeJS.ProcessEnv | undefined;
91
+ readonly tokens: number | null;
92
+ }): HarnessWindowSyncResult | null;
53
93
  /**
54
94
  * Read env-var-based context percentage. Returns `null` when the env
55
95
  * is absent or unparseable. Adapter-driven: the caller passes the
@@ -1,6 +1,141 @@
1
+ /**
2
+ * AC-1 — auto context-percent probe.
3
+ *
4
+ * Reads the current AI CLI context-fill ratio without requiring the
5
+ * LLM to pass `--prompt-size <bytes>` manually. Strategy: ask the
6
+ * registered `IdeAdapter.compact` profile which env-var to read and,
7
+ * when that misses, ask the adapter for a vendor-specific fallback —
8
+ * no hard-coded IDE names. Per-adapter:
9
+ *
10
+ * - claude-code: its adapter-declared env-var (MVP) + a
11
+ * `readContextPercentFallback` that polls the statusline /
12
+ * transcript (see claude-code-adapter.ts).
13
+ * - trae / codex / cursor / qoder / tongyi-lingma / hermes /
14
+ * openclaw / zcode: each adapter fills its own env-var; until
15
+ * L2-dogfood verifies each surface, adapters may omit `compact`
16
+ * and the probe returns `source: 'conservative-fallback'`.
17
+ *
18
+ * Resolution order (user-overridden → env-var → adapter fallback →
19
+ * conservative-fallback):
20
+ * 1. `promptSizeBytes` (P0 `--prompt-size <bytes>` escape hatch) →
21
+ * `source: 'user-overridden'`.
22
+ * 2. `adapter.compact.envVarForContextPercent` env-var →
23
+ * `source: '<ideId>-env'`.
24
+ * 3. `adapter.compact.readContextPercentFallback?.(input)` — the
25
+ * adapter owns any vendor-specific statusline / transcript probe.
26
+ * 4. `ratio: 0` with `source: 'conservative-fallback'` — the
27
+ * orchestrator MUST NOT auto-fire compact on this signal.
28
+ */
29
+ import { join } from 'node:path';
1
30
  import { detectIdeFromEnv } from './ide-detect.js';
2
31
  import { getAdapter } from '../ide/ide-registry.js';
3
32
  import { readContextWindowTokensOverride } from '../config/config-service.js';
33
+ import { isHarnessWindowInRange, parseHarnessWindowTokens, readHarnessWindow, syncHarnessWindow } from './harness-window-config.js';
34
+ /**
35
+ * Resolve WHERE the harness auto-compact window lives, for the IDE the
36
+ * current env selects. Returns `null` when either half is undeclared:
37
+ * - the adapter exposes no `compact` profile, or
38
+ * - the adapter's `compact` profile declares no
39
+ * `autoCompactWindowEnvVar` (the IDE has no such knob), or
40
+ * - the adapter declares no machine-local settings layer.
41
+ *
42
+ * Both the path and the key name come from the adapter's OWN declarations,
43
+ * so this module names no IDE and no settings literal — the vendor-neutrality
44
+ * guard's shape-3 rule applies to every registry consumer, and this is one.
45
+ *
46
+ * Slice 2026-09-13-auto-compact-trigger-ownership.
47
+ */
48
+ export function resolveHarnessWindowLocation(input) {
49
+ const env = input.env ?? process.env;
50
+ const detected = detectIdeFromEnv(env);
51
+ const ideId = (detected === 'unknown' ? 'claude-code' : detected);
52
+ const adapter = getAdapter(ideId);
53
+ const envVar = adapter.compact?.autoCompactWindowEnvVar;
54
+ const fileName = adapter.settings.localSettingsFileName ?? adapter.settings.settingsFileName;
55
+ if (envVar === undefined || fileName === undefined)
56
+ return null;
57
+ return {
58
+ settingsPath: join(input.projectRoot, adapter.settings.dirName, fileName),
59
+ envVar,
60
+ // Carried so the writer can refuse to target the user's own home directory
61
+ // (`--project .` from a fresh terminal): the path alone cannot say which
62
+ // root it came from, and this is the only place a location is built.
63
+ projectRoot: input.projectRoot
64
+ };
65
+ }
66
+ /**
67
+ * Read the window peaks-loop last configured for the harness, plus whether
68
+ * the user has opted out of peaks-loop managing it. Returns `null` when the
69
+ * active adapter exposes no window knob at all.
70
+ */
71
+ export function readHarnessWindowState(input) {
72
+ const location = resolveHarnessWindowLocation(input);
73
+ if (location === null)
74
+ return null;
75
+ return readHarnessWindow({ location, env: input.env ?? process.env });
76
+ }
77
+ /**
78
+ * The RAW window peaks-loop's context ratio must divide by — T1's number.
79
+ *
80
+ * The SETTINGS FILE is the source of truth, so its value wins whenever it has
81
+ * one; the env-first read is consulted only when the file is silent (no key
82
+ * yet, or the IDE injects the value from an outer layer).
83
+ *
84
+ * Why not "whatever the read returned" (the round-2 shape): the process env is
85
+ * a snapshot the harness froze at session start, so it is stale by exactly one
86
+ * write. Trusting it over the file is what let a stale env revert a human's
87
+ * hand-edited window (B1) and downgrade a 1M file to a 200K one (a second
88
+ * session's frozen env), and it is also what made T1's consistency depend on
89
+ * *rewriting the file* — the same unconditional write that destroyed the human's
90
+ * value. Dividing by the file instead makes peaks-loop's ratio and the harness's
91
+ * configured window one number BY CONSTRUCTION, with no write required. The
92
+ * write is then free to be gated on provenance (see `syncHarnessWindow`).
93
+ *
94
+ * Falls back to `undefined` (no harness layer at all) when the adapter
95
+ * declares no knob or neither side carries the key — the adapter then resolves
96
+ * its own window exactly as it did before this slice.
97
+ */
98
+ export function resolveHarnessRatioWindow(state) {
99
+ if (state === null)
100
+ return undefined;
101
+ // E1 (rid 2026-09-13-defects-e): a value outside the band the harness accepts
102
+ // is not a window, however well-formed an integer it is — the harness ignores
103
+ // or caps it, so handing it over here would make the ratio's denominator a
104
+ // number the harness is not compacting on. This is the read half of the same
105
+ // guard the writer applies before planting one; without it, a value already on
106
+ // disk (hand-planted, or written by a release that predates the band) would
107
+ // keep poisoning the ratio on every probe.
108
+ //
109
+ // The two NULLs are deliberate and different: an out-of-band value is refused
110
+ // here (the adapter falls through to its own resolution and the writer's
111
+ // notice names the skipped number), while an UNPARSEABLE one is still handed
112
+ // over — the adapter validates it and warns about a hand-edited `500k`, which
113
+ // is a closer reading of the user's intent than silently ignoring it.
114
+ const raw = state.fileRaw !== undefined ? state.fileRaw : state.raw;
115
+ const tokens = parseHarnessWindowTokens(raw);
116
+ if (tokens !== null && !isHarnessWindowInRange(tokens))
117
+ return undefined;
118
+ return raw;
119
+ }
120
+ /**
121
+ * Materialize a window into the harness's own settings, so the number
122
+ * peaks-loop divides by and the number the harness compacts against are one
123
+ * value rather than two resolutions (slice
124
+ * 2026-09-13-auto-compact-trigger-ownership).
125
+ *
126
+ * The caller passes the denominator it JUST USED (`probe.capacityTokens`), not
127
+ * a re-derived one — that is what makes the two sides structurally identical.
128
+ * `tokens: null` (a percent-only probe carries no token window) is a
129
+ * deliberate no-op: peaks-loop never invents a window it did not measure.
130
+ *
131
+ * Returns `null` when the active adapter declares no window knob at all.
132
+ */
133
+ export function syncHarnessWindowForProject(input) {
134
+ const location = resolveHarnessWindowLocation(input);
135
+ if (location === null)
136
+ return null;
137
+ return syncHarnessWindow({ location, tokens: input.tokens, env: input.env ?? process.env });
138
+ }
4
139
  /**
5
140
  * Read env-var-based context percentage. Returns `null` when the env
6
141
  * is absent or unparseable. Adapter-driven: the caller passes the
@@ -61,6 +196,17 @@ export function readContextPercent(input) {
61
196
  };
62
197
  }
63
198
  if (adapter.compact) {
199
+ // Slice 2026-09-13-auto-compact-trigger-ownership: the window peaks-loop
200
+ // configured for the harness, read from the adapter's declared settings
201
+ // path + key. `null` (no knob declared, or the key absent) leaves the
202
+ // adapter's chain byte-identical to before this slice.
203
+ //
204
+ // The RAW value is handed over, not the parsed one: the adapter validates
205
+ // it (`parseContextWindowOverride`) and warns on a bad hand-edited value,
206
+ // so parsing here too would be a second, silently-disagreeing parser —
207
+ // exactly the duplicated-resolution shape this slice exists to delete.
208
+ const harnessWindowState = readHarnessWindowState({ projectRoot: input.projectRoot, env });
209
+ const harnessWindowTokens = resolveHarnessRatioWindow(harnessWindowState);
64
210
  // Primary: read the adapter-declared env-var (no hard-coded IDE names).
65
211
  const primary = readEnvPercent(env, adapter.compact.envVarForContextPercent);
66
212
  if (primary !== null) {
@@ -85,7 +231,15 @@ export function readContextPercent(input) {
85
231
  sessionId: input.sessionId,
86
232
  outerSessionId: input.outerSessionId,
87
233
  env,
88
- configWindowTokens: readContextWindowTokensOverride(input.projectRoot)
234
+ configWindowTokens: readContextWindowTokensOverride(input.projectRoot),
235
+ // Slice 2026-09-13-auto-compact-trigger-ownership: hand the adapter the
236
+ // window peaks-loop configured for the harness, so the ratio it reports
237
+ // divides by the very number the harness compacts against — plus whether
238
+ // peaks-loop is the one that wrote it, so the adapter may self-correct
239
+ // its own earlier resolution without ever overruling a human's pin.
240
+ ...(harnessWindowTokens !== undefined
241
+ ? { harnessWindowTokens, harnessWindowPeakWritten: harnessWindowState?.peakWritten === true }
242
+ : {})
89
243
  });
90
244
  if (fallback)
91
245
  return fallback;