@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +81 -9
  5. package/dist/cli/run-cli.js +442 -46
  6. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  25. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  26. package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
  27. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  28. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  29. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  30. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  31. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  32. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  33. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  34. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  35. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  36. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  37. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  38. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  39. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  40. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  41. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  42. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  43. package/dist/runtime/context.js +3 -1
  44. package/dist/schemas/agent-profile.schema.json +1 -1
  45. package/dist/schemas/code-review-result.schema.json +2 -2
  46. package/dist/schemas/code-work-batch.schema.json +4 -3
  47. package/dist/schemas/code-work-result.schema.json +4 -4
  48. package/dist/schemas/harness-config.schema.json +23 -0
  49. package/dist/schemas/plan-review-result.schema.json +2 -2
  50. package/dist/schemas/run-control-receipt.schema.json +99 -0
  51. package/dist/schemas/run-control-request.schema.json +36 -0
  52. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  53. package/dist/services/cleanup.js +8 -5
  54. package/dist/services/cli-operation-classifier.js +11 -1
  55. package/dist/services/code-checks.js +543 -96
  56. package/dist/services/controller-fanout.js +116 -0
  57. package/dist/services/dashboard.js +9 -0
  58. package/dist/services/engines.js +50 -19
  59. package/dist/services/eval-snapshots.js +639 -60
  60. package/dist/services/execution-policy.js +177 -0
  61. package/dist/services/external-work-launch.js +110 -0
  62. package/dist/services/harness-adapter.js +147 -24
  63. package/dist/services/harness-config.js +68 -0
  64. package/dist/services/hooks.js +413 -168
  65. package/dist/services/lanes.js +1 -1
  66. package/dist/services/lifecycle-command.js +74 -6
  67. package/dist/services/lifecycle-invocations.js +637 -0
  68. package/dist/services/managed-daemon-binding.js +38 -0
  69. package/dist/services/managed-processes.js +138 -22
  70. package/dist/services/merge-queue.js +6 -6
  71. package/dist/services/merge-server.js +75 -29
  72. package/dist/services/migrations.js +1 -1
  73. package/dist/services/native-daemon-history.js +49 -0
  74. package/dist/services/native-session-control.js +39 -0
  75. package/dist/services/projects.js +7 -3
  76. package/dist/services/prompts.js +4 -2
  77. package/dist/services/protocols.js +11 -2
  78. package/dist/services/recovery-observation-budget.js +61 -0
  79. package/dist/services/recovery-snapshot-database.js +107 -0
  80. package/dist/services/run-control-receipt.js +56 -0
  81. package/dist/services/run-control-worker.js +363 -0
  82. package/dist/services/run-control.js +876 -0
  83. package/dist/services/run-controller-adapter.js +211 -0
  84. package/dist/services/run-controller-capture.js +134 -0
  85. package/dist/services/run-controller-process.js +189 -0
  86. package/dist/services/run-controller-recovery.js +222 -0
  87. package/dist/services/run-controller-state.js +31 -0
  88. package/dist/services/run-controller.js +733 -0
  89. package/dist/services/run-engine-bindings.js +20 -62
  90. package/dist/services/run-fork.js +124 -0
  91. package/dist/services/run-observations.js +112 -0
  92. package/dist/services/run-recovery-runtime.js +69 -0
  93. package/dist/services/run-recovery.js +327 -0
  94. package/dist/services/runs.js +280 -40
  95. package/dist/services/runtime-budget.js +362 -0
  96. package/dist/services/runtime-scope-capture.js +50 -0
  97. package/dist/services/runtime-scope-control.js +450 -0
  98. package/dist/services/runtime-scope-resume.js +543 -0
  99. package/dist/services/runtime-scope-stop.js +99 -0
  100. package/dist/services/runtime-scope-worker.js +210 -0
  101. package/dist/services/runtime-service.js +96 -0
  102. package/dist/services/schema-validation.js +12 -12
  103. package/dist/services/stage-context.js +2 -2
  104. package/dist/services/stage-lifecycle.js +32 -10
  105. package/dist/services/stage-pause.js +25 -13
  106. package/dist/services/usage.js +37 -93
  107. package/dist/services/vnext-code-review.js +149 -89
  108. package/dist/services/vnext-code.js +224 -216
  109. package/dist/services/vnext-execution-profile.js +5 -3
  110. package/dist/services/vnext-fanout.js +135 -21
  111. package/dist/services/vnext-merge.js +258 -89
  112. package/dist/services/vnext-plan-review.js +75 -64
  113. package/dist/services/vnext-plan.js +77 -29
  114. package/dist/services/vnext-protocolize.js +21 -14
  115. package/dist/services/vnext-specify.js +26 -16
  116. package/dist/services/work-registry.js +605 -157
  117. package/dist/services/workspace-bootstrap.js +76 -0
  118. package/dist/shared/errors.js +12 -0
  119. package/dist/storage/database.js +341 -31
  120. package/dist/storage/writer-contract.js +77 -0
  121. package/dist/storage/writer-migration.js +102 -0
  122. package/package.json +20 -13
  123. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,6 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { quote } from "shell-quote";
