peaks-loop 4.0.52 → 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 +89 -2
- 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/dispatch/sub-agent-dispatcher.d.ts +0 -1
- package/dist/services/dispatch/sub-agent-dispatcher.js +0 -3
- 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 -0
- package/dist/services/prd/handoff-gate-evidence.js +145 -0
- 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
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
// PEM private keys, JWTs, GitHub / GitLab tokens, AWS access keys. Used
|
|
6
6
|
// both by the extract path (`assertSafeMemory`) and the backup path
|
|
7
7
|
// (`assertSafeMemoryFileContent`).
|
|
8
|
+
// - `findSensitiveMemoryTitleTerm` — the PROSE predicate for
|
|
9
|
+
// `memory.title`. It is deliberately NOT the config-key predicate: see
|
|
10
|
+
// the block comment above it (slice C0).
|
|
8
11
|
// - `assertSafeMemory` — full safety gate applied during extraction.
|
|
9
|
-
// Combines
|
|
10
|
-
//
|
|
12
|
+
// Combines the metadata key scan + the content pattern scan + the title
|
|
13
|
+
// scan. Each failure names the check that fired, and the title one names
|
|
14
|
+
// the term it matched.
|
|
11
15
|
// - `assertSafeMemoryFileContent` — lighter version for backup: just
|
|
12
16
|
// the content pattern scan, since the file already lives in
|
|
13
17
|
// `.peaks/memory/` and was authored through the normal pipeline.
|
|
@@ -17,7 +21,109 @@
|
|
|
17
21
|
// for index.json regeneration.
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
import { closeSync, constants, openSync, writeFileSync } from 'node:fs';
|
|
20
|
-
import { containsSensitiveConfigValue
|
|
24
|
+
import { containsSensitiveConfigValue } from '../../../config/config-service.js';
|
|
25
|
+
/**
|
|
26
|
+
* The names of the three checks `assertSafeMemory` can fail on, written once.
|
|
27
|
+
*
|
|
28
|
+
* The name of the check that fired rides the refusal message, so the reader is
|
|
29
|
+
* told WHICH rule stopped the write instead of only that something was
|
|
30
|
+
* refused. Callers route their remedy on these same constants (see
|
|
31
|
+
* `memoryExtractNextActions` in `src/cli/commands/core/memory-command.ts`) —
|
|
32
|
+
* two strings kept equal by hand is how a message and its advice drift apart.
|
|
33
|
+
*/
|
|
34
|
+
export const SENSITIVE_MEMORY_CHECKS = {
|
|
35
|
+
metadataKey: 'metadata key scan',
|
|
36
|
+
content: 'content scan',
|
|
37
|
+
title: 'title scan'
|
|
38
|
+
};
|
|
39
|
+
/** The stable prefix of every refusal this module raises on the extract path. */
|
|
40
|
+
const SENSITIVE_MEMORY_REFUSAL = 'Refusing to store sensitive memory content';
|
|
41
|
+
/**
|
|
42
|
+
* Credential TERMS as they appear in prose — the word-level counterpart of the
|
|
43
|
+
* config-key predicate `config-service.isSensitiveConfigPath`.
|
|
44
|
+
*
|
|
45
|
+
* WHY THE TITLE CHECK EXISTS AT ALL (it is not redundant with the content
|
|
46
|
+
* scanner). `hasSensitiveMemoryContent` looks for a credential *value*: its
|
|
47
|
+
* first pattern needs a `:` or `=`, so a title that is nothing but `apiKey`
|
|
48
|
+
* carries no value and slips past it. The title scan is the only rule that can
|
|
49
|
+
* see "the title IS a credential name" — so this check was kept, and only its
|
|
50
|
+
* predicate was replaced.
|
|
51
|
+
*
|
|
52
|
+
* WHY IT DOES NOT REUSE `isSensitiveConfigPath`. That predicate answers "is
|
|
53
|
+
* this a CONFIG KEY", and it answers by SUBSTRING: `includes('auth')` is true
|
|
54
|
+
* for `authority`, `author`, `unauthorized`. On a config key that breadth is
|
|
55
|
+
* harmless — an auth-bearing key IS a credential key, and the config domain is
|
|
56
|
+
* untouched here. On a prose title it is a false refusal the author cannot see
|
|
57
|
+
* the cause of: slice C0, where `peaks memory extract` refused a memory titled
|
|
58
|
+
* "Derive from the authority, never re-declare it" and told the user to
|
|
59
|
+
* "remove secrets" from a memory that had none.
|
|
60
|
+
*
|
|
61
|
+
* HOW IT MATCHES. The title is cut into alphanumeric segments (camelCase
|
|
62
|
+
* boundaries included) and a match is a CONTIGUOUS RUN of segments whose
|
|
63
|
+
* concatenation is one of the terms below. Runs — not single words — are what
|
|
64
|
+
* keep the credential-name spellings a substring check caught and a naive
|
|
65
|
+
* word-per-word check would lose: `private_key`, `api key`, `myApiKey` and
|
|
66
|
+
* `access token` all still match, while `authority`, `author`, `tokenizer`,
|
|
67
|
+
* `secretary` and `credentialed` do not.
|
|
68
|
+
*
|
|
69
|
+
* The residual narrowing is named rather than hidden: an UNSEPARATED compound
|
|
70
|
+
* with a term buried mid-word (`mysecretstuff`) no longer matches. That is the
|
|
71
|
+
* same class as the false refusals above — a word containing `secret` is not a
|
|
72
|
+
* credential term — and the memory body is scanned for values either way.
|
|
73
|
+
*
|
|
74
|
+
* The plural forms are listed explicitly instead of deriving them by stripping
|
|
75
|
+
* a trailing `s` from each match: that rule would turn `secretaries` into
|
|
76
|
+
* `secretarie` → `secret`, which is precisely the substring confusion this
|
|
77
|
+
* predicate exists to end.
|
|
78
|
+
*/
|
|
79
|
+
const SENSITIVE_PROSE_TERMS = new Set([
|
|
80
|
+
'apikey', 'apikeys',
|
|
81
|
+
'accesskey', 'accesskeys',
|
|
82
|
+
'privatekey', 'privatekeys',
|
|
83
|
+
'secretkey', 'secretkeys',
|
|
84
|
+
'accesstoken', 'accesstokens',
|
|
85
|
+
'authtoken', 'authtokens',
|
|
86
|
+
'authkey', 'authkeys',
|
|
87
|
+
'refreshtoken', 'refreshtokens',
|
|
88
|
+
'token', 'tokens',
|
|
89
|
+
'secret', 'secrets',
|
|
90
|
+
'password', 'passwords',
|
|
91
|
+
'passwd',
|
|
92
|
+
'bearer',
|
|
93
|
+
'credential', 'credentials'
|
|
94
|
+
]);
|
|
95
|
+
/** A lower→upper transition: the boundary between the words of `apiKey`. */
|
|
96
|
+
const CAMEL_CASE_BOUNDARY = /([a-z0-9])([A-Z])/g;
|
|
97
|
+
/** `myApiKey` → `['my', 'api', 'key']`; `private_key` → `['private', 'key']`. */
|
|
98
|
+
function proseSegments(text) {
|
|
99
|
+
return text
|
|
100
|
+
.replace(CAMEL_CASE_BOUNDARY, '$1 $2')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.split(/[^a-z0-9]+/)
|
|
103
|
+
.filter((segment) => segment.length > 0);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The credential term this title contains (as a run of words), or `null`.
|
|
107
|
+
*
|
|
108
|
+
* Returns the MATCHED TERM rather than a boolean because the refusal message
|
|
109
|
+
* names it: `title scan matched the credential term "apikey"` is actionable,
|
|
110
|
+
* while "Refusing to store sensitive memory content" is what sent slice C0's
|
|
111
|
+
* user looking for a secret that was not there. The returned string is a
|
|
112
|
+
* dictionary word from the list above — never the title's own text, and never
|
|
113
|
+
* a value — so echoing it into the error envelope cannot leak anything.
|
|
114
|
+
*/
|
|
115
|
+
export function findSensitiveMemoryTitleTerm(title) {
|
|
116
|
+
const segments = proseSegments(title);
|
|
117
|
+
for (let start = 0; start < segments.length; start += 1) {
|
|
118
|
+
let run = '';
|
|
119
|
+
for (let end = start; end < segments.length; end += 1) {
|
|
120
|
+
run += segments[end];
|
|
121
|
+
if (SENSITIVE_PROSE_TERMS.has(run))
|
|
122
|
+
return run;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
21
127
|
export function hasSensitiveMemoryContent(content) {
|
|
22
128
|
return /(?:api[_-]?key|token|secret|password|credential|bearer)\s*[:=]/i.test(content)
|
|
23
129
|
|| /\bauthorization\s*:\s*bearer\s+\S+/i.test(content)
|
|
@@ -30,14 +136,68 @@ export function hasSensitiveMemoryContent(content) {
|
|
|
30
136
|
|| /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(content)
|
|
31
137
|
|| /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/.test(content);
|
|
32
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The refusal `assertSafeMemory` raises: the check that fired and the term it
|
|
141
|
+
* matched travel as DATA, not only as prose.
|
|
142
|
+
*
|
|
143
|
+
* WHY A TYPE RATHER THAN A MESSAGE. The envelope redactor behind every
|
|
144
|
+
* `fail()` (`packages/peaks-loop-shared/src/result.ts`) strips the words
|
|
145
|
+
* secret / token / password / api-key out of every failure MESSAGE — a blanket
|
|
146
|
+
* rule, applied to messages this module does not own. So a message that names
|
|
147
|
+
* the matched term reaches the CLI reading `… the credential term
|
|
148
|
+
* "[redacted]" …`: the term is named, and then removed, on the way out.
|
|
149
|
+
*
|
|
150
|
+
* The term is a word from `SENSITIVE_PROSE_TERMS` — a fixed vocabulary, never a
|
|
151
|
+
* value — so there is nothing about it to redact, and it rides a field
|
|
152
|
+
* instead: `fail()` redacts `message` only. The CLI routes its remedy on
|
|
153
|
+
* `check` for the same reason — a substring test against a message whose words
|
|
154
|
+
* can be rewritten is not a routing rule, it is a guess.
|
|
155
|
+
*
|
|
156
|
+
* NOTHING HERE WEAKENS THE REDACTOR. The content scan never puts its match in
|
|
157
|
+
* this object (see `matchedTerm` below), so no value can reach an envelope
|
|
158
|
+
* through it; the redactor's guarantee is intact and is pinned by the cases in
|
|
159
|
+
* `tests/unit/services/memory/memory-title-sensitive-scan.test.ts`.
|
|
160
|
+
*/
|
|
161
|
+
export class UnsafeMemoryError extends Error {
|
|
162
|
+
/** One of `SENSITIVE_MEMORY_CHECKS` — which rule refused the write. */
|
|
163
|
+
check;
|
|
164
|
+
/**
|
|
165
|
+
* The credential term the TITLE scan matched, or `null` for the other two
|
|
166
|
+
* checks. `null` is not an omission: their match can BE the credential
|
|
167
|
+
* (`ghp_…`, the PEM header, the JWT), which is why their messages describe
|
|
168
|
+
* the pattern family and never echo the text.
|
|
169
|
+
*/
|
|
170
|
+
matchedTerm;
|
|
171
|
+
constructor(check, detail, matchedTerm = null) {
|
|
172
|
+
super(`${SENSITIVE_MEMORY_REFUSAL}: ${check} ${detail}.`);
|
|
173
|
+
this.name = 'UnsafeMemoryError';
|
|
174
|
+
this.check = check;
|
|
175
|
+
this.matchedTerm = matchedTerm;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
33
178
|
export function assertSafeMemory(memory) {
|
|
34
179
|
const content = `${memory.title}\n${memory.kind}\n${memory.body}`;
|
|
35
180
|
const metadata = { title: memory.title, kind: memory.kind, body: memory.body };
|
|
36
|
-
if (containsSensitiveConfigValue(metadata)
|
|
37
|
-
throw new
|
|
181
|
+
if (containsSensitiveConfigValue(metadata)) {
|
|
182
|
+
throw new UnsafeMemoryError(SENSITIVE_MEMORY_CHECKS.metadataKey, 'matched a credential key in the memory metadata');
|
|
183
|
+
}
|
|
184
|
+
if (hasSensitiveMemoryContent(content)) {
|
|
185
|
+
// The match is deliberately NOT echoed, in the message or in the error's
|
|
186
|
+
// fields: for most of these patterns the match IS the credential, so
|
|
187
|
+
// copying it would write the value this scan exists to keep out.
|
|
188
|
+
//
|
|
189
|
+
// The DETAIL is worded around the envelope redactor's vocabulary on
|
|
190
|
+
// purpose. Its catch-all (`/(secret|token|password|api[-_ ]?key)/gi`)
|
|
191
|
+
// rewrites those four words wherever they appear in a failure message, so
|
|
192
|
+
// naming the pattern families in their own words arrives on the CLI as
|
|
193
|
+
// "an [redacted] / [redacted] / [redacted] assignment" — a remedy sentence
|
|
194
|
+
// redacted into uselessness by the very policy it agrees with. Measured on
|
|
195
|
+
// the real CLI, both wordings; this one survives intact.
|
|
196
|
+
throw new UnsafeMemoryError(SENSITIVE_MEMORY_CHECKS.content, 'matched a credential value in the memory content (a `key=value` credential assignment, a Bearer header, a PEM private key, a JWT, or a provider credential)');
|
|
38
197
|
}
|
|
39
|
-
|
|
40
|
-
|
|
198
|
+
const titleTerm = findSensitiveMemoryTitleTerm(memory.title);
|
|
199
|
+
if (titleTerm !== null) {
|
|
200
|
+
throw new UnsafeMemoryError(SENSITIVE_MEMORY_CHECKS.title, `matched the credential term "${titleTerm}" in memory.title`, titleTerm);
|
|
41
201
|
}
|
|
42
202
|
}
|
|
43
203
|
export function assertSafeMemoryFileContent(content) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `gateEvidence` comes FROM, and where its promise is checked.
|
|
3
|
+
*
|
|
4
|
+
* B2 (merged with the B1 repair), rid `rid-b2-gate-evidence-wiring`.
|
|
5
|
+
*
|
|
6
|
+
* B1 made the field real at the SERVICE layer and QA found the hole that
|
|
7
|
+
* mattered (F1 of `rid-b1-qa`): all three frontmatter producers passed
|
|
8
|
+
* nothing, so no capsule on disk ever carried the field — the dead surface
|
|
9
|
+
* had moved one layer out, from "no producer function" to "no production
|
|
10
|
+
* caller". This module closes that by DERIVING the map instead of accepting
|
|
11
|
+
* it from a caller.
|
|
12
|
+
*
|
|
13
|
+
* WHY DERIVED, NOT PASSED IN. Every path in the map is a function of the
|
|
14
|
+
* session id, the request id and the REQUEST TYPE, all of which the producer
|
|
15
|
+
* already has. Asking the caller to supply the map would put a second source
|
|
16
|
+
* next to the derivation, and a CLI flag (`--gate-evidence <json>`) would
|
|
17
|
+
* additionally require the user to hand-author JSON, which the project's
|
|
18
|
+
* Human-NL-Choice-Only rule forbids. So: one function, computed, no flag.
|
|
19
|
+
*
|
|
20
|
+
* THE TYPE-AWARE TENSION, AND HOW IT IS RESOLVED. The schema doc
|
|
21
|
+
* (`skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:53`) calls
|
|
22
|
+
* the five keys a schema and says "Missing keys → Gate C failure". Taken
|
|
23
|
+
* literally that demands all five paths in every capsule — and then a `docs`
|
|
24
|
+
* slice, which Gate C requires NO evidence from, would fail the gate for
|
|
25
|
+
* "declaring" `audit/perf-<rid>.md` and `audit/security-<rid>.md`, two files
|
|
26
|
+
* its type is not supposed to produce. Declaring evidence that does not and
|
|
27
|
+
* should not exist is a false claim, and a gate that fails on a false claim
|
|
28
|
+
* the writer invented is worse than useless.
|
|
29
|
+
*
|
|
30
|
+
* Resolution, and the reason it is not a special case: the map declares
|
|
31
|
+
* EXACTLY what this request type's transition gate requires, read from
|
|
32
|
+
* `PREREQUISITES_BY_TYPE` — the same table `checkPrerequisites` enforces at
|
|
33
|
+
* `rd:qa-handoff`. So:
|
|
34
|
+
*
|
|
35
|
+
* - feature / refactor → projectScan, prdHandoff, codeReview,
|
|
36
|
+
* securityReview, perfBaseline (all five)
|
|
37
|
+
* - bugfix → the same five
|
|
38
|
+
* - config → projectScan, securityReview (TWO keys, verified
|
|
39
|
+
* against the table rather than assumed — `CONFIG_TABLE['rd:qa-handoff']`
|
|
40
|
+
* is `[SECURITY_REVIEW]` alone, so there is no `prdHandoff`: config has no
|
|
41
|
+
* `AUDIT_REQUIRES_HANDOFF` row. An earlier revision of this comment
|
|
42
|
+
* claimed three, and QA caught prose that disagreed with the code, which
|
|
43
|
+
* is the defect this module exists to end. The security path declared is
|
|
44
|
+
* the config-specific `rd/security-review.md` — genuinely ridless, per its
|
|
45
|
+
* own row — not the documented `audit/security-<rid>.md`, which no config
|
|
46
|
+
* slice writes)
|
|
47
|
+
* - docs / chore → NOTHING. No `rd:qa-handoff` row ⇒ nothing for Gate C
|
|
48
|
+
* to validate ⇒ nothing declared (see below)
|
|
49
|
+
*
|
|
50
|
+
* WHY `projectScan` IS DECLARED, AND WHY ONLY FOR GATED TYPES. It is not read
|
|
51
|
+
* from the table — it is Gate A's artifact, and Gate A is type-independent
|
|
52
|
+
* ("After workspace init + project scan", step 0.6 of every workflow). But a
|
|
53
|
+
* declaration is only worth making when something VERIFIES it, and the only
|
|
54
|
+
* verifier is `checkDeclaredGateEvidence`, which runs on `rd:qa-handoff`. For a
|
|
55
|
+
* type with a gate row, Gate C therefore does check this path like any other.
|
|
56
|
+
* For `docs`/`chore` nothing ever reads it, so it is not declared: keeping an
|
|
57
|
+
* inert statement in every docs capsule would recreate, in miniature, the exact
|
|
58
|
+
* defect this whole line of work exists to remove — a claim no one reads, which
|
|
59
|
+
* is how the field was dead in the first place (F1 of `rid-b2-qa`). Scope, in
|
|
60
|
+
* one sentence: **this map declares the evidence paths Gate C will check for
|
|
61
|
+
* this slice**, so an empty declaration is the honest one when there is no such
|
|
62
|
+
* gate.
|
|
63
|
+
*
|
|
64
|
+
* THE REJECTED ALTERNATIVE, named so it is not silently revisited: declaring
|
|
65
|
+
* all five unconditionally and teaching Gate C to check only the
|
|
66
|
+
* type-required subset. Rejected because the capsule would then carry
|
|
67
|
+
* permanent false statements about files that must never exist, and every
|
|
68
|
+
* reader of the frontmatter (peaks-qa's own checklist among them) would need
|
|
69
|
+
* the same type-conditioned filter to avoid mis-reading them.
|
|
70
|
+
*
|
|
71
|
+
* NO DERIVATION → NO DECLARATION. When there is no request artifact to read
|
|
72
|
+
* the type off, this returns `undefined` and the producers write no
|
|
73
|
+
* `gateEvidence` block at all. Never a partial map: a half-derived declaration
|
|
74
|
+
* is a false statement about the missing half's keys, and the pre-B2 bytes are
|
|
75
|
+
* the honest fallback. An id the artifact service REFUSES is not this case and
|
|
76
|
+
* is not reported as `undefined` — see `deriveGateEvidenceForRequest`.
|
|
77
|
+
*/
|
|
78
|
+
import { type RequestType } from '../artifacts/artifact-prerequisites.js';
|
|
79
|
+
import { type GateEvidence } from './handoff-types.js';
|
|
80
|
+
/**
|
|
81
|
+
* The `gateEvidence` map for a slice of `requestType` — the evidence paths
|
|
82
|
+
* Gate C will check for it. Pure: no disk access, so a caller can compute it
|
|
83
|
+
* without side effects. Empty (`{}`) — never a partial map — when the type has
|
|
84
|
+
* no `rd:qa-handoff` row, because then there is no gate to declare anything to.
|
|
85
|
+
*/
|
|
86
|
+
export declare function deriveGateEvidence(opts: {
|
|
87
|
+
readonly sessionId: string;
|
|
88
|
+
readonly requestId: string;
|
|
89
|
+
readonly requestType: RequestType;
|
|
90
|
+
}): GateEvidence;
|
|
91
|
+
/**
|
|
92
|
+
* Derive for a request by reading its PRD artifact — the only place the
|
|
93
|
+
* request TYPE is recorded (`- type: <t>`, written by `peaks request init`
|
|
94
|
+
* and read back by `request-artifact-service.extractMetadata`).
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` when there is NO artifact to read — the one case the
|
|
97
|
+
* producers turn into "write no block" (see the header). That is now the ONLY
|
|
98
|
+
* `undefined`: a `showRequestArtifact` that THROWS is deliberately not caught,
|
|
99
|
+
* so a caller can tell "this slice has nothing to declare" apart from "the
|
|
100
|
+
* artifact could not be read".
|
|
101
|
+
*
|
|
102
|
+
* F4 (`rid-f4-ceiling-breach`). This used to be `catch { return undefined }`,
|
|
103
|
+
* which folded those two into one value — and the repository's own ratchet
|
|
104
|
+
* caught it (`capability-guard-runner/contracts/J03.ts`, rule
|
|
105
|
+
* `catch-return-null`; the ceiling was written for exactly this shape). Nothing
|
|
106
|
+
* is lost by letting the throw through. The refusals `showRequestArtifact` can
|
|
107
|
+
* raise are the rid/sid ones, and BOTH callers reach a byte-identical message
|
|
108
|
+
* on the next statement anyway — `assertSafeHandoffIds` (`handoff-service.ts`)
|
|
109
|
+
* for `prd handoff init`, `generateEvidence`'s own guard for the evidence
|
|
110
|
+
* generator — so the user-visible failure is unchanged; only its origin moves
|
|
111
|
+
* two lines earlier. What the catch DID cover in practice was an I/O failure
|
|
112
|
+
* while reading the artifact file, and swallowing that writes a capsule with no
|
|
113
|
+
* declaration at all, which Gate C reads as "nothing declared, nothing to
|
|
114
|
+
* check" (`field-absent` is not this check's business, below). A silent hole is
|
|
115
|
+
* worse than a loud, already-duplicated one.
|
|
116
|
+
*/
|
|
117
|
+
export declare function deriveGateEvidenceForRequest(opts: {
|
|
118
|
+
readonly projectRoot: string;
|
|
119
|
+
readonly sessionId: string;
|
|
120
|
+
readonly requestId: string;
|
|
121
|
+
}): Promise<GateEvidence | undefined>;
|
|
122
|
+
/** `missing` / `warnings` in the same shape `checkPrerequisites` reports. */
|
|
123
|
+
export interface GateEvidenceDeclarationCheck {
|
|
124
|
+
readonly ok: boolean;
|
|
125
|
+
readonly missing: ReadonlyArray<{
|
|
126
|
+
path: string;
|
|
127
|
+
description: string;
|
|
128
|
+
}>;
|
|
129
|
+
readonly warnings: ReadonlyArray<{
|
|
130
|
+
path: string;
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
}>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* GATE C — is the capsule's OWN declaration true?
|
|
137
|
+
*
|
|
138
|
+
* The division of labour, stated so the two checks cannot drift: the
|
|
139
|
+
* prerequisite table says which artifacts this type MUST produce (it owns the
|
|
140
|
+
* requirement and its file existence); this function says the capsule must
|
|
141
|
+
* not CLAIM evidence it does not have (it owns the declaration). Neither
|
|
142
|
+
* re-implements the other, and both read their paths from wherever the path
|
|
143
|
+
* really comes from.
|
|
144
|
+
*
|
|
145
|
+
* A missing declared path FAILS and names the key, because a declaration the
|
|
146
|
+
* gate cannot verify is exactly the "prose pretending to be data" state this
|
|
147
|
+
* field was created to end. Outcomes that are not failures, each for its own
|
|
148
|
+
* reason:
|
|
149
|
+
* - no capsule → not this check's business (`AUDIT_REQUIRES_HANDOFF` owns it)
|
|
150
|
+
* - `field-absent` → the capsule declares nothing; every pre-B1 capsule,
|
|
151
|
+
* and every slice whose producer could not derive a type
|
|
152
|
+
* - an unknown key → a WARNING: the five-key vocabulary is a typo trap, and
|
|
153
|
+
* the type's real requirement is still enforced by the table, so this
|
|
154
|
+
* informs without blocking
|
|
155
|
+
*/
|
|
156
|
+
export declare function checkDeclaredGateEvidence(opts: {
|
|
157
|
+
readonly projectRoot: string;
|
|
158
|
+
readonly sessionId: string;
|
|
159
|
+
readonly requestId: string;
|
|
160
|
+
}): Promise<GateEvidenceDeclarationCheck>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `gateEvidence` comes FROM, and where its promise is checked.
|
|
3
|
+
*
|
|
4
|
+
* B2 (merged with the B1 repair), rid `rid-b2-gate-evidence-wiring`.
|
|
5
|
+
*
|
|
6
|
+
* B1 made the field real at the SERVICE layer and QA found the hole that
|
|
7
|
+
* mattered (F1 of `rid-b1-qa`): all three frontmatter producers passed
|
|
8
|
+
* nothing, so no capsule on disk ever carried the field — the dead surface
|
|
9
|
+
* had moved one layer out, from "no producer function" to "no production
|
|
10
|
+
* caller". This module closes that by DERIVING the map instead of accepting
|
|
11
|
+
* it from a caller.
|
|
12
|
+
*
|
|
13
|
+
* WHY DERIVED, NOT PASSED IN. Every path in the map is a function of the
|
|
14
|
+
* session id, the request id and the REQUEST TYPE, all of which the producer
|
|
15
|
+
* already has. Asking the caller to supply the map would put a second source
|
|
16
|
+
* next to the derivation, and a CLI flag (`--gate-evidence <json>`) would
|
|
17
|
+
* additionally require the user to hand-author JSON, which the project's
|
|
18
|
+
* Human-NL-Choice-Only rule forbids. So: one function, computed, no flag.
|
|
19
|
+
*
|
|
20
|
+
* THE TYPE-AWARE TENSION, AND HOW IT IS RESOLVED. The schema doc
|
|
21
|
+
* (`skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:53`) calls
|
|
22
|
+
* the five keys a schema and says "Missing keys → Gate C failure". Taken
|
|
23
|
+
* literally that demands all five paths in every capsule — and then a `docs`
|
|
24
|
+
* slice, which Gate C requires NO evidence from, would fail the gate for
|
|
25
|
+
* "declaring" `audit/perf-<rid>.md` and `audit/security-<rid>.md`, two files
|
|
26
|
+
* its type is not supposed to produce. Declaring evidence that does not and
|
|
27
|
+
* should not exist is a false claim, and a gate that fails on a false claim
|
|
28
|
+
* the writer invented is worse than useless.
|
|
29
|
+
*
|
|
30
|
+
* Resolution, and the reason it is not a special case: the map declares
|
|
31
|
+
* EXACTLY what this request type's transition gate requires, read from
|
|
32
|
+
* `PREREQUISITES_BY_TYPE` — the same table `checkPrerequisites` enforces at
|
|
33
|
+
* `rd:qa-handoff`. So:
|
|
34
|
+
*
|
|
35
|
+
* - feature / refactor → projectScan, prdHandoff, codeReview,
|
|
36
|
+
* securityReview, perfBaseline (all five)
|
|
37
|
+
* - bugfix → the same five
|
|
38
|
+
* - config → projectScan, securityReview (TWO keys, verified
|
|
39
|
+
* against the table rather than assumed — `CONFIG_TABLE['rd:qa-handoff']`
|
|
40
|
+
* is `[SECURITY_REVIEW]` alone, so there is no `prdHandoff`: config has no
|
|
41
|
+
* `AUDIT_REQUIRES_HANDOFF` row. An earlier revision of this comment
|
|
42
|
+
* claimed three, and QA caught prose that disagreed with the code, which
|
|
43
|
+
* is the defect this module exists to end. The security path declared is
|
|
44
|
+
* the config-specific `rd/security-review.md` — genuinely ridless, per its
|
|
45
|
+
* own row — not the documented `audit/security-<rid>.md`, which no config
|
|
46
|
+
* slice writes)
|
|
47
|
+
* - docs / chore → NOTHING. No `rd:qa-handoff` row ⇒ nothing for Gate C
|
|
48
|
+
* to validate ⇒ nothing declared (see below)
|
|
49
|
+
*
|
|
50
|
+
* WHY `projectScan` IS DECLARED, AND WHY ONLY FOR GATED TYPES. It is not read
|
|
51
|
+
* from the table — it is Gate A's artifact, and Gate A is type-independent
|
|
52
|
+
* ("After workspace init + project scan", step 0.6 of every workflow). But a
|
|
53
|
+
* declaration is only worth making when something VERIFIES it, and the only
|
|
54
|
+
* verifier is `checkDeclaredGateEvidence`, which runs on `rd:qa-handoff`. For a
|
|
55
|
+
* type with a gate row, Gate C therefore does check this path like any other.
|
|
56
|
+
* For `docs`/`chore` nothing ever reads it, so it is not declared: keeping an
|
|
57
|
+
* inert statement in every docs capsule would recreate, in miniature, the exact
|
|
58
|
+
* defect this whole line of work exists to remove — a claim no one reads, which
|
|
59
|
+
* is how the field was dead in the first place (F1 of `rid-b2-qa`). Scope, in
|
|
60
|
+
* one sentence: **this map declares the evidence paths Gate C will check for
|
|
61
|
+
* this slice**, so an empty declaration is the honest one when there is no such
|
|
62
|
+
* gate.
|
|
63
|
+
*
|
|
64
|
+
* THE REJECTED ALTERNATIVE, named so it is not silently revisited: declaring
|
|
65
|
+
* all five unconditionally and teaching Gate C to check only the
|
|
66
|
+
* type-required subset. Rejected because the capsule would then carry
|
|
67
|
+
* permanent false statements about files that must never exist, and every
|
|
68
|
+
* reader of the frontmatter (peaks-qa's own checklist among them) would need
|
|
69
|
+
* the same type-conditioned filter to avoid mis-reading them.
|
|
70
|
+
*
|
|
71
|
+
* NO DERIVATION → NO DECLARATION. When there is no request artifact to read
|
|
72
|
+
* the type off, this returns `undefined` and the producers write no
|
|
73
|
+
* `gateEvidence` block at all. Never a partial map: a half-derived declaration
|
|
74
|
+
* is a false statement about the missing half's keys, and the pre-B2 bytes are
|
|
75
|
+
* the honest fallback. An id the artifact service REFUSES is not this case and
|
|
76
|
+
* is not reported as `undefined` — see `deriveGateEvidenceForRequest`.
|
|
77
|
+
*/
|
|
78
|
+
import { join } from 'node:path';
|
|
79
|
+
import { pathExists } from 'peaks-loop-shared/fs';
|
|
80
|
+
import { getPrerequisitesFor } from '../artifacts/artifact-prerequisites.js';
|
|
81
|
+
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
82
|
+
import { readHandoffGateEvidence } from './handoff-gate-evidence.js';
|
|
83
|
+
import { resolveHandoffPath } from './handoff-service.js';
|
|
84
|
+
import { GATE_EVIDENCE_KEYS } from './handoff-types.js';
|
|
85
|
+
import { PROJECT_SCAN_RELATIVE } from './project-scan-reader.js';
|
|
86
|
+
/**
|
|
87
|
+
* Map ONE prerequisite of the `rd:qa-handoff` table onto the five-key
|
|
88
|
+
* vocabulary. The PATHS come from the table (single source); this function
|
|
89
|
+
* only names which key each required artifact answers to, so a change to a
|
|
90
|
+
* prerequisite's path moves the declared path with it.
|
|
91
|
+
*
|
|
92
|
+
* `mut/mut-report.json`, `rd/karpathy-review-<rid>.md`,
|
|
93
|
+
* `rd/third-party-review.md`, `qa/test-cases/<rid>.md` and the unit-test
|
|
94
|
+
* marker have no key — the vocabulary is five keys, and those artifacts are
|
|
95
|
+
* already enforced by the table itself, so they are deliberately not
|
|
96
|
+
* declared here rather than being forced into a key that does not mean them.
|
|
97
|
+
*/
|
|
98
|
+
function gateKeyOfPrerequisitePath(relativePath) {
|
|
99
|
+
if (relativePath.startsWith('prd/handoff'))
|
|
100
|
+
return 'prdHandoff';
|
|
101
|
+
if (relativePath.startsWith('rd/code-review'))
|
|
102
|
+
return 'codeReview';
|
|
103
|
+
if (relativePath.startsWith('audit/security') || relativePath.startsWith('rd/security-review')) {
|
|
104
|
+
return 'securityReview';
|
|
105
|
+
}
|
|
106
|
+
if (relativePath.startsWith('audit/perf'))
|
|
107
|
+
return 'perfBaseline';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The `gateEvidence` map for a slice of `requestType` — the evidence paths
|
|
112
|
+
* Gate C will check for it. Pure: no disk access, so a caller can compute it
|
|
113
|
+
* without side effects. Empty (`{}`) — never a partial map — when the type has
|
|
114
|
+
* no `rd:qa-handoff` row, because then there is no gate to declare anything to.
|
|
115
|
+
*/
|
|
116
|
+
export function deriveGateEvidence(opts) {
|
|
117
|
+
const requirements = getPrerequisitesFor('rd', 'qa-handoff', opts.requestType);
|
|
118
|
+
// docs / chore land here: `MINIMAL_TABLE` has no row at that transition, so
|
|
119
|
+
// nothing is required and nothing is declared. An empty map renders no block
|
|
120
|
+
// at all (`handoff-frontmatter.ts`), so such a capsule is byte-identical to a
|
|
121
|
+
// pre-B2 one and carries no statement that nothing verifies.
|
|
122
|
+
if (requirements.length === 0)
|
|
123
|
+
return {};
|
|
124
|
+
const sessionRoot = join('.peaks', '_runtime', opts.sessionId);
|
|
125
|
+
const evidence = {
|
|
126
|
+
projectScan: PROJECT_SCAN_RELATIVE
|
|
127
|
+
};
|
|
128
|
+
for (const prerequisite of requirements) {
|
|
129
|
+
const key = gateKeyOfPrerequisitePath(prerequisite.relativePath);
|
|
130
|
+
if (key === null)
|
|
131
|
+
continue;
|
|
132
|
+
// `<rid>` is the table's own placeholder, resolved the way the table's
|
|
133
|
+
// resolver resolves it, so the declaration names the file the gate looks
|
|
134
|
+
// for.
|
|
135
|
+
evidence[key] = join(sessionRoot, prerequisite.relativePath.replace('<rid>', opts.requestId));
|
|
136
|
+
}
|
|
137
|
+
return evidence;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Derive for a request by reading its PRD artifact — the only place the
|
|
141
|
+
* request TYPE is recorded (`- type: <t>`, written by `peaks request init`
|
|
142
|
+
* and read back by `request-artifact-service.extractMetadata`).
|
|
143
|
+
*
|
|
144
|
+
* Returns `undefined` when there is NO artifact to read — the one case the
|
|
145
|
+
* producers turn into "write no block" (see the header). That is now the ONLY
|
|
146
|
+
* `undefined`: a `showRequestArtifact` that THROWS is deliberately not caught,
|
|
147
|
+
* so a caller can tell "this slice has nothing to declare" apart from "the
|
|
148
|
+
* artifact could not be read".
|
|
149
|
+
*
|
|
150
|
+
* F4 (`rid-f4-ceiling-breach`). This used to be `catch { return undefined }`,
|
|
151
|
+
* which folded those two into one value — and the repository's own ratchet
|
|
152
|
+
* caught it (`capability-guard-runner/contracts/J03.ts`, rule
|
|
153
|
+
* `catch-return-null`; the ceiling was written for exactly this shape). Nothing
|
|
154
|
+
* is lost by letting the throw through. The refusals `showRequestArtifact` can
|
|
155
|
+
* raise are the rid/sid ones, and BOTH callers reach a byte-identical message
|
|
156
|
+
* on the next statement anyway — `assertSafeHandoffIds` (`handoff-service.ts`)
|
|
157
|
+
* for `prd handoff init`, `generateEvidence`'s own guard for the evidence
|
|
158
|
+
* generator — so the user-visible failure is unchanged; only its origin moves
|
|
159
|
+
* two lines earlier. What the catch DID cover in practice was an I/O failure
|
|
160
|
+
* while reading the artifact file, and swallowing that writes a capsule with no
|
|
161
|
+
* declaration at all, which Gate C reads as "nothing declared, nothing to
|
|
162
|
+
* check" (`field-absent` is not this check's business, below). A silent hole is
|
|
163
|
+
* worse than a loud, already-duplicated one.
|
|
164
|
+
*/
|
|
165
|
+
export async function deriveGateEvidenceForRequest(opts) {
|
|
166
|
+
// F4 (`rid-f4-ceiling-breach`). The throw from `showRequestArtifact` is
|
|
167
|
+
// propagated, so a caller can tell "this slice has nothing to declare"
|
|
168
|
+
// (artifact missing → `artifact === null` → `requestType === null` →
|
|
169
|
+
// `undefined`) apart from "the artifact could not be read" (an id the
|
|
170
|
+
// service refuses → rejected with `Invalid request id: ...`). The old
|
|
171
|
+
// `catch { return undefined }` collapsed both into the same value, and the
|
|
172
|
+
// `catch-return-null` ratchet caught it. The refusals `showRequestArtifact`
|
|
173
|
+
// raises are the rid / sid ones, and both callers reach a byte-identical
|
|
174
|
+
// guard a few lines up — `assertSafeHandoffIds` for `prd handoff init`,
|
|
175
|
+
// `generateEvidence`'s own guard for the evidence generator — so the
|
|
176
|
+
// user-visible failure is unchanged; only its origin moves earlier.
|
|
177
|
+
const artifact = await showRequestArtifact({
|
|
178
|
+
projectRoot: opts.projectRoot,
|
|
179
|
+
role: 'prd',
|
|
180
|
+
requestId: opts.requestId,
|
|
181
|
+
sessionId: opts.sessionId
|
|
182
|
+
});
|
|
183
|
+
const requestType = artifact?.requestType ?? null;
|
|
184
|
+
if (requestType === null)
|
|
185
|
+
return undefined;
|
|
186
|
+
return deriveGateEvidence({
|
|
187
|
+
sessionId: opts.sessionId,
|
|
188
|
+
requestId: opts.requestId,
|
|
189
|
+
requestType
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* GATE C — is the capsule's OWN declaration true?
|
|
194
|
+
*
|
|
195
|
+
* The division of labour, stated so the two checks cannot drift: the
|
|
196
|
+
* prerequisite table says which artifacts this type MUST produce (it owns the
|
|
197
|
+
* requirement and its file existence); this function says the capsule must
|
|
198
|
+
* not CLAIM evidence it does not have (it owns the declaration). Neither
|
|
199
|
+
* re-implements the other, and both read their paths from wherever the path
|
|
200
|
+
* really comes from.
|
|
201
|
+
*
|
|
202
|
+
* A missing declared path FAILS and names the key, because a declaration the
|
|
203
|
+
* gate cannot verify is exactly the "prose pretending to be data" state this
|
|
204
|
+
* field was created to end. Outcomes that are not failures, each for its own
|
|
205
|
+
* reason:
|
|
206
|
+
* - no capsule → not this check's business (`AUDIT_REQUIRES_HANDOFF` owns it)
|
|
207
|
+
* - `field-absent` → the capsule declares nothing; every pre-B1 capsule,
|
|
208
|
+
* and every slice whose producer could not derive a type
|
|
209
|
+
* - an unknown key → a WARNING: the five-key vocabulary is a typo trap, and
|
|
210
|
+
* the type's real requirement is still enforced by the table, so this
|
|
211
|
+
* informs without blocking
|
|
212
|
+
*/
|
|
213
|
+
export async function checkDeclaredGateEvidence(opts) {
|
|
214
|
+
const capsule = resolveHandoffPath({
|
|
215
|
+
projectRoot: opts.projectRoot,
|
|
216
|
+
sessionId: opts.sessionId,
|
|
217
|
+
requestId: opts.requestId
|
|
218
|
+
});
|
|
219
|
+
if (capsule === null)
|
|
220
|
+
return { ok: true, missing: [], warnings: [] };
|
|
221
|
+
const declared = await readHandoffGateEvidence(capsule);
|
|
222
|
+
if (declared.status === 'field-absent' || declared.status === 'file-missing') {
|
|
223
|
+
return { ok: true, missing: [], warnings: [] };
|
|
224
|
+
}
|
|
225
|
+
// A capsule with NO frontmatter block cannot declare anything, and
|
|
226
|
+
// `AUDIT_REQUIRES_HANDOFF` — the prereq that owns capsule validity — accepts
|
|
227
|
+
// that shape on a substring check (`schemaVersion: 2` + `sha256:` appearing
|
|
228
|
+
// anywhere). Failing it here would make Gate C refuse capsules the gate next
|
|
229
|
+
// to it accepts, breaking the "the checker and `request transition` agree"
|
|
230
|
+
// property that `pipeline-verify-contract-drift.test.ts` pins. So the line is
|
|
231
|
+
// drawn at the fence, not at readability: no fence ⇒ nothing declared;
|
|
232
|
+
// a fence whose YAML will not parse ⇒ a declaration that may be inside and
|
|
233
|
+
// unreadable, and THAT fails below.
|
|
234
|
+
if (declared.status === 'frontmatter-malformed' && declared.reason === 'no-frontmatter-fence') {
|
|
235
|
+
return { ok: true, missing: [], warnings: [] };
|
|
236
|
+
}
|
|
237
|
+
if (declared.status !== 'ok') {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
missing: [
|
|
241
|
+
{
|
|
242
|
+
path: `gateEvidence(${declared.status})`,
|
|
243
|
+
description: `the handoff's gateEvidence declaration is unusable (${declared.status}) — ` +
|
|
244
|
+
'a declaration that cannot be read is not the same as no declaration'
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
warnings: []
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const missing = [];
|
|
251
|
+
for (const key of GATE_EVIDENCE_KEYS) {
|
|
252
|
+
const declaredPath = declared.evidence[key];
|
|
253
|
+
if (declaredPath === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
if (await pathExists(join(opts.projectRoot, declaredPath)))
|
|
256
|
+
continue;
|
|
257
|
+
missing.push({
|
|
258
|
+
path: `gateEvidence.${key}`,
|
|
259
|
+
description: `gateEvidence.${key} declares "${declaredPath}", which does not exist`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const warnings = declared.unknownKeys.map((key) => ({
|
|
263
|
+
path: `gateEvidence.${key}`,
|
|
264
|
+
code: 'gate-evidence-unknown-key',
|
|
265
|
+
message: `gateEvidence declares "${key}", which is not one of the five keys ` +
|
|
266
|
+
`(${GATE_EVIDENCE_KEYS.join(', ')}) — a misspelled key is why a required gate ` +
|
|
267
|
+
'can read as undeclared'
|
|
268
|
+
}));
|
|
269
|
+
return { ok: missing.length === 0, missing, warnings };
|
|
270
|
+
}
|
|
@@ -21,6 +21,7 @@ import { dirname, join } from 'node:path';
|
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
22
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
23
23
|
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
24
|
+
import { deriveGateEvidence } from './gate-evidence-derivation.js';
|
|
24
25
|
import { handoffRelativePath, sha256OfBody } from './handoff-service.js';
|
|
25
26
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
26
27
|
/**
|
|
@@ -64,7 +65,18 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
64
65
|
goals: [],
|
|
65
66
|
acceptanceCriteria: [],
|
|
66
67
|
preservedBehavior: [],
|
|
67
|
-
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')
|
|
68
|
+
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, ''),
|
|
69
|
+
// B2: DERIVED from the request type of the artifact this producer just
|
|
70
|
+
// read. B1 gave this function an optional caller-supplied map instead;
|
|
71
|
+
// with a single-source derivation available that option was the wrong
|
|
72
|
+
// shape — it left the one production caller (`request-commands.ts`) able
|
|
73
|
+
// to pass nothing, which is exactly the F1 hole. There is no input to
|
|
74
|
+
// forget now.
|
|
75
|
+
gateEvidence: deriveGateEvidence({
|
|
76
|
+
sessionId: opts.sessionId,
|
|
77
|
+
requestId: opts.requestId,
|
|
78
|
+
requestType: artifact.requestType
|
|
79
|
+
})
|
|
68
80
|
};
|
|
69
81
|
const content = `${serializeHandoffFrontmatter(frontmatter)}${body}`;
|
|
70
82
|
mkdirSync(dirname(handoffPath), { recursive: true });
|