peaks-loop 4.0.46 → 4.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
  5. package/dist/cli/commands/code-runtime-commands.js +99 -16
  6. package/dist/cli/commands/compact-command.js +129 -1
  7. package/dist/cli/commands/container-commands.js +3 -3
  8. package/dist/cli/commands/core/skill-command.js +45 -10
  9. package/dist/cli/commands/cron-commands.js +2 -1
  10. package/dist/cli/commands/e2e-verify.js +3 -3
  11. package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
  12. package/dist/cli/commands/hooks-commands.js +10 -1
  13. package/dist/cli/commands/loop-commands.js +1 -0
  14. package/dist/cli/commands/playwright-commands.js +2 -1
  15. package/dist/cli/commands/reinject-command.d.ts +72 -0
  16. package/dist/cli/commands/reinject-command.js +174 -0
  17. package/dist/cli/commands/request-commands.js +6 -3
  18. package/dist/cli/commands/shadcn-commands.js +1 -0
  19. package/dist/cli/commands/test-commands.js +2 -1
  20. package/dist/cli/commands/vm-commands.js +7 -7
  21. package/dist/cli/commands/workspace/init-command.js +24 -2
  22. package/dist/cli/commands/worktree-lease-commands.js +4 -4
  23. package/dist/cli/index.js +49 -2
  24. package/dist/cli/program.js +5 -0
  25. package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
  26. package/dist/services/adapter/adapter-registry.js +1 -1
  27. package/dist/services/artifacts/artifact-service.js +1 -1
  28. package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
  29. package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
  30. package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
  31. package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
  32. package/dist/services/code/auto-compact-lifecycle.d.ts +11 -1
  33. package/dist/services/code/auto-compact-lifecycle.js +11 -4
  34. package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
  35. package/dist/services/code/auto-compact-orchestrator.js +153 -32
  36. package/dist/services/code/orchestrator-can-do.d.ts +4 -2
  37. package/dist/services/code/orchestrator-can-do.js +37 -5
  38. package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
  39. package/dist/services/codegraph/codegraph-process-runner.js +3 -2
  40. package/dist/services/compact/request-transition-hook.js +5 -2
  41. package/dist/services/compact-history/compact-history-service.d.ts +61 -0
  42. package/dist/services/compact-history/compact-history-service.js +49 -0
  43. package/dist/services/config/config-safety.d.ts +52 -0
  44. package/dist/services/config/config-safety.js +75 -1
  45. package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
  46. package/dist/services/context/auto-compact-dispatcher.js +113 -40
  47. package/dist/services/context/auto-compact-reader.d.ts +68 -28
  48. package/dist/services/context/auto-compact-reader.js +155 -1
  49. package/dist/services/context/auto-compact-types.d.ts +89 -12
  50. package/dist/services/context/auto-compact-types.js +16 -32
  51. package/dist/services/context/harness-window-config.d.ts +412 -0
  52. package/dist/services/context/harness-window-config.js +607 -0
  53. package/dist/services/context/main-session-monitor.d.ts +27 -0
  54. package/dist/services/context/main-session-monitor.js +32 -1
  55. package/dist/services/context/post-compact-reinjection.d.ts +221 -0
  56. package/dist/services/context/post-compact-reinjection.js +491 -0
  57. package/dist/services/dispatch/merge-back-runner.js +5 -5
  58. package/dist/services/dispatch/service-shutdown.js +3 -3
  59. package/dist/services/doc/doc-generator.js +2 -1
  60. package/dist/services/env/shell-probe.js +1 -1
  61. package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
  62. package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
  63. package/dist/services/hooks/auto-compact-hook-install.js +8 -0
  64. package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
  65. package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
  66. package/dist/services/ide/ide-registry.d.ts +12 -0
  67. package/dist/services/ide/ide-registry.js +14 -0
  68. package/dist/services/ide/ide-types.d.ts +59 -0
  69. package/dist/services/lint/detect-eslint.js +2 -2
  70. package/dist/services/lint/eslint-runner.js +3 -1
  71. package/dist/services/loop/evaluator-dispatcher.js +2 -1
  72. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
  73. package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
  74. package/dist/services/memory/project-memory-service/store/paths.js +15 -6
  75. package/dist/services/prd/best-practice-auto-trigger.js +1 -0
  76. package/dist/services/release/version-precheck-service.d.ts +2 -1
  77. package/dist/services/release/version-precheck-service.js +82 -12
  78. package/dist/services/runtime/vendor-adapter.d.ts +29 -4
  79. package/dist/services/runtime/vendors/claude-code.js +1 -1
  80. package/dist/services/runtime/vendors/codex.js +1 -1
  81. package/dist/services/runtime/vendors/copilot.js +1 -1
  82. package/dist/services/sc/sc-service.js +1 -1
  83. package/dist/services/scan/diff-scope-service.js +2 -2
  84. package/dist/services/scan/file-size-scan.js +2 -2
  85. package/dist/services/scan/orphan-service.js +2 -1
  86. package/dist/services/scan/type-sanity-service.js +2 -2
  87. package/dist/services/skillhub/tar-runtime.js +1 -0
  88. package/dist/services/skills/hooks-codegate-superpowers.d.ts +8 -0
  89. package/dist/services/skills/hooks-codegate-superpowers.js +40 -3
  90. package/dist/services/skills/hooks-settings-service.d.ts +12 -0
  91. package/dist/services/skills/hooks-settings-service.js +77 -10
  92. package/dist/services/skills/session-start-hook-constants.d.ts +41 -0
  93. package/dist/services/skills/session-start-hook-constants.js +41 -0
  94. package/dist/services/skills/skill-presence-service.js +9 -0
  95. package/dist/services/slice/slice-check-service.js +2 -1
  96. package/dist/services/slice/slice-decompose-runners.js +2 -1
  97. package/dist/services/upgrade/upgrade-service.js +1 -0
  98. package/dist/services/workflow/workflow-skip-service.js +2 -1
  99. package/dist/services/workspace/migrate-service.js +1 -1
  100. package/dist/services/workspace/workspace-claude-settings-materializer.js +51 -7
  101. package/dist/services/workspace/workspace-service.js +8 -0
  102. package/dist/services/worktree/host-worktree-reconciler.js +1 -0
  103. package/dist/services/worktree/long-path-cleanup.js +3 -2
  104. package/dist/shared/process.js +1 -1
  105. package/package.json +5 -5
  106. package/scripts/install-skills.mjs +1 -0
  107. package/scripts/watch.mjs +3 -1
  108. package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
  109. package/skills/bee/peaks-prd/SKILL.md +1 -1
  110. package/skills/bee/peaks-qa/SKILL.md +2 -2
  111. package/skills/bee/peaks-rd/SKILL.md +2 -2
  112. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  113. package/skills/bee/peaks-sc/SKILL.md +1 -1
  114. package/skills/bee/peaks-security-audit/SKILL.md +1 -1
  115. package/skills/bee/peaks-txt/SKILL.md +1 -1
  116. package/skills/bee/peaks-ui/SKILL.md +1 -1
  117. package/skills/peaks-audit/SKILL.md +1 -1
  118. package/skills/peaks-code/SKILL.md +2 -2
  119. package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
  120. package/skills/peaks-content/SKILL.md +1 -1
  121. package/skills/peaks-doctor/SKILL.md +1 -1
  122. package/skills/peaks-final-review/SKILL.md +1 -1
  123. package/skills/peaks-ide/SKILL.md +1 -1
  124. package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
  125. package/skills/peaks-resume/SKILL.md +1 -1
  126. package/skills/peaks-slice-decompose/SKILL.md +1 -1
  127. package/skills/peaks-solo/SKILL.md +1 -1
  128. package/skills/peaks-sop/SKILL.md +1 -1
  129. package/skills/peaks-status/SKILL.md +1 -1
  130. package/skills/peaks-test/SKILL.md +1 -1