5
+ import { observeLifecycleCommand } from "../services/lifecycle-invocations.js";
6
+ import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, issueLifecycleInvocation, lifecycleInvocationScope, settleLifecycleInvocation, settleLifecycleRejection } from "../services/lifecycle-invocations.js";
3
7
  import { createContext, createRouterContext } from "../runtime/context.js";
8
+ import { migrateStoreWriter } from "../storage/writer-migration.js";
4
9
  import { helpForArgs } from "./help.js";
5
10
  import { AppError, isAppError } from "../shared/errors.js";
6
11
  import { writeJson } from "../shared/json.js";
@@ -10,7 +15,7 @@ import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlan
10
15
  import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
11
16
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
12
17
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
13
- import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
18
+ import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeHook, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
14
19
  import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
15
20
  import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
16
21
  import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
@@ -26,7 +31,7 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunSe
26
31
  import { previewNextEntityId } from "../services/ids.js";
27
32
  import { validateSchema } from "../services/schema-validation.js";
28
33
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
29
- import { ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
34
+ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
30
35
  import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
31
36
  import { publishProjectSummary } from "../services/project-summary.js";
32
37
  import { requireProjectByRoot } from "../services/projects.js";
@@ -39,18 +44,30 @@ import { finishVnextPlan, isVnextPlanRun, startVnextPlan } from "../services/vne
39
44
  import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
40
45
  import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services/vnext-code.js";
41
46
  import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
42
- import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
47
+ import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
43
48
  import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
44
- import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
49
+ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout } from "../services/vnext-fanout.js";
45
50
  import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
46
51
  import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
47
52
  import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
48
53
  import { renderWorkerPrompt } from "../services/prompts.js";
49
54
  import { finishStage, startStage } from "../services/stage-lifecycle.js";
