@deksden-com/dd-flow-cli 0.9.0-beta.74 → 0.9.0-beta.79
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 +8 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +3 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +7 -11
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +85 -42
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +207 -94
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
|
17
17
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
18
18
|
import { activateRecoveryWork, assertRecoveryHookCurrent, assertRunMutationAllowed, assertWorkSettlementAllowed, recoveryGuard, recoveryPromptForWork, settleRecoveryWork } from "./run-recovery.js";
|
|
19
19
|
import { runControlDirective } from "./run-control.js";
|
|
20
|
+
import { prepareError, prepareJsonFile } from "../cli/input-preparation.js";
|
|
20
21
|
const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
|
|
21
22
|
export function ensureWorkRegistry(context) { context.db.exec("SELECT 1 FROM works LIMIT 1"); context.db.exec("SELECT 1 FROM work_sessions LIMIT 1"); }
|
|
22
23
|
function ownsPhysicalWorkSession(context, projectId, owner, identity) {
|
|
@@ -105,8 +106,12 @@ export function createChildWork(context, input) {
|
|
|
105
106
|
}
|
|
106
107
|
/** Validate a proposed batch before PLAN accepts it, without registering Work. */
|
|
107
108
|
export function validateWorkBatchFile(file) {
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
validateWorkBatch(prepareJsonFile(file, "work batch").value);
|
|
110
|
+
}
|
|
111
|
+
/** Validate an already prepared batch without reopening its source file. */
|
|
112
|
+
export function validateWorkBatch(value) {
|
|
113
|
+
const parsed = value;
|
|
114
|
+
if (!parsed || !Array.isArray(parsed.works) || parsed.works.length === 0)
|
|
110
115
|
throw new AppError("validation", "work batch requires non-empty works", 2);
|
|
111
116
|
const items = parsed.works.map((item) => validateItem(item, true));
|
|
112
117
|
const keys = new Set(items.map((item) => item.key));
|
|
@@ -122,29 +127,46 @@ export function validateWorkBatchFile(file) {
|
|
|
122
127
|
assertNoCycles(items.map((item) => ({ id: item.key, dependencies: item.depends_on ?? [] })));
|
|
123
128
|
assertNoParentCycles(items.filter((item) => Boolean(item.parent)).map((item) => ({ id: item.key, parent: item.parent })));
|
|
124
129
|
}
|
|
125
|
-
export function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
130
|
+
export function prepareWorkBatch(context, input) {
|
|
131
|
+
if (input.file !== undefined && input.value !== undefined)
|
|
132
|
+
throw new AppError("usage", "Work batch accepts either file or value, not both", 2);
|
|
133
|
+
if (input.file === undefined && input.value === undefined)
|
|
134
|
+
throw new AppError("usage", "Work batch input is required", 2);
|
|
135
|
+
const parsed = (input.file !== undefined ? prepareJsonFile(input.file, "work batch").value : input.value);
|
|
136
|
+
if (!parsed || !Array.isArray(parsed.works) || parsed.works.length === 0)
|
|
129
137
|
throw new AppError("validation", "work batch requires non-empty works", 2);
|
|
130
138
|
const items = parsed.works.map((item) => validateItem(item));
|
|
131
139
|
const keys = new Set(items.map((item) => item.key));
|
|
132
140
|
if (keys.size !== items.length)
|
|
133
141
|
throw new AppError("validation", "work batch keys must be unique", 2);
|
|
142
|
+
assertNoCycles(items.map(item => ({ id: item.key, dependencies: item.depends_on ?? [] })));
|
|
143
|
+
assertNoParentCycles(items.filter((item) => Boolean(item.parent)).map(item => ({ id: item.key, parent: item.parent })));
|
|
144
|
+
prepareBatchParent(context, input.parentWorkId, items);
|
|
145
|
+
return items;
|
|
146
|
+
}
|
|
147
|
+
function prepareBatchParent(context, parentWorkId, items) {
|
|
148
|
+
const keys = new Set(items.map(item => item.key));
|
|
149
|
+
const parent = requireWork(context, parentWorkId);
|
|
150
|
+
assertRunMutationAllowed(context, parent.project_id, parent.run_id);
|
|
151
|
+
if (parent.status !== "running")
|
|
152
|
+
throw new AppError("invalid_work_state", "Batch parent must be running", 2, { work_id: parent.work_id, status: parent.status });
|
|
153
|
+
const existing = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ?`, [parent.project_id, parent.run_id]);
|
|
154
|
+
const existingIds = new Set(existing.map((row) => row.work_id));
|
|
155
|
+
for (const item of items)
|
|
156
|
+
for (const dependency of item.depends_on ?? [])
|
|
157
|
+
if (!keys.has(dependency) && !existingIds.has(dependency))
|
|
158
|
+
throw new AppError("validation", "work dependency is unknown", 2, { key: item.key, dependency });
|
|
159
|
+
for (const item of items)
|
|
160
|
+
if (item.parent && !keys.has(item.parent) && !existingIds.has(item.parent))
|
|
161
|
+
throw new AppError("validation", "work parent is unknown", 2, { key: item.key, parent: item.parent });
|
|
162
|
+
return parent;
|
|
163
|
+
}
|
|
164
|
+
export function addWorkBatch(context, input) {
|
|
165
|
+
const items = input.prepared ?? prepareWorkBatch(context, input);
|
|
134
166
|
return context.db.writeTransaction(() => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
throw new AppError("invalid_work_state", "Batch parent must be running", 2, { work_id: parent.work_id, status: parent.status });
|
|
139
|
-
const existing = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ?`, [parent.project_id, parent.run_id]);
|
|
140
|
-
const existingIds = new Set(existing.map((row) => row.work_id));
|
|
141
|
-
for (const item of items)
|
|
142
|
-
for (const dependency of item.depends_on ?? [])
|
|
143
|
-
if (!keys.has(dependency) && !existingIds.has(dependency))
|
|
144
|
-
throw new AppError("validation", "work dependency is unknown", 2, { key: item.key, dependency });
|
|
145
|
-
for (const item of items)
|
|
146
|
-
if (item.parent && !keys.has(item.parent) && !existingIds.has(item.parent))
|
|
147
|
-
throw new AppError("validation", "work parent is unknown", 2, { key: item.key, parent: item.parent });
|
|
167
|
+
// Only mutable authority/references are rechecked under the write lock.
|
|
168
|
+
// Caller bytes were read once, before admission, and are never reopened.
|
|
169
|
+
const parent = prepareBatchParent(context, input.parentWorkId, items);
|
|
148
170
|
const now = context.now();
|
|
149
171
|
const allocated = nextWorkIds(context, parent.project_id, items.map((item) => item.key));
|
|
150
172
|
const ids = new Map(items.map((item, index) => [item.key, allocated[index]]));
|
|
@@ -295,36 +317,60 @@ export function materializePendingWorkResults(context, projectId, runId) {
|
|
|
295
317
|
}
|
|
296
318
|
export function mutateWorkDeps(context, input) {
|
|
297
319
|
if (input.action === "list") {
|
|
298
|
-
const work =
|
|
299
|
-
return { work_id: work.work_id, depends_on:
|
|
320
|
+
const { work, dependencies } = prepareWorkDeps(context, input);
|
|
321
|
+
return { work_id: work.work_id, depends_on: dependencies };
|
|
300
322
|
}
|
|
323
|
+
prepareWorkDeps(context, input);
|
|
301
324
|
return context.db.writeTransaction(() => {
|
|
302
|
-
const work =
|
|
303
|
-
|
|
304
|
-
if (work.status !== "created")
|
|
305
|
-
throw new AppError("invalid_work_state", "Dependencies change only while Work is created", 2);
|
|
306
|
-
const dependencies = new Set(parseDependencies(work));
|
|
307
|
-
if (input.action === "clear")
|
|
308
|
-
dependencies.clear();
|
|
309
|
-
for (const id of input.on ?? []) {
|
|
310
|
-
const dependency = requireWork(context, id);
|
|
311
|
-
if (dependency.run_id !== work.run_id)
|
|
312
|
-
throw new AppError("validation", "Dependencies must stay in one RUN", 2);
|
|
313
|
-
if (input.action === "add")
|
|
314
|
-
dependencies.add(dependency.work_id);
|
|
315
|
-
else
|
|
316
|
-
dependencies.delete(dependency.work_id);
|
|
317
|
-
}
|
|
318
|
-
if (dependencies.has(work.work_id))
|
|
319
|
-
throw new AppError("validation", "Work cannot depend on itself", 2);
|
|
320
|
-
assertNoCycles(context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ?`, [work.project_id, work.run_id]).map((row) => ({ id: row.work_id, dependencies: row.work_id === work.work_id ? [...dependencies] : parseDependencies(row) })));
|
|
321
|
-
context.db.run("UPDATE works SET depends_on_json = ?, updated_at = ? WHERE work_id = ?", [JSON.stringify([...dependencies]), context.now(), work.work_id]);
|
|
325
|
+
const { work, dependencies } = prepareWorkDeps(context, input);
|
|
326
|
+
context.db.run("UPDATE works SET depends_on_json = ?, updated_at = ? WHERE work_id = ?", [JSON.stringify(dependencies), context.now(), work.work_id]);
|
|
322
327
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
323
|
-
return { work_id: work.work_id, depends_on:
|
|
328
|
+
return { work_id: work.work_id, depends_on: dependencies };
|
|
324
329
|
});
|
|
325
330
|
}
|
|
326
|
-
export function
|
|
327
|
-
|
|
331
|
+
export function prepareWorkDeps(context, input) {
|
|
332
|
+
if (!["list", "add", "remove", "clear"].includes(input.action) || (input.on !== undefined && (!Array.isArray(input.on) || input.on.some(id => typeof id !== "string" || !id.trim()))))
|
|
333
|
+
throw new AppError("usage", "Invalid Work dependency action or --on target", 2);
|
|
334
|
+
if (["add", "remove"].includes(input.action) && !input.on?.length)
|
|
335
|
+
throw new AppError("usage", "Dependency add/remove requires at least one --on target", 2);
|
|
336
|
+
if (["list", "clear"].includes(input.action) && input.on?.length)
|
|
337
|
+
throw new AppError("usage", "Dependency list/clear does not accept --on targets", 2);
|
|
338
|
+
if (input.action === "list") {
|
|
339
|
+
const work = requireWork(context, input.workId);
|
|
340
|
+
return { work, dependencies: parseDependencies(work) };
|
|
341
|
+
}
|
|
342
|
+
const work = requireWork(context, input.workId);
|
|
343
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
344
|
+
if (work.status !== "created")
|
|
345
|
+
throw new AppError("invalid_work_state", "Dependencies change only while Work is created", 2);
|
|
346
|
+
const dependencies = new Set(parseDependencies(work));
|
|
347
|
+
if (input.action === "clear")
|
|
348
|
+
dependencies.clear();
|
|
349
|
+
for (const id of input.on ?? []) {
|
|
350
|
+
const dependency = requireWork(context, id);
|
|
351
|
+
if (dependency.project_id !== work.project_id || dependency.run_id !== work.run_id)
|
|
352
|
+
throw new AppError("validation", "Dependencies must stay in one project and RUN", 2);
|
|
353
|
+
if (input.action === "add")
|
|
354
|
+
dependencies.add(dependency.work_id);
|
|
355
|
+
else
|
|
356
|
+
dependencies.delete(dependency.work_id);
|
|
357
|
+
}
|
|
358
|
+
if (dependencies.has(work.work_id))
|
|
359
|
+
throw new AppError("validation", "Work cannot depend on itself", 2);
|
|
360
|
+
assertNoCycles(context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ?`, [work.project_id, work.run_id]).map((row) => ({ id: row.work_id, dependencies: row.work_id === work.work_id ? [...dependencies] : parseDependencies(row) })));
|
|
361
|
+
return { work, dependencies: [...dependencies] };
|
|
362
|
+
}
|
|
363
|
+
export function prepareWorkDelete(context, id) {
|
|
364
|
+
const work = requireWork(context, id);
|
|
365
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
366
|
+
if (work.status !== "created" || work.started_at || context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? LIMIT 1", [work.work_id]) || context.db.get("SELECT 1 FROM works WHERE depends_on_json LIKE ? LIMIT 1", [`%${work.work_id}%`]))
|
|
367
|
+
throw new AppError("invalid_work_state", "Only an unstarted unreferenced Work may be deleted", 2);
|
|
368
|
+
return work;
|
|
369
|
+
}
|
|
370
|
+
export function deleteWork(context, id) {
|
|
371
|
+
prepareWorkDelete(context, id);
|
|
372
|
+
return context.db.writeTransaction(() => { const work = prepareWorkDelete(context, id); context.db.run("DELETE FROM works WHERE work_id = ?", [work.work_id]); refreshRunWorkProjection(context, work.project_id, work.run_id); return { ok: true, deleted: work.work_id }; });
|
|
373
|
+
}
|
|
328
374
|
export function shortWorkId(id) { return /^WRK-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id; }
|
|
329
375
|
/**
|
|
330
376
|
* Return the engine-bound command, never a PATH-dependent `dd-flow` token.
|
|
@@ -588,27 +634,76 @@ export function finishFanInWork(context, id, result) { const work = requireWork(
|
|
|
588
634
|
throw new AppError("active_child_work", "Fan-in Work still has active children", 2, { work_id: work.work_id }); const now = context.now(); context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [result, now, now, work.work_id]); context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = COALESCE(completed_at, ?), updated_at = ? WHERE work_id = ? AND status = 'running'", [now, now, work.work_id]); refreshRunWorkProjection(context, work.project_id, work.run_id); appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_completed", work_id: work.work_id, fan_in: true }); return { ok: true, work_id: work.work_id, status: "completed", fan_in: true }; }
|
|
589
635
|
export function failWork(context, id, reason) { return settle(context, id, "failed", reason); }
|
|
590
636
|
export function cancelWork(context, id, reason) { return settle(context, id, "cancelled", reason); }
|
|
591
|
-
export function
|
|
637
|
+
export function prepareWorkSettlement(context, id, status, result) {
|
|
638
|
+
if (status !== "completed" && (typeof result !== "string" || !result.trim()))
|
|
639
|
+
throw new AppError("usage", "Work settlement requires a nonempty --reason", 2);
|
|
592
640
|
const work = requireWork(context, id);
|
|
641
|
+
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
642
|
+
if (work.status !== "running" && !(status === "cancelled" && work.status === "created"))
|
|
643
|
+
throw new AppError("invalid_work_state", "Work is not running", 2, { status: work.status });
|
|
644
|
+
if (status === "completed" && context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created', 'running', 'paused') LIMIT 1", [work.work_id]))
|
|
645
|
+
throw new AppError("active_child_work", "Work cannot complete while a child Work is active", 2, { work_id: work.work_id });
|
|
646
|
+
const run = requireRun(context, work.project_id, work.run_id);
|
|
647
|
+
const link = context.db.get("SELECT id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, completed_at FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
648
|
+
if (work.status === "running" && !link)
|
|
649
|
+
throw new AppError("runtime_missing", "Running Work has no open Work/Session link", 1, { work_id: work.work_id });
|
|
650
|
+
return { work, run, link };
|
|
651
|
+
}
|
|
652
|
+
export function prepareWorkRetry(context, id, reason) {
|
|
653
|
+
if (typeof reason !== "string" || !reason.trim())
|
|
654
|
+
throw new AppError("usage", "Work retry requires a nonempty --reason", 2);
|
|
655
|
+
const work = requireWork(context, id);
|
|
656
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
593
657
|
const run = requireRun(context, work.project_id, work.run_id);
|
|
594
658
|
const directory = path.join(requireRunHome(run), "works", work.work_id);
|
|
595
659
|
const journal = path.join(directory, ".retry.json");
|
|
660
|
+
let pending = null;
|
|
661
|
+
if (fs.existsSync(journal)) {
|
|
662
|
+
let value;
|
|
663
|
+
try {
|
|
664
|
+
value = JSON.parse(fs.readFileSync(journal, "utf8"));
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
if (!(error instanceof SyntaxError))
|
|
668
|
+
throw error;
|
|
669
|
+
throw new AppError("work_retry_invalid", "Retry journal is not valid JSON", 1);
|
|
670
|
+
}
|
|
671
|
+
const item = value;
|
|
672
|
+
if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
|
|
673
|
+
throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
|
|
674
|
+
pending = item;
|
|
675
|
+
}
|
|
676
|
+
if (work.status !== "failed" && !(pending && work.status === "created"))
|
|
677
|
+
throw new AppError("invalid_work_state", "Only failed Work may be retried", 2);
|
|
678
|
+
const attempts = path.join(directory, "attempts");
|
|
679
|
+
let number = 1;
|
|
680
|
+
while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
|
|
681
|
+
number += 1;
|
|
682
|
+
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
|
|
683
|
+
const archive = path.join(attempts, prepared.archive);
|
|
684
|
+
if (work.status === "failed") {
|
|
685
|
+
if (prepared.result_sha256 !== (work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")))
|
|
686
|
+
throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
|
|
687
|
+
const receipt = path.join(archive, "accepted-receipt.json");
|
|
688
|
+
if (fs.existsSync(receipt) && fs.readFileSync(receipt, "utf8") !== JSON.stringify({ work_id: work.work_id, status: work.status, result: work.result, result_sha256: prepared.result_sha256 }))
|
|
689
|
+
throw new AppError("work_retry_conflict", "Archived receipt differs from failed Work", 1);
|
|
690
|
+
for (const file of prepared.entries) {
|
|
691
|
+
const source = path.join(directory, file), target = path.join(archive, file);
|
|
692
|
+
if (fs.existsSync(source) && fs.existsSync(target))
|
|
693
|
+
throw new AppError("archive_conflict", "Both live and archived Work artifact exist; preserve both", 1, { source, target });
|
|
694
|
+
if (!fs.existsSync(source) && !fs.existsSync(target))
|
|
695
|
+
throw new AppError("archive_artifact_missing", "Work artifact is absent from both retry locations", 1, { source, target });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return { work, run, directory, journal, pending, prepared, archive };
|
|
699
|
+
}
|
|
700
|
+
export function retryWork(context, id, reason) {
|
|
701
|
+
const { work, run, directory, journal } = prepareWorkRetry(context, id, reason);
|
|
596
702
|
let archive = "";
|
|
597
703
|
context.db.beginWriteTransaction();
|
|
598
704
|
try {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
602
|
-
if (pending && (pending.work_id !== work.work_id || !/^ATT-\d{3,}$/.test(pending.archive) || pending.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file))))
|
|
603
|
-
throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
|
|
604
|
-
if (current.status !== "failed" && !(pending && current.status === "created"))
|
|
605
|
-
throw new AppError("invalid_work_state", "Only failed Work may be retried", 2);
|
|
606
|
-
const attempts = path.join(directory, "attempts");
|
|
607
|
-
let number = 1;
|
|
608
|
-
while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
|
|
609
|
-
number += 1;
|
|
610
|
-
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: current.result === null ? null : crypto.createHash("sha256").update(current.result).digest("hex") };
|
|
611
|
-
archive = path.join(attempts, prepared.archive);
|
|
705
|
+
const { work: current, pending, prepared, archive: targetArchive } = prepareWorkRetry(context, work.work_id, reason);
|
|
706
|
+
archive = targetArchive;
|
|
612
707
|
if (current.status === "failed") {
|
|
613
708
|
if (prepared.result_sha256 !== (current.result === null ? null : crypto.createHash("sha256").update(current.result).digest("hex")))
|
|
614
709
|
throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
|
|
@@ -678,17 +773,8 @@ function sharedReviewInput(work, run, initialize = false) {
|
|
|
678
773
|
return typeof value.read_only_input.fingerprint === "string" ? value.read_only_input.fingerprint : null;
|
|
679
774
|
}
|
|
680
775
|
async function settle(context, id, status, result, progress, retry) {
|
|
681
|
-
const work =
|
|
682
|
-
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
776
|
+
const { work, run, link } = prepareWorkSettlement(context, id, status, result);
|
|
683
777
|
id = work.work_id;
|
|
684
|
-
if (work.status !== "running" && !(status === "cancelled" && work.status === "created"))
|
|
685
|
-
throw new AppError("invalid_work_state", "Work is not running", 2, { status: work.status });
|
|
686
|
-
if (status === "completed" && context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created', 'running', 'paused') LIMIT 1", [id]))
|
|
687
|
-
throw new AppError("active_child_work", "Work cannot complete while a child Work is active", 2, { work_id: id });
|
|
688
|
-
const run = requireRun(context, work.project_id, work.run_id);
|
|
689
|
-
const link = context.db.get("SELECT id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, completed_at FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [id]);
|
|
690
|
-
if (work.status === "running" && !link)
|
|
691
|
-
throw new AppError("runtime_missing", "Running Work has no open Work/Session link", 1, { work_id: id });
|
|
692
778
|
let receipts = [];
|
|
693
779
|
let coordinationDrift = [];
|
|
694
780
|
const assertReadOnlyInput = () => {
|
|
@@ -776,16 +862,28 @@ function bindSession(context, work, run, identity, now) {
|
|
|
776
862
|
// Work ancestry and provider-tree containment are different relations. An
|
|
777
863
|
// isolated worker is logically a child Work but physically a provider root.
|
|
778
864
|
const parentSession = inferredParentSession ?? identity.parentSessionId;
|
|
865
|
+
// Stored provider ancestry uses internal keys, just like hook_events.
|
|
866
|
+
// Resolve native IDs only when rendering diagnostics; changing storage here
|
|
867
|
+
// breaks existing bindings and usage ancestry joins.
|
|
779
868
|
const providerParentSession = identity.parentSessionId;
|
|
869
|
+
const identityDetails = (sessionKey = identity.sessionId, parentKey = parentSession) => ({
|
|
870
|
+
internal_session_key: sessionKey,
|
|
871
|
+
native_session_id: sessionKey === identity.sessionId ? identity.nativeSessionId : context.db.get("SELECT provider_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, sessionKey])?.provider_session_id ?? null,
|
|
872
|
+
internal_parent_session_key: parentKey,
|
|
873
|
+
native_parent_session_id: parentKey ? context.db.get("SELECT provider_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, parentKey])?.provider_session_id ?? null : null,
|
|
874
|
+
harness: identity.harness
|
|
875
|
+
});
|
|
780
876
|
if (work.parent_work_id && !parentSession)
|
|
781
877
|
throw new AppError("parent_session_required", "Child Work requires a confirmed parent Work/Session link", 1, { work_id: work.work_id, parent_work_id: work.parent_work_id });
|
|
782
878
|
if (work.launch_policy === "fresh_agent_required" && (identity.sessionId === parentSession || context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? LIMIT 1", [work.project_id, work.run_id, identity.sessionId])))
|
|
783
|
-
throw new AppError("fresh_session_required", "This Work requires a fresh Session in this RUN", 1, { work_id: work.work_id,
|
|
879
|
+
throw new AppError("fresh_session_required", "This Work requires a fresh Session in this RUN", 1, { work_id: work.work_id, effect: "no_effect", recoverable: true, handoff_required: true, ...identityDetails() });
|
|
784
880
|
const existing = context.db.get("SELECT session_id, parent_session_id, provider_parent_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, identity.sessionId]);
|
|
785
|
-
|
|
786
|
-
|
|
881
|
+
// A reuse-allowed Work can follow another Work in this same Session. Its
|
|
882
|
+
// Work predecessor is not a new physical/logical parent of the Session.
|
|
883
|
+
if (existing && existing.parent_session_id && parentSession && parentSession !== identity.sessionId && existing.parent_session_id !== parentSession)
|
|
884
|
+
throw new AppError("session_parent_conflict", "Observed Session already has a different immutable parent", 1, { work_id: work.work_id, ...identityDetails(existing.session_id, existing.parent_session_id), conflicting_internal_parent_session_key: parentSession });
|
|
787
885
|
if (existing?.provider_parent_session_id && providerParentSession && existing.provider_parent_session_id !== providerParentSession)
|
|
788
|
-
throw new AppError("provider_session_parent_conflict", "Observed provider Session already has a different immutable provider parent", 1, { session_id:
|
|
886
|
+
throw new AppError("provider_session_parent_conflict", "Observed provider Session already has a different immutable provider parent", 1, { work_id: work.work_id, ...identityDetails(existing.session_id, existing.parent_session_id), conflicting_internal_provider_parent_session_key: providerParentSession, conflicting_native_parent_session_id: context.db.get("SELECT provider_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, providerParentSession])?.provider_session_id ?? null });
|
|
789
887
|
context.db.run(`INSERT INTO sessions (session_id, project_id, harness, provider_session_id, provider_parent_session_id, agent_id, parent_session_id, provider, model, reasoning, mode, agent_type, project_root, flow_kind, status, run_id, protocol_id, worker_id, workspace_path, continuation_policy, current_stage, next_action, last_action_hash, continuation_count, stop_reason, transcript_path, cwd, metadata_json, coverage_units_json, created_at, updated_at, stopped_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vnext', 'active', ?, NULL, ?, ?, 'go_router', 'work', NULL, NULL, 0, NULL, ?, ?, '{}', '[]', ?, ?, NULL) ON CONFLICT(session_id, project_id) DO UPDATE SET harness = excluded.harness, provider_session_id = COALESCE(excluded.provider_session_id, provider_session_id), provider_parent_session_id = COALESCE(excluded.provider_parent_session_id, provider_parent_session_id), agent_id = COALESCE(excluded.agent_id, agent_id), parent_session_id = COALESCE(excluded.parent_session_id, parent_session_id), provider = COALESCE(excluded.provider, provider), model = COALESCE(excluded.model, model), reasoning = COALESCE(excluded.reasoning, reasoning), mode = COALESCE(excluded.mode, mode), agent_type = COALESCE(excluded.agent_type, agent_type), flow_kind = excluded.flow_kind, run_id = excluded.run_id, worker_id = excluded.worker_id, workspace_path = excluded.workspace_path, transcript_path = COALESCE(excluded.transcript_path, transcript_path), cwd = excluded.cwd, updated_at = excluded.updated_at`, [identity.sessionId, work.project_id, identity.harness, identity.providerSessionId, providerParentSession, identity.agentId, existing?.parent_session_id ?? (parentSession === identity.sessionId ? null : parentSession), identity.provider, identity.model, identity.reasoning, identity.mode, identity.agentType, run.project_root, work.run_id, work.work_id, run.workspace_root, identity.transcriptPath, run.workspace_root, now, now]);
|
|
790
888
|
reactivateBoundSession(context, work.project_id, identity.sessionId, now);
|
|
791
889
|
}
|
|
@@ -799,6 +897,15 @@ export function bindSessionForResume(context, workId, identity, now) {
|
|
|
799
897
|
function reactivateBoundSession(context, projectId, sessionId, now) {
|
|
800
898
|
context.db.run("UPDATE sessions SET status = 'active', stop_reason = NULL, stopped_at = NULL, updated_at = ? WHERE project_id = ? AND session_id = ?", [now, projectId, sessionId]);
|
|
801
899
|
}
|
|
900
|
+
export function prepareWorkResult(context, id, result) {
|
|
901
|
+
try {
|
|
902
|
+
const { work, run, link } = prepareWorkSettlement(context, id, "completed", result);
|
|
903
|
+
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
prepareError(error, { work_id: id });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
802
909
|
function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, runId, resultPath) {
|
|
803
910
|
if (!work.result_schema)
|
|
804
911
|
return;
|
|
@@ -811,14 +918,7 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
|
|
|
811
918
|
catch {
|
|
812
919
|
throw new AppError("validation", "Work result must be JSON for its declared result schema", 2, { work_id: work.work_id, result_schema: work.result_schema });
|
|
813
920
|
}
|
|
814
|
-
|
|
815
|
-
fs.writeFileSync(candidate, result);
|
|
816
|
-
try {
|
|
817
|
-
validateSchema({ schemaName: work.result_schema.replace(/^dd-flow\//, "").replace(/@\d+$/, ""), file: candidate, projectRoot, runId, runRoot: runHome });
|
|
818
|
-
}
|
|
819
|
-
finally {
|
|
820
|
-
fs.rmSync(candidate, { force: true });
|
|
821
|
-
}
|
|
921
|
+
validateSchema({ schemaName: work.result_schema.replace(/^dd-flow\//, "").replace(/@\d+$/, ""), file: resultPath, data: parsed, projectRoot, runId, runRoot: runHome });
|
|
822
922
|
if (work.result_schema === "dd-flow/code-work-result@3")
|
|
823
923
|
validateCodeWorkResult(work, parsed, workspaceRoot, runHome, runId);
|
|
824
924
|
if (work.result_schema === "dd-flow/code-review-result@1")
|
|
@@ -887,13 +987,13 @@ function renderWorkerPrompt(context, work, run, dependencies) {
|
|
|
887
987
|
: [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not."];
|
|
888
988
|
const completionRepair = mergeWork
|
|
889
989
|
? ["A failed integration check is evidence for a source-repair cycle, not permission to repair the integration workspace. Follow the exact merge repair command returned by dd-flow."]
|
|
890
|
-
: ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then use the returned retry_command with your result.
|
|
990
|
+
: ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then use the returned retry_command with your result. Internal invocation IDs are runtime-owned; do not copy or invent them. Do not invent a cause that does not appear in the retained output."];
|
|
891
991
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies materialization at Work finish. Execution occurs at its declared run_at gate, not necessarily in this Work.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
892
992
|
if (packet)
|
|
893
993
|
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
894
994
|
if (packet)
|
|
895
995
|
codeContext.push("<temporary_services>", "Prefer the declared check launcher: it already owns check resources. If the planned scenario genuinely requires an interactive HTTP service, use the managed supervisor below. This is a template: replace the project service command, port names and readiness path from the plan/project instructions; do not invent a fixed port.", `${command} runtime process start --run ${run.id} --project-root ${JSON.stringify(run.project_root)} --command '<project-service-command>' --ports api --ready-port api --ready-path /health --json --progress-jsonl`, "The service receives DD_FLOW_PORT_API (and equivalent variables for all declared names). The command stays running as its supervisor. Retain its tool handle; wait for the service ready event and read its service.json receipt. Pass those exact ports and the same project environment to reset/seed, API and browser operations.", "A ready receipt proves service readiness only. Record the scenario outcome and real evidence separately. After the scenario, execute the exact stop_command from that receipt, then wait for the supervisor to exit. Never use pkill/killall or stop a sibling's process. If cleanup fails, retain the process id and report the failure; do not claim the resource is free.", "</temporary_services>", "");
|
|
896
|
-
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command with a quoted heredoc (replace the example JSON with your result):\n${finishWorkCommand} <<'DD_FLOW_RESULT'\n{}\nDD_FLOW_RESULT`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
996
|
+
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command with a quoted heredoc (replace the example JSON with your result):\n${finishWorkCommand} <<'DD_FLOW_RESULT'\n{}\nDD_FLOW_RESULT`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
897
997
|
}
|
|
898
998
|
export function resultSchemaGuidance(work, runId) {
|
|
899
999
|
const schema = work.result_schema;
|
|
@@ -913,11 +1013,17 @@ export function resultSchemaGuidance(work, runId) {
|
|
|
913
1013
|
"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."];
|
|
914
1014
|
}
|
|
915
1015
|
if (schema === "dd-flow/code-review-result@1") {
|
|
916
|
-
|
|
1016
|
+
const aspect = { aspect_id: "assigned_aspect_id", verdict: "pass", summary: "Conclusion.", evidence_refs: ["path/to/evidence"] };
|
|
1017
|
+
const clean = { schema_id: schema, verdict: "pass", summary: "Concise evidence-backed conclusion.", aspects: [aspect], findings: [] };
|
|
1018
|
+
const mixed = { ...clean, verdict: "findings", aspects: [aspect, { ...aspect, aspect_id: "another_assigned_aspect_id", verdict: "findings" }], findings: [{ finding_id: "FIND-001", aspect_id: "another_assigned_aspect_id", priority: "p1", problem: "Describe the evidenced defect.", impact: "Describe its material impact.", required_outcome: "Describe the required correction.", evidence_refs: ["path/to/evidence"], obligation_refs: ["assigned_obligation_ref"] }] };
|
|
1019
|
+
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.", "Examples below show no findings and mixed aspect outcomes, not completed results. Replace placeholder ids and paths with assigned aspects, obligations and existing evidence. Choose verdict pass, findings or blocked. Keep findings: [] when none exist. Report only material, direct-evidence defects, not taste or cosmetics.", "```json", JSON.stringify(clean, null, 2), "```", "```json", JSON.stringify(mixed, null, 2), "```"];
|
|
917
1020
|
}
|
|
918
1021
|
if (schema !== "dd-flow/plan-review-result@1")
|
|
919
1022
|
return [];
|
|
920
|
-
|
|
1023
|
+
const aspect = { aspect_id: "assigned_aspect_id", verdict: "pass", summary: "Conclusion for this aspect.", evidence_refs: ["path/to/evidence"], findings: [] };
|
|
1024
|
+
const clean = { schema_id: schema, plan_revision: 1, overall_verdict: "pass", summary: "Concise evidence-backed conclusion.", aspects: [aspect] };
|
|
1025
|
+
const mixed = { ...clean, overall_verdict: "needs_changes", aspects: [aspect, { ...aspect, aspect_id: "another_assigned_aspect_id", verdict: "needs_changes", findings: [{ finding_id: "FIND-001", severity: "high", summary: "Describe the evidenced defect.", evidence_refs: ["path/to/evidence"] }] }] };
|
|
1026
|
+
return [refs, "Examples below show no findings and mixed aspect outcomes, not completed results. Assess each assigned aspect once, replace placeholder ids, plan_revision and paths with packet values and existing evidence. Choose verdict pass, watch, needs_changes or blocked. Keep findings: [] when an aspect has no findings. Do not add fields. Number findings locally as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them.", "```json", JSON.stringify(clean, null, 2), "```", "```json", JSON.stringify(mixed, null, 2), "```"];
|
|
921
1027
|
}
|
|
922
1028
|
export function validateCodeReviewResultIdentity(work, value) {
|
|
923
1029
|
const group = codeReviewGroup(work);
|
|
@@ -1006,10 +1112,23 @@ function codeReviewGroup(work) {
|
|
|
1006
1112
|
? { key: value.key, aspect_ids: value.aspect_ids }
|
|
1007
1113
|
: null;
|
|
1008
1114
|
}
|
|
1009
|
-
function requireWork(context, id) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1115
|
+
function requireWork(context, id) {
|
|
1116
|
+
ensureWorkRegistry(context);
|
|
1117
|
+
const configured = context.env.DD_FLOW_INVOCATION_SCOPE;
|
|
1118
|
+
const scope = configured ? JSON.parse(configured) : undefined;
|
|
1119
|
+
const projectId = scope?.projectRoot ? context.db.get("SELECT id FROM projects WHERE root = ?", [scope.projectRoot])?.id : undefined;
|
|
1120
|
+
const scoped = projectId && scope?.runId ? " AND project_id = ? AND run_id = ?" : "";
|
|
1121
|
+
const params = projectId && scope?.runId ? [projectId, scope.runId] : [];
|
|
1122
|
+
const exact = context.db.get(`SELECT ${workColumns} FROM works WHERE work_id = ?${scoped}`, [id, ...params]);
|
|
1123
|
+
if (exact)
|
|
1124
|
+
return exact;
|
|
1125
|
+
if (!/^WRK-\d{3,}$/.test(id))
|
|
1126
|
+
throw new AppError("not_found", "Work is not registered in the current RUN", 1, { work_id: id });
|
|
1127
|
+
const matches = context.db.all(`SELECT ${workColumns} FROM works WHERE work_id LIKE ?${scoped} ORDER BY work_id`, [`${id}-%`, ...params]);
|
|
1128
|
+
if (matches.length !== 1)
|
|
1129
|
+
throw new AppError(matches.length ? "ambiguous_work_alias" : "not_found", matches.length ? "Short Work alias is ambiguous in the current RUN" : "Work is not registered in the current RUN", 1, { work_id: id, matches: matches.map((work) => work.work_id) });
|
|
1130
|
+
return matches[0];
|
|
1131
|
+
}
|
|
1013
1132
|
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]); if (!run?.run_root)
|
|
1014
1133
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId }); return run; }
|
|
1015
1134
|
function requireRunHome(run) { if (!run.run_root)
|
|
@@ -1051,12 +1170,6 @@ function validateItem(value, requireExecutionContext = false) { if (!value || ty
|
|
|
1051
1170
|
throw new AppError("validation", "result_schema must be a non-empty schema id", 2); const payload = code ? item : (item.payload && typeof item.payload === "object" && !Array.isArray(item.payload) ? item.payload : undefined); return { key: item.key, task: item.task, ...(Array.isArray(item.depends_on) ? { depends_on: item.depends_on } : {}), ...(typeof item.parent === "string" ? { parent: item.parent } : {}), ...(typeof item.launch_policy === "string" ? { launch_policy: item.launch_policy } : {}), ...(typeof item.result_schema === "string" ? { result_schema: item.result_schema } : {}), ...(payload ? { payload } : {}) }; }
|
|
1052
1171
|
function allowsEmptyRepairChecks(item) { const repair = item.repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
1053
1172
|
return false; const value = repair; return typeof value.check_receipt_id === "string" || (Array.isArray(value.review_findings) && value.review_findings.some((finding) => typeof finding === "object" && finding !== null && Array.isArray(finding.check_refs) && finding.check_refs.some((ref) => typeof ref === "string"))) || (Array.isArray(value.semantic_unresolved) && value.semantic_unresolved.some((item) => typeof item === "string")); }
|
|
1054
|
-
function readJson(file) { try {
|
|
1055
|
-
return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
|
1056
|
-
}
|
|
1057
|
-
catch (error) {
|
|
1058
|
-
throw new AppError("validation", `Invalid JSON file: ${String(error)}`, 2, { file });
|
|
1059
|
-
} }
|
|
1060
1173
|
export function refreshRunWorkProjection(context, projectId, runId) { refreshRunSessionProjection(context, projectId, runId); const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (run?.run_root) {
|
|
1061
1174
|
const obsolete = path.join(run.run_root, "work.json");
|
|
1062
1175
|
if (fs.existsSync(obsolete))
|