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.
- package/CHANGELOG.md +24 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.js +48 -8
- package/dist/cli/commands/compact-command.js +112 -0
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +12 -0
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/request-commands.js +8 -0
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/index.js +5 -45
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +119 -0
- package/dist/services/code/auto-compact-lifecycle.js +169 -0
- package/dist/services/code/auto-compact-orchestrator.js +13 -2
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/ide/ide-registry.d.ts +19 -0
- package/dist/services/ide/ide-registry.js +21 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +81 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +9 -0
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
- package/dist/services/skills/hooks-settings-service.js +14 -4
- package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
- package/dist/services/skills/session-start-hook-constants.js +45 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +29 -11
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
- package/package.json +6 -6
- package/skills/bee/peaks-prd/SKILL.md +7 -5
- package/skills/bee/peaks-qa/SKILL.md +5 -5
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +8 -6
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- 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
|
-
{
|
|
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
|
|
246
|
-
//
|
|
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
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
const candidates =
|
|
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
|
|
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 (
|
|
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
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
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 {
|
|
15
|
+
import { readArtifactState } from '../artifacts/request-artifact-state-helpers.js';
|
|
15
16
|
export function extractState(markdown) {
|
|
16
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
217
|
+
const hit = candidates.find((candidate) => existsSync(candidate.path));
|
|
218
|
+
if (hit !== undefined) {
|
|
111
219
|
anyEvidenceResolved = true;
|
|
112
|
-
if (
|
|
220
|
+
if (hit.legacyRoot)
|
|
113
221
|
allResolvedPathsCanonical = false;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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} (${
|
|
123
|
-
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}
|
|
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
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
282
|
+
const hit = candidates.find((candidate) => existsSync(candidate.path));
|
|
283
|
+
if (hit !== undefined) {
|
|
178
284
|
anyEvidenceResolved = true;
|
|
179
|
-
if (
|
|
285
|
+
if (hit.legacyRoot)
|
|
180
286
|
allResolvedPathsCanonical = false;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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} (${
|
|
190
|
-
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}
|
|
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 };
|