50
- import { addWorkBatch, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
55
+ import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
56
+ import { launchExternalWork } from "../services/external-work-launch.js";
51
57
  import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
58
+ import { forkRunFromSnapshot } from "../services/run-fork.js";
59
+ import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
60
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
52
61
  import { loadExternalStageContext } from "../services/stage-context.js";
53
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
62
+ import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
63
+ import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns } from "../services/runtime-budget.js";
64
+ import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
65
+ import { launchRuntimeScopeControl, launchRuntimeScopeResume, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
66
+ import { launchRunController, resumeRunController, serveRunController, runControllerStatus, answerRunController, supplyRunControllerContext } from "../services/run-controller.js";
67
+ import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
68
+ import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
69
+ import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
70
+ import { startRuntimeService } from "../services/runtime-service.js";
54
71
  const defaultIo = {
55
72
  stdout: process.stdout,
56
73
  stderr: process.stderr,
@@ -58,9 +75,20 @@ const defaultIo = {
58
75
  };
59
76
  const flagValue = "__dd_flow_flag__";
60
77
  export async function runCli(args, io = defaultIo, env = process.env) {
61
- const output = parseOutputOptions(args);
78
+ let output;
79
+ try {
80
+ output = parseOutputOptions(args);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ writeJson(io.stderr, { ok: false, error: { code: "usage", message } });
85
+ return 2;
86
+ }
62
87
  const progress = progressForCommand(output.args);
63
88
  let heartbeat;
89
+ let invocationSettlement;
90
+ let settleHook;
91
+ let diagnosticHome;
64
92
  try {
65
93
  if (output.args.length === 1 && output.args[0] === "--version") {
66
94
  const payload = getCliVersionReport();
@@ -75,6 +103,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
75
103
  io.stdout.write(`${help}\n`);
76
104
  return 0;
77
105
  }
106
+ assertSingletonOptions(output.args);
107
+ if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
108
+ const controlArgs = parseArgs(output.args.slice(2));
109
+ requiredOption(controlArgs, "run");
110
+ requiredOption(controlArgs, "project-root");
111
+ }
112
+ if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
113
+ && hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
114
+ throw new AppError("usage", "--result-stdin requires non-interactive input. Pipe or redirect the complete JSON into this command, or use --result-file; interactive terminals can truncate long JSON and wait indefinitely for EOF.", 2);
115
+ }
78
116
  const classification = classifyCliOperation(output.args, env);
79
117
  if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
80
118
  throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
@@ -94,14 +132,54 @@ export async function runCli(args, io = defaultIo, env = process.env) {
94
132
  if (routed?.routed && !routed.inProcess) {
95
133
  if (heartbeat)
96
134
  clearInterval(heartbeat);
97
- emitHumanProgress(io, output, progress, "done");
135
+ emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
136
+ if (output.progressJsonl && routed.exitCode !== 0)
137
+ writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
98
138
  return routed.exitCode;
99
139
  }
100
140
  const dispatchContext = routed?.inProcess
101
141
  ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
102
- : createContext(env, databaseModeForCommand(output.args, classification));
142
+ : createContext({ ...env }, databaseModeForCommand(output.args, classification));
143
+ diagnosticHome = dispatchContext.ddFlowHome;
103
144
  preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
104
- const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot, classification, output);
145
+ dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
146
+ let dispatchArgs = output.args;
147
+ settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
148
+ let retainedOutcome;
149
+ const invocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
150
+ if (invocationId) {
151
+ const command = quote(["dd-flow", ...output.args]);
152
+ const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
153
+ assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
154
+ if (admission.replay) {
155
+ retainedOutcome = admission.outcome;
156
+ settleHook = undefined;
157
+ delete dispatchContext.observeLifecycleHook;
158
+ }
159
+ else {
160
+ invocationSettlement = { context: dispatchContext, id: invocationId };
161
+ dispatchArgs = [...output.args, "--hook-event-id", admission.eventKey];
162
+ dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
163
+ dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
164
+ }
165
+ }
166
+ if (retainedOutcome?.error) {
167
+ const error = retainedOutcome.error;
168
+ throw new AppError(error.code, error.message, error.exitCode, error.details);
169
+ }
170
+ if (!retainedOutcome)
171
+ settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
172
+ const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output);
173
+ const completedHook = settleHook;
174
+ settleHook = undefined;
175
+ completedHook?.({ result });
176
+ if (invocationSettlement) {
177
+ const completed = invocationSettlement;
178
+ invocationSettlement = undefined;
179
+ // A failure to persist an already completed result is an unknown reply,
180
+ // not a failed operation eligible for a new productive retry.
181
+ settleLifecycleInvocation(completed.context, completed.id, { result });
182
+ }
105
183
  const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
106
184
  const resultWithWarnings = addWarnings(result, refreshWarnings);
107
185
  if (heartbeat)
@@ -117,9 +195,41 @@ export async function runCli(args, io = defaultIo, env = process.env) {
117
195
  io.stdout.write(renderHumanResult(output.args, payload));
118
196
  return completedCommandExitCode(payload);
119
197
  }
120
- catch (error) {
198
+ catch (caught) {
199
+ const error = isAppError(caught) ? caught : new AppError("unexpected", caught instanceof Error ? caught.message : String(caught), 1, { effect: "unknown", cause: { code: caught?.code ?? null, sqlite_extended_code: caught?.errcode ?? null } });
200
+ if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
201
+ try {
202
+ const directory = path.join(diagnosticHome, "diagnostics", "cli");
203
+ fs.mkdirSync(directory, { recursive: true });
204
+ const artifact = path.join(directory, `${randomUUID()}.json`);
205
+ fs.writeFileSync(artifact, JSON.stringify({ code: error.code, message: error.message, details: error.details, stack: caught instanceof Error ? caught.stack : null }), { flag: "wx", mode: 0o600 });
206
+ error.details.diagnostic_artifact = artifact;
207
+ }
208
+ catch (secondary) {
209
+ io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
210
+ }
211
+ }
121
212
  if (heartbeat)
122
213
  clearInterval(heartbeat);
214
+ if (invocationSettlement && error.details.effect !== "committed") {
215
+ try {
216
+ if (isAppError(error))
217
+ settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
218
+ else
219
+ settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
220
+ }
221
+ catch (persistenceError) {
222
+ io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
223
+ }
224
+ }
225
+ if (settleHook) {
226
+ try {
227
+ settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
228
+ }
229
+ catch (persistence) {
230
+ io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
231
+ }
232
+ }
123
233
  const payload = isAppError(error)
124
234
  ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
125
235
  : { ok: false, error: { code: "unexpected", message: String(error) } };
@@ -127,7 +237,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
127
237
  if (output.progressJsonl)
128
238
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
129
239
  const errorPayload = attachProgress(payload, progress, output);
130
- const visible = writeResponseFile(errorPayload, output);
240
+ let visible = errorPayload;
241
+ try {
242
+ visible = writeResponseFile(errorPayload, output);
243
+ }
244
+ catch (publication) {
245
+ error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
246
+ }
131
247
  if (output.json && output.progressJsonl)
132
248
  writeJson(io.stdout, errorPayload);
133
249
  else if (output.json)
@@ -152,6 +268,7 @@ function resolveProjectRootBeforeRouting(args, env) {
152
268
  function databaseModeForCommand(args, classification) {
153
269
  const [family, command] = args;
154
270
  if (family === "engine" ||
271
+ family === "storage" && command === "migrate-writer" ||
155
272
  family === "version" ||
156
273
  family === "schema" ||
157
274
  family === "canon" && command === "resolve" ||
@@ -503,6 +620,20 @@ function shortSha(value) {
503
620
  async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
504
621
  const [family, command, ...rest] = args;
505
622
  const parsed = parseArgs(rest);
623
+ if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
624
+ const payload = JSON.parse(await readStdin(io.stdin));
625
+ if (typeof payload.command !== "string" || !payload.scope || typeof payload.scope !== "object")
626
+ throw new AppError("usage", "Expected command and managed scope on stdin", 2);
627
+ assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
628
+ return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
629
+ }
630
+ if (family === "storage" && command === "migrate-writer") {
631
+ if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
632
+ throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
633
+ if (parsed.options.get("offline")?.[0] !== flagValue)
634
+ throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
635
+ return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
636
+ }
506
637
  if (family === "engine") {
507
638
  return dispatchEngine(context, command, parsed, classification);
508
639
  }
@@ -517,7 +648,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
517
648
  return getCliVersionReport();
518
649
  }
519
650
  if (family === "runtime") {
520
- return await dispatchRuntime(context, command, parsed);
651
+ return await dispatchRuntime(context, command, parsed, commandProgress(io, output));
521
652
  }
522
653
  if (family === "canon") {
523
654
  return dispatchCanon(context, command, parsed);
@@ -660,12 +791,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
660
791
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
661
792
  if (action === "status")
662
793
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
794
+ if (action === "reconcile")
795
+ return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
663
796
  if (action === "dispatch") {
664
797
  if (stage === "plan-review")
665
798
  return dispatchVnextPlanReview(context, { projectRoot, runId });
666
799
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
667
800
  }
668
- throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
801
+ throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch|reconcile> <RUN> --stage <stage> --project-root <path>; reconcile requires --observations-file <native-observations.json>", 2);
802
+ }
803
+ if (family === "stage" && command === "native") {
804
+ if (requiredPosition(parsed, 0, "native action") !== "observe")
805
+ throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
806
+ return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
669
807
  }
670
808
  if (family === "run" && command === "capacity") {
671
809
  if (requiredPosition(parsed, 0, "capacity action") !== "record")
@@ -752,21 +890,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
752
890
  }
753
891
  if (family === "work" && command === "delete")
754
892
  return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
893
+ if (family === "work" && command === "launch")
894
+ return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
755
895
  if (family === "work" && command === "start") {
756
896
  const hookEventId = optionalOption(parsed, "hook-event-id");
897
+ const recoveryId = optionalOption(parsed, "recovery-id");
757
898
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
758
- return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
899
+ return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
759
900
  }
760
901
  if (family === "work" && command === "finish") {
761
902
  const resultFile = optionalOption(parsed, "result-file");
762
903
  const resultStdin = hasOption(parsed, "result-stdin");
763
904
  if (Boolean(resultFile) === resultStdin)
764
905
  throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
906
+ const workId = requiredPosition(parsed, 0, "work-id");
907
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
908
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
909
+ const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = optionalOption(parsed, "reason");
910
+ if (Boolean(retryReceiptId) !== Boolean(retryReason))
911
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
765
912
  const result = resultFile ? fs.readFileSync(path.resolve(resultFile), "utf8") : await readStdin(io.stdin);
766
- return await finishWork(context, requiredPosition(parsed, 0, "work-id"), result, commandProgress(io, output));
913
+ return await finishWork(context, workId, result, commandProgress(io, output), retryReceiptId ? { receiptId: retryReceiptId, reason: retryReason } : undefined);
914
+ }
915
+ if (family === "work" && command === "fail") {
916
+ const workId = requiredPosition(parsed, 0, "work-id");
917
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
918
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
919
+ return failWork(context, workId, requiredOption(parsed, "reason"));
767
920
  }
768
- if (family === "work" && command === "fail")
769
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
770
921
  if (family === "work" && command === "cancel")
771
922
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
772
923
  if (family === "work" && command === "retry")
@@ -874,9 +1025,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
874
1025
  const projectRoot = requiredOption(parsed, "project-root");
875
1026
  const runId = requiredPosition(parsed, 0, "run-id");
876
1027
  const stage = requiredOption(parsed, "stage");
1028
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
877
1029
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
878
1030
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
879
1031
  const resultStdin = hasOption(parsed, "result-stdin");
1032
+ const retryCheckId = optionalOption(parsed, "retry-check");
1033
+ const retryReason = optionalOption(parsed, "reason");
1034
+ const hookEventId = optionalOption(parsed, "hook-event-id");
1035
+ if (retryCheckId && !retryReason?.trim())
1036
+ throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
1037
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1038
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1039
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1040
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1041
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1042
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1043
+ if (protectedLifecycle)
1044
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
880
1045
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
881
1046
  const outcome = optionalOption(parsed, "outcome");
882
1047
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -888,65 +1053,77 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
888
1053
  if (!resultFile && !resultStdin) {
889
1054
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
890
1055
  }
891
- return finishVnextSpecifyStage(context, {
1056
+ return controlledReply(await finishVnextSpecifyStage(context, {
892
1057
  projectRoot,
893
1058
  runId,
894
1059
  ...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
895
1060
  outcome: outcome
896
- });
1061
+ }));
897
1062
  }
898
1063
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
899
1064
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
900
1065
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
901
1066
  }
902
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
1067
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile }));
903
1068
  }
