peaks-loop 4.0.48 → 4.0.50

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 (156) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/audit-commands.js +1 -0
  5. package/dist/cli/commands/baseline-commands.js +163 -25
  6. package/dist/cli/commands/compact-command.js +1 -3
  7. package/dist/cli/commands/core/skill-command.js +53 -4
  8. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  9. package/dist/cli/commands/core/standards-command.js +74 -0
  10. package/dist/cli/commands/feedback-commands.d.ts +11 -7
  11. package/dist/cli/commands/feedback-commands.js +49 -17
  12. package/dist/cli/commands/final-review-commands.js +12 -0
  13. package/dist/cli/commands/hooks-commands.js +55 -38
  14. package/dist/cli/commands/loop-eval-commands.js +22 -6
  15. package/dist/cli/commands/share-commands.js +37 -11
  16. package/dist/cli/commands/slice-integrate-commands.js +17 -0
  17. package/dist/cli/commands/web-commands.js +8 -1
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  20. package/dist/services/adapter/adapter.d.ts +30 -0
  21. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  22. package/dist/services/adapter/claude-adapter.js +12 -0
  23. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  24. package/dist/services/adapter/codex-adapter.js +12 -0
  25. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  26. package/dist/services/adapter/copilot-adapter.js +12 -0
  27. package/dist/services/artifacts/artifact-prerequisites.js +10 -0
  28. package/dist/services/artifacts/request-artifact-service.js +59 -38
  29. package/dist/services/audit/backing-detector.d.ts +25 -7
  30. package/dist/services/audit/backing-detector.js +33 -17
  31. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  32. package/dist/services/audit/enforcer-liveness.js +100 -0
  33. package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
  34. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  35. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  36. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  37. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  38. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  39. package/dist/services/audit/enforcers/lint-style.js +38 -2
  40. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  41. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  42. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  43. package/dist/services/audit/red-lines-service.js +51 -7
  44. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  45. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  46. package/dist/services/capability-audit-service/index.d.ts +3 -1
  47. package/dist/services/capability-audit-service/index.js +1 -0
  48. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  49. package/dist/services/capability-audit-service/runner.js +76 -15
  50. package/dist/services/capability-audit-service/types.d.ts +48 -0
  51. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  52. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  53. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  54. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  55. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  56. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  57. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  58. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  59. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  60. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  61. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  62. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  63. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  64. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  65. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  66. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  67. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  68. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  69. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  70. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  71. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  72. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  73. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  74. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  75. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  76. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  77. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  78. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  79. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  80. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  81. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  82. package/dist/services/capability-guard-runner/registry.js +140 -0
  83. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  84. package/dist/services/capability-guard-runner/runner.js +63 -6
  85. package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
  86. package/dist/services/code/auto-compact-lifecycle.js +65 -16
  87. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  88. package/dist/services/code/auto-compact-modes.js +20 -4
  89. package/dist/services/code/auto-compact-orchestrator.js +119 -19
  90. package/dist/services/code/compact-event-settle.d.ts +20 -8
  91. package/dist/services/code/compact-event-settle.js +21 -0
  92. package/dist/services/code/post-compact-detector.js +20 -11
  93. package/dist/services/code/step-08-gate.js +21 -6
  94. package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
  95. package/dist/services/config/config-safety.js +11 -9
  96. package/dist/services/context/auto-compact-types.d.ts +20 -2
  97. package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
  98. package/dist/services/feedback/feedback-promotion-service.js +341 -20
  99. package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
  100. package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
  101. package/dist/services/final-review/pre-post-diff.js +10 -2
  102. package/dist/services/job/job-progress-store.js +18 -3
  103. package/dist/services/observability/jsonl-store.d.ts +19 -0
  104. package/dist/services/observability/jsonl-store.js +27 -2
  105. package/dist/services/observability/observability-service.d.ts +11 -4
  106. package/dist/services/observability/observability-service.js +16 -3
  107. package/dist/services/prd/handoff-service.js +43 -0
  108. package/dist/services/qa/qa-business-review-state.js +19 -5
  109. package/dist/services/sc/sc-service.d.ts +8 -0
  110. package/dist/services/sc/sc-service.js +8 -1
  111. package/dist/services/scan/api-diff-types.js +20 -2
  112. package/dist/services/security/safe-settings-path.js +19 -1
  113. package/dist/services/session/getSessionDir.d.ts +33 -0
  114. package/dist/services/session/getSessionDir.js +60 -0
  115. package/dist/services/skill/skill-search-service.d.ts +3 -3
  116. package/dist/services/slice/slice-review-state.js +19 -4
  117. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  118. package/dist/services/standards/loop-engineering-lint.js +6 -0
  119. package/dist/services/web/daemon-registry.js +27 -2
  120. package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
  121. package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
  122. package/dist/services/workflow/pipeline-verify-service.js +23 -10
  123. package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
  124. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  125. package/dist/services/workspace/claude-settings-template.js +105 -83
  126. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  127. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  128. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  129. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  130. package/dist/services/workspace/workspace-service.js +11 -1
  131. package/dist/shared/fs-utils.d.ts +26 -0
  132. package/dist/shared/fs-utils.js +35 -0
  133. package/dist/shared/runtime-root.d.ts +73 -0
  134. package/dist/shared/runtime-root.js +77 -0
  135. package/package.json +9 -7
  136. package/scripts/copy-templates.mjs +0 -12
  137. package/scripts/install-skills.mjs +154 -53
  138. package/skills/bee/peaks-qa/SKILL.md +0 -1
  139. package/skills/bee/peaks-rd/SKILL.md +0 -1
  140. package/skills/peaks-code/SKILL.md +12 -10
  141. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  142. package/skills/peaks-code/references/runbook.md +3 -0
  143. package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
  144. package/skills/peaks-code/references/startup-sequence.md +2 -2
  145. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  146. package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
  147. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  148. package/dist/cli/commands/context-builder-commands.js +0 -85
  149. package/dist/services/hooks/write-gate.js +0 -111
  150. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  151. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  152. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  153. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  154. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  155. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  156. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -0,0 +1,140 @@
