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
@@ -8,17 +8,13 @@
8
8
  *
9
9
  * File budget: ≤ 400 lines (rid-006 split).
10
10
  */
11
- import { existsSync } from 'node:fs';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { getPrerequisitesFor, prerequisiteBodyViolations } from '../artifacts/artifact-prerequisites.js';
13
14
  import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
14
- import { resolveSecurityFindingsPath, resolvePerformanceFindingsPath } from './artifact-paths.js';
15
+ import { readArtifactState } from '../artifacts/request-artifact-state-helpers.js';
15
16
  export function extractState(markdown) {
16
- for (const rawLine of markdown.split(/\r?\n/)) {
17
- const match = /^-\s*state:\s*(.+?)\s*$/.exec(rawLine.trim());
18
- if (match?.[1])
19
- return match[1];
20
- }
21
- return 'unknown';
17
+ return readArtifactState(markdown) ?? 'unknown';
22
18
  }
23
19
  /**
24
20
  * As of slice 2026-06-05-change-id-as-unit-of-work, the file's durable
@@ -44,12 +40,91 @@ export async function findRequestFile(projectRoot, role, rid) {
44
40
  }
45
41
  return { path: artifact.path, content: artifact.content, sessionId };
46
42
  }
43
+ /**
44
+ * Where the CURRENT contract puts one gate's evidence.
45
+ *
46
+ * The contract is the `artifact-prerequisites.ts` table — the table
47
+ * `peaks request transition` enforces and the one the peaks-qa prose
48
+ * contract mirrors (`skills/bee/peaks-qa/references/qa-transition-gates.md`).
49
+ * `probeName` is the location this checker probes today. The contract's
50
+ * `relativePath` is returned first, then its accepted historical locations in
51
+ * the table's own declared order — `legacyRelativePath`, then
52
+ * `legacyRelativePaths` (the same composition `artifact-prerequisites.ts`' own
53
+ * resolver performs, so a hit here and a hit there land on the same file).
54
+ *
55
+ * Returns `null` when the contract carries no prerequisite for that name at
56
+ * all — the artifact was retired and no gate may fail on it.
57
+ *
58
+ * This lookup exists because hard-coding the paths here is what let
59
+ * `verify-pipeline` fall a minor release behind the gate: it kept demanding
60
+ * `rd/security-review.md` as mandatory after the table had demoted that file
61
+ * to a `legacyRelativePath` fallback of `audit/security.md`, and it kept
62
+ * demanding `qa/security-findings-<rid>.md` / `qa/performance-findings-<rid>.md`
63
+ * after the v2.11.0 D1/D4 trim dropped them from `qa:verdict-issued`
64
+ * altogether (rid 2026-09-14-verify-pipeline-contract-drift).
65
+ */
66
+ /**
67
+ * The prerequisite the contract carries for `probeName`, or null when it
68
+ * carries none. `contractEvidencePaths` is this object's path projection; the
69
+ * BODY contract (`mustContain` / `mustContainAny` / `headingMustContain`) rides
70
+ * on the same object, which is why `contractBodyViolations` below consults it
71
+ * instead of probing for the file's existence alone.
72
+ */
73
+ export function contractPrerequisite(role, state, requestType, probeName) {
74
+ for (const prereq of getPrerequisitesFor(role, state, requestType)) {
75
+ const legacyPaths = [
76
+ ...(prereq.legacyRelativePath !== undefined ? [prereq.legacyRelativePath] : []),
77
+ ...(prereq.legacyRelativePaths ?? [])
78
+ ];
79
+ if (prereq.relativePath !== probeName && !legacyPaths.includes(probeName))
80
+ continue;
81
+ return prereq;
82
+ }
83
+ return null;
84
+ }
85
+ export function contractEvidencePaths(role, state, requestType, probeName) {
86
+ const prereq = contractPrerequisite(role, state, requestType, probeName);
87
+ if (prereq === null)
88
+ return null;
89
+ const legacyPaths = [
90
+ ...(prereq.legacyRelativePath !== undefined ? [prereq.legacyRelativePath] : []),
91
+ ...(prereq.legacyRelativePaths ?? [])
92
+ ];
93
+ return [prereq.relativePath, ...legacyPaths.filter((legacy) => legacy !== prereq.relativePath)];
94
+ }
95
+ /**
96
+ * The contract's BODY checks for the prerequisite named by `probeName`, applied
97
+ * to the file that resolved. Empty when the name matches nothing in the table or
98
+ * the table pins no body markers for it.
99
+ *
100
+ * `existsSync` alone made this checker laxer than the contract it claims to
101
+ * derive from: it reported `prd-handoff passed = true` for a handoff carrying
102
+ * `schemaVersion: 1` and no `sha256:` line — the exact file
103
+ * `AUDIT_REQUIRES_HANDOFF` refuses at `rd:qa-handoff`. Deriving the *path* from
104
+ * the table while dropping the table's *body* contract is the drift this module
105
+ * exists to remove, so the check goes through the table's own implementation
106
+ * (`prerequisiteBodyViolations`) rather than a second copy of the markers.
107
+ */
108
+ function contractBodyViolations(role, state, requestType, probeName, absolutePath) {
109
+ const prerequisite = contractPrerequisite(role, state, requestType, probeName);
110
+ if (prerequisite === null)
111
+ return [];
112
+ return prerequisiteBodyViolations(prerequisite, readFileSync(absolutePath, 'utf8'));
113
+ }
47
114
  export function rdGatesForType(requestType) {
48
115
  const gates = [
49
116
  { name: 'rd-request-exists', description: 'RD request artifact created', passed: false, detail: '' }
50
117
  ];
51
- if (requestType === 'feature' || requestType === 'refactor') {
52
- gates.push({ name: 'tech-doc', description: 'Technical design doc', passed: false, detail: '' });
118
+ // This slot used to pin a `tech-doc` gate to `rd/tech-doc.md`, an artifact
119
+ // v2.11.0 Group A retired and the table no longer carries — so the gate
120
+ // failed trees `peaks request transition` accepts (QA repair cycle 1). The
121
+ // design / scope record the contract DOES name at `rd:qa-handoff` is
122
+ // `prd/handoff.md`, so that is the gate: table-derived, like the others.
123
+ // (`rd/tech-doc.md` still has two non-gate readers — final-review's evidence
124
+ // budget and the resume detector. Neither is served by failing a compliant
125
+ // slice.)
126
+ if (contractEvidencePaths('rd', 'qa-handoff', requestType, 'prd/handoff.md') !== null) {
127
+ gates.push({ name: 'prd-handoff', description: 'PRD handoff capsule (approved scope + non-goals)', passed: false, detail: '' });
53
128
  }
54
129
  if (requestType === 'bugfix') {
55
130
  gates.push({ name: 'bug-analysis', description: 'Bug root-cause analysis', passed: false, detail: '' });
@@ -60,6 +135,17 @@ export function rdGatesForType(requestType) {
60
135
  if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix' || requestType === 'config') {
61
136
  gates.push({ name: 'security-review', description: 'Security review evidence', passed: false, detail: '' });
62
137
  }
138
+ // The perf evidence the contract requires at `rd:qa-handoff` is `AUDIT_PERF`
139
+ // — `audit/perf-<rid>.md`, with `audit/perf.md` and `rd/perf-baseline.md` as
140
+ // its two declared legacy tiers (rid-scoped since slice
141
+ // `2026-09-14-audit-artifact-rid-scoping`). This gate used to live on the QA
142
+ // side as `performance-findings`, checking `qa/performance-findings-<rid>.md`
143
+ // — a path the v2.11.0 D1/D4 trim dropped. Relocating the gate (rather than
144
+ // deleting it) is what keeps AC3's control meaningful: perf evidence that is
145
+ // genuinely absent must still fail.
146
+ if (contractEvidencePaths('rd', 'qa-handoff', requestType, 'rd/perf-baseline.md') !== null) {
147
+ gates.push({ name: 'perf-baseline', description: 'Performance audit evidence', passed: false, detail: '' });
148
+ }
63
149
  return gates;
64
150
  }
65
151
  export function qaGatesForType(requestType) {
@@ -70,124 +156,157 @@ export function qaGatesForType(requestType) {
70
156
  gates.push({ name: 'test-cases', description: 'QA test cases', passed: false, detail: '' });
71
157
  gates.push({ name: 'test-report', description: 'QA test report with execution results', passed: false, detail: '' });
72
158
  }
73
- if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix' || requestType === 'config') {
74
- gates.push({ name: 'security-findings', description: 'QA security findings', passed: false, detail: '' });
75
- }
76
- if (requestType === 'feature' || requestType === 'refactor') {
77
- gates.push({ name: 'performance-findings', description: 'QA performance findings', passed: false, detail: '' });
78
- }
159
+ // The `security-findings` / `performance-findings` gates were removed here
160
+ // (rid 2026-09-14-verify-pipeline-contract-drift). peaks-qa does not own
161
+ // security review or performance review — `peaks-qa/SKILL.md` says so, and
162
+ // the v2.11.0 D1/D4 trim dropped both from every `qa:verdict-issued` table.
163
+ // The evidence they used to demand is now checked on the RD side, at the
164
+ // paths the contract actually names (`audit/security-<rid>.md`,
165
+ // `audit/perf-<rid>.md`; rid-scoped since slice
166
+ // `2026-09-14-audit-artifact-rid-scoping`, with `audit/security.md` /
167
+ // `audit/perf.md` and `rd/security-review.md` / `rd/perf-baseline.md` as the
168
+ // declared legacy tiers behind them).
79
169
  return gates;
80
170
  }
81
171
  export const RD_QA_HANDOFF_STATES = new Set(['qa-handoff', 'handed-off', 'implemented']);
82
172
  export const QA_COMPLETE_STATES = new Set(['verdict-issued']);
83
- /** Resolve RD evidence files (tech-doc / bug-analysis / code-review / security-review)
173
+ /** Resolve RD evidence files (prd-handoff / bug-analysis / code-review / security-review / perf-baseline)
84
174
  * by probing canonical + legacy paths for each gate. Mutates the gate.passed /
85
175
  * gate.detail fields in place and pushes violations / nextActions. Returns the
86
176
  * updated tracker. Verbatim-move from `pipeline-verify-service.ts`. */
87
- export function resolveRdEvidencePaths(gates, rdEvidenceDir, projectRoot, violations, nextActions, tracker) {
88
- const RD_EVIDENCE_FILE = {
89
- 'tech-doc': 'tech-doc.md',
90
- 'bug-analysis': 'bug-analysis.md',
91
- 'code-review': 'code-review.md',
92
- 'security-review': 'security-review.md'
177
+ export function resolveRdEvidencePaths(gates, rdEvidenceDir, projectRoot, rid, requestType, violations, nextActions, tracker) {
178
+ // The path each gate is about. The contract is consulted first — see
179
+ // `contractEvidencePaths`; `rdGatesForType` only builds a gate when the
180
+ // contract carries its prerequisite, so this is the name the gate stands
181
+ // for, not a second path table.
182
+ const RD_EVIDENCE_PROBE = {
183
+ 'prd-handoff': 'prd/handoff.md',
184
+ 'bug-analysis': 'rd/bug-analysis.md',
185
+ 'code-review': 'rd/code-review.md',
186
+ 'security-review': 'rd/security-review.md',
187
+ 'perf-baseline': 'rd/perf-baseline.md'
93
188
  };
94
189
  let anyEvidenceResolved = tracker.anyEvidenceResolved;
95
190
  let allResolvedPathsCanonical = tracker.allResolvedPathsCanonical;
96
191
  for (const gate of gates.slice(1)) {
97
- const fileName = RD_EVIDENCE_FILE[gate.name];
98
- const canonicalPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, 'rd', fileName);
99
- const legacyMisplacedPath = join(projectRoot, '.peaks', rdEvidenceDir, 'rd', fileName);
100
- const legacyChangeAxisPath = join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir, 'rd', fileName);
101
- let resolvedPath = null;
102
- let usedLegacy = false;
103
- for (const candidate of [canonicalPath, legacyMisplacedPath, legacyChangeAxisPath]) {
104
- if (existsSync(candidate)) {
105
- resolvedPath = candidate;
106
- usedLegacy = candidate !== canonicalPath;
107
- break;
192
+ const probeName = RD_EVIDENCE_PROBE[gate.name];
193
+ const relativePaths = (contractEvidencePaths('rd', 'qa-handoff', requestType, probeName) ?? [probeName])
194
+ .map((relative) => relative.replace('<rid>', rid));
195
+ const primaryRelativePath = relativePaths[0];
196
+ const canonicalPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, primaryRelativePath);
197
+ // Two independent axes, kept apart because they mean different things:
198
+ // - `legacyRoot` — the file sits under a pre-F3 session home
199
+ // (`.peaks/<sid>/…` or `.peaks/_runtime/change/<sid>/…`). That is a
200
+ // misplaced write, and it is the DEPRECATION violation's subject.
201
+ // - `legacyForm` — the file sits under one of the contract's declared
202
+ // `legacyRelativePaths`. The contract accepts those on purpose ("a
203
+ // legacy hit must keep the gate open, or existing sessions would fail
204
+ // the transition on upgrade"), so this is reported through
205
+ // `acceptedForm` and is *not* a violation.
206
+ const candidates = [];
207
+ for (const relative of relativePaths) {
208
+ const legacyForm = relative !== primaryRelativePath;
209
+ for (const [root, legacyRoot] of [
210
+ [join(projectRoot, '.peaks', '_runtime', rdEvidenceDir), false],
211
+ [join(projectRoot, '.peaks', rdEvidenceDir), true],
212
+ [join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir), true]
213
+ ]) {
214
+ candidates.push({ path: join(root, relative), legacyRoot, legacyForm });
108
215
  }
109
216
  }
110
- if (resolvedPath !== null) {
217
+ const hit = candidates.find((candidate) => existsSync(candidate.path));
218
+ if (hit !== undefined) {
111
219
  anyEvidenceResolved = true;
112
- if (usedLegacy)
220
+ if (hit.legacyRoot)
113
221
  allResolvedPathsCanonical = false;
114
- gate.passed = true;
115
- gate.detail = resolvedPath + (usedLegacy ? ' [DEPRECATION_LEGACY_PATH_USED]' : '');
116
- if (usedLegacy) {
117
- violations.push(`DEPRECATION_LEGACY_PATH_USED: ${resolvedPath} — move the file into .peaks/_runtime/${rdEvidenceDir}/rd/ (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
222
+ // Existing is not the contract — the file must also satisfy the body
223
+ // markers the table pins for this prereq (see `contractBodyViolations`).
224
+ const bodyViolations = contractBodyViolations('rd', 'qa-handoff', requestType, probeName, hit.path);
225
+ if (bodyViolations.length > 0) {
226
+ gate.passed = false;
227
+ gate.detail = `${hit.path} — ${bodyViolations.join('; ')}`;
228
+ violations.push(`RD evidence does not satisfy the contract: ${gate.description} (${primaryRelativePath}) — ${bodyViolations.join('; ')}`);
229
+ nextActions.push(`Fix .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} to satisfy the contract: ${bodyViolations.join('; ')}`);
230
+ }
231
+ else {
232
+ gate.passed = true;
233
+ gate.detail = hit.path
234
+ + (hit.legacyRoot ? ' [DEPRECATION_LEGACY_PATH_USED]' : '')
235
+ + (hit.legacyForm ? ' [LEGACY_EVIDENCE_PATH]' : '');
236
+ }
237
+ if (hit.legacyRoot) {
238
+ violations.push(`DEPRECATION_LEGACY_PATH_USED: ${hit.path} — move the file into .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
118
239
  }
119
240
  }
120
241
  else {
121
242
  gate.detail = `missing: ${canonicalPath}`;
122
- violations.push(`RD evidence missing: ${gate.description} (${fileName})`);
123
- nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/rd/${fileName}`);
243
+ violations.push(`RD evidence missing: ${gate.description} (${primaryRelativePath})`);
244
+ nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath}`);
124
245
  }
125
246
  }
126
247
  return { anyEvidenceResolved, allResolvedPathsCanonical };
127
248
  }
128
- /** Resolve QA evidence files (test-cases / test-report / security-findings /
129
- * performance-findings) by probing canonical + legacy paths for each gate.
130
- * For security/perf findings, delegates to the artifact-paths resolver.
131
- * Mutates the gate.passed / gate.detail fields in place and pushes
132
- * violations / nextActions. Returns the updated tracker. Verbatim-move
133
- * from `pipeline-verify-service.ts`. */
134
- export function resolveQaEvidencePaths(gates, projectRoot, rdEvidenceDir, changeIdForResolver, rid, violations, nextActions, tracker) {
135
- const QA_EVIDENCE_FILE = {
136
- 'test-cases': `test-cases/${rid}.md`,
137
- 'test-report': `test-reports/${rid}.md`,
138
- 'security-findings': '',
139
- 'performance-findings': ''
249
+ /** Resolve QA evidence files (test-cases / test-report) by probing canonical
250
+ * + legacy paths for each gate. Mutates the gate.passed / gate.detail fields
251
+ * in place and pushes violations / nextActions. Returns the updated tracker.
252
+ * Verbatim-move from `pipeline-verify-service.ts`.
253
+ *
254
+ * The `security-findings` / `performance-findings` branches that used to live
255
+ * here are gone: those artifacts are not in any `qa:verdict-issued` table
256
+ * (v2.11.0 D1/D4), and the evidence they stood for is resolved on the RD side
257
+ * at the contract's own paths (`audit/security-<rid>.md`,
258
+ * `audit/perf-<rid>.md`). */
259
+ export function resolveQaEvidencePaths(gates, projectRoot, rdEvidenceDir, requestType, rid, violations, nextActions, tracker) {
260
+ const QA_EVIDENCE_PROBE = {
261
+ 'test-cases': 'qa/test-cases/<rid>.md',
262
+ 'test-report': 'qa/test-reports/<rid>.md'
140
263
  };
141
264
  let anyEvidenceResolved = tracker.anyEvidenceResolved;
142
265
  let allResolvedPathsCanonical = tracker.allResolvedPathsCanonical;
143
266
  for (const gate of gates.slice(1)) {
144
- if (gate.name === 'security-findings' || gate.name === 'performance-findings') {
145
- const resolver = gate.name === 'security-findings' ? resolveSecurityFindingsPath : resolvePerformanceFindingsPath;
146
- const resolved = resolver({ projectRoot, sessionId: changeIdForResolver, rid });
147
- if (existsSync(resolved.path)) {
148
- anyEvidenceResolved = true;
149
- if (resolved.form === 'legacy')
150
- allResolvedPathsCanonical = false;
151
- gate.passed = true;
152
- gate.detail = resolved.path;
153
- if (resolved.form === 'legacy') {
154
- violations.push(`QA evidence accepted in legacy form (will be rejected after next minor release): ${resolved.path} — re-run peaks workflow plan refresh to migrate`);
155
- }
156
- }
157
- else {
158
- gate.detail = `missing: ${resolved.path}`;
159
- violations.push(`QA evidence missing: ${gate.description} (${resolved.path})`);
160
- nextActions.push(`Create ${resolved.path} (or use the legacy non-suffixed form during the 1-minor-release back-compat window)`);
161
- }
162
- continue;
163
- }
164
- const fileName = QA_EVIDENCE_FILE[gate.name];
165
- const canonicalQaPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, 'qa', fileName);
166
- const legacyMisplacedQaPath = join(projectRoot, '.peaks', rdEvidenceDir, 'qa', fileName);
167
- const legacyChangeAxisQaPath = join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir, 'qa', fileName);
168
- let resolvedQaPath = null;
169
- let usedLegacyQa = false;
170
- for (const candidate of [canonicalQaPath, legacyMisplacedQaPath, legacyChangeAxisQaPath]) {
171
- if (existsSync(candidate)) {
172
- resolvedQaPath = candidate;
173
- usedLegacyQa = candidate !== canonicalQaPath;
174
- break;
267
+ const probeName = QA_EVIDENCE_PROBE[gate.name];
268
+ const relativePaths = (contractEvidencePaths('qa', 'verdict-issued', requestType, probeName) ?? [probeName])
269
+ .map((relative) => relative.replace('<rid>', rid));
270
+ const primaryRelativePath = relativePaths[0];
271
+ const canonicalQaPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, primaryRelativePath);
272
+ const candidates = [];
273
+ for (const relative of relativePaths) {
274
+ for (const [root, legacyRoot] of [
275
+ [join(projectRoot, '.peaks', '_runtime', rdEvidenceDir), false],
276
+ [join(projectRoot, '.peaks', rdEvidenceDir), true],
277
+ [join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir), true]
278
+ ]) {
279
+ candidates.push({ path: join(root, relative), legacyRoot });
175
280
  }
176
281
  }
177
- if (resolvedQaPath !== null) {
282
+ const hit = candidates.find((candidate) => existsSync(candidate.path));
283
+ if (hit !== undefined) {
178
284
  anyEvidenceResolved = true;
179
- if (usedLegacyQa)
285
+ if (hit.legacyRoot)
180
286
  allResolvedPathsCanonical = false;
181
- gate.passed = true;
182
- gate.detail = resolvedQaPath + (usedLegacyQa ? ' [DEPRECATION_LEGACY_PATH_USED]' : '');
183
- if (usedLegacyQa) {
184
- violations.push(`DEPRECATION_LEGACY_PATH_USED: ${resolvedQaPath} — move the file into .peaks/_runtime/${rdEvidenceDir}/qa/ (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
287
+ // Same contract check as the RD side: the `test-report` / `test-cases`
288
+ // prereqs pin `## Test execution` / `## Test cases` / `test(`, and a
289
+ // checker that only probed existence reported green on files the
290
+ // `qa:verdict-issued` gate rejects.
291
+ const bodyViolations = contractBodyViolations('qa', 'verdict-issued', requestType, probeName, hit.path);
292
+ if (bodyViolations.length > 0) {
293
+ gate.passed = false;
294
+ gate.detail = `${hit.path} — ${bodyViolations.join('; ')}`;
295
+ violations.push(`QA evidence does not satisfy the contract: ${gate.description} (${primaryRelativePath}) — ${bodyViolations.join('; ')}`);
296
+ nextActions.push(`Fix .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} to satisfy the contract: ${bodyViolations.join('; ')}`);
297
+ }
298
+ else {
299
+ gate.passed = true;
300
+ gate.detail = hit.path + (hit.legacyRoot ? ' [DEPRECATION_LEGACY_PATH_USED]' : '');
301
+ }
302
+ if (hit.legacyRoot) {
303
+ violations.push(`DEPRECATION_LEGACY_PATH_USED: ${hit.path} — move the file into .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
185
304
  }
186
305
  }
187
306
  else {
188
307
  gate.detail = `missing: ${canonicalQaPath}`;
189
- violations.push(`QA evidence missing: ${gate.description} (${fileName})`);
190
- nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/qa/${fileName}`);
308
+ violations.push(`QA evidence missing: ${gate.description} (${primaryRelativePath})`);
309
+ nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath}`);
191
310
  }
192
311
  }
193
312
  return { anyEvidenceResolved, allResolvedPathsCanonical };
@@ -96,7 +96,7 @@ export async function verifyPipeline(options) {
96
96
  // to `options.rid` would make every missing-evidence path look like
97
97
  // a per-rid scope dir.
98
98
  const rdEvidenceDir = resolvedChangeId || options.sessionId || getSessionIdCanonical(options.projectRoot) || options.rid;
99
- const rdTracker = resolveRdEvidencePaths(rdGates, rdEvidenceDir, options.projectRoot, violations, nextActions, { anyEvidenceResolved: false, allResolvedPathsCanonical: true });
99
+ const rdTracker = resolveRdEvidencePaths(rdGates, rdEvidenceDir, options.projectRoot, options.rid, requestType, violations, nextActions, { anyEvidenceResolved: false, allResolvedPathsCanonical: true });
100
100
  // Check if RD reached qa-handoff
101
101
  if (rdInvoked && !RD_QA_HANDOFF_STATES.has(rdState)) {
102
102
  violations.push(`RD not ready for QA: state is "${rdState}" — must reach "qa-handoff" (unit tests, karpathy-guidelines §1 Think / §2 Simplicity / §3 Surgical / §4 Goal-Driven, code review, security review complete)`);
@@ -118,16 +118,11 @@ export async function verifyPipeline(options) {
118
118
  nextActions.push('Invoke Skill(skill="peaks-qa") with the request-id for functional/performance/security testing');
119
119
  qaGates[0].detail = 'not found';
120
120
  }
121
- // Check QA evidence files.
122
- // v2.18.1 bug #5 fix: when no RD/QA artifact is on disk yet
123
- // (resolvedChangeId is empty), fall back to the current session id
124
- // from the binding-store instead of `rdEvidenceDir` (= the rid). The
125
- // session axis `.peaks/_runtime/<sessionId>/qa/...` is the canonical
126
- // v2.17.0 home; the legacy `_runtime/change/<sessionId>/qa/...` probe
127
- // should only fire for pre-v2.17.0 workspaces, not as a default for
128
- // new requests.
129
- const changeIdForResolver = resolvedChangeId || getSessionIdCanonical(options.projectRoot) || rdEvidenceDir;
130
- const qaTracker = resolveQaEvidencePaths(qaGates, options.projectRoot, rdEvidenceDir, changeIdForResolver, options.rid, violations, nextActions, rdTracker);
121
+ // Check QA evidence files. (The v2.18.1 bug #5 `changeIdForResolver`
122
+ // fallback — current session id when no RD/QA artifact is on disk yet —
123
+ // was dropped by rid 2026-09-14-verify-pipeline-contract-drift along with
124
+ // the security/perf findings branch that was its only consumer.)
125
+ const qaTracker = resolveQaEvidencePaths(qaGates, options.projectRoot, rdEvidenceDir, requestType, options.rid, violations, nextActions, rdTracker);
131
126
  const anyEvidenceResolved = qaTracker.anyEvidenceResolved;
132
127
  const allResolvedPathsCanonical = qaTracker.allResolvedPathsCanonical;
133
128
  // Check if QA reached verdict-issued
@@ -225,20 +220,26 @@ export async function verifyPipeline(options) {
225
220
  const complete = rdInvoked && qaInvoked && allRdGatesPassed && allQaGatesPassed && allFeedbackGatesPassed
226
221
  && RD_QA_HANDOFF_STATES.has(rdState) && QA_COMPLETE_STATES.has(qaState);
227
222
  // Slice 025 — derive the `acceptedForm` and `gateC` verdict. The form is
228
- // 'suffixed' if both the security + perf gates passed via the new
229
- // per-rid path; 'legacy' if either was consumed via the legacy fallback;
230
- // 'none' if neither passed.
231
- const secGate = qaGates.find((g) => g.name === 'security-findings');
232
- const perfGate = qaGates.find((g) => g.name === 'performance-findings');
233
- const secForm = secGate?.detail?.includes(`-${options.rid}.md`) ? 'suffixed' : 'legacy';
234
- const perfForm = perfGate?.detail?.includes(`-${options.rid}.md`) ? 'suffixed' : 'legacy';
223
+ // 'suffixed' when the contract's current path served the file and 'legacy'
224
+ // when the deprecated fallback did; 'none' if neither gate passed.
225
+ //
226
+ // rid 2026-09-14-verify-pipeline-contract-drift: the two gates moved from the
227
+ // QA phase to the RD phase, because that is where the current contract puts
228
+ // the evidence (`AUDIT_SECURITY` / `AUDIT_PERF` at `rd:qa-handoff`). The
229
+ // `-<rid>.md` suffix that used to distinguish the forms is a real one again
230
+ // after slice `2026-09-14-audit-artifact-rid-scoping` rid-scoped the audit
231
+ // paths; the resolver marks which form served the file with
232
+ // `[LEGACY_EVIDENCE_PATH]`.
233
+ const secGate = rdGates.find((g) => g.name === 'security-review');
234
+ const perfGate = rdGates.find((g) => g.name === 'perf-baseline');
235
+ const formOf = (gate) => gate?.passed === true && !gate.detail.includes('LEGACY_EVIDENCE_PATH') ? 'suffixed' : 'legacy';
236
+ const secForm = formOf(secGate);
237
+ const perfForm = formOf(perfGate);
235
238
  const acceptedForm = !secGate?.passed && !perfGate?.passed
236
239
  ? 'none'
237
- : (secForm === 'suffixed' && perfForm === 'suffixed')
238
- ? 'suffixed'
239
- : (secForm === 'legacy' || perfForm === 'legacy')
240
- ? 'legacy'
241
- : 'suffixed';
240
+ : (secForm === 'legacy' || perfForm === 'legacy')
241
+ ? 'legacy'
242
+ : 'suffixed';
242
243
  const gateC = allQaGatesPassed ? 'pass' : 'fail';
243
244
  const gateH = allFeedbackGatesPassed ? 'pass' : 'fail';
244
245
  // Slice 2026-06-28-code-mode-bypass-fix (defect #3): `true` when
@@ -59,9 +59,16 @@ export type PipelineVerification = {
59
59
  };
60
60
  violations: string[];
61
61
  nextActions: string[];
62
- /** Form of the security/performance findings artifacts Gate C accepted
63
- * (slice 025). `'suffixed'` for the new per-rid form, `'legacy'` for the
64
- * pre-slice-025 non-suffixed form, `'none'` when neither was found. */
62
+ /** Form of the security/performance evidence the RD gates accepted
63
+ * (slice 025). `'suffixed'` when the current contract's path served the
64
+ * file, `'legacy'` when a deprecated fallback did, `'none'` when neither
65
+ * gate passed. The per-rid `<rid>.md` suffix the union was named after was
66
+ * retired with `qa/security-findings-<rid>.md` (v2.11.0 D1/D4); the evidence
67
+ * now lives at `audit/security-<rid>.md` / `audit/perf-<rid>.md`
68
+ * (rid-scoped since slice `2026-09-14-audit-artifact-rid-scoping`) with
69
+ * `audit/security.md` / `audit/perf.md` and
70
+ * `rd/security-review.md` / `rd/perf-baseline.md` as the declared legacy
71
+ * fallbacks (rid 2026-09-14-verify-pipeline-contract-drift). */
65
72
  acceptedForm?: 'suffixed' | 'legacy' | 'none';
66
73
  /** `gateC` is the pre-computed verdict string (AC7 dogfood shape). */
67
74
  gateC?: 'pass' | 'fail';
@@ -186,7 +186,8 @@ function resolveSkipAppliedBy() {
186
186
  const { execFileSync } = require('node:child_process');
187
187
  const out = execFileSync('git', ['config', '--global', 'user.name'], {
188
188
  encoding: 'utf8',
189
- stdio: ['ignore', 'pipe', 'ignore']
189
+ stdio: ['ignore', 'pipe', 'ignore'],
190
+ windowsHide: true
190
191
  });
191
192
  const trimmed = out.trim();
192
193
  if (trimmed.length > 0) {
@@ -83,15 +83,35 @@ export declare const CLAUDE_SETTINGS_LOCAL_FILENAME = ".claude/settings.local.js
83
83
  */
84
84
  export declare const TEMPLATE_VERSION = "1.7.0";
85
85
  /**
86
- * Compare two serialized template strings for semantic equivalence: does the
87
- * on-disk file already declare everything the generated template declares?
86
+ * Compare two serialized template strings: does the on-disk file already
87
+ * declare every entry the generated tree declares?
88
88
  *
89
- * Returns `true` iff both strings parse to objects whose
90
- * `hooks.PreToolUse` arrays are structurally identical (same length;
91
- * each entry's `matcher`, `hooks[].type`, `hooks[].command` match) AND the
92
- * on-disk `env` already carries every exemption the template declares (extra
93
- * on-disk keys and extra globs are allowed — a user may exempt other trees,
94
- * and a requirement the file already exceeds must not re-trigger a write).
89
+ * OWNERSHIP IS PER ENTRY, NOT PER KEY (rid 2026-09-13-two-decisions item ②).
90
+ * This comparator answers "is each entry the GENERATED tree declares present
91
+ * on disk?", NOT "are the two `hooks` trees identical". Extra on-disk entries
92
+ * are IGNORED, so an entry another writer put in this file never makes it look
93
+ * drifted.
94
+ *
95
+ * That is the deliberate other half of the entry-level merge in
96
+ * `mergeTemplateOwnedHooks` / `workspace-claude-settings-materializer.ts`.
97
+ * `.claude/settings.local.json` has a SECOND writer of `hooks.PreToolUse`:
98
+ * `installAutoCompactHook` appends a `Bash|Task` entry. Under the previous
99
+ * exact-tree rule the merged file carried 4 entries against a 3-entry
100
+ * generated tree, so every `peaks workspace init` answered "drifted",
101
+ * rewrote, and reported `refreshed` forever — precisely the state whole-key
102
+ * ownership existed to prevent, and the reason the merge could not ship alone.
103
+ *
104
+ * Matching is order-insensitive AND multiset-aware: the template declares TWO
105
+ * `Bash` entries, and each must have its own counterpart on disk, so a file
106
+ * carrying only one of them is still reported as drifted (the previous
107
+ * index-by-index loop had the same property; it is load-bearing, not a
108
+ * detail).
109
+ *
110
+ * Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
111
+ * arrays satisfy that containment AND the on-disk `env` already carries every
112
+ * exemption the template declares (extra on-disk keys and extra globs are
113
+ * allowed — a user may exempt other trees, and a requirement the file already
114
+ * exceeds must not re-trigger a write).
95
115
  *
96
116
  * Returns `false` on any `JSON.parse` error, shape mismatch, or
97
117
  * missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
@@ -101,6 +121,34 @@ export declare const TEMPLATE_VERSION = "1.7.0";
101
121
  * refresh a stale `.peaks/.claude-settings-template.json` on disk.
102
122
  */
103
123
  export declare function templateContentMatches(generated: string, onDisk: string): boolean;
124
+ /**
125
+ * Merge the on-disk `hooks.PreToolUse` list with the template's.
126
+ *
127
+ * THE OWNERSHIP RULE (rid 2026-09-13-two-decisions item ②): this template owns
128
+ * the entries IT DECLARES — and nothing else. Every other on-disk entry is
129
+ * carried across verbatim, whatever its matcher, because the template has no
130
+ * opinion about it:
131
+ *
132
+ * - a `matcher` the template does not declare (`Bash|Task`, the auto-compact
133
+ * hook `installAutoCompactHook` appends) is never touched;
134
+ * - surplus entries BEYOND the template's count for a declared matcher (a
135
+ * user's own `Bash` hook) are surplus too, and survive;
136
+ * - an on-disk entry that fills a declared slot is REPLACED by the template's
137
+ * entry for it. That is what makes a hand-edited (or older-release) entry
138
+ * self-heal instead of lingering next to a correct copy of itself.
139
+ *
140
+ * Slot counting is per `matcher` and positional within it: the template
141
+ * declares TWO `Bash` entries, so the first two on-disk `Bash` entries are
142
+ * theirs and a third is the user's. The template's entries are emitted first,
143
+ * in template order, then the preserved ones in their on-disk order — which is
144
+ * a fixed point: re-merging the result yields the result (the template's own
145
+ * entries are encountered first and refill their own slots).
146
+ *
147
+ * Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
148
+ * rather than dropped: guessing at their shape is how a user's entry gets
149
+ * deleted.
150
+ */
151
+ export declare function mergeTemplateOwnedHooks(onDisk: ReadonlyArray<unknown>, template: ReadonlyArray<unknown>): unknown[];
104
152
  /**
105
153
  * Absolute path of the shipped Write|Edit|MultiEdit gate script.
106
154
  *