triad-plus 1.7.0 → 1.9.0
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 +32 -0
- package/README.md +23 -12
- package/adapters/antigravity/.agents/agents/triad-developer/agent.md +7 -3
- package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +5 -0
- package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +5 -0
- package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +7 -2
- package/adapters/antigravity/.agents/skills/triad/SKILL.md +22 -0
- package/adapters/claude-code/.claude/agents/triad-developer.md +9 -4
- package/adapters/claude-code/.claude/agents/triad-evaluator.md +5 -0
- package/adapters/claude-code/.claude/agents/triad-reviewer.md +10 -5
- package/adapters/claude-code/.claude/commands/triad.md +22 -0
- package/adapters/claude-code/README.md +11 -3
- package/adapters/codex/prompts/triad.md +28 -0
- package/adapters/copilot/.github/agents/triad-developer.agent.md +12 -6
- package/adapters/copilot/.github/agents/triad-evaluator.agent.md +7 -1
- package/adapters/copilot/.github/agents/triad-orchestrator.agent.md +15 -1
- package/adapters/copilot/.github/agents/triad-reviewer.agent.md +11 -5
- package/adapters/copilot/.github/skills/triad/SKILL.md +23 -0
- package/adapters/hermes/skills/triad/SKILL.md +23 -0
- package/adapters/opencode/.opencode/agents/triad-developer.md +9 -4
- package/adapters/opencode/.opencode/agents/triad-evaluator.md +6 -0
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +15 -0
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +10 -3
- package/adapters/opencode/.opencode/commands/triad.md +21 -0
- package/adapters/opencode/README.md +19 -4
- package/adapters/registry.mjs +2 -0
- package/bin/triad-plus.js +8 -2
- package/docs/architecture.md +15 -0
- package/docs/assignment-packets.md +35 -0
- package/docs/bmad-integration.md +187 -82
- package/docs/configuration.md +28 -0
- package/docs/evaluator-plus.md +9 -0
- package/docs/npx-installation.md +4 -0
- package/docs/operating-guide.it.md +18 -0
- package/docs/operating-guide.md +17 -0
- package/docs/quality-contract.md +119 -0
- package/docs/verification.md +37 -0
- package/integrations/bmad/README.md +17 -9
- package/integrations/bmad/epics-parser.mjs +697 -0
- package/integrations/bmad/story-importer.mjs +143 -5
- package/package.json +2 -2
- package/runtime/lib/assignment-packet.mjs +516 -0
- package/runtime/lib/quality-baseline.mjs +205 -0
- package/runtime/triad-assignment-packet.mjs +62 -0
- package/runtime/triad-bmad-intake.mjs +150 -0
- package/runtime/triad-evaluator-validate.mjs +261 -0
- package/runtime/triad-verify.mjs +33 -7
- package/schemas/evaluator-plus-result.schema.json +16 -0
- package/schemas/quality-baseline.schema.json +42 -0
- package/schemas/verification-evidence.schema.json +9 -1
- package/skills/triad-loop-bootstrap/SKILL.md +47 -4
- package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +13 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/quality-baseline.json +15 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +7 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +16 -1
- package/skills/triad-loop-bootstrap/assets/project.yaml +5 -0
- package/skills/triad-loop-developer/SKILL.md +19 -8
- package/skills/triad-loop-evaluator/SKILL.md +12 -2
- package/skills/triad-loop-orchestrator/SKILL.md +134 -11
- package/skills/triad-loop-reviewer/SKILL.md +15 -5
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { loadQualityBaseline, qualityCriteriaByScope, validateQualityBaselineManifest } from "./lib/quality-baseline.mjs";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const VERDICTS = new Set(["PASS", "FAIL", "INDETERMINATE"]);
|
|
7
|
+
const SHA256 = /^[a-f0-9]{64}$/i;
|
|
8
|
+
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
9
|
+
|
|
10
|
+
function evaluatorError(code, message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.code = code;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rejectUnknownProperties(value, allowed, label) {
|
|
17
|
+
for (const key of Object.keys(value)) {
|
|
18
|
+
if (!allowed.has(key)) throw evaluatorError("evaluator_result_invalid", `${label} contains unknown property: ${key}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireBaseResult(result) {
|
|
23
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw evaluatorError("evaluator_result_invalid", "Evaluator+ result must be an object");
|
|
24
|
+
rejectUnknownProperties(result, new Set(["schema_version", "feature_id", "candidate_fingerprint", "quality_baseline_fingerprint", "verdict", "summary", "evidence_refs", "criteria", "created_at"]), "Evaluator+ result");
|
|
25
|
+
if (result.schema_version !== 1) throw evaluatorError("evaluator_result_invalid", "Evaluator+ schema_version must be 1");
|
|
26
|
+
if (typeof result.feature_id !== "string" || !result.feature_id.trim()) throw evaluatorError("evaluator_result_invalid", "Evaluator+ feature_id must be non-empty");
|
|
27
|
+
if (typeof result.candidate_fingerprint !== "string" || !result.candidate_fingerprint.trim()) throw evaluatorError("evaluator_result_invalid", "Evaluator+ candidate_fingerprint must be non-empty");
|
|
28
|
+
if (result.quality_baseline_fingerprint !== undefined && (typeof result.quality_baseline_fingerprint !== "string" || !SHA256.test(result.quality_baseline_fingerprint))) throw evaluatorError("evaluator_result_invalid", "Evaluator+ quality_baseline_fingerprint must be a SHA-256 hex digest");
|
|
29
|
+
if (!VERDICTS.has(result.verdict)) throw evaluatorError("evaluator_result_invalid", "Evaluator+ verdict must be PASS, FAIL, or INDETERMINATE");
|
|
30
|
+
if (typeof result.summary !== "string" || !result.summary.trim()) throw evaluatorError("evaluator_result_invalid", "Evaluator+ summary must be non-empty");
|
|
31
|
+
if (!Array.isArray(result.evidence_refs) || result.evidence_refs.some((ref) => typeof ref !== "string")) throw evaluatorError("evaluator_result_invalid", "Evaluator+ evidence_refs must be an array of strings");
|
|
32
|
+
if (typeof result.created_at !== "string" || !DATE_TIME.test(result.created_at) || Number.isNaN(Date.parse(result.created_at))) throw evaluatorError("evaluator_result_invalid", "Evaluator+ created_at must be an RFC 3339 date-time");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function aggregateEvaluatorVerdict(criteria) {
|
|
36
|
+
if (!Array.isArray(criteria)) throw evaluatorError("evaluator_result_invalid", "Evaluator+ criteria must be an array");
|
|
37
|
+
if (criteria.some((criterion) => criterion.verdict === "FAIL")) return "FAIL";
|
|
38
|
+
if (criteria.some((criterion) => criterion.verdict === "INDETERMINATE")) return "INDETERMINATE";
|
|
39
|
+
return "PASS";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateCriterionShape(criterion) {
|
|
43
|
+
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) throw evaluatorError("evaluator_result_invalid", "Evaluator+ criterion entries must be objects");
|
|
44
|
+
rejectUnknownProperties(criterion, new Set(["id", "scope", "verdict", "summary", "evidence_refs"]), "Evaluator+ criterion");
|
|
45
|
+
if (typeof criterion.id !== "string" || !criterion.id.trim()) throw evaluatorError("evaluator_result_invalid", "Evaluator+ criterion id must be non-empty");
|
|
46
|
+
if (criterion.scope !== "product_quality") throw evaluatorError("evaluator_result_invalid", `Evaluator+ criterion ${criterion.id} is not product_quality`);
|
|
47
|
+
if (!VERDICTS.has(criterion.verdict)) throw evaluatorError("evaluator_result_invalid", `Evaluator+ criterion ${criterion.id} has an invalid verdict`);
|
|
48
|
+
if (typeof criterion.summary !== "string" || !criterion.summary.trim()) throw evaluatorError("evaluator_result_invalid", `Evaluator+ criterion ${criterion.id} summary must be non-empty`);
|
|
49
|
+
if (!Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.some((ref) => typeof ref !== "string")) throw evaluatorError("evaluator_result_invalid", `Evaluator+ criterion ${criterion.id} evidence_refs must be an array of strings`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate an Evaluator+ result. Without a quality baseline the v1.7 legacy
|
|
54
|
+
* result contract remains valid. With a baseline, every product-quality
|
|
55
|
+
* criterion must be covered exactly once and the aggregate is deterministic.
|
|
56
|
+
*/
|
|
57
|
+
export function validateEvaluatorResult(result, { qualityBaseline = null, expectedCandidateFingerprint = null } = {}) {
|
|
58
|
+
requireBaseResult(result);
|
|
59
|
+
if (!qualityBaseline) {
|
|
60
|
+
if (result.criteria !== undefined) {
|
|
61
|
+
if (!Array.isArray(result.criteria)) throw evaluatorError("evaluator_result_invalid", "Evaluator+ criteria must be an array");
|
|
62
|
+
for (const criterion of result.criteria) validateCriterionShape(criterion);
|
|
63
|
+
}
|
|
64
|
+
return { valid: true, legacy: true, verdict: result.verdict, result };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const baseline = qualityBaseline.manifest ?? qualityBaseline;
|
|
68
|
+
const validatedBaseline = validateQualityBaselineManifest(baseline);
|
|
69
|
+
const expectedBaselineFingerprint = validatedBaseline.fingerprint;
|
|
70
|
+
if (qualityBaseline.fingerprint !== undefined && qualityBaseline.fingerprint !== expectedBaselineFingerprint) {
|
|
71
|
+
throw evaluatorError("evaluator_quality_baseline_mismatch", "loaded Quality Baseline fingerprint does not match its canonical content");
|
|
72
|
+
}
|
|
73
|
+
if (result.quality_baseline_fingerprint !== expectedBaselineFingerprint) {
|
|
74
|
+
throw evaluatorError("evaluator_quality_baseline_mismatch", "Evaluator+ result quality baseline fingerprint does not match the approved contract");
|
|
75
|
+
}
|
|
76
|
+
if (typeof expectedCandidateFingerprint !== "string" || !expectedCandidateFingerprint.trim()) {
|
|
77
|
+
throw evaluatorError("evaluator_candidate_binding_missing", "Quality Contract Evaluator+ validation requires an expected candidate fingerprint");
|
|
78
|
+
}
|
|
79
|
+
if (result.candidate_fingerprint !== expectedCandidateFingerprint) {
|
|
80
|
+
throw evaluatorError("evaluator_candidate_fingerprint_mismatch", "Evaluator+ result candidate fingerprint does not match the approved candidate");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { product_quality: expectedCriteria, delivery_closure: deliveryCriteria } = qualityCriteriaByScope(baseline);
|
|
84
|
+
if (!Array.isArray(result.criteria)) throw evaluatorError("evaluator_result_invalid", "Quality Contract Evaluator+ result must include criteria");
|
|
85
|
+
const expectedById = new Map(expectedCriteria.map((criterion) => [criterion.id, criterion]));
|
|
86
|
+
const deliveryIds = new Set(deliveryCriteria.map((criterion) => criterion.id));
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const criterion of result.criteria) {
|
|
89
|
+
validateCriterionShape(criterion);
|
|
90
|
+
if (seen.has(criterion.id)) throw evaluatorError("evaluator_result_invalid", `duplicate Evaluator+ criterion: ${criterion.id}`);
|
|
91
|
+
seen.add(criterion.id);
|
|
92
|
+
if (deliveryIds.has(criterion.id)) throw evaluatorError("evaluator_result_invalid", `delivery_closure criterion was sent to Evaluator+: ${criterion.id}`);
|
|
93
|
+
if (!expectedById.has(criterion.id)) throw evaluatorError("evaluator_result_invalid", `unexpected product_quality criterion: ${criterion.id}`);
|
|
94
|
+
}
|
|
95
|
+
for (const criterion of expectedCriteria) {
|
|
96
|
+
if (!seen.has(criterion.id)) throw evaluatorError("evaluator_result_invalid", `missing product_quality criterion: ${criterion.id}`);
|
|
97
|
+
}
|
|
98
|
+
const aggregate = aggregateEvaluatorVerdict(result.criteria);
|
|
99
|
+
if (result.verdict !== aggregate) throw evaluatorError("evaluator_result_invalid", `Evaluator+ aggregate verdict must be ${aggregate}`);
|
|
100
|
+
return {
|
|
101
|
+
valid: true,
|
|
102
|
+
legacy: false,
|
|
103
|
+
verdict: aggregate,
|
|
104
|
+
quality_baseline_fingerprint: expectedBaselineFingerprint,
|
|
105
|
+
product_quality_criteria: result.criteria,
|
|
106
|
+
excluded_delivery_closure_ids: deliveryCriteria.map((criterion) => criterion.id),
|
|
107
|
+
result,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateClosureCriterion(criterion, expectedScope = "delivery_closure") {
|
|
112
|
+
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) throw evaluatorError("delivery_closure_invalid", "delivery closure criteria must be objects");
|
|
113
|
+
const allowed = new Set(["id", "scope", "verdict", "summary", "evidence_refs"]);
|
|
114
|
+
for (const key of Object.keys(criterion)) if (!allowed.has(key)) throw evaluatorError("delivery_closure_invalid", `delivery closure criterion contains unknown property: ${key}`);
|
|
115
|
+
if (typeof criterion.id !== "string" || !criterion.id.trim()) throw evaluatorError("delivery_closure_invalid", "delivery closure criterion id must be non-empty");
|
|
116
|
+
if (criterion.scope !== expectedScope) throw evaluatorError("delivery_closure_invalid", `delivery closure criterion ${criterion.id} has the wrong scope`);
|
|
117
|
+
if (!VERDICTS.has(criterion.verdict)) throw evaluatorError("delivery_closure_invalid", `delivery closure criterion ${criterion.id} has an invalid verdict`);
|
|
118
|
+
if (typeof criterion.summary !== "string" || !criterion.summary.trim()) throw evaluatorError("delivery_closure_invalid", `delivery closure criterion ${criterion.id} summary must be non-empty`);
|
|
119
|
+
if (!Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.some((ref) => typeof ref !== "string")) throw evaluatorError("delivery_closure_invalid", `delivery closure criterion ${criterion.id} evidence_refs must be an array of strings`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validate the separate delivery-closure criterion record. */
|
|
123
|
+
export function validateDeliveryClosureCriteria(qualityBaseline, results) {
|
|
124
|
+
const baseline = qualityBaseline?.manifest ?? qualityBaseline;
|
|
125
|
+
const validatedBaseline = validateQualityBaselineManifest(baseline);
|
|
126
|
+
const expected = qualityCriteriaByScope(baseline).delivery_closure;
|
|
127
|
+
if (!Array.isArray(results)) throw evaluatorError("delivery_closure_invalid", "delivery closure results must be an array");
|
|
128
|
+
const expectedIds = new Set(expected.map((criterion) => criterion.id));
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const result of results) {
|
|
131
|
+
validateClosureCriterion(result);
|
|
132
|
+
if (seen.has(result.id)) throw evaluatorError("delivery_closure_invalid", `duplicate delivery closure criterion: ${result.id}`);
|
|
133
|
+
seen.add(result.id);
|
|
134
|
+
if (!expectedIds.has(result.id)) throw evaluatorError("delivery_closure_invalid", `unexpected delivery closure criterion: ${result.id}`);
|
|
135
|
+
}
|
|
136
|
+
for (const criterion of expected) {
|
|
137
|
+
if (!seen.has(criterion.id)) throw evaluatorError("delivery_closure_invalid", `missing delivery closure criterion: ${criterion.id}`);
|
|
138
|
+
}
|
|
139
|
+
const verdict = aggregateEvaluatorVerdict(results);
|
|
140
|
+
if (qualityBaseline?.fingerprint !== undefined && qualityBaseline.fingerprint !== validatedBaseline.fingerprint) {
|
|
141
|
+
throw evaluatorError("delivery_closure_invalid", "loaded Quality Baseline fingerprint does not match its canonical content");
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
valid: true,
|
|
145
|
+
verdict,
|
|
146
|
+
can_deliver: verdict === "PASS",
|
|
147
|
+
quality_baseline_fingerprint: validatedBaseline.fingerprint,
|
|
148
|
+
criteria: results,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { VERDICTS };
|
|
153
|
+
|
|
154
|
+
function cliError(code, message) {
|
|
155
|
+
const error = new Error(message);
|
|
156
|
+
error.code = code;
|
|
157
|
+
return error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseArgs(argv) {
|
|
161
|
+
const args = { mode: null, project: process.cwd(), baseline: null, result: null, expectedCandidateFingerprint: null };
|
|
162
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
163
|
+
const flag = argv[index];
|
|
164
|
+
if (flag === "--help" || flag === "-h") {
|
|
165
|
+
process.stdout.write("Usage: node runtime/triad-evaluator-validate.mjs --mode <baseline|evaluator|delivery> --baseline <path> [--result <path>] [--project <root>] [--expected-candidate-fingerprint <sha256>]\n");
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const key = {
|
|
169
|
+
"--mode": "mode",
|
|
170
|
+
"--project": "project",
|
|
171
|
+
"--baseline": "baseline",
|
|
172
|
+
"--result": "result",
|
|
173
|
+
"--expected-candidate-fingerprint": "expectedCandidateFingerprint",
|
|
174
|
+
}[flag];
|
|
175
|
+
if (!key) throw cliError("quality_contract_cli_invalid", `unknown option: ${flag}`);
|
|
176
|
+
const value = argv[index + 1];
|
|
177
|
+
if (!value || value.startsWith("--")) throw cliError("quality_contract_cli_invalid", `${flag} requires a value`);
|
|
178
|
+
args[key] = value;
|
|
179
|
+
index += 1;
|
|
180
|
+
}
|
|
181
|
+
if (!args.mode || !["baseline", "evaluator", "delivery"].includes(args.mode)) throw cliError("quality_contract_cli_invalid", "--mode must be baseline, evaluator, or delivery");
|
|
182
|
+
if (args.mode !== "baseline" && !args.result) throw cliError("quality_contract_cli_invalid", "--result is required");
|
|
183
|
+
if ((args.mode === "baseline" || args.mode === "delivery") && !args.baseline) throw cliError("quality_contract_cli_invalid", `--baseline is required for ${args.mode} validation`);
|
|
184
|
+
return args;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function readJsonFile(root, value, label) {
|
|
188
|
+
const target = path.isAbsolute(value) ? value : path.resolve(root, value);
|
|
189
|
+
let source;
|
|
190
|
+
try {
|
|
191
|
+
source = await readFile(target, "utf8");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw cliError("quality_contract_cli_invalid", `${label} cannot be read: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(source);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw cliError("quality_contract_cli_invalid", `${label} is not valid JSON: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Explicit control-plane entry point. It reloads the baseline from disk on
|
|
204
|
+
* every invocation so Evaluator+ and delivery closure cannot rely on a stale
|
|
205
|
+
* manifest/source check performed earlier in the run.
|
|
206
|
+
*/
|
|
207
|
+
async function runCli() {
|
|
208
|
+
const args = parseArgs(process.argv.slice(2));
|
|
209
|
+
if (!args) return;
|
|
210
|
+
const projectRoot = await realpath(args.project);
|
|
211
|
+
const qualityBaseline = args.baseline
|
|
212
|
+
? await loadQualityBaseline(args.baseline, { projectRoot })
|
|
213
|
+
: null;
|
|
214
|
+
if (args.mode === "baseline") {
|
|
215
|
+
process.stdout.write(`${JSON.stringify({
|
|
216
|
+
valid: true,
|
|
217
|
+
mode: "baseline",
|
|
218
|
+
quality_baseline_fingerprint: qualityBaseline.fingerprint,
|
|
219
|
+
sources: qualityBaseline.sources,
|
|
220
|
+
})}\n`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (args.mode === "evaluator") {
|
|
224
|
+
const result = await readJsonFile(projectRoot, args.result, "Evaluator+ result");
|
|
225
|
+
if (qualityBaseline && (!args.expectedCandidateFingerprint || !SHA256.test(args.expectedCandidateFingerprint))) {
|
|
226
|
+
throw cliError("evaluator_candidate_binding_missing", "Quality Contract Evaluator+ validation requires --expected-candidate-fingerprint");
|
|
227
|
+
}
|
|
228
|
+
const validated = validateEvaluatorResult(result, {
|
|
229
|
+
qualityBaseline,
|
|
230
|
+
expectedCandidateFingerprint: qualityBaseline ? args.expectedCandidateFingerprint : null,
|
|
231
|
+
});
|
|
232
|
+
process.stdout.write(`${JSON.stringify({
|
|
233
|
+
valid: true,
|
|
234
|
+
mode: "evaluator",
|
|
235
|
+
legacy: validated.legacy,
|
|
236
|
+
verdict: validated.verdict,
|
|
237
|
+
quality_baseline_fingerprint: validated.quality_baseline_fingerprint ?? null,
|
|
238
|
+
candidate_fingerprint: result.candidate_fingerprint,
|
|
239
|
+
product_quality_criteria: validated.product_quality_criteria ?? null,
|
|
240
|
+
})}\n`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const results = await readJsonFile(projectRoot, args.result, "delivery closure results");
|
|
245
|
+
const validated = validateDeliveryClosureCriteria(qualityBaseline, results);
|
|
246
|
+
process.stdout.write(`${JSON.stringify({
|
|
247
|
+
valid: true,
|
|
248
|
+
mode: "delivery",
|
|
249
|
+
verdict: validated.verdict,
|
|
250
|
+
can_deliver: validated.can_deliver,
|
|
251
|
+
quality_baseline_fingerprint: validated.quality_baseline_fingerprint,
|
|
252
|
+
criteria: validated.criteria,
|
|
253
|
+
})}\n`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
257
|
+
runCli().catch((error) => {
|
|
258
|
+
process.stdout.write(`${JSON.stringify({ valid: false, error: { code: error.code ?? "quality_contract_cli_invalid", message: error.message } })}\n`);
|
|
259
|
+
process.exitCode = 2;
|
|
260
|
+
});
|
|
261
|
+
}
|
package/runtime/triad-verify.mjs
CHANGED
|
@@ -6,7 +6,9 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { writeAtomicJson } from "./lib/evidence.mjs";
|
|
7
7
|
import { calculateCandidateFingerprint, collectCandidateChanges, worktreeBranch } from "./lib/fingerprint.mjs";
|
|
8
8
|
import { executeGates, gateSelectionEvidence, loadTrustedGates, resolveGateSelection } from "./lib/gates.mjs";
|
|
9
|
+
import { resolveQualityContract } from "./lib/quality-baseline.mjs";
|
|
9
10
|
import { evaluateScopeContract, parseScopeContract } from "./lib/scope-contract.mjs";
|
|
11
|
+
import { resolveAssignmentContext, validateAssignmentPacket } from "./lib/assignment-packet.mjs";
|
|
10
12
|
|
|
11
13
|
const argv = process.argv.slice(2);
|
|
12
14
|
const option = (name) => {
|
|
@@ -24,6 +26,10 @@ function sha256(value) {
|
|
|
24
26
|
return createHash("sha256").update(value).digest("hex");
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function evidenceFingerprint(value) {
|
|
30
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value) ? value : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
async function sha256File(value) {
|
|
28
34
|
return sha256(await readFile(value));
|
|
29
35
|
}
|
|
@@ -65,7 +71,7 @@ async function resolveAssignment(projectRoot, trigger, explicitAssignment) {
|
|
|
65
71
|
return { assignmentPath, assignment: JSON.parse(source), assignmentHash: sha256(source) };
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
async function buildInvalidEvidence({ runId, trigger, assignment, reason, outputPath, failureCode = "verification_context_invalid", gateSelection = null }) {
|
|
74
|
+
async function buildInvalidEvidence({ runId, trigger, assignment, reason, outputPath, failureCode = "verification_context_invalid", gateSelection = null, qualityBaselineFingerprint = null, assignmentPacket = null }) {
|
|
69
75
|
const evidence = {
|
|
70
76
|
schema_version: 1,
|
|
71
77
|
run_id: runId,
|
|
@@ -77,9 +83,13 @@ async function buildInvalidEvidence({ runId, trigger, assignment, reason, output
|
|
|
77
83
|
baseline: {
|
|
78
84
|
prd_sha256: assignment?.expected_prd_sha256 ?? null,
|
|
79
85
|
card_sha256: assignment?.expected_card_sha256 ?? null,
|
|
86
|
+
quality_baseline_fingerprint: evidenceFingerprint(qualityBaselineFingerprint ?? assignment?.expected_quality_baseline_fingerprint),
|
|
80
87
|
git_head: null,
|
|
81
88
|
candidate_fingerprint: null,
|
|
82
89
|
},
|
|
90
|
+
assignment_packet: assignmentPacket ?? (assignment?.assignment_packet_path || assignment?.assignment_packet_sha256
|
|
91
|
+
? { path: assignment.assignment_packet_path ?? null, sha256: assignment.assignment_packet_sha256 ?? null }
|
|
92
|
+
: null),
|
|
83
93
|
gates: [],
|
|
84
94
|
required_gates_passed: false,
|
|
85
95
|
status: "invalid_context",
|
|
@@ -147,6 +157,8 @@ async function main() {
|
|
|
147
157
|
let assignmentPath;
|
|
148
158
|
let outputPath;
|
|
149
159
|
let gateSelection = null;
|
|
160
|
+
let qualityBaseline = null;
|
|
161
|
+
let assignmentPacket = null;
|
|
150
162
|
try {
|
|
151
163
|
let assignmentHash;
|
|
152
164
|
({ assignmentPath, assignment, assignmentHash } = await resolveAssignment(projectRoot, trigger, option("--assignment")));
|
|
@@ -160,12 +172,20 @@ async function main() {
|
|
|
160
172
|
if (!assignment.assignment_id) throw new Error("assignment ID is required");
|
|
161
173
|
if (assignment.agent_id !== trigger.agent_id || assignment.agent_type !== "triad_developer") throw new Error("developer assignment does not match trigger");
|
|
162
174
|
if ((await realpath(path.resolve(assignment.project_root ?? projectRoot))) !== projectRoot) throw new Error("assignment project root mismatch");
|
|
163
|
-
const
|
|
164
|
-
|
|
175
|
+
const assignmentContext = await resolveAssignmentContext(assignment, { projectRoot });
|
|
176
|
+
const worktree = assignmentContext.worktree;
|
|
165
177
|
const prdPath = path.resolve(projectRoot, assignment.prd_path ?? "artifacts/prd.md");
|
|
166
178
|
const cardPath = path.resolve(projectRoot, assignment.card_path);
|
|
167
179
|
await access(prdPath);
|
|
168
180
|
await access(cardPath);
|
|
181
|
+
// The packet is an immutable, shared assignment contract. Validate its
|
|
182
|
+
// binding before skills, scope, or expensive gates so a stale packet is
|
|
183
|
+
// reported as invalid context and consumes no retry budget.
|
|
184
|
+
assignmentPacket = await validateAssignmentPacket(assignment, projectRoot);
|
|
185
|
+
// Resolve the optional immutable Quality Contract before any baseline or
|
|
186
|
+
// expensive-gate check so a bound source drift keeps its precise failure
|
|
187
|
+
// classification (including when the source is the PRD itself).
|
|
188
|
+
qualityBaseline = await resolveQualityContract(assignment, projectRoot);
|
|
169
189
|
if ((await sha256File(prdPath)) !== assignment.expected_prd_sha256) throw new Error("PRD baseline hash mismatch");
|
|
170
190
|
if ((await sha256File(cardPath)) !== assignment.expected_card_sha256) throw new Error("feature card hash mismatch");
|
|
171
191
|
const repositorySkills = await validateRepositorySkills(assignment.required_repository_skills, worktree);
|
|
@@ -203,11 +223,13 @@ async function main() {
|
|
|
203
223
|
baseline: {
|
|
204
224
|
prd_sha256: assignment.expected_prd_sha256,
|
|
205
225
|
card_sha256: assignment.expected_card_sha256,
|
|
226
|
+
quality_baseline_fingerprint: qualityBaseline?.fingerprint ?? null,
|
|
206
227
|
gates_sha256: null,
|
|
207
228
|
git_head: before.git_head,
|
|
208
229
|
candidate_fingerprint: before.value,
|
|
209
230
|
branch,
|
|
210
231
|
},
|
|
232
|
+
assignment_packet: assignmentPacket,
|
|
211
233
|
repository_skills: repositorySkills,
|
|
212
234
|
scope,
|
|
213
235
|
gates: [],
|
|
@@ -237,13 +259,15 @@ async function main() {
|
|
|
237
259
|
trigger,
|
|
238
260
|
assignment_ref: path.relative(projectRoot, assignmentPath),
|
|
239
261
|
baseline: {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
prd_sha256: assignment.expected_prd_sha256,
|
|
263
|
+
card_sha256: assignment.expected_card_sha256,
|
|
264
|
+
quality_baseline_fingerprint: qualityBaseline?.fingerprint ?? null,
|
|
265
|
+
gates_sha256: trusted.actualHash,
|
|
243
266
|
git_head: before.git_head,
|
|
244
267
|
candidate_fingerprint: before.value,
|
|
245
268
|
branch,
|
|
246
269
|
},
|
|
270
|
+
assignment_packet: assignmentPacket,
|
|
247
271
|
repository_skills: repositorySkills,
|
|
248
272
|
scope,
|
|
249
273
|
gate_selection: gateSelectionEvidence(gateSelection),
|
|
@@ -269,7 +293,9 @@ async function main() {
|
|
|
269
293
|
reason: error.message,
|
|
270
294
|
outputPath,
|
|
271
295
|
failureCode: error.code ?? "verification_context_invalid",
|
|
272
|
-
gateSelection
|
|
296
|
+
gateSelection,
|
|
297
|
+
qualityBaselineFingerprint: qualityBaseline?.fingerprint ?? null,
|
|
298
|
+
assignmentPacket,
|
|
273
299
|
});
|
|
274
300
|
process.stdout.write(`${JSON.stringify({ run_id: runId, status: evidence.status, evidence: outputPath ?? null })}\n`);
|
|
275
301
|
process.exitCode = 3;
|
|
@@ -8,9 +8,25 @@
|
|
|
8
8
|
"schema_version": { "const": 1 },
|
|
9
9
|
"feature_id": { "type": "string", "minLength": 1 },
|
|
10
10
|
"candidate_fingerprint": { "type": "string", "minLength": 1 },
|
|
11
|
+
"quality_baseline_fingerprint": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" },
|
|
11
12
|
"verdict": { "enum": ["PASS", "FAIL", "INDETERMINATE"] },
|
|
12
13
|
"summary": { "type": "string", "minLength": 1 },
|
|
13
14
|
"evidence_refs": { "type": "array", "items": { "type": "string" } },
|
|
15
|
+
"criteria": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["id", "scope", "verdict", "summary", "evidence_refs"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": { "type": "string", "minLength": 1 },
|
|
22
|
+
"scope": { "const": "product_quality" },
|
|
23
|
+
"verdict": { "enum": ["PASS", "FAIL", "INDETERMINATE"] },
|
|
24
|
+
"summary": { "type": "string", "minLength": 1 },
|
|
25
|
+
"evidence_refs": { "type": "array", "items": { "type": "string" } }
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
14
30
|
"created_at": { "type": "string", "format": "date-time" }
|
|
15
31
|
},
|
|
16
32
|
"additionalProperties": false
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://triad.engineering/schemas/quality-baseline.schema.json",
|
|
4
|
+
"title": "Triad immutable Quality Baseline",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "id", "revision", "sources", "criteria", "fingerprint"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"id": { "type": "string", "minLength": 1 },
|
|
11
|
+
"revision": { "type": "integer", "minimum": 1 },
|
|
12
|
+
"sources": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"minItems": 1,
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["id", "role", "path", "sha256"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"id": { "type": "string", "minLength": 1 },
|
|
21
|
+
"role": { "type": "string", "minLength": 1 },
|
|
22
|
+
"path": { "type": "string", "minLength": 1, "pattern": "^(?![\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+" },
|
|
23
|
+
"sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"criteria": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["id", "scope", "requirement"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "string", "minLength": 1 },
|
|
35
|
+
"scope": { "enum": ["product_quality", "delivery_closure"] },
|
|
36
|
+
"requirement": { "type": "string", "minLength": 1 }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"fingerprint": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
"assignment_id": { "type": ["string", "null"] },
|
|
13
13
|
"assignment_sha256": { "type": ["string", "null"] },
|
|
14
14
|
"trigger": { "type": "object", "required": ["event", "agent_id", "agent_type"], "properties": { "event": { "type": "string" }, "agent_id": { "type": ["string", "null"] }, "agent_type": { "type": ["string", "null"] } } },
|
|
15
|
-
"baseline": { "type": "object", "required": ["prd_sha256", "card_sha256", "git_head", "candidate_fingerprint"], "properties": { "prd_sha256": { "type": ["string", "null"] }, "card_sha256": { "type": ["string", "null"] }, "git_head": { "type": ["string", "null"] }, "candidate_fingerprint": { "type": ["string", "null"] }, "branch": { "type": ["string", "null"] } } },
|
|
15
|
+
"baseline": { "type": "object", "required": ["prd_sha256", "card_sha256", "git_head", "candidate_fingerprint"], "properties": { "prd_sha256": { "type": ["string", "null"] }, "card_sha256": { "type": ["string", "null"] }, "quality_baseline_fingerprint": { "type": ["string", "null"], "pattern": "^[a-fA-F0-9]{64}$" }, "git_head": { "type": ["string", "null"] }, "candidate_fingerprint": { "type": ["string", "null"] }, "branch": { "type": ["string", "null"] } } },
|
|
16
|
+
"assignment_packet": {
|
|
17
|
+
"type": ["object", "null"],
|
|
18
|
+
"required": ["path", "sha256"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": { "type": ["string", "null"] },
|
|
21
|
+
"sha256": { "type": ["string", "null"], "pattern": "^[a-fA-F0-9]{64}$" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
16
24
|
"scope": {
|
|
17
25
|
"type": "object",
|
|
18
26
|
"properties": {
|
|
@@ -13,13 +13,36 @@ Collect only missing essentials: project ID/title, readable PRD source, target
|
|
|
13
13
|
repositories with base/project branches and worktrees, measurable success
|
|
14
14
|
conditions, runnable quality gates, practical-test need, and integration need.
|
|
15
15
|
|
|
16
|
+
## Native BMAD planning boundary
|
|
17
|
+
|
|
18
|
+
When the owner input is a native BMAD `epics.md` artifact, BMAD is the
|
|
19
|
+
planning authority for Epic/Story boundaries and acceptance criteria. The
|
|
20
|
+
canonical Stories in that read-only artifact are the sole planning boundaries:
|
|
21
|
+
one canonical Story maps to one normal Triad Card. Bootstrap must not independently
|
|
22
|
+
re-decompose the PRD into another feature-card plan, create a second Story set,
|
|
23
|
+
or invoke an LLM to decide native Story boundaries. The native flow is
|
|
24
|
+
BMAD `epics.md` → canonical Stories → Triad Cards; the generic PRD-only
|
|
25
|
+
feature-plan step below does not apply to that input. Triad remains execution
|
|
26
|
+
authority after the planning handoff.
|
|
27
|
+
|
|
16
28
|
1. Create `<control-repository>/projects/<project-id>/`.
|
|
17
29
|
2. Copy `assets/project.yaml` to `project.yaml`; register only declared product
|
|
18
30
|
repositories, branches, and worktrees.
|
|
19
31
|
3. Copy the approved PRD to `artifacts/prd.md`; record source, collection time,
|
|
20
32
|
snapshot, revision when available, and SHA-256.
|
|
21
|
-
4. Copy `assets/loop-template/` to `.loop
|
|
22
|
-
under `features/` and a complete
|
|
33
|
+
4. Copy `assets/loop-template/` to `.loop/`. For ordinary PRD-only input,
|
|
34
|
+
create bounded feature cards under `features/` and a complete
|
|
35
|
+
`feature-plan.md`. For native BMAD input, ingest the canonical Stories and
|
|
36
|
+
materialize one normal Card per Story instead; do not perform a second
|
|
37
|
+
semantic decomposition.
|
|
38
|
+
When an owner-approved Quality Bar or additional intent/architecture source
|
|
39
|
+
exists, copy `assets/loop-template/quality-baseline.json`, fill only the
|
|
40
|
+
declared source hashes and criteria, compute its canonical fingerprint, and
|
|
41
|
+
bind `project.quality_contract.baseline` plus `fingerprint`. Use
|
|
42
|
+
`criteria: []` when no machine-readable Quality Bar was supplied; never
|
|
43
|
+
invent criteria. The manifest is immutable for the run and a requirement
|
|
44
|
+
change creates a new revision/rebaseline event rather than editing it in
|
|
45
|
+
place.
|
|
23
46
|
5. Replace every gate placeholder. Gate executors in v1 are only
|
|
24
47
|
`control-plane`; remove a non-applicable gate with a recorded reason instead
|
|
25
48
|
of declaring manual or MCP execution.
|
|
@@ -39,10 +62,30 @@ conditions, runnable quality gates, practical-test need, and integration need.
|
|
|
39
62
|
The normal state route is `draft → ready → in_progress → verifying → in_review
|
|
40
63
|
→ approved|rework|blocked`. The Orchestrator owns those records. Before each
|
|
41
64
|
Developer dispatch, write an active assignment with a unique assignment ID,
|
|
42
|
-
feature, attempt, expected branch, worktree, PRD/card/gate
|
|
43
|
-
run ID.
|
|
65
|
+
feature, repository ID, attempt, expected branch, worktree, PRD/card/gate
|
|
66
|
+
hashes, and verifier run ID. Populate only the packet's bounded `context`
|
|
67
|
+
fields (relevant excerpts, acceptance, verification mapping, touch points,
|
|
68
|
+
constraints, risks, and prior evidence). Then create its immutable Assignment Packet and bind the returned path
|
|
69
|
+
and SHA-256 in that assignment:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node .triad-runtime/triad-assignment-packet.mjs \
|
|
73
|
+
--project /absolute/path/to/control-workspace \
|
|
74
|
+
--assignment .loop/runtime/assignments/<assignment-file>.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The command returns JSON containing the packet path and the dispatch context;
|
|
78
|
+
launch the delegated role with `dispatch.cwd` set to the declared product
|
|
79
|
+
worktree. The verifier produces environment-derived evidence; it never changes
|
|
44
80
|
card state.
|
|
45
81
|
|
|
82
|
+
The control workspace and product worktree may be separate directories. An
|
|
83
|
+
external worktree is authorized by an exact repository mapping in
|
|
84
|
+
`project.yaml` (`repositories[].worktree`, falling back to `path`), not by an
|
|
85
|
+
assignment escape hatch. A mapped external worktree is valid even when the
|
|
86
|
+
legacy `allow_external_worktree` field is absent or false; an unmapped external
|
|
87
|
+
path fails closed before dispatch or gate execution.
|
|
88
|
+
|
|
46
89
|
Use local worktrees for enabled multi-repository integration and declare a final
|
|
47
90
|
integration card. Do not publish packages, alter registry versions, or create a
|
|
48
91
|
release as a bootstrap/integration step. Re-baseline only after an explicit owner
|
|
@@ -22,6 +22,19 @@
|
|
|
22
22
|
|
|
23
23
|
- Evidence/review chain: `<verification/review/evaluator references>`
|
|
24
24
|
|
|
25
|
+
## Quality Contract closure
|
|
26
|
+
|
|
27
|
+
- Quality Baseline: `<path, revision, fingerprint or legacy PRD-only mode>`
|
|
28
|
+
- Product-quality Evaluator+ verdict: `<PASS|FAIL|INDETERMINATE|not configured>`
|
|
29
|
+
|
|
30
|
+
| Criterion | Scope | Verdict | Evidence references |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `<delivery criterion ID>` | `delivery_closure` | `<PASS|FAIL|INDETERMINATE>` | `<refs>` |
|
|
33
|
+
|
|
34
|
+
Delivery is `delivered` only when every configured `delivery_closure` criterion
|
|
35
|
+
is recorded as `PASS`. Product-quality criteria are evaluated by the fresh
|
|
36
|
+
Evaluator+ packet and are never silently evaluated during delivery closure.
|
|
37
|
+
|
|
25
38
|
## Local-worktree integration
|
|
26
39
|
|
|
27
40
|
| Card | Consumer worktree | Provider worktree | Branch/commit map | Setup command | Data/event/callback gate |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"id": "REPLACE_ME_QUALITY_BASELINE_ID",
|
|
4
|
+
"revision": 1,
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"id": "prd",
|
|
8
|
+
"role": "intent",
|
|
9
|
+
"path": "artifacts/prd.md",
|
|
10
|
+
"sha256": "REPLACE_ME_SHA256"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"criteria": [],
|
|
14
|
+
"fingerprint": "REPLACE_ME_SHA256"
|
|
15
|
+
}
|
|
@@ -7,9 +7,16 @@ delivery:
|
|
|
7
7
|
handoff: null
|
|
8
8
|
branches: []
|
|
9
9
|
evaluator_report: null
|
|
10
|
+
quality_criteria: [] # delivery_closure results; product_quality stays in Evaluator+
|
|
10
11
|
delivered_at: null
|
|
11
12
|
owner_message: null
|
|
12
13
|
|
|
14
|
+
quality_contract:
|
|
15
|
+
baseline: null
|
|
16
|
+
fingerprint: null
|
|
17
|
+
revision: null
|
|
18
|
+
rebaseline_events: []
|
|
19
|
+
|
|
13
20
|
feature_plan:
|
|
14
21
|
path: feature-plan.md
|
|
15
22
|
status: declared
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"feature_id": "REPLACE_ME_FEATURE_ID",
|
|
8
8
|
"attempt": 1,
|
|
9
9
|
"project_root": "REPLACE_ME_ABSOLUTE_CONTROL_WORKSPACE",
|
|
10
|
+
"repository_id": "REPLACE_ME_REPOSITORY_ID",
|
|
10
11
|
"worktree": "REPLACE_ME_DECLARED_WORKTREE_OR_ABSOLUTE_PATH",
|
|
11
12
|
"expected_branch": "REPLACE_ME_PROJECT_BRANCH",
|
|
12
13
|
"allow_external_worktree": false,
|
|
@@ -16,6 +17,8 @@
|
|
|
16
17
|
"expected_prd_sha256": "REPLACE_ME_SHA256",
|
|
17
18
|
"expected_card_sha256": "REPLACE_ME_SHA256",
|
|
18
19
|
"expected_gates_sha256": "REPLACE_ME_SHA256",
|
|
20
|
+
"quality_baseline_path": null,
|
|
21
|
+
"expected_quality_baseline_fingerprint": null,
|
|
19
22
|
"required_gate_ids": [],
|
|
20
23
|
"scope_contract": null,
|
|
21
24
|
"required_repository_skills": [
|
|
@@ -29,5 +32,17 @@
|
|
|
29
32
|
}
|
|
30
33
|
],
|
|
31
34
|
"verification_run_id": "REPLACE_ME_UUID",
|
|
32
|
-
"evidence_directory": ".loop/evidence/REPLACE_ME_FEATURE_ID/attempt-001"
|
|
35
|
+
"evidence_directory": ".loop/evidence/REPLACE_ME_FEATURE_ID/attempt-001",
|
|
36
|
+
"assignment_packet_path": null,
|
|
37
|
+
"assignment_packet_sha256": null,
|
|
38
|
+
"context": {
|
|
39
|
+
"relevant_prd_excerpts": [],
|
|
40
|
+
"relevant_adr_excerpts": [],
|
|
41
|
+
"acceptance_criteria": [],
|
|
42
|
+
"verification_mapping": [],
|
|
43
|
+
"expected_paths": [],
|
|
44
|
+
"constraints": [],
|
|
45
|
+
"risks": [],
|
|
46
|
+
"previous_evidence": []
|
|
47
|
+
}
|
|
33
48
|
}
|
|
@@ -12,6 +12,11 @@ project:
|
|
|
12
12
|
snapshot: artifacts/prd.md
|
|
13
13
|
sha256: null
|
|
14
14
|
source_revision: null
|
|
15
|
+
quality_contract:
|
|
16
|
+
# Optional immutable target for v1.8 projects. Leave both values null for
|
|
17
|
+
# legacy PRD-only behavior; bootstrap fills them only from declared sources.
|
|
18
|
+
baseline: null # artifacts/quality-baseline.json
|
|
19
|
+
fingerprint: null
|
|
15
20
|
loop: .loop
|
|
16
21
|
control_plane:
|
|
17
22
|
verification: optional
|