@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1344 -376
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +575 -293
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +835 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +343 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +69 -28
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +701 -164
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/entity-references.js +8 -0
  131. package/dist/shared/errors.js +12 -0
  132. package/dist/storage/database.js +444 -32
  133. package/dist/storage/paths.js +17 -4
  134. package/dist/storage/work-references.js +23 -0
  135. package/dist/storage/writer-contract.js +77 -0
  136. package/dist/storage/writer-migration.js +102 -0
  137. package/package.json +11 -4
  138. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,56 +1,80 @@
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 { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
7
+ import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleInvocation, settleLifecycleInvocation, settleLifecycleRejection } from "../services/lifecycle-invocations.js";
3
8
  import { createContext, createRouterContext } from "../runtime/context.js";
9
+ import { migrateStoreWriter } from "../storage/writer-migration.js";
4
10
  import { helpForArgs } from "./help.js";
5
11
  import { AppError, isAppError } from "../shared/errors.js";
12
+ import { entityReferenceVariants } from "../shared/entity-references.js";
6
13
  import { writeJson } from "../shared/json.js";
7
- import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
8
- import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
9
- import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlanItem } from "../services/plans.js";
10
- import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
14
+ import { archiveProject, prepareProjectArchive, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
15
+ import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, prepareProtocolRegistration, requireProtocol, syncProtocolFromRun, prepareProtocolTransition, transitionProtocol } from "../services/protocols.js";
16
+ import { getPlanStatus, preparePlanItemMutation, updatePlanItem } from "../services/plans.js";
17
+ import { cancelMergeQueueJob, prepareMergeQueueCancellation, prepareMergeQueueWait, prepareMergeClaim, prepareMergeBundleClaim, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, prepareMergeBundleCompletion, completeMergeJob, prepareMergeJobCompletion, failMergeBundle, prepareMergeBundleFailure, failMergeJob, prepareMergeJobFailure, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, prepareMergeJobNote, waitNextMergeJob } from "../services/merge-queue.js";
11
18
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
12
- 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";
14
- import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
15
- import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
16
- import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
17
- import { getCanonStatus, registerCanonRoot, resolveCanonRoot } from "../services/canon.js";
19
+ import { cleanupApply, cleanupScan, prepareCleanupApply } from "../services/cleanup.js";
20
+ import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeHook, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, prepareCodexHome, prepareCodexHooks, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, prepareCodexHomeRemoval, removeCodexHooks } from "../services/hooks.js";
21
+ import { bootstrapWorktree, closeWorktree, createWorktreeRecord, prepareWorktreeCreate, prepareWorktreeBootstrap, prepareWorktreeClose, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
22
+ import { acquireLaneLock, cancelLaneWaiter, prepareLaneWaiterCancellation, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, prepareLaneWorkspace, prepareLaneLock, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
23
+ import { getProjectConfigStatus, prepareProjectConfigValue, setProjectConfigValue } from "../services/config.js";
24
+ import { getCanonStatus, registerCanonRoot, resolveCanonRoot, prepareCanonRegistration } from "../services/canon.js";
18
25
  import { getRuntimeStatus } from "../services/status.js";
19
26
  import { getCliVersionReport } from "../services/build-info.js";
20
27
  import { parseCommandArgs } from "../services/lifecycle-command.js";
21
- import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
28
+ import { prepareError, prepareJsonFile, prepareJsonSchema, prepareJsonText, prepareTextFile } from "./input-preparation.js";
29
+ import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, prepareDashboardRefresh, prepareDashboardRender, prepareGlobalDashboardRender, prepareDashboardOptions, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
22
30
  import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
23
- import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
31
+ import { getFlowSessionStatus, prepareFlowSessionPayload, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
24
32
  import { getProtocolBranchContext } from "../services/branch-context.js";
25
- import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunSessions, getFlowRunTimeline, getStoredFlowRunUsage, getFlowRunFlagsStatus, getFlowRunStatus, getFlowRunConfig, getFlowRunVariables, listFlowRuns, reviseFlowRunFlags, setFlowRunConfig, setFlowRunVariable, startFlowRun } from "../services/runs.js";
33
+ import { attachFlowRunStage, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion, completeFlowRun, prepareFlowRunCompletion, completeFlowRunStage, getFlowRunSessions, getFlowRunTimeline, getStoredFlowRunUsage, getFlowRunFlagsStatus, getFlowRunStatus, getFlowRunConfig, getFlowRunVariables, listFlowRuns, reviseFlowRunFlags, setFlowRunConfig, prepareFlowRunConfig, prepareFlowRunVariable, setFlowRunVariable, startFlowRun, prepareFlowRunStart } from "../services/runs.js";
26
34
  import { previewNextEntityId } from "../services/ids.js";
27
35
  import { validateSchema } from "../services/schema-validation.js";
28
36
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
29
- import { ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
30
- import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
37
+ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
38
+ import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, prepareMigrationApply, verifyMigrationReport } from "../services/migrations.js";
31
39
  import { publishProjectSummary } from "../services/project-summary.js";
32
40
  import { requireProjectByRoot } from "../services/projects.js";
33
41
  import { resolveProjectRoot } from "../storage/paths.js";
34
- import { doctorEngines, bindRunEngine, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
42
+ import { doctorEngines, bindRunEngine, prepareRunEngineBind, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
35
43
  import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
36
- import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify } from "../services/vnext-specify.js";
37
- import { finishVnextProtocolize, isVnextProtocolizeRun, startVnextProtocolize } from "../services/vnext-protocolize.js";
38
- import { finishVnextPlan, isVnextPlanRun, startVnextPlan } from "../services/vnext-plan.js";
39
- import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
40
- import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services/vnext-code.js";
41
- import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
42
- import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
44
+ import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify, prepareVnextSpecifyStart } from "../services/vnext-specify.js";
45
+ import { finishVnextProtocolize, isVnextProtocolizeRun, prepareProtocolizeResultPath, prepareVnextProtocolizeStart, startVnextProtocolize } from "../services/vnext-protocolize.js";
46
+ import { finishVnextPlan, isVnextPlanRun, startVnextPlan, prepareVnextPlanStart, prepareVnextPlanFinish } from "../services/vnext-plan.js";
47
+ import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, prepareVnextPlanReviewStart, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
48
+ import { addVnextCodeRepair, finishVnextCode, prepareVnextCodeRepair, prepareVnextCodeStart, startVnextCode } from "../services/vnext-code.js";
49
+ import { finishVnextCodeReview, isVnextCodeReviewRun, prepareCodeReviewDecision, prepareVnextCodeReviewRepair, prepareVnextCodeReviewStart, startVnextCodeReview } from "../services/vnext-code-review.js";
50
+ import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, prepareVnextMergeFinish, prepareVnextMergeStart, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
43
51
  import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
44
- import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
45
- import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
46
- import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
52
+ import { getVnextFanoutStatus, observeVnextNativeChildren, prepareNativeObservations, reconcileVnextFanout } from "../services/vnext-fanout.js";
53
+ import { pauseStageForUser, resumeStageAfterUser, prepareStagePause, prepareStageResume } from "../services/stage-pause.js";
54
+ import { blockStageForRuntime, unblockStageAfterRuntimeRepair, prepareStageRuntimeBlock, prepareStageRuntimeUnblock } from "../services/stage-blocker.js";
47
55
  import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
48
- import { renderWorkerPrompt } from "../services/prompts.js";
49
- 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";
51
- import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
52
- import { loadExternalStageContext } from "../services/stage-context.js";
53
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
56
+ import { prepareWorkerPrompt, renderWorkerPrompt } from "../services/prompts.js";
57
+ import { finishStage, prepareStageFinish, startStage, prepareLegacyStageStart } from "../services/stage-lifecycle.js";
58
+ import { addWorkBatch, prepareWorkBatch, prepareWorkDeps, prepareWorkDelete, prepareWorkSettlement, prepareWorkRetry, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, prepareWorkResult, retryWork, showWork, startWork } from "../services/work-registry.js";
59
+ import { launchExternalWork } from "../services/external-work-launch.js";
60
+ import { createEvalBootstrapSnapshot, prepareEvalBootstrapSnapshot, createEvalRunSnapshot, prepareEvalRunSnapshotCreation, prepareVnextSpecifyRun, prepareEvalRunRestore, prepareEvalBootstrapRestore, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
61
+ import { forkRunFromSnapshot, prepareRunFork } from "../services/run-fork.js";
62
+ import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
63
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
64
+ import { loadExternalStageContext, prepareExternalStageContext } from "../services/stage-context.js";
65
+ import { vnextStageDirectory } from "../domain/stage-catalog.js";
66
+ import { prepareCommandRoute } from "./command-inputs.js";
67
+ import { parseLifecycleCommand } from "../services/lifecycle-command.js";
68
+ import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
69
+ import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, prepareRuntimeScopeControl, prepareRuntimeScopeFence, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
70
+ import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
71
+ import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
72
+ import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
73
+ import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
74
+ import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
75
+ import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
76
+ import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
77
+ import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
54
78
  const defaultIo = {
55
79
  stdout: process.stdout,
56
80
  stderr: process.stderr,
@@ -58,9 +82,20 @@ const defaultIo = {
58
82
  };
59
83
  const flagValue = "__dd_flow_flag__";
60
84
  export async function runCli(args, io = defaultIo, env = process.env) {
61
- const output = parseOutputOptions(args);
85
+ let output;
86
+ try {
87
+ output = parseOutputOptions(args);
88
+ }
89
+ catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ writeJson(io.stderr, { ok: false, error: { code: "usage", message } });
92
+ return 2;
93
+ }
62
94
  const progress = progressForCommand(output.args);
63
95
  let heartbeat;
96
+ let invocationSettlement;
97
+ let settleHook;
98
+ let diagnosticHome;
64
99
  try {
65
100
  if (output.args.length === 1 && output.args[0] === "--version") {
66
101
  const payload = getCliVersionReport();
@@ -75,6 +110,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
75
110
  io.stdout.write(`${help}\n`);
76
111
  return 0;
77
112
  }
113
+ const suppliedInvocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
114
+ if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
115
+ const controlArgs = parseArgs(output.args.slice(2));
116
+ requiredOption(controlArgs, "run");
117
+ requiredOption(controlArgs, "project-root");
118
+ }
119
+ if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
120
+ && hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
121
+ 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);
122
+ }
78
123
  const classification = classifyCliOperation(output.args, env);
79
124
  if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
80
125
  throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
@@ -83,25 +128,101 @@ export async function runCli(args, io = defaultIo, env = process.env) {
83
128
  });
84
129
  }
85
130
  const routerContext = createRouterContext(env);
131
+ const publicCommand = quote(["dd-flow", ...output.args]);
132
+ const lifecycleCommand = parseLifecycleCommand(publicCommand);
133
+ const observedInvocation = observedLifecycleInvocation(routerContext, publicCommand);
134
+ if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
135
+ throw new AppError("invocation_receipt_missing", "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
136
+ }
137
+ const invocationId = observedInvocation?.id ?? suppliedInvocationId;
138
+ const boundInvocationArgs = invocationId && invocationId !== suppliedInvocationId
139
+ ? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
140
+ : output.args;
141
+ const observedOperation = lifecycleCommand.kind === "standalone" ? lifecycleCommand.invocation.operation : null;
142
+ if (observedInvocation && !observedOperation)
143
+ throw new AppError("invocation_command_invalid", "Observed lifecycle command could not be parsed", 1);
144
+ const invocationScope = invocationId ? lifecycleInvocationScope({ ...routerContext, env }, invocationId) : undefined;
145
+ const invocationArgs = invocationScope && observedOperation
146
+ ? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
147
+ : boundInvocationArgs;
86
148
  emitHumanProgress(io, output, progress, "start");
87
149
  if (output.progressJsonl) {
88
150
  writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
89
151
  heartbeat = setInterval(() => writeProgressJsonl(io, "running", "Command is still running"), 15_000);
90
152
  }
91
- const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
92
- const routedArgs = output.responseFile ? [...output.args, "--response-file", output.responseFile] : output.args;
153
+ // Route by retained authority, so a mistyped project path reaches the CLI
154
+ // validator instead of selecting a different engine or failing preflight.
155
+ const scopeProjectRoot = invocationScope?.projectRoot ?? resolveProjectRootBeforeRouting(output.args, env);
156
+ const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
93
157
  const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
94
158
  if (routed?.routed && !routed.inProcess) {
95
159
  if (heartbeat)
96
160
  clearInterval(heartbeat);
97
- emitHumanProgress(io, output, progress, "done");
161
+ emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
162
+ if (output.progressJsonl && routed.exitCode !== 0)
163
+ writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
98
164
  return routed.exitCode;
99
165
  }
166
+ let preparedInput;
167
+ let preparedRoute;
168
+ if (!invocationId) {
169
+ const preparationContext = createContext({ ...env }, "read_existing");
170
+ try {
171
+ preparedInput = await prepareCliInput(invocationArgs, preparationContext, io, scopeProjectRoot, output.responseFile);
172
+ }
173
+ finally {
174
+ preparationContext.db.close?.();
175
+ }
176
+ }
100
177
  const dispatchContext = routed?.inProcess
101
- ? 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));
103
- preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
104
- const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot, classification, output);
178
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification))
179
+ : createContext({ ...env }, databaseModeForCommand(invocationArgs, classification));
180
+ diagnosticHome = dispatchContext.ddFlowHome;
181
+ dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
182
+ let dispatchArgs = invocationArgs;
183
+ let retainedOutcome;
184
+ // Issued transports may deliver their receipt asynchronously. Observe only
185
+ // after admission; retained outcomes must not need a fresh hook receipt.
186
+ if (invocationId) {
187
+ const command = quote(["dd-flow", ...invocationArgs]);
188
+ const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
189
+ validateArgs: () => { preparedRoute = prepareCommandRoute(invocationArgs); },
190
+ prepare: async () => { preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile, preparedRoute); },
191
+ assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
192
+ if (admission.replay) {
193
+ retainedOutcome = admission.outcome;
194
+ settleHook = undefined;
195
+ delete dispatchContext.observeLifecycleHook;
196
+ }
197
+ else {
198
+ invocationSettlement = { context: dispatchContext, id: invocationId };
199
+ dispatchArgs = [...invocationArgs, "--hook-event-id", admission.eventKey];
200
+ preparedInput?.route?.parsed.options.set("hook-event-id", [admission.eventKey]);
201
+ dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
202
+ dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
203
+ }
204
+ }
205
+ if (retainedOutcome?.error) {
206
+ const error = retainedOutcome.error;
207
+ throw new AppError(error.code, error.message, error.exitCode, error.details);
208
+ }
209
+ if (!retainedOutcome && !preparedInput)
210
+ preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile);
211
+ if (!retainedOutcome)
212
+ preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
213
+ if (!retainedOutcome)
214
+ settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
215
+ const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output, preparedInput);
216
+ const completedHook = settleHook;
217
+ settleHook = undefined;
218
+ completedHook?.({ result });
219
+ if (invocationSettlement) {
220
+ const completed = invocationSettlement;
221
+ invocationSettlement = undefined;
222
+ // A failure to persist an already completed result is an unknown reply,
223
+ // not a failed operation eligible for a new productive retry.
224
+ settleLifecycleInvocation(completed.context, completed.id, { result });
225
+ }
105
226
  const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
