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
@@ -8,6 +8,7 @@ import { isDirectory } from 'peaks-loop-shared/fs';
8
8
  // is no longer read. Path-safety helpers now live at
9
9
  // `shared/path-safety.ts` if this module ever needs them.
10
10
  import { verifyPipeline } from '../workflow/pipeline-verify-service.js';
11
+ import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
11
12
  import { findMockViolations } from '../audit/enforcers/mock-placement.js';
12
13
  import { runRedLinesAudit } from '../audit/red-lines-service.js';
13
14
  /**
@@ -239,20 +240,29 @@ async function runUnitTests(projectRoot, runTests) {
239
240
  }
240
241
  };
241
242
  }
243
+ // Slice `2026-09-14-audit-artifact-rid-scoping`: the evidence filenames
244
+ // carry the rid now. Candidate order is canonical-first, then the
245
+ // back-compat tiers — the same order `artifact-prerequisites.ts` resolves
246
+ // in, so this boundary gate and the transition gate agree on which file is
247
+ // a slice's evidence. `<rid>` is substituted at probe time.
242
248
  const REVIEW_FILES = [
243
- { name: 'code-review', path: 'rd/code-review.md', label: 'code-review' },
249
+ {
250
+ name: 'code-review',
251
+ paths: ['rd/code-review-<rid>.md', 'rd/code-review.md'],
252
+ label: 'code-review'
253
+ },
244
254
  // v2.12.0 collapse: security + perf moved to standalone audit skills.
245
- // `slice check` accepts EITHER the v2.11.x legacy path OR the v2.12.0
246
- // canonical audit path during the 1-minor-release back-compat window
247
- // (v2.13.0 hard-deletes the legacy paths — see CHANGELOG [2.12.0]).
255
+ // `slice check` accepts the rid-scoped audit path, the bare v2.12.0 path
256
+ // OR the v2.11.x legacy path during the 1-minor-release back-compat
257
+ // window (v2.13.0 hard-deletes the legacy paths — see CHANGELOG [2.12.0]).
248
258
  {
249
259
  name: 'security-review',
250
- paths: ['audit/security.md', 'rd/security-review.md'],
260
+ paths: ['audit/security-<rid>.md', 'audit/security.md', 'rd/security-review.md'],
251
261
  label: 'security-review'
252
262
  },
253
263
  {
254
264
  name: 'perf-baseline',
255
- paths: ['audit/perf.md', 'rd/perf-baseline.md'],
265
+ paths: ['audit/perf-<rid>.md', 'audit/perf.md', 'rd/perf-baseline.md'],
256
266
  label: 'perf-baseline'
257
267
  }
258
268
  ];
@@ -284,11 +294,11 @@ async function runReviewFanout(projectRoot, rid, refresh) {
284
294
  const found = [];
285
295
  for (const review of REVIEW_FILES) {
286
296
  let hit = null;
287
- // v2.12.0 back-compat: each entry may list multiple candidate paths.
288
- // First hit (in declared order) wins; canonical paths come first so
289
- // v2.12.0+ slices preferentially report the new path even when both
290
- // are present during migration.
291
- const candidates = 'path' in review ? [review.path] : review.paths;
297
+ // Back-compat: each entry lists multiple candidate paths. First hit (in
298
+ // declared order) wins; canonical paths come first so a current slice
299
+ // preferentially reports its own rid-scoped evidence even when a legacy
300
+ // file is also present during migration.
301
+ const candidates = review.paths.map((candidate) => candidate.replace('<rid>', rid));
292
302
  for (const candidate of candidates) {
293
303
  for (const scope of scopes) {
294
304
  const abs = join(projectRoot, '.peaks', scope, candidate);
@@ -367,6 +377,14 @@ export async function sliceCheck(options) {
367
377
  throw new Error('No --rid supplied. Pass --rid <id> on the CLI to identify which slice to check.');
368
378
  }
369
379
  const rid = options.rid;
380
+ // The rid becomes a filename in `runReviewFanout`'s candidate list and a
381
+ // directory segment in its `scopes` — both joined onto the project root. The
382
+ // rid is the repo's own request-id shape, and `request-artifact-service.ts`
383
+ // already refuses anything else; apply the same guard here rather than
384
+ // probing paths a hostile `--rid` steers.
385
+ if (!REQUEST_ID_PATTERN.test(rid)) {
386
+ throw new Error(`Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`);
387
+ }
370
388
  const totalStart = Date.now();
371
389
  const stages = [];
372
390
  let unitTestsRunMode = 'skipped';
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
16
  import { dirname, join, resolve } from 'node:path';
17
+ import { isUnsafePathInput } from '../../shared/path-safety.js';
17
18
  /** The 5 default review items per slice (the 12 Gaps memory checklist). */
