peaks-loop 4.0.53 → 4.0.54
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 +49 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/codegraph-commands.js +164 -5
- package/dist/cli/commands/core/memory-command.js +35 -2
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/prd-commands.js +15 -0
- package/dist/cli/commands/sub-agent-shared.d.ts +23 -0
- package/dist/cli/commands/sub-agent-shared.js +39 -0
- package/dist/services/artifacts/artifact-prerequisites.js +34 -0
- package/dist/services/audit/enforcer-liveness.js +1 -1
- package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +44 -0
- package/dist/services/codegraph/codegraph-config-repair-writer.js +112 -13
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +23 -1
- package/dist/services/codegraph/codegraph-exclude-repair.js +6 -0
- package/dist/services/evidence/evidence-generator.js +11 -3
- package/dist/services/memory/project-memory-service/index.d.ts +1 -1
- package/dist/services/memory/project-memory-service/index.js +1 -1
- package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +59 -0
- package/dist/services/memory/project-memory-service/store/atomic-write.js +167 -7
- package/dist/services/prd/gate-evidence-derivation.d.ts +160 -0
- package/dist/services/prd/gate-evidence-derivation.js +270 -0
- package/dist/services/prd/handoff-auto-regen.js +13 -1
- package/dist/services/prd/handoff-frontmatter.d.ts +1 -1
- package/dist/services/prd/handoff-frontmatter.js +62 -0
- package/dist/services/prd/handoff-gate-evidence.d.ts +95 -1
- package/dist/services/prd/handoff-gate-evidence.js +125 -57
- package/dist/services/prd/handoff-service.d.ts +21 -1
- package/dist/services/prd/handoff-service.js +45 -2
- package/dist/services/prd/handoff-types.d.ts +40 -0
- package/dist/services/prd/handoff-types.js +28 -1
- package/dist/services/prd/project-scan-reader.d.ts +7 -0
- package/dist/services/prd/project-scan-reader.js +7 -1
- package/dist/services/rd/reviewer-dispatch-policy.d.ts +36 -8
- package/dist/services/rd/reviewer-dispatch-policy.js +36 -8
- package/package.json +6 -6
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +26 -10
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +1 -1
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +7 -5
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- /package/{docs → contracts}/test-style-contract.md +0 -0
|
@@ -1,77 +1,145 @@
|
|
|
1
1
|
// src/services/prd/handoff-gate-evidence.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// carries a `gateEvidence: string[]` field naming the gate names whose
|
|
5
|
-
// `gateEvidence` is asserted by this handoff. Pre-S2, the field was
|
|
6
|
-
// written into the frontmatter (by `initHandoff`) but NO consumer in
|
|
7
|
-
// `src/` ever READ it back — the field was prose, not data.
|
|
3
|
+
// The SOLE reader of the handoff frontmatter's `gateEvidence` field.
|
|
8
4
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
5
|
+
// WHAT THE FIELD IS (slice B1, rid `rid-b1-gate-evidence-producer`): a map
|
|
6
|
+
// from one of five fixed gate keys to the PATH of that gate's evidence file.
|
|
7
|
+
// The normative description is
|
|
8
|
+
// `skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:35-41`; the
|
|
9
|
+
// key set is `GATE_EVIDENCE_KEYS` in `./handoff-types.js`.
|
|
14
10
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
11
|
+
// WHAT THIS FILE USED TO SAY, AND WHY THAT WAS FALSE: its header claimed the
|
|
12
|
+
// field was a `string[]` of "gate names" and that `initHandoff` wrote it.
|
|
13
|
+
// Neither held. `grep -rn gateEvidence src/` hit this file and nothing else —
|
|
14
|
+
// there was no producer (`HandoffFrontmatter` had no such field,
|
|
15
|
+
// `serializeHandoffFrontmatter` never emitted it, `initHandoff` rejected it)
|
|
16
|
+
// and no consumer. The field was prose describing data that did not exist,
|
|
17
|
+
// and the comment asserting a producer was the reason nobody noticed.
|
|
21
18
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
19
|
+
// WHAT IS ON DISK NOW, stated precisely because a header that overclaims is
|
|
20
|
+
// the defect above. B1 added the producer FUNCTIONS; B2 wired their callers,
|
|
21
|
+
// which is what made the difference — until then every producer passed
|
|
22
|
+
// nothing, so no capsule carried the field and saying otherwise would have
|
|
23
|
+
// described a fact that held only inside tests (F1 of `rid-b1-qa`). At
|
|
24
|
+
// HEAD after B2: the three producers DERIVE the map from the request type
|
|
25
|
+
// (`services/prd/gate-evidence-derivation.ts`), so a capsule written by
|
|
26
|
+
// `peaks prd handoff init`, by the auto-regen on `prd:handed-off`, or by
|
|
27
|
+
// `peaks evidence generate` carries this field whenever that slice's PRD
|
|
28
|
+
// artifact is readable — and carries no `gateEvidence` block at all when it
|
|
29
|
+
// is not. Gate C (`checkPrerequisites` at `rd:qa-handoff`) fails a declared
|
|
30
|
+
// path that is not on disk. Every clause above is asserted end-to-end in
|
|
31
|
+
// `tests/unit/prd/gate-evidence-derivation.test.ts`.
|
|
32
|
+
//
|
|
33
|
+
// WHY IT IS STILL STAND-ALONE: keeping the typed-value logic here, rather
|
|
34
|
+
// than in `handoff-service.ts`, keeps `HandoffFrontmatter` a pure data shape
|
|
35
|
+
// and lets this reader stay PERMISSIVE where `readHandoff` must throw.
|
|
36
|
+
//
|
|
37
|
+
// PERMISSIVE, RE-ARGUED (the old rationale did not survive contact with its
|
|
38
|
+
// own return type): the pre-B1 reader returned `null` for "field absent",
|
|
39
|
+
// "field malformed", "YAML broken" and "file missing" alike. Its stated
|
|
40
|
+
// justification — "a malformed frontmatter is the caller's signal to surface
|
|
41
|
+
// the failure to the operator" — was therefore unimplementable: the caller
|
|
42
|
+
// could not distinguish a handoff that DECLARED NOTHING from one whose
|
|
43
|
+
// declaration was CORRUPT, so it could not tell whether to proceed or to
|
|
44
|
+
// stop. The rationale was a promise the shape could not keep.
|
|
45
|
+
//
|
|
46
|
+
// It holds now, because the shape changed rather than the policy: this reader
|
|
47
|
+
// still never throws (a broken capsule must not crash the gate pipeline, and
|
|
48
|
+
// `readHandoff` already owns the throwing contract for capsules that must be
|
|
49
|
+
// valid), but every outcome is a DISTINCT `status`. Under the map shape the
|
|
50
|
+
// callers that matter are Gate C and the QA role, and both must fail closed on
|
|
51
|
+
// `field-not-map` / `value-not-string` while passing `field-absent` — which is
|
|
52
|
+
// only expressible if those are different values. So: permissive stays, and it
|
|
53
|
+
// is now a decision the return type can actually support.
|
|
27
54
|
import { readFile } from 'node:fs/promises';
|
|
28
55
|
import { parse as parseYaml } from 'yaml';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
import { isGateEvidenceKey } from './handoff-types.js';
|
|
57
|
+
/** Frontmatter is the `---`-fenced YAML block at the top of the file. */
|
|
58
|
+
const FRONTMATTER_FENCE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
59
|
+
function isPlainMap(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
function describeType(value) {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return 'array';
|
|
65
|
+
if (value === null)
|
|
66
|
+
return 'null';
|
|
67
|
+
return typeof value;
|
|
68
|
+
}
|
|
69
|
+
/** Classify a `gateEvidence` VALUE (already located — see the reader for the
|
|
70
|
+
* fence/YAML/presence layers, which are file-level concerns, not shape). */
|
|
71
|
+
export function classifyGateEvidence(value) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return { kind: 'absent' };
|
|
74
|
+
if (!isPlainMap(value))
|
|
75
|
+
return { kind: 'not-map', actualType: describeType(value) };
|
|
76
|
+
const nonStringKeys = Object.keys(value).filter((key) => typeof value[key] !== 'string');
|
|
77
|
+
if (nonStringKeys.length > 0) {
|
|
78
|
+
return { kind: 'value-not-string', keys: nonStringKeys.sort() };
|
|
38
79
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
const evidence = {};
|
|
81
|
+
const unknownKeys = [];
|
|
82
|
+
for (const key of Object.keys(value)) {
|
|
83
|
+
if (isGateEvidenceKey(key)) {
|
|
84
|
+
evidence[key] = value[key];
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
unknownKeys.push(key);
|
|
88
|
+
}
|
|
43
89
|
}
|
|
44
|
-
|
|
45
|
-
return { gateEvidence: null };
|
|
46
|
-
const gateEvidence = parsed['gateEvidence'];
|
|
47
|
-
if (!Array.isArray(gateEvidence))
|
|
48
|
-
return { gateEvidence: null };
|
|
49
|
-
return { gateEvidence };
|
|
90
|
+
return { kind: 'ok', evidence, unknownKeys: unknownKeys.sort() };
|
|
50
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Read + classify the `gateEvidence` map out of `filePath`. Never throws.
|
|
94
|
+
* See the header for the permissive contract and
|
|
95
|
+
* `HandoffGateEvidenceResult` for the outcomes.
|
|
96
|
+
*/
|
|
51
97
|
export async function readHandoffGateEvidence(filePath) {
|
|
52
|
-
if (!existsSync(filePath))
|
|
53
|
-
return null;
|
|
54
98
|
let raw;
|
|
55
99
|
try {
|
|
56
100
|
raw = await readFile(filePath, 'utf8');
|
|
57
101
|
}
|
|
58
|
-
catch (
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
catch (error) {
|
|
103
|
+
// N1 of `handoff-service.verifyHandoff` is the same lesson one layer up:
|
|
104
|
+
// folding every read failure into `file-missing` sends an operator to
|
|
105
|
+
// look for a file that is right there. Only a genuine absence is
|
|
106
|
+
// `file-missing`.
|
|
107
|
+
const code = error.code;
|
|
108
|
+
if (code === 'ENOENT')
|
|
109
|
+
return { status: 'file-missing' };
|
|
110
|
+
return { status: 'read-error', reason: `${code ?? 'unknown'}` };
|
|
111
|
+
}
|
|
112
|
+
const match = FRONTMATTER_FENCE.exec(raw);
|
|
113
|
+
if (!match) {
|
|
114
|
+
return { status: 'frontmatter-malformed', reason: 'no-frontmatter-fence' };
|
|
62
115
|
}
|
|
116
|
+
let parsed;
|
|
63
117
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
parsed = parseYaml(match[1] ?? '');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
return { status: 'frontmatter-malformed', reason: `yaml-parse-error: ${message}` };
|
|
123
|
+
}
|
|
124
|
+
if (!isPlainMap(parsed)) {
|
|
125
|
+
return { status: 'frontmatter-malformed', reason: 'frontmatter-not-a-map' };
|
|
71
126
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
// `in`, not `!== undefined`: `gateEvidence:` with an empty value parses to
|
|
128
|
+
// `null`, which IS a (broken) declaration and must not be reported as if
|
|
129
|
+
// the author had written nothing.
|
|
130
|
+
if (!('gateEvidence' in parsed))
|
|
131
|
+
return { status: 'field-absent' };
|
|
132
|
+
// The shape rule is shared with `handoff-service.isHandoffFrontmatter` — see
|
|
133
|
+
// `classifyGateEvidence`. This reader only decides how to REPORT it.
|
|
134
|
+
const shape = classifyGateEvidence(parsed['gateEvidence']);
|
|
135
|
+
switch (shape.kind) {
|
|
136
|
+
case 'absent':
|
|
137
|
+
return { status: 'field-absent' };
|
|
138
|
+
case 'not-map':
|
|
139
|
+
return { status: 'field-not-map', actualType: shape.actualType };
|
|
140
|
+
case 'value-not-string':
|
|
141
|
+
return { status: 'value-not-string', keys: shape.keys };
|
|
142
|
+
case 'ok':
|
|
143
|
+
return { status: 'ok', evidence: shape.evidence, unknownKeys: shape.unknownKeys };
|
|
76
144
|
}
|
|
77
145
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the body content as UTF-8 bytes. The body MUST be the literal
|
|
23
23
|
* markdown source — no normalization, no trailing-newline padding.
|
|
24
24
|
*/
|
|
25
|
-
import type { Handoff, HandoffProbe } from './handoff-types.js';
|
|
25
|
+
import type { GateEvidence, Handoff, HandoffProbe } from './handoff-types.js';
|
|
26
26
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
27
27
|
export declare function sha256OfBody(body: string): string;
|
|
28
28
|
/**
|
|
@@ -74,6 +74,26 @@ export declare function initHandoff(opts: {
|
|
|
74
74
|
preservedBehavior: readonly string[];
|
|
75
75
|
/** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff-<rid>.md`. */
|
|
76
76
|
handoffPath?: string;
|
|
77
|
+
/** Paths to the gate evidence files, keyed by gate. Omitted field and
|
|
78
|
+
* empty map are equivalent here: neither renders a `gateEvidence` block,
|
|
79
|
+
* so a caller that declares nothing writes bytes identical to a pre-B1
|
|
80
|
+
* capsule.
|
|
81
|
+
*
|
|
82
|
+
* THIS IS NOT A SECOND SOURCE (B2). In production the value reaching this
|
|
83
|
+
* parameter is always `deriveGateEvidence(...)`
|
|
84
|
+
* (`services/prd/gate-evidence-derivation.ts`), which computes it from the
|
|
85
|
+
* request type and the gate table. This function stays pure — it does not
|
|
86
|
+
* read the type itself — so the map can be constructed without touching
|
|
87
|
+
* the disk; that is why the parameter exists rather than an internal call.
|
|
88
|
+
*
|
|
89
|
+
* SHAPE CONTRACT (F2 of `rid-b1-qa`, now enforced in both directions): a
|
|
90
|
+
* map this accepts is EXACTLY what `readHandoffGateEvidence` reports as
|
|
91
|
+
* `evidence` — `classifyGateEvidence` is the one shape rule behind both,
|
|
92
|
+
* and `parseHandoffContent` stores the classified map rather than the raw
|
|
93
|
+
* YAML. A declaration both paths REFUSE (`not-map` / `value-not-string`)
|
|
94
|
+
* is refused here by a throw and there by a status; neither silently
|
|
95
|
+
* produces a map the other cannot. */
|
|
96
|
+
gateEvidence?: GateEvidence;
|
|
77
97
|
}): Handoff;
|
|
78
98
|
/** Write a Handoff to disk. `projectRoot` is the absolute project
|
|
79
99
|
* root (so the `.peaks/_runtime/...` path is resolved absolutely).
|
|
@@ -30,6 +30,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
30
30
|
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
31
31
|
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
32
32
|
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
33
|
+
import { classifyGateEvidence } from './handoff-gate-evidence.js';
|
|
33
34
|
/** Required schema version for new handoffs. */
|
|
34
35
|
const HANDOFF_SCHEMA_VERSION = '2';
|
|
35
36
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
@@ -139,6 +140,12 @@ export function initHandoff(opts) {
|
|
|
139
140
|
acceptanceCriteria: [...opts.acceptanceCriteria],
|
|
140
141
|
preservedBehavior: [...opts.preservedBehavior],
|
|
141
142
|
handoffPath,
|
|
143
|
+
// Conditionally spread rather than `gateEvidence: undefined`: this
|
|
144
|
+
// tsconfig sets `exactOptionalPropertyTypes`, and an explicit `undefined`
|
|
145
|
+
// would also make `serializeHandoffFrontmatter`'s
|
|
146
|
+
// `frontmatter.gateEvidence` key present-but-undefined for every caller
|
|
147
|
+
// that declares nothing.
|
|
148
|
+
...(opts.gateEvidence === undefined ? {} : { gateEvidence: opts.gateEvidence }),
|
|
142
149
|
};
|
|
143
150
|
return { frontmatter, body: opts.body };
|
|
144
151
|
}
|
|
@@ -233,8 +240,21 @@ function parseHandoffContent(content) {
|
|
|
233
240
|
// frontmatter is returned with the canonical `'2'` rather than the raw scalar
|
|
234
241
|
// — otherwise every downstream `=== '2'` comparison would depend on which
|
|
235
242
|
// producer wrote the file.
|
|
243
|
+
//
|
|
244
|
+
// F2: `gateEvidence` is normalized the same way, for the same reason. The
|
|
245
|
+
// raw YAML value is not returned: the CLASSIFIED map is. Unknown keys are
|
|
246
|
+
// not part of `GateEvidence`, so storing the raw object made the interface
|
|
247
|
+
// claim a five-key map while holding a six-key one — and made
|
|
248
|
+
// `frontmatter.gateEvidence` disagree with what the field's reader reports
|
|
249
|
+
// for the same bytes. The predicate above has already rejected every shape
|
|
250
|
+
// that is not `absent` or `ok`, so this spread only ever adds a clean map.
|
|
251
|
+
const gateEvidenceShape = classifyGateEvidence(parsed.gateEvidence);
|
|
236
252
|
return {
|
|
237
|
-
frontmatter: {
|
|
253
|
+
frontmatter: {
|
|
254
|
+
...parsed,
|
|
255
|
+
schemaVersion: HANDOFF_SCHEMA_VERSION,
|
|
256
|
+
...(gateEvidenceShape.kind === 'ok' ? { gateEvidence: gateEvidenceShape.evidence } : {})
|
|
257
|
+
},
|
|
238
258
|
body
|
|
239
259
|
};
|
|
240
260
|
}
|
|
@@ -296,5 +316,28 @@ function isHandoffFrontmatter(value) {
|
|
|
296
316
|
Array.isArray(v.goals) &&
|
|
297
317
|
Array.isArray(v.acceptanceCriteria) &&
|
|
298
318
|
Array.isArray(v.preservedBehavior) &&
|
|
299
|
-
typeof v.handoffPath === 'string'
|
|
319
|
+
typeof v.handoffPath === 'string' &&
|
|
320
|
+
isGateEvidence(v.gateEvidence));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* B1: `gateEvidence` is optional, but when present it MUST be a map of
|
|
324
|
+
* strings — an ARRAY here is the pre-B1 shape its own test file used to
|
|
325
|
+
* write, and it is a broken declaration, not a claim.
|
|
326
|
+
*
|
|
327
|
+
* F2 of `rid-b1-qa` removed this function's own copy of the shape rule. It
|
|
328
|
+
* used to be a second predicate (same boundary, different downstream result)
|
|
329
|
+
* that let the same bytes read one way here and another way through
|
|
330
|
+
* `readHandoffGateEvidence`; both now ask `classifyGateEvidence`. Only
|
|
331
|
+
* `absent` and `ok` are accepted: a malformed declaration must be refused by
|
|
332
|
+
* `readHandoff` (which is documented to throw on malformed input) exactly as
|
|
333
|
+
* the reader refuses it, so the two can never disagree about the same file.
|
|
334
|
+
*
|
|
335
|
+
* Unknown keys are still accepted HERE — the map is normalized to the five
|
|
336
|
+
* known keys by the caller — because refusing them would make a typo render
|
|
337
|
+
* a whole capsule unreadable, and the reader's `unknownKeys` is the surface
|
|
338
|
+
* that makes the typo diagnosable instead.
|
|
339
|
+
*/
|
|
340
|
+
function isGateEvidence(value) {
|
|
341
|
+
const kind = classifyGateEvidence(value).kind;
|
|
342
|
+
return kind === 'absent' || kind === 'ok';
|
|
300
343
|
}
|
|
@@ -24,6 +24,39 @@
|
|
|
24
24
|
* IDs in frontmatter). Every consumer MUST refuse a handoff whose
|
|
25
25
|
* frontmatter is missing or carries `schemaVersion !== '2'`. */
|
|
26
26
|
export type HandoffSchemaVersion = '2';
|
|
27
|
+
/**
|
|
28
|
+
* The FIVE fixed `gateEvidence` keys, in canonical serialization order.
|
|
29
|
+
*
|
|
30
|
+
* This array is the single source of truth for the key set: the
|
|
31
|
+
* `GateEvidenceKey` type is derived from it, the serializer iterates it,
|
|
32
|
+
* and the reader matches against it. Slice B1
|
|
33
|
+
* (`2026-09-17-4-0-51-cleanup`) collapsed four mutually contradictory
|
|
34
|
+
* descriptions of this field (a `string[]` in the reader's code comment,
|
|
35
|
+
* a `string[]` of PATHS in its tests, a map of paths in
|
|
36
|
+
* `skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:35-41`)
|
|
37
|
+
* onto the documented map. Do NOT re-spell these five literals anywhere
|
|
38
|
+
* else — a second copy is how the four descriptions diverged.
|
|
39
|
+
*/
|
|
40
|
+
export declare const GATE_EVIDENCE_KEYS: readonly ["projectScan", "prdHandoff", "codeReview", "securityReview", "perfBaseline"];
|
|
41
|
+
/** One of the five `gateEvidence` keys. Derived, never re-spelled. */
|
|
42
|
+
export type GateEvidenceKey = (typeof GATE_EVIDENCE_KEYS)[number];
|
|
43
|
+
/**
|
|
44
|
+
* Is `key` one of the five? THE membership test — the serializer's
|
|
45
|
+
* unknown-key check, the reader's classifier and the shape predicate all ask
|
|
46
|
+
* this one function, so no caller can disagree about what the key set is.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isGateEvidenceKey(key: string): key is GateEvidenceKey;
|
|
49
|
+
/**
|
|
50
|
+
* `gateEvidence` — a partial map from gate key to the PATH of that gate's
|
|
51
|
+
* evidence file. Partial because a slice may legitimately not have run
|
|
52
|
+
* every gate; the consumer (Gate C, wired in slice B2) fails on a missing
|
|
53
|
+
* key, which is where "did you declare it?" is decided — NOT here.
|
|
54
|
+
*
|
|
55
|
+
* Unknown keys are not expressible in this type. The reader still reports
|
|
56
|
+
* them at runtime (`unknownKeys`) so a typo is diagnosable rather than
|
|
57
|
+
* silently dropped.
|
|
58
|
+
*/
|
|
59
|
+
export type GateEvidence = Readonly<Partial<Record<GateEvidenceKey, string>>>;
|
|
27
60
|
/**
|
|
28
61
|
* Handoff frontmatter — the structured header block between the
|
|
29
62
|
* leading and trailing `---` fences. `readonly` enforces D1's
|
|
@@ -47,6 +80,13 @@ export interface HandoffFrontmatter {
|
|
|
47
80
|
readonly preservedBehavior: readonly string[];
|
|
48
81
|
/** Absolute path to the handoff file on disk (for re-verify). */
|
|
49
82
|
readonly handoffPath: string;
|
|
83
|
+
/** Paths to the evidence files peaks-qa validates at Gate C, keyed by
|
|
84
|
+
* gate. ABSENT (not `{}`) when the slice declared none — the serializer
|
|
85
|
+
* omits the block entirely rather than emitting an empty map, so old
|
|
86
|
+
* handoffs gain no noise line. Read it through
|
|
87
|
+
* `readHandoffGateEvidence`, which reports WHY it is unusable instead of
|
|
88
|
+
* collapsing every failure to `null`. */
|
|
89
|
+
readonly gateEvidence?: GateEvidence;
|
|
50
90
|
}
|
|
51
91
|
/** Handoff — frontmatter + body. The body is the markdown source of
|
|
52
92
|
* truth that sub-agents and downstream consumers parse. */
|
|
@@ -20,4 +20,31 @@
|
|
|
20
20
|
* `.peaks/_runtime/current-change`). NEVER write under
|
|
21
21
|
* `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The FIVE fixed `gateEvidence` keys, in canonical serialization order.
|
|
25
|
+
*
|
|
26
|
+
* This array is the single source of truth for the key set: the
|
|
27
|
+
* `GateEvidenceKey` type is derived from it, the serializer iterates it,
|
|
28
|
+
* and the reader matches against it. Slice B1
|
|
29
|
+
* (`2026-09-17-4-0-51-cleanup`) collapsed four mutually contradictory
|
|
30
|
+
* descriptions of this field (a `string[]` in the reader's code comment,
|
|
31
|
+
* a `string[]` of PATHS in its tests, a map of paths in
|
|
32
|
+
* `skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:35-41`)
|
|
33
|
+
* onto the documented map. Do NOT re-spell these five literals anywhere
|
|
34
|
+
* else — a second copy is how the four descriptions diverged.
|
|
35
|
+
*/
|
|
36
|
+
export const GATE_EVIDENCE_KEYS = [
|
|
37
|
+
'projectScan',
|
|
38
|
+
'prdHandoff',
|
|
39
|
+
'codeReview',
|
|
40
|
+
'securityReview',
|
|
41
|
+
'perfBaseline'
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Is `key` one of the five? THE membership test — the serializer's
|
|
45
|
+
* unknown-key check, the reader's classifier and the shape predicate all ask
|
|
46
|
+
* this one function, so no caller can disagree about what the key set is.
|
|
47
|
+
*/
|
|
48
|
+
export function isGateEvidenceKey(key) {
|
|
49
|
+
return GATE_EVIDENCE_KEYS.includes(key);
|
|
50
|
+
}
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* errors propagate (no silent failures).
|
|
11
11
|
*/
|
|
12
12
|
import type { BusinessKnowledge, ProjectScan } from './project-scan-types.js';
|
|
13
|
+
/**
|
|
14
|
+
* `.peaks/project-scan/project-scan.md`, project-relative. Exported so the
|
|
15
|
+
* gate-evidence derivation (`gate-evidence-derivation.ts`) can declare this
|
|
16
|
+
* path without spelling the literal a fourth time — it was already inlined in
|
|
17
|
+
* `project-commands.ts`, `workspace/init-command.ts` and here.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PROJECT_SCAN_RELATIVE: string;
|
|
13
20
|
/** Read `.peaks/project-scan/project-scan.md`. Returns `null` when the
|
|
14
21
|
* file or its parent dir is absent (fresh project). Throws on other
|
|
15
22
|
* IO failures or malformed YAML. */
|
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { parse as parseYaml } from 'yaml';
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `.peaks/project-scan/project-scan.md`, project-relative. Exported so the
|
|
17
|
+
* gate-evidence derivation (`gate-evidence-derivation.ts`) can declare this
|
|
18
|
+
* path without spelling the literal a fourth time — it was already inlined in
|
|
19
|
+
* `project-commands.ts`, `workspace/init-command.ts` and here.
|
|
20
|
+
*/
|
|
21
|
+
export const PROJECT_SCAN_RELATIVE = join('.peaks', 'project-scan', 'project-scan.md');
|
|
16
22
|
const BUSINESS_KNOWLEDGE_RELATIVE = join('.peaks', 'project-scan', 'business-knowledge.md');
|
|
17
23
|
/** Read `.peaks/project-scan/project-scan.md`. Returns `null` when the
|
|
18
24
|
* file or its parent dir is absent (fresh project). Throws on other
|
|
@@ -26,12 +26,33 @@
|
|
|
26
26
|
* PRD handoff + the project-scoped audit templates (slice v2.12.0
|
|
27
27
|
* Tier 1+2+3 = Group A).
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* This module holds the machine-readable decision table for that collapse;
|
|
30
|
+
* the prose the LLM runner actually reads is
|
|
31
|
+
* `skills/bee/peaks-rd/references/parallel-review-fanout.md`. It used to
|
|
32
|
+
* describe itself as "consumed by the LLM-side runner", which named no
|
|
33
|
+
* mechanism — an LLM reads prose, not TypeScript exports — and that is the
|
|
34
|
+
* reason every export below sat at zero importers until slice F2.
|
|
35
|
+
*
|
|
36
|
+
* Pinning is half-done. The two test files this block used to cite —
|
|
37
|
+
* `tests/unit/rd/karpathy-skip-on-config-docs-chore.test.ts` (the 5 → 3
|
|
38
|
+
* element pinning) and
|
|
39
|
+
* `tests/unit/rd/deprecated-reviewer-back-compat.test.ts` (the 8
|
|
40
|
+
* back-compat cases) — were both deleted in `f17aa377`. The **predicate
|
|
41
|
+
* half** was re-pinned by `tests/unit/rd/reviewer-dispatch-policy.test.ts`
|
|
42
|
+
* (`7191140f`, 6 cases: `RD_DEPRECATED_REVIEWERS` + `isDeprecatedReviewer`).
|
|
43
|
+
* The **decision-table half** — `RD_FANOUT_REVIEWERS`' 3-element
|
|
44
|
+
* membership, `reviewerListFor`, `karpathySlotIndex`,
|
|
45
|
+
* `shouldDispatchKarpathy` — still has no pin.
|
|
46
|
+
*
|
|
47
|
+
* Slice F2 (rid-f2-ac1-wiring) gave this module its FIRST caller in
|
|
48
|
+
* `src/cli/commands/sub-agent-shared.ts` (`deprecatedReviewerWarnings`),
|
|
49
|
+
* invoked from the dispatch chokepoint in
|
|
50
|
+
* `src/cli/commands/dispatch-commands.ts` and pinned by
|
|
51
|
+
* `tests/unit/cli/sub-agent-dispatch-deprecated-reviewer.test.ts`. Before
|
|
52
|
+
* that, all 13 exports had zero importers across src/ + packages/ +
|
|
53
|
+
* scripts/, so nothing rejected or rerouted `security-reviewer` /
|
|
54
|
+
* `perf-baseline-reviewer` on the way in. That import is still the only
|
|
55
|
+
* one: the other 12 exports below remain unreferenced in this repo.
|
|
35
56
|
*
|
|
36
57
|
* For `config | docs | chore` request types, the slice already skips
|
|
37
58
|
* the entire fanout (SKILL.md line 132 says "Config / docs / chore: no
|
|
@@ -47,8 +68,15 @@
|
|
|
47
68
|
* back-compat window. The `isDeprecatedReviewer(name)` predicate lets
|
|
48
69
|
* dispatchers (or legacy on-disk rd/{security-review,perf-baseline}.md
|
|
49
70
|
* readers) detect a removed slot and route to the new audit skill
|
|
50
|
-
* instead of failing the gate.
|
|
51
|
-
*
|
|
71
|
+
* instead of failing the gate. Tier 5 (`artifact-prerequisites.ts`)
|
|
72
|
+
* holds the matching prereq-side back-compat: `AUDIT_SECURITY` /
|
|
73
|
+
* `AUDIT_PERF` accept the legacy `rd/security-review.md` /
|
|
74
|
+
* `rd/perf-baseline.md` artifacts via `legacyRelativePaths`.
|
|
75
|
+
*
|
|
76
|
+
* Both halves are wired as of slice F2 and both ACCEPT the legacy slot.
|
|
77
|
+
* The dispatch side emits the reroute notice as a warning
|
|
78
|
+
* (`deprecatedReviewerWarnings`) rather than a refusal, precisely so it
|
|
79
|
+
* does not disagree with the prereq side about the same deprecation.
|
|
52
80
|
*/
|
|
53
81
|
export declare const RD_REVIEW_REQUEST_TYPES: readonly ["feat", "bugfix", "refactor", "config", "docs", "chore"];
|
|
54
82
|
export type RdReviewRequestType = (typeof RD_REVIEW_REQUEST_TYPES)[number];
|
|
@@ -26,12 +26,33 @@
|
|
|
26
26
|
* PRD handoff + the project-scoped audit templates (slice v2.12.0
|
|
27
27
|
* Tier 1+2+3 = Group A).
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* This module holds the machine-readable decision table for that collapse;
|
|
30
|
+
* the prose the LLM runner actually reads is
|
|
31
|
+
* `skills/bee/peaks-rd/references/parallel-review-fanout.md`. It used to
|
|
32
|
+
* describe itself as "consumed by the LLM-side runner", which named no
|
|
33
|
+
* mechanism — an LLM reads prose, not TypeScript exports — and that is the
|
|
34
|
+
* reason every export below sat at zero importers until slice F2.
|
|
35
|
+
*
|
|
36
|
+
* Pinning is half-done. The two test files this block used to cite —
|
|
37
|
+
* `tests/unit/rd/karpathy-skip-on-config-docs-chore.test.ts` (the 5 → 3
|
|
38
|
+
* element pinning) and
|
|
39
|
+
* `tests/unit/rd/deprecated-reviewer-back-compat.test.ts` (the 8
|
|
40
|
+
* back-compat cases) — were both deleted in `f17aa377`. The **predicate
|
|
41
|
+
* half** was re-pinned by `tests/unit/rd/reviewer-dispatch-policy.test.ts`
|
|
42
|
+
* (`7191140f`, 6 cases: `RD_DEPRECATED_REVIEWERS` + `isDeprecatedReviewer`).
|
|
43
|
+
* The **decision-table half** — `RD_FANOUT_REVIEWERS`' 3-element
|
|
44
|
+
* membership, `reviewerListFor`, `karpathySlotIndex`,
|
|
45
|
+
* `shouldDispatchKarpathy` — still has no pin.
|
|
46
|
+
*
|
|
47
|
+
* Slice F2 (rid-f2-ac1-wiring) gave this module its FIRST caller in
|
|
48
|
+
* `src/cli/commands/sub-agent-shared.ts` (`deprecatedReviewerWarnings`),
|
|
49
|
+
* invoked from the dispatch chokepoint in
|
|
50
|
+
* `src/cli/commands/dispatch-commands.ts` and pinned by
|
|
51
|
+
* `tests/unit/cli/sub-agent-dispatch-deprecated-reviewer.test.ts`. Before
|
|
52
|
+
* that, all 13 exports had zero importers across src/ + packages/ +
|
|
53
|
+
* scripts/, so nothing rejected or rerouted `security-reviewer` /
|
|
54
|
+
* `perf-baseline-reviewer` on the way in. That import is still the only
|
|
55
|
+
* one: the other 12 exports below remain unreferenced in this repo.
|
|
35
56
|
*
|
|
36
57
|
* For `config | docs | chore` request types, the slice already skips
|
|
37
58
|
* the entire fanout (SKILL.md line 132 says "Config / docs / chore: no
|
|
@@ -47,8 +68,15 @@
|
|
|
47
68
|
* back-compat window. The `isDeprecatedReviewer(name)` predicate lets
|
|
48
69
|
* dispatchers (or legacy on-disk rd/{security-review,perf-baseline}.md
|
|
49
70
|
* readers) detect a removed slot and route to the new audit skill
|
|
50
|
-
* instead of failing the gate.
|
|
51
|
-
*
|
|
71
|
+
* instead of failing the gate. Tier 5 (`artifact-prerequisites.ts`)
|
|
72
|
+
* holds the matching prereq-side back-compat: `AUDIT_SECURITY` /
|
|
73
|
+
* `AUDIT_PERF` accept the legacy `rd/security-review.md` /
|
|
74
|
+
* `rd/perf-baseline.md` artifacts via `legacyRelativePaths`.
|
|
75
|
+
*
|
|
76
|
+
* Both halves are wired as of slice F2 and both ACCEPT the legacy slot.
|
|
77
|
+
* The dispatch side emits the reroute notice as a warning
|
|
78
|
+
* (`deprecatedReviewerWarnings`) rather than a refusal, precisely so it
|
|
79
|
+
* does not disagree with the prereq side about the same deprecation.
|
|
52
80
|
*/
|
|
53
81
|
export const RD_REVIEW_REQUEST_TYPES = [
|
|
54
82
|
'feat',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.54",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"schemas/*.json",
|
|
84
84
|
".claude-plugin/**",
|
|
85
85
|
"config/eslint/.peaks-rules.cjs",
|
|
86
|
-
"
|
|
86
|
+
"contracts/test-style-contract.md",
|
|
87
87
|
"README.md",
|
|
88
88
|
"README-en.md",
|
|
89
89
|
"CHANGELOG.md",
|
|
@@ -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-
|
|
106
|
-
"peaks-loop-
|
|
107
|
-
"peaks-loop-
|
|
108
|
-
"peaks-loop-
|
|
105
|
+
"peaks-loop-internal-runtime": "0.0.39",
|
|
106
|
+
"peaks-loop-shared": "0.0.88",
|
|
107
|
+
"peaks-loop-shared-channel": "0.0.56",
|
|
108
|
+
"peaks-loop-mut": "0.1.52"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@changesets/cli": "2.31.1",
|
|
@@ -65,7 +65,7 @@ Every RD action MUST align with the 4 Karpathy guidelines (full text at `andrej-
|
|
|
65
65
|
3. **Surgical Changes** — touch only what the user's request requires. Remove imports / variables / functions that *your* changes made unused. Do not refactor adjacent code. Every changed line must trace to the user's request.
|
|
66
66
|
4. **Goal-Driven Execution** — define verifiable success criteria (`peaks request show --role rd` carries ACs from PRD). For multi-step work, state plan + verify checkpoints before acting.
|
|
67
67
|
|
|
68
|
-
Cross-references: Slice 1 PRD §AC-1. The 4-point assertion guard that PRD named was deleted in `f17aa377`;
|
|
68
|
+
Cross-references: Slice 1 PRD §AC-1. The 4-point assertion guard that PRD named was deleted in `f17aa377`; it is re-pinned by `tests/unit/skills/karpathy-injection.test.ts`, which fails if any of the 4 titles above is dropped. The canonical skill id is `andrej-karpathy-skills:karpathy-guidelines`.
|
|
69
69
|
|
|
70
70
|
## Scope directory (slice 10 — read scopeDir from envelope)
|
|
71
71
|
|