106
227
  const resultWithWarnings = addWarnings(result, refreshWarnings);
107
228
  if (heartbeat)
@@ -117,17 +238,59 @@ export async function runCli(args, io = defaultIo, env = process.env) {
117
238
  io.stdout.write(renderHumanResult(output.args, payload));
118
239
  return completedCommandExitCode(payload);
119
240
  }
120
- catch (error) {
241
+ catch (caught) {
242
+ 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 } });
243
+ if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
244
+ try {
245
+ const directory = path.join(diagnosticHome, "diagnostics", "cli");
246
+ fs.mkdirSync(directory, { recursive: true });
247
+ const artifact = path.join(directory, `${randomUUID()}.json`);
248
+ 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 });
249
+ error.details.diagnostic_artifact = artifact;
250
+ }
251
+ catch (secondary) {
252
+ io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
253
+ }
254
+ }
121
255
  if (heartbeat)
122
256
  clearInterval(heartbeat);
257
+ if (invocationSettlement && error.details.effect !== "committed") {
258
+ try {
259
+ if (isAppError(error))
260
+ settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
261
+ else
262
+ settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
263
+ }
264
+ catch (persistenceError) {
265
+ io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
266
+ }
267
+ }
268
+ if (settleHook) {
269
+ try {
270
+ settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
271
+ }
272
+ catch (persistence) {
273
+ io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
274
+ }
275
+ }
123
276
  const payload = isAppError(error)
124
277
  ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
125
278
  : { ok: false, error: { code: "unexpected", message: String(error) } };
279
+ progress.splice(0, progress.length, ...progress.filter(event => event.phase !== "done"));
280
+ if (progress.length)
281
+ progress.push({ phase: "failed", message: payload.error.message, at: new Date().toISOString() });
126
282
  emitHumanProgress(io, output, progress, "failed");
127
283
  if (output.progressJsonl)
128
284
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
129
285
  const errorPayload = attachProgress(payload, progress, output);
130
- const visible = writeResponseFile(errorPayload, output);
286
+ let visible = errorPayload;
287
+ try {
288
+ if (error.details.parameter !== "response-file")
289
+ visible = writeResponseFile(errorPayload, output);
290
+ }
291
+ catch (publication) {
292
+ error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
293
+ }
131
294
  if (output.json && output.progressJsonl)
132
295
  writeJson(io.stdout, errorPayload);
133
296
  else if (output.json)
@@ -151,7 +314,22 @@ function resolveProjectRootBeforeRouting(args, env) {
151
314
  }
