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
@@ -1,13 +1,18 @@
1
1
  /**
2
- * v2.13.2 AC-4 — prd/handoff.md auto-regen on prd:handed-off.
2
+ * v2.13.2 AC-4 — prd/handoff-<rid>.md auto-regen on prd:handed-off.
3
3
  *
4
4
  * When `peaks request transition --role prd --state handed-off` succeeds
5
- * and `prd/handoff.md` is missing, this helper writes a sha256-locked
5
+ * and this slice's capsule is missing, this helper writes a sha256-locked
6
6
  * handoff (schemaVersion: 2) using the request artifact body as the
7
- * handoff body. If the handoff already exists, it's NOT overwritten —
7
+ * handoff body. If the capsule already exists, it's NOT overwritten —
8
8
  * the existing handoff is canonical (it may carry a richer body that
9
9
  * peaks-prd produced in an earlier session).
10
10
  *
11
+ * Slice `2026-09-14-prd-capsule-rid-scoping`: the path carries the rid, so
12
+ * "already exists" is now asked per SLICE. The pre-rid-scoping bare name is
13
+ * deliberately NOT consulted — writing there would recreate the
14
+ * one-slot-per-session collision for the next slice in the session.
15
+ *
11
16
  * Karpathy §3 (Surgical Changes): this file only owns the auto-regen
12
17
  * path. The other 11 transitions are untouched.
13
18
  */
@@ -15,7 +20,8 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
15
20
  import { dirname, join } from 'node:path';
16
21
  import { createHash } from 'node:crypto';
17
22
  import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
18
- import { sha256OfBody } from './handoff-service.js';
23
+ import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
24
+ import { handoffRelativePath, sha256OfBody } from './handoff-service.js';
19
25
  import { normalizePath } from '../../shared/path-utils.js';