904
1069
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
905
1070
  if (optionalOption(parsed, "code-work-batch-file"))
906
1071
  throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
907
- return finishVnextPlan(context, { projectRoot, runId });
1072
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId }));
908
1073
  }
909
1074
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
910
1075
  if (resultStdin || hasOption(parsed, "outcome"))
911
1076
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
912
1077
  const decisionFile = optionalOption(parsed, "decision-file");
913
- return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
1078
+ return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) }));
914
1079
  }
915
1080
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
916
1081
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
917
1082
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
918
1083
  }
919
- return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
1084
+ return controlledReply(await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
920
1085
  }
921
1086
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
922
1087
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
923
1088
  throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
924
- return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), progress: commandProgress(io, output) });
1089
+ return controlledReply(await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
925
1090
  }
926
1091
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
927
1092
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
928
1093
  throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
929
- return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), progress: commandProgress(io, output) });
1094
+ return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
930
1095
  }
931
1096
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
932
1097
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
933
1098
  }
934
- return finishStage(context, {
1099
+ return controlledReply(await finishStage(context, {
935
1100
  projectRoot,
936
1101
  runId,
937
1102
  stage,
938
1103
  ...(dir ? { dir } : {}),
939
1104
  ...(semanticFile ? { semanticFile } : {})
940
- });
1105
+ }));
941
1106
  }