152
315
  function databaseModeForCommand(args, classification) {
153
316
  const [family, command] = args;
317
+ // Managed attempts are issued only from a prepared store. Opening that
318
+ // existing writer must not run migrations before their input is admitted.
319
+ if (parseArgs(args.slice(2)).options.has("invocation-id"))
320
+ return "hook";
321
+ // Native hook ingress must remain synchronous, but it must not run schema
322
+ // migration/VACUUM while the provider is waiting for its decision. Startup
323
+ // prepares the store; the hook mode only verifies the prepared contract and
324
+ // records the receipt.
325
+ if (family === "codex" && command === "hook" && args[2] === "handle") {
326
+ // Only the participating PreToolUse ingress needs the prepared hot-path
327
+ // contract. SessionStart/Stop/PostToolUse are observational no-ops here
328
+ // and retain the ordinary startup path for backwards compatibility.
329
+ return optionalOption(parseArgs(args.slice(3)), "event") === "PreToolUse" ? "hook" : "initialize";
330
+ }
154
331
  if (family === "engine" ||
332
+ family === "storage" && command === "migrate-writer" ||
155
333
  family === "version" ||
156
334
  family === "schema" ||
157
335
  family === "canon" && command === "resolve" ||
@@ -207,8 +385,10 @@ function parseOutputOptions(args) {
207
385
  continue;
208
386
  }
209
387
  if (arg === "--response-file") {
388
+ if (responseFile !== null)
389
+ throw new AppError("usage", "--response-file may be supplied only once", 2);
210
390
  const file = args[++index];
211
- if (!file)
391
+ if (!file?.trim() || file.startsWith("--"))
212
392
  throw new AppError("usage", "--response-file requires a path", 2);
213
393
  responseFile = file;
214
394
  continue;
@@ -220,6 +400,33 @@ function parseOutputOptions(args) {
220
400
  const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
221
401
  return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
222
402
  }
403
+ /** Validate the response destination without creating it. The response is an
404
+ * optional publication, but a known-bad destination should never be found
405
+ * only after a successful lifecycle mutation. */
406
+ function prepareResponseDestination(value) {
407
+ const file = path.resolve(value);
408
+ const parent = path.dirname(file);
409
+ try {
410
+ let ancestor = parent;
411
+ for (;;) {
412
+ try {
413
+ if (!fs.statSync(ancestor).isDirectory())
414
+ throw new AppError("response_file_parent_invalid", "--response-file parent must be a directory", 2, { file, parent });
415
+ break;
416
+ }
417
+ catch (error) {
418
+ if (error.code !== "ENOENT" || path.dirname(ancestor) === ancestor)
419
+ throw error;
420
+ ancestor = path.dirname(ancestor);
421
+ }
422
+ }
423
+ if (fs.existsSync(file) && !fs.statSync(file).isFile())
424
+ throw new AppError("response_file_invalid", "--response-file must name a regular file", 2, { file });
425
+ }
426
+ catch (error) {
427
+ return prepareError(error, { parameter: "response-file", file, parent });
428
+ }
429
+ }
223
430
  /** Persist the complete machine response for harnesses that cap tool output.
224
431
  * The visible response keeps routing fields but omits the potentially large
225
432
  * rendered prompt; an agent can safely read the named file without rerunning
@@ -500,27 +707,636 @@ function stringValue(value) {
500
707
  function shortSha(value) {
501
708
  return value ? value.slice(0, 12) : "unknown";
502
709
  }
503
- async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
504
- const [family, command, ...rest] = args;
505
- const parsed = parseArgs(rest);
710
+ /** All caller supplied file/stdin data is consumed here, before a managed
711
+ * invocation is allowed to claim execution or a stage claims its hook event. */
712
+ async function prepareCliInput(...args) {
713
+ try {
714
+ return await prepareCliInputValue(...args);
715
+ }
716
+ catch (error) {
717
+ return prepareError(error);
718
+ }
719
+ }
720
+ async function prepareCliInputValue(args, context, io, scopeProjectRoot, responseFile = null, route = prepareCommandRoute(args)) {
721
+ const { family, command, parsed } = route;
722
+ const prepared = { route };
723
+ if (route.key === "project register")
724
+ resolveProjectRoot(requiredOption(parsed, "root"));
725
+ if (route.key === "project archive")
726
+ prepareProjectArchive(context, { idOrAlias: parsed.positional[0], root: optionalOption(parsed, "root"), reason: requiredOption(parsed, "reason") });
727
+ if (route.key === "protocol register")
728
+ prepareProtocolRegistration(context, { handshakeId: requiredPosition(parsed, 0, "handshake-id"), projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "workspace-path") });
729
+ if (route.key === "canon register")
730
+ prepared.canonRegistration = prepareCanonRegistration(context, { root: requiredOption(parsed, "root") });
731
+ if (route.key === "run attach-stage")
732
+ prepareFlowRunStageAttachment(context, stageAttachmentInput(parsed));
733
+ if (route.key === "run complete-stage")
734
+ prepareFlowRunStageCompletion(context, stageCompletionInput(parsed));
735
+ if (route.key === "run override")
736
+ prepareFlowRunCompletion(context, runOverrideInput(parsed));
737
+ if (route.key === "engine bind-run")
738
+ prepared.engineBind = prepareRunEngineBind(context, engineBindInput(parsed));
739
+ if (route.key === "worktree create")
740
+ prepared.worktreeCreate = prepareWorktreeCreate(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree create"), branch: requiredOption(parsed, "branch"), base: requiredOption(parsed, "base"), path: requiredOption(parsed, "path") });
741
+ if (route.key === "worktree bootstrap")
742
+ prepared.worktreeBootstrap = prepareWorktreeBootstrap(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree bootstrap") });
743
+ if (route.key === "worktree close")
744
+ prepareWorktreeClose(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree close"), mode: requiredOption(parsed, "mode") });
745
+ if (route.key.startsWith("plan item "))
746
+ preparePlanItemMutation(context, planItemInput(parsed, requiredScopeProjectRoot(scopeProjectRoot, route.key)));
747
+ if (route.key === "lane workspace set")
748
+ prepareLaneWorkspace(context, laneWorkspaceInput(parsed));
749
+ if (route.key === "lane waiter cancel")
750
+ prepareLaneWaiterCancellation(context, laneWaiterCancelInput(parsed));
751
+ if (route.key === "merge-queue complete")
752
+ prepareMergeJobCompletion(context, { ...mergeJobTarget(parsed, scopeProjectRoot), summary: requiredOption(parsed, "summary") });
753
+ if (route.key === "merge-queue cancel")
754
+ prepareMergeQueueCancellation(context, mergeCancellationInput(parsed, scopeProjectRoot));
755
+ if (route.key === "merge-queue wait-next")
756
+ prepareMergeQueueWait(context, mergeWaitInput(parsed, scopeProjectRoot));
757
+ if (route.key === "merge-queue next" || route.key === "merge-queue claim")
758
+ prepareMergeClaim(context, { projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"), workerId: requiredWorkerId(parsed), workspacePath: workspacePathOption(parsed), ...(route.key === "merge-queue claim" ? { protocolId: requiredPosition(parsed, 0, "protocol-id") } : {}) });
759
+ if (route.key === "merge-queue note")
760
+ prepareMergeJobNote(context, { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue note"), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
761
+ if (route.key === "merge bundle complete")
762
+ prepareMergeBundleCompletion(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
763
+ if (route.key === "merge bundle claim")
764
+ prepareMergeBundleClaim(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed) });
765
+ if (route.key === "merge bundle fail")
766
+ prepareMergeBundleFailure(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
767
+ if (route.key === "merge-queue fail")
768
+ prepareMergeJobFailure(context, { ...mergeJobTarget(parsed, scopeProjectRoot), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
769
+ if (route.key.startsWith("lane lock ") && route.key !== "lane lock status") {
770
+ const action = requiredPosition(parsed, 0, "lane lock action");
771
+ prepareLaneLock(context, {
772
+ projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"),
773
+ workspacePath: workspacePathOption(parsed), leaseToken: optionalOption(parsed, "lease-token"), reason: optionalOption(parsed, "reason"),
774
+ ttlSeconds: optionalNumber(parsed, "ttl"), timeoutSeconds: optionalNumber(parsed, "timeout"), pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
775
+ }, action);
776
+ }
777
+ if (route.key === "run recovery begin")
778
+ prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "interruption-json"), "interruption");
779
+ if (route.key === "run recovery seal")
780
+ prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "settlement-json"), "settlement");
781
+ if (route.key === "run recovery capture")
782
+ prepared.recoveryCapture = prepareRecoveryCapture(context, recoveryCaptureInput(parsed));
783
+ if (route.key === "run recovery resume")
784
+ prepared.recoveryResume = prepareRunRecoveryResume(context, recoveryResumeInput(parsed));
785
+ if (route.key === "run drive answer")
786
+ prepared.controllerAnswer = prepareRunControllerAnswer(context, controllerAnswerInput(parsed));
787
+ if (route.key === "run drive launch")
788
+ prepared.controllerLaunch = prepareRunControllerLaunch(context, controllerLaunchInput(parsed));
789
+ if (route.key === "run drive context")
790
+ prepared.controllerContext = prepareRunControllerContext(context, controllerContextInput(parsed));
791
+ if (route.key === "protocol transition" || route.key === "transition")
792
+ prepared.protocolTransition = prepareProtocolTransition(context, protocolTransitionInput(parsed, scopeProjectRoot ?? resolveOperationProjectRoot(context, args)));
793
+ if (route.key === "codex home init")
794
+ prepared.codexHome = prepareCodexHome(context, codexHomeInput(parsed));
795
+ if (route.key === "codex home remove")
796
+ prepareCodexHomeRemoval(context, { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), mode: requiredOption(parsed, "mode") });
797
+ if (["codex hooks install", "codex hooks remove"].includes(route.key))
798
+ prepared.codexHooks = prepareCodexHooks(context, { projectRoot: requiredOption(parsed, "project-root"), target: optionalOption(parsed, "target"), profile: optionalOption(parsed, "profile"), yes: hasOption(parsed, "yes") });
799
+ if (route.key === "migration apply")
800
+ prepared.migrationApply = prepareMigrationApply(context, migrationApplyInput(parsed, classifyCliOperation(args, context.env)));
801
+ if (route.key === "migration verify")
802
+ prepared.migrationVerification = verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
803
+ if (responseFile)
804
+ prepareResponseDestination(responseFile);
805
+ if (family === "dashboard") {
806
+ const action = dashboardAction(command);
807
+ prepareDashboardOptions({ format: optionalOption(parsed, "format"), viewer: optionalOption(parsed, "viewer"), open: optionalOption(parsed, "open") });
808
+ const target = resolveDashboardTarget(context, { action, project: optionalOption(parsed, "project"), projectRoot: optionalOption(parsed, "project-root"), protocol: optionalOption(parsed, "protocol"), all: hasOption(parsed, "all"), global: hasOption(parsed, "global") || command?.endsWith("-global") });
809
+ if (action === "refresh" && target.kind !== "global" && optionalOption(parsed, "output") !== undefined) {
810
+ throw new AppError("validation", "--output is supported only for global dashboard refresh; use dashboard render for a custom project/protocol output", 2, { parameter: "output" });
811
+ }
812
+ if (target.kind === "protocol")
813
+ requireProtocol(context, target.protocolId, target.project.id);
814
+ if (action !== "data") {
815
+ if (target.kind === "global" || target.kind === "all_projects") {
816
+ prepareGlobalDashboardRender(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? (command?.endsWith("-global") ? undefined : "html") });
817
+ }
818
+ else {
819
+ const format = optionalOption(parsed, "format") ?? (action === "open" || hasOption(parsed, "project") ? "html" : undefined);
820
+ const input = { projectRoot: target.project.root, format, ...(target.kind === "protocol" ? { protocol: target.protocolId } : {}) };
821
+ if (action === "refresh")
822
+ prepareDashboardRefresh(context, { ...input, open: optionalOption(parsed, "open") });
823
+ else if (action === "render")
824
+ prepareDashboardRender(context, { ...input, output: optionalOption(parsed, "output") });
825
+ else if (target.kind === "protocol") {
826
+ if (format !== "html")
827
+ throw new AppError("validation", "Protocol dashboard open supports --format html", 2, { protocol: target.protocolId });
828
+ prepareDashboardRender(context, input);
829
+ }
830
+ }
831
+ }
832
+ }
833
+ if (route.key === "prompt render") {
834
+ const input = workerPromptInput(parsed);
835
+ prepared.workerPrompt = { input, value: prepareWorkerPrompt(context, input) };
836
+ }
837
+ if (route.key === "run snapshot restore" || route.key === "run snapshot bootstrap restore") {
838
+ try {
839
+ const input = snapshotRestoreInput(parsed);
840
+ if (route.key === "run snapshot restore")
841
+ prepared.snapshotRestore = prepareEvalRunRestore(context, input);
842
+ else
843
+ prepared.bootstrapRestore = prepareEvalBootstrapRestore(input);
844
+ }
845
+ catch (error) {
846
+ return prepareError(error, { parameter: "snapshot/project-root" });
847
+ }
848
+ }
849
+ if (route.key === "run snapshot bootstrap create") {
850
+ try {
851
+ prepareEvalBootstrapSnapshot(context, { projectRoot: requiredOption(parsed, "project-root"), output: requiredOption(parsed, "output") });
852
+ }
853
+ catch (error) {
854
+ return prepareError(error, { parameter: "project-root/output" });
855
+ }
856
+ }
857
+ if (route.key === "run snapshot create") {
858
+ prepareEvalRunSnapshotCreation(context, snapshotCreateInput(parsed));
859
+ }
860
+ if (route.key === "run start") {
861
+ try {
862
+ const input = runStartInput(parsed);
863
+ prepared.runStart = { input, value: prepareFlowRunStart(context, input) };
864
+ }
865
+ catch (error) {
866
+ return prepareError(error, { parameter: "run start" });
867
+ }
868
+ }
869
+ if (route.key === "run fork") {
870
+ try {
871
+ prepared.runFork = prepareRunFork(runForkInput(parsed));
872
+ }
873
+ catch (error) {
874
+ return prepareError(error, { parameter: "from/output" });
875
+ }
876
+ }
877
+ if (["runtime scope fence", "runtime scope stop"].includes(route.key))
878
+ prepareRuntimeScopeFence(requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"));
879
+ if (route.key === "runtime scope resume")
880
+ prepareRuntimeScopeResumeRequest(context, runtimeScopeResumeInput(parsed));
881
+ if (route.key === "runtime scope reconcile")
882
+ prepareRuntimeScopeReconciliation(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id"));
883
+ if (route.key === "runtime scope serve")
884
+ prepareRuntimeScopeServing(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")));
885
+ if (route.key === "runtime scope control") {
886
+ try {
887
+ const raw = optionalOption(parsed, "manifest-json");
888
+ prepared.runtimeManifest = prepareRuntimeScopeControl(requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), requiredOption(parsed, "mode"), raw === undefined ? undefined : prepareJsonText(raw, { file: "<manifest-json>", label: "scope manifest" }).value, context);
889
+ }
890
+ catch (error) {
891
+ return prepareError(error, { parameter: "scope-id/request-id/manifest-json" });
892
+ }
893
+ }
894
+ if (route.key === "runtime process register") {
895
+ prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
896
+ }
897
+ if (route.key === "runtime process heartbeat")
898
+ prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
899
+ if (route.key === "runtime process finish")
900
+ prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
901
+ if (route.key === "runtime process confirm")
902
+ prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
903
+ if (route.key === "runtime process stop")
904
+ prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
905
+ if (route.key === "runtime process reconcile")
906
+ prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
907
+ if (route.key === "runtime process release-turn") {
908
+ prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
909
+ }
910
+ if (route.key === "runtime process start") {
911
+ try {
912
+ prepareRuntimeService(context, runtimeServiceInput(parsed));
913
+ }
914
+ catch (error) {
915
+ return prepareError(error, { parameter: "runtime process start" });
916
+ }
917
+ }
918
+ if (family === "work" && command === "retry") {
919
+ try {
920
+ prepareWorkRetry(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
921
+ }
922
+ catch (error) {
923
+ return prepareError(error, { parameter: "work-id/reason" });
924
+ }
925
+ }
926
+ if (family === "work" && (command === "fail" || command === "cancel")) {
927
+ try {
928
+ prepareWorkSettlement(context, requiredPosition(parsed, 0, "work-id"), command === "fail" ? "failed" : "cancelled", requiredOption(parsed, "reason"));
929
+ }
930
+ catch (error) {
931
+ return prepareError(error, { parameter: "work-id/reason" });
932
+ }
933
+ }
934
+ if (family === "work" && (command === "deps" || command === "delete")) {
935
+ try {
936
+ if (command === "delete")
937
+ prepareWorkDelete(context, requiredPosition(parsed, 0, "work-id"));
938
+ else
939
+ prepareWorkDeps(context, { workId: requiredPosition(parsed, 1, "work-id"), action: requiredPosition(parsed, 0, "action"), on: parsed.options.get("on") ?? [] });
940
+ }
941
+ catch (error) {
942
+ return prepareError(error, { parameter: "work-id/on" });
943
+ }
944
+ }
945
+ if (route.key === "cleanup apply") {
946
+ const planFile = requiredOption(parsed, "plan-file");
947
+ try {
948
+ prepared.cleanup = prepareCleanupApply(context, { projectRoot: requiredOption(parsed, "project-root"), planFile, reason: requiredOption(parsed, "reason"), force: hasOption(parsed, "force") });
949
+ }
950
+ catch (error) {
951
+ return prepareError(error, { parameter: "plan-file", file: planFile });
952
+ }
953
+ }
954
+ if (route.key === "stage block" || route.key === "stage unblock") {
955
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
956
+ try {
957
+ if (route.key === "stage block") {
958
+ if (!hasOption(parsed, "summary-stdin"))
959
+ throw new AppError("usage", "stage block requires --summary-stdin", 2);
960
+ prepared.interactionText = await readStdin(io.stdin);
961
+ prepareStageRuntimeBlock(context, { ...input, kind: requiredOption(parsed, "kind"), code: requiredOption(parsed, "code"), summary: prepared.interactionText, retryable: hasOption(parsed, "retryable") });
962
+ }
963
+ else
964
+ prepareStageRuntimeUnblock(context, input);
965
+ }
966
+ catch (error) {
967
+ return prepareError(error, { parameter: "stage/work/summary" });
968
+ }
969
+ }
970
+ if (route.key === "zcode invocation prepare") {
971
+ const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label: "invocation issuance" }).value;
972
+ const payload = value && typeof value === "object" ? value : {};
973
+ try {
974
+ prepared.lifecycleIssuance = prepareLifecycleIssuance(payload.command, payload.scope);
975
+ }
976
+ catch (error) {
977
+ if (error instanceof AppError && ["invocation_command_invalid", "invocation_scope_invalid"].includes(error.code))
978
+ return prepareError(new AppError("usage", error.message, 2, { cause_code: error.code }), { parameter: "stdin" });
979
+ throw error;
980
+ }
981
+ assertLifecycleInvocationCurrent(context, prepared.lifecycleIssuance.scope, prepared.lifecycleIssuance.command);
982
+ }
983
+ if (route.key === "work add-batch") {
984
+ const file = requiredOption(parsed, "file");
985
+ try {
986
+ prepared.workBatch = prepareWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file });
987
+ }
988
+ catch (error) {
989
+ return prepareError(error, { parameter: "file/parent", file });
990
+ }
991
+ }
992
+ if (route.key === "work repair add") {
993
+ if (!hasOption(parsed, "task-stdin"))
994
+ throw new AppError("usage", "work repair add requires --task-stdin", 2);
995
+ if (io.stdin?.isTTY)
996
+ throw new AppError("usage", "--task-stdin requires piped or redirected input", 2);
997
+ prepared.workRepair = prepareCliWorkRepair(context, parsed, await readStdin(io.stdin), scopeProjectRoot);
998
+ }
999
+ if (route.key === "run config set" || route.key === "run vars set") {
1000
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), key: requiredOption(parsed, "key"), value: requiredOption(parsed, "value") };
1001
+ try {
1002
+ if (route.key === "run config set")
1003
+ prepareFlowRunConfig(context, { ...input, reason: requiredOption(parsed, "reason") });
1004
+ else
1005
+ prepareFlowRunVariable(context, input);
1006
+ }
1007
+ catch (error) {
1008
+ return prepareError(error, { parameter: "run config/vars" });
1009
+ }
1010
+ }
1011
+ if (route.key === "schema validate") {
1012
+ try {
1013
+ prepared.schemaValidation = dispatchSchema(context, command, parsed);
1014
+ }
1015
+ catch (error) {
1016
+ return prepareError(error, { parameter: "file", file: requiredOption(parsed, "file") });
1017
+ }
1018
+ }
1019
+ if (route.key === "project config set") {
1020
+ try {
1021
+ prepareProjectConfigValue(requiredOption(parsed, "key"), requiredOption(parsed, "value"));
1022
+ }
1023
+ catch (error) {
1024
+ return prepareError(error, { parameter: "key/value" });
1025
+ }
1026
+ }
1027
+ if (family === "session" && command === "register") {
1028
+ try {
1029
+ prepared.sessionPayload = prepareFlowSessionPayload({ payloadBase64: optionalOption(parsed, "payload-base64"), payloadFile: optionalOption(parsed, "payload-file"), payloadJson: optionalOption(parsed, "payload-json") });
1030
+ }
1031
+ catch (error) {
1032
+ return prepareError(error, { parameter: "session payload" });
1033
+ }
1034
+ }
1035
+ const run = (projectRoot, runId) => {
1036
+ try {
1037
+ const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
1038
+ const row = context.db.get("SELECT run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
1039
+ if (!row)
1040
+ throw new AppError("not_found", "RUN is not registered", 2, { run_id: runId });
1041
+ if (!row.run_root)
1042
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId });
1043
+ return { runRoot: row.run_root, workspaceRoot: row.workspace_root, indexJson: row.index_json };
1044
+ }
1045
+ catch (error) {
1046
+ return prepareError(error, { parameter: "run-id", run_id: runId });
1047
+ }
1048
+ };
1049
+ const json = (file, stdin, label, schema) => {
1050
+ if (stdin) {
1051
+ // The caller awaits this helper; the synchronous facade below is only
1052
+ // used for file inputs. Keep stdin handling in the stage branch.
1053
+ throw new AppError("usage", `${label} stdin preparation must be awaited`, 2);
1054
+ }
1055
+ if (!file)
1056
+ throw new AppError("usage", `${label} input is required`, 2);
1057
+ return prepareJsonSchema(file, label, schema);
1058
+ };
1059
+ if (family === "stage" && command === "start") {
1060
+ const stage = requiredOption(parsed, "stage");
1061
+ if (stage !== "specify" && ["bootstrap", "intake-file", "intake-stdin"].some(option => hasOption(parsed, option)))
1062
+ throw new AppError("usage", "--bootstrap and intake inputs are only supported for SPECIFY", 2);
1063
+ requiredOption(parsed, "project-root");
1064
+ if (!hasOption(parsed, "bootstrap"))
1065
+ requiredPosition(parsed, 0, "run-id");
1066
+ if (hasOption(parsed, "session-id"))
1067
+ throw new AppError("usage", "--session-id is managed by the harness lifecycle adapter and is not accepted", 2);
1068
+ const contextFile = optionalOption(parsed, "context-file"), contextSha256 = optionalOption(parsed, "context-sha256");
1069
+ if (Boolean(contextFile) !== Boolean(contextSha256))
1070
+ throw new AppError("usage", "stage start requires --context-file together with --context-sha256", 2);
1071
+ if ([hasOption(parsed, "intake-file"), hasOption(parsed, "intake-stdin"), Boolean(contextFile)].filter(Boolean).length > 1)
1072
+ throw new AppError("usage", "Use exactly one of --intake-file, --intake-stdin or --context-file", 2);
1073
+ if (contextFile && contextSha256) {
1074
+ try {
1075
+ prepared.stageStart = { externalContext: loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) };
1076
+ }
1077
+ catch (error) {
1078
+ return prepareError(error, { file: contextFile, parameter: "context-file" });
1079
+ }
1080
+ if (!hasOption(parsed, "bootstrap")) {
1081
+ const { runRoot } = run(requiredOption(parsed, "project-root"), requiredPosition(parsed, 0, "run-id"));
1082
+ prepareExternalStageContext({ stageRoot: path.join(runRoot, vnextStageDirectory(stage)), loaded: prepared.stageStart.externalContext });
1083
+ }
1084
+ }
1085
+ if (stage === "specify") {
1086
+ const intakeFile = optionalOption(parsed, "intake-file");
1087
+ if (intakeFile)
1088
+ prepared.stageStart = { intakeMarkdown: prepareTextFile(intakeFile, "SPECIFY intake").text };
1089
+ else if (hasOption(parsed, "intake-stdin"))
1090
+ prepared.stageStart = { intakeMarkdown: await readStdin(io.stdin) };
1091
+ else if (prepared.stageStart?.externalContext)
1092
+ prepared.stageStart.intakeMarkdown = runnerIntake(prepared.stageStart.externalContext);
1093
+ const projectRoot = requiredOption(parsed, "project-root");
1094
+ const bootstrap = hasOption(parsed, "bootstrap");
1095
+ if (isVnextSpecifyFlow(projectRoot) && (bootstrap || isVnextSpecifyRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") }))) {
1096
+ prepared.specifyStart = prepareVnextSpecifyStart(context, { projectRoot, ...(prepared.stageStart?.intakeMarkdown !== undefined ? { intakeMarkdown: prepared.stageStart.intakeMarkdown } : {}), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}), slug: optionalOption(parsed, "subject"), ...(!bootstrap ? { runId: requiredPosition(parsed, 0, "run-id") } : {}) });
1097
+ }
1098
+ else if (bootstrap) {
1099
+ if (hasOption(parsed, "intake-stdin") || contextFile)
1100
+ throw new AppError("usage", "Legacy SPECIFY bootstrap supports --intake-file, not stdin or runner context", 2);
1101
+ prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot, stage, bootstrap: { subject: requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) }, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) }, prepared.stageStart?.intakeMarkdown);
1102
+ }
1103
+ }
1104
+ if (stage === "merge" && isVnextMergeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1105
+ prepared.mergeStart = prepareVnextMergeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1106
+ }
1107
+ if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1108
+ prepared.protocolizeStart = prepareVnextProtocolizeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1109
+ }
1110
+ if (stage === "plan" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1111
+ prepared.planStart = prepareVnextPlanStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1112
+ }
1113
+ if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1114
+ prepared.planReviewStart = prepareVnextPlanReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1115
+ }
1116
+ if (stage === "code" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1117
+ prepared.codeStart = prepareVnextCodeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1118
+ }
1119
+ if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1120
+ prepared.codeReviewStart = prepareVnextCodeReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1121
+ }
1122
+ if (![prepared.specifyStart, prepared.protocolizeStart, prepared.planStart, prepared.planReviewStart, prepared.codeStart, prepared.codeReviewStart, prepared.mergeStart, prepared.legacyStageStart].some(Boolean)) {
1123
+ if (prepared.stageStart?.externalContext)
1124
+ throw new AppError("usage", "--context-file is currently supported only by vNext flow stages", 2);
1125
+ prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) });
1126
+ }
1127
+ }
1128
+ if (family === "stage" && (command === "pause" || command === "resume")) {
1129
+ requiredPosition(parsed, 0, "run-id");
1130
+ requiredOption(parsed, "project-root");
1131
+ requiredOption(parsed, "stage");
1132
+ requiredOption(parsed, "work");
1133
+ const parameter = command === "pause" ? "question-stdin" : "answer-stdin";
1134
+ if (!hasOption(parsed, parameter))
1135
+ throw new AppError("usage", `stage ${command} requires --${parameter}`, 2);
1136
+ if (io.stdin?.isTTY)
1137
+ throw new AppError("usage", `--${parameter} requires piped or redirected input`, 2);
1138
+ prepared.interactionText = await readStdin(io.stdin);
1139
+ if (!prepared.interactionText.trim())
1140
+ return prepareError(new AppError("validation", `stage ${command} requires non-empty --${parameter}`, 2), { parameter });
1141
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
1142
+ try {
1143
+ if (command === "pause")
1144
+ prepareStagePause(context, { ...input, question: prepared.interactionText });
1145
+ else
1146
+ prepared.resumeEvidence = prepareStageResume(context, { ...input, answer: prepared.interactionText }).evidence;
1147
+ }
1148
+ catch (error) {
1149
+ return prepareError(error, { parameter: "stage/work" });
1150
+ }
1151
+ }
1152
+ if (family === "work" && command === "finish") {
1153
+ const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = optionalOption(parsed, "reason");
1154
+ if (Boolean(retryReceiptId) !== Boolean(retryReason))
1155
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
1156
+ const resultFile = optionalOption(parsed, "result-file");
1157
+ const resultStdin = hasOption(parsed, "result-stdin");
1158
+ if (Boolean(resultFile) === resultStdin)
1159
+ throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
1160
+ prepared.workFinish = { result: resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin) };
1161
+ prepareWorkResult(context, requiredPosition(parsed, 0, "work-id"), prepared.workFinish.result);
1162
+ }
1163
+ if (family === "stage" && command === "fanout" && requiredPosition(parsed, 0, "fanout action") === "reconcile") {
1164
+ prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
1165
+ }
1166
+ if (family === "stage" && command === "native" && requiredPosition(parsed, 0, "native action") === "observe") {
1167
+ prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
1168
+ }
1169
+ if (family !== "stage" || command !== "finish")
1170
+ return prepared;
1171
+ const projectRoot = requiredOption(parsed, "project-root");
1172
+ const runId = requiredPosition(parsed, 0, "run-id");
1173
+ const stage = requiredOption(parsed, "stage");
1174
+ if (hasOption(parsed, "status"))
1175
+ throw new AppError("usage", "Stage status belongs in the semantic input file", 2);
1176
+ if (["result-file", "semantic-file", "data", "result-stdin"].filter(key => hasOption(parsed, key)).length > 1)
1177
+ throw new AppError("usage", "Use exactly one result input: --result-file, --semantic-file, --data or --result-stdin", 2);
1178
+ if (Boolean(optionalOption(parsed, "retry-check")) !== Boolean(optionalOption(parsed, "reason")))
1179
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
1180
+ const resultStdin = hasOption(parsed, "result-stdin");
1181
+ const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
1182
+ const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
1183
+ const runtime = run(projectRoot, runId);
1184
+ const terminalReplay = (name) => {
1185
+ let index;
1186
+ try {
1187
+ index = JSON.parse(runtime.indexJson);
1188
+ }
1189
+ catch (error) {
1190
+ throw new AppError("runtime_artifact_invalid", "RUN index is not valid retained JSON", 1, { run_id: runId, cause: error instanceof Error ? error.message : String(error) });
1191
+ }
1192
+ return index.stage_runs?.some(entry => entry.stage === name && entry.status === "done") === true
1193
+ && fs.existsSync(path.join(runtime.runRoot, vnextStageDirectory(name), "stage-report.json"));
1194
+ };
1195
+ const acceptStageInputs = (allowed) => {
1196
+ const options = allowed.split(" ");
1197
+ for (const name of ["dir", "semantic-file", "data", "result-file", "result-stdin", "outcome", "decision-file", "verification-file", "retry-check", "reason", "request", "work"]) {
1198
+ if (hasOption(parsed, name) && !options.includes(name))
1199
+ throw new AppError("usage", `stage finish ${stage} does not accept --${name} for this flow`, 2, { parameter: name });
1200
+ }
1201
+ };
1202
+ const fromStdin = async (label, schema) => {
1203
+ const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label });
1204
+ try {
1205
+ validateSchema({ ...schema, file: value.file, data: value.value });
1206
+ }
1207
+ catch (error) {
1208
+ return prepareError(error, { file: value.file, label });
1209
+ }
1210
+ return value;
1211
+ };
1212
+ if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
1213
+ acceptStageInputs("result-file semantic-file data result-stdin outcome");
1214
+ const outcome = optionalOption(parsed, "outcome");
1215
+ if (!outcome || !["specified", "failed", "cancelled"].includes(outcome))
1216
+ throw new AppError("usage", "vNext SPECIFY requires --outcome specified|failed|cancelled; use stage pause for a user question", 2);
1217
+ if (Boolean(resultFile) === resultStdin)
1218
+ throw new AppError("usage", "vNext SPECIFY finish requires exactly one result input", 2);
1219
+ const schema = { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot };
1220
+ const input = resultStdin ? await fromStdin("SPECIFY result", schema) : json(resultFile, false, "SPECIFY result", schema);
1221
+ prepared.stageFinish = { value: input.value, text: input.text };
1222
+ }
1223
+ else if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
1224
+ acceptStageInputs("result-file semantic-file data");
1225
+ if (!resultFile || resultStdin || hasOption(parsed, "outcome"))
1226
+ throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
1227
+ let absolute;
1228
+ try {
1229
+ absolute = prepareProtocolizeResultPath(runtime.runRoot, resultFile);
1230
+ }
1231
+ catch (error) {
1232
+ return prepareError(error, { file: resultFile });
1233
+ }
1234
+ const input = json(absolute, false, "PROTOCOLIZE result", { schemaName: "vnext-protocolize-result", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1235
+ prepared.stageFinish = { value: input.value, text: input.text };
1236
+ }
1237
+ else if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
1238
+ acceptStageInputs("");
1239
+ if (hasOption(parsed, "code-work-batch-file"))
1240
+ throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
1241
+ try {
1242
+ prepared.planFinish = prepareVnextPlanFinish(context, { projectRoot, runId });
1243
+ }
1244
+ catch (error) {
1245
+ return prepareError(error, { parameter: "PLAN artifacts" });
1246
+ }
1247
+ }
1248
+ else if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
1249
+ acceptStageInputs("request work retry-check reason");
1250
+ if (resultStdin || resultFile || hasOption(parsed, "outcome"))
1251
+ throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
1252
+ requiredOption(parsed, "request");
1253
+ requiredOption(parsed, "work");
1254
+ try {
1255
+ prepared.mergeFinish = prepareVnextMergeFinish(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(optionalOption(parsed, "retry-check") ? { retryCheckId: optionalOption(parsed, "retry-check"), retryReason: optionalOption(parsed, "reason") } : {}) });
1256
+ }
1257
+ catch (error) {
1258
+ return prepareError(error, { parameter: "MERGE finish" });
1259
+ }
1260
+ }
1261
+ else if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
1262
+ acceptStageInputs("decision-file");
1263
+ if (resultStdin || hasOption(parsed, "outcome"))
1264
+ throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
1265
+ const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "04-plan-review", "decision.json");
1266
+ const input = json(decision, false, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1267
+ prepared.stageFinish = { value: input.value, text: input.text };
1268
+ }
1269
+ else if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
1270
+ acceptStageInputs("verification-file retry-check reason");
1271
+ if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome"))
1272
+ throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
1273
+ const verification = optionalOption(parsed, "verification-file");
1274
+ if (!verification)
1275
+ throw new AppError("usage", "vNext CODE finish requires --verification-file", 2);
1276
+ if (!terminalReplay(stage)) {
1277
+ const input = json(verification, false, "CODE verification", { schemaName: "code-verification", projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1278
+ prepared.stageFinish = { value: input.value, text: input.text };
1279
+ }
1280
+ }
1281
+ else if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
1282
+ acceptStageInputs("decision-file retry-check reason");
1283
+ if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
1284
+ throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
1285
+ const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "06-code-review", "decision.json");
1286
+ if (!terminalReplay(stage)) {
1287
+ const input = prepareCodeReviewDecision(runtime.runRoot, optionalOption(parsed, "decision-file"));
1288
+ try {
1289
+ validateSchema({ schemaName: "code-review-decision", file: decision, data: input.value, projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1290
+ }
1291
+ catch (error) {
1292
+ return prepareError(error, { file: decision, label: "CODE-REVIEW decision" });
1293
+ }
1294
+ prepared.stageFinish = input;
1295
+ }
1296
+ }
1297
+ else {
1298
+ acceptStageInputs("dir semantic-file data");
1299
+ if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin)
1300
+ throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
1301
+ prepared.legacyStageFinish = prepareStageFinish(context, { projectRoot, runId, stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}), ...(semanticFile ? { semanticFile } : {}) });
1302
+ }
1303
+ return prepared;
1304
+ }
1305
+ async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args), prepared = {}) {
1306
+ if (!prepared.route)
1307
+ throw new AppError("command_not_prepared", "Command dispatch requires prepared arguments", 1);
1308
+ const { family, command, parsed } = prepared.route;
1309
+ if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
1310
+ const payload = prepared.lifecycleIssuance;
1311
+ if (!payload)
1312
+ throw new AppError("command_not_prepared", "Invocation issuance requires prepared input", 1);
1313
+ assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
1314
+ return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
1315
+ }
1316
+ if (family === "storage" && command === "migrate-writer") {
1317
+ if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1318
+ throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
1319
+ if (parsed.options.get("offline")?.[0] !== flagValue)
1320
+ throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
1321
+ return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
1322
+ }
506
1323
  if (family === "engine") {
507
- return dispatchEngine(context, command, parsed, classification);
1324
+ return dispatchEngine(context, command, parsed, classification, prepared);
508
1325
  }