20
26
  /**
21
27
  * Auto-regen the prd/handoff.md under `.peaks/_runtime/<sid>/prd/`.
@@ -25,7 +31,7 @@ export async function autoRegenPrdHandoff(opts) {
25
31
  if (opts.role !== 'prd') {
26
32
  return { status: 'failed', reason: 'role must be prd' };
27
33
  }
28
- const handoffPath = join(opts.projectRoot, '.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md');
34
+ const handoffPath = join(opts.projectRoot, handoffRelativePath(opts.sessionId, opts.requestId));
29
35
  if (existsSync(handoffPath)) {
30
36
  return { status: 'skipped-exists', path: handoffPath };
31
37
  }
@@ -41,28 +47,26 @@ export async function autoRegenPrdHandoff(opts) {
41
47
  const body = artifact.content;
42
48
  const sha256 = sha256OfBody(body);
43
49
  // v2.13.3 AC-4 — align with `AUDIT_REQUIRES_HANDOFF` prereq which
44
- // pins `mustContain: ['schemaVersion: 2', 'sha256:']`. The previous
45
- // field name `handoffHash` made peaks-loop write a handoff that the
46
- // own prereq resolver would reject with "missing section(s): sha256:".
47
- // Primary field is now `sha256`; `handoffHash` is kept as a literal
48
- // alias for back-compat with any consumer (UI / external scripts)
49
- // that still reads the old key.
50
- const frontmatter = [
51
- '---',
52
- `requestId: ${opts.requestId}`,
53
- `sessionId: ${opts.sessionId}`,
54
- 'schemaVersion: 2',
55
- `sha256: ${sha256}`,
56
- `handoffHash: ${sha256}`,
57
- `writtenAt: ${new Date().toISOString()}`,
58
- 'goals: []',
59
- 'acceptanceCriteria: []',
60
- 'preservedBehavior: []',
61
- `handoffPath: ${normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')}`,
62
- '---',
63
- ''
64
- ].join('\n');
65
- const content = `${frontmatter}${body}`;
50
+ // pins `mustContain: ['schemaVersion: 2', 'sha256:']`. Primary field is
51
+ // `sha256`; `handoffHash` is kept as a literal alias for the readers that
52
+ // still use the old key.
53
+ //
54
+ // Slice `2026-09-14-handoff-writer-gate-divergence`: this block used to be
55
+ // hand-rolled here while `handoff-service.serializeHandoff` rendered the
56
+ // SAME contract a second, incompatible way. Both now go through
57
+ // `serializeHandoffFrontmatter`, so the two producers cannot drift again.
58
+ const frontmatter = {
59
+ requestId: opts.requestId,
60
+ sessionId: opts.sessionId,
61
+ schemaVersion: '2',
62
+ handoffHash: sha256,
63
+ writtenAt: new Date().toISOString(),
64
+ goals: [],
65
+ acceptanceCriteria: [],
66
+ preservedBehavior: [],
67
+ handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')
68
+ };
69
+ const content = `${serializeHandoffFrontmatter(frontmatter)}${body}`;
66
70
  mkdirSync(dirname(handoffPath), { recursive: true });
67
71
  writeFileSync(handoffPath, content, 'utf8');
68
72
  const recomputed = createHash('sha256').update(body, 'utf8').digest('hex');
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The ONE canonical serialization of `prd/handoff.md` frontmatter.
3
+ *
4
+ * Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
5
+ * the handoff capsule is written by two producers and read by four
6
+ * consumers, and they did not agree on the bytes:
7
+ *
8
+ * | consumer / producer | requirement |
9
+ * |---|---|
10
+ * | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
11
+ * | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
12
+ * | `handoff-service.readHandoff` | YAML string `handoffHash` |
13
+ * | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
14
+ *
15
+ * `handoff-service.serializeHandoff` fed the frontmatter through
16
+ * `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
17
+ * `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
18
+ * hand-rolled a second serialization that happened to satisfy all four. Two
19
+ * producers, two facts about the same contract, one of them wrong: that is
20
+ * the defect, and copying the right shape into a third place would keep it.
21
+ *
22
+ * So both producers call THIS function. `sha256` is emitted only here, and
23
+ * it is emitted plain, because the two audit loaders anchor a regex on it.
24
+ *
25
+ * Scalar-quoting rule — one rule, one documented exception set:
26
+ * - every ordinary string is emitted through `yamlScalar` (a JSON
27
+ * double-quoted scalar, which is valid YAML 1.2 and round-trips
28
+ * backslashes correctly on Windows paths);
29
+ * - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
30
+ * they are the two scalars the gate and the loaders match textually and a
31
+ * quoted scalar is invisible to all three. Both values are structurally
32
+ * constrained (a version digit and 64 hex chars), so plain is unambiguous.
33
+ *
34
+ * `handoffHash` is quoted even though it is also a 64-hex value: the reader
35
+ * parses the block through YAML and requires a STRING, and a bare all-digit
36
+ * sha256 would parse as a YAML number and be refused by the shape check.
37
+ */
38
+ import type { HandoffFrontmatter } from './handoff-types.js';
39
+ /**
40
+ * Serialize `frontmatter` into the fenced block, terminated by the closing
41
+ * `---` and a trailing newline. Callers append the body verbatim, which keeps
42
+ * the sha256 of the body independent of how the frontmatter renders.
43
+ */
44
+ export declare function serializeHandoffFrontmatter(frontmatter: HandoffFrontmatter): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The ONE canonical serialization of `prd/handoff.md` frontmatter.
3
+ *
4
+ * Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
5
+ * the handoff capsule is written by two producers and read by four
6
+ * consumers, and they did not agree on the bytes:
7
+ *
8
+ * | consumer / producer | requirement |
9
+ * |---|---|
10
+ * | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
11
+ * | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
12
+ * | `handoff-service.readHandoff` | YAML string `handoffHash` |
13
+ * | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
14
+ *
15
+ * `handoff-service.serializeHandoff` fed the frontmatter through
16
+ * `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
17
+ * `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
18
+ * hand-rolled a second serialization that happened to satisfy all four. Two
19
+ * producers, two facts about the same contract, one of them wrong: that is
20
+ * the defect, and copying the right shape into a third place would keep it.
21
+ *
22
+ * So both producers call THIS function. `sha256` is emitted only here, and
23
+ * it is emitted plain, because the two audit loaders anchor a regex on it.
24
+ *
25
+ * Scalar-quoting rule — one rule, one documented exception set:
26
+ * - every ordinary string is emitted through `yamlScalar` (a JSON
27
+ * double-quoted scalar, which is valid YAML 1.2 and round-trips
28
+ * backslashes correctly on Windows paths);
29
+ * - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
30
+ * they are the two scalars the gate and the loaders match textually and a
31
+ * quoted scalar is invisible to all three. Both values are structurally
32
+ * constrained (a version digit and 64 hex chars), so plain is unambiguous.
33
+ *
34
+ * `handoffHash` is quoted even though it is also a 64-hex value: the reader
35
+ * parses the block through YAML and requires a STRING, and a bare all-digit
36
+ * sha256 would parse as a YAML number and be refused by the shape check.
37
+ */
38
+ /** Render a string as a YAML double-quoted scalar. JSON string escapes are a
39
+ * subset of YAML 1.2's double-quoted escapes, so this is valid YAML and
40
+ * handles `\` (Windows paths), quotes, colons and newlines in one step. */
41
+ function yamlScalar(value) {
42
+ return JSON.stringify(value);
43
+ }
44
+ /** Render `key: []` or a YAML block sequence of quoted scalars. */
45
+ function blockSequence(key, values) {
46
+ if (values.length === 0)
47
+ return [`${key}: []`];
48
+ return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
49
+ }
50
+ /**
51
+ * Serialize `frontmatter` into the fenced block, terminated by the closing
52
+ * `---` and a trailing newline. Callers append the body verbatim, which keeps
53
+ * the sha256 of the body independent of how the frontmatter renders.
54
+ */
55
+ export function serializeHandoffFrontmatter(frontmatter) {
56
+ const lines = [
57
+ '---',
58
+ `requestId: ${yamlScalar(frontmatter.requestId)}`,
59
+ `sessionId: ${yamlScalar(frontmatter.sessionId)}`,
60
+ // PLAIN by construction (`HandoffSchemaVersion` is the literal '2').
61
+ `schemaVersion: ${frontmatter.schemaVersion}`,
62
+ // PLAIN: this is the field both audit loaders read, via an anchored
63
+ // regex that a quoted scalar would not match.
64
+ `sha256: ${frontmatter.handoffHash}`,
65
+ // Quoted: `readHandoff` requires a YAML string here.
66
+ `handoffHash: ${yamlScalar(frontmatter.handoffHash)}`,
67
+ `writtenAt: ${yamlScalar(frontmatter.writtenAt)}`,
68
+ ...blockSequence('goals', frontmatter.goals),
69
+ ...blockSequence('acceptanceCriteria', frontmatter.acceptanceCriteria),
70
+ ...blockSequence('preservedBehavior', frontmatter.preservedBehavior),
71
+ `handoffPath: ${yamlScalar(frontmatter.handoffPath)}`,
72
+ '---',
73
+ ];
74
+ return `${lines.join('\n')}\n`;
75
+ }
@@ -3,7 +3,9 @@
3
3
  * `v2-11-rm-rd-techdoc-immutable-handoff`).
