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.
Files changed (44) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/codegraph-commands.js +164 -5
  5. package/dist/cli/commands/core/memory-command.js +35 -2
  6. package/dist/cli/commands/dispatch-commands.js +4 -2
  7. package/dist/cli/commands/prd-commands.js +15 -0
  8. package/dist/cli/commands/sub-agent-shared.d.ts +23 -0
  9. package/dist/cli/commands/sub-agent-shared.js +39 -0
  10. package/dist/services/artifacts/artifact-prerequisites.js +34 -0
  11. package/dist/services/audit/enforcer-liveness.js +1 -1
  12. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +44 -0
  13. package/dist/services/codegraph/codegraph-config-repair-writer.js +112 -13
  14. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +23 -1
  15. package/dist/services/codegraph/codegraph-exclude-repair.js +6 -0
  16. package/dist/services/evidence/evidence-generator.js +11 -3
  17. package/dist/services/memory/project-memory-service/index.d.ts +1 -1
  18. package/dist/services/memory/project-memory-service/index.js +1 -1
  19. package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +59 -0
  20. package/dist/services/memory/project-memory-service/store/atomic-write.js +167 -7
  21. package/dist/services/prd/gate-evidence-derivation.d.ts +160 -0
  22. package/dist/services/prd/gate-evidence-derivation.js +270 -0
  23. package/dist/services/prd/handoff-auto-regen.js +13 -1
  24. package/dist/services/prd/handoff-frontmatter.d.ts +1 -1
  25. package/dist/services/prd/handoff-frontmatter.js +62 -0
  26. package/dist/services/prd/handoff-gate-evidence.d.ts +95 -1
  27. package/dist/services/prd/handoff-gate-evidence.js +125 -57
  28. package/dist/services/prd/handoff-service.d.ts +21 -1
  29. package/dist/services/prd/handoff-service.js +45 -2
  30. package/dist/services/prd/handoff-types.d.ts +40 -0
  31. package/dist/services/prd/handoff-types.js +28 -1
  32. package/dist/services/prd/project-scan-reader.d.ts +7 -0
  33. package/dist/services/prd/project-scan-reader.js +7 -1
  34. package/dist/services/rd/reviewer-dispatch-policy.d.ts +36 -8
  35. package/dist/services/rd/reviewer-dispatch-policy.js +36 -8
  36. package/package.json +6 -6
  37. package/skills/bee/peaks-rd/SKILL.md +1 -1
  38. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +26 -10
  39. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +1 -1
  40. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -1
  41. package/skills/peaks-code/references/periodic-checkpoint.md +7 -5
  42. package/skills/peaks-code/references/startup-sequence.md +1 -1
  43. package/skills/peaks-final-review/SKILL.md +1 -1
  44. /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
- // AC-5 of slice 2026-09-17-4-0-51-cleanup: the handoff's frontmatter
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
- // This module is the SOLE reader for that field. It is a stand-alone
10
- // reader (not a member of `handoff-service.ts`) so that the
11
- // `HandoffFrontmatter` type stays a pure-data shape and this consumer
12
- // can be added independently without disturbing every existing
13
- // parse-and-validate path.
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
- // Returns:
16
- // - `string[]` — the gate names declared in the frontmatter
17
- // - `null` — the file is missing, the frontmatter is malformed,
18
- // the field is absent, or the field is not an array
19
- // of strings. Callers MUST treat null as "no claim"
20
- // and not as a failure.
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
- // The reader is intentionally permissive (returns null instead of
23
- // throwing) because a malformed frontmatter is the caller's signal to
24
- // surface the failure to the operator, not the reader's signal to
25
- // throw inside the gate pipeline.
26
- import { existsSync } from 'node:fs';
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
- function parseGateEvidenceFrontmatter(raw) {
30
- // Frontmatter is the `---`-fenced YAML block at the top of the file.
31
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
32
- if (!match)
33
- return { gateEvidence: null };
34
- const yamlBody = match[1] ?? '';
35
- let parsed;
36
- try {
37
- parsed = parseYaml(yamlBody);
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
- catch (_error) {
40
- // reader is permissive by design: malformed YAML → no claim (not throw)
41
- void _error;
42
- return { gateEvidence: null };
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
- if (parsed === null || typeof parsed !== 'object')
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 (_error) {
59
- // reader is permissive by design: missing file → no claim (not throw)
60
- void _error;
61
- return null;
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
- const { gateEvidence } = parseGateEvidenceFrontmatter(raw);
65
- if (gateEvidence === null)
66
- return null;
67
- // Filter to strings only — a non-string element (number/boolean/
68
- // null) is treated as "not a gate name" rather than as an error.
69
- const strings = gateEvidence.filter((item) => typeof item === 'string');
70
- return strings.length > 0 ? strings : null;
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
- catch (_error) {
73
- // reader is permissive by design: unexpected parse failure → no claim
74
- void _error;
75
- return null;
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: { ...parsed, schemaVersion: HANDOFF_SCHEMA_VERSION },
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
- export {};
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
- const PROJECT_SCAN_RELATIVE = join('.peaks', 'project-scan', 'project-scan.md');
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
- * The dispatch policy here is the **canonical decision table** consumed
30
- * by the LLM-side runner and pinned by tests:
31
- * - `tests/unit/rd/karpathy-skip-on-config-docs-chore.test.ts`
32
- * (5 → 3 element pinning)
33
- * - `tests/unit/rd/deprecated-reviewer-back-compat.test.ts`
34
- * (NEW in v2.12.0 — 8 cases; back-compat for the 2 removed slots)
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. See Tier 5 (`artifact-prerequisites.ts`)
51
- * for the matching prereq-side back-compat (`mustContainAny` form).
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
- * The dispatch policy here is the **canonical decision table** consumed
30
- * by the LLM-side runner and pinned by tests:
31
- * - `tests/unit/rd/karpathy-skip-on-config-docs-chore.test.ts`
32
- * (5 → 3 element pinning)
33
- * - `tests/unit/rd/deprecated-reviewer-back-compat.test.ts`
34
- * (NEW in v2.12.0 — 8 cases; back-compat for the 2 removed slots)
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. See Tier 5 (`artifact-prerequisites.ts`)
51
- * for the matching prereq-side back-compat (`mustContainAny` form).
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.53",
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
- "docs/test-style-contract.md",
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-shared": "0.0.87",
106
- "peaks-loop-internal-runtime": "0.0.38",
107
- "peaks-loop-mut": "0.1.51",
108
- "peaks-loop-shared-channel": "0.0.55"
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`; the block below is held by documentation only. The canonical skill id is `andrej-karpathy-skills:karpathy-guidelines`.
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