509
1326
  if (family === "status") {
510
- const rootParsed = parseArgs([command ?? "", ...rest]);
511
1327
  return getRuntimeStatus(context, {
512
- projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root"),
513
- checkRegistry: hasOption(rootParsed, "check-registry")
1328
+ projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
1329
+ checkRegistry: hasOption(parsed, "check-registry")
514
1330
  });
515
1331
  }
516
1332
  if (family === "version") {
517
1333
  return getCliVersionReport();
518
1334
  }
519
1335
  if (family === "runtime") {
520
- return await dispatchRuntime(context, command, parsed);
1336
+ return await dispatchRuntime(context, command, parsed, commandProgress(io, output), prepared);
521
1337
  }
522
1338
  if (family === "canon") {
523
- return dispatchCanon(context, command, parsed);
1339
+ return dispatchCanon(context, command, parsed, prepared);
524
1340
  }
525
1341
  if (family === "project" && command === "register") {
526
1342
  return registerProject(context, { root: requiredOption(parsed, "root") });
@@ -597,16 +1413,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
597
1413
  return readyForMerge(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol ready-for-merge") });
598
1414
  }
599
1415
  if (family === "protocol" && command === "transition") {
600
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
601
- const reason = optionalOption(parsed, "reason");
602
- return transitionProtocol(context, {
603
- protocolId,
604
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"),
605
- to: requiredOption(parsed, "to"),
606
- jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
607
- force: hasOption(parsed, "force"),
608
- ...(reason ? { reason } : {})
609
- });
1416
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
610
1417
  }
611
1418
  if (family === "protocol" && command === "sync-from-run") {
612
1419
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -631,17 +1438,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
631
1438
  });
632
1439
  }
633
1440
  if (family === "transition") {
634
- const transitionParsed = parseArgs([command ?? "", ...rest]);
635
- const protocolId = requiredPosition(transitionParsed, 0, "protocol-id");
636
- const reason = optionalOption(transitionParsed, "reason");
637
- return transitionProtocol(context, {
638
- protocolId,
639
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "transition"),
640
- to: requiredOption(transitionParsed, "to"),
641
- jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
642
- force: hasOption(transitionParsed, "force"),
643
- ...(reason ? { reason } : {})
644
- });
1441
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
645
1442
  }
646
1443
  if (family === "plan" && command === "status") {
647
1444
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -660,12 +1457,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
660
1457
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
661
1458
  if (action === "status")
662
1459
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
1460
+ if (action === "reconcile")
1461
+ return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: prepared.observations ?? prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value });
663
1462
  if (action === "dispatch") {
664
1463
  if (stage === "plan-review")
665
1464
  return dispatchVnextPlanReview(context, { projectRoot, runId });
666
1465
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
667
1466
  }
668
- throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
1467
+ 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);
1468
+ }
1469
+ if (family === "stage" && command === "native") {
1470
+ if (requiredPosition(parsed, 0, "native action") !== "observe")
1471
+ throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
1472
+ return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: prepared.observations ?? prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value });
669
1473
  }
670
1474
  if (family === "run" && command === "capacity") {
671
1475
  if (requiredPosition(parsed, 0, "capacity action") !== "record")
@@ -677,16 +1481,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
677
1481
  return dispatchPlanItem(context, parsed, requiredScopeProjectRoot(scopeProjectRoot, "plan item"));
678
1482
  }
679
1483
  if (family === "prompt" && command === "render") {
680
- const workspaceRoot = optionalOption(parsed, "workspace-root");
681
- const taskFile = optionalOption(parsed, "task-file");
682
- return renderWorkerPrompt(context, {
683
- projectRoot: requiredOption(parsed, "project-root"),
684
- runId: requiredOption(parsed, "run"),
685
- stage: requiredOption(parsed, "stage"),
686
- profile: requiredOption(parsed, "profile"),
687
- ...(taskFile ? { taskFile } : { planItemId: requiredOption(parsed, "plan-item") }),
688
- ...(workspaceRoot ? { workspaceRoot } : {})
689
- });
1484
+ return renderWorkerPrompt(context, prepared.workerPrompt?.input ?? workerPromptInput(parsed), prepared.workerPrompt?.value);
690
1485
  }
691
1486
  if (family === "work" && command === "status") {
692
1487
  return getVnextWorkStatus(context, {
@@ -695,7 +1490,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
695
1490
  });
696
1491
  }
697
1492
  if (family === "work" && command === "add-batch") {
698
- return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file") });
1493
+ return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file"), prepared: prepared.workBatch });
699
1494
  }