942
1107
  if (family === "stage" && command === "pause") {
943
1108
  if (!hasOption(parsed, "question-stdin"))
944
1109
  throw new AppError("usage", "stage pause requires --question-stdin", 2);
1110
+ const projectRoot = requiredOption(parsed, "project-root");
1111
+ const runId = requiredPosition(parsed, 0, "run-id");
1112
+ const stage = requiredOption(parsed, "stage");
1113
+ const workId = requiredOption(parsed, "work");
1114
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1115
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1116
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1117
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1118
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1119
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1120
+ if (protectedLifecycle)
1121
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
945
1122
  return pauseStageForUser(context, {
946
- projectRoot: requiredOption(parsed, "project-root"),
947
- runId: requiredPosition(parsed, 0, "run-id"),
948
- stage: requiredOption(parsed, "stage"),
949
- workId: requiredOption(parsed, "work"),
1123
+ projectRoot,
1124
+ runId,
1125
+ stage,
1126
+ workId,
950
1127
  question: await readStdin(io.stdin)
951
1128
  });
952
1129
  }
@@ -1007,6 +1184,48 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1007
1184
  return dispatchSession(context, command, parsed);
1008
1185
  }
1009
1186
  if (family === "run") {
1187
+ if (command === "control") {
1188
+ const action = requiredPosition(parsed, 0, "control action");
1189
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1190
+ if (action === "status")
1191
+ return runControlStatus(context, scope);
1192
+ if (action === "reconcile") {
1193
+ if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1194
+ throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
1195
+ return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
1196
+ }
1197
+ if (action === "resume") {
1198
+ if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
1199
+ throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
1200
+ return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
1201
+ }
1202
+ if (action === "serve")
1203
+ return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
1204
+ if (action === "pause" || action === "stop")
1205
+ return await requestRunControl(context, {
1206
+ ...scope, action, requestId: requiredOption(parsed, "request-id"),
1207
+ ...(hasOption(parsed, "force") ? { force: true } : {}),
1208
+ ...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
1209
+ ...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
1210
+ ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
1211
+ });
1212
+ throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
1213
+ }
1214
+ if (command === "drive") {
1215
+ const action = requiredPosition(parsed, 0, "drive action");
1216
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1217
+ if (action === "status")
1218
+ return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
1219
+ if (action === "launch")
1220
+ return await launchRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256"), ...(optionalOption(parsed, "stop-after") ? { stopAfter: optionalOption(parsed, "stop-after") } : {}), ...(optionalOption(parsed, "capture-root") ? { captureRoot: optionalOption(parsed, "capture-root") } : {}) });
1221
+ if (action === "serve")
1222
+ return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
1223
+ if (action === "answer")
1224
+ return answerRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), pauseId: requiredOption(parsed, "pause-id"), answerFile: requiredOption(parsed, "answer-file") });
1225
+ if (action === "context")
1226
+ return supplyRunControllerContext(context, { ...scope, controllerId: requiredOption(parsed, "controller-id"), requestId: requiredOption(parsed, "request-id"), stage: requiredOption(parsed, "stage"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256") });
1227
+ throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
1228
+ }
1010
1229
  return dispatchRun(context, command, parsed);
1011
1230
  }
1012
1231
  if (family === "stat") {
@@ -1039,6 +1258,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1039
1258
  if (family === "codex" && command === "hook") {
1040
1259
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
1041
1260
  }
1261
+ if (family === "zcode" && command === "hook") {
1262
+ return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
1263
+ }
1042
1264
  if (family === "zcode" && command === "event") {
1043
1265
  return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
1044
1266
  }
@@ -1063,6 +1285,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1063
1285
  throw new AppError("usage", "Unknown opencode usage action", 2);
1064
1286
  return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1065
1287
  }
1288
+ if (family === "droid" && command === "event") {
1289
+ if (requiredPosition(parsed, 0, "droid event action") !== "handle")
1290
+ throw new AppError("usage", "Unknown droid event action", 2);
1291
+ return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1292
+ }
1293
+ if (family === "droid" && command === "usage") {
1294
+ if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
1295
+ throw new AppError("usage", "Unknown droid usage action", 2);
1296
+ return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1297
+ }
1066
1298
  if (family === "agy" && command === "event")
1067
1299
  return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
1068
1300
  if (family === "agy" && command === "usage") {
@@ -1247,6 +1479,8 @@ async function dispatchMerge(context, command, parsed, progress) {
1247
1479
  }
1248
1480
  if (command === "apply")
1249
1481
  return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
1482
+ if (command === "repair")
1483
+ return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
1250
1484
  if (command === "request") {
1251
1485
  const action = requiredPosition(parsed, 0, "request action");
1252
1486
  const requestId = requiredPosition(parsed, 1, "merge-request-id");
@@ -1410,6 +1644,24 @@ function dispatchSession(context, command, parsed) {
1410
1644
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1411
1645
  }
1412
1646
  function dispatchRun(context, command, parsed) {
1647
+ if (command === "recovery") {
1648
+ const action = requiredPosition(parsed, 0, "recovery action");
1649
+ const projectRoot = requiredOption(parsed, "project-root");
1650
+ const runId = requiredPosition(parsed, 1, "run-id");
1651
+ if (action === "inspect")
1652
+ return inspectRunRecovery(context, { projectRoot, runId });
1653
+ if (action === "begin")
1654
+ return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") });
1655
+ if (action === "seal")
1656
+ return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") });
1657
+ if (action === "capture")
1658
+ return recordRecoveryCapture(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") });
1659
+ if (action === "resume")
1660
+ return resumeRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), daemonId: requiredOption(parsed, "daemon-id"), harness: requiredOption(parsed, "harness"), nativeSessionId: requiredOption(parsed, "native-session-id") });
1661
+ if (action === "accept")
1662
+ return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1663
+ throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
1664
+ }
1413
1665
  if (command === "snapshot") {
1414
1666
  const action = requiredPosition(parsed, 0, "snapshot action");
1415
1667
  if (action === "bootstrap") {
@@ -1428,16 +1680,20 @@ function dispatchRun(context, command, parsed) {
1428
1680
  }
1429
1681
  if (action === "create") {
1430
1682
  const candidate = parsed.options.has("candidate");
1683
+ const incomplete = parsed.options.has("incomplete");
1684
+ const recoveryId = optionalOption(parsed, "recovery-id");
1431
1685
  const stageEntry = optionalOption(parsed, "stage-entry");
1432
- if (candidate && stageEntry !== undefined)
1433
- throw new AppError("usage", "Use either --candidate or --stage-entry, not both", 2);
1434
- if (!candidate && stageEntry === undefined)
1435
- throw new AppError("usage", "Snapshot create requires --candidate or --stage-entry", 2);
1686
+ if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) + Number(recoveryId !== undefined) !== 1)
1687
+ throw new AppError("usage", "Use exactly one of --candidate, --incomplete, --recovery-id or --stage-entry", 2);
1436
1688
  const input = {
1437
1689
  projectRoot: requiredOption(parsed, "project-root"),
1438
1690
  runId: requiredPosition(parsed, 1, "run-id"),
1439
1691
  output: requiredOption(parsed, "output")
1440
1692
  };
1693
+ if (incomplete)
1694
+ return createEvalRunSnapshot(context, { ...input, incomplete: true });
1695
+ if (recoveryId)
1696
+ return createEvalRunSnapshot(context, { ...input, recoveryId });
1441
1697
  return candidate
1442
1698
  ? createEvalRunSnapshot(context, { ...input, candidate: true })
1443
1699
  : createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
@@ -1445,12 +1701,23 @@ function dispatchRun(context, command, parsed) {
1445
1701
  if (action === "restore")
1446
1702
  return restoreEvalRunSnapshot(context, {
1447
1703
  snapshot: requiredOption(parsed, "snapshot"),
1704
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1705
+ ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}),
1448
1706
  projectRoot: requiredOption(parsed, "project-root")
1449
1707
  });
1450
1708
  throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore | dd-flow run snapshot create <RUN> (--stage-entry <stage>|--candidate) --project-root <path> --output <directory> | dd-flow run snapshot restore --snapshot <directory> --project-root <path>", 2);
1451
1709
  }
1710
+ if (command === "fork") {
1711
+ return forkRunFromSnapshot(context, {
1712
+ snapshot: requiredOption(parsed, "from"),
1713
+ output: requiredOption(parsed, "output"),
1714
+ engineVersion: requiredOption(parsed, "engine-version"),
1715
+ requestId: requiredOption(parsed, "request-id"),
1716
+ ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {})
1717
+ });
1718
+ }
1452
1719
  if (command === "prepare-vnext-specify") {
1453
- return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
1720
+ return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
1454
1721
  }