1
+ // src/services/capability-guard-runner/registry.ts
2
+ //
3
+ // The single production entry point for the 15 P0 guard contracts.
4
+ //
5
+ // Why a registry: before this, `baseline run-guard` hard-coded `J01`, and the
6
+ // only caller of `runAllGuards` was its own unit test (which passed synthetic
7
+ // contracts against `projectRoot: '/'`). There was no production path that ran
8
+ // all fifteen journeys — so "15 pass / 0 fail" was an assertion nobody made.
9
+ //
10
+ // Each contract declares the baseline row AND the verbatim invariant text it
11
+ // enforces; `assertBaselineRef` resolves that against
12
+ // openspec/baselines/current/capability-baseline.json before the contract runs.
13
+ import { P0_JOURNEY_IDS } from '../capability-baseline/types.js';
14
+ import { runJ01Contract } from './contracts/J01.js';
15
+ import { runJ02Contract } from './contracts/J02.js';
16
+ import { runJ03Contract } from './contracts/J03.js';
17
+ import { runJ04Contract } from './contracts/J04.js';
18
+ import { runJ05Contract } from './contracts/J05.js';
19
+ import { runJ06Contract } from './contracts/J06.js';
20
+ import { runJ07Contract } from './contracts/J07.js';
21
+ import { runJ08Contract } from './contracts/J08.js';
22
+ import { runJ09Contract } from './contracts/J09.js';
23
+ import { runJ10Contract } from './contracts/J10.js';
24
+ import { runJ11Contract } from './contracts/J11.js';
25
+ import { runJ12Contract } from './contracts/J12.js';
26
+ import { runJ13Contract } from './contracts/J13.js';
27
+ import { runJ14Contract } from './contracts/J14.js';
28
+ import { runJ15Contract } from './contracts/J15.js';
29
+ /** The integration test that pins each journey's contract. */
30
+ const ARTIFACTS = {
31
+ J01: 'tests/integration/capability-guard/J01-envelope-arg-shapes.test.ts',
32
+ J02: 'tests/integration/capability-guard/J02-workflow-trace.test.ts',
33
+ J03: 'tests/integration/capability-guard/J03-problem-resolution-flow.test.ts',
34
+ J04: 'tests/integration/capability-guard/J04-audit-goal-binding.test.ts',
35
+ J05: 'tests/integration/capability-guard/J05-final-review-shape.test.ts',
36
+ J06: 'tests/integration/capability-guard/J06-resume-deepest-gate.test.ts',
37
+ J07: 'tests/integration/capability-guard/J07-test-runner-fidelity.test.ts',
38
+ J08: 'tests/integration/capability-guard/J08-asset-roundtrip.test.ts',
39
+ J09: 'tests/integration/capability-guard/J09-sop-register.test.ts',
40
+ J10: 'tests/integration/capability-guard/J10-ide-install-assertion.test.ts',
41
+ J11: 'tests/integration/capability-guard/J11-doctor-cli-snapshot.test.ts',
42
+ J12: 'tests/integration/capability-guard/J12-lease-lifecycle.test.ts',
43
+ J13: 'tests/integration/capability-guard/J13-content-pipeline-trace.test.ts',
44
+ J14: 'tests/integration/capability-guard/J14-issue-orchestrator-trace.test.ts',
45
+ J15: 'tests/integration/capability-guard/J15-spec-coverage.test.ts'
46
+ };
47
+ /** One row per P0 journey. `invariant` must be verbatim baseline text. */
48
+ const SPECS = {
49
+ J01: {
50
+ kind: 'envelope-arg-shapes',
51
+ invariant: 'The CLI verb is never shown to the user as a required input',
52
+ execute: runJ01Contract
53
+ },
54
+ J02: {
55
+ kind: 'workflow-trace',
56
+ invariant: 'Every hard gate (audit / RD / QA / final-review) is enforced; a failure transitions state back to the prior role',
57
+ execute: runJ02Contract
58
+ },
59
+ J03: {
60
+ kind: 'workflow-trace',
61
+ invariant: 'No silent-catch or fake-green pattern is reintroduced in src/services/**',
62
+ execute: runJ03Contract
63
+ },
64
+ J04: {
65
+ kind: 'hook-assertion',
66
+ invariant: 'The audit covers exactly six dimensions: correctness, completeness, scope, risks, alternatives, constraints',
67
+ execute: runJ04Contract
68
+ },
69
+ J05: {
70
+ kind: 'workflow-trace',
71
+ invariant: 'Every final review contains exactly four dimensions: functional-completeness, problem-resolution, no-new-bugs, existing-functionality-intact',
72
+ execute: runJ05Contract
73
+ },
74
+ J06: {
75
+ kind: 'workflow-trace',
76
+ invariant: 'The resume option always identifies the deepest completed gate (audit / RD / QA / final-review)',
77
+ execute: runJ06Contract
78
+ },
79
+ J07: {
80
+ kind: 'cli-output-golden',
81
+ invariant: "Per-test fingerprint cache never returns 'passed' for an unverified file (fileMtime + fileSha256 gate)",
82
+ execute: runJ07Contract
83
+ },
84
+ J08: {
85
+ kind: 'asset-roundtrip',
86
+ invariant: 'Crystallization requires all gates (audit / RD / QA / final-review) to be in a passing state',
87
+ execute: runJ08Contract
88
+ },
89
+ J09: {
90
+ kind: 'sop-register',
91
+ invariant: 'An SOP id must match SOP_ID_PATTERN (lowercase kebab, no dots/slashes) to prevent path traversal',
92
+ execute: runJ09Contract
93
+ },
94
+ J10: {
95
+ kind: 'hook-assertion',
96
+ invariant: 'planHookInstall() runs before applyHookInstall() so the diff is reviewable',
97
+ execute: runJ10Contract
98
+ },
99
+ J11: {
100
+ kind: 'cli-output-golden',
101
+ invariant: 'Doctor checks live under src/services/doctor/doctor-service/checks/ as a code-driven fixed-registry tree',
102
+ execute: runJ11Contract
103
+ },
104
+ J12: {
105
+ kind: 'concurrency-lease',
106
+ invariant: 'Auto-release on terminal heartbeat is best-effort but idempotent: duplicate release is a no-op',
107
+ execute: runJ12Contract
108
+ },
109
+ J13: {
110
+ kind: 'workflow-trace',
111
+ invariant: 'The skill does NOT import peaks-code internals; it reuses the Loop Engineering primitives',
112
+ execute: runJ13Contract
113
+ },
114
+ J14: {
115
+ kind: 'workflow-trace',
116
+ invariant: 'Every fix carries an AI-modified declaration in the commit body (RL-3)',
117
+ execute: runJ14Contract
118
+ },
119
+ J15: {
120
+ kind: 'spec-coverage',
121
+ invariant: 'Coverage-summary discovery order is fixed: <projectRoot>/coverage/coverage-summary.json then /openspec/coverage-summary.json',
122
+ execute: runJ15Contract
123
+ }
124
+ };
125
+ export const GUARD_CONTRACTS = P0_JOURNEY_IDS.map((journeyId) => {
126
+ const spec = SPECS[journeyId];
127
+ return {
128
+ journeyId,
129
+ kind: spec.kind,
130
+ source: { baselineRow: journeyId, invariant: spec.invariant },
131
+ execute: spec.execute,
132
+ evidence: { kind: spec.kind, artifact: ARTIFACTS[journeyId] }
133
+ };
134
+ });
135
+ export function getGuardContract(journeyId) {
136
+ return GUARD_CONTRACTS.find((c) => c.journeyId === journeyId);
137
+ }
138
+ export function isJourneyId(value) {
139
+ return P0_JOURNEY_IDS.includes(value);
140
+ }
@@ -1,5 +1,22 @@
1
+ import type { CapabilityBaselineRow } from '../capability-baseline/types.js';
1
2
  import type { GuardContract, GuardContext, GuardRunResult } from './types.js';
