@deksden-com/dd-flow-cli 0.9.0-beta.88 → 0.9.0-beta.89
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 +6 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/command-inputs.js +13 -10
- package/dist/cli/help.js +2 -0
- package/dist/cli/hook-ingress.js +2 -2
- package/dist/cli/run-cli.js +27 -12
- package/dist/services/codex-hook-delivery.js +3 -3
- package/dist/services/command-context.js +80 -0
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +4 -2
- package/dist/services/hooks.js +21 -15
- package/dist/services/lifecycle-command.js +27 -0
- package/dist/services/lifecycle-invocations.js +65 -31
- package/dist/services/recovery-snapshot-database.js +1 -1
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-controller.js +4 -1
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/stage-pause.js +7 -21
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/vnext-code-review.js +13 -11
- package/dist/services/vnext-code.js +39 -21
- package/dist/services/vnext-fanout.js +27 -6
- package/dist/services/vnext-merge.js +13 -10
- package/dist/services/vnext-plan-review.js +6 -5
- package/dist/services/work-registry.js +18 -9
- package/dist/storage/database.js +15 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
6
6
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
7
7
|
import { shortEntityReference } from "../shared/entity-references.js";
|
|
8
8
|
import { resolveWorkReference } from "../storage/work-references.js";
|
|
9
|
-
import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand, shellSuffixOffset } from "./lifecycle-command.js";
|
|
9
|
+
import { commandOption, continuationCommand, lifecycleCommandFromPayload, parseLifecycleCommand, shellSuffixOffset } from "./lifecycle-command.js";
|
|
10
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
11
|
import { assertRunMutationAllowed, assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
12
12
|
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
@@ -15,8 +15,49 @@ import { appendEvent } from "./run-controller-state.js";
|
|
|
15
15
|
import { recoverCommittedWorkStart } from "./work-registry.js";
|
|
16
16
|
import { observeCommandHook } from "./hooks.js";
|
|
17
17
|
import { runtimeHarness } from "./execution-policy.js";
|
|
18
|
-
import {
|
|
18
|
+
import { normalizedPathAlias, resolveContextualPaths } from "./command-context.js";
|
|
19
|
+
import { isRegisteredRepair } from "./repair-intents.js";
|
|
20
|
+
import { stageAttempt } from "./stage-work-graph.js";
|
|
19
21
|
import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
|
|
22
|
+
import { canonicalPath } from "../storage/paths.js";
|
|
23
|
+
/** Auxiliary commands have no invocation capability, but may not escape their
|
|
24
|
+
* native daemon's project/RUN. Context aliases never grant mutation authority. */
|
|
25
|
+
export function assertAuxiliaryCommandScope(context, command, projectRoot) {
|
|
26
|
+
const auxiliary = continuationCommand(command);
|
|
27
|
+
const daemonId = context.env.DD_FLOW_DAEMON_ID;
|
|
28
|
+
if (!auxiliary || !daemonId)
|
|
29
|
+
return;
|
|
30
|
+
const event = observedLifecycleNativeEvent(context, command, daemonId);
|
|
31
|
+
if (!event)
|
|
32
|
+
throw new AppError("invocation_receipt_missing", "Auxiliary command has no exact native receipt", 1, { phase: "prepare", effect: "no_effect" });
|
|
33
|
+
const project = context.db.get("SELECT root FROM projects WHERE id = ?", [event.project_id]);
|
|
34
|
+
if (!project || !projectRoot || canonicalPath(projectRoot, false) !== project.root)
|
|
35
|
+
throw new AppError("invocation_scope_mismatch", "Auxiliary command differs from its native project scope", 1, { phase: "prepare", effect: "no_effect" });
|
|
36
|
+
for (const record of managedProcessStatus(context)) {
|
|
37
|
+
if (record.project_id !== event.project_id || !record.run_id || !record.kind.endsWith("-daemon"))
|
|
38
|
+
continue;
|
|
39
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
40
|
+
if (typeof metadata.state_dir !== "string")
|
|
41
|
+
continue;
|
|
42
|
+
let state;
|
|
43
|
+
try {
|
|
44
|
+
state = readNativeDaemonState(metadata.state_dir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (state.daemon_id !== daemonId)
|
|
50
|
+
continue;
|
|
51
|
+
requireManagedDaemonBinding(context, { stateDir: metadata.state_dir, projectId: event.project_id, runId: record.run_id });
|
|
52
|
+
const reference = auxiliary.args.options.get("run")?.[0] ?? auxiliary.args.positional[2];
|
|
53
|
+
const run = context.db.get("SELECT id FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [event.project_id, reference, reference]);
|
|
54
|
+
if (run?.id !== record.run_id)
|
|
55
|
+
throw new AppError("invocation_scope_mismatch", "Auxiliary command differs from its native RUN scope", 1, { phase: "prepare", effect: "no_effect" });
|
|
56
|
+
assertRunMutationAllowed(context, event.project_id, record.run_id, metadata.run_generation);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new AppError("invocation_scope_unavailable", "Auxiliary command has no live managed daemon binding", 1, { phase: "prepare", effect: "no_effect" });
|
|
60
|
+
}
|
|
20
61
|
// Conclusive result/check rejections, never transport, ownership, registry or
|
|
21
62
|
// unknown-effect failures. These validators reject before accepting the result.
|
|
22
63
|
export function isRetryableLifecycleRejection(code, details = {}) {
|
|
@@ -122,9 +163,6 @@ function normalizedInvocationValue(key, value) {
|
|
|
122
163
|
}
|
|
123
164
|
/** Shell/JSON builders sometimes preserve the shell escape before an alias.
|
|
124
165
|
* Accept it only for the small declared alias vocabulary. */
|
|
125
|
-
function normalizedPathAlias(value) {
|
|
126
|
-
return value.replace(/^\\+(?=@(?:project|workspace|run)(?:\/|$))/, "");
|
|
127
|
-
}
|
|
128
166
|
function quoteModelFacingArgs(argv) {
|
|
129
167
|
return argv.map(value => /^@(project|workspace|run)(?:\/.*)?$/.test(value)
|
|
130
168
|
? `'${value.replaceAll("'", `'\\''`)}'`
|
|
@@ -181,23 +219,11 @@ export function expandLifecycleInvocationArgs(context, args, scope, operation) {
|
|
|
181
219
|
const pathOptions = contextualPathOptionsForLifecycle(operation);
|
|
182
220
|
const run = scope.runId ? context.db.get("SELECT id, short_id, workspace_root, run_root FROM runs WHERE id = ?", [scope.runId]) : undefined;
|
|
183
221
|
const roots = { project: scope.projectRoot, workspace: run?.workspace_root, run: run?.run_root };
|
|
184
|
-
return args.map((value, index) => {
|
|
222
|
+
return resolveContextualPaths(args.map((value, index) => {
|
|
185
223
|
if (run && !args[index - 1]?.startsWith("--") && [run.short_id, normalizedInvocationValue("positional", run.id)].includes(value) && !operation.startsWith("work_"))
|
|
186
224
|
return run.id;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return value;
|
|
190
|
-
value = normalizedPathAlias(value);
|
|
191
|
-
if (!value.startsWith("@"))
|
|
192
|
-
return value;
|
|
193
|
-
const match = /^@(project|workspace|run)(?:\/(.*))?$/.exec(value);
|
|
194
|
-
if (!match)
|
|
195
|
-
throw new AppError("usage", `Unknown path alias: ${value}`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: option });
|
|
196
|
-
const root = roots[match[1]];
|
|
197
|
-
if (!root)
|
|
198
|
-
throw new AppError("usage", `Path alias @${match[1]} is unavailable without a bound RUN`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: option });
|
|
199
|
-
return assertPathWithin(root, path.join(root, match[2] ?? ""), `${match[1]}_alias`);
|
|
200
|
-
});
|
|
225
|
+
return value;
|
|
226
|
+
}), pathOptions, roots);
|
|
201
227
|
}
|
|
202
228
|
/** Validate alias spelling before native-receipt lookup, so a correctable
|
|
203
229
|
* command typo is never reported as missing infrastructure evidence. */
|
|
@@ -259,7 +285,7 @@ export function assertLifecycleOutcomes(context, scope) {
|
|
|
259
285
|
const outcome = JSON.parse(row.outcome_json);
|
|
260
286
|
if (outcome.disposition === "retrying" && outcome.deadline <= Date.now())
|
|
261
287
|
throw new AppError("lifecycle_outcome_unknown", "Storage retry has no terminal receipt; reconcile its committed evidence", 1, { lifecycle_outcome: outcome, effect: "unknown" });
|
|
262
|
-
if (outcome.disposition === "fatal")
|
|
288
|
+
if (outcome.disposition === "fatal" && !isRegisteredRepair(context, outcome.error?.details?.continuation, { projectId: project.id, runId: scope.runId }))
|
|
263
289
|
throw new AppError(outcome.error?.code ?? "lifecycle_outcome_unknown", outcome.error?.message || "Mandatory lifecycle failed (historical receipt has no message)", 1, { ...outcome.error?.details, lifecycle_outcome: outcome });
|
|
264
290
|
}
|
|
265
291
|
}
|
|
@@ -272,7 +298,7 @@ export function assertLifecycleOutcomes(context, scope) {
|
|
|
272
298
|
ORDER BY a.rowid`, [scope.projectRoot, scope.runId, scope.generation]);
|
|
273
299
|
for (const row of rows) {
|
|
274
300
|
const error = (row.outcome_json ? JSON.parse(row.outcome_json) : null)?.error;
|
|
275
|
-
if (typeof error?.code === "string" && !isCorrectableLifecycleRejection(error.code, error.details))
|
|
301
|
+
if (typeof error?.code === "string" && !isCorrectableLifecycleRejection(error.code, error.details) && !isRegisteredRepair(context, error.details?.continuation, { projectId: project.id, runId: scope.runId }))
|
|
276
302
|
throw new AppError(error.code, typeof error.message === "string" ? error.message : "Mandatory lifecycle failed", 1, { ...(error.details ?? {}), invocation_id: row.id });
|
|
277
303
|
}
|
|
278
304
|
}
|
|
@@ -280,7 +306,8 @@ export function assertLifecycleOutcomes(context, scope) {
|
|
|
280
306
|
* atomic controller notification; missing notifications reconcile from the row. */
|
|
281
307
|
export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
282
308
|
const parsed = parseLifecycleCommand(command);
|
|
283
|
-
|
|
309
|
+
const auxiliary = continuationCommand(command);
|
|
310
|
+
if (parsed.kind !== "standalone" && !auxiliary)
|
|
284
311
|
return () => { };
|
|
285
312
|
let anchor;
|
|
286
313
|
context.observeLifecycleHook = (projectId, eventId) => {
|
|
@@ -291,6 +318,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
291
318
|
return;
|
|
292
319
|
let runId = context.db.get("SELECT run_id FROM sessions WHERE project_id = ? AND harness = ? AND (session_id = ? OR session_id = ? OR provider_session_id = ?) AND run_id IS NOT NULL ORDER BY session_id = ? DESC LIMIT 1", [projectId, event.harness, event.session_id, event.parent_session_id, event.parent_session_id, event.session_id])?.run_id;
|
|
293
320
|
let generation;
|
|
321
|
+
let correlation = {};
|
|
294
322
|
// A first Stage command precedes logical Session binding. Its native root
|
|
295
323
|
// is already owned by the controller's retained create receipt.
|
|
296
324
|
if (context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'run_controllers'")) {
|
|
@@ -305,6 +333,8 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
305
333
|
return;
|
|
306
334
|
runId = owners[0].run_id;
|
|
307
335
|
generation = owners[0].generation;
|
|
336
|
+
const current = JSON.parse(owners[0].state_json);
|
|
337
|
+
correlation = { stage: current.stage, ...(current.stage ? { attempt: stageAttempt(context, projectId, runId, current.stage) } : {}), controller_turn: current.turns };
|
|
308
338
|
}
|
|
309
339
|
}
|
|
310
340
|
if (event.daemon_id) {
|
|
@@ -333,13 +363,13 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
333
363
|
}
|
|
334
364
|
if (!runId)
|
|
335
365
|
return;
|
|
336
|
-
anchor = { project_id: projectId, run_id: runId, generation: generation ?? recoveryGuard(context, projectId, runId)?.generation ?? 0, hook_event_id: eventId, session_id: event.session_id, harness: event.harness, operation: parsed.invocation.operation };
|
|
366
|
+
anchor = { project_id: projectId, run_id: runId, generation: generation ?? recoveryGuard(context, projectId, runId)?.generation ?? 0, hook_event_id: eventId, session_id: event.session_id, harness: event.harness, operation: parsed.kind === "standalone" ? parsed.invocation.operation : auxiliary.operation, ...correlation, ...(auxiliary ? { auxiliary: true } : {}) };
|
|
337
367
|
};
|
|
338
368
|
if (nativeEvent)
|
|
339
369
|
context.observeLifecycleHook(nativeEvent.project_id, nativeEvent.id);
|
|
340
370
|
else
|
|
341
371
|
observeCommandHook(context, command);
|
|
342
|
-
const invocationId = commandOption(parsed.invocation, "invocation-id");
|
|
372
|
+
const invocationId = parsed.kind === "standalone" ? commandOption(parsed.invocation, "invocation-id") : undefined;
|
|
343
373
|
if (!anchor && invocationId && !nativeEvent) {
|
|
344
374
|
const attempt = load(context, invocationId);
|
|
345
375
|
checkCommand(attempt, command);
|
|
@@ -359,7 +389,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
359
389
|
return;
|
|
360
390
|
const error = outcome.error ? errorRecord(outcome.error) : undefined;
|
|
361
391
|
const correctable = error && isCorrectableLifecycleRejection(error.code, error.details);
|
|
362
|
-
const record = { ...anchor, ...(error ? { error } : outcome), effect: error ? error.details.effect ?? (correctable ? "no_effect" : "unknown") : "committed", disposition: error ? correctable ? "correctable" : "fatal" : "completed" };
|
|
392
|
+
const record = { ...anchor, ...(error ? { error } : outcome), effect: error ? error.details.effect ?? (correctable ? "no_effect" : "unknown") : "committed", disposition: error ? auxiliary ? "pending_continuation" : correctable ? "correctable" : "fatal" : "completed" };
|
|
363
393
|
try {
|
|
364
394
|
context.db.writeTransaction(() => {
|
|
365
395
|
const retained = context.db.get("SELECT outcome_json FROM hook_events WHERE id = ?", [anchor.hook_event_id]);
|
|
@@ -550,7 +580,9 @@ export function settleLifecycleRejection(context, id, error) {
|
|
|
550
580
|
details.recoverable = true;
|
|
551
581
|
if (storageRetry)
|
|
552
582
|
details.storage_retry = 1;
|
|
553
|
-
details.retry_instruction =
|
|
583
|
+
details.retry_instruction = isRegisteredRepair(context, details.continuation)
|
|
584
|
+
? "The repair is already registered. End this coordinator turn so the controller can launch its fresh child. Use retry_command only after that repair completes, with fresh semantic verification."
|
|
585
|
+
: "Correct only the evidenced defect, then use retry_command with the required result input. Reissuing the old command only reads its retained outcome.";
|
|
554
586
|
}
|
|
555
587
|
else if (isCorrectableLifecycleRejection(error.code, error.details)) {
|
|
556
588
|
details.effect = "no_effect";
|
|
@@ -829,15 +861,17 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
|
|
|
829
861
|
return null;
|
|
830
862
|
const signature = (value) => {
|
|
831
863
|
const parsed = parseLifecycleCommand(value);
|
|
832
|
-
|
|
864
|
+
const candidate = parsed.kind === "standalone" && !parsed.invocation.wrapped ? parsed.invocation : continuationCommand(value);
|
|
865
|
+
if (!candidate)
|
|
833
866
|
return null;
|
|
834
|
-
return JSON.stringify([
|
|
835
|
-
[...
|
|
867
|
+
return JSON.stringify([candidate.operation, candidate.args.positional,
|
|
868
|
+
[...candidate.args.options].filter(([key]) => !["json", "progress-jsonl", "hook-event-id"].includes(key)).sort(([a], [b]) => a.localeCompare(b))]);
|
|
836
869
|
};
|
|
837
870
|
const expected = signature(command);
|
|
838
871
|
if (!expected)
|
|
839
872
|
return null;
|
|
840
|
-
const
|
|
873
|
+
const parsed = parseLifecycleCommand(command);
|
|
874
|
+
const eventKey = parsed.kind === "standalone" ? commandOption(parsed.invocation, "hook-event-id") : undefined;
|
|
841
875
|
const matches = context.db.all("SELECT project_id, id, event_key, sanitized_summary FROM hook_events WHERE daemon_id = ? AND status = 'observed' AND outcome_json IS NULL", [daemonId]).filter(row => {
|
|
842
876
|
if (eventKey && row.event_key !== eventKey)
|
|
843
877
|
return false;
|
|
@@ -49,7 +49,7 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
|
|
|
49
49
|
merge_servers: "0", merge_sessions: "0", merge_queue: "0", lanes: "0", lane_locks: "0", lane_waiters: "0",
|
|
50
50
|
sqlite_sequence: "0"
|
|
51
51
|
};
|
|
52
|
-
for (const table of ["works", "check_receipts", "merge_requests", "flow_run_flag_mutations", "usage", "flow_session_segments", "flow_jobs", "run_recovery_guards", "run_controllers", "run_controls"])
|
|
52
|
+
for (const table of ["works", "stage_work_memberships", "repair_intents", "check_receipts", "merge_requests", "flow_run_flag_mutations", "usage", "flow_session_segments", "flow_jobs", "run_recovery_guards", "run_controllers", "run_controls"])
|
|
53
53
|
rules[table] = ownRun;
|
|
54
54
|
for (const table of ["plan_bindings", "plan_progress", "worktree_records", "audit_events"])
|
|
55
55
|
rules[table] = `${ownProject} AND protocol_id IN (${protocols})`;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
|
|
3
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
/** Checks finish before entering this synchronous registration transaction. */
|
|
6
|
+
export function ensureAggregateRepair(context, input, create) {
|
|
7
|
+
if (!input.failures.length || input.failures.some(receipt => receipt.status !== "failed" && receipt.status !== "aborted"))
|
|
8
|
+
throw new AppError("repair_intent_invalid", "Repair requires settled failed receipts", 1);
|
|
9
|
+
const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
|
|
10
|
+
return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
|
|
11
|
+
}
|
|
12
|
+
export function ensureRepairIntent(context, input, create) {
|
|
13
|
+
return context.db.writeTransaction(() => {
|
|
14
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
15
|
+
const attempt = stageAttempt(context, input.projectId, input.runId, input.stage);
|
|
16
|
+
const payload = JSON.stringify({ project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, cycle: stageCycle(context, input.projectId, input.runId, input.stage), causes: input.causes });
|
|
17
|
+
const identity = crypto.createHash("sha256").update(payload).digest("hex");
|
|
18
|
+
const prior = context.db.get("SELECT work_id, payload_json FROM repair_intents WHERE identity = ?", [identity]);
|
|
19
|
+
if (prior && prior.payload_json !== payload)
|
|
20
|
+
throw new AppError("repair_intent_conflict", "Repair identity has different causes", 1);
|
|
21
|
+
const workId = prior?.work_id ?? create();
|
|
22
|
+
if (!prior) {
|
|
23
|
+
bindStageWork(context, { projectId: input.projectId, runId: input.runId, workId, stage: input.stage, attempt });
|
|
24
|
+
context.db.run("INSERT INTO repair_intents (identity, project_id, run_id, stage, attempt, payload_hash, payload_json, work_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [identity, input.projectId, input.runId, input.stage, attempt, identity, payload, workId, context.now()]);
|
|
25
|
+
}
|
|
26
|
+
return { schema_id: "dd-flow/continuation@1", kind: "repair_required", project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, identity, work_id: workId };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function isRegisteredRepair(context, value, scope) {
|
|
30
|
+
if (!value || typeof value !== "object")
|
|
31
|
+
return false;
|
|
32
|
+
const receipt = value;
|
|
33
|
+
if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
|
|
34
|
+
return false;
|
|
35
|
+
if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
|
|
36
|
+
return false;
|
|
37
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
|
|
38
|
+
return false;
|
|
39
|
+
return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
|
|
40
|
+
}
|
|
@@ -26,6 +26,7 @@ import { controllerNativeChildren, controllerStageEntryPrompt, nextControllerFan
|
|
|
26
26
|
import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
27
27
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
28
28
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
29
|
+
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
29
30
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady } from "./run-controller-process.js";
|
|
30
31
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
31
32
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
@@ -750,7 +751,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
750
751
|
saveState(context, row, state);
|
|
751
752
|
continue;
|
|
752
753
|
}
|
|
753
|
-
|
|
754
|
+
if (state.last_continuation === fanout.fingerprint)
|
|
755
|
+
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
756
|
+
retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
|
|
754
757
|
saveState(context, row, state);
|
|
755
758
|
await prompt(session, fanout.prompt);
|
|
756
759
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
/** Only the trusted launcher is a shell fragment; all caller data is argv. */
|
|
7
|
+
export function runtimeCommand(context, args) {
|
|
8
|
+
return `${flowCommand(context)} ${args.map(shellArgument).join(" ")}`;
|
|
9
|
+
}
|
|
10
|
+
export function shellArgument(value) {
|
|
11
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
12
|
+
}
|
|
13
|
+
export function flowCommand(context) {
|
|
14
|
+
const defaultHome = path.resolve(path.join(os.homedir(), ".dd-flow"));
|
|
15
|
+
const quoted = (value) => `"${value.replace(/[\\"$`]/g, character => `\\${character}`)}"`;
|
|
16
|
+
const home = `DD_FLOW_HOME=${quoted(context.ddFlowHome)}`;
|
|
17
|
+
// The runner binds this variable to its isolated published executable.
|
|
18
|
+
if (context.env.DD_FLOW_BIN && path.isAbsolute(context.env.DD_FLOW_BIN))
|
|
19
|
+
return `${home} "$DD_FLOW_BIN"`;
|
|
20
|
+
const isolated = path.join(context.ddFlowHome, "bin", "dd-flow");
|
|
21
|
+
if (context.ddFlowHome !== defaultHome && fs.existsSync(isolated))
|
|
22
|
+
return `${home} ${quoted(isolated)}`;
|
|
23
|
+
return context.ddFlowHome === defaultHome ? "dd-flow" : `${home} dd-flow`;
|
|
24
|
+
}
|
|
25
|
+
export function recoveryEvidenceInput(runRoot, receiptId) {
|
|
26
|
+
const basename = crypto.createHash("sha256").update(receiptId).digest("hex").slice(0, 20);
|
|
27
|
+
return {
|
|
28
|
+
schema_id: "dd-flow/continuation@1",
|
|
29
|
+
kind: "needs_input",
|
|
30
|
+
actor: "coordinator",
|
|
31
|
+
input_file: path.join(runRoot, `check-recovery-${basename}.json`),
|
|
32
|
+
input_contract: { reason: "Nonempty evidence explaining what environment was restored; do not invent recovery." },
|
|
33
|
+
instruction: "Only after restoring the environment, write this JSON file. Then use command_after_input. This is not permission to change product code or bypass the failed check."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function checkRetryArgs(context, projectRoot, runId, receiptId) {
|
|
37
|
+
if (!receiptId)
|
|
38
|
+
return [];
|
|
39
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_root = ? AND id = ?", [projectRoot, runId]);
|
|
40
|
+
if (!run?.run_root)
|
|
41
|
+
throw new AppError("runtime_missing", "Check recovery requires a bound RUN root", 1);
|
|
42
|
+
return ["--retry-check", receiptId, "--reason-file", recoveryEvidenceInput(run.run_root, receiptId).input_file];
|
|
43
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { quote } from "shell-quote";
|
|
6
5
|
import { lifecycleRetryCommands, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
@@ -13,6 +12,8 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
13
12
|
import { appendFlowRunTimelineEvent, pauseFlowRunStage, resumeFlowRunStage } from "./runs.js";
|
|
14
13
|
import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.js";
|
|
15
14
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
15
|
+
import { runtimeCommand } from "./runtime-command.js";
|
|
16
|
+
export { flowCommand } from "./runtime-command.js";
|
|
16
17
|
export function prepareStagePause(context, input) {
|
|
17
18
|
if (typeof input.question !== "string" || !input.question.trim())
|
|
18
19
|
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
@@ -159,8 +160,10 @@ export function resumeStageAfterUser(context, input) {
|
|
|
159
160
|
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
160
161
|
}
|
|
161
162
|
export function stagePauseCommand(context, input) {
|
|
162
|
-
const
|
|
163
|
-
|
|
163
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
164
|
+
const run = requireRun(context, project.id, input.runId);
|
|
165
|
+
const questionInputPath = path.join(requireRunHome(run), "works", input.workId, "question-input.md");
|
|
166
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
164
167
|
}
|
|
165
168
|
/** Render separate file-write and lifecycle-call instructions for HITL. */
|
|
166
169
|
export function stagePauseCommandTemplate(command) {
|
|
@@ -185,24 +188,7 @@ Then run this standalone command:
|
|
|
185
188
|
${command}`;
|
|
186
189
|
}
|
|
187
190
|
function stageResumeCommand(context, input) {
|
|
188
|
-
return managedLifecycleCommand(context,
|
|
189
|
-
}
|
|
190
|
-
export function flowCommand(context) {
|
|
191
|
-
const defaultHome = path.resolve(path.join(os.homedir(), ".dd-flow"));
|
|
192
|
-
// Desktop/ACP tool processes are not required to inherit the environment of
|
|
193
|
-
// the adapter that created their Session. In an isolated runtime, a bare
|
|
194
|
-
// `dd-flow` can therefore select the host-global router despite a correct
|
|
195
|
-
// DD_FLOW_HOME. The runner supplies an absolute executable for that case;
|
|
196
|
-
// make every generated lifecycle command self-contained.
|
|
197
|
-
const executable = context.env.DD_FLOW_BIN;
|
|
198
|
-
if (executable && path.isAbsolute(executable)) {
|
|
199
|
-
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} "$DD_FLOW_BIN"`;
|
|
200
|
-
}
|
|
201
|
-
const isolatedExecutable = path.join(context.ddFlowHome, "bin", "dd-flow");
|
|
202
|
-
if (context.ddFlowHome !== defaultHome && fs.existsSync(isolatedExecutable)) {
|
|
203
|
-
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} ${JSON.stringify(isolatedExecutable)}`;
|
|
204
|
-
}
|
|
205
|
-
return context.ddFlowHome === defaultHome ? "dd-flow" : `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow`;
|
|
191
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "resume", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--answer-file", input.answerInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
206
192
|
}
|
|
207
193
|
function requireRun(context, projectId, id) {
|
|
208
194
|
const rows = context.db.all("SELECT id, short_id, project_id, project_root, run_root, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, id, id]);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
export function stageAttempt(context, projectId, runId, stage) {
|
|
3
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
4
|
+
if (!run)
|
|
5
|
+
throw new AppError("not_found", "Stage graph RUN is unavailable", 1);
|
|
6
|
+
const stages = JSON.parse(run.index_json).stage_runs ?? [];
|
|
7
|
+
return [...stages].reverse().find(item => item.stage === stage)?.attempt ?? "try-001";
|
|
8
|
+
}
|
|
9
|
+
export function bindStageWork(context, input) {
|
|
10
|
+
context.db.run("INSERT OR IGNORE INTO stage_work_memberships (project_id, run_id, work_id, stage, attempt, cycle) VALUES (?, ?, ?, ?, ?, ?)", [input.projectId, input.runId, input.workId, input.stage, input.attempt ?? stageAttempt(context, input.projectId, input.runId, input.stage), input.cycle ?? stageCycle(context, input.projectId, input.runId, input.stage)]);
|
|
11
|
+
}
|
|
12
|
+
export function stageCycle(context, projectId, runId, stage) {
|
|
13
|
+
if (stage !== "code" && stage !== "code-review")
|
|
14
|
+
return "";
|
|
15
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
16
|
+
const cycle = row ? JSON.parse(row.index_json).variables?.["merge.source_repair"]?.cycle : undefined;
|
|
17
|
+
return typeof cycle === "string" ? cycle : stage === "code-review" ? "initial" : "";
|
|
18
|
+
}
|
|
19
|
+
export function stageWorkIds(context, projectId, runId, stage) {
|
|
20
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stage_work_memberships'"))
|
|
21
|
+
throw new AppError("stage_graph_compatibility_required", "Stage Work membership is unavailable; use the RUN's pinned engine", 1);
|
|
22
|
+
return new Set(context.db.all("SELECT work_id FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [projectId, runId, stage, stageAttempt(context, projectId, runId, stage), stageCycle(context, projectId, runId, stage)]).map(row => row.work_id));
|
|
23
|
+
}
|
|
24
|
+
/** Generic Work APIs also serve non-stage runs; null preserves that contract. */
|
|
25
|
+
export function currentStageWorkIds(context, projectId, runId) {
|
|
26
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
27
|
+
if (!row)
|
|
28
|
+
return null;
|
|
29
|
+
const index = JSON.parse(row.index_json);
|
|
30
|
+
const stage = [...(index.stage_runs ?? [])].reverse().find(item => item.status === "running")?.stage;
|
|
31
|
+
return stage ? stageWorkIds(context, projectId, runId, stage) : null;
|
|
32
|
+
}
|
|
33
|
+
export function stageGraphScope(context, projectId, runId, stage) {
|
|
34
|
+
return { project_id: projectId, run_id: runId, attempt: stageAttempt(context, projectId, runId, stage), cycle: stageCycle(context, projectId, runId, stage) };
|
|
35
|
+
}
|
|
@@ -7,10 +7,10 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
8
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
|
-
import {
|
|
10
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair, prepareVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
13
|
+
import { addVnextCodeRepair, prepareVnextCodeRepair, registerAggregateCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
14
|
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
15
15
|
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
16
|
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
@@ -18,6 +18,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
18
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
19
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
20
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
21
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
21
22
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
22
23
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
23
24
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
@@ -93,8 +94,8 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
93
94
|
fs.writeFileSync(promptPath, prompt);
|
|
94
95
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
95
96
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
96
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items });
|
|
97
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
97
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
|
|
98
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
98
99
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
99
100
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
100
101
|
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -225,15 +226,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
225
226
|
outcome: "repair_required",
|
|
226
227
|
retry_after_workspace_change: true,
|
|
227
228
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
retry_command: finishCommand(context, run.id, projectRoot, decisionFile, failed[0].id)
|
|
229
|
+
continuation: registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks: finalChecks, generation }),
|
|
230
|
+
instruction: "Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review."
|
|
231
231
|
});
|
|
232
232
|
const stopTarget = executionStopTarget(run);
|
|
233
233
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
234
234
|
const now = context.now();
|
|
235
235
|
const startedAt = stageStartedAt(run, now);
|
|
236
|
-
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command:
|
|
236
|
+
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]), usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"]) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
|
|
237
237
|
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
238
238
|
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
239
239
|
const acceptedPaths = changedPaths(run.workspace_root);
|
|
@@ -259,7 +259,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
259
259
|
}
|
|
260
260
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
261
261
|
});
|
|
262
|
-
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context,
|
|
262
|
+
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {}) };
|
|
263
263
|
}
|
|
264
264
|
export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
|
|
265
265
|
if (!accepted)
|
|
@@ -327,7 +327,7 @@ export function assertCodeReviewCoverage(groups, reviewers) {
|
|
|
327
327
|
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
328
328
|
}
|
|
329
329
|
export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
|
|
330
|
-
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.",
|
|
330
|
+
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
331
331
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
332
332
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
333
333
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -483,7 +483,9 @@ function requireRun(context, projectId, runId) { const run = context.db.get("SEL
|
|
|
483
483
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
484
484
|
function requireHome(run) { if (!run.run_root)
|
|
485
485
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
486
|
-
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
|
|
486
|
+
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
|
|
487
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code-review", "--decision-file", decision, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
488
|
+
}
|
|
487
489
|
function findFiles(root, name) { if (!fs.existsSync(root))
|
|
488
490
|
return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
489
491
|
const file = path.join(root, entry.name);
|