4
4
  *
5
5
  * Owns the immutable handoff at
6
- * `.peaks/_runtime/<sessionId>/prd/handoff.md`:
6
+ * `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
7
+ * the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
8
+ * readable through `resolveHandoffPath`):
7
9
  *
8
10
  * - `initHandoff` — pure; computes sha256 of the body and returns
9
11
  * a Handoff whose frontmatter `handoffHash` matches.
@@ -23,6 +25,43 @@
23
25
  import type { Handoff, HandoffProbe } from './handoff-types.js';
24
26
  /** Compute the lowercase hex sha256 of a UTF-8 string. */
25
27
  export declare function sha256OfBody(body: string): string;
28
+ /**
29
+ * The canonical capsule path for ONE SLICE, relative to the project root.
30
+ *
31
+ * Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
32
+ * per SESSION (`prd/handoff.md`), so the second slice's handoff silently
33
+ * overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
34
+ * because it never checked WHOSE rid the file named. Measured on
35
+ * `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
36
+ * capsule left by a different line of work.
37
+ *
38
+ * This is the WRITE target, so it always carries the rid — a consumer-side
39
+ * fallback here would leave a rid-scoped requirement with a bare-name writer,
40
+ * which is the defect shape this slice exists to remove. Readers that must
41
+ * tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
42
+ */
43
+ export declare function handoffRelativePath(sessionId: string, requestId: string): string;
44
+ /**
45
+ * The capsule a CONSUMER should read for (session, requestId): the rid-scoped
46
+ * path when it is on disk, else the pre-rid-scoping bare name. Returns null
47
+ * when neither exists.
48
+ *
49
+ * Three sessions on disk still hold only the bare file
50
+ * (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
51
+ * `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
52
+ * the gate and for every reader below.
53
+ *
54
+ * `requestId` is optional because the detect-only audit surface reaches its
55
+ * service without one. Such a caller can name only the bare path: a session
56
+ * holding nothing but rid-scoped capsules reports missing rather than picking
57
+ * among its siblings' capsules, which would re-open the cross-slice mix-up
58
+ * this scoping exists to close (fail closed, not "some capsule is there").
59
+ */
60
+ export declare function resolveHandoffPath(opts: {
61
+ projectRoot: string;
62
+ sessionId: string;
63
+ requestId?: string;
64
+ }): string | null;
26
65
  /** Pure: produce a Handoff with the frontmatter populated. Hash is
27
66
  * computed here; callers MUST NOT pre-populate `handoffHash`. */