1455
1722
  if (command === "start") {
1456
1723
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
@@ -1467,6 +1734,7 @@ function dispatchRun(context, command, parsed) {
1467
1734
  nextAction: optionalOption(parsed, "next-action"),
1468
1735
  preset: optionalOption(parsed, "preset"),
1469
1736
  taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
1737
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1470
1738
  ...(protocolOverrides ? { protocolOverrides } : {}),
1471
1739
  runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
1472
1740
  });
@@ -1581,30 +1849,113 @@ function dispatchRun(context, command, parsed) {
1581
1849
  }
1582
1850
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1583
1851
  }
1584
- async function dispatchRuntime(context, command, parsed) {
1852
+ async function dispatchRuntime(context, command, parsed, progress) {
1853
+ if (command === "scope") {
1854
+ if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
1855
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1856
+ throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
1857
+ return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
1858
+ }
1859
+ if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
1860
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1861
+ throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
1862
+ return { ok: true, ...await launchRuntimeScopeResume(context, { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") }) };
1863
+ }
1864
+ if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
1865
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1866
+ throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
1867
+ const mode = requiredOption(parsed, "mode");
1868
+ if (mode !== "pause" && mode !== "stop")
1869
+ throw new AppError("usage", "Scope control mode must be pause or stop", 2);
1870
+ return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, hasOption(parsed, "manifest-json") ? JSON.parse(requiredOption(parsed, "manifest-json")) : undefined) };
1871
+ }
1872
+ if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
1873
+ if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1874
+ throw new AppError("usage", "Invalid internal scope worker arguments", 2);
1875
+ return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
1876
+ }
1877
+ if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
1878
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1879
+ throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
1880
+ return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1881
+ }
1882
+ if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
1883
+ if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
1884
+ throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
1885
+ return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
1886
+ }
1887
+ if (parsed.positional.length !== 1 || parsed.positional[0] !== "fence" || [...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1888
+ throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
1889
+ return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1890
+ }
1585
1891
  if (command !== "process")
1586
- throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
1892
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
1587
1893
  const action = requiredPosition(parsed, 0, "runtime process action");
1894
+ if (action === "check-admission")
1895
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
1896
+ if (action === "start")
1897
+ return await startRuntimeService(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), command: requiredOption(parsed, "command"), ports: requiredOption(parsed, "ports").split(","), readyPort: requiredOption(parsed, "ready-port"), readyPath: optionalOption(parsed, "ready-path") ?? "/", timeoutMs: optionalPositiveNumber(parsed, "ready-timeout-ms", true) ?? 30_000, progress });
1898
+ if (action === "stop")
1899
+ return { ok: true, process: await stopManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(optionalPositiveNumber(parsed, "grace-ms", true) ? { graceMs: optionalPositiveNumber(parsed, "grace-ms", true) } : {}) }) };
1588
1900
  if (action === "register") {
1589
1901
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1902
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
1903
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
1904
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
1905
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
1590
1906
  const process = registerManagedProcess(context, {
1591
1907
  kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1592
- projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1908
+ uniqueActiveOperation: requiredOption(parsed, "kind") === "eval-observer",
1909
+ ...(requiredOption(parsed, "kind") === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
1910
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true),
1911
+ projectId, runId,
1593
1912
  workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1594
1913
  operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
1595
- stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
1914
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {}),
1915
+ metadata: { ...(budget ? { budget, harness: optionalOption(parsed, "harness") ?? null } : {}), ...(projectId && runId ? { run_generation: guard?.generation ?? 0, run_admitted: Boolean(run) && !["draining", "sealed", "resuming"].includes(guard?.status ?? "") } : {}), ...(optionalOption(parsed, "dispatch-barrier") ? { dispatch_barrier: optionalOption(parsed, "dispatch-barrier") } : {}), ...(optionalOption(parsed, "role") ? { role: optionalOption(parsed, "role") } : {}), ...(optionalOption(parsed, "state-dir") ? { state_dir: optionalOption(parsed, "state-dir") } : {}), ...(optionalOption(parsed, "adapter-executable") ? { adapter_executable: optionalOption(parsed, "adapter-executable") } : {}) }
1596
1916
  });