3
+ /**
4
+ * Resolve the contract's declared baseline row out of the frozen baseline and
5
+ * assert the declared invariant is verbatim text in that row.
6
+ *
7
+ * Before this, the check was `if (!source.baselineRow || !source.invariant)`,
8
+ * i.e. it only asserted the two fields were truthy. Nothing ever read the
9
+ * baseline, so a contract could name a journey / invariant that does not exist
10
+ * in the frozen file and still run to a green result.
11
+ */
12
+ export declare function assertBaselineRef(contract: GuardContract, projectRoot: string): CapabilityBaselineRow;
2
13
  export declare function runGuard(contract: GuardContract, ctx: GuardContext): Promise<GuardRunResult>;
14
+ /**
15
+ * Run every contract and aggregate. A contract that throws (bad baseline
16
+ * reference, unreadable source file, a probe that blows up) is recorded as a
17
+ * `fail` result rather than aborting the whole run — the gate has to be able
18
+ * to say "this journey is broken", not just crash.
19
+ */
3
20
  export declare function runAllGuards(contracts: ReadonlyArray<GuardContract>, ctx: GuardContext): Promise<{
4
21
  readonly pass: number;
5
22
  readonly fail: number;
@@ -7,3 +24,12 @@ export declare function runAllGuards(contracts: ReadonlyArray<GuardContract>, ct
7
24
  readonly total: number;
8
25
  readonly results: ReadonlyArray<GuardRunResult>;
9
26
  }>;
27
+ /**
28
+ * Exit-code contract for a guard summary. `skipped` must not be
29
+ * indistinguishable from `pass`: a run that quietly skipped work is a
30
+ * different outcome from a run that verified work, so it gets its own code.
31
+ */
32
+ export declare function exitCodeForGuardSummary(summary: {
33
+ readonly fail: number;
34
+ readonly skipped: number;
35
+ }): 0 | 1 | 2;
@@ -1,16 +1,61 @@
1
- function assertBaselineRef(contract) {
2
- if (!contract.source.baselineRow || !contract.source.invariant) {
3
- throw new Error('GUARD_CONTRACT_MISSING_BASELINE_REF: contract must reference a baseline row and invariant');
1
+ // src/services/capability-guard-runner/runner.ts
2
+ import { readBaselineFile } from '../capability-baseline/store.js';
3
+ /**
4
+ * Resolve the contract's declared baseline row out of the frozen baseline and
5
+ * assert the declared invariant is verbatim text in that row.
6
+ *
7
+ * Before this, the check was `if (!source.baselineRow || !source.invariant)`,
8
+ * i.e. it only asserted the two fields were truthy. Nothing ever read the
9
+ * baseline, so a contract could name a journey / invariant that does not exist
10
+ * in the frozen file and still run to a green result.
11
+ */
12
+ export function assertBaselineRef(contract, projectRoot) {
13
+ const r = readBaselineFile(projectRoot);
14
+ if (!r.ok) {
15
+ throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} cannot read the frozen baseline at ${projectRoot} (${r.error.code}: ${r.error.message})`);
4
16
  }
17
+ const row = r.file.rows.find((x) => x.journeyId === contract.source.baselineRow);
18
+ if (!row) {
19
+ throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} references baseline row ${contract.source.baselineRow}, which is not in the frozen baseline`);
20
+ }
21
+ const declared = contract.source.invariant;
22
+ if (typeof declared !== 'string' || declared.trim().length === 0) {
23
+ throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} declares an empty invariant`);
24
+ }
25
+ if (!row.invariants.includes(declared)) {
26
+ throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} declares invariant "${declared}", which is not a verbatim invariant of baseline row ${row.journeyId}`);
27
+ }
28
+ return row;
5
29
  }
