peaks-loop 4.0.47 → 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 (104) hide show
  1. package/CHANGELOG.md +24 -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.js +48 -8
  9. package/dist/cli/commands/compact-command.js +112 -0
  10. package/dist/cli/commands/config-commands.js +15 -9
  11. package/dist/cli/commands/dashboard-long-run.js +6 -0
  12. package/dist/cli/commands/dispatch-commands.js +11 -1
  13. package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
  14. package/dist/cli/commands/hooks-commands.js +4 -4
  15. package/dist/cli/commands/job-commands.js +8 -0
  16. package/dist/cli/commands/loop-eval-commands.js +15 -0
  17. package/dist/cli/commands/perf-audit-commands.js +2 -0
  18. package/dist/cli/commands/playwright-commands.js +12 -0
  19. package/dist/cli/commands/prd-commands.js +1 -1
  20. package/dist/cli/commands/qa-commands.js +22 -0
  21. package/dist/cli/commands/request-commands.js +8 -0
  22. package/dist/cli/commands/scan-commands.js +1 -1
  23. package/dist/cli/commands/security-audit-commands.js +2 -0
  24. package/dist/cli/commands/slice-integrate-commands.js +5 -0
  25. package/dist/cli/commands/statusline-commands.js +44 -4
  26. package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
  27. package/dist/cli/commands/sub-agent/detached.js +47 -22
  28. package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
  29. package/dist/cli/commands/verdict-aggregate-command.js +95 -13
  30. package/dist/cli/commands/workflow-commands.js +1 -1
  31. package/dist/cli/index.js +5 -45
  32. package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
  33. package/dist/services/artifacts/artifact-prerequisites.js +130 -65
  34. package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
  35. package/dist/services/artifacts/request-artifact-service.js +18 -8
  36. package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
  37. package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
  38. package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
  39. package/dist/services/audit-independent/perf-audit-service.js +27 -5
  40. package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
  41. package/dist/services/audit-independent/security-audit-service.js +28 -6
  42. package/dist/services/code/auto-compact-lifecycle.d.ts +119 -0
  43. package/dist/services/code/auto-compact-lifecycle.js +169 -0
  44. package/dist/services/code/auto-compact-orchestrator.js +13 -2
  45. package/dist/services/code/compact-event-settle.d.ts +122 -0
  46. package/dist/services/code/compact-event-settle.js +219 -0
  47. package/dist/services/compact-history/compact-history-service.d.ts +14 -0
  48. package/dist/services/config/config-restore.d.ts +12 -1
  49. package/dist/services/config/config-restore.js +35 -4
  50. package/dist/services/config/config-rollback.js +6 -1
  51. package/dist/services/context/harness-context-witness.d.ts +310 -0
  52. package/dist/services/context/harness-context-witness.js +606 -0
  53. package/dist/services/evidence/evidence-generator.js +86 -49
  54. package/dist/services/final-review/final-review-service.d.ts +9 -0
  55. package/dist/services/final-review/final-review-service.js +36 -12
  56. package/dist/services/ide/ide-registry.d.ts +19 -0
  57. package/dist/services/ide/ide-registry.js +21 -0
  58. package/dist/services/job/job-state-store.js +7 -0
  59. package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
  60. package/dist/services/prd/handoff-auto-regen.js +31 -27
  61. package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
  62. package/dist/services/prd/handoff-frontmatter.js +75 -0
  63. package/dist/services/prd/handoff-service.d.ts +41 -2
  64. package/dist/services/prd/handoff-service.js +81 -8
  65. package/dist/services/prd/handoff-types.d.ts +3 -2
  66. package/dist/services/prd/handoff-types.js +3 -2
  67. package/dist/services/qa/qa-business-review-state.js +9 -0
  68. package/dist/services/scan/karpathy-service.js +2 -2
  69. package/dist/services/session/session-checkpoint-service.js +8 -0
  70. package/dist/services/skill/resume-detector.js +29 -11
  71. package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
  72. package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
  73. package/dist/services/skills/hooks-settings-service.js +14 -4
  74. package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
  75. package/dist/services/skills/session-start-hook-constants.js +45 -0
  76. package/dist/services/skills/skill-statusline-service.d.ts +14 -0
  77. package/dist/services/slice/slice-check-service.js +29 -11
  78. package/dist/services/slice/slice-review-state.js +8 -0
  79. package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
  80. package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
  81. package/dist/services/workflow/pipeline-verify-service.js +24 -23
  82. package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
  83. package/dist/services/workspace/claude-settings-template.d.ts +56 -8
  84. package/dist/services/workspace/claude-settings-template.js +98 -20
  85. package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
  86. package/package.json +6 -6
  87. package/skills/bee/peaks-prd/SKILL.md +7 -5
  88. package/skills/bee/peaks-qa/SKILL.md +5 -5
  89. package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
  90. package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
  91. package/skills/bee/peaks-rd/SKILL.md +8 -6
  92. package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
  93. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
  94. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
  95. package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
  96. package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
  97. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
  98. package/skills/peaks-code/SKILL.md +1 -1
  99. package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
  100. package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
  101. package/skills/peaks-code/references/resume-detection.md +13 -7
  102. package/skills/peaks-code/references/runbook.md +3 -2
  103. package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
  104. package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