1597
1917
  return { ok: true, process };
1598
1918
  }
1599
1919
  if (action === "confirm") {
1600
1920
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1601
1921
  const processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
1602
- return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
1922
+ return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
1603
1923
  }
1604
1924
  if (action === "heartbeat") {
1605
1925
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1606
1926
  return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
1607
1927
  }
1928
+ if (action === "admission") {
1929
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1930
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || record.state !== "running" || !record.pid || !record.pid_started_at || !processIsAlive(record))
1931
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
1932
+ const metadata = JSON.parse(record.metadata_json);
1933
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
1934
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
1935
+ if (record.project_id && record.run_id) {
1936
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1937
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
1938
+ const recovering = recoveryRuntimeAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), daemonId: optionalOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
1939
+ if (!recovering) {
1940
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
1941
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
1942
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
1943
+ }
1944
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
1945
+ const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
1946
+ const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
1947
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
1948
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
1949
+ }
1950
+ const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
1951
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
1952
+ }
1953
+ if (action === "release-turn") {
1954
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1955
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1956
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
1957
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
1958
+ }
1608
1959
  if (action === "finish") {
1609
1960
  const state = requiredOption(parsed, "state");
1610
1961
  if (state !== "stopped" && state !== "failed")
@@ -1615,7 +1966,7 @@ async function dispatchRuntime(context, command, parsed) {
1615
1966
  return { ok: true, processes: managedProcessStatus(context) };
1616
1967
  if (action === "reconcile")
1617
1968
  return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalPositiveNumber(parsed, "grace-ms", true)) };