700
1495
  if (family === "work" && command === "ls") {
701
1496
  return listWorks(context, { ...(optionalOption(parsed, "run") ? { runId: optionalOption(parsed, "run") } : {}), ...(optionalOption(parsed, "parent") ? { parentWorkId: optionalOption(parsed, "parent") } : {}), ...(optionalOption(parsed, "status") ? { status: optionalOption(parsed, "status") } : {}), ...(hasOption(parsed, "ready") ? { ready: true } : {}), ...(hasOption(parsed, "include-results") ? { includeResults: true } : {}), ...(optionalOption(parsed, "limit") ? { limit: Number(optionalOption(parsed, "limit")) } : {}) });
@@ -703,48 +1498,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
703
1498
  if (family === "work" && command === "show")
704
1499
  return showWork(context, requiredPosition(parsed, 0, "work-id"));
705
1500
  if (family === "work" && command === "repair" && requiredPosition(parsed, 0, "action") === "add") {
706
- if (!hasOption(parsed, "task-stdin"))
707
- throw new AppError("usage", "work repair add requires --task-stdin", 2);
708
- const origins = parsed.options.get("origin-work") ?? [];
709
- const findings = parsed.options.get("from-finding") ?? [];
710
- const unresolved = parsed.options.get("from-unresolved") ?? [];
711
- if (!origins.length && !findings.length)
712
- throw new AppError("usage", "work repair add requires --from-check with --origin-work, --from-finding, or --from-unresolved with --origin-work", 2);
713
- if (findings.length)
714
- return addVnextCodeReviewRepair(context, {
715
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
716
- runId: requiredOption(parsed, "run"),
717
- findingIds: findings,
718
- checkRefsByFinding: Object.fromEntries(findings.map((finding) => [finding, parsed.options.get("check-ref") ?? []])),
719
- objective: await readStdin(io.stdin)
720
- });
721
- if (unresolved.length) {
722
- if (!origins.length)
723
- throw new AppError("usage", "semantic repair requires at least one --origin-work", 2);
724
- const verificationPath = requiredOption(parsed, "verification-file");
725
- return addVnextCodeRepair(context, {
726
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
727
- runId: requiredOption(parsed, "run"),
728
- originWorkIds: origins,
729
- semanticUnresolved: unresolved,
730
- verificationPath,
731
- objective: await readStdin(io.stdin)
732
- });
733
- }
734
- if (!hasOption(parsed, "from-check"))
735
- throw new AppError("usage", "check repair requires --from-check", 2);
736
- if (!origins.length)
737
- throw new AppError("usage", "check repair requires at least one --origin-work", 2);
738
- const receipts = parsed.options.get("from-check") ?? [];
739
- if (receipts.length !== 1)
740
- throw new AppError("usage", "check repair accepts exactly one --from-check; create one causal repair Work per failed receipt", 2);
741
- return addVnextCodeRepair(context, {
742
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
743
- runId: requiredOption(parsed, "run"),
744
- checkReceiptId: receipts[0],
745
- originWorkIds: origins,
746
- objective: await readStdin(io.stdin)
747
- });
1501
+ if (!prepared.workRepair)
1502
+ throw new AppError("cli_input_unprepared", "Repair input was not prepared", 1);
1503
+ return addVnextCodeRepair(context, prepared.workRepair.input, prepared.workRepair.value);
748
1504
  }
749
1505
  if (family === "work" && command === "deps") {
750
1506
  const on = parsed.options.get("on") ?? [];
@@ -752,21 +1508,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
752
1508
  }
753
1509
  if (family === "work" && command === "delete")
754
1510
  return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
1511
+ if (family === "work" && command === "launch")
1512
+ return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
755
1513
  if (family === "work" && command === "start") {
756
1514
  const hookEventId = optionalOption(parsed, "hook-event-id");
1515
+ const recoveryId = optionalOption(parsed, "recovery-id");
757
1516
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
758
- return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
1517
+ return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
759
1518
  }
760
1519
  if (family === "work" && command === "finish") {
761
1520
  const resultFile = optionalOption(parsed, "result-file");
762
1521
  const resultStdin = hasOption(parsed, "result-stdin");
763
1522
  if (Boolean(resultFile) === resultStdin)
764
1523
  throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
765
- 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));
1524
+ const workId = requiredPosition(parsed, 0, "work-id");
1525
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
1526
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1527
+ const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = optionalOption(parsed, "reason");
1528
+ if (Boolean(retryReceiptId) !== Boolean(retryReason))
1529
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
1530
+ const result = prepared.workFinish?.result ?? (resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin));
1531
+ return await finishWork(context, workId, result, commandProgress(io, output), retryReceiptId ? { receiptId: retryReceiptId, reason: retryReason } : undefined);
1532
+ }
1533
+ if (family === "work" && command === "fail") {
1534
+ const workId = requiredPosition(parsed, 0, "work-id");
1535
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
1536
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1537
+ return failWork(context, workId, requiredOption(parsed, "reason"));
767
1538
  }
768
- if (family === "work" && command === "fail")
769
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
770
1539
  if (family === "work" && command === "cancel")
771
1540
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
772
1541
  if (family === "work" && command === "retry")
@@ -788,12 +1557,12 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
788
1557
  const requireSessionBinding = hasOption(parsed, "require-session-binding");
789
1558
  const projectRoot = requiredOption(parsed, "project-root");
790
1559
  const stage = requiredOption(parsed, "stage");
791
- const externalContext = contextFile && contextSha256 ? loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) : undefined;
1560
+ const externalContext = prepared.stageStart?.externalContext;
792
1561
  const claimVnextStageStart = (runId) => {
793
1562
  registerProject(context, { root: projectRoot });
794
1563
  return findRecentMatchingHookEvent(context, {
795
1564
  projectId: requireProjectByRoot(context, resolveProjectRoot(projectRoot)).id,
796
- matchKey: stageStartMatchKey(runId, stage, projectRoot, contextSha256),
1565
+ matchKey: entityReferenceVariants(runId).map((reference) => stageStartMatchKey(reference, stage, projectRoot, contextSha256)),
797
1566
  errorCode: "trusted_session_binding_required",
798
1567
  operation: `vNext ${stage.toUpperCase()} start`
799
1568
  }).eventKey;
@@ -819,37 +1588,37 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
819
1588
  }
820
1589
  const result = launchVnextSpecify(context, {
821
1590
  projectRoot,
822
- ...(externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
1591
+ ...(prepared.stageStart?.intakeMarkdown !== undefined ? { intakeMarkdown: prepared.stageStart.intakeMarkdown } : externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
823
1592
  hookEventId,
824
1593
  ...(subject ? { slug: subject } : {}),
825
1594
  ...(externalContext ? { externalContext } : {}),
826
1595
  ...(!bootstrap ? { runId: requiredPosition(parsed, 0, "run-id") } : {})
827
- });
1596
+ }, prepared.specifyStart);
828
1597
  return attachExternalStageContext(result, externalContext);
829
1598
  }
830
1599
  if (!bootstrap && stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
831
1600
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
832
- return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1601
+ return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.protocolizeStart), externalContext);
833
1602
  }
834
1603
  if (!bootstrap && stage === "plan" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
835
1604
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
836
- return attachExternalStageContext(startVnextPlan(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1605
+ return attachExternalStageContext(startVnextPlan(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planStart), externalContext);
837
1606
  }
838
1607
  if (!bootstrap && stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
839
1608
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
840
- return attachExternalStageContext(startVnextPlanReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1609
+ return attachExternalStageContext(startVnextPlanReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planReviewStart), externalContext);
841
1610
  }
842
1611
  if (!bootstrap && stage === "code" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
843
1612
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
844
- return attachExternalStageContext(await startVnextCode(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1613
+ return attachExternalStageContext(await startVnextCode(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.codeStart), externalContext);
845
1614
  }
846
1615
  if (!bootstrap && stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
847
1616
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
848
- return attachExternalStageContext(startVnextCodeReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1617
+ return attachExternalStageContext(startVnextCodeReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.codeReviewStart), externalContext);
849
1618
  }
850
1619
  if (!bootstrap && stage === "merge" && isVnextMergeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
851
1620
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
852
- return attachExternalStageContext(await startVnextMerge(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
1621
+ return attachExternalStageContext(await startVnextMerge(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.mergeStart), externalContext);
853
1622
  }
854
1623
  if (intakeStdin) {
855
1624
  throw new AppError("usage", "--intake-stdin is only available for vNext SPECIFY", 2);
@@ -864,7 +1633,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
864
1633
  ...(hookEventId ? { hookEventId } : {}),
865
1634
  ...(requireSessionBinding ? { requireSessionBinding: true } : {}),
866
1635
  ...(dir ? { dir } : {})
867
- });
1636
+ }, prepared.legacyStageStart);
868
1637
  }
869
1638
  if (family === "stage" && command === "finish") {
870
1639
  const dir = optionalOption(parsed, "dir");
@@ -874,9 +1643,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
874
1643
  const projectRoot = requiredOption(parsed, "project-root");
875
1644
  const runId = requiredPosition(parsed, 0, "run-id");
876
1645
  const stage = requiredOption(parsed, "stage");
1646
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
877
1647
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
878
1648
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
879
1649
  const resultStdin = hasOption(parsed, "result-stdin");
1650
+ const retryCheckId = optionalOption(parsed, "retry-check");
1651
+ const retryReason = optionalOption(parsed, "reason");
1652
+ const hookEventId = optionalOption(parsed, "hook-event-id");
1653
+ if (retryCheckId && !retryReason?.trim())
1654
+ throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
1655
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1656
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1657
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1658
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1659
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1660
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1661
+ if (protectedLifecycle)
1662
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
880
1663
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
881
1664
  const outcome = optionalOption(parsed, "outcome");
882
1665
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -888,66 +1671,79 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
888
1671
  if (!resultFile && !resultStdin) {
889
1672
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
890
1673
  }
891
- return finishVnextSpecifyStage(context, {
1674
+ return controlledReply(await finishVnextSpecifyStage(context, {
892
1675
  projectRoot,
893
1676
  runId,
894
- ...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
1677
+ ...(resultFile ? { resultFile } : prepared.stageFinish?.value ? {} : { resultJson: await readStdin(io.stdin) }),
1678
+ ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}),
895
1679
  outcome: outcome
896
- });
1680
+ }));
897
1681
  }
898
1682
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
899
1683
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
900
1684
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
901
1685
  }
902
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
1686
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile, ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}) }));
903
1687
  }
904
1688
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
905
1689
  if (optionalOption(parsed, "code-work-batch-file"))
906
1690
  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 });
1691
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId, ...(prepared.planFinish ? { prepared: prepared.planFinish } : {}) }));
908
1692
  }
909
1693
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
910
1694
  if (resultStdin || hasOption(parsed, "outcome"))
911
1695
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
912
1696
  const decisionFile = optionalOption(parsed, "decision-file");
913
- return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
1697
+ return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}), ...(prepared.stageFinish?.value ? { decision: prepared.stageFinish.value, decisionText: prepared.stageFinish.text } : {}) }));
914
1698
  }
915
1699
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
916
1700
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
917
1701
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
918
1702
  }
919
- return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
1703
+ return controlledReply(await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(prepared.stageFinish?.value ? { verification: prepared.stageFinish.value } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
920
1704
  }
921
1705
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
922
1706
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
923
1707
  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) });
1708
+ return controlledReply(await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(prepared.stageFinish?.value ? { decision: prepared.stageFinish.value, decisionText: prepared.stageFinish.text } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
925
1709
  }
926
1710
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
927
1711
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
928
1712
  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) });
1713
+ return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }, prepared.mergeFinish));
930
1714
  }
931
1715
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
932
1716
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
933
1717
  }
934
- return finishStage(context, {
1718
+ return controlledReply(await finishStage(context, {
935
1719
  projectRoot,
936
1720
  runId,
937
1721
  stage,
938
1722
  ...(dir ? { dir } : {}),
939
1723
  ...(semanticFile ? { semanticFile } : {})
940
- });
1724
+ }, prepared.legacyStageFinish));
941
1725
  }
942
1726
  if (family === "stage" && command === "pause") {
943
1727
  if (!hasOption(parsed, "question-stdin"))
944
1728
  throw new AppError("usage", "stage pause requires --question-stdin", 2);
1729
+ const projectRoot = requiredOption(parsed, "project-root");
1730
+ const runId = requiredPosition(parsed, 0, "run-id");
1731
+ const stage = requiredOption(parsed, "stage");
1732
+ const workId = requiredOption(parsed, "work");
1733
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1734
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1735
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1736
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1737
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1738
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1739
+ if (protectedLifecycle)
1740
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
945
1741
  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"),
950
- question: await readStdin(io.stdin)
1742
+ projectRoot,
1743
+ runId,
1744
+ stage,
1745
+ workId,
1746
+ question: prepared.interactionText ?? await readStdin(io.stdin)
951
1747
  });
952
1748
  }
953
1749
  if (family === "stage" && command === "resume") {
@@ -959,7 +1755,8 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
959
1755
  runId: requiredPosition(parsed, 0, "run-id"),
960
1756
  stage: requiredOption(parsed, "stage"),
961
1757
  workId: requiredOption(parsed, "work"),
962
- answer: await readStdin(io.stdin),
1758
+ answer: prepared.interactionText ?? await readStdin(io.stdin),
1759
+ evidence: prepared.resumeEvidence,
963
1760
  ...(hookEventId ? { hookEventId } : {})
964
1761
  });
965
1762
  }
@@ -976,7 +1773,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
976
1773
  workId: requiredOption(parsed, "work"),
977
1774
  kind,
978
1775
  code: requiredOption(parsed, "code"),
979
- summary: await readStdin(io.stdin),
1776
+ summary: prepared.interactionText ?? await readStdin(io.stdin),
980
1777
  retryable: hasOption(parsed, "retryable")
981
1778
  });
982
1779
  }
@@ -1001,13 +1798,55 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1001
1798
  return dispatchMergeWorker(context, command, parsed);
1002
1799
  }
1003
1800
  if (family === "cleanup") {
1004
- return dispatchCleanup(context, command, parsed);
1801
+ return dispatchCleanup(context, command, parsed, prepared.cleanup);
1005
1802
  }
1006
1803
  if (family === "session") {
1007
- return dispatchSession(context, command, parsed);
1804
+ return dispatchSession(context, command, parsed, prepared.sessionPayload);
1008
1805
  }