6
30
  export async function runGuard(contract, ctx) {
7
- assertBaselineRef(contract);
8
- return contract.execute(ctx);
31
+ assertBaselineRef(contract, ctx.projectRoot);
32
+ return contract.execute({ ...ctx, contract });
9
33
  }
34
+ /**
35
+ * Run every contract and aggregate. A contract that throws (bad baseline
36
+ * reference, unreadable source file, a probe that blows up) is recorded as a
37
+ * `fail` result rather than aborting the whole run — the gate has to be able
38
+ * to say "this journey is broken", not just crash.
39
+ */
10
40
  export async function runAllGuards(contracts, ctx) {
11
41
  const results = [];
12
42
  for (const c of contracts) {
13
- results.push(await runGuard(c, ctx));
43
+ try {
44
+ results.push(await runGuard(c, ctx));
45
+ }
46
+ catch (e) {
47
+ results.push({
48
+ journeyId: c.journeyId,
49
+ contract: c.kind,
50
+ status: 'fail',
51
+ diff: {
52
+ before: 'contract runs against the frozen baseline',
53
+ after: e.message,
54
+ reason: `${c.journeyId} contract could not run`
55
+ },
56
+ artifactPath: c.evidence.artifact
57
+ });
58
+ }
14
59
  }
15
60
  return {
16
61
  pass: results.filter((r) => r.status === 'pass').length,
@@ -20,3 +65,15 @@ export async function runAllGuards(contracts, ctx) {
20
65
  results
21
66
  };
22
67
  }
68
+ /**
69
+ * Exit-code contract for a guard summary. `skipped` must not be
70
+ * indistinguishable from `pass`: a run that quietly skipped work is a
71
+ * different outcome from a run that verified work, so it gets its own code.
72
+ */
73
+ export function exitCodeForGuardSummary(summary) {
74
+ if (summary.fail > 0)
75
+ return 1;
76
+ if (summary.skipped > 0)
77
+ return 2;
78
+ return 0;
79
+ }
@@ -51,6 +51,65 @@ export declare function resolveDispatchedStage(input: {
51
51
  * attempts inside the same millisecond distinct.
52
52
  */
53
53
  export declare function newCompactRunId(now: Date): string;
54
+ /**
55
+ * rid `2026-09-14-compact-dispatch-backoff`: the compact run this session has
56
+ * already DISPATCHED and whose outcome is still unknown — the backoff token.
57
+ *
58
+ * WHY A BACKOFF IS NEEDED AT ALL. Once the ratio crosses the auto-fire
59
+ * threshold it STAYS crossed: nothing in peaks-loop can compact a running
60
+ * session, and the harness fires only at its own red line. So the dispatch
61
+ * obligation became unsatisfiable and fired on every probe. Measured in one
62
+ * real session (2026-09-13T22:43:33Z → 2026-09-14T14:15:02Z, ~15.5 h):
63
+ * 1035 `dispatch` rows, 444 checkpoints, and ZERO compactions — 1075 by
64
+ * 14:22:09.112Z. The dispatch itself is idempotent — `ide-native` only
65
+ * installs a PreToolUse hook, and a second install of the same hook is a
66
+ * documented no-op — so 1074 of those rows installed nothing (their own
67
+ * `dispatchMessage` says `already installed`) and carried no new information.
68
+ * Only noise: a signal that fires a thousand times is not a signal.
69
+ *
70
+ * WHY NO NEW STORE. The one-record-per-session lifecycle store already holds
71
+ * the one fact the backoff needs: is a compact attempt dispatched and not yet
72
+ * superseded? `armed` and `compacting` are exactly those two stages, and
73
+ * `completed` / `failed` mean the attempt is over and the next crossing is a
74
+ * legitimate new one. The state lives here rather than in
75
+ * `compact-history.jsonl`, which is append-only and never rewritten — so the
76
+ * backoff cannot be built by editing history, which is the point.
77
+ *
78
+ * STALENESS IS DELIBERATELY IGNORED, for `settleOpenLifecycleRun`'s reason: a
79
+ * probe arriving after `staleAfterMs` has learned nothing about whether the run
80
+ * is still live. Age cannot make re-dispatching honest, so the stale window
81
+ * must not decide it. (`settleOpenLifecycleRun` reads with the same window for
82
+ * the same reason; the two must not grow two policies.)
83
+ *
84
+ * `queued` / `preparing` / `verifying` are NOT open here: they are transient
85
+ * stages inside a dispatch or a settle, and a process that died in one of them
86
+ * left no compact attempt to protect — the next probe should be free to
87
+ * dispatch. `failed` is not open either, by the same argument: a failure is a
88
+ * reason to try again, not a reason to stay quiet.
89
+ */
90
+ /**
91
+ * The answer to "is a compact attempt outstanding for this session?".
92
+ *
93
+ * `none` and `unresolvable` must not be the same value. The caller uses
94
+ * `none` to ADMIT a dispatch; `unresolvable` means the question could
95
+ * not be asked, and a caller that admits on "could not ask" has a gate
96
+ * that reads a string rather than the artifact it names.
97
+ */
98
+ export type OpenDispatchRunRead = {
99
+ readonly kind: 'open';
100
+ readonly runId: string;
101
+ readonly stage: 'armed' | 'compacting';
102
+ readonly triggerRatio: number;
103
+ } | {
104
+ readonly kind: 'none';
105
+ } | {
106
+ readonly kind: 'unresolvable';
107
+ readonly reason: string;
108
+ };
109
+ export declare function readOpenDispatchRun(input: {
110
+ readonly projectRoot: string;
111
+ readonly sessionId: string;
112
+ }): OpenDispatchRunRead;
54
113
  /**
55
114
  * Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
56
115
  * builder. Carries `runId`, `triggerRatio` and `redLine` forward from
@@ -132,6 +191,18 @@ export declare function summarizeLifecycleError(error: unknown): string;
132
191
  * `afterRatio` to append the "observed compaction point" row that makes the
133
192
  * intent-vs-observed gap readable after a real session. The return value is
134
193
  * telemetry only — callers that ignore it are unaffected.
194
+ *
195
+ * `lifecycleWritten` (repair R6) makes the returned record honest about
196
+ * whether the RUN actually moved. Before it, a failed write still returned
197
+ * the full envelope, so a store that could not be written reported a settle
198
+ * on every probe, forever: the run stayed `armed`, the next probe found it
199
+ * open again, and each one added another "observed compaction point" row —
200
+ * an unbounded append driven by a write that never happened. A failed write
201
+ * is not an absent run (the facts are real and stay true, so the caller may
202
+ * still record the measurement), but it is also not a settled one. This is
203
+ * the same field `settleOpenLifecycleRunOnCompactEvent` carries, for the
204
+ * same reason, so the two settle paths no longer disagree about what a write
205
+ * failure means.
135
206
  */
136
207
  export declare function settleOpenLifecycleRun(input: {
137
208
  readonly projectRoot: string;
@@ -140,10 +211,14 @@ export declare function settleOpenLifecycleRun(input: {
140
211
  readonly source: string;
141
212
  readonly autoFireThreshold: number;
142
213
  readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
214
+ /** Failure injection for the write below — the seam `CompactLifecyclePublisher` already takes. */
215
+ readonly failLifecycleWrite?: boolean | undefined;
143
216
  }): {
144
217
  readonly runId: string;
145
218
  readonly triggerRatio: number;
146
219
  readonly afterRatio: number;
220
+ /** `false` when the `completed` write threw, so the run is STILL open. */
221
+ readonly lifecycleWritten: boolean;
147
222
  } | null;
148
223
  /**
149
224
  * rid `2026-09-13-compact-event-settle`: close out an open compact run because
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
15
15
  import { AUTO_COMPACT_RED_LINE_RATIO } from '../context/auto-compact-types.js';
16
+ import { tryGetSessionDir } from '../session/getSessionDir.js';
16
17
  /**
17
18
  * PRD-002b slice 2 — extract the few cross-cutting magic numbers
18
19
  * that actually appear at runtime call sites in this orchestrator.
@@ -68,11 +69,18 @@ export function newCompactRunId(now) {
68
69
  * later. Wrapping that unbounded read here keeps the magic number out
69
70
  * of the settle path and makes the intent self-documenting.
70
71
  *
71
- * Returns `null` for any non-`valid` kind (missing / invalid /
72
- * stalled). Errors from the underlying read are swallowed — settling
73
- * is best-effort telemetry and must never bubble.
72
+ * The session id is resolved through `tryGetSessionDir` FIRST, so the
73
+ * guard's throw is answered before the store is entered and the failing
74
+ * branch is a value the caller must handle rather than a `catch` nobody
75
+ * reads. The residual `catch` is therefore unreachable for a bad id;
76
+ * anything that reaches it is a store fault, and a store fault is still
77
+ * not "there is no open run" — hence `unresolvable`, never `none`.
74
78
  */
75
79
  function readOpenCompactLifecycle(input) {
80
+ const resolved = tryGetSessionDir(input.projectRoot, input.sessionId);
81
+ if (!resolved.ok) {
82
+ return { kind: 'unresolvable', reason: resolved.reason };
83
+ }
76
84
  let out;
77
85
  try {
78
86
  out = readCompactLifecycle({
@@ -85,10 +93,21 @@ function readOpenCompactLifecycle(input) {
85
93
  staleAfterMs: Number.MAX_SAFE_INTEGER
86
94
  });
87
95
  }
88
- catch {
89
- return null;
96
+ catch (error) {
97
+ return { kind: 'unresolvable', reason: summarizeLifecycleError(error) };
90
98
  }
91
- return out.kind === 'valid' ? out.record : null;
99
+ return out.kind === 'valid' ? { kind: 'found', record: out.record } : { kind: 'none' };
100
+ }
101
+ export function readOpenDispatchRun(input) {
102
+ const read = readOpenCompactLifecycle(input);
103
+ if (read.kind === 'unresolvable')
104
+ return read;
105
+ if (read.kind === 'none')
106
+ return { kind: 'none' };
107
+ const record = read.record;
108
+ if (record.stage !== 'armed' && record.stage !== 'compacting')
109
+ return { kind: 'none' };
110
+ return { kind: 'open', runId: record.runId, stage: record.stage, triggerRatio: record.triggerRatio };
92
111
  }
93
112
  /**
94
113
  * Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
@@ -220,6 +239,18 @@ export function summarizeLifecycleError(error) {
220
239
  * `afterRatio` to append the "observed compaction point" row that makes the
221
240
  * intent-vs-observed gap readable after a real session. The return value is
222
241
  * telemetry only — callers that ignore it are unaffected.
242
+ *
243
+ * `lifecycleWritten` (repair R6) makes the returned record honest about
244
+ * whether the RUN actually moved. Before it, a failed write still returned
245
+ * the full envelope, so a store that could not be written reported a settle
246
+ * on every probe, forever: the run stayed `armed`, the next probe found it
247
+ * open again, and each one added another "observed compaction point" row —
248
+ * an unbounded append driven by a write that never happened. A failed write
249
+ * is not an absent run (the facts are real and stay true, so the caller may
250
+ * still record the measurement), but it is also not a settled one. This is
251
+ * the same field `settleOpenLifecycleRunOnCompactEvent` carries, for the
252
+ * same reason, so the two settle paths no longer disagree about what a write
253
+ * failure means.
223
254
  */
224
255
  export function settleOpenLifecycleRun(input) {
225
256
  // A `conservative-fallback` probe means no signal was available at
@@ -229,12 +260,17 @@ export function settleOpenLifecycleRun(input) {
229
260
  return null;
230
261
  if (input.measuredRatio >= input.autoFireThreshold)
231
262
  return null;
232
- const prior = readOpenCompactLifecycle({
263
+ const openRead = readOpenCompactLifecycle({
233
264
  projectRoot: input.projectRoot,
234
265
  sessionId: input.sessionId
235
266
  });
236
- if (prior === null)
267
+ // Both `none` and `unresolvable` mean "nothing to settle" HERE, and the
268
+ // collapse is legitimate on this path rather than a fail-open: settling
269
+ // ADMITS nothing. No record can exist under an id that names no session
270
+ // directory, so there is nothing this probe could have been about.
271
+ if (openRead.kind !== 'found')
237
272
  return null;
273
+ const prior = openRead.record;
238
274
  // Only a run that was actually dispatched can be completed by a
239
275
  // post-compact measurement. Both `compacting` (the in-band trigger is
240
276
  // satisfied) and `armed` (a trigger was registered and could fire at
@@ -253,6 +289,8 @@ export function settleOpenLifecycleRun(input) {
253
289
  ...(withAfterRatio ? { afterRatio: input.measuredRatio } : {})
254
290
  };
255
291
  try {
292
+ if (input.failLifecycleWrite)
293
+ throw new Error('lifecycle store unavailable');
256
294
  writeCompactLifecycle({
257
295
  projectRoot: input.projectRoot,
258
296
  sessionId: input.sessionId,
@@ -260,7 +298,10 @@ export function settleOpenLifecycleRun(input) {
260
298
  });
261
299
  }
262
300
  catch {
263
- return;
301
+ // Best-effort telemetry, as everywhere in this file — but the failure is
302
+ // REPORTED to the caller as `lifecycleWritten: false` rather than folded
303
+ // into a record that reads as settled.
304
+ return false;
264
305
  }
265
306
  try {
266
307
  input.onLifecycleStage?.(stage, record);
@@ -268,12 +309,14 @@ export function settleOpenLifecycleRun(input) {
268
309
  catch {
269
310
  // Observer failures are not ours to propagate.
270
311
  }
312
+ return true;
271
313
  };
272
314
  // `verifying` = we hold a measurement and are checking it.
273
315
  emit('verifying', false);
274
- // `completed` = the measurement confirms the drop; publish it.
275
- emit('completed', true);
276
- return { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio: input.measuredRatio };
316
+ // `completed` = the measurement confirms the drop; publish it. This write
317
+ // is the one that closes the run, so it is the one that is reported.
318
+ const lifecycleWritten = emit('completed', true);
319
+ return { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio: input.measuredRatio, lifecycleWritten };
277
320
  }
278
321
  /**
279
322
  * rid `2026-09-13-compact-event-settle`: close out an open compact run because
@@ -332,12 +375,16 @@ export function settleOpenLifecycleRun(input) {
332
375
  * two apart ends up telling the user a falsehood (see `compact-event-settle.ts`).
333
376
  */
334
377
  export function settleOpenLifecycleRunOnCompactEvent(input) {
335
- const prior = readOpenCompactLifecycle({
378
+ const openRead = readOpenCompactLifecycle({
336
379
  projectRoot: input.projectRoot,
337
380
  sessionId: input.sessionId
338
381
  });
339
- if (prior === null)
382
+ // Same collapse as `settleOpenLifecycleRun` above, for the same reason:
383
+ // an unresolvable id names no session directory, so no open run exists
384
+ // for this event to be about, and `null` here admits nothing.
385
+ if (openRead.kind !== 'found')
340
386
  return null;
387
+ const prior = openRead.record;
341
388
  if (prior.stage !== 'compacting' && prior.stage !== 'armed')
342
389
  return null;
343
390
  const afterRatio = input.measuredRatio !== null && input.measuredRatio < prior.triggerRatio ? input.measuredRatio : null;
@@ -410,12 +457,14 @@ export function settleOpenLifecycleRunOnCompactEvent(input) {
410
457
  export function fillEventSettledMeasurement(input) {
411
458
  if (input.source === 'conservative-fallback')
412
459
  return null;
413
- const prior = readOpenCompactLifecycle({
460
+ const openRead = readOpenCompactLifecycle({
414
461
  projectRoot: input.projectRoot,
415
462
  sessionId: input.sessionId
416
463
  });
417
- if (prior === null)
464
+ // Same collapse as the two settle paths above — `null` here admits nothing.
465
+ if (openRead.kind !== 'found')
418
466
  return null;
467
+ const prior = openRead.record;
419
468
  // Exactly one shape is owed a number: the one the EVENT path leaves behind.
420
469
  // `settleOpenLifecycleRun` never writes it (it always carries `afterRatio`),
421
470
  // and a `failed` run never dispatched, so it has no row to pair with.
@@ -2,8 +2,8 @@
2
2
  * Slice 2026-07-28 — auto-compact mode table for 24h long-run awareness.
3
3
  *
4
4
  * The default `'standard'` mode preserves the v2.13.0 zero-pause contract
5
- * thresholds (preCompact=0.85, redLine=0.95). The `'partial'` mode fires
6
- * earlier for 24h long-run scenarios where the user has explicitly
5
+ * thresholds (autoFire=0.80, preCompact=0.85, redLine=0.95). The `'partial'`
6
+ * mode fires earlier for 24h long-run scenarios where the user has explicitly
7
7
  * opted into higher compaction cadence via `peaks session 24h-mode`.
8
8
  *
9
9
  * Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
@@ -26,5 +26,16 @@ export declare const AUTO_COMPACT_THRESHOLDS: Readonly<Record<AutoCompactMode, {
26
26
  }>>;
27
27
  export declare function thresholdFor(mode: AutoCompactMode, kind: 'autoFire' | 'preCompact' | 'redLine'): number;
28
28
  export declare function isValidMode(value: string): value is AutoCompactMode;
29
+ /**
30
+ * Human-readable form of a mode's threshold row.
31
+ *
32
+ * Derived from `AUTO_COMPACT_THRESHOLDS` rather than hand-written. Diagnosis
33
+ * 2026-09-15 (C1) found this string advertising `0.85/0.95` for `standard`
34
+ * while the table had auto-fired at `0.80` since slice
35
+ * 2026-07-29-context-evaluation-accuracy added the `autoFire` tier — the
36
+ * description named only the two thresholds that were left over from the
37
+ * v2.13.0 contract, and skipped the one that actually fires. Reading the
38
+ * numbers out of the table makes that drift unrepresentable.
39
+ */
29
40
  export declare function describeMode(mode: AutoCompactMode): string;
30
41
  export declare function isPartialModeEligible(contextPercent: number): boolean;
@@ -2,8 +2,8 @@
2
2
  * Slice 2026-07-28 — auto-compact mode table for 24h long-run awareness.
3
3
  *
4
4
  * The default `'standard'` mode preserves the v2.13.0 zero-pause contract
5
- * thresholds (preCompact=0.85, redLine=0.95). The `'partial'` mode fires
6
- * earlier for 24h long-run scenarios where the user has explicitly
5
+ * thresholds (autoFire=0.80, preCompact=0.85, redLine=0.95). The `'partial'`
6
+ * mode fires earlier for 24h long-run scenarios where the user has explicitly
7
7
  * opted into higher compaction cadence via `peaks session 24h-mode`.
8
8
  *
9
9
  * Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
@@ -28,10 +28,26 @@ export function thresholdFor(mode, kind) {
28
28
  export function isValidMode(value) {
29
29
  return value === 'standard' || value === 'partial';
30
30
  }
31
+ function pct(ratio) {
32
+ return `${(ratio * 100).toFixed(0)}%`;
33
+ }
34
+ /**
35
+ * Human-readable form of a mode's threshold row.
36
+ *
37
+ * Derived from `AUTO_COMPACT_THRESHOLDS` rather than hand-written. Diagnosis
38
+ * 2026-09-15 (C1) found this string advertising `0.85/0.95` for `standard`
39
+ * while the table had auto-fired at `0.80` since slice
40
+ * 2026-07-29-context-evaluation-accuracy added the `autoFire` tier — the
41
+ * description named only the two thresholds that were left over from the
42
+ * v2.13.0 contract, and skipped the one that actually fires. Reading the
43
+ * numbers out of the table makes that drift unrepresentable.
44
+ */
31
45
  export function describeMode(mode) {
46
+ const { autoFire, preCompact, redLine } = AUTO_COMPACT_THRESHOLDS[mode];
47
+ const bands = `auto-fire ${pct(autoFire)} / pre-compact ${pct(preCompact)} / red-line ${pct(redLine)}`;
32
48
  return mode === 'standard'
33
- ? 'standard (0.85/0.95 — v2.13.0 zero-pause contract)'
34
- : 'partial (0.70/0.85 — 24h long-run mode)';
49
+ ? `standard (${bands} — v2.13.0 zero-pause contract)`
50
+ : `partial (${bands} — 24h long-run mode)`;
35
51
  }
36
52
  export function isPartialModeEligible(contextPercent) {
37
53
  return contextPercent >= AUTO_COMPACT_THRESHOLDS.partial.preCompact;