1618
- throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
1969
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
1619
1970
  }
1620
1971
  function dispatchStat(context, command, parsed) {
1621
1972
  if (command === "usage") {
@@ -1797,16 +2148,37 @@ function dispatchSchema(context, command, parsed) {
1797
2148
  if (command === "validate") {
1798
2149
  const schemaDir = optionalOption(parsed, "schema-dir");
1799
2150
  const projectRoot = optionalOption(parsed, "project-root");
2151
+ const file = requiredOption(parsed, "file");
2152
+ // A generic validation command can still bind to the exact RUN without
2153
+ // searching project directories: run_id is a structural field in the
2154
+ // submitted artifact and the database owns its materialized run_root.
2155
+ const explicitRunId = optionalOption(parsed, "run");
2156
+ const declaredRunId = explicitRunId ?? readRunId(file);
2157
+ const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
2158
+ const bound = projectRoot
2159
+ ? context.db.get("SELECT r.id, r.run_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE p.root = ? AND (r.id = ? OR r.run_root = ?) LIMIT 1", [fs.realpathSync(projectRoot), declaredRunId ?? "", fileRunRoot ?? ""])
2160
+ : undefined;
1800
2161
  return validateSchema({
1801
2162
  schemaName: requiredOption(parsed, "schema"),
1802
- file: requiredOption(parsed, "file"),
2163
+ file,
1803
2164
  ...(schemaDir ? { schemaDir } : {}),
1804
2165
  ...(projectRoot ? { projectRoot } : {}),
2166
+ ...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
2167
+ ...(bound?.run_root ? { runRoot: bound.run_root } : {}),
1805
2168
  ddFlowHome: context.ddFlowHome
1806
2169
  });
1807
2170
  }
1808
2171
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
1809
2172
  }
2173
+ function readRunId(file) {
2174
+ try {
2175
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
2176
+ return typeof value.run_id === "string" && value.run_id.trim() ? value.run_id : undefined;
2177
+ }
2178
+ catch {
2179
+ return undefined;
2180
+ }
2181
+ }
1810
2182
  async function dispatchLane(context, command, parsed) {
1811
2183
  if (command === "status") {
1812
2184
  return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
@@ -2006,6 +2378,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
2006
2378
  }
2007
2379
  throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
2008
2380
  }
2381
+ function dispatchZcodeHook(context, parsed, stdin) {
2382
+ if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
2383
+ throw new AppError("usage", "Unknown zcode hook action", 2);
2384
+ const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
2385
+ if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
2386
+ return { hookSpecificOutput: result.hookSpecificOutput };
2387
+ }
2388
+ return {};
2389
+ }
2009
2390
  function dispatchGrokEvent(context, parsed, stdin) {
2010
2391
  const action = requiredPosition(parsed, 0, "grok event action");
2011
2392
  if (action === "handle") {
@@ -2128,6 +2509,21 @@ function parseFlowFlagOverrides(values) {
2128
2509
  function parseArgs(args) {
2129
2510
  return parseCommandArgs(args);
2130
2511
  }
2512
+ /** Most CLI options name one authority, path or target. Silently taking the
2513
+ * last duplicate can widen a mutation after an operator typo. Keep only the
2514
+ * options consumed as intentional arrays repeatable. */
2515
+ function assertSingletonOptions(args) {
2516
+ const repeatable = new Set(["alias", "flag", "hide", "check-ref", "evidence", "from-check", "from-finding", "from-unresolved", "on", "origin-work"]);
2517
+ const parsed = parseCommandArgs(args);
2518
+ if (args[0] === "stage" && args[1] === "finish"
2519
+ && ["result-file", "semantic-file", "data", "result-stdin"].filter(name => parsed.options.has(name)).length > 1) {
2520
+ throw new AppError("usage", "Stage finish requires exactly one result input; result-file, semantic-file, data and result-stdin are mutually exclusive", 2);
2521
+ }
2522
+ for (const [name, values] of parsed.options) {
2523
+ if (!repeatable.has(name) && values.length > 1)
2524
+ throw new AppError("usage", `--${name} may be supplied only once`, 2);
2525
+ }
2526
+ }
2131
2527
  function requiredPosition(parsed, index, label) {
2132
2528
  const value = parsed.positional[index];
2133
2529
  if (!value) {
@@ -2303,7 +2699,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2303
2699
  if (family === "merge-queue") {
2304
2700
  return projectRootForMergeQueueMutation(context, command, parsed);
2305
2701
  }
2306
- if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
2702
+ if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
2307
2703
  return requiredOption(parsed, "project-root");
2308
2704
  }
2309
2705
  if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
@@ -2336,7 +2732,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2336
2732
  if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
2337
2733
  return requiredOption(parsed, "project-root");
2338
2734
  }
2339
- if (family === "agy" && ((command === "event" && requiredPosition(parsed, 0, "agy event action") === "handle") || command === "usage"))
2735
+ if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
2340
2736
  return requiredOption(parsed, "project-root");
2341
2737
  if (family === "codex" && command === "hooks") {
2342
2738
  const action = requiredPosition(parsed, 0, "codex hooks action");