18
19
  export const DEFAULT_REVIEW_ITEMS = [
19
20
  { id: 'business-match', question: '这个 slice 做完,业务流程对吗?(跟产品最初给的需求匹配)' },
@@ -32,6 +33,13 @@ export function buildEmptySliceReview(sliceId, sessionId, now = new Date()) {
32
33
  };
33
34
  }
34
35
  export function getReviewDir(projectRoot, sessionId) {
36
+ // Sid axis. Every `peaks slice-review|score|accept|reject` subcommand reaches
37
+ // the runtime tree through this one constructor. Measured: `--session-id
38
+ // ../../../../…/PWNED` wrote `slice-reviews/<slice-id>.json` outside every
39
+ // project root under an `ok: true` envelope (RD sweep case A26).
40
+ if (isUnsafePathInput(sessionId)) {
41
+ throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
42
+ }
35
43
  return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'slice-reviews');
36
44
  }
37
45
  export function getReviewPath(projectRoot, sessionId, sliceId) {
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * File budget: ≤ 400 lines (rid-006 split).
10
10
  */
11
- import type { RequestType } from '../artifacts/artifact-prerequisites.js';
11
+ import { type ArtifactPrerequisite, type RequestType } from '../artifacts/artifact-prerequisites.js';
12
+ import type { RequestArtifactRole, RequestArtifactState } from '../artifacts/request-artifact-service.js';
12
13
  import type { PipelineGate } from './pipeline-verify-types.js';
13
14
  export declare function extractState(markdown: string): string;
14
15
  /**
@@ -24,6 +25,38 @@ export declare function findRequestFile(projectRoot: string, role: string, rid:
24
25
  content: string;
25
26
  sessionId: string;
26
27
  } | null>;
28
+ /**
29
+ * Where the CURRENT contract puts one gate's evidence.
30
+ *
31
+ * The contract is the `artifact-prerequisites.ts` table — the table
32
+ * `peaks request transition` enforces and the one the peaks-qa prose
33
+ * contract mirrors (`skills/bee/peaks-qa/references/qa-transition-gates.md`).
34
+ * `probeName` is the location this checker probes today. The contract's
35
+ * `relativePath` is returned first, then its accepted historical locations in
36
+ * the table's own declared order — `legacyRelativePath`, then
37
+ * `legacyRelativePaths` (the same composition `artifact-prerequisites.ts`' own
38
+ * resolver performs, so a hit here and a hit there land on the same file).
39
+ *
40
+ * Returns `null` when the contract carries no prerequisite for that name at
41
+ * all — the artifact was retired and no gate may fail on it.
42
+ *
43
+ * This lookup exists because hard-coding the paths here is what let
44
+ * `verify-pipeline` fall a minor release behind the gate: it kept demanding
45
+ * `rd/security-review.md` as mandatory after the table had demoted that file
46
+ * to a `legacyRelativePath` fallback of `audit/security.md`, and it kept
47
+ * demanding `qa/security-findings-<rid>.md` / `qa/performance-findings-<rid>.md`
48
+ * after the v2.11.0 D1/D4 trim dropped them from `qa:verdict-issued`
49
+ * altogether (rid 2026-09-14-verify-pipeline-contract-drift).
50
+ */
51
+ /**
52
+ * The prerequisite the contract carries for `probeName`, or null when it
53
+ * carries none. `contractEvidencePaths` is this object's path projection; the
54
+ * BODY contract (`mustContain` / `mustContainAny` / `headingMustContain`) rides
55
+ * on the same object, which is why `contractBodyViolations` below consults it
56
+ * instead of probing for the file's existence alone.
57
+ */
58
+ export declare function contractPrerequisite(role: RequestArtifactRole, state: RequestArtifactState, requestType: RequestType, probeName: string): ArtifactPrerequisite | null;
59
+ export declare function contractEvidencePaths(role: RequestArtifactRole, state: RequestArtifactState, requestType: RequestType, probeName: string): string[] | null;
27
60
  export declare function rdGatesForType(requestType: RequestType): PipelineGate[];
28
61
  export declare function qaGatesForType(requestType: RequestType): PipelineGate[];
29
62
  export declare const RD_QA_HANDOFF_STATES: Set<string>;
@@ -33,15 +66,19 @@ export interface CanonicalPathTracker {
33
66
  anyEvidenceResolved: boolean;
34
67
  allResolvedPathsCanonical: boolean;
35
68
  }
36
- /** Resolve RD evidence files (tech-doc / bug-analysis / code-review / security-review)
69
+ /** Resolve RD evidence files (prd-handoff / bug-analysis / code-review / security-review / perf-baseline)
37
70
  * by probing canonical + legacy paths for each gate. Mutates the gate.passed /
38
71
  * gate.detail fields in place and pushes violations / nextActions. Returns the
39
72
  * updated tracker. Verbatim-move from `pipeline-verify-service.ts`. */
40
- export declare function resolveRdEvidencePaths(gates: PipelineGate[], rdEvidenceDir: string, projectRoot: string, violations: string[], nextActions: string[], tracker: CanonicalPathTracker): CanonicalPathTracker;
41
- /** Resolve QA evidence files (test-cases / test-report / security-findings /
42
- * performance-findings) by probing canonical + legacy paths for each gate.
43
- * For security/perf findings, delegates to the artifact-paths resolver.
44
- * Mutates the gate.passed / gate.detail fields in place and pushes
45
- * violations / nextActions. Returns the updated tracker. Verbatim-move
46
- * from `pipeline-verify-service.ts`. */
47
- export declare function resolveQaEvidencePaths(gates: PipelineGate[], projectRoot: string, rdEvidenceDir: string, changeIdForResolver: string, rid: string, violations: string[], nextActions: string[], tracker: CanonicalPathTracker): CanonicalPathTracker;
73
+ export declare function resolveRdEvidencePaths(gates: PipelineGate[], rdEvidenceDir: string, projectRoot: string, rid: string, requestType: RequestType, violations: string[], nextActions: string[], tracker: CanonicalPathTracker): CanonicalPathTracker;
74
+ /** Resolve QA evidence files (test-cases / test-report) by probing canonical
75
+ * + legacy paths for each gate. Mutates the gate.passed / gate.detail fields
76
+ * in place and pushes violations / nextActions. Returns the updated tracker.
77
+ * Verbatim-move from `pipeline-verify-service.ts`.
78
+ *
79
+ * The `security-findings` / `performance-findings` branches that used to live
80
+ * here are gone: those artifacts are not in any `qa:verdict-issued` table
81
+ * (v2.11.0 D1/D4), and the evidence they stood for is resolved on the RD side
82
+ * at the contract's own paths (`audit/security-<rid>.md`,
83
+ * `audit/perf-<rid>.md`). */
84
+ export declare function resolveQaEvidencePaths(gates: PipelineGate[], projectRoot: string, rdEvidenceDir: string, requestType: RequestType, rid: string, violations: string[], nextActions: string[], tracker: CanonicalPathTracker): CanonicalPathTracker;
@@ -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 };