@@ -214,28 +214,34 @@ export function summarizeLifecycleError(error) {
214
214
  * `afterRatio: 0` there would publish a fabricated number;
215
215
  * - the ratio is still at or above the auto-fire threshold — the
216
216
  * compact has not landed, so the run stays open.
217
+ *
218
+ * Returns the settled record, or `null` when nothing settled. Slice
219
+ * 2026-09-13-auto-compact-trigger-ownership: the caller needs the settled
220
+ * `afterRatio` to append the "observed compaction point" row that makes the
221
+ * intent-vs-observed gap readable after a real session. The return value is
222
+ * telemetry only — callers that ignore it are unaffected.
217
223
  */
218
224
  export function settleOpenLifecycleRun(input) {
219
225
  // A `conservative-fallback` probe means no signal was available at
220
226
  // all. Its `ratio: 0` is the absence of a measurement, so it can
221
227
  // never be evidence that the context shrank.
222
228
  if (input.source === 'conservative-fallback')
223
- return;
229
+ return null;
224
230
  if (input.measuredRatio >= input.autoFireThreshold)
225
- return;
231
+ return null;
226
232
  const prior = readOpenCompactLifecycle({
227
233
  projectRoot: input.projectRoot,
228
234
  sessionId: input.sessionId
229
235
  });
230
236
  if (prior === null)
231
- return;
237
+ return null;
232
238
  // Only a run that was actually dispatched can be completed by a
233
239
  // post-compact measurement. Both `compacting` (the in-band trigger is
234
240
  // satisfied) and `armed` (a trigger was registered and could fire at
235
241
  // any time) qualify: the measured drop is proof that SOME compact
236
242
  // landed, and this is the only open run to attribute it to.
237
243
  if (prior.stage !== 'compacting' && prior.stage !== 'armed')
238
- return;
244
+ return null;
239
245
  const emit = (stage, withAfterRatio) => {
240
246
  const record = {
241
247
  schemaVersion: 1,
@@ -267,4 +273,5 @@ export function settleOpenLifecycleRun(input) {
267
273
  emit('verifying', false);
268
274
  // `completed` = the measurement confirms the drop; publish it.
269
275
  emit('completed', true);
276
+ return { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio: input.measuredRatio };
270
277
  }
@@ -31,7 +31,21 @@ export interface AutoCompactInput {
31
31
  * autonomously at 0.85+ with zero human / zero LLM intervention.
32
32
  */
33
33
  readonly force?: boolean | undefined;
34
- /** Skip the 95% red-line gate (test seam — never true in production). */
34
+ /**
35
+ * INERT — nothing reads this. It is threaded from the CLI's
36
+ * `--bypass-red-line` (a published flag, kept for backward compatibility;
37
+ * see the option's own note) down to `evaluateAutoCompactDecision`, where the
38
+ * red-line branch ignores it.
39
+ *
40
+ * Why nothing reads it any more: it used to skip a gate that refused to
41
+ * dispatch below 0.85. That gate was removed (slice
42
+ * 2026-09-13-auto-compact-trigger-ownership, T3/A1) — peaks-loop cannot
43
+ * compact a running session, so the refusal could not shorten the wait it
44
+ * was waiting for, and it deadlocked the runner at the worst moment. With the
45
+ * gate gone, the red line still ASKS the harness to compact (when it always
46
+ * did) and no longer needs a bypass. Kept as a field so the published
47
+ * signature does not change under callers that pass it.
48
+ */
35
49
  readonly bypassRedLine?: boolean | undefined;
36
50
  /** Current session id (default = resolve via session-id-service). */
37
51
  readonly sessionId?: string | undefined;
@@ -93,7 +107,7 @@ export interface AutoCompactTestHooks {
93
107
  *
94
108
  * - ratio < preCompact → 'none' or 'soft-warn'
95
109
  * - ratio ≥ preCompact → 'pre-compact' (async-friendly path)
96
- * - ratio ≥ redLine → 'red-line' (synchronous gate)
110
+ * - ratio ≥ redLine → 'red-line' (ask the harness; dispatch continues)
97
111
  *
98
112
  * Slice 2026-07-28 (rid-027): `mode` selects the threshold table.
99
113
  * Default `'standard'` (0.85/0.95). `'partial'` (0.70/0.85) is used
@@ -109,8 +123,13 @@ export declare function evaluateCompactTrigger(ratio: number, mode?: AutoCompact
109
123
  * - 0.85 ≤ ratio < 0.95 → pre-compact; if in-flight batch
110
124
  * present, defer (D6.e); else dispatch
111
125
  * IDE compact asynchronously.
112
- * - ratio ≥ 0.95 → red-line; ALWAYS dispatch synchronously
113
- * regardless of in-flight batch.
126
+ * - ratio ≥ 0.95 → red-line; ask the harness to compact
127
+ * regardless of in-flight batch. Nothing is
128
+ * gated — dispatch is NOT blocked (slice
129
+ * 2026-09-13-auto-compact-trigger-ownership:
130
+ * peaks-loop cannot compact a running session,
131
+ * so a "block" gated nothing and deadlocked the
132
+ * runner).
114
133
  */
115
134
  export declare function evaluateAutoCompactDecision(input: {
116
135
  ratio: number;
@@ -129,6 +148,7 @@ export declare function evaluateAutoCompactDecision(input: {
129
148
  */
130
149
  inflightBatch?: boolean | InFlightBatchProbe | undefined;
131
150
  force?: boolean | undefined;
151
+ /** Accepted and ignored — see `AutoCompactInput.bypassRedLine`. */
132
152
  bypassRedLine?: boolean | undefined;
133
153
  mode?: AutoCompactMode | undefined;
134
154
  /**
@@ -170,13 +190,14 @@ export declare function buildConvergencePlan(input: {
170
190
  * 5. If trigger.kind === 'pre-compact' AND in-flight batch → wait.
171
191
  * 6. If trigger.kind === 'pre-compact' → async dispatch (write
172
192
  * checkpoint + IDE compact; orchestrator returns immediately).
173
- * 7. If trigger.kind === 'red-line' → synchronous gate: refuse
174
- * sub-agent dispatch, dispatch IDE compact, mark `redLineGated`.
193
+ * 7. If trigger.kind === 'red-line' → dispatch the IDE compact, report
194
+ * that the harness has been asked and that dispatch continues.
175
195
  *
176
196
  * The caller (CLI or skill body) handles the actual return — D7's
177
- * post-compact-detect will pick up the checkpoint on the next turn.
178
- * For red-line, the caller MUST block further tool calls until the
179
- * post-compact probe confirms ratio < 0.85.
197
+ * post-compact-detect will pick up the checkpoint on the next turn. For
198
+ * red-line, the caller keeps working; it re-probes and, if the ratio is
199
+ * still rising with no compact from the harness, reports that instead of
200
+ * stalling.
180
201
  */
181
202
  export declare function runAutoCompact(input: AutoCompactInput): Promise<AutoCompactResult>;
182
203
  /** Re-export for callers that need to surface the trigger shape. */
@@ -200,6 +221,29 @@ export interface CompactHistoryEvent {
200
221
  readonly ok: boolean;
201
222
  readonly checkpointPath: string;
202
223
  readonly dispatchMessage: string;
224
+ /**
225
+ * Slice 2026-09-13-auto-compact-trigger-ownership (T4): the calibration
226
+ * instrument. `windowTokens` is the denominator peaks-loop divided by
227
+ * (`probe.capacityTokens`), so `beforeRatio * windowTokens` is the exact
228
+ * TOKEN POINT peaks-loop asked the harness to compact at. That number is
229
+ * the deliverable — NOT a hand-picked threshold: this slice could not run a
230
+ * real Claude Code session, so the real trigger point of
231
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` (documented as a window, observed to
232
+ * fire near the window's end) has no measured answer yet. Recording the
233
+ * intent lets the first real session produce one.
234
+ */
235
+ readonly windowTokens?: number | null;
236
+ /** Which layer produced `windowTokens` (see `ContextWindowSource`). */
237
+ readonly windowSource?: string | null;
238
+ /**
239
+ * `dispatch` (default, and the only kind earlier releases wrote) or
240
+ * `observed` — a row appended when a later probe MEASURED the ratio after
241
+ * a dispatched compact, proving one landed. The pair is what yields the
242
+ * intent-vs-observed delta.
243
+ */
244
+ readonly kind?: 'dispatch' | 'observed';
245
+ /** `observed` rows only: the measured post-compact ratio. */
246
+ readonly afterRatio?: number;
203
247
  }
204
248
  export declare function appendCompactHistoryEvent(input: {
205
249
  readonly projectRoot: string;
@@ -9,10 +9,15 @@
9
9
  * conversation history — so peaks-loop drives the entire compaction:
10
10
  *
11
11
  * 1. Read current context % (via IDE adapter's `readContextPercent`).
12
- * 2. If ratio ≥ 0.95 (RED LINE): synchronous gate — peaks-loop
13
- * refuses sub-agent dispatch and forces IDE compact immediately.
14
- * The LLM cannot opt out (compact red line — keeps the runner
15
- * alive).
12
+ * 2. If ratio ≥ 0.95 (RED LINE): peaks-loop ASKS the harness to
13
+ * compact and reports that it is waiting. It does NOT block
14
+ * sub-agent dispatch (slice
15
+ * 2026-09-13-auto-compact-trigger-ownership, T3): peaks-loop owns
16
+ * the *decision*, the harness owns the *capability*, and peaks-loop
17
+ * has no way to compact a running session itself — so a "gate"
18
+ * here gated nothing and deadlocked the runner at the worst
19
+ * moment. If the ratio keeps rising and the harness has not
20
+ * compacted, the honest move is to say so and hand control back.
16
21
  * 3. If 0.85 ≤ ratio < 0.95 (pre-compact zone): peaks-loop prepares
17
22
  * the convergence toolkit (checkpoint + auto-decisions log +
18
23
  * IDE-dispatch handle) and surfaces it to the LLM. The LLM
@@ -24,15 +29,17 @@
24
29
  * Why two tiers (vs. one): the LLM uses the 0.85–0.95 zone for
25
30
  * intelligent convergence — wait for in-flight sub-agents, finish
26
31
  * the current todo row, persist a checkpoint, then compact. At 0.95
27
- * the window is gone; peaks-loop takes over synchronously. Net effect:
28
- * the LLM-runner keeps working with context < 95% without human
29
- * intervention.
32
+ * peaks-loop stops negotiating and requests the compact outright. Net
33
+ * effect: the LLM-runner keeps working at any ratio without human
34
+ * intervention, and without a gate it cannot satisfy.
30
35
  */
31
36
  import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
32
37
  import { dirname, join } from 'node:path';
33
38
  import { getSessionIdCanonical } from '../session/session-manager.js';
34
39
  import { resolveOuterSessionId } from '../session/binding-status-service.js';
35
- import { AUTO_COMPACT_PRE_COMPACT_RATIO } from '../context/auto-compact-types.js';
40
+ import { resolveCanonicalProjectRoot } from '../config/config-service.js';
41
+ import { describeHarnessWindowSync, harnessWindowSyncWarning } from '../context/harness-window-config.js';
42
+ import { AUTO_COMPACT_PRE_COMPACT_RATIO, DEPRECATED_ENVELOPE_FIELDS } from '../context/auto-compact-types.js';
36
43
  import { describeMode, thresholdFor } from './auto-compact-modes.js';
37
44
  import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
38
45
  import { CompactLifecyclePublisher, newCompactRunId, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
@@ -44,7 +51,7 @@ const PRE_COMPACT_REASON = 'pre-compact-auto';
44
51
  *
45
52
  * - ratio < preCompact → 'none' or 'soft-warn'
46
53
  * - ratio ≥ preCompact → 'pre-compact' (async-friendly path)
47
- * - ratio ≥ redLine → 'red-line' (synchronous gate)
54
+ * - ratio ≥ redLine → 'red-line' (ask the harness; dispatch continues)
48
55
  *
49
56
  * Slice 2026-07-28 (rid-027): `mode` selects the threshold table.
50
57
  * Default `'standard'` (0.85/0.95). `'partial'` (0.70/0.85) is used
@@ -64,10 +71,17 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
64
71
  };
65
72
  }
66
73
  if (ratio >= redLine) {
74
+ // Slice 2026-09-13-auto-compact-trigger-ownership (T3): the red line no
75
+ // longer claims it blocks anything. It cannot: peaks-loop has no
76
+ // executor for a running session (no `/compact` the model may invoke, no
77
+ // hook-initiated compact, no `--compact` flag), so "refuse dispatch until
78
+ // ratio < 0.85" was a gate with no key — a constructive deadlock at the
79
+ // exact moment the runner most needed to keep working. What peaks-loop
80
+ // CAN do is ask the harness (whose own trigger is armed) and say so.
67
81
  return {
68
82
  kind: 'red-line',
69
83
  ratio,
70
- message: `Context at ${(ratio * 100).toFixed(1)}% ≥ ${(redLine * 100).toFixed(0)}% red line (mode=${mode}). Synchronous compact REQUIRED (LLM cannot opt out).`
84
+ message: `Context at ${(ratio * 100).toFixed(1)}% ≥ ${(redLine * 100).toFixed(0)}% red line (mode=${mode}). peaks-loop has asked the harness to compact and is WAITING for it — sub-agent dispatch is NOT blocked; carry on and re-probe with \`peaks code context-now\`.`
71
85
  };
72
86
  }
73
87
  if (ratio < preCompact) {
@@ -104,8 +118,13 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
104
118
  * - 0.85 ≤ ratio < 0.95 → pre-compact; if in-flight batch
105
119
  * present, defer (D6.e); else dispatch
106
120
  * IDE compact asynchronously.
107
- * - ratio ≥ 0.95 → red-line; ALWAYS dispatch synchronously
108
- * regardless of in-flight batch.
121
+ * - ratio ≥ 0.95 → red-line; ask the harness to compact
122
+ * regardless of in-flight batch. Nothing is
123
+ * gated — dispatch is NOT blocked (slice
124
+ * 2026-09-13-auto-compact-trigger-ownership:
125
+ * peaks-loop cannot compact a running session,
126
+ * so a "block" gated nothing and deadlocked the
127
+ * runner).
109
128
  */
110
129
  export function evaluateAutoCompactDecision(input) {
111
130
  const trigger = evaluateCompactTrigger(input.ratio, input.mode ?? 'standard');
@@ -123,7 +142,10 @@ export function evaluateAutoCompactDecision(input) {
123
142
  return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'soft-warn' };
124
143
  }
125
144
  if (trigger.kind === 'red-line') {
126
- // Red line: ignore in-flight batch — synchronous dispatch wins.
145
+ // Red line: ignore in-flight batch — the harness is asked NOW rather than
146
+ // waiting for the batch to drain. Not "synchronous dispatch": peaks-loop
147
+ // has no synchronous compact to run, it can only request one and report
148
+ // that it is waiting.
127
149
  return { shouldCompact: true, reason: 'red-line', trigger, action: 'red-line' };
128
150
  }
129
151
  // pre-compact zone (0.85 ≤ ratio < 0.95): honor D6.e in-flight deferral.
@@ -162,7 +184,7 @@ export function buildConvergencePlan(input) {
162
184
  checkpointPath: input.checkpointPath,
163
185
  nextActions: [...input.nextActions],
164
186
  resumeHint: input.redLine === true
165
- ? 'RED-LINE compact: post-compact-detect must confirm ratio < 0.85 before resuming work.'
187
+ ? 'RED-LINE compact requested from the harness; work CONTINUES (nothing is blocked). Re-probe with `peaks code context-now`; if the ratio is still ≥ 0.95 and the harness has not compacted, report it and hand control back to the user.'
166
188
  : 'post-compact-detect shouldAutoResume → resume pre-compact plan from checkpoint'
167
189
  };
168
190
  }
@@ -204,6 +226,13 @@ function appendAutoDecisionLog(input) {
204
226
  * The file is gitignored under `.peaks/_runtime/<sessionId>/txt/` and
205
227
  * is one-shot: the LLM should `mv` it to `.consumed` after firing
206
228
  * `/compact`. A re-run will overwrite.
229
+ *
230
+ * ZERO READERS as of slice 2026-09-13-auto-compact-trigger-ownership: the
231
+ * write survives, but nothing consumes the file, and its `nextAction` asks
232
+ * for a `/compact` the model cannot invoke (the Skill tool exposes only
233
+ * `/init` and `/security-review`; hooks can observe or veto, never initiate).
234
+ * Retained deliberately for the sibling A2 slice, which owns harness-side
235
+ * state re-injection. Not a capability peaks-loop has today.
207
236
  */
208
237
  function writeMainSessionCompactIntent(input) {
209
238
  const dir = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'txt');
@@ -242,7 +271,7 @@ function writePreCompactCheckpoint(input) {
242
271
  // rehydrates from the auto-decisions log + open question list.
243
272
  mode: 'full-auto',
244
273
  currentPlan: input.redLine === true
245
- ? 'RED-LINE compact just executed; confirm ratio < 0.85 before resuming work'
274
+ ? 'RED-LINE compact REQUESTED from the harness (not executed by peaks-loop); work continues'
246
275
  : 'auto-compact in progress; resume from auto-decisions.md',
247
276
  openQuestions: [],
248
277
  recentDecisions: [],
@@ -276,13 +305,14 @@ function resolveAutoCompactMode(projectRoot) {
276
305
  * 5. If trigger.kind === 'pre-compact' AND in-flight batch → wait.
277
306
  * 6. If trigger.kind === 'pre-compact' → async dispatch (write
278
307
  * checkpoint + IDE compact; orchestrator returns immediately).
279
- * 7. If trigger.kind === 'red-line' → synchronous gate: refuse
280
- * sub-agent dispatch, dispatch IDE compact, mark `redLineGated`.
308
+ * 7. If trigger.kind === 'red-line' → dispatch the IDE compact, report
309
+ * that the harness has been asked and that dispatch continues.
281
310
  *
282
311
  * The caller (CLI or skill body) handles the actual return — D7's
283
- * post-compact-detect will pick up the checkpoint on the next turn.
284
- * For red-line, the caller MUST block further tool calls until the
285
- * post-compact probe confirms ratio < 0.85.
312
+ * post-compact-detect will pick up the checkpoint on the next turn. For
313
+ * red-line, the caller keeps working; it re-probes and, if the ratio is
314
+ * still rising with no compact from the harness, reports that instead of
315
+ * stalling.
286
316
  */
287
317
  export async function runAutoCompact(input) {
288
318
  const sessionId = input.sessionId ?? getSessionIdCanonical(input.projectRoot);
@@ -300,7 +330,7 @@ export async function runAutoCompact(input) {
300
330
  // v2.13.0 zero-pause contract.
301
331
  const mode = input.mode ?? resolveAutoCompactMode(input.projectRoot);
302
332
  // Lazy import to avoid the AC-1 module depending on the orchestrator.
303
- const { readContextPercent } = await import('../context/auto-compact-reader.js');
333
+ const { readContextPercent, syncHarnessWindowForProject } = await import('../context/auto-compact-reader.js');
304
334
  const outerSessionId = resolveOuterSessionId(input.projectRoot, sessionId, input.env ?? process.env);
305
335
  const probe = readContextPercent({
306
336
  projectRoot: input.projectRoot,
@@ -308,6 +338,21 @@ export async function runAutoCompact(input) {
308
338
  outerSessionId,
309
339
  env: input.env
310
340
  });
341
+ // Slice 2026-09-13-auto-compact-trigger-ownership (T1 + T2): write the very
342
+ // denominator this probe divided by into the harness's own machine-local
343
+ // settings, so "peaks-loop's 85%" and "the harness's trigger" are the same
344
+ // point on one scale. Idempotent — an unchanged value performs no write, so
345
+ // a hook firing this on every Bash call cannot churn the file.
346
+ const harnessWindow = syncHarnessWindowForProject({
347
+ // Promoted to the git root first (`--project .` is what the PreToolUse
348
+ // hook passes): the harness's settings live at the project root, and the
349
+ // envelope must report an absolute path for the write it claims to have
350
+ // made. Fail-open — `resolveCanonicalProjectRoot` returns its input when
351
+ // nothing resolves.
352
+ projectRoot: resolveCanonicalProjectRoot(input.projectRoot),
353
+ env: input.env,
354
+ tokens: probe.capacityTokens ?? null
355
+ });
311
356
  const decision = evaluateAutoCompactDecision({
312
357
  ratio: probe.ratio,
313
358
  // Slice 4.0.8 (D4d): production `inFlightBatch` MUST come from
@@ -337,7 +382,7 @@ export async function runAutoCompact(input) {
337
382
  // still open at `compacting`. Nothing is written when there is no
338
383
  // open run, when the ratio is still high, or when the probe could
339
384
  // not measure at all.
340
- settleOpenLifecycleRun({
385
+ const settled = settleOpenLifecycleRun({
341
386
  projectRoot: input.projectRoot,
342
387
  sessionId,
343
388
  measuredRatio: probe.ratio,
@@ -345,19 +390,60 @@ export async function runAutoCompact(input) {
345
390
  autoFireThreshold: thresholdFor(mode, 'autoFire'),
346
391
  onLifecycleStage: input.onLifecycleStage
347
392
  });
393
+ // Slice 2026-09-13-auto-compact-trigger-ownership (T4): a settle means a
394
+ // dispatched compact demonstrably landed. Append an `observed` row
395
+ // carrying the measured ratio, so `peaks compact history` can show
396
+ // "asked at R (intent) / landed by R' (observed)" once a real session has
397
+ // run. Without this row the intent has nothing to be compared against.
398
+ if (settled !== null) {
399
+ appendObservedCompactEvent({
400
+ projectRoot: input.projectRoot,
401
+ sessionId,
402
+ event: {
403
+ schemaVersion: 1,
404
+ kind: 'observed',
405
+ ts: new Date().toISOString(),
406
+ target: input.target ?? 'main',
407
+ mode,
408
+ ide: probe.ide,
409
+ pathway: 'post-compact-probe',
410
+ beforeRatio: settled.triggerRatio,
411
+ afterRatio: probe.ratio,
412
+ redLine: false,
413
+ ok: true,
414
+ checkpointPath: '',
415
+ dispatchMessage: `post-compact probe measured ratio ${(probe.ratio * 100).toFixed(1)}% (source=${probe.source}) after the compact dispatched at ${(settled.triggerRatio * 100).toFixed(1)}%`,
416
+ windowTokens: probe.capacityTokens ?? null,
417
+ windowSource: probe.capacitySource ?? null
418
+ }
419
+ });
420
+ }
348
421
  return {
349
422
  ok: true,
350
423
  code: decision.reason === 'in-flight-batch' ? 'AUTO_COMPACT_WAIT' : 'AUTO_COMPACT_SKIP',
351
- message: decision.trigger.kind === 'soft-warn'
424
+ message: `${decision.trigger.kind === 'soft-warn'
352
425
  ? decision.trigger.message
353
426
  : decision.reason === 'in-flight-batch'
354
427
  ? `In-flight batch detected; deferring pre-compact (ratio=${(probe.ratio * 100).toFixed(1)}%); next probe will re-evaluate.`
355
- : `Context at ${(probe.ratio * 100).toFixed(1)}%; below the ${(thresholdFor(mode, 'autoFire') * 100).toFixed(0)}% auto-fire threshold (mode=${mode}).`,
428
+ : `Context at ${(probe.ratio * 100).toFixed(1)}%; below the ${(thresholdFor(mode, 'autoFire') * 100).toFixed(0)}% auto-fire threshold (mode=${mode}).`}${
429
+ // No compact was needed, but the sync may still have rewritten the
430
+ // harness's settings (the first probe of a project always does). The
431
+ // notice is appended ONLY for an actual write — the other actions'
432
+ // sentences would be noise on every quiet probe — PLUS the one refusal
433
+ // that is not quiet: a refused write that leaves peaks-loop's number and
434
+ // the harness's pinned window disagreeing. That is the moment the ratio
435
+ // stops describing the harness's trigger, so staying silent is exactly
436
+ // the failure 要告知 exists to prevent.
437
+ harnessWindow !== null && harnessWindow !== undefined &&
438
+ (harnessWindow.action === 'written' || harnessWindowSyncWarning(harnessWindow) !== null)
439
+ ? ` ${describeHarnessWindowSync(harnessWindow)}`
440
+ : ''}`,
356
441
  data: {
357
442
  sessionId,
358
443
  ratio: probe.ratio,
359
444
  source: probe.source,
360
- decision: decision.reason === 'in-flight-batch' ? 'in-flight-batch' : 'below-threshold'
445
+ decision: decision.reason === 'in-flight-batch' ? 'in-flight-batch' : 'below-threshold',
446
+ harnessWindow
361
447
  }
362
448
  };
363
449
  }
@@ -392,14 +478,20 @@ export async function runAutoCompact(input) {
392
478
  });
393
479
  nextActions = isRedLine
394
480
  ? [
395
- 'RED-LINE compact dispatched — further sub-agent dispatch BLOCKED until ratio < 0.85',
481
+ 'RED-LINE: harness compact requested — sub-agent dispatch is NOT blocked; keep working',
396
482
  'Post-compact resume picks up the convergence plan from auto-decisions.md',
397
- 'Next `peaks code auto-compact` probe will confirm ratio dropped below 0.85'
483
+ 'Next `peaks code context-now` probe will confirm the ratio dropped; if it keeps climbing and the harness has not compacted, report that to the user and hand control back',
484
+ // Slice 2026-09-13-auto-compact-trigger-ownership: this command
485
+ // also rewrites the harness's own settings, so it says so too. The
486
+ // user accepted that write on the condition 要告知 — a write only
487
+ // one of the two syncing commands reports is not a notice.
488
+ describeHarnessWindowSync(harnessWindow)
398
489
  ]
399
490
  : [
400
491
  'Pre-compact dispatched — IDE compact in progress (async)',
401
492
  'Post-compact resume picks up the convergence plan from auto-decisions.md',
402
- 'Next `peaks code auto-compact` probe will confirm ratio dropped below 0.85'
493
+ 'Next `peaks code auto-compact` probe will confirm ratio dropped below 0.85',
494
+ describeHarnessWindowSync(harnessWindow)
403
495
  ];
404
496
  plan = buildConvergencePlan({
405
497
  sessionId,
@@ -426,7 +518,14 @@ export async function runAutoCompact(input) {
426
518
  source: probe.source,
427
519
  target: input.target ?? 'main',
428
520
  mode,
429
- redLineGated: isRedLine
521
+ redLineRequested: isRedLine,
522
+ // Compatibility alias + its record. `redLineGated` shipped from
523
+ // 2.13.0 to 4.0.46, so a consumer outside this repo reads it and must
524
+ // not start receiving `undefined`; the record is what makes the
525
+ // deprecation visible to that consumer. See the type.
526
+ redLineGated: isRedLine,
527
+ deprecatedFields: DEPRECATED_ENVELOPE_FIELDS,
528
+ harnessWindow
430
529
  }
431
530
  };
432
531
  }
@@ -474,7 +573,10 @@ export async function runAutoCompact(input) {
474
573
  convergencePlan: plan,
475
574
  target,
476
575
  mode,
477
- redLineGated: isRedLine
576
+ redLineRequested: isRedLine,
577
+ redLineGated: isRedLine,
578
+ deprecatedFields: DEPRECATED_ENVELOPE_FIELDS,
579
+ harnessWindow
478
580
  }
479
581
  };
480
582
  }
@@ -506,6 +608,7 @@ export async function runAutoCompact(input) {
506
608
  sessionId,
507
609
  event: {
508
610
  schemaVersion: 1,
611
+ kind: 'dispatch',
509
612
  ts: now.toISOString(),
510
613
  target,
511
614
  mode,
@@ -516,6 +619,10 @@ export async function runAutoCompact(input) {
516
619
  ok: dispatch.ok,
517
620
  checkpointPath,
518
621
  dispatchMessage: dispatch.message,
622
+ // T4 calibration: the exact denominator this dispatch divided by, so
623
+ // `beforeRatio * windowTokens` is the token point we asked for.
624
+ windowTokens: probe.capacityTokens ?? null,
625
+ windowSource: probe.capacitySource ?? null,
519
626
  },
520
627
  });
521
628
  }
@@ -527,7 +634,7 @@ export async function runAutoCompact(input) {
527
634
  : 'AUTO_COMPACT_DISPATCH_FAILED',
528
635
  message: dispatch.ok
529
636
  ? isRedLine
530
- ? `RED-LINE compact dispatched (${dispatch.ide} / ${dispatch.pathway} / target=${target} / mode=${mode} — ${describeMode(mode)}); checkpoint at ${checkpointPath}. Further sub-agent dispatch is BLOCKED until ratio < 0.85.`
637
+ ? `RED-LINE: harness compact REQUESTED (${dispatch.ide} / ${dispatch.pathway} / target=${target} / mode=${mode} — ${describeMode(mode)}); checkpoint at ${checkpointPath}. Sub-agent dispatch is NOT blocked — keep working and re-probe with \`peaks code context-now\`.`
531
638
  : `Auto-compact dispatched (${dispatch.ide} / ${dispatch.pathway} / target=${target} / mode=${mode} — ${describeMode(mode)}); checkpoint at ${checkpointPath}.`
532
639
  : `Auto-compact checkpoint written but IDE dispatch failed: ${dispatch.message}`,
533
640
  data: {
@@ -539,7 +646,10 @@ export async function runAutoCompact(input) {
539
646
  dispatch,
540
647
  target,
541
648
  mode,
542
- redLineGated: isRedLine
649
+ redLineRequested: isRedLine,
650
+ redLineGated: isRedLine,
651
+ deprecatedFields: DEPRECATED_ENVELOPE_FIELDS,
652
+ harnessWindow
543
653
  }
544
654
  };
545
655
  }
@@ -550,6 +660,17 @@ export function appendCompactHistoryEvent(input) {
550
660
  const path = join(dir, 'compact-history.jsonl');
551
661
  appendFileSync(path, JSON.stringify(input.event) + '\n', 'utf8');
552
662
  }
663
+ /**
664
+ * Best-effort variant for the post-compact measurement row. Telemetry must
665
+ * never change the probe's return envelope, so an append failure here is
666
+ * swallowed — same discipline as the dispatch-path append.
667
+ */
668
+ function appendObservedCompactEvent(input) {
669
+ try {
670
+ appendCompactHistoryEvent(input);
671
+ }
672
+ catch { /* best-effort; the probe result is already settled */ }
673
+ }
553
674
  // Keep dirname import live for symmetry with sibling services that
554
675
  // use it for path joins; tree-shaking removes it in builds.
555
676
  void dirname;
@@ -18,8 +18,10 @@
18
18
  * decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
19
19
  * (the LLM should AskUserQuestion, which is cheap).
20
20
  * Q4 — Is context usage sustainable? (probe `peaks code context-now
21
- * --json`). ratio ≥ 0.95 → blocker (red-line); ≥ 0.85 →
22
- * blocker (auto-compact-now).
21
+ * --json`). ratio ≥ 0.95 → WARNING (red-line, ask-and-wait);
22
+ * ≥ 0.85 → WARNING (pre-compact band). Never a blocker — see the
23
+ * comment on the Q4 branch in `buildOrchestratorCanDoResult`, and
24
+ * `evaluateCompactTrigger`, which is the face this one must agree with.
23
25
  *
24
26
  * Decision rule:
25
27
  * canDoInSession === (blockers.length === 0)
@@ -18,8 +18,10 @@
18
18
  * decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
19
19
  * (the LLM should AskUserQuestion, which is cheap).
20
20
  * Q4 — Is context usage sustainable? (probe `peaks code context-now
21
- * --json`). ratio ≥ 0.95 → blocker (red-line); ≥ 0.85 →
22
- * blocker (auto-compact-now).
21
+ * --json`). ratio ≥ 0.95 → WARNING (red-line, ask-and-wait);
22
+ * ≥ 0.85 → WARNING (pre-compact band). Never a blocker — see the
23
+ * comment on the Q4 branch in `buildOrchestratorCanDoResult`, and
24
+ * `evaluateCompactTrigger`, which is the face this one must agree with.
23
25
  *
24
26
  * Decision rule:
25
27
  * canDoInSession === (blockers.length === 0)
@@ -153,6 +155,7 @@ export async function probeSubAgentAvailable(projectRoot, peaksBin = DEFAULT_PEA
153
155
  await execFileAsync(command, [...args, 'sub-agent', 'dispatch', '--role', 'rd', '--help'], {
154
156
  cwd: projectRoot,
155
157
  timeout: 5000,
158
+ windowsHide: true,
156
159
  });
157
160
  return true;
158
161
  }
@@ -171,6 +174,7 @@ export async function probeContextRatio(projectRoot, peaksBin = DEFAULT_PEAKS_BI
171
174
  const { stdout } = await execFileAsync(command, [...args, 'code', 'context-now', '--project', projectRoot, '--json'], {
172
175
  cwd: projectRoot,
173
176
  timeout: 10000,
177
+ windowsHide: true,
174
178
  });
175
179
  const parsed = JSON.parse(stdout);
176
180
  const ratio = typeof parsed.data?.ratio === 'number' ? parsed.data.ratio : 0;
@@ -205,13 +209,41 @@ export function buildOrchestratorCanDoResult(input, signals) {
205
209
  blockers.push('sub-agent dispatch unavailable (peaks sub-agent dispatch --help failed)');
206
210
  suggestions.push('verify peaks CLI is on PATH; check `peaks --version`');
207
211
  }
208
- // Q4 — context ratio gate. ≥0.95 → red-line; ≥0.85 → pre-compact.
212
+ // Q4 — context ratio. ≥0.95 → red-line; ≥0.85 → pre-compact. A WARNING, not
213
+ // a blocker, and the difference is deliberate (E3, rid 2026-09-13-defects-e).
214
+ //
215
+ // Why this is not a blocker any more, and why it is not an oversight:
216
+ //
217
+ // The peak this answers is "can this slice run in the current session".
218
+ // Context ratio cannot answer "no" to it. The reason is the one the
219
+ // T3 slice (2026-09-13-auto-compact-trigger-ownership) landed on the
220
+ // OTHER face of this same threshold: peaks-loop has no executor for a
221
+ // running session, so it cannot compact its way out of a high ratio —
222
+ // `evaluateCompactTrigger` therefore says of the red line "peaks-loop has
223
+ // asked the harness to compact and is WAITING for it — sub-agent dispatch
224
+ // is NOT blocked; carry on and re-probe". A probe that returned
225
+ // `canDoInSession: false` here would contradict that sentence for the same
226
+ // 0.95 on the same number, and its only programmatic consumer
227
+ // (`code-orchestrator-can-do.ts`) turns the false into exit code 1 — i.e.
228
+ // it would re-open, at the CLI layer, exactly the deadlock T3 deleted.
229
+ //
230
+ // Nor does the pre-compact zone (0.85–0.95) earn a blocker: the trigger's
231
+ // message there is "peaks-loop already fired the auto-compact pathway; the
232
+ // LLM does not need to act", which is the opposite of "you may not proceed".
233
+ //
234
+ // What a blocker would still need to be true: THE ORCHESTRATOR itself
235
+ // cannot continue. It can — the slice's edits are delegated to a sub-agent
236
+ // (Q1), and the orchestrator's own context is only spent co-ordinating.
237
+ //
238
+ // So the ratio is reported, warned about, and given a next action; the
239
+ // verdict is left to the blockers that really are un-survivable (a
240
+ // hard-blocked path family, an unreachable sub-agent dispatcher).
209
241
  if (signals.q4ContextRatio >= ORCHESTRATOR_REDLINE_RATIO) {
210
- blockers.push(`context red-line (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_REDLINE_RATIO}); auto-compact now or push to next session`);
242
+ warnings.push(`context red-line (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_REDLINE_RATIO}): peaks-loop has asked the harness to compact and is waiting for it; dispatch is NOT blocked — carry on and re-probe with \`peaks code context-now\``);
211
243
  suggestions.push('peaks code auto-compact');
212
244
  }
213
245
  else if (signals.q4ContextRatio >= ORCHESTRATOR_PRECOMPACT_RATIO) {
214
- blockers.push(`context near limit (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_PRECOMPACT_RATIO}); auto-compact or push to next session`);
246
+ warnings.push(`context near limit (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_PRECOMPACT_RATIO}): in the pre-compact band; peaks-loop fires the auto-compact pathway itself and dispatch is NOT blocked`);
215
247
  suggestions.push('peaks code auto-compact');
216
248
  }
217
249
  // Q3 — user-decision keywords → soft warning, NOT a blocker. The
@@ -176,7 +176,8 @@ export const CODEGRAPH_CONFIG_FILENAME = 'config.json';
176
176
  export function readTrackedFiles(projectRoot) {
177
177
  const stdout = execFileSync('git', ['-C', projectRoot, 'ls-files'], {
178
178
  encoding: 'utf8',
179
- maxBuffer: 64 * 1024 * 1024
179
+ maxBuffer: 64 * 1024 * 1024,
180
+ windowsHide: true
180
181
  });
181
182
  return stdout
182
183
  .split('\n')
@@ -27,7 +27,7 @@ function terminateCodegraphProcess(childProcess) {
27
27
  }
28
28
  if (process.platform === 'win32') {
29
29
  if (process.env.SystemRoot) {
30
- spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
30
+ spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore', windowsHide: true });
31
31
  }
32
32
  else {
33
33
  childProcess.kill();
@@ -47,7 +47,8 @@ export function defaultCodegraphProcessRunner(invocation) {
47
47
  cwd: invocation.cwd,
48
48
  detached: process.platform !== 'win32',
49
49
  env: createCodegraphEnvironment(),
50
- shell: false
50
+ shell: false,
51
+ windowsHide: true
51
52
  });
52
53
  const timeout = setTimeout(() => {
53
54
  terminateCodegraphProcess(childProcess);
@@ -15,8 +15,11 @@
15
15
  *
16
16
  * The hook is INVOKED from `src/cli/commands/request-commands.ts`
17
17
  * (RD→QA boundary) and from any future QA→final-review boundary. The
18
- * 0.95 red line is NOT changed: the existing auto-compact
19
- * orchestrator continues to refuse dispatch at ratio ≥ 0.95.
18
+ * 0.95 red line is unchanged in THRESHOLD but not in behaviour: the
19
+ * auto-compact orchestrator asks the harness to compact and reports that
20
+ * it is waiting. It does not refuse sub-agent dispatch — peaks-loop has no
21
+ * executor for a running session, so a refusal gated nothing and deadlocked
22
+ * the runner (slice 2026-09-13-auto-compact-trigger-ownership).
20
23
  */
21
24
  import { existsSync, readFileSync } from 'node:fs';
22
25
  import { join } from 'node:path';