@deksden-com/dd-flow-cli 0.9.0-beta.30 → 0.9.0-beta.32
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 +14 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +12 -7
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/services/eval-snapshots.js +2 -0
- package/dist/services/vnext-plan-review.js +4 -1
- package/dist/services/work-registry.js +20 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Reject reviewer results without per-aspect evidence when the Work is finished, and distinguish missing evidence from an aspect-coverage mismatch in legacy stage validation.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.31
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Treat an authoritative fully-idle Antigravity root Stop as settlement evidence
|
|
14
|
+
for otherwise unobserved native children, while preserving their distinct
|
|
15
|
+
`settled_by_root` status rather than claiming individual completion.
|
|
16
|
+
|
|
3
17
|
## 0.9.0-beta.30
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.32",
|
|
4
|
+
"cli_commit": "2d5d953e65e996d9f57d1d4edec97a730c578acb",
|
|
5
|
+
"built_at": "2026-09-07T18:27:14.770Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.6",
|
|
8
8
|
"commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
|
|
@@ -222,13 +222,17 @@ export class Runtime {
|
|
|
222
222
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
223
223
|
this.lastResult = result; this.markActivity();
|
|
224
224
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
225
|
-
|
|
225
|
+
const pendingTerminal = current && result.status === "SUCCESS";
|
|
226
|
+
if (pendingTerminal) current.terminalResult = result;
|
|
227
|
+
if (pendingTerminal && rootStop?.fullyIdle === true) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" });
|
|
228
|
+
const terminalReceipt = pendingTerminal ? this.receipt(result) : null;
|
|
229
|
+
await this.persist({ active_tree: pendingTerminal ? !terminalReceipt.settled : result.status === "RUNNING" || rootStop?.fullyIdle === false, last_result: result });
|
|
226
230
|
try { this.usageIngest = await forwardUsage(this.config, result.conversation_id ?? this.init.conversation_id, result, this.toolSnapshot(), [...this.descendants.values()]); }
|
|
227
231
|
catch (error) { this.usageIngest = { status: "failed", error: error.message ?? String(error) }; await this.journal("usage_ingest_failed", this.usageIngest); }
|
|
228
232
|
if (!current) return;
|
|
229
233
|
if (this.active !== current) return;
|
|
230
|
-
this.active = null;
|
|
231
234
|
if (result.status === "ERROR" || result.status === "CANCELLED") {
|
|
235
|
+
this.active = null;
|
|
232
236
|
const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
|
|
233
237
|
const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
|
|
234
238
|
: /rate.?limit|too many requests|\b429\b/.test(normalized) ? "agy_provider_rate_limited"
|
|
@@ -236,17 +240,18 @@ export class Runtime {
|
|
|
236
240
|
: "agy_provider_failed";
|
|
237
241
|
return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
238
242
|
}
|
|
239
|
-
current.resolve(
|
|
243
|
+
if (terminalReceipt.settled) { this.active = null; current.resolve(terminalReceipt); }
|
|
240
244
|
}
|
|
241
245
|
receipt(result = this.lastResult) {
|
|
242
246
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
243
247
|
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
244
|
-
const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled"].includes(child.status));
|
|
248
|
+
const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
|
|
245
249
|
// A terminal root result is the provider's authoritative end-of-turn
|
|
246
250
|
// observation when AGY does not emit a root Stop hook. A received
|
|
247
251
|
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
248
252
|
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
249
|
-
|
|
253
|
+
const terminalPending = this.active?.terminalResult === result;
|
|
254
|
+
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean((!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
250
255
|
}
|
|
251
256
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
252
257
|
async prompt(text, assertDispatch = captureDispatchGuard(this)) { if (!text) throw new DaemonError("prompt_required", "prompt is required"); await this.start(); await this.draining; if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running"); if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled"); const root = this.init?.conversation_id; const prior = this.sessionObservations.get(root) ?? {}; this.turnGeneration++; this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null }); this.lastProviderEvent = null; await this.persist({ active_tree: true }); return await new Promise((resolve, reject) => { assertDispatch(); this.active = { resolve, reject, generation: this.turnGeneration, previousError: this.lastResult?.error ?? null }; this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => { if (error) { this.active = null; reject(error); } }); }); }
|
|
@@ -258,10 +263,10 @@ export class Runtime {
|
|
|
258
263
|
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
259
264
|
return child;
|
|
260
265
|
}
|
|
261
|
-
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && Number.isInteger(floor) && step <= floor; const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); const parent = child?.parent_provider_session_id ?? null;
|
|
266
|
+
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && Number.isInteger(floor) && step <= floor; const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const settled = terminalPending ? this.receipt(this.lastResult) : null; const completed = settled?.settled ? this.active : null; if (completed) this.active = null; const parent = child?.parent_provider_session_id ?? null;
|
|
262
267
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
263
268
|
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
264
|
-
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null }); return { event_id: eventId, daemon_id: this.config.daemonId, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
269
|
+
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(completed ? { active_tree: false } : {}); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null }); completed?.resolve(settled); return { event_id: eventId, daemon_id: this.config.daemonId, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
265
270
|
}
|
|
266
271
|
async cancel() { cancelPendingDispatch(this); if (!this.child || this.exited) return { ...this.receipt(), settled: Boolean(this.exited) }; try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
267
272
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"schema_id": {"const": "dd-flow/code-review-result@1"},
|
|
9
9
|
"verdict": {"enum": ["pass", "findings", "blocked"]},
|
|
10
10
|
"summary": {"type": "string", "minLength": 1},
|
|
11
|
-
"aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "items": {"type": "string"}}}}},
|
|
11
|
+
"aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}},
|
|
12
12
|
"findings": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["finding_id", "aspect_id", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs"], "properties": {"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"}, "aspect_id": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "obligation_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"aspect_id": {"type": "string", "minLength": 1},
|
|
21
21
|
"verdict": {"enum": ["pass", "watch", "needs_changes", "blocked"]},
|
|
22
22
|
"summary": {"type": "string", "minLength": 1},
|
|
23
|
-
"evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
|
23
|
+
"evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
24
24
|
"findings": {
|
|
25
25
|
"type": "array",
|
|
26
26
|
"items": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"},
|
|
32
32
|
"severity": {"enum": ["blocker", "high", "medium", "low", "info"]},
|
|
33
33
|
"summary": {"type": "string", "minLength": 1},
|
|
34
|
-
"evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}}
|
|
34
|
+
"evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -8,6 +8,7 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
8
8
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
10
10
|
import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
11
|
+
import { assertRunWorkResultArtifacts } from "./work-registry.js";
|
|
11
12
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
12
13
|
export function createEvalBootstrapSnapshot(context, input) {
|
|
13
14
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -80,6 +81,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
80
81
|
const project = requireProjectByRoot(context, projectRoot);
|
|
81
82
|
const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
|
|
82
83
|
assertDedicatedHome(context, project.id, input.runId);
|
|
84
|
+
assertRunWorkResultArtifacts(context, project.id, input.runId);
|
|
83
85
|
const recovery = typeof input.recoveryId === "string";
|
|
84
86
|
if (recovery) {
|
|
85
87
|
const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
|
|
@@ -423,8 +423,11 @@ function reviewerEvidenceFailures(context, input) {
|
|
|
423
423
|
continue;
|
|
424
424
|
}
|
|
425
425
|
const actual = result.aspects.map((item) => item.aspect_id);
|
|
426
|
-
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id))
|
|
426
|
+
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
|
|
427
427
|
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
|
|
428
|
+
const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
|
|
429
|
+
if (missingEvidence.length)
|
|
430
|
+
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
|
|
428
431
|
for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
|
|
429
432
|
try {
|
|
430
433
|
assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
|
|
@@ -182,6 +182,17 @@ export function listWorks(context, input) {
|
|
|
182
182
|
}) };
|
|
183
183
|
}
|
|
184
184
|
export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
|
|
185
|
+
/** DB rows are authoritative; reject snapshots that would preserve a rewritten Work receipt. */
|
|
186
|
+
export function assertRunWorkResultArtifacts(context, projectId, runId) {
|
|
187
|
+
const works = context.db.all(`SELECT w.work_id, w.result, (SELECT ws.result_path FROM work_sessions ws WHERE ws.work_id = w.work_id ORDER BY ws.created_at DESC LIMIT 1) AS result_path FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.result_schema IS NOT NULL AND w.result IS NOT NULL AND w.status = 'completed'`, [projectId, runId]);
|
|
188
|
+
for (const work of works) {
|
|
189
|
+
if (!work.result_path || !fs.existsSync(work.result_path))
|
|
190
|
+
throw new AppError("work_result_artifact_missing", "Completed structured Work has no retained result artifact", 1, { work_id: work.work_id, result_path: work.result_path });
|
|
191
|
+
const actual = fs.readFileSync(work.result_path, "utf8");
|
|
192
|
+
if (actual !== work.result)
|
|
193
|
+
throw new AppError("work_result_artifact_drift", "Completed Work result artifact no longer matches its authoritative receipt", 1, { work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: crypto.createHash("sha256").update(actual).digest("hex") });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
185
196
|
export function mutateWorkDeps(context, input) {
|
|
186
197
|
const work = requireWork(context, input.workId);
|
|
187
198
|
if (input.action === "list")
|
|
@@ -619,11 +630,11 @@ export function resultSchemaGuidance(work, runId) {
|
|
|
619
630
|
"Each evidence entry has exactly obligation_ref (one assigned ref) and refs (nonempty list of existing evidence paths). Each resolution has finding_ref, summary and nonempty evidence_refs. Do not invent file names or finding ids."];
|
|
620
631
|
}
|
|
621
632
|
if (schema === "dd-flow/code-review-result@1") {
|
|
622
|
-
return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: [] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
|
|
633
|
+
return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: ["path/to/evidence"] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
|
|
623
634
|
}
|
|
624
635
|
if (schema !== "dd-flow/plan-review-result@1")
|
|
625
636
|
return [];
|
|
626
|
-
return [refs, "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: [], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
|
|
637
|
+
return [refs, "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: ["path/to/evidence"], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
|
|
627
638
|
}
|
|
628
639
|
export function validateCodeReviewResultIdentity(work, value) {
|
|
629
640
|
const group = codeReviewGroup(work);
|
|
@@ -652,6 +663,7 @@ export function validateCodeReviewResultIdentity(work, value) {
|
|
|
652
663
|
function validateCodeReviewResult(work, value, input) {
|
|
653
664
|
validateCodeReviewResultIdentity(work, value);
|
|
654
665
|
const result = value;
|
|
666
|
+
assertReviewAspectEvidence(work, result.aspects ?? [], "CODE");
|
|
655
667
|
const findings = result.findings ?? [];
|
|
656
668
|
// A reviewer is a child Work and cannot pause its coordinator-owned Stage.
|
|
657
669
|
// Its structured blocked result is evidence for the coordinator, which then
|
|
@@ -681,6 +693,7 @@ export function validatePlanReviewResult(work, value, input) {
|
|
|
681
693
|
const unexpected = ids.filter((id) => !expected.has(id));
|
|
682
694
|
if (new Set(ids).size !== ids.length || missing.length || unexpected.length)
|
|
683
695
|
throw new AppError("review_evidence_invalid", "PLAN reviewer result must assess every assigned aspect exactly once", 2, { work_id: work.work_id, group: group.key, missing, unexpected });
|
|
696
|
+
assertReviewAspectEvidence(work, aspects, "PLAN");
|
|
684
697
|
const findingIds = aspects.flatMap((aspect) => aspect.findings ?? []).map((finding) => finding.finding_id ?? "");
|
|
685
698
|
const invalid = findingIds.filter((id) => !/^FIND-\d{3}$/.test(id));
|
|
686
699
|
if (new Set(findingIds).size !== findingIds.length || invalid.length)
|
|
@@ -695,6 +708,11 @@ export function validatePlanReviewResult(work, value, input) {
|
|
|
695
708
|
assertPortableArtifactRef(ref, input);
|
|
696
709
|
}
|
|
697
710
|
}
|
|
711
|
+
function assertReviewAspectEvidence(work, aspects, stage) {
|
|
712
|
+
const missing = aspects.filter((aspect) => !Array.isArray(aspect.evidence_refs) || aspect.evidence_refs.length === 0).map((aspect) => aspect.aspect_id ?? "");
|
|
713
|
+
if (missing.length)
|
|
714
|
+
throw new AppError("review_evidence_invalid", `${stage} reviewer result requires non-empty evidence_refs for every assigned aspect`, 2, { work_id: work.work_id, missing_aspects: missing });
|
|
715
|
+
}
|
|
698
716
|
function codeReviewGroup(work) {
|
|
699
717
|
const payload = parsePayload(work);
|
|
700
718
|
const group = payload?.group;
|