1009
1806
  if (family === "run") {
1010
- return dispatchRun(context, command, parsed);
1807
+ if (command === "control") {
1808
+ const action = requiredPosition(parsed, 0, "control action");
1809
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1810
+ if (action === "status")
1811
+ return runControlStatus(context, scope);
1812
+ if (action === "reconcile") {
1813
+ if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1814
+ throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
1815
+ return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
1816
+ }
1817
+ if (action === "resume") {
1818
+ if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
1819
+ throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
1820
+ return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
1821
+ }
1822
+ if (action === "serve")
1823
+ return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
1824
+ if (action === "pause" || action === "stop")
1825
+ return await requestRunControl(context, {
1826
+ ...scope, action, requestId: requiredOption(parsed, "request-id"),
1827
+ ...(hasOption(parsed, "force") ? { force: true } : {}),
1828
+ ...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
1829
+ ...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
1830
+ ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
1831
+ });
1832
+ throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
1833
+ }
1834
+ if (command === "drive") {
1835
+ const action = requiredPosition(parsed, 0, "drive action");
1836
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1837
+ if (action === "status")
1838
+ return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
1839
+ if (action === "launch")
1840
+ return await launchRunController(context, controllerLaunchInput(parsed), prepared.controllerLaunch);
1841
+ if (action === "serve")
1842
+ return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
1843
+ if (action === "answer")
1844
+ return answerRunController(context, controllerAnswerInput(parsed), prepared.controllerAnswer);
1845
+ if (action === "context")
1846
+ return supplyRunControllerContext(context, controllerContextInput(parsed), prepared.controllerContext);
1847
+ throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
1848
+ }
1849
+ return dispatchRun(context, command, parsed, prepared);
1011
1850
  }
1012
1851
  if (family === "stat") {
1013
1852
  return dispatchStat(context, command, parsed);
@@ -1022,23 +1861,26 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1022
1861
  return dispatchDashboard(context, command, parsed);
1023
1862
  }
1024
1863
  if (family === "schema") {
1025
- return dispatchSchema(context, command, parsed);
1864
+ return prepared.schemaValidation;
1026
1865
  }
1027
1866
  if (family === "memory") {
1028
1867
  return dispatchMemory(command, parsed);
1029
1868
  }
1030
1869
  if (family === "migration") {
1031
- return dispatchMigration(context, command, parsed, classification);
1870
+ return dispatchMigration(context, command, parsed, classification, prepared);
1032
1871
  }
1033
1872
  if (family === "codex" && command === "hooks") {
1034
- return dispatchCodexHooks(context, parsed);
1873
+ return dispatchCodexHooks(context, parsed, prepared);
1035
1874
  }
1036
1875
  if (family === "codex" && command === "home") {
1037
- return dispatchCodexHome(context, parsed);
1876
+ return dispatchCodexHome(context, parsed, prepared);
1038
1877
  }
1039
1878
  if (family === "codex" && command === "hook") {
1040
1879
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
1041
1880
  }
1881
+ if (family === "zcode" && command === "hook") {
1882
+ return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
1883
+ }
1042
1884
  if (family === "zcode" && command === "event") {
1043
1885
  return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
1044
1886
  }
@@ -1063,6 +1905,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1063
1905
  throw new AppError("usage", "Unknown opencode usage action", 2);
1064
1906
  return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1065
1907
  }
1908
+ if (family === "droid" && command === "event") {
1909
+ if (requiredPosition(parsed, 0, "droid event action") !== "handle")
1910
+ throw new AppError("usage", "Unknown droid event action", 2);
1911
+ return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1912
+ }
1913
+ if (family === "droid" && command === "usage") {
1914
+ if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
1915
+ throw new AppError("usage", "Unknown droid usage action", 2);
1916
+ return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1917
+ }
1066
1918
  if (family === "agy" && command === "event")
1067
1919
  return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
1068
1920
  if (family === "agy" && command === "usage") {
@@ -1071,11 +1923,14 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1071
1923
  return ingestAgyUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1072
1924
  }
1073
1925
  if (family === "worktree") {
1074
- return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
1926
+ return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`), prepared);
1075
1927
  }
1076
1928
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
1077
1929
  }
1078
- function dispatchEngine(context, command, parsed, classification) {
1930
+ function engineBindInput(parsed) {
1931
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), packageName: optionalOption(parsed, "package"), engineVersion: requiredOption(parsed, "engine-version"), integrityChecksum: optionalOption(parsed, "integrity-checksum"), reason: requiredOption(parsed, "reason") };
1932
+ }
1933
+ function dispatchEngine(context, command, parsed, classification, prepared) {
1079
1934
  if (command === "install") {
1080
1935
  return installCurrentEngine(context, { force: hasOption(parsed, "force") });
1081
1936
  }
@@ -1100,18 +1955,11 @@ function dispatchEngine(context, command, parsed, classification) {
1100
1955
  });
1101
1956
  }
1102
1957
  if (command === "bind-run") {
1103
- return bindRunEngine(context, {
1104
- projectRoot: requiredOption(parsed, "project-root"),
1105
- runId: requiredPosition(parsed, 0, "run-id"),
1106
- packageName: optionalOption(parsed, "package"),
1107
- engineVersion: requiredOption(parsed, "engine-version"),
1108
- integrityChecksum: optionalOption(parsed, "integrity-checksum"),
1109
- reason: requiredOption(parsed, "reason")
1110
- });
1958
+ return bindRunEngine(context, engineBindInput(parsed), prepared.engineBind);
1111
1959
  }
1112
1960
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
1113
1961
  }
1114
- function dispatchMigration(context, command, parsed, classification) {
1962
+ function dispatchMigration(context, command, parsed, classification, prepared) {
1115
1963
  if (command === "impact")
1116
1964
  return assessMigrationImpact(context, { projectRoot: requiredOption(parsed, "project-root"), ...(optionalOption(parsed, "target-version") ? { targetVersion: optionalOption(parsed, "target-version") } : {}), ...(optionalOption(parsed, "mode") ? { mode: optionalOption(parsed, "mode") } : {}) });
1117
1965
  if (command === "plan" || command === "report") {
@@ -1127,26 +1975,42 @@ function dispatchMigration(context, command, parsed, classification) {
1127
1975
  });
1128
1976
  }
1129
1977
  if (command === "verify") {
1130
- return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
1978
+ return prepared.migrationVerification ?? verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
1131
1979
  }
1132
1980
  if (command === "apply") {
1133
- return applyMigration(context, {
1134
- projectRoot: requiredOption(parsed, "project-root"),
1135
- sourceVersion: optionalOption(parsed, "source-version"),
1136
- targetVersion: optionalOption(parsed, "target-version"),
1137
- runId: optionalOption(parsed, "run"),
1138
- backupPath: optionalOption(parsed, "backup-path"),
1139
- backupCreatedAt: optionalOption(parsed, "backup-created-at"),
1140
- reportFile: optionalOption(parsed, "file"),
1141
- allowActive: false,
1142
- classification
1143
- });
1981
+ return applyMigration(context, migrationApplyInput(parsed, classification), prepared.migrationApply);
1144
1982
  }
1145
1983
  throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
1146
1984
  }
1147
- function dispatchCanon(context, command, parsed) {
1985
+ function protocolTransitionInput(parsed, scopeProjectRoot) {
1986
+ const reason = optionalOption(parsed, "reason");
1987
+ return { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"), to: requiredOption(parsed, "to"), jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]), force: hasOption(parsed, "force"), ...(reason ? { reason } : {}) };
1988
+ }
1989
+ function controllerAnswerInput(parsed) {
1990
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), requestId: requiredOption(parsed, "request-id"), pauseId: requiredOption(parsed, "pause-id"), answerFile: requiredOption(parsed, "answer-file") };
1991
+ }
1992
+ function controllerLaunchInput(parsed) {
1993
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), 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") } : {}) };
1994
+ }
1995
+ function controllerContextInput(parsed) {
1996
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), controllerId: requiredOption(parsed, "controller-id"), requestId: requiredOption(parsed, "request-id"), stage: requiredOption(parsed, "stage"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256") };
1997
+ }
1998
+ function migrationApplyInput(parsed, classification) {
1999
+ return {
2000
+ projectRoot: requiredOption(parsed, "project-root"),
2001
+ sourceVersion: optionalOption(parsed, "source-version"),
2002
+ targetVersion: optionalOption(parsed, "target-version"),
2003
+ runId: optionalOption(parsed, "run"),
2004
+ backupPath: optionalOption(parsed, "backup-path"),
2005
+ backupCreatedAt: optionalOption(parsed, "backup-created-at"),
2006
+ reportFile: optionalOption(parsed, "file"),
2007
+ allowActive: false,
2008
+ classification
2009
+ };
2010
+ }
2011
+ function dispatchCanon(context, command, parsed, prepared) {
1148
2012
  if (command === "register") {
1149
- return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
2013
+ return registerCanonRoot(context, { root: requiredOption(parsed, "root") }, prepared.canonRegistration);
1150
2014
  }
1151
2015
  if (command === "status") {
1152
2016
  return getCanonStatus(context, { root: optionalOption(parsed, "root") });
@@ -1156,6 +2020,15 @@ function dispatchCanon(context, command, parsed) {
1156
2020
  }
1157
2021
  throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
1158
2022
  }
2023
+ function mergeJobTarget(parsed, scopeProjectRoot) {
2024
+ return { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue"), workerId: requiredWorkerId(parsed), workspacePath: workspacePathOption(parsed) };
2025
+ }
2026
+ function mergeCancellationInput(parsed, scopeProjectRoot) {
2027
+ return { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"), reason: requiredOption(parsed, "reason"), workerId: optionalOption(parsed, "worker-id"), workspacePath: optionalOption(parsed, "path"), force: hasOption(parsed, "force") };
2028
+ }
2029
+ function mergeWaitInput(parsed, scopeProjectRoot) {
2030
+ return { projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"), workerId: requiredWorkerId(parsed), timeoutSeconds: optionalNumber(parsed, "timeout"), pollIntervalSeconds: optionalNumber(parsed, "poll-interval"), workspacePath: workspacePathOption(parsed), acquireLock: parseOptionalBoolean(optionalOption(parsed, "acquire-lock"), "acquire-lock") ?? false };
2031
+ }
1159
2032
  function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1160
2033
  if (hasOption(parsed, "session-id")) {
1161
2034
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
@@ -1179,32 +2052,17 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1179
2052
  });
1180
2053
  }
1181
2054
  if (command === "wait-next") {
1182
- return waitNextMergeJob(context, {
1183
- projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
1184
- workerId: requiredWorkerId(parsed),
1185
- timeoutSeconds: optionalNumber(parsed, "timeout"),
1186
- pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
1187
- workspacePath: workspacePathOption(parsed),
1188
- acquireLock: parseOptionalBoolean(optionalOption(parsed, "acquire-lock"), "acquire-lock") ?? false
1189
- });
2055
+ return waitNextMergeJob(context, mergeWaitInput(parsed, scopeProjectRoot));
1190
2056
  }
1191
2057
  if (command === "complete") {
1192
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1193
2058
  return completeMergeJob(context, {
1194
- protocolId,
1195
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
1196
- workerId: requiredWorkerId(parsed),
1197
- workspacePath: workspacePathOption(parsed),
2059
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1198
2060
  summary: requiredOption(parsed, "summary")
1199
2061
  });
1200
2062
  }
1201
2063
  if (command === "fail") {
1202
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1203
2064
  return failMergeJob(context, {
1204
- protocolId,
1205
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
1206
- workerId: requiredWorkerId(parsed),
1207
- workspacePath: workspacePathOption(parsed),
2065
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1208
2066
  reason: requiredOption(parsed, "reason"),
1209
2067
  requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
1210
2068
  });
@@ -1219,15 +2077,7 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1219
2077
  });
1220
2078
  }
1221
2079
  if (command === "cancel") {
1222
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1223
- return cancelMergeQueueJob(context, {
1224
- protocolId,
1225
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"),
1226
- reason: requiredOption(parsed, "reason"),
1227
- workerId: optionalOption(parsed, "worker-id"),
1228
- workspacePath: optionalOption(parsed, "path"),
1229
- force: hasOption(parsed, "force")
1230
- });
2080
+ return cancelMergeQueueJob(context, mergeCancellationInput(parsed, scopeProjectRoot));
1231
2081
  }
1232
2082
  throw new AppError("usage", `Unknown merge-queue command: ${command ?? "<empty>"}`, 2);
1233
2083
  }
@@ -1247,6 +2097,8 @@ async function dispatchMerge(context, command, parsed, progress) {
1247
2097
  }
1248
2098
  if (command === "apply")
1249
2099
  return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
2100
+ if (command === "repair")
2101
+ return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
1250
2102
  if (command === "request") {
1251
2103
  const action = requiredPosition(parsed, 0, "request action");
1252
2104
  const requestId = requiredPosition(parsed, 1, "merge-request-id");
@@ -1343,7 +2195,7 @@ function completedCommandExitCode(payload) {
1343
2195
  const exitCode = payload.exit_code;
1344
2196
  return typeof exitCode === "number" && Number.isInteger(exitCode) ? exitCode : 0;
1345
2197
  }
1346
- function dispatchCleanup(context, command, parsed) {
2198
+ function dispatchCleanup(context, command, parsed, prepared) {
1347
2199
  if (command === "scan") {
1348
2200
  return cleanupScan(context, { projectRoot: requiredOption(parsed, "project-root") });
1349
2201
  }
@@ -1351,6 +2203,7 @@ function dispatchCleanup(context, command, parsed) {
1351
2203
  return cleanupApply(context, {
1352
2204
  projectRoot: requiredOption(parsed, "project-root"),
1353
2205
  planFile: requiredOption(parsed, "plan-file"),
2206
+ prepared,
1354
2207
  reason: requiredOption(parsed, "reason"),
1355
2208
  force: hasOption(parsed, "force")
1356
2209
  });
@@ -1371,12 +2224,14 @@ function dispatchProjectConfig(context, parsed) {
1371
2224
  }
1372
2225
  throw new AppError("usage", `Unknown project config action: ${action}`, 2);
1373
2226
  }
1374
- function dispatchSession(context, command, parsed) {
2227
+ function dispatchSession(context, command, parsed, preparedPayload) {
1375
2228
  if (command === "register") {
1376
2229
  return registerFlowSession(context, {
2230
+ ...(preparedPayload ? { preparedPayload } : {}),
1377
2231
  payloadBase64: optionalOption(parsed, "payload-base64"),
1378
2232
  payloadJson: optionalOption(parsed, "payload-json"),
1379
2233
  payloadFile: optionalOption(parsed, "payload-file"),
2234
+ hookEventId: optionalOption(parsed, "hook-event-id"),
1380
2235
  sessionId: optionalOption(parsed, "session-id")
1381
2236
  });
1382
2237
  }
@@ -1409,7 +2264,45 @@ function dispatchSession(context, command, parsed) {
1409
2264
  }
1410
2265
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1411
2266
  }
1412
- function dispatchRun(context, command, parsed) {
2267
+ function snapshotCreateInput(parsed) {
2268
+ const recoveryId = optionalOption(parsed, "recovery-id"), stageEntry = optionalOption(parsed, "stage-entry");
2269
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), output: requiredOption(parsed, "output"), candidate: parsed.options.has("candidate"), incomplete: parsed.options.has("incomplete"), ...(recoveryId !== undefined ? { recoveryId } : {}), ...(stageEntry !== undefined ? { stageEntry } : {}) };
2270
+ }
2271
+ function snapshotRestoreInput(parsed) {
2272
+ return { snapshot: requiredOption(parsed, "snapshot"), projectRoot: requiredOption(parsed, "project-root"), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}) };
2273
+ }
2274
+ function runStartInput(parsed) {
2275
+ const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
2276
+ return { projectRoot: requiredOption(parsed, "project-root"), workspaceRoot: optionalOption(parsed, "workspace-root"), flowKind: requiredOption(parsed, "flow-kind"), subjectType: requiredOption(parsed, "subject-type"), subjectId: requiredOption(parsed, "subject-id"), slug: requiredOption(parsed, "slug"), nextAction: optionalOption(parsed, "next-action"), preset: optionalOption(parsed, "preset"), taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(protocolOverrides !== undefined ? { protocolOverrides } : {}), runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag")) };
2277
+ }
2278
+ function runForkInput(parsed) {
2279
+ return { snapshot: requiredOption(parsed, "from"), output: requiredOption(parsed, "output"), engineVersion: requiredOption(parsed, "engine-version"), requestId: requiredOption(parsed, "request-id"), ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {}) };
2280
+ }
2281
+ function recoveryCaptureInput(parsed) {
2282
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") };
2283
+ }
2284
+ function recoveryResumeInput(parsed) {
2285
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), daemonId: requiredOption(parsed, "daemon-id"), harness: requiredOption(parsed, "harness"), nativeSessionId: requiredOption(parsed, "native-session-id") };
2286
+ }
2287
+ function dispatchRun(context, command, parsed, prepared = {}) {
2288
+ if (command === "recovery") {
2289
+ const action = requiredPosition(parsed, 0, "recovery action");
2290
+ const projectRoot = requiredOption(parsed, "project-root");
2291
+ const runId = requiredPosition(parsed, 1, "run-id");
2292
+ if (action === "inspect")
2293
+ return inspectRunRecovery(context, { projectRoot, runId });
2294
+ if (action === "begin")
2295
+ return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") }, prepared.recoveryPayload);
2296
+ if (action === "seal")
2297
+ return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") }, prepared.recoveryPayload);
2298
+ if (action === "capture")
2299
+ return recordRecoveryCapture(context, recoveryCaptureInput(parsed), prepared.recoveryCapture);
2300
+ if (action === "resume")
2301
+ return resumeRunRecovery(context, recoveryResumeInput(parsed), prepared.recoveryResume);
2302
+ if (action === "accept")
2303
+ return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
2304
+ throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
2305
+ }
1413
2306
  if (command === "snapshot") {
1414
2307
  const action = requiredPosition(parsed, 0, "snapshot action");
1415
2308
  if (action === "bootstrap") {
@@ -1420,56 +2313,27 @@ function dispatchRun(context, command, parsed) {
1420
2313
  output: requiredOption(parsed, "output")
1421
2314
  });
1422
2315
  if (bootstrapAction === "restore")
1423
- return restoreEvalBootstrapSnapshot(context, {
1424
- snapshot: requiredOption(parsed, "snapshot"),
1425
- projectRoot: requiredOption(parsed, "project-root")
1426
- });
2316
+ return restoreEvalBootstrapSnapshot(context, snapshotRestoreInput(parsed), prepared.bootstrapRestore);
1427
2317
  throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore", 2);
1428
2318
  }
1429
2319
  if (action === "create") {
1430
- const candidate = parsed.options.has("candidate");
1431
- 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);
1436
- const input = {
1437
- projectRoot: requiredOption(parsed, "project-root"),
1438
- runId: requiredPosition(parsed, 1, "run-id"),
1439
- output: requiredOption(parsed, "output")
1440
- };
1441
- return candidate
1442
- ? createEvalRunSnapshot(context, { ...input, candidate: true })
1443
- : createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
2320
+ return createEvalRunSnapshot(context, snapshotCreateInput(parsed));
1444
2321
  }
1445
2322
  if (action === "restore")
1446
- return restoreEvalRunSnapshot(context, {
1447
- snapshot: requiredOption(parsed, "snapshot"),
1448
- projectRoot: requiredOption(parsed, "project-root")
1449
- });
2323
+ return restoreEvalRunSnapshot(context, snapshotRestoreInput(parsed), prepared.snapshotRestore);
1450
2324
  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
2325
  }
2326
+ if (command === "fork") {
2327
+ return forkRunFromSnapshot(context, runForkInput(parsed), prepared.runFork);
2328
+ }
1452
2329
  if (command === "prepare-vnext-specify") {
1453
- return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
2330
+ return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
1454
2331
  }
1455
2332
  if (command === "start") {
1456
2333
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
1457
2334
  assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
1458
2335
  }
1459
- const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
1460
- return startFlowRun(context, {
1461
- projectRoot: requiredOption(parsed, "project-root"),
1462
- workspaceRoot: optionalOption(parsed, "workspace-root"),
1463
- flowKind: requiredOption(parsed, "flow-kind"),
1464
- subjectType: requiredOption(parsed, "subject-type"),
1465
- subjectId: requiredOption(parsed, "subject-id"),
1466
- slug: requiredOption(parsed, "slug"),
1467
- nextAction: optionalOption(parsed, "next-action"),
1468
- preset: optionalOption(parsed, "preset"),
1469
- taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
1470
- ...(protocolOverrides ? { protocolOverrides } : {}),
1471
- runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
1472
- });
2336
+ return startFlowRun(context, prepared.runStart?.input ?? runStartInput(parsed), prepared.runStart?.value);
1473
2337
  }
1474
2338
  if (command === "status") {
1475
2339
  return getFlowRunStatus(context, {
@@ -1539,83 +2403,163 @@ function dispatchRun(context, command, parsed) {
1539
2403
  throw new AppError("usage", `Unknown run flags command: ${action}`, 2);
1540
2404
  }
1541
2405
  if (command === "attach-stage") {
1542
- return attachFlowRunStage(context, {
1543
- projectRoot: requiredOption(parsed, "project-root"),
1544
- runId: requiredPosition(parsed, 0, "run-id"),
1545
- stage: requiredOption(parsed, "stage"),
1546
- dir: requiredOption(parsed, "dir"),
1547
- status: requiredOption(parsed, "status"),
1548
- dataSchemaId: optionalOption(parsed, "data-schema-id")
1549
- });
2406
+ return attachFlowRunStage(context, stageAttachmentInput(parsed));
1550
2407
  }
1551
2408
  if (command === "complete-stage") {
1552
- const stageReport = optionalOption(parsed, "stage-report") ?? optionalOption(parsed, "dashboard");
1553
- return completeFlowRunStage(context, {
1554
- projectRoot: requiredOption(parsed, "project-root"),
1555
- runId: requiredPosition(parsed, 0, "run-id"),
1556
- stage: requiredOption(parsed, "stage"),
1557
- status: requiredOption(parsed, "status"),
1558
- stageReport,
1559
- data: optionalOption(parsed, "data"),
1560
- dataSchemaId: optionalOption(parsed, "data-schema-id"),
1561
- report: optionalOption(parsed, "report"),
1562
- aliases: allOptions(parsed, "alias")
1563
- });
2409
+ return completeFlowRunStage(context, stageCompletionInput(parsed));
1564
2410
  }
1565
2411
  if (command === "complete") {
1566
2412
  throw new AppError("usage", "run complete is retired; only an operator may use run override", 2);
1567
2413
  }
1568
2414
  if (command === "override") {
1569
- const status = requiredOption(parsed, "status");
1570
- if (!['cancelled', 'failed'].includes(status)) {
1571
- throw new AppError("usage", "run override status must be cancelled or failed", 2, { status });
1572
- }
1573
- return completeFlowRun(context, {
1574
- projectRoot: requiredOption(parsed, "project-root"),
1575
- runId: requiredPosition(parsed, 0, "run-id"),
1576
- status,
1577
- verdict: optionalOption(parsed, "verdict"),
1578
- nextAction: optionalOption(parsed, "next-action"),
1579
- manualOverrideReason: requiredOption(parsed, "reason")
1580
- });
2415
+ return completeFlowRun(context, runOverrideInput(parsed));
1581
2416
  }
1582
2417
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1583
2418
  }
1584
- async function dispatchRuntime(context, command, parsed) {
2419
+ function stageAttachmentInput(parsed) {
2420
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), dir: requiredOption(parsed, "dir"), status: requiredOption(parsed, "status"), dataSchemaId: optionalOption(parsed, "data-schema-id") };
2421
+ }
2422
+ function runOverrideInput(parsed) {
2423
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), status: requiredOption(parsed, "status"), verdict: optionalOption(parsed, "verdict"), nextAction: optionalOption(parsed, "next-action"), manualOverrideReason: requiredOption(parsed, "reason") };
2424
+ }
2425
+ function stageCompletionInput(parsed) {
2426
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), status: requiredOption(parsed, "status"), stageReport: optionalOption(parsed, "stage-report") ?? optionalOption(parsed, "dashboard"), data: optionalOption(parsed, "data"), dataSchemaId: optionalOption(parsed, "data-schema-id"), report: optionalOption(parsed, "report"), aliases: allOptions(parsed, "alias") };
2427
+ }
2428
+ function runtimeServiceInput(parsed) {
2429
+ return { 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 };
2430
+ }
2431
+ function runtimeProcessStopInput(parsed) {
2432
+ const graceMs = optionalNumber(parsed, "grace-ms");
2433
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
2434
+ }
2435
+ function runtimeScopeResumeInput(parsed) {
2436
+ return { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") };
2437
+ }
2438
+ function runtimeProcessRegistrationInput(context, parsed) {
2439
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2440
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
2441
+ const kind = requiredOption(parsed, "kind");
2442
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
2443
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
2444
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
2445
+ return {
2446
+ kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
2447
+ ...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
2448
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
2449
+ workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
2450
+ operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
2451
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
2452
+ 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") } : {}) }
2453
+ };
2454
+ }
2455
+ function runtimeProcessConfirmationInput(parsed) {
2456
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
2457
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId !== undefined ? { processGroupId } : {}), ...(leaseMs !== undefined ? { leaseMs } : {}) };
2458
+ }
2459
+ function runtimeProcessHeartbeatInput(parsed) {
2460
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2461
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
2462
+ }
2463
+ function runtimeProcessFinishInput(parsed) {
2464
+ const state = requiredOption(parsed, "state");
2465
+ if (state !== "stopped" && state !== "failed")
2466
+ throw new AppError("validation", "--state must be stopped or failed", 2);
2467
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
2468
+ }
2469
+ async function dispatchRuntime(context, command, parsed, progress, prepared = {}) {
2470
+ if (command === "scope") {
2471
+ if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
2472
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2473
+ throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
2474
+ return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
2475
+ }
2476
+ if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
2477
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2478
+ throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
2479
+ return { ok: true, ...await launchRuntimeScopeResume(context, runtimeScopeResumeInput(parsed)) };
2480
+ }
2481
+ if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
2482
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2483
+ throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
2484
+ const mode = requiredOption(parsed, "mode");
2485
+ if (mode !== "pause" && mode !== "stop")
2486
+ throw new AppError("usage", "Scope control mode must be pause or stop", 2);
2487
+ return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, undefined, undefined, undefined, prepared.runtimeManifest) };
2488
+ }
2489
+ if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
2490
+ if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2491
+ throw new AppError("usage", "Invalid internal scope worker arguments", 2);
2492
+ return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
2493
+ }
2494
+ if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
2495
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2496
+ throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
2497
+ return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
2498
+ }
2499
+ if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
2500
+ if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
2501
+ throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
2502
+ return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
2503
+ }
2504
+ 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))
2505
+ throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
2506
+ return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
2507
+ }
1585
2508
  if (command !== "process")
1586
- throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
2509
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
1587
2510
  const action = requiredPosition(parsed, 0, "runtime process action");
2511
+ if (action === "check-admission")
2512
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
2513
+ if (action === "start")
2514
+ return await startRuntimeService(context, { ...runtimeServiceInput(parsed), progress });
2515
+ if (action === "stop")
2516
+ return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
1588
2517
  if (action === "register") {
1589
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1590
- const process = registerManagedProcess(context, {
1591
- kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1592
- projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1593
- workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1594
- operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
1595
- stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
1596
- });
2518
+ const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
1597
2519
  return { ok: true, process };
1598
2520
  }
1599
- if (action === "confirm") {
1600
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1601
- 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 } : {}) }) };
1603
- }
1604
- if (action === "heartbeat") {
1605
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1606
- return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
2521
+ if (action === "confirm")
2522
+ return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
2523
+ if (action === "heartbeat")
2524
+ return { ok: heartbeatManagedProcess(context, runtimeProcessHeartbeatInput(parsed)) };
2525
+ if (action === "admission") {
2526
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2527
+ 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))
2528
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
2529
+ const metadata = JSON.parse(record.metadata_json);
2530
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
2531
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
2532
+ if (record.project_id && record.run_id) {
2533
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2534
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
2535
+ 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") });
2536
+ if (!recovering) {
2537
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
2538
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
2539
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
2540
+ }
2541
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
2542
+ 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")]);
2543
+ 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") });
2544
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
2545
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
2546
+ }
2547
+ 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") });
2548
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
1607
2549
  }
1608
- if (action === "finish") {
1609
- const state = requiredOption(parsed, "state");
1610
- if (state !== "stopped" && state !== "failed")
1611
- throw new AppError("validation", "--state must be stopped or failed", 2);
1612
- return { ok: finishManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state: state, reason: optionalOption(parsed, "reason") ?? null }) };
2550
+ if (action === "release-turn") {
2551
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2552
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2553
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
2554
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
1613
2555
  }
2556
+ if (action === "finish")
2557
+ return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
1614
2558
  if (action === "status")
1615
2559
  return { ok: true, processes: managedProcessStatus(context) };
1616
2560
  if (action === "reconcile")
1617
- 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);
2561
+ return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
2562
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
1619
2563
  }
1620
2564
  function dispatchStat(context, command, parsed) {
1621
2565
  if (command === "usage") {
@@ -1802,7 +2746,9 @@ function dispatchSchema(context, command, parsed) {
1802
2746
  // searching project directories: run_id is a structural field in the
1803
2747
  // submitted artifact and the database owns its materialized run_root.
1804
2748
  const explicitRunId = optionalOption(parsed, "run");
1805
- const declaredRunId = explicitRunId ?? readRunId(file);
2749
+ const data = prepareJsonFile(file, "schema input").value;
2750
+ const declared = data && typeof data === "object" && "run_id" in data ? data.run_id : undefined;
2751
+ const declaredRunId = explicitRunId ?? (typeof declared === "string" && declared.trim() ? declared : undefined);
1806
2752
  const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
1807
2753
  const bound = projectRoot
1808
2754
  ? 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 ?? ""])
@@ -1810,6 +2756,7 @@ function dispatchSchema(context, command, parsed) {
1810
2756
  return validateSchema({
1811
2757
  schemaName: requiredOption(parsed, "schema"),
1812
2758
  file,
2759
+ data,
1813
2760
  ...(schemaDir ? { schemaDir } : {}),
1814
2761
  ...(projectRoot ? { projectRoot } : {}),
1815
2762
  ...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
@@ -1819,14 +2766,11 @@ function dispatchSchema(context, command, parsed) {
1819
2766
  }
1820
2767
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
1821
2768
  }
1822
- function readRunId(file) {
1823
- try {
1824
- const value = JSON.parse(fs.readFileSync(file, "utf8"));
1825
- return typeof value.run_id === "string" && value.run_id.trim() ? value.run_id : undefined;
1826
- }
1827
- catch {
1828
- return undefined;
1829
- }
2769
+ function laneWorkspaceInput(parsed) {
2770
+ return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workspacePath: requiredOption(parsed, "path"), branch: optionalOption(parsed, "branch") };
2771
+ }
2772
+ function laneWaiterCancelInput(parsed) {
2773
+ return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"), reason: requiredOption(parsed, "reason") };
1830
2774
  }
1831
2775
  async function dispatchLane(context, command, parsed) {
1832
2776
  if (command === "status") {
@@ -1838,24 +2782,14 @@ async function dispatchLane(context, command, parsed) {
1838
2782
  if (command === "waiter") {
1839
2783
  const action = requiredPosition(parsed, 0, "lane waiter action");
1840
2784
  if (action === "cancel") {
1841
- return cancelLaneWaiter(context, {
1842
- projectRoot: requiredOption(parsed, "project-root"),
1843
- lane: requiredOption(parsed, "lane"),
1844
- workerId: requiredOption(parsed, "worker-id"),
1845
- reason: requiredOption(parsed, "reason")
1846
- });
2785
+ return cancelLaneWaiter(context, laneWaiterCancelInput(parsed));
1847
2786
  }
1848
2787
  throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
1849
2788
  }
1850
2789
  if (command === "workspace") {
1851
2790
  const action = requiredPosition(parsed, 0, "lane workspace action");
1852
2791
  if (action === "set") {
1853
- return setLaneWorkspace(context, {
1854
- projectRoot: requiredOption(parsed, "project-root"),
1855
- lane: requiredOption(parsed, "lane"),
1856
- workspacePath: requiredOption(parsed, "path"),
1857
- branch: optionalOption(parsed, "branch")
1858
- });
2792
+ return setLaneWorkspace(context, laneWorkspaceInput(parsed));
1859
2793
  }
1860
2794
  if (action === "check") {
1861
2795
  return checkLaneWorkspace(context, {
@@ -1927,7 +2861,7 @@ async function dispatchLane(context, command, parsed) {
1927
2861
  }
1928
2862
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
1929
2863
  }
1930
- function dispatchCodexHooks(context, parsed) {
2864
+ function dispatchCodexHooks(context, parsed, prepared) {
1931
2865
  const action = requiredPosition(parsed, 0, "codex hooks action");
1932
2866
  if (action === "print") {
1933
2867
  return printCodexHooks(context, {
@@ -1949,7 +2883,7 @@ function dispatchCodexHooks(context, parsed) {
1949
2883
  target: optionalOption(parsed, "target"),
1950
2884
  profile: optionalOption(parsed, "profile"),
1951
2885
  yes: hasOption(parsed, "yes")
1952
- });
2886
+ }, prepared.codexHooks);
1953
2887
  }
1954
2888
  if (action === "remove") {
1955
2889
  return removeCodexHooks(context, {
@@ -1957,11 +2891,14 @@ function dispatchCodexHooks(context, parsed) {
1957
2891
  target: optionalOption(parsed, "target"),
1958
2892
  profile: optionalOption(parsed, "profile"),
1959
2893
  yes: hasOption(parsed, "yes")
1960
- });
2894
+ }, prepared.codexHooks);
1961
2895
  }
1962
2896
  throw new AppError("usage", `Unknown codex hooks action: ${action}`, 2);
1963
2897
  }
1964
- function dispatchCodexHome(context, parsed) {
2898
+ function codexHomeInput(parsed) {
2899
+ return { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), sourceHome: optionalOption(parsed, "source-home"), targetHome: optionalOption(parsed, "target-home") };
2900
+ }
2901
+ function dispatchCodexHome(context, parsed, prepared) {
1965
2902
  const action = requiredPosition(parsed, 0, "codex home action");
1966
2903
  if (action === "plan") {
1967
2904
  return planCodexHome(context, {
@@ -1972,12 +2909,7 @@ function dispatchCodexHome(context, parsed) {
1972
2909
  });
1973
2910
  }
1974
2911
  if (action === "init") {
1975
- return initCodexHome(context, {
1976
- projectRoot: requiredOption(parsed, "project-root"),
1977
- profile: optionalOption(parsed, "profile"),
1978
- sourceHome: optionalOption(parsed, "source-home"),
1979
- targetHome: optionalOption(parsed, "target-home")
1980
- });
2912
+ return initCodexHome(context, codexHomeInput(parsed), prepared.codexHome);
1981
2913
  }
1982
2914
  if (action === "status") {
1983
2915
  return getCodexHomeStatus(context, {
@@ -2000,11 +2932,14 @@ function dispatchCodexHome(context, parsed) {
2000
2932
  }
2001
2933
  throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
2002
2934
  }
2003
- function dispatchCodexHook(context, parsed, stdin) {
2935
+ async function dispatchCodexHook(context, parsed, stdin) {
2004
2936
  const action = requiredPosition(parsed, 0, "codex hook action");
2005
2937
  if (action === "handle") {
2006
2938
  const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
2007
- const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }) : context;
2939
+ const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }, "hook") : context;
2940
+ const proof = await verifyCodexHookDelivery(hookContext.env, JSON.parse(stdin || "{}"));
2941
+ if (proof)
2942
+ hookContext.codexHookProof = proof;
2008
2943
  const result = handleCodexHook(hookContext, {
2009
2944
  event: requiredOption(parsed, "event"),
2010
2945
  projectRoot: optionalOption(parsed, "project-root"),
@@ -2027,6 +2962,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
2027
2962
  }
2028
2963
  throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
2029
2964
  }
2965
+ function dispatchZcodeHook(context, parsed, stdin) {
2966
+ if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
2967
+ throw new AppError("usage", "Unknown zcode hook action", 2);
2968
+ const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
2969
+ if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
2970
+ return { hookSpecificOutput: result.hookSpecificOutput };
2971
+ }
2972
+ return {};
2973
+ }
2030
2974
  function dispatchGrokEvent(context, parsed, stdin) {
2031
2975
  const action = requiredPosition(parsed, 0, "grok event action");
2032
2976
  if (action === "handle") {
@@ -2050,7 +2994,7 @@ function dispatchAgyEvent(context, parsed, stdin) {
2050
2994
  return handleAgyEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
2051
2995
  throw new AppError("usage", `Unknown agy event action: ${action}`, 2);
2052
2996
  }
2053
- function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
2997
+ function dispatchWorktree(context, command, parsed, scopeProjectRoot, prepared) {
2054
2998
  if (command === "plan") {
2055
2999
  return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
2056
3000
  }
@@ -2061,13 +3005,13 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
2061
3005
  branch: requiredOption(parsed, "branch"),
2062
3006
  base: requiredOption(parsed, "base"),
2063
3007
  path: requiredOption(parsed, "path")
2064
- });
3008
+ }, prepared.worktreeCreate);
2065
3009
  }
2066
3010
  if (command === "status") {
2067
3011
  return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
2068
3012
  }
2069
3013
  if (command === "bootstrap") {
2070
- return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
3014
+ return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot }, prepared.worktreeBootstrap);
2071
3015
  }
2072
3016
  if (command === "close") {
2073
3017
  return closeWorktree(context, {
@@ -2079,52 +3023,50 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
2079
3023
  throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
2080
3024
  }
2081
3025
  function dispatchPlanItem(context, parsed, projectRoot) {
3026
+ return updatePlanItem(context, planItemInput(parsed, projectRoot));
3027
+ }
3028
+ function planItemInput(parsed, projectRoot) {
2082
3029
  const action = requiredPosition(parsed, 0, "plan item action");
2083
3030
  const protocolId = requiredPosition(parsed, 1, "protocol-id");
2084
3031
  const itemId = requiredPosition(parsed, 2, "item-id");
2085
3032
  if (action === "start") {
2086
- return startPlanItem(context, { protocolId, projectRoot, itemId });
3033
+ return { protocolId, projectRoot, itemId, action };
2087
3034
  }
2088
3035
  if (action === "done") {
2089
- return completePlanItem(context, {
3036
+ return {
3037
+ action,
2090
3038
  protocolId,
2091
3039
  projectRoot,
2092
3040
  itemId,
2093
3041
  summary: requiredOption(parsed, "summary"),
2094
3042
  evidence: parsed.options.get("evidence") ?? []
2095
- });
3043
+ };
2096
3044
  }
2097
3045
  if (action === "block") {
2098
- return blockPlanItem(context, {
3046
+ return {
3047
+ action,
2099
3048
  protocolId,
2100
3049
  projectRoot,
2101
3050
  itemId,
2102
3051
  reason: requiredOption(parsed, "reason"),
2103
3052
  userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
2104
- });
3053
+ };
2105
3054
  }
2106
3055
  if (action === "skip") {
2107
- return skipPlanItem(context, {
3056
+ return {
3057
+ action,
2108
3058
  protocolId,
2109
3059
  projectRoot,
2110
3060
  itemId,
2111
3061
  reason: requiredOption(parsed, "reason")
2112
- });
3062
+ };
2113
3063
  }
2114
3064
  throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
2115
3065
  }
2116
3066
  function readOptionalJsonFile(file) {
2117
3067
  if (!file)
2118
3068
  return undefined;
2119
- if (!fs.existsSync(file)) {
2120
- throw new AppError("not_found", `JSON file does not exist: ${file}`, 1);
2121
- }
2122
- try {
2123
- return JSON.parse(fs.readFileSync(file, "utf8"));
2124
- }
2125
- catch {
2126
- throw new AppError("validation", `JSON file is malformed: ${file}`, 2);
2127
- }
3069
+ return prepareJsonFile(file, "JSON input").value;
2128
3070
  }
2129
3071
  function parseFlowFlagOverrides(values) {
2130
3072
  const result = {};
@@ -2159,7 +3101,11 @@ function requiredPosition(parsed, index, label) {
2159
3101
  function optionalOption(parsed, key) {
2160
3102
  const values = parsed.options.get(key);
2161
3103
  const value = values?.[values.length - 1];
2162
- return value === flagValue ? undefined : value;
3104
+ if (value === flagValue)
3105
+ throw new AppError("usage", `--${key} requires a value`, 2);
3106
+ if (value !== undefined && !value.trim())
3107
+ throw new AppError("usage", `--${key} requires a non-empty value`, 2);
3108
+ return value;
2163
3109
  }
2164
3110
  function allOptions(parsed, key) {
2165
3111
  return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
@@ -2324,7 +3270,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2324
3270
  if (family === "merge-queue") {
2325
3271
  return projectRootForMergeQueueMutation(context, command, parsed);
2326
3272
  }
2327
- if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
3273
+ if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
2328
3274
  return requiredOption(parsed, "project-root");
2329
3275
  }
2330
3276
  if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
@@ -2357,7 +3303,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2357
3303
  if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
2358
3304
  return requiredOption(parsed, "project-root");
2359
3305
  }
2360
- if (family === "agy" && ((command === "event" && requiredPosition(parsed, 0, "agy event action") === "handle") || command === "usage"))
3306
+ if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
2361
3307
  return requiredOption(parsed, "project-root");
2362
3308
  if (family === "codex" && command === "hooks") {
2363
3309
  const action = requiredPosition(parsed, 0, "codex hooks action");
@@ -2394,14 +3340,36 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
2394
3340
  function protocolProjectRoot(context, protocolId) {
2395
3341
  return requireProtocol(context, protocolId).project_root;
2396
3342
  }
3343
+ function prepareCliWorkRepair(context, parsed, objective, scopeProjectRoot) {
3344
+ const origins = parsed.options.get("origin-work") ?? [], findings = parsed.options.get("from-finding") ?? [], unresolved = parsed.options.get("from-unresolved") ?? [], receipts = parsed.options.get("from-check") ?? [];
3345
+ if ([findings.length > 0, unresolved.length > 0, receipts.length > 0].filter(Boolean).length !== 1)
3346
+ throw new AppError("usage", "Repair requires exactly one source: --from-check, --from-finding or --from-unresolved", 2);
3347
+ const scope = { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"), runId: requiredOption(parsed, "run"), objective };
3348
+ if (findings.length) {
3349
+ if (origins.length || hasOption(parsed, "verification-file"))
3350
+ throw new AppError("usage", "Finding repair derives its origins and cannot accept --origin-work or --verification-file", 2);
3351
+ return prepareVnextCodeReviewRepair(context, { ...scope, findingIds: findings, checkRefsByFinding: Object.fromEntries(findings.map(finding => [finding, parsed.options.get("check-ref") ?? []])) });
3352
+ }
3353
+ if (!origins.length)
3354
+ throw new AppError("usage", "Repair requires at least one --origin-work", 2);
3355
+ if (hasOption(parsed, "check-ref"))
3356
+ throw new AppError("usage", "--check-ref is only supported with --from-finding", 2);
3357
+ if (receipts.length > 1)
3358
+ throw new AppError("usage", "Create one repair Work per --from-check receipt", 2);
3359
+ if (!unresolved.length && hasOption(parsed, "verification-file"))
3360
+ throw new AppError("usage", "--verification-file is only supported with --from-unresolved", 2);
3361
+ const input = { ...scope, originWorkIds: origins, ...(unresolved.length ? { semanticUnresolved: unresolved, verificationPath: requiredOption(parsed, "verification-file") } : { checkReceiptId: receipts[0] }) };
3362
+ return { input, value: prepareVnextCodeRepair(context, input) };
3363
+ }
3364
+ function workerPromptInput(parsed) {
3365
+ const workspaceRoot = optionalOption(parsed, "workspace-root"), taskFile = optionalOption(parsed, "task-file");
3366
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), stage: requiredOption(parsed, "stage"), profile: requiredOption(parsed, "profile"), ...(taskFile ? { taskFile } : { planItemId: requiredOption(parsed, "plan-item") }), ...(workspaceRoot ? { workspaceRoot } : {}) };
3367
+ }
2397
3368
  function runnerIntake(context) {
2398
3369
  const input = context.value.task_input?.[0];
2399
3370
  if (!input?.path)
2400
3371
  throw new AppError("validation", "Runner SPECIFY context requires one ordered task_input file", 2);
2401
- const file = path.resolve(input.path);
2402
- if (!fs.existsSync(file) || !fs.statSync(file).isFile())
2403
- throw new AppError("not_found", "Runner SPECIFY task input is missing", 2, { task_input: file });
2404
- return fs.readFileSync(file, "utf8");
3372
+ return prepareTextFile(input.path, "Runner SPECIFY task input").text;
2405
3373
  }
2406
3374
  /** Stage services install context before their lifecycle transition. Keeping
2407
3375
  * this check here makes an incomplete service migration fail closed instead of