28
67
  export declare function initHandoff(opts: {
@@ -33,7 +72,7 @@ export declare function initHandoff(opts: {
33
72
  goals: readonly string[];
34
73
  acceptanceCriteria: readonly string[];
35
74
  preservedBehavior: readonly string[];
36
- /** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff.md`. */
75
+ /** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff-<rid>.md`. */
37
76
  handoffPath?: string;
38
77
  }): Handoff;
39
78
  /** Write a Handoff to disk. `projectRoot` is the absolute project
@@ -3,7 +3,9 @@
3
3
  * `v2-11-rm-rd-techdoc-immutable-handoff`).
4
4
  *
5
5
  * Owns the immutable handoff at
6
- * `.peaks/_runtime/<sessionId>/prd/handoff.md`:
6
+ * `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
7
+ * the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
8
+ * readable through `resolveHandoffPath`):
7
9
  *
8
10
  * - `initHandoff` — pure; computes sha256 of the body and returns
9
11
  * a Handoff whose frontmatter `handoffHash` matches.
@@ -21,20 +23,68 @@
21
23
  * markdown source — no normalization, no trailing-newline padding.
22
24
  */
23
25
  import { createHash } from 'node:crypto';
26
+ import { existsSync } from 'node:fs';
24
27
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
25
28
  import { dirname, join } from 'node:path';
26
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
29
+ import { parse as parseYaml } from 'yaml';
30
+ import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
27
31
  /** Required schema version for new handoffs. */
28
32
  const HANDOFF_SCHEMA_VERSION = '2';
29
33
  /** Compute the lowercase hex sha256 of a UTF-8 string. */
30
34
  export function sha256OfBody(body) {
31
35
  return createHash('sha256').update(body, 'utf8').digest('hex');
32
36
  }
37
+ /**
38
+ * The canonical capsule path for ONE SLICE, relative to the project root.
39
+ *
40
+ * Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
41
+ * per SESSION (`prd/handoff.md`), so the second slice's handoff silently
42
+ * overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
43
+ * because it never checked WHOSE rid the file named. Measured on
44
+ * `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
45
+ * capsule left by a different line of work.
46
+ *
47
+ * This is the WRITE target, so it always carries the rid — a consumer-side
48
+ * fallback here would leave a rid-scoped requirement with a bare-name writer,
49
+ * which is the defect shape this slice exists to remove. Readers that must
50
+ * tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
51
+ */
52
+ export function handoffRelativePath(sessionId, requestId) {
53
+ return join('.peaks', '_runtime', sessionId, 'prd', `handoff-${requestId}.md`);
54
+ }
55
+ /**
56
+ * The capsule a CONSUMER should read for (session, requestId): the rid-scoped
57
+ * path when it is on disk, else the pre-rid-scoping bare name. Returns null
58
+ * when neither exists.
59
+ *
60
+ * Three sessions on disk still hold only the bare file
61
+ * (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
62
+ * `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
63
+ * the gate and for every reader below.
64
+ *
65
+ * `requestId` is optional because the detect-only audit surface reaches its
66
+ * service without one. Such a caller can name only the bare path: a session
67
+ * holding nothing but rid-scoped capsules reports missing rather than picking
68
+ * among its siblings' capsules, which would re-open the cross-slice mix-up
69
+ * this scoping exists to close (fail closed, not "some capsule is there").
70
+ */
71
+ export function resolveHandoffPath(opts) {
72
+ const candidates = [
73
+ ...(opts.requestId !== undefined ? [handoffRelativePath(opts.sessionId, opts.requestId)] : []),
74
+ join('.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md')
75
+ ];
76
+ for (const relative of candidates) {
77
+ const absolute = join(opts.projectRoot, relative);
78
+ if (existsSync(absolute))
79
+ return absolute;
80
+ }
81
+ return null;
82
+ }
33
83
  /** Pure: produce a Handoff with the frontmatter populated. Hash is
34
84
  * computed here; callers MUST NOT pre-populate `handoffHash`. */
35
85
  export function initHandoff(opts) {
36
86
  const handoffPath = opts.handoffPath ??
37
- join('.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md');
87
+ handoffRelativePath(opts.sessionId, opts.requestId);
38
88
  const handoffHash = sha256OfBody(opts.body);
39
89
  const frontmatter = {
40
90
  requestId: opts.requestId,
@@ -146,8 +196,11 @@ function parseHandoffContent(content) {
146
196
  };
147
197
  }
148
198
  function serializeHandoff(handoff) {
149
- const yamlStr = stringifyYaml(handoff.frontmatter).trimEnd();
150
- return `---\n${yamlStr}\n---\n${handoff.body}`;
199
+ // The one canonical frontmatter rendering, shared with
200
+ // `handoff-auto-regen.ts`. `yaml.stringify` used to render this block and
201
+ // emitted `schemaVersion: "2"` + a bare `handoffHash:`, which the
202
+ // `AUDIT_REQUIRES_HANDOFF` gate and both audit loaders all reject.
203
+ return `${serializeHandoffFrontmatter(handoff.frontmatter)}${handoff.body}`;
151
204
  }
152
205
  /**
153
206
  * N1: accept BOTH shapes of `schemaVersion` — the string `'2'` and the bare
@@ -161,13 +214,33 @@ function serializeHandoff(handoff) {
161
214
  * readable handoff was satisfied by a handoff the parser would not read, and
162
215
  * `peaks prd handoff verify` exited 1 on a healthy file.
163
216
  *
164
- * Quoting the writer instead is NOT a fix: it would delete the very substring
165
- * the prereq pins, turning a broken read into a broken gate. The tolerant read
166
- * is the only change that satisfies both consumers.
217
+ * Slice `2026-09-14-handoff-writer-gate-divergence` then fixed the other half:
218
+ * the writer no longer emits the quoted form at all (see
219
+ * `handoff-frontmatter.ts`). This tolerance stays because handoffs already on
220
+ * disk were written by the old writer and by hand; the writer fix must not
221
+ * retroactively make them unreadable.
167
222
  */
168
223
  function isSchemaVersion2(value) {
169
224
  return value === HANDOFF_SCHEMA_VERSION || value === 2;
170
225
  }
226
+ /**
227
+ * The READER's shape check — deliberately looser than `verifyHandoff`:
228
+ * `handoffHash` must be a string, and nothing about its VALUE is validated here.
229
+ *
230
+ * The accepted-but-unverifiable shape, stated rather than left to be
231
+ * discovered: a capsule carrying `handoffHash: sha256:<hex>` — the form the
232
+ * pre-fix writer and this session's hand-corrected capsules use — is READ by
233
+ * `readHandoff` and can NEVER pass `verifyHandoff`. The verifier compares the
234
+ * value to `sha256OfBody(body)` byte for byte (`:127-135`) and no prefix
235
+ * normalization exists anywhere in this module, so the leading `sha256:`
236
+ * guarantees `hash-mismatch`. ONLY the bare-hex form verifies, which is what
237
+ * every writer now emits (`handoff-frontmatter.ts`).
238
+ *
239
+ * So `readHandoff` succeeding is NOT evidence that a capsule is verifiable —
240
+ * the tolerance above exists so old capsules stay READABLE, not so they become
241
+ * acceptable. Request §三 bullet 3 asked for exactly this confirmation
242
+ * (rid `2026-09-14-handoff-writer-gate-divergence`).
243
+ */
171
244
  function isHandoffFrontmatter(value) {
172
245
  if (!value || typeof value !== 'object')
173
246
  return false;
@@ -14,8 +14,9 @@
14
14
  * (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
15
15
  *
16
16
  * Path convention: the file lands at
17
- * `.peaks/_runtime/<sessionId>/prd/handoff.md` (gitignored session
18
- * artifact; the binding to `<sessionId>` lives in
17
+ * `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
18
+ * `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
19
+ * stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
19
20
  * `.peaks/_runtime/current-change`). NEVER write under
20
21
  * `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
21
22
  */
@@ -14,8 +14,9 @@
14
14
  * (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
15
15
  *
16
16
  * Path convention: the file lands at
17
- * `.peaks/_runtime/<sessionId>/prd/handoff.md` (gitignored session
18
- * artifact; the binding to `<sessionId>` lives in
17
+ * `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
18
+ * `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
19
+ * stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
19
20
  * `.peaks/_runtime/current-change`). NEVER write under
20
21
  * `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
21
22
  */
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
18
18
  import { dirname, join, resolve } from 'node:path';
19
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
19
20
  /** 6-item business checklist (12 Gaps QA perspective). */
20
21
  export const QA_BUSINESS_ITEMS = [
21
22
  { id: 'business-flow', question: '这个功能"用起来"对吗?(业务流程顺不顺,操作路径是否反人类,跟现有系统交互有没有断层)' },
@@ -36,6 +37,14 @@ export function buildEmptyQaReview(requestId, sessionId, now = new Date()) {
36
37
  };
37
38
  }
38
39
  export function getQaReviewDir(projectRoot, sessionId) {
40
+ // Sid axis. Every `peaks qa-business-review|score|accept|reject` subcommand
41
+ // reaches the runtime tree through this one constructor, so one guard here
42
+ // covers the whole family — measured: `--session-id ../../../../…/PWNED`
43
+ // wrote `qa-business-reviews/<rid>.json` outside every project root under an
44
+ // `ok: true` envelope (RD sweep case A17).
45
+ if (isUnsafePathInput(sessionId)) {
46
+ throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
47
+ }
39
48
  return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'qa-business-reviews');
40
49
  }
41
50
  export function getQaReviewPath(projectRoot, sessionId, requestId) {
@@ -34,7 +34,8 @@ export interface PrecheckOptions {
34
34
  export interface PrecheckEnvelope {
35
35
  readonly ok: boolean;
36
36
  readonly overall: LayerStatus;
37
- readonly rootVersion: string;
37
+ /** `null` when the root `package.json` is missing or unreadable — see `rootVsShared`. */
38
+ readonly rootVersion: string | null;
38
39
  readonly strict: boolean;
39
40
  readonly snapshotAt: string;
40
41
  readonly layers: {
@@ -37,12 +37,45 @@ function upgrade(result, strict) {
37
37
  }
38
38
  return result;
39
39
  }
40
+ /** Read + parse a JSON manifest, mapping both failure modes onto a blocker description. */
41
+ function readPackageJson(path, label) {
42
+ let raw;
43
+ try {
44
+ raw = readFileSync(path, 'utf8');
45
+ }
46
+ catch {
47
+ return {
48
+ ok: false,
49
+ message: `${label} is missing or unreadable at ${path}`,
50
+ remediation: `run \`peaks release precheck\` from a project root that has a readable ${label}`
51
+ };
52
+ }
53
+ try {
54
+ return { ok: true, value: JSON.parse(raw) };
55
+ }
56
+ catch {
57
+ return {
58
+ ok: false,
59
+ message: `${label} at ${path} is not valid JSON`,
60
+ remediation: `fix the ${label} syntax; \`peaks release precheck\` cannot gate a version it cannot parse`
61
+ };
62
+ }
63
+ }
40
64
  function readRootVersion(projectRoot) {
41
- const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
65
+ const path = join(projectRoot, 'package.json');
66
+ const read = readPackageJson(path, 'root package.json');
67
+ if (!read.ok) {
68
+ return read;
69
+ }
70
+ const pkg = read.value;
42
71
  if (typeof pkg.version !== 'string' || !SEMVER_RE.test(pkg.version)) {
43
- throw new Error(`invalid root package.json#version: ${JSON.stringify(pkg.version)}`);
72
+ return {
73
+ ok: false,
74
+ message: `invalid root package.json#version: ${JSON.stringify(pkg.version)}`,
75
+ remediation: 'set root package.json#version to a clean semver (e.g. 4.0.0) before running `peaks release precheck`'
76
+ };
44
77
  }
45
- return pkg.version;
78
+ return { ok: true, version: pkg.version };
46
79
  }
47
80
  function rollup(layers, strict) {
48
81
  const statuses = Object.values(layers).map((l) => l.status);
@@ -58,7 +91,16 @@ function rollup(layers, strict) {
58
91
  // Layer A — rootVsShared (mirrors publish.yml §(A) on-disk gate only)
59
92
  // ---------------------------------------------------------------------------
60
93
  export function runRootVsShared(opts) {
61
- const rootVersion = readRootVersion(opts.projectRoot);
94
+ const rootRead = readRootVersion(opts.projectRoot);
95
+ if (!rootRead.ok) {
96
+ return {
97
+ status: 'blocker',
98
+ message: rootRead.message,
99
+ remediation: rootRead.remediation,
100
+ observed: { rootVersion: null, sharedVersion: null }
101
+ };
102
+ }
103
+ const rootVersion = rootRead.version;
62
104
  const sharedDist = join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'dist', 'version.js');
63
105
  let sharedVersion = null;
64
106
  let distExists = false;
@@ -112,7 +154,18 @@ export function runRootVsShared(opts) {
112
154
  // Layer B — tagCollision (CLI-only; not in publish.yml today)
113
155
  // ---------------------------------------------------------------------------
114
156
  export function runTagCollision(opts) {
115
- const rootVersion = readRootVersion(opts.projectRoot);
157
+ const rootRead = readRootVersion(opts.projectRoot);
158
+ if (!rootRead.ok) {
159
+ // Fail closed: without a version there is no tag name to check, so this
160
+ // layer cannot claim "safe to publish".
161
+ return {
162
+ status: 'blocker',
163
+ message: rootRead.message,
164
+ remediation: rootRead.remediation,
165
+ observed: { tagName: null }
166
+ };
167
+ }
168
+ const rootVersion = rootRead.version;
116
169
  const tagName = `v${rootVersion}`;
117
170
  // 2026-09-10: no shell. `git` is `git.exe` on Windows, so the wrapper bought
118
171
  // nothing — and it actively broke this layer, because `projectRoot` is an
@@ -125,7 +178,8 @@ export function runTagCollision(opts) {
125
178
  // blocker). It also emitted DEP0190 on every run, on every layer.
126
179
  const res = spawnSync('git', ['-C', opts.projectRoot, 'tag', '--list', tagName], {
127
180
  encoding: 'utf8',
128
- timeout: 5_000
181
+ timeout: 5_000,
182
+ windowsHide: true
129
183
  });
130
184
  if (res.error !== null && res.error !== undefined) {
131
185
  return {
@@ -198,8 +252,16 @@ export function runChangesetStaged(opts) {
198
252
  // Layer D — workspaceLockstep (CLI-only; not in publish.yml today)
199
253
  // ---------------------------------------------------------------------------
200
254
  export function runWorkspaceLockstep(opts) {
201
- const rootPkgRaw = readFileSync(join(opts.projectRoot, 'package.json'), 'utf8');
202
- const rootPkg = JSON.parse(rootPkgRaw);
255
+ const rootRead = readPackageJson(join(opts.projectRoot, 'package.json'), 'root package.json');
256
+ if (!rootRead.ok) {
257
+ return {
258
+ status: 'blocker',
259
+ message: rootRead.message,
260
+ remediation: rootRead.remediation,
261
+ observed: { sharedDep: null }
262
+ };
263
+ }
264
+ const rootPkg = rootRead.value;
203
265
  const allDeps = {
204
266
  ...(rootPkg.dependencies ?? {}),
205
267
  ...(rootPkg.devDependencies ?? {})
@@ -222,9 +284,16 @@ export function runWorkspaceLockstep(opts) {
222
284
  observed: { sharedDep }
223
285
  };
224
286
  }
225
- const sharedPkgRaw = readFileSync(join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'package.json'), 'utf8');
226
- const sharedPkg = JSON.parse(sharedPkgRaw);
227
- const sharedVersion = sharedPkg.version ?? '';
287
+ const sharedRead = readPackageJson(join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'package.json'), 'packages/peaks-loop-shared/package.json');
288
+ if (!sharedRead.ok) {
289
+ return {
290
+ status: 'blocker',
291
+ message: sharedRead.message,
292
+ remediation: sharedRead.remediation,
293
+ observed: { sharedDep, sharedVersion: null }
294
+ };
295
+ }
296
+ const sharedVersion = sharedRead.value.version ?? '';
228
297
  if (!SEMVER_RE.test(sharedVersion)) {
229
298
  return {
230
299
  status: 'blocker',
@@ -244,7 +313,8 @@ export function runWorkspaceLockstep(opts) {
244
313
  // runAllLayers — orchestrator
245
314
  // ---------------------------------------------------------------------------
246
315
  export function runAllLayers(opts) {
247
- const rootVersion = readRootVersion(opts.projectRoot);
316
+ const rootRead = readRootVersion(opts.projectRoot);
317
+ const rootVersion = rootRead.ok ? rootRead.version : null;
248
318
  const strict = opts.strict === true;
249
319
  const layers = {
250
320
  rootVsShared: upgrade(runRootVsShared(opts), strict),
@@ -20,11 +20,36 @@
20
20
  *
21
21
  * Vendor-specific verb strings (`claude --compact`, `codex --compact`,
22
22
  * `copilot compact`) MUST live ONLY in adapter implementations under
23
- * `src/services/runtime/vendors/<vendor>.ts`. The runtime service that
23
+ * `src/services/runtime/vendors/<vendor>.ts` and
24
+ * `src/services/ide/adapters/<vendor>-adapter.ts`. The runtime service that
24
25
  * orchestrates these adapters (runtime-service.ts) and the CLI commands
25
- * (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs
26
- * — verified by AC-1: `rg -n "claude --compact|codex --compact|copilot
27
- * compact" src/services/code/` must return 0 matches.
26
+ * (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs.
27
+ *
28
+ * Slice 2026-09-12-auto-compact-vendor-neutrality widened AC-1's scope from
29
+ * `src/services/code/` to ALL of `src/`, and had to change how it is
30
+ * enforced. Both changes came from measurement:
31
+ *
32
+ * - The old scope excluded the files that carried the defect AC-1
33
+ * describes — the compact dispatcher (`src/services/context/`) and the
34
+ * hook installer (`src/services/hooks/`) — so it returned 0 by
35
+ * construction. A grep over a directory that does not contain the code
36
+ * being checked is not evidence of anything.
37
+ * - Widened as a raw grep over `src/`, the check returns 13, not 0. Twelve
38
+ * hits are the phrase quoted in COMMENTS and doc strings (this block
39
+ * among them); the thirteenth is `compactCommand: 'claude --compact'`
40
+ * in `src/services/ide/adapters/claude-code-adapter.ts` — the one place
41
+ * the string is supposed to be. A text grep measures prose, not code.
42
+ *
43
+ * AC-1 is therefore enforced by the AST-based check in
44
+ * `tests/unit/runtime/vendor-neutral-identity-guard.test.ts`: no vendor
45
+ * verb in any STRING LITERAL of `src/` outside the adapter implementations.
46
+ * Parsed, the count is 1 and the single hit is that adapter — which is the
47
+ * property AC-1 was always about.
48
+ *
49
+ * AC-1 covers VERB STRINGS only. The compact path's defects were IDENTITY
50
+ * COMPARISONS (`ideId !== 'claude-code'`), a second shape the same guard
51
+ * file covers and AC-1 never could. Read that guard's header for what
52
+ * neither check catches before treating vendor neutrality as proven.
28
53
  *
29
54
  * See `.peaks/_runtime/2026-07-08-session-17918f/prd/002-adapter-runtime-and-polyrepo.md`
30
55
  * for the source PRD.