@@ -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';
@@ -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
  *
@@ -86,15 +86,35 @@ export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
86
86
  */
87
87
  export const TEMPLATE_VERSION = '1.7.0';
88
88
  /**
89
- * Compare two serialized template strings for semantic equivalence: does the
90
- * on-disk file already declare everything the generated template declares?
89
+ * Compare two serialized template strings: does the on-disk file already
90
+ * declare every entry the generated tree declares?
91
91
  *
92
- * Returns `true` iff both strings parse to objects whose
93
- * `hooks.PreToolUse` arrays are structurally identical (same length;
94
- * each entry's `matcher`, `hooks[].type`, `hooks[].command` match) AND the
95
- * on-disk `env` already carries every exemption the template declares (extra
96
- * on-disk keys and extra globs are allowed — a user may exempt other trees,
97
- * and a requirement the file already exceeds must not re-trigger a write).
92
+ * OWNERSHIP IS PER ENTRY, NOT PER KEY (rid 2026-09-13-two-decisions item ②).
93
+ * This comparator answers "is each entry the GENERATED tree declares present
94
+ * on disk?", NOT "are the two `hooks` trees identical". Extra on-disk entries
95
+ * are IGNORED, so an entry another writer put in this file never makes it look
96
+ * drifted.
97
+ *
98
+ * That is the deliberate other half of the entry-level merge in
99
+ * `mergeTemplateOwnedHooks` / `workspace-claude-settings-materializer.ts`.
100
+ * `.claude/settings.local.json` has a SECOND writer of `hooks.PreToolUse`:
101
+ * `installAutoCompactHook` appends a `Bash|Task` entry. Under the previous
102
+ * exact-tree rule the merged file carried 4 entries against a 3-entry
103
+ * generated tree, so every `peaks workspace init` answered "drifted",
104
+ * rewrote, and reported `refreshed` forever — precisely the state whole-key
105
+ * ownership existed to prevent, and the reason the merge could not ship alone.
106
+ *
107
+ * Matching is order-insensitive AND multiset-aware: the template declares TWO
108
+ * `Bash` entries, and each must have its own counterpart on disk, so a file
109
+ * carrying only one of them is still reported as drifted (the previous
110
+ * index-by-index loop had the same property; it is load-bearing, not a
111
+ * detail).
112
+ *
113
+ * Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
114
+ * arrays satisfy that containment AND the on-disk `env` already carries every
115
+ * exemption the template declares (extra on-disk keys and extra globs are
116
+ * allowed — a user may exempt other trees, and a requirement the file already
117
+ * exceeds must not re-trigger a write).
98
118
  *
99
119
  * Returns `false` on any `JSON.parse` error, shape mismatch, or
100
120
  * missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
@@ -121,20 +141,15 @@ export function templateContentMatches(generated, onDisk) {
121
141
  if (!isTemplateShape(parsedGenerated) || !isTemplateShape(parsedOnDisk)) {
122
142
  return false;
123
143
  }
124
- const generatedEntries = parsedGenerated.hooks.PreToolUse;
125
- const onDiskEntries = parsedOnDisk.hooks.PreToolUse;
126
- if (generatedEntries.length !== onDiskEntries.length) {
127
- return false;
128
- }
129
- for (let i = 0; i < generatedEntries.length; i += 1) {
130
- const a = generatedEntries[i];
131
- const b = onDiskEntries[i];
132
- if (a.matcher !== b.matcher) {
133
- return false;
134
- }
135
- if (!sameHooksArray(a.hooks, b.hooks)) {
144
+ // Multiset containment: consume one on-disk entry per generated entry so a
145
+ // file holding a single copy of a doubly-declared entry still fails.
146
+ const unmatched = [...parsedOnDisk.hooks.PreToolUse];
147
+ for (const required of parsedGenerated.hooks.PreToolUse) {
148
+ const at = unmatched.findIndex((candidate) => sameEntry(required, candidate));
149
+ if (at === -1) {
136
150
  return false;
137
151
  }
152
+ unmatched.splice(at, 1);
138
153
  }
139
154
  // A project installed by a release that predates a template-declared
140
155
  // exemption still needs the refresh this comparator gates — otherwise the
@@ -143,6 +158,69 @@ export function templateContentMatches(generated, onDisk) {
143
158
  // uses, so the two writers cannot drift apart.
144
159
  return hasExternalGateExemptions({ env: parsedOnDisk.env });
145
160
  }
161
+ /**
162
+ * Merge the on-disk `hooks.PreToolUse` list with the template's.
163
+ *
164
+ * THE OWNERSHIP RULE (rid 2026-09-13-two-decisions item ②): this template owns
165
+ * the entries IT DECLARES — and nothing else. Every other on-disk entry is
166
+ * carried across verbatim, whatever its matcher, because the template has no
167
+ * opinion about it:
168
+ *
169
+ * - a `matcher` the template does not declare (`Bash|Task`, the auto-compact
170
+ * hook `installAutoCompactHook` appends) is never touched;
171
+ * - surplus entries BEYOND the template's count for a declared matcher (a
172
+ * user's own `Bash` hook) are surplus too, and survive;
173
+ * - an on-disk entry that fills a declared slot is REPLACED by the template's
174
+ * entry for it. That is what makes a hand-edited (or older-release) entry
175
+ * self-heal instead of lingering next to a correct copy of itself.
176
+ *
177
+ * Slot counting is per `matcher` and positional within it: the template
178
+ * declares TWO `Bash` entries, so the first two on-disk `Bash` entries are
179
+ * theirs and a third is the user's. The template's entries are emitted first,
180
+ * in template order, then the preserved ones in their on-disk order — which is
181
+ * a fixed point: re-merging the result yields the result (the template's own
182
+ * entries are encountered first and refill their own slots).
183
+ *
184
+ * Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
185
+ * rather than dropped: guessing at their shape is how a user's entry gets
186
+ * deleted.
187
+ */
188
+ export function mergeTemplateOwnedHooks(onDisk, template) {
189
+ const slots = new Map();
190
+ for (const entry of template) {
191
+ if (!isPreToolUseEntry(entry))
192
+ continue;
193
+ slots.set(entry.matcher, (slots.get(entry.matcher) ?? 0) + 1);
194
+ }
195
+ const taken = new Map();
196
+ const preserved = [];
197
+ for (const entry of onDisk) {
198
+ // Unowned by construction: not a shape the template could have declared.
199
+ if (!isPreToolUseEntry(entry)) {
200
+ preserved.push(entry);
201
+ continue;
202
+ }
203
+ const declared = slots.get(entry.matcher) ?? 0;
204
+ const used = taken.get(entry.matcher) ?? 0;
205
+ if (used >= declared) {
206
+ preserved.push(entry);
207
+ continue;
208
+ }
209
+ taken.set(entry.matcher, used + 1);
210
+ }
211
+ return [...template, ...preserved];
212
+ }
213
+ /** Structural equality of two `PreToolUse` entries. */
214
+ function sameEntry(a, b) {
215
+ return a.matcher === b.matcher && sameHooksArray(a.hooks, b.hooks);
216
+ }
217
+ function isPreToolUseEntry(value) {
218
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
219
+ return false;
220
+ }
221
+ const candidate = value;
222
+ return typeof candidate.matcher === 'string' && Array.isArray(candidate.hooks);
223
+ }
146
224
  function isTemplateShape(value) {
147
225
  if (typeof value !== 'object' || value === null) {
148
226
  return false;
@@ -13,7 +13,7 @@ import { existsSync, readFileSync } from 'node:fs';
13
13
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
15
  import { withExternalGateExemptions } from '../skills/hooks-codegate-superpowers.js';
16
- import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME, templateContentMatches } from './claude-settings-template.js';
16
+ import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME, mergeTemplateOwnedHooks, templateContentMatches } from './claude-settings-template.js';
17
17
  /** Read a file as text, or `undefined` when it cannot be read. */
18
18
  function readTextIfPresent(filePath) {
19
19
  try {
@@ -48,12 +48,16 @@ function readEnvObject(serialized) {
48
48
  * The top-level keys this function's template is allowed to DECIDE. Every other
49
49
  * key on disk belongs to whoever put it there and is carried across verbatim.
50
50
  *
51
- * - `hooks` — the tree this function exists to keep in sync. `peaks workspace
52
- * init` is the writer that converges a consumer's file on the current
51
+ * - `hooks` — the tree this function exists to keep in sync, ENTRY BY ENTRY.
52
+ * `peaks workspace init` converges a consumer's file on the current
53
53
  * release's handler set, and `templateContentMatches` (the drift detector
54
- * that decides whether to rewrite at all) compares exactly this tree. Letting
55
- * the disk win here would make the rewrite a no-op that reports `refreshed`
56
- * forever.
54
+ * that decides whether to rewrite at all) asks whether every entry the
55
+ * generated tree declares is present — not whether the trees are equal.
56
+ * Letting the disk win outright would make the rewrite a no-op that reports
57
+ * `refreshed` forever AND would never deliver a changed handler; letting the
58
+ * template win outright is what deleted the auto-compact hook. See
59
+ * `mergeHooksTree` / `mergeTemplateOwnedHooks` for the rule that does
60
+ * neither.
57
61
  * - `env` — jointly owned with `peaks hooks install`, which unions the user's
58
62
  * exemption globs into it. Handled as a union below, not by either side
59
63
  * winning outright.
@@ -62,6 +66,35 @@ function readEnvObject(serialized) {
62
66
  * direction matters. A whitelist drops every key it was not told about — which
63
67
  * is how `permissions` was lost — whereas anything absent from this list is
64
68
  * preserved by default, including keys no release of peaks-loop knows about.
69
+ *
70
+ * ⚠️ `hooks` USED TO BE OWNED WHOLE — every entry in it was deleted by the next
71
+ * `peaks workspace init` unless the template declared it, silently:
72
+ * `templateContentMatches` saw the extra entry, answered "drifted", and the
73
+ * rewrite emitted `{...template}`. That was not hypothetical.
74
+ * `.claude/settings.local.json` has a second writer of peaks' OWN hooks:
75
+ * `installAutoCompactHook` (`src/services/hooks/auto-compact-hook-install.ts`),
76
+ * reached from `peaks code auto-compact` on an adapter declaring
77
+ * `compactPathway: 'ide-native'` — which `claude-code` does. Measured on a
78
+ * throwaway project root (rid 2026-09-13-two-decisions item ②):
79
+ *
80
+ * init (written, 3 PreToolUse entries: Write|Edit|MultiEdit, Bash, Bash)
81
+ * → installAutoCompactHook (installed, 4: … | Bash|Task)
82
+ * → init again (REFRESHED, 3: … ) ← the Bash|Task entry is gone
83
+ *
84
+ * and nothing re-installs it: the hook's whole job was to fire on the next
85
+ * Bash/Task call, so once it is deleted the auto-compact contract stops
86
+ * silently.
87
+ *
88
+ * FIXED 2026-09-13 (user-decided): this template now owns only the entries IT
89
+ * DECLARES. `mergeHooksTree` below unions the rest of the on-disk `hooks` tree
90
+ * across verbatim, and `templateContentMatches` — the drift detector — was
91
+ * changed in the same slice from "the trees are identical" to "every entry the
92
+ * generated tree declares is present". The two halves are one change: the merge
93
+ * alone would leave the comparator comparing a 3-entry generated tree against a
94
+ * 4-entry file on EVERY init and reporting `refreshed` forever, which is the
95
+ * state whole-key ownership existed to prevent. See
96
+ * `mergeTemplateOwnedHooks` in `claude-settings-template.ts` for the ownership
97
+ * rule and `templateContentMatches` for the containment rule it implies.
65
98
  */
66
99
  const TEMPLATE_OWNED_KEYS = new Set(['hooks', 'env']);
67
100
  /** `template`'s own keys, then every on-disk key the template does not own. */
@@ -72,8 +105,34 @@ function carryUserOwnedKeys(onDisk, template) {
72
105
  continue;
73
106
  merged[key] = value;
74
107
  }
108
+ merged.hooks = mergeHooksTree(onDisk.hooks, template.hooks);
75
109
  return merged;
76
110
  }
111
+ /**
112
+ * Merge the on-disk `hooks` tree with the template's, one event at a time.
113
+ *
114
+ * Only the events the template DECLARES are merged (and only their declared
115
+ * entries — see `mergeTemplateOwnedHooks`); every other event, and every other
116
+ * key under `hooks`, is carried across from the disk untouched. The template
117
+ * currently declares `PreToolUse` alone, so this is what keeps a hand-added or
118
+ * future-installer `SessionStart` entry in this file instead of deleting it —
119
+ * the latent half of the hazard the header describes.
120
+ */
121
+ function mergeHooksTree(onDiskHooks, templateHooks) {
122
+ const onDisk = isPlainRecord(onDiskHooks) ? { ...onDiskHooks } : {};
123
+ const template = isPlainRecord(templateHooks) ? templateHooks : {};
124
+ for (const [event, declared] of Object.entries(template)) {
125
+ const current = onDisk[event];
126
+ onDisk[event] = Array.isArray(declared)
127
+ ? mergeTemplateOwnedHooks(Array.isArray(current) ? current : [], declared)
128
+ : declared;
129
+ }
130
+ return onDisk;
131
+ }
132
+ /** A JSON object, as opposed to a null / array / primitive. */
133
+ function isPlainRecord(value) {
134
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
135
+ }
77
136
  /**
78
137
  * The peaks-managed snippet appended to the consumer project's
79
138
  * `.peaks/.gitignore` so the local-only settings file never lands
@@ -213,6 +272,17 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
213
272
  * Returns the action taken so the caller can surface it in the
214
273
  * envelope. Read failures are treated as drift so a malformed
215
274
  * on-disk file always self-heals on the next init.
275
+ *
276
+ * WHAT IS COMPARED (rid 2026-09-13-two-decisions item ②): the copy is checked
277
+ * against the TEMPLATE'S OWN entries — `buildClaudeSettingsLocalJson()` — not
278
+ * against `serialized`, the merged LOCAL file content it is written from. This
279
+ * file is a copy of the template (its name and this doc both say so), so
280
+ * "is it current?" is a question about the template's entries only; asking it
281
+ * against the merged local file made the copy report `refreshed` once for every
282
+ * entry another writer had added to `.claude/settings.local.json` — drift noise
283
+ * about a file the copy does not own, on the very init that is supposed to be a
284
+ * no-op. With entry-containment semantics the copy is current as soon as it
285
+ * declares every template entry, whatever else it carries.
216
286
  */
217
287
  async function writeOfflineTemplateCopy(projectRoot, serialized) {
218
288
  const copyPath = join(projectRoot, '.peaks', '.claude-settings-template.json');
@@ -222,7 +292,8 @@ async function writeOfflineTemplateCopy(projectRoot, serialized) {
222
292
  try {
223
293
  const { readFile } = await import('node:fs/promises');
224
294
  const existing = await readFile(copyPath, 'utf8');
225
- if (templateContentMatches(serialized, existing)) {
295
+ const declared = JSON.stringify(buildClaudeSettingsLocalJson());
296
+ if (templateContentMatches(declared, existing)) {
226
297
  action = 'already-current';
227
298
  }
228
299
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.47",
3
+ "version": "4.0.48",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -102,10 +102,10 @@
102
102
  "picomatch": "4.0.4",
103
103
  "yaml": "^2.9.0",
104
104
  "zod": "^4.4.3",
105
- "peaks-loop-internal-runtime": "0.0.32",
106
- "peaks-loop-shared": "0.0.81",
107
- "peaks-loop-mut": "0.1.45",
108
- "peaks-loop-shared-channel": "0.0.49"
105
+ "peaks-loop-internal-runtime": "0.0.33",
106
+ "peaks-loop-shared-channel": "0.0.50",
107
+ "peaks-loop-shared": "0.0.82",
108
+ "peaks-loop-mut": "0.1.46"
109
109
  },
110
110
  "devDependencies": {
111
111
  "@changesets/cli": "2.31.1",
@@ -141,7 +141,7 @@
141
141
  "test:dev": "vitest run tests/unit",
142
142
  "test:dev:cli": "vitest run tests/unit",
143
143
  "test:unit": "vitest run tests/unit",
144
- "test:integration": "vitest run tests/integration",
144
+ "test:integration": "vitest run --config vitest.config.integration.ts tests/integration",
145
145
  "test:capability-guard": "vitest run --config vitest.config.integration.ts tests/integration/capability-guard",
146
146
  "test:cli": "vitest run tests/unit",
147
147
  "test:workflow": "vitest run tests/unit",
@@ -122,7 +122,7 @@ Then display: `Peaks-Loop Skill: peaks-prd | Peaks-Loop Gate: startup | Next: <o
122
122
 
123
123
  ## Mandatory per-request artifact
124
124
 
125
- Every PRD invocation — feature, bug, refactor, clarification — must write a durable artifact at `.peaks/_runtime/<session-id>/prd/requests/<request-id>.md`. The artifact is the canonical trace; the chat transcript is not. Handoff to RD/UI/QA is blocked while the artifact is missing or in `draft` state. After user confirmation, the **immutable handoff** is written separately at `.peaks/_runtime/<sid>/prd/handoff.md` (Step 5.5 below) — sha256-locked, schemaVersion: 2 — and is the source of truth for RD, QA, and the 4 audit sub-agents (code-reviewer / security-reviewer / karpathy-reviewer / qa-test-cases-writer).
125
+ Every PRD invocation — feature, bug, refactor, clarification — must write a durable artifact at `.peaks/_runtime/<session-id>/prd/requests/<request-id>.md`. The artifact is the canonical trace; the chat transcript is not. Handoff to RD/UI/QA is blocked while the artifact is missing or in `draft` state. After user confirmation, the **immutable handoff** is written separately at `.peaks/_runtime/<sid>/prd/handoff-<rid>.md` (Step 5.5 below) — sha256-locked, schemaVersion: 2 — and is the source of truth for RD, QA, and the 4 audit sub-agents (code-reviewer / security-reviewer / karpathy-reviewer / qa-test-cases-writer).
126
126
 
127
127
  Use `<request-id>` of the form `YYYY-MM-DD-<kebab-slug>` (or whatever id the user assigned) so PRD/UI/RD/QA/SC can cross-link the same request.
128
128
 
@@ -198,15 +198,17 @@ peaks request show <request-id> --role prd --project <repo> --json
198
198
 
199
199
  # 5.5 — write the immutable handoff (sha256-locked; BLOCKING before RD/QA handoff)
200
200
  # Reads the PRD request artifact body, computes sha256, writes a v2.11.0 handoff
201
- # at .peaks/_runtime/<sid>/prd/handoff.md that downstream RD + QA + 4 audit
202
- # sub-agents consume as the authoritative source of truth. Dry-run by default
203
- # (omit --apply) so the operator can review before commit.
201
+ # at .peaks/_runtime/<sid>/prd/handoff-<rid>.md that downstream RD + QA + 4 audit
202
+ # sub-agents consume as the authoritative source of truth. The rid is part of
203
+ # the filename: one capsule per SLICE, so a second slice in the same session
204
+ # cannot overwrite the first's. Dry-run by default (omit --apply) so the
205
+ # operator can review before commit.
204
206
  peaks prd handoff init \
205
207
  --rid <request-id> --sid <session-id> --change-id <change-id> \
206
208
  --body "@.peaks/_runtime/<session-id>/prd/requests/<request-id>.md" \
207
209
  --goals <G-ids> --ac <AC-ids> --preserve <P-ids> \
208
210
  [--project <repo>] [--apply]
209
- peaks prd handoff verify --path .peaks/_runtime/<session-id>/prd/handoff.md
211
+ peaks prd handoff verify --path .peaks/_runtime/<session-id>/prd/handoff-<request-id>.md
210
212
 
211
213
  peaks skill presence:clear --project <repo> # handoff complete, remove presence indicator
212
214
  ```
@@ -104,9 +104,9 @@ Project-level security + perf plans live at `.peaks/_runtime/<sessionId>/qa/secu
104
104
 
105
105
  ## QA fan-out (业务 only — v2.11.0 D1)
106
106
 
107
- When peaks-qa is the **main loop** (i.e. it is the active skill and is about to run its own sub-agent dispatch, rather than being a sub-agent itself), it fans out only the **business verification** sub-agent: `qa-business`. Security and performance review are **NOT** peaks-qa's responsibility in v2.11.0 — they are owned by peaks-rd's 4-way audit fan-out (code-review + security-review + perf-baseline + karpathy-review) and the rd-side evidence files (`rd/security-review.md`, `rd/perf-baseline.md`). peaks-qa reads those files by reference; it does NOT re-do them.
107
+ When peaks-qa is the **main loop** (i.e. it is the active skill and is about to run its own sub-agent dispatch, rather than being a sub-agent itself), it fans out only the **business verification** sub-agent: `qa-business`. Security and performance review are **NOT** peaks-qa's responsibility in v2.11.0 — they are owned by peaks-rd's 4-way audit fan-out (code-review + security-review + perf-baseline + karpathy-review) and the rd-side evidence files (`audit/security-<rid>.md`, `audit/perf-<rid>.md`). peaks-qa reads those files by reference; it does NOT re-do them.
108
108
 
109
- > **v2.15.0+ 校准:** `qa-business` 只跑业务/产品视角的 6 项验收清单(业务流程 / 需求覆盖 / 边界 case / UI 装配 / 异常态语调 / 能上线吗),**不跑技术指标**(覆盖率 / 性能 / 安全)。技术指标由 RD 4-way fan-out 自决,QA 只读 `rd/security-review.md` + `rd/perf-baseline.md`。详见 `.peaks/memory/peaks-loop-slice-review-and-qa-perspective.md`。
109
+ > **v2.15.0+ 校准:** `qa-business` 只跑业务/产品视角的 6 项验收清单(业务流程 / 需求覆盖 / 边界 case / UI 装配 / 异常态语调 / 能上线吗),**不跑技术指标**(覆盖率 / 性能 / 安全)。技术指标由 RD 4-way fan-out 自决,QA 只读 `audit/security-<rid>.md` + `audit/perf-<rid>.md`。详见 `.peaks/memory/peaks-loop-slice-review-and-qa-perspective.md`。
110
110
 
111
111
  If the PRD or project warrants it, subdivide `qa-business` further into roles like `qa-business-api` / `qa-business-frontend` / `qa-business-regression`. Subdivision must stay ≤ 2 levels deep (RL-4).
112
112
 
@@ -128,7 +128,7 @@ When this skill is running in the main Claude session (not as a sub-agent), befo
128
128
  - verify API behavior and frontend behavior when either surface exists;
129
129
  - generate a validation report with commands, browser evidence, findings, and residual risks.
130
130
 
131
- **Out of scope (v2.11.0 D1/D4):** peaks-qa does **not** own security review or performance review. Those are owned by peaks-rd's audit fan-out (sub-agents `security-review` and `perf-baseline`) and the rd-side evidence files. peaks-qa reads `rd/security-review.md` and `rd/perf-baseline.md` by reference; it does NOT produce `qa/security-findings.md` or `qa/performance-findings.md` of its own.
131
+ **Out of scope (v2.11.0 D1/D4):** peaks-qa does **not** own security review or performance review. Those are owned by peaks-rd's audit fan-out (sub-agents `security-review` and `perf-baseline`) and the rd-side evidence files. peaks-qa reads `audit/security-<rid>.md` and `audit/perf-<rid>.md` by reference; it does NOT produce `qa/security-findings.md` or `qa/performance-findings.md` of its own.
132
132
 
133
133
  ## Mandatory per-request artifact
134
134
 
@@ -148,7 +148,7 @@ See `references/qa-runbook.md` for the full 10-step runbook (steps #0–#9) with
148
148
 
149
149
  You cannot declare a phase complete from memory. CLI enforcement: the gates below are ALSO enforced by `peaks request transition`, which fails with `code: PREREQUISITES_MISSING` if any are absent. Per-type required files: feature / refactor → test-cases + test-reports + security-findings + performance-findings; bugfix → test-cases + test-reports + security-findings (perf optional); config → security-findings only; docs / chore → none.
150
150
 
151
- Gate index: A (test-cases), A2 (tests executed), A3 (security reference — v2.11.0: read rd/security-review.md), A4 (performance reference — v2.11.0: read rd/perf-baseline.md), B (test-reports with results), C (all 3 QA files present before verdict: test-cases + test-reports + requests — security/perf evidence live under rd/), D (browser screenshots), E (acceptance coverage scan), F (QA artifact lint).
151
+ Gate index: A (test-cases), A2 (tests executed), A3 (security reference — v2.12.0+: read audit/security-<rid>.md), A4 (performance reference — v2.12.0+: read audit/perf-<rid>.md), B (test-reports with results), C (all 3 QA files present before verdict: test-cases + test-reports + requests — security/perf evidence live under audit/), D (browser screenshots), E (acceptance coverage scan), F (QA artifact lint).
152
152
 
153
153
  → see `references/qa-transition-gates.md` for the full per-gate contract + `ls` / `grep` shell snippets.
154
154
 
@@ -194,7 +194,7 @@ Every QA invocation must produce a test-report artifact at `.peaks/_runtime/<ses
194
194
 
195
195
  ## Mandatory validation gates
196
196
 
197
- QA cannot pass a change until the report contains evidence for every applicable gate. The 9 gates (0 test-case generation, 1 test-report, 2 unit tests, 3 API validation, 4 frontend browser validation, 5 browser-error feedback loop, 8 library version regressions, 9 validation report, 10 acceptance coverage) are mapped to Peaks-Loop Gates A/A2/B/C/D/E/F. **v2.11.0 D1/D4 trim:** Gates A3 (security) and A4 (performance) are no longer peaks-qa's responsibility — security review and performance baseline live under peaks-rd's audit fan-out (rd/security-review.md + rd/perf-baseline.md) and are cited by reference from the test report.
197
+ QA cannot pass a change until the report contains evidence for every applicable gate. The 9 gates (0 test-case generation, 1 test-report, 2 unit tests, 3 API validation, 4 frontend browser validation, 5 browser-error feedback loop, 8 library version regressions, 9 validation report, 10 acceptance coverage) are mapped to Peaks-Loop Gates A/A2/B/C/D/E/F. **v2.11.0 D1/D4 trim:** Gates A3 (security) and A4 (performance) are no longer peaks-qa's responsibility — security review and performance baseline live under peaks-rd's audit fan-out (audit/security-<rid>.md + audit/perf-<rid>.md) and are cited by reference from the test report.
198
198
 
199
199
  If Playwright MCP is unavailable, the LLM checks its own tool list for the Playwright MCP server entry; if absent, the LLM tells the user the install command (`claude mcp add playwright -- npx @playwright/mcp@latest` for Claude Code) and marks the gate blocked with the missing capability. Screenshots, logs, manual steps, or other tools must not substitute for the mandatory frontend browser gate. Do not silently downgrade frontend validation to API-only testing.
200
200
 
@@ -36,8 +36,8 @@ peaks openspec validate <change-id> --project <repo> --prefer-external --json
36
36
  # 5. EXECUTE tests against the actual implementation — Peaks-Loop Gate A2
37
37
  # Run the project test command. Record output. Tests on paper are worthless.
38
38
  # NOTE (v2.11.0 D1/D4): Security review + performance check are NOT run by peaks-qa.
39
- # They are owned by peaks-rd's audit fan-out and surface as rd/security-review.md and
40
- # rd/perf-baseline.md under .peaks/_runtime/<sessionId>/rd/. Read them by reference;
39
+ # They are owned by the independent audit skills and surface as audit/security-<rid>.md
40
+ # and audit/perf-<rid>.md under .peaks/_runtime/<sessionId>/audit/. Read them by reference;
41
41
  # do NOT re-do them or create qa/security-findings.md / qa/performance-findings.md.
42
42
 
43
43
  # 6. write test-report — MANDATORY, write to .peaks/_runtime/<sessionId>/qa/test-reports/<request-id>.md
@@ -11,7 +11,7 @@
11
11
  | config | (none) | `qa/test-reports/<rid>.md` |
12
12
  | docs / chore | (none) | (none) |
13
13
 
14
- Security and performance evidence surface under `rd/security-review.md` and `rd/perf-baseline.md` (peaks-rd's audit fan-out) and are referenced by reference from the QA test report body. The pre-v2.11.0 `qa/security-findings.md` / `qa/performance-findings.md` files are no longer required; existing ones are kept for auditability but ignored by the gate.
14
+ Security and performance evidence surface under `audit/security-<rid>.md` and `audit/perf-<rid>.md` (the independent `peaks-security-audit` / `peaks-perf-audit` skills, v2.12.0+; slice `2026-09-14-audit-artifact-rid-scoping` put the rid in the filename so two slices in one session cannot overwrite each other's evidence) and are referenced by reference from the QA test report body. The older `rd/security-review.md` / `rd/perf-baseline.md` and the ridless `audit/security.md` / `audit/perf.md` remain accepted back-compat tiers. The pre-v2.11.0 `qa/security-findings.md` / `qa/performance-findings.md` files are no longer required; existing ones are kept for auditability but ignored by the gate.
15
15
 
16
16
  **Peaks-Loop Gate A — After test-case generation:**
17
17
  ```bash
@@ -29,18 +29,18 @@ npx vitest run --changed --reporter=verbose 2>&1 | tail -30
29
29
 
30
30
  **Peaks-Loop Gate A3 — Security review referenced (v2.11.0 D1/D4: read-only reference, NOT a separate QA file):**
31
31
  ```bash
32
- # peaks-qa does NOT own a qa/security-findings.md. peaks-rd's audit fan-out
33
- # produces rd/security-review.md; QA references it by path in the test report body.
34
- grep -E "rd/security-review\\.md|security-review" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
32
+ # peaks-qa does NOT own a qa/security-findings.md. The security audit produces
33
+ # audit/security-<rid>.md; QA references it by path in the test report body.
34
+ grep -E "audit/security|rd/security-review|security-review" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
35
35
  # Expected: at least one reference to the rd-side security review.
36
36
  # Empty → BLOCKED: the test report must cite where security evidence lives.
37
37
  ```
38
38
 
39
39
  **Peaks-Loop Gate A4 — Performance baseline referenced (v2.11.0 D1/D4: read-only reference, NOT a separate QA file):**
40
40
  ```bash
41
- # peaks-qa does NOT own a qa/performance-findings.md. peaks-rd's audit fan-out
42
- # produces rd/perf-baseline.md; QA references it by path in the test report body.
43
- grep -E "rd/perf-baseline\\.md|perf-baseline" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
41
+ # peaks-qa does NOT own a qa/performance-findings.md. The perf audit produces
42
+ # audit/perf-<rid>.md; QA references it by path in the test report body.
43
+ grep -E "audit/perf|rd/perf-baseline|perf-baseline" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
44
44
  # Expected: at least one reference to the rd-side perf baseline.
45
45
  # Empty → BLOCKED: the test report must cite where perf evidence lives.
46
46
  ```