@deksden-com/dd-flow-cli 0.9.0-beta.10 → 0.9.0-beta.100

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 (146) hide show
  1. package/CHANGELOG.md +491 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1437 -384
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/schemas/agent-profile.schema.json +1 -1
  52. package/dist/schemas/code-review-result.schema.json +1 -1
  53. package/dist/schemas/code-work-batch.schema.json +3 -3
  54. package/dist/schemas/code-work-result.schema.json +3 -3
  55. package/dist/schemas/plan-review-result.schema.json +2 -2
  56. package/dist/schemas/run-control-receipt.schema.json +99 -0
  57. package/dist/schemas/run-control-request.schema.json +36 -0
  58. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  59. package/dist/services/canon.js +11 -8
  60. package/dist/services/cleanup.js +57 -13
  61. package/dist/services/cli-operation-classifier.js +22 -24
  62. package/dist/services/code-checks.js +543 -96
  63. package/dist/services/codex-hook-delivery.js +28 -0
  64. package/dist/services/command-context.js +80 -0
  65. package/dist/services/config.js +11 -8
  66. package/dist/services/continuation-outcome.js +20 -0
  67. package/dist/services/controller-fanout.js +134 -0
  68. package/dist/services/dashboard.js +103 -31
  69. package/dist/services/engines.js +121 -76
  70. package/dist/services/eval-snapshots.js +832 -73
  71. package/dist/services/execution-policy.js +170 -0
  72. package/dist/services/external-work-launch.js +110 -0
  73. package/dist/services/harness-adapter.js +147 -24
  74. package/dist/services/harness-config.js +2 -0
  75. package/dist/services/hooks.js +596 -300
  76. package/dist/services/lanes.js +61 -53
  77. package/dist/services/lifecycle-command.js +103 -17
  78. package/dist/services/lifecycle-contract.js +30 -0
  79. package/dist/services/lifecycle-invocations.js +1128 -0
  80. package/dist/services/managed-daemon-binding.js +38 -0
  81. package/dist/services/managed-processes.js +283 -27
  82. package/dist/services/merge-queue.js +179 -104
  83. package/dist/services/merge-server.js +73 -28
  84. package/dist/services/migrations.js +14 -9
  85. package/dist/services/native-daemon-history.js +49 -0
  86. package/dist/services/native-session-control.js +39 -0
  87. package/dist/services/plan-runtime.js +19 -8
  88. package/dist/services/plans.js +28 -25
  89. package/dist/services/projects.js +18 -5
  90. package/dist/services/prompts.js +23 -16
  91. package/dist/services/protocols.js +64 -20
  92. package/dist/services/recovery-observation-budget.js +61 -0
  93. package/dist/services/recovery-snapshot-database.js +107 -0
  94. package/dist/services/repair-intents.js +40 -0
  95. package/dist/services/run-control-receipt.js +56 -0
  96. package/dist/services/run-control-worker.js +363 -0
  97. package/dist/services/run-control.js +876 -0
  98. package/dist/services/run-controller-adapter.js +222 -0
  99. package/dist/services/run-controller-capture.js +136 -0
  100. package/dist/services/run-controller-process.js +189 -0
  101. package/dist/services/run-controller-recovery.js +222 -0
  102. package/dist/services/run-controller-state.js +36 -0
  103. package/dist/services/run-controller.js +803 -0
  104. package/dist/services/run-engine-bindings.js +1 -1
  105. package/dist/services/run-fork.js +138 -0
  106. package/dist/services/run-observations.js +112 -0
  107. package/dist/services/run-recovery-runtime.js +69 -0
  108. package/dist/services/run-recovery.js +343 -0
  109. package/dist/services/runs.js +368 -74
  110. package/dist/services/runtime-budget.js +439 -0
  111. package/dist/services/runtime-command.js +43 -0
  112. package/dist/services/runtime-scope-capture.js +50 -0
  113. package/dist/services/runtime-scope-control.js +450 -0
  114. package/dist/services/runtime-scope-resume.js +565 -0
  115. package/dist/services/runtime-scope-stop.js +99 -0
  116. package/dist/services/runtime-scope-worker.js +243 -0
  117. package/dist/services/runtime-service.js +97 -0
  118. package/dist/services/schema-validation.js +10 -8
  119. package/dist/services/sessions.js +35 -69
  120. package/dist/services/stage-blocker.js +17 -7
  121. package/dist/services/stage-context.js +44 -18
  122. package/dist/services/stage-lifecycle.js +99 -102
  123. package/dist/services/stage-pause.js +84 -55
  124. package/dist/services/stage-work-graph.js +35 -0
  125. package/dist/services/usage.js +37 -93
  126. package/dist/services/vnext-code-review.js +185 -79
  127. package/dist/services/vnext-code.js +282 -248
  128. package/dist/services/vnext-execution-profile.js +5 -3
  129. package/dist/services/vnext-fanout.js +164 -17
  130. package/dist/services/vnext-merge.js +291 -104
  131. package/dist/services/vnext-plan-review.js +139 -81
  132. package/dist/services/vnext-plan.js +138 -69
  133. package/dist/services/vnext-protocolize.js +129 -56
  134. package/dist/services/vnext-specify.js +88 -65
  135. package/dist/services/work-registry.js +705 -162
  136. package/dist/services/workspace-bootstrap.js +76 -0
  137. package/dist/services/worktrees.js +85 -35
  138. package/dist/shared/entity-references.js +8 -0
  139. package/dist/shared/errors.js +12 -0
  140. package/dist/storage/database.js +459 -32
  141. package/dist/storage/paths.js +17 -4
  142. package/dist/storage/work-references.js +23 -0
  143. package/dist/storage/writer-contract.js +77 -0
  144. package/dist/storage/writer-migration.js +102 -0
  145. package/package.json +24 -13
  146. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,56 +1,82 @@
1
1
  import fs from "node:fs";
2
+ import { resolveCommandPaths, resolveCommandProjectRoot } from "../services/command-context.js";
3
+ import { assertAuxiliaryCommandScope } from "../services/lifecycle-invocations.js";
2
4
  import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { quote } from "shell-quote";
7
+ import { observeLifecycleCommand, observedLifecycleNativeEvent } from "../services/lifecycle-invocations.js";
8
+ import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
9
+ import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
3
10
  import { createContext, createRouterContext } from "../runtime/context.js";
11
+ import { migrateStoreWriter } from "../storage/writer-migration.js";
4
12
  import { helpForArgs } from "./help.js";
5
13
  import { AppError, isAppError } from "../shared/errors.js";
14
+ import { entityReferenceVariants } from "../shared/entity-references.js";
6
15
  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";
16
+ import { archiveProject, prepareProjectArchive, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
17
+ import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, prepareProtocolRegistration, requireProtocol, syncProtocolFromRun, prepareProtocolTransition, transitionProtocol } from "../services/protocols.js";
18
+ import { getPlanStatus, preparePlanItemMutation, updatePlanItem } from "../services/plans.js";
19
+ 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
20
  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";
21
+ import { cleanupApply, cleanupScan, prepareCleanupApply } from "../services/cleanup.js";
22
+ 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";
23
+ import { bootstrapWorktree, closeWorktree, createWorktreeRecord, prepareWorktreeCreate, prepareWorktreeBootstrap, prepareWorktreeClose, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
24
+ import { acquireLaneLock, cancelLaneWaiter, prepareLaneWaiterCancellation, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, prepareLaneWorkspace, prepareLaneLock, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
25
+ import { getProjectConfigStatus, prepareProjectConfigValue, setProjectConfigValue } from "../services/config.js";
26
+ import { getCanonStatus, registerCanonRoot, resolveCanonRoot, prepareCanonRegistration } from "../services/canon.js";
18
27
  import { getRuntimeStatus } from "../services/status.js";
19
28
  import { getCliVersionReport } from "../services/build-info.js";
20
29
  import { parseCommandArgs } from "../services/lifecycle-command.js";
21
- import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
30
+ import { prepareError, prepareJsonFile, prepareJsonSchema, prepareJsonText, prepareTextFile } from "./input-preparation.js";
31
+ import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, prepareDashboardRefresh, prepareDashboardRender, prepareGlobalDashboardRender, prepareDashboardOptions, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
22
32
  import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
23
- import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
33
+ import { getFlowSessionStatus, prepareFlowSessionPayload, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
24
34
  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";
35
+ 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
36
  import { previewNextEntityId } from "../services/ids.js";
27
37
  import { validateSchema } from "../services/schema-validation.js";
28
38
  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";
39
+ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
40
+ import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, prepareMigrationApply, verifyMigrationReport } from "../services/migrations.js";
31
41
  import { publishProjectSummary } from "../services/project-summary.js";
32
42
  import { requireProjectByRoot } from "../services/projects.js";
33
43
  import { resolveProjectRoot } from "../storage/paths.js";
34
- import { doctorEngines, bindRunEngine, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
44
+ import { doctorEngines, bindRunEngine, prepareRunEngineBind, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
35
45
  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";
46
+ import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify, prepareVnextSpecifyStart } from "../services/vnext-specify.js";
47
+ import { finishVnextProtocolize, isVnextProtocolizeRun, prepareProtocolizeResultPath, prepareVnextProtocolizeStart, startVnextProtocolize } from "../services/vnext-protocolize.js";
48
+ import { finishVnextPlan, isVnextPlanRun, startVnextPlan, prepareVnextPlanStart, prepareVnextPlanFinish } from "../services/vnext-plan.js";
49
+ import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, prepareVnextPlanReviewStart, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
50
+ import { addVnextCodeRepair, finishVnextCode, prepareVnextCodeRepair, prepareVnextCodeStart, startVnextCode } from "../services/vnext-code.js";
51
+ import { finishVnextCodeReview, isVnextCodeReviewRun, prepareCodeReviewDecision, prepareVnextCodeReviewRepair, prepareVnextCodeReviewStart, startVnextCodeReview } from "../services/vnext-code-review.js";
52
+ import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, prepareVnextMergeFinish, prepareVnextMergeStart, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
43
53
  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";
54
+ import { getVnextFanoutStatus, observeVnextNativeChildren, prepareNativeObservations, reconcileVnextFanout } from "../services/vnext-fanout.js";
55
+ import { pauseStageForUser, resumeStageAfterUser, prepareStagePause, prepareStageResume } from "../services/stage-pause.js";
56
+ import { blockStageForRuntime, unblockStageAfterRuntimeRepair, prepareStageRuntimeBlock, prepareStageRuntimeUnblock } from "../services/stage-blocker.js";
47
57
  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";
58
+ import { prepareWorkerPrompt, renderWorkerPrompt } from "../services/prompts.js";
59
+ import { finishStage, prepareStageFinish, startStage, prepareLegacyStageStart } from "../services/stage-lifecycle.js";
60
+ import { addWorkBatch, prepareWorkBatch, prepareWorkDeps, prepareWorkDelete, prepareWorkSettlement, prepareWorkRetry, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, prepareWorkResult, retryWork, showWork, startWork } from "../services/work-registry.js";
61
+ import { launchExternalWork } from "../services/external-work-launch.js";
62
+ import { createEvalBootstrapSnapshot, prepareEvalBootstrapSnapshot, createEvalRunSnapshot, prepareEvalRunSnapshotCreation, prepareVnextSpecifyRun, prepareEvalRunRestore, prepareEvalBootstrapRestore, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
63
+ import { forkRunFromSnapshot, prepareRunFork } from "../services/run-fork.js";
64
+ import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
65
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
66
+ import { loadExternalStageContext, prepareExternalStageContext } from "../services/stage-context.js";
67
+ import { vnextStageDirectory } from "../domain/stage-catalog.js";
68
+ import { prepareCommandRoute } from "./command-inputs.js";
69
+ import { parseLifecycleCommand } from "../services/lifecycle-command.js";
70
+ import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
71
+ import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, prepareRuntimeScopeControl, prepareRuntimeScopeFence, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
72
+ import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
73
+ import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
74
+ import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
75
+ import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
76
+ import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
77
+ import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
78
+ import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
79
+ import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
54
80
  const defaultIo = {
55
81
  stdout: process.stdout,
56
82
  stderr: process.stderr,
@@ -58,9 +84,36 @@ const defaultIo = {
58
84
  };
59
85
  const flagValue = "__dd_flow_flag__";
60
86
  export async function runCli(args, io = defaultIo, env = process.env) {
61
- const output = parseOutputOptions(args);
87
+ let output;
88
+ try {
89
+ output = parseOutputOptions(args);
90
+ }
91
+ catch (error) {
92
+ const rejection = isAppError(error) ? error : new AppError("usage", error instanceof Error ? error.message : String(error), 2);
93
+ Object.assign(rejection.details, { phase: "prepare", effect: "no_effect", recoverable: true, ...rejection.details });
94
+ try {
95
+ const command = quote(["dd-flow", ...args]), event = observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID);
96
+ if (event) {
97
+ const context = createContext({ ...env }, "hook");
98
+ try {
99
+ observeLifecycleCommand(context, command, event)({ error: rejection });
100
+ }
101
+ finally {
102
+ context.db.close?.();
103
+ }
104
+ }
105
+ }
106
+ catch (persistence) {
107
+ rejection.details.lifecycle_diagnostic_error = String(persistence);
108
+ }
109
+ writeJson(io.stderr, { ok: false, error: { code: rejection.code, message: rejection.message, details: rejection.details } });
110
+ return rejection.exitCode;
111
+ }
62
112
  const progress = progressForCommand(output.args);
63
113
  let heartbeat;
114
+ let invocationSettlement;
115
+ let settleHook;
116
+ let diagnosticHome;
64
117
  try {
65
118
  if (output.args.length === 1 && output.args[0] === "--version") {
66
119
  const payload = getCliVersionReport();
@@ -75,6 +128,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
75
128
  io.stdout.write(`${help}\n`);
76
129
  return 0;
77
130
  }
131
+ const suppliedInvocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
132
+ if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
133
+ const controlArgs = parseArgs(output.args.slice(2));
134
+ requiredOption(controlArgs, "run");
135
+ requiredOption(controlArgs, "project-root");
136
+ }
137
+ if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
138
+ && hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
139
+ 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);
140
+ }
78
141
  const classification = classifyCliOperation(output.args, env);
79
142
  if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
80
143
  throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
@@ -83,25 +146,143 @@ export async function runCli(args, io = defaultIo, env = process.env) {
83
146
  });
84
147
  }
85
148
  const routerContext = createRouterContext(env);
149
+ const publicCommand = quote(["dd-flow", ...output.args]);
150
+ const lifecycleCommand = parseLifecycleCommand(publicCommand);
151
+ if (lifecycleCommand.kind === "standalone")
152
+ validateLifecyclePathAliases(output.args, lifecycleCommand.invocation.operation);
153
+ const observedInvocation = observedLifecycleInvocation(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID);
154
+ if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
155
+ const diagnostic = observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID);
156
+ if (diagnostic)
157
+ throw new AppError("lifecycle_shell_syntax_invalid", `Native observer rejected the shell form: ${String(diagnostic.reason ?? "unsupported shell composition")}. Write structured input to a file and run the returned dd-flow command separately.`, 2, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: true });
158
+ const nativeEvent = observedLifecycleNativeEvent(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID);
159
+ throw new AppError(nativeEvent ? "invocation_assignment_missing" : "invocation_receipt_missing", nativeEvent ? "Native call was observed, but its arguments do not match an issued lifecycle assignment" : "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
160
+ }
161
+ const invocationId = observedInvocation?.id ?? suppliedInvocationId;
162
+ let boundInvocationArgs;
163
+ try {
164
+ boundInvocationArgs = observedInvocation
165
+ ? resolveObservedLifecycleArgs(routerContext, observedInvocation.id, output.args)
166
+ : invocationId && invocationId !== suppliedInvocationId
167
+ ? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
168
+ : output.args;
169
+ }
170
+ catch (error) {
171
+ if (!(observedInvocation && error instanceof AppError))
172
+ throw error;
173
+ const rejectionContext = createContext({ ...env }, "hook");
174
+ try {
175
+ Object.assign(error.details, settleLifecyclePreparationRejection(rejectionContext, observedInvocation.id, error));
176
+ }
177
+ finally {
178
+ rejectionContext.db.close?.();
179
+ }
180
+ throw error;
181
+ }
182
+ const observedOperation = lifecycleCommand.kind === "standalone" ? lifecycleCommand.invocation.operation : null;
183
+ if (observedInvocation && !observedOperation)
184
+ throw new AppError("invocation_command_invalid", "Observed lifecycle command could not be parsed", 1);
185
+ const invocationScope = invocationId ? lifecycleInvocationScope({ ...routerContext, env }, invocationId) : undefined;
186
+ const expandedInvocationArgs = invocationScope && observedOperation
187
+ ? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
188
+ : boundInvocationArgs;
189
+ const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
190
+ if (retainedOutput?.responseFile)
191
+ output.responseFile = retainedOutput.responseFile;
192
+ let invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
86
193
  emitHumanProgress(io, output, progress, "start");
87
194
  if (output.progressJsonl) {
88
195
  writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
89
196
  heartbeat = setInterval(() => writeProgressJsonl(io, "running", "Command is still running"), 15_000);
90
197
  }
91
- const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
92
- const routedArgs = output.responseFile ? [...output.args, "--response-file", output.responseFile] : output.args;
198
+ // Route by retained authority, so a mistyped project path reaches the CLI
199
+ // validator instead of selecting a different engine or failing preflight.
200
+ const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env));
201
+ assertAuxiliaryCommandScope({ ...routerContext, env }, publicCommand, scopeProjectRoot);
202
+ invocationArgs = resolveCommandPaths(routerContext, invocationArgs, scopeProjectRoot);
203
+ const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
93
204
  const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
94
205
  if (routed?.routed && !routed.inProcess) {
95
206
  if (heartbeat)
96
207
  clearInterval(heartbeat);
97
- emitHumanProgress(io, output, progress, "done");
208
+ emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
209
+ if (output.progressJsonl && routed.exitCode !== 0)
210
+ writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
98
211
  return routed.exitCode;
99
212
  }
213
+ let preparedInput;
214
+ let preparedRoute;
215
+ if (!invocationId) {
216
+ const preparationContext = createContext({ ...env }, "read_existing");
217
+ try {
218
+ preparedInput = await prepareCliInput(invocationArgs, preparationContext, io, scopeProjectRoot, output.responseFile);
219
+ }
220
+ finally {
221
+ preparationContext.db.close?.();
222
+ }
223
+ }
100
224
  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);
225
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification))
226
+ : createContext({ ...env }, databaseModeForCommand(invocationArgs, classification));
227
+ diagnosticHome = dispatchContext.ddFlowHome;
228
+ dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
229
+ let dispatchArgs = invocationArgs;
230
+ let retainedOutcome;
231
+ // Issued transports may deliver their receipt asynchronously. Observe only
232
+ // after admission; retained outcomes must not need a fresh hook receipt.
233
+ if (invocationId) {
234
+ const command = quote(["dd-flow", ...invocationArgs]);
235
+ const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
236
+ validateArgs: () => {
237
+ try {
238
+ preparedRoute = prepareCommandRoute(invocationArgs);
239
+ }
240
+ catch (error) {
241
+ if (error instanceof AppError && error.code === "usage" && ["invocation-id", "hook-event-id"].includes(String(error.details.parameter ?? ""))) {
242
+ throw new AppError("lifecycle_contract_invalid", `The runtime generated arguments rejected by its own CLI contract: ${error.message}`, 1, { ...error.details, recoverable: false, operation: observedOperation });
243
+ }
244
+ throw error;
245
+ }
246
+ },
247
+ prepare: async () => { preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile, preparedRoute); },
248
+ assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
249
+ if (admission.replay) {
250
+ retainedOutcome = admission.outcome;
251
+ settleHook = undefined;
252
+ delete dispatchContext.observeLifecycleHook;
253
+ }
254
+ else {
255
+ invocationSettlement = { context: dispatchContext, id: invocationId };
256
+ dispatchArgs = [...invocationArgs, "--hook-event-id", admission.eventKey];
257
+ preparedInput?.route?.parsed.options.set("hook-event-id", [admission.eventKey]);
258
+ dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
259
+ dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
260
+ }
261
+ }
262
+ if (retainedOutcome?.error) {
263
+ const error = retainedOutcome.error;
264
+ throw new AppError(error.code, error.message, error.exitCode, error.details);
265
+ }
266
+ if (!retainedOutcome && !preparedInput)
267
+ preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile);
268
+ if (!retainedOutcome)
269
+ preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
270
+ if (!retainedOutcome) {
271
+ const command = lifecycleCommand.kind === "none" ? publicCommand : quote(["dd-flow", ...dispatchArgs]);
272
+ const event = lifecycleCommand.kind === "none" && env.DD_FLOW_DAEMON_ID ? observedLifecycleNativeEvent(dispatchContext, command, env.DD_FLOW_DAEMON_ID) : null;
273
+ settleHook = observeLifecycleCommand(dispatchContext, command, event ?? undefined);
274
+ }
275
+ const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output, preparedInput);
276
+ const completedHook = settleHook;
277
+ settleHook = undefined;
278
+ completedHook?.({ result });
279
+ if (invocationSettlement) {
280
+ const completed = invocationSettlement;
281
+ invocationSettlement = undefined;
282
+ // A failure to persist an already completed result is an unknown reply,
283
+ // not a failed operation eligible for a new productive retry.
284
+ settleLifecycleInvocation(completed.context, completed.id, { result });
285
+ }
105
286
  const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
106
287
  const resultWithWarnings = addWarnings(result, refreshWarnings);
107
288
  if (heartbeat)
@@ -117,17 +298,76 @@ export async function runCli(args, io = defaultIo, env = process.env) {
117
298
  io.stdout.write(renderHumanResult(output.args, payload));
118
299
  return completedCommandExitCode(payload);
119
300
  }
120
- catch (error) {
301
+ catch (caught) {
302
+ 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 } });
303
+ if (!settleHook && !invocationSettlement) {
304
+ let earlyContext;
305
+ try {
306
+ const command = quote(["dd-flow", ...output.args]);
307
+ const event = observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID);
308
+ if (event) {
309
+ earlyContext = createContext({ ...env }, "hook");
310
+ observeLifecycleCommand(earlyContext, command, event)({ error });
311
+ }
312
+ }
313
+ catch (persistence) {
314
+ error.details.lifecycle_diagnostic_error = String(persistence);
315
+ }
316
+ finally {
317
+ earlyContext?.db.close?.();
318
+ }
319
+ }
320
+ if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
321
+ try {
322
+ const directory = path.join(diagnosticHome, "diagnostics", "cli");
323
+ fs.mkdirSync(directory, { recursive: true });
324
+ const artifact = path.join(directory, `${randomUUID()}.json`);
325
+ 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 });
326
+ error.details.diagnostic_artifact = artifact;
327
+ }
328
+ catch (secondary) {
329
+ io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
330
+ }
331
+ }
121
332
  if (heartbeat)
122
333
  clearInterval(heartbeat);
334
+ if (invocationSettlement && error.details.effect !== "committed") {
335
+ try {
336
+ if (isAppError(error))
337
+ settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
338
+ else
339
+ settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
340
+ }
341
+ catch (persistenceError) {
342
+ io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
343
+ }
344
+ }
345
+ if (settleHook) {
346
+ try {
347
+ settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
348
+ }
349
+ catch (persistence) {
350
+ io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
351
+ }
352
+ }
123
353
  const payload = isAppError(error)
124
354
  ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
125
355
  : { ok: false, error: { code: "unexpected", message: String(error) } };
356
+ progress.splice(0, progress.length, ...progress.filter(event => event.phase !== "done"));
357
+ if (progress.length)
358
+ progress.push({ phase: "failed", message: payload.error.message, at: new Date().toISOString() });
126
359
  emitHumanProgress(io, output, progress, "failed");
127
360
  if (output.progressJsonl)
128
361
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
129
362
  const errorPayload = attachProgress(payload, progress, output);
130
- const visible = writeResponseFile(errorPayload, output);
363
+ let visible = errorPayload;
364
+ try {
365
+ if (error.details.parameter !== "response-file")
366
+ visible = writeResponseFile(errorPayload, output);
367
+ }
368
+ catch (publication) {
369
+ error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
370
+ }
131
371
  if (output.json && output.progressJsonl)
132
372
  writeJson(io.stdout, errorPayload);
133
373
  else if (output.json)
@@ -151,7 +391,22 @@ function resolveProjectRootBeforeRouting(args, env) {
151
391
  }
152
392
  function databaseModeForCommand(args, classification) {
153
393
  const [family, command] = args;
394
+ // Managed attempts are issued only from a prepared store. Opening that
395
+ // existing writer must not run migrations before their input is admitted.
396
+ if (parseArgs(args.slice(2)).options.has("invocation-id"))
397
+ return "hook";
398
+ // Native hook ingress must remain synchronous, but it must not run schema
399
+ // migration/VACUUM while the provider is waiting for its decision. Startup
400
+ // prepares the store; the hook mode only verifies the prepared contract and
401
+ // records the receipt.
402
+ if (family === "codex" && command === "hook" && args[2] === "handle") {
403
+ // Only the participating PreToolUse ingress needs the prepared hot-path
404
+ // contract. SessionStart/Stop/PostToolUse are observational no-ops here
405
+ // and retain the ordinary startup path for backwards compatibility.
406
+ return optionalOption(parseArgs(args.slice(3)), "event") === "PreToolUse" ? "hook" : "initialize";
407
+ }
154
408
  if (family === "engine" ||
409
+ family === "storage" && command === "migrate-writer" ||
155
410
  family === "version" ||
156
411
  family === "schema" ||
157
412
  family === "canon" && command === "resolve" ||
@@ -207,8 +462,10 @@ function parseOutputOptions(args) {
207
462
  continue;
208
463
  }
209
464
  if (arg === "--response-file") {
465
+ if (responseFile !== null)
466
+ throw new AppError("usage", "--response-file may be supplied only once", 2);
210
467
  const file = args[++index];
211
- if (!file)
468
+ if (!file?.trim() || file.startsWith("--"))
212
469
  throw new AppError("usage", "--response-file requires a path", 2);
213
470
  responseFile = file;
214
471
  continue;
@@ -220,6 +477,33 @@ function parseOutputOptions(args) {
220
477
  const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
221
478
  return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
222
479
  }
480
+ /** Validate the response destination without creating it. The response is an
481
+ * optional publication, but a known-bad destination should never be found
482
+ * only after a successful lifecycle mutation. */
483
+ function prepareResponseDestination(value) {
484
+ const file = path.resolve(value);
485
+ const parent = path.dirname(file);
486
+ try {
487
+ let ancestor = parent;
488
+ for (;;) {
489
+ try {
490
+ if (!fs.statSync(ancestor).isDirectory())
491
+ throw new AppError("response_file_parent_invalid", "--response-file parent must be a directory", 2, { file, parent });
492
+ break;
493
+ }
494
+ catch (error) {
495
+ if (error.code !== "ENOENT" || path.dirname(ancestor) === ancestor)
496
+ throw error;
497
+ ancestor = path.dirname(ancestor);
498
+ }
499
+ }
500
+ if (fs.existsSync(file) && !fs.statSync(file).isFile())
501
+ throw new AppError("response_file_invalid", "--response-file must name a regular file", 2, { file });
502
+ }
503
+ catch (error) {
504
+ return prepareError(error, { parameter: "response-file", file, parent });
505
+ }
506
+ }
223
507
  /** Persist the complete machine response for harnesses that cap tool output.
224
508
  * The visible response keeps routing fields but omits the potentially large
225
509
  * rendered prompt; an agent can safely read the named file without rerunning
@@ -500,27 +784,650 @@ function stringValue(value) {
500
784
  function shortSha(value) {
501
785
  return value ? value.slice(0, 12) : "unknown";
502
786
  }
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);
787
+ /** All caller supplied file/stdin data is consumed here, before a managed
788
+ * invocation is allowed to claim execution or a stage claims its hook event. */
789
+ async function prepareCliInput(...args) {
790
+ try {
791
+ return await prepareCliInputValue(...args);
792
+ }
793
+ catch (error) {
794
+ return prepareError(error);
795
+ }
796
+ }
797
+ async function prepareCliInputValue(args, context, io, scopeProjectRoot, responseFile = null, route = prepareCommandRoute(args)) {
798
+ const { family, command, parsed } = route;
799
+ const prepared = { route };
800
+ if (optionalOption(parsed, "reason-file")) {
801
+ const value = prepareJsonFile(requiredOption(parsed, "reason-file"), "Check recovery evidence").value;
802
+ if (!value || typeof value.reason !== "string" || !value.reason.trim() || Object.keys(value).some(key => key !== "reason"))
803
+ throw new AppError("validation", "Recovery evidence must be a JSON object with one nonempty reason", 2);
804
+ prepared.retryReason = value.reason;
805
+ }
806
+ else if (optionalOption(parsed, "retry-check"))
807
+ prepared.retryReason = optionalOption(parsed, "reason");
808
+ if (route.key === "project register")
809
+ resolveProjectRoot(requiredOption(parsed, "root"));
810
+ if (route.key === "project archive")
811
+ prepareProjectArchive(context, { idOrAlias: parsed.positional[0], root: optionalOption(parsed, "root"), reason: requiredOption(parsed, "reason") });
812
+ if (route.key === "protocol register")
813
+ prepareProtocolRegistration(context, { handshakeId: requiredPosition(parsed, 0, "handshake-id"), projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "workspace-path") });
814
+ if (route.key === "canon register")
815
+ prepared.canonRegistration = prepareCanonRegistration(context, { root: requiredOption(parsed, "root") });
816
+ if (route.key === "run attach-stage")
817
+ prepareFlowRunStageAttachment(context, stageAttachmentInput(parsed));
818
+ if (route.key === "run complete-stage")
819
+ prepareFlowRunStageCompletion(context, stageCompletionInput(parsed));
820
+ if (route.key === "run override")
821
+ prepareFlowRunCompletion(context, runOverrideInput(parsed));
822
+ if (route.key === "engine bind-run")
823
+ prepared.engineBind = prepareRunEngineBind(context, engineBindInput(parsed));
824
+ if (route.key === "worktree create")
825
+ 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") });
826
+ if (route.key === "worktree bootstrap")
827
+ prepared.worktreeBootstrap = prepareWorktreeBootstrap(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree bootstrap") });
828
+ if (route.key === "worktree close")
829
+ prepareWorktreeClose(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree close"), mode: requiredOption(parsed, "mode") });
830
+ if (route.key.startsWith("plan item "))
831
+ preparePlanItemMutation(context, planItemInput(parsed, requiredScopeProjectRoot(scopeProjectRoot, route.key)));
832
+ if (route.key === "lane workspace set")
833
+ prepareLaneWorkspace(context, laneWorkspaceInput(parsed));
834
+ if (route.key === "lane waiter cancel")
835
+ prepareLaneWaiterCancellation(context, laneWaiterCancelInput(parsed));
836
+ if (route.key === "merge-queue complete")
837
+ prepareMergeJobCompletion(context, { ...mergeJobTarget(parsed, scopeProjectRoot), summary: requiredOption(parsed, "summary") });
838
+ if (route.key === "merge-queue cancel")
839
+ prepareMergeQueueCancellation(context, mergeCancellationInput(parsed, scopeProjectRoot));
840
+ if (route.key === "merge-queue wait-next")
841
+ prepareMergeQueueWait(context, mergeWaitInput(parsed, scopeProjectRoot));
842
+ if (route.key === "merge-queue next" || route.key === "merge-queue claim")
843
+ 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") } : {}) });
844
+ if (route.key === "merge-queue note")
845
+ prepareMergeJobNote(context, { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue note"), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
846
+ if (route.key === "merge bundle complete")
847
+ prepareMergeBundleCompletion(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
848
+ if (route.key === "merge bundle claim")
849
+ prepareMergeBundleClaim(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed) });
850
+ if (route.key === "merge bundle fail")
851
+ prepareMergeBundleFailure(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
852
+ if (route.key === "merge-queue fail")
853
+ prepareMergeJobFailure(context, { ...mergeJobTarget(parsed, scopeProjectRoot), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
854
+ if (route.key.startsWith("lane lock ") && route.key !== "lane lock status") {
855
+ const action = requiredPosition(parsed, 0, "lane lock action");
856
+ prepareLaneLock(context, {
857
+ projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"),
858
+ workspacePath: workspacePathOption(parsed), leaseToken: optionalOption(parsed, "lease-token"), reason: optionalOption(parsed, "reason"),
859
+ ttlSeconds: optionalNumber(parsed, "ttl"), timeoutSeconds: optionalNumber(parsed, "timeout"), pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
860
+ }, action);
861
+ }
862
+ if (route.key === "run recovery begin")
863
+ prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "interruption-json"), "interruption");
864
+ if (route.key === "run recovery seal")
865
+ prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "settlement-json"), "settlement");
866
+ if (route.key === "run recovery capture")
867
+ prepared.recoveryCapture = prepareRecoveryCapture(context, recoveryCaptureInput(parsed));
868
+ if (route.key === "run recovery resume")
869
+ prepared.recoveryResume = prepareRunRecoveryResume(context, recoveryResumeInput(parsed));
870
+ if (route.key === "run drive answer")
871
+ prepared.controllerAnswer = prepareRunControllerAnswer(context, controllerAnswerInput(parsed));
872
+ if (route.key === "run drive launch")
873
+ prepared.controllerLaunch = prepareRunControllerLaunch(context, controllerLaunchInput(parsed));
874
+ if (route.key === "run drive context")
875
+ prepared.controllerContext = prepareRunControllerContext(context, controllerContextInput(parsed));
876
+ if (route.key === "protocol transition" || route.key === "transition")
877
+ prepared.protocolTransition = prepareProtocolTransition(context, protocolTransitionInput(parsed, scopeProjectRoot ?? resolveOperationProjectRoot(context, args)));
878
+ if (route.key === "codex home init")
879
+ prepared.codexHome = prepareCodexHome(context, codexHomeInput(parsed));
880
+ if (route.key === "codex home remove")
881
+ prepareCodexHomeRemoval(context, { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), mode: requiredOption(parsed, "mode") });
882
+ if (["codex hooks install", "codex hooks remove"].includes(route.key))
883
+ prepared.codexHooks = prepareCodexHooks(context, { projectRoot: requiredOption(parsed, "project-root"), target: optionalOption(parsed, "target"), profile: optionalOption(parsed, "profile"), yes: hasOption(parsed, "yes") });
884
+ if (route.key === "migration apply")
885
+ prepared.migrationApply = prepareMigrationApply(context, migrationApplyInput(parsed, classifyCliOperation(args, context.env)));
886
+ if (route.key === "migration verify")
887
+ prepared.migrationVerification = verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
888
+ if (responseFile)
889
+ prepareResponseDestination(responseFile);
890
+ if (family === "dashboard") {
891
+ const action = dashboardAction(command);
892
+ prepareDashboardOptions({ format: optionalOption(parsed, "format"), viewer: optionalOption(parsed, "viewer"), open: optionalOption(parsed, "open") });
893
+ 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") });
894
+ if (action === "refresh" && target.kind !== "global" && optionalOption(parsed, "output") !== undefined) {
895
+ throw new AppError("validation", "--output is supported only for global dashboard refresh; use dashboard render for a custom project/protocol output", 2, { parameter: "output" });
896
+ }
897
+ if (target.kind === "protocol")
898
+ requireProtocol(context, target.protocolId, target.project.id);
899
+ if (action !== "data") {
900
+ if (target.kind === "global" || target.kind === "all_projects") {
901
+ prepareGlobalDashboardRender(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? (command?.endsWith("-global") ? undefined : "html") });
902
+ }
903
+ else {
904
+ const format = optionalOption(parsed, "format") ?? (action === "open" || hasOption(parsed, "project") ? "html" : undefined);
905
+ const input = { projectRoot: target.project.root, format, ...(target.kind === "protocol" ? { protocol: target.protocolId } : {}) };
906
+ if (action === "refresh")
907
+ prepareDashboardRefresh(context, { ...input, open: optionalOption(parsed, "open") });
908
+ else if (action === "render")
909
+ prepareDashboardRender(context, { ...input, output: optionalOption(parsed, "output") });
910
+ else if (target.kind === "protocol") {
911
+ if (format !== "html")
912
+ throw new AppError("validation", "Protocol dashboard open supports --format html", 2, { protocol: target.protocolId });
913
+ prepareDashboardRender(context, input);
914
+ }
915
+ }
916
+ }
917
+ }
918
+ if (route.key === "prompt render") {
919
+ const input = workerPromptInput(parsed);
920
+ prepared.workerPrompt = { input, value: prepareWorkerPrompt(context, input) };
921
+ }
922
+ if (route.key === "run snapshot restore" || route.key === "run snapshot bootstrap restore") {
923
+ try {
924
+ const input = snapshotRestoreInput(parsed);
925
+ if (route.key === "run snapshot restore")
926
+ prepared.snapshotRestore = prepareEvalRunRestore(context, input);
927
+ else
928
+ prepared.bootstrapRestore = prepareEvalBootstrapRestore(input);
929
+ }
930
+ catch (error) {
931
+ return prepareError(error, { parameter: "snapshot/project-root" });
932
+ }
933
+ }
934
+ if (route.key === "run snapshot bootstrap create") {
935
+ try {
936
+ prepareEvalBootstrapSnapshot(context, { projectRoot: requiredOption(parsed, "project-root"), output: requiredOption(parsed, "output") });
937
+ }
938
+ catch (error) {
939
+ return prepareError(error, { parameter: "project-root/output" });
940
+ }
941
+ }
942
+ if (route.key === "run snapshot create") {
943
+ prepareEvalRunSnapshotCreation(context, snapshotCreateInput(parsed));
944
+ }
945
+ if (route.key === "run start") {
946
+ try {
947
+ const input = runStartInput(parsed);
948
+ prepared.runStart = { input, value: prepareFlowRunStart(context, input) };
949
+ }
950
+ catch (error) {
951
+ return prepareError(error, { parameter: "run start" });
952
+ }
953
+ }
954
+ if (route.key === "run fork") {
955
+ try {
956
+ prepared.runFork = prepareRunFork(runForkInput(parsed));
957
+ }
958
+ catch (error) {
959
+ return prepareError(error, { parameter: "from/output" });
960
+ }
961
+ }
962
+ if (["runtime scope fence", "runtime scope stop"].includes(route.key))
963
+ prepareRuntimeScopeFence(requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"));
964
+ if (route.key === "runtime scope resume")
965
+ prepareRuntimeScopeResumeRequest(context, runtimeScopeResumeInput(parsed));
966
+ if (route.key === "runtime scope reconcile")
967
+ prepareRuntimeScopeReconciliation(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id"));
968
+ if (route.key === "runtime scope serve")
969
+ prepareRuntimeScopeServing(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")));
970
+ if (route.key === "runtime scope control") {
971
+ try {
972
+ const raw = optionalOption(parsed, "manifest-json");
973
+ 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);
974
+ }
975
+ catch (error) {
976
+ return prepareError(error, { parameter: "scope-id/request-id/manifest-json" });
977
+ }
978
+ }
979
+ if (route.key === "runtime process register") {
980
+ prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
981
+ }
982
+ if (route.key === "runtime process heartbeat")
983
+ prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
984
+ if (route.key === "runtime process finish")
985
+ prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
986
+ if (route.key === "runtime process confirm")
987
+ prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
988
+ if (route.key === "runtime process stop")
989
+ prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
990
+ if (route.key === "runtime process reconcile")
991
+ prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
992
+ if (route.key === "runtime process release-turn") {
993
+ prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
994
+ }
995
+ if (route.key === "runtime process start") {
996
+ try {
997
+ prepareRuntimeService(context, runtimeServiceInput(parsed));
998
+ }
999
+ catch (error) {
1000
+ return prepareError(error, { parameter: "runtime process start" });
1001
+ }
1002
+ }
1003
+ if (family === "work" && command === "retry") {
1004
+ try {
1005
+ prepareWorkRetry(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
1006
+ }
1007
+ catch (error) {
1008
+ return prepareError(error, { parameter: "work-id/reason" });
1009
+ }
1010
+ }
1011
+ if (family === "work" && (command === "fail" || command === "cancel")) {
1012
+ try {
1013
+ prepareWorkSettlement(context, requiredPosition(parsed, 0, "work-id"), command === "fail" ? "failed" : "cancelled", requiredOption(parsed, "reason"));
1014
+ }
1015
+ catch (error) {
1016
+ return prepareError(error, { parameter: "work-id/reason" });
1017
+ }
1018
+ }
1019
+ if (family === "work" && (command === "deps" || command === "delete")) {
1020
+ try {
1021
+ if (command === "delete")
1022
+ prepareWorkDelete(context, requiredPosition(parsed, 0, "work-id"));
1023
+ else
1024
+ prepareWorkDeps(context, { workId: requiredPosition(parsed, 1, "work-id"), action: requiredPosition(parsed, 0, "action"), on: parsed.options.get("on") ?? [] });
1025
+ }
1026
+ catch (error) {
1027
+ return prepareError(error, { parameter: "work-id/on" });
1028
+ }
1029
+ }
1030
+ if (route.key === "cleanup apply") {
1031
+ const planFile = requiredOption(parsed, "plan-file");
1032
+ try {
1033
+ prepared.cleanup = prepareCleanupApply(context, { projectRoot: requiredOption(parsed, "project-root"), planFile, reason: requiredOption(parsed, "reason"), force: hasOption(parsed, "force") });
1034
+ }
1035
+ catch (error) {
1036
+ return prepareError(error, { parameter: "plan-file", file: planFile });
1037
+ }
1038
+ }
1039
+ if (route.key === "stage block" || route.key === "stage unblock") {
1040
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
1041
+ try {
1042
+ if (route.key === "stage block") {
1043
+ const summaryFile = optionalOption(parsed, "summary-file"), summaryStdin = hasOption(parsed, "summary-stdin");
1044
+ if (Boolean(summaryFile) === summaryStdin)
1045
+ throw new AppError("usage", "stage block requires exactly one of --summary-file or --summary-stdin", 2);
1046
+ prepared.interactionText = summaryFile ? prepareTextFile(summaryFile, "Runtime block summary").text : await readStdin(io.stdin);
1047
+ prepareStageRuntimeBlock(context, { ...input, kind: requiredOption(parsed, "kind"), code: requiredOption(parsed, "code"), summary: prepared.interactionText, retryable: hasOption(parsed, "retryable") });
1048
+ }
1049
+ else
1050
+ prepareStageRuntimeUnblock(context, input);
1051
+ }
1052
+ catch (error) {
1053
+ return prepareError(error, { parameter: "stage/work/summary" });
1054
+ }
1055
+ }
1056
+ if (route.key === "zcode invocation prepare") {
1057
+ const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label: "invocation issuance" }).value;
1058
+ const payload = value && typeof value === "object" ? value : {};
1059
+ try {
1060
+ prepared.lifecycleIssuance = prepareLifecycleIssuance(payload.command, payload.scope);
1061
+ }
1062
+ catch (error) {
1063
+ if (error instanceof AppError && ["invocation_command_invalid", "invocation_scope_invalid"].includes(error.code))
1064
+ return prepareError(new AppError("usage", error.message, 2, { cause_code: error.code }), { parameter: "stdin" });
1065
+ throw error;
1066
+ }
1067
+ assertLifecycleInvocationCurrent(context, prepared.lifecycleIssuance.scope, prepared.lifecycleIssuance.command);
1068
+ }
1069
+ if (route.key === "work add-batch") {
1070
+ const file = requiredOption(parsed, "file");
1071
+ try {
1072
+ prepared.workBatch = prepareWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file });
1073
+ }
1074
+ catch (error) {
1075
+ return prepareError(error, { parameter: "file/parent", file });
1076
+ }
1077
+ }
1078
+ if (route.key === "work repair add") {
1079
+ const taskFile = optionalOption(parsed, "task-file"), taskStdin = hasOption(parsed, "task-stdin");
1080
+ if (Boolean(taskFile) === taskStdin)
1081
+ throw new AppError("usage", "work repair add requires exactly one of --task-file or --task-stdin", 2);
1082
+ if (taskStdin && io.stdin?.isTTY)
1083
+ throw new AppError("usage", "--task-stdin requires piped or redirected input", 2);
1084
+ const task = taskFile ? prepareTextFile(taskFile, "Repair task").text : await readStdin(io.stdin);
1085
+ prepared.workRepair = prepareCliWorkRepair(context, parsed, task, scopeProjectRoot);
1086
+ }
1087
+ if (route.key === "run config set" || route.key === "run vars set") {
1088
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), key: requiredOption(parsed, "key"), value: requiredOption(parsed, "value") };
1089
+ try {
1090
+ if (route.key === "run config set")
1091
+ prepareFlowRunConfig(context, { ...input, reason: requiredOption(parsed, "reason") });
1092
+ else
1093
+ prepareFlowRunVariable(context, input);
1094
+ }
1095
+ catch (error) {
1096
+ return prepareError(error, { parameter: "run config/vars" });
1097
+ }
1098
+ }
1099
+ if (route.key === "schema validate") {
1100
+ try {
1101
+ prepared.schemaValidation = dispatchSchema(context, command, parsed);
1102
+ }
1103
+ catch (error) {
1104
+ return prepareError(error, { parameter: "file", file: requiredOption(parsed, "file") });
1105
+ }
1106
+ }
1107
+ if (route.key === "project config set") {
1108
+ try {
1109
+ prepareProjectConfigValue(requiredOption(parsed, "key"), requiredOption(parsed, "value"));
1110
+ }
1111
+ catch (error) {
1112
+ return prepareError(error, { parameter: "key/value" });
1113
+ }
1114
+ }
1115
+ if (family === "session" && command === "register") {
1116
+ try {
1117
+ prepared.sessionPayload = prepareFlowSessionPayload({ payloadBase64: optionalOption(parsed, "payload-base64"), payloadFile: optionalOption(parsed, "payload-file"), payloadJson: optionalOption(parsed, "payload-json") });
1118
+ }
1119
+ catch (error) {
1120
+ return prepareError(error, { parameter: "session payload" });
1121
+ }
1122
+ }
1123
+ const run = (projectRoot, runId) => {
1124
+ try {
1125
+ const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
1126
+ const row = context.db.get("SELECT run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
1127
+ if (!row)
1128
+ throw new AppError("not_found", "RUN is not registered", 2, { run_id: runId });
1129
+ if (!row.run_root)
1130
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId });
1131
+ return { runRoot: row.run_root, workspaceRoot: row.workspace_root, indexJson: row.index_json };
1132
+ }
1133
+ catch (error) {
1134
+ return prepareError(error, { parameter: "run-id", run_id: runId });
1135
+ }
1136
+ };
1137
+ const json = (file, stdin, label, schema) => {
1138
+ if (stdin) {
1139
+ // The caller awaits this helper; the synchronous facade below is only
1140
+ // used for file inputs. Keep stdin handling in the stage branch.
1141
+ throw new AppError("usage", `${label} stdin preparation must be awaited`, 2);
1142
+ }
1143
+ if (!file)
1144
+ throw new AppError("usage", `${label} input is required`, 2);
1145
+ return prepareJsonSchema(file, label, schema);
1146
+ };
1147
+ if (family === "stage" && command === "start") {
1148
+ const stage = requiredOption(parsed, "stage");
1149
+ if (stage !== "specify" && ["bootstrap", "intake-file", "intake-stdin"].some(option => hasOption(parsed, option)))
1150
+ throw new AppError("usage", "--bootstrap and intake inputs are only supported for SPECIFY", 2);
1151
+ requiredOption(parsed, "project-root");
1152
+ if (!hasOption(parsed, "bootstrap"))
1153
+ requiredPosition(parsed, 0, "run-id");
1154
+ if (hasOption(parsed, "session-id"))
1155
+ throw new AppError("usage", "--session-id is managed by the harness lifecycle adapter and is not accepted", 2);
1156
+ const contextFile = optionalOption(parsed, "context-file"), contextSha256 = optionalOption(parsed, "context-sha256");
1157
+ if (Boolean(contextFile) !== Boolean(contextSha256))
1158
+ throw new AppError("usage", "stage start requires --context-file together with --context-sha256", 2);
1159
+ if ([hasOption(parsed, "intake-file"), hasOption(parsed, "intake-stdin"), Boolean(contextFile)].filter(Boolean).length > 1)
1160
+ throw new AppError("usage", "Use exactly one of --intake-file, --intake-stdin or --context-file", 2);
1161
+ if (contextFile && contextSha256) {
1162
+ try {
1163
+ prepared.stageStart = { externalContext: loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) };
1164
+ }
1165
+ catch (error) {
1166
+ return prepareError(error, { file: contextFile, parameter: "context-file" });
1167
+ }
1168
+ if (!hasOption(parsed, "bootstrap")) {
1169
+ const { runRoot } = run(requiredOption(parsed, "project-root"), requiredPosition(parsed, 0, "run-id"));
1170
+ prepareExternalStageContext({ stageRoot: path.join(runRoot, vnextStageDirectory(stage)), loaded: prepared.stageStart.externalContext });
1171
+ }
1172
+ }
1173
+ if (stage === "specify") {
1174
+ const intakeFile = optionalOption(parsed, "intake-file");
1175
+ if (intakeFile)
1176
+ prepared.stageStart = { intakeMarkdown: prepareTextFile(intakeFile, "SPECIFY intake").text };
1177
+ else if (hasOption(parsed, "intake-stdin"))
1178
+ prepared.stageStart = { intakeMarkdown: await readStdin(io.stdin) };
1179
+ else if (prepared.stageStart?.externalContext)
1180
+ prepared.stageStart.intakeMarkdown = runnerIntake(prepared.stageStart.externalContext);
1181
+ const projectRoot = requiredOption(parsed, "project-root");
1182
+ const bootstrap = hasOption(parsed, "bootstrap");
1183
+ if (isVnextSpecifyFlow(projectRoot) && (bootstrap || isVnextSpecifyRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") }))) {
1184
+ 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") } : {}) });
1185
+ }
1186
+ else if (bootstrap) {
1187
+ if (hasOption(parsed, "intake-stdin") || contextFile)
1188
+ throw new AppError("usage", "Legacy SPECIFY bootstrap supports --intake-file, not stdin or runner context", 2);
1189
+ prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot, stage, bootstrap: { subject: requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) }, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) }, prepared.stageStart?.intakeMarkdown);
1190
+ }
1191
+ }
1192
+ if (stage === "merge" && isVnextMergeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1193
+ prepared.mergeStart = prepareVnextMergeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1194
+ }
1195
+ if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1196
+ prepared.protocolizeStart = prepareVnextProtocolizeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1197
+ }
1198
+ if (stage === "plan" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1199
+ prepared.planStart = prepareVnextPlanStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1200
+ }
1201
+ if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1202
+ prepared.planReviewStart = prepareVnextPlanReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1203
+ }
1204
+ if (stage === "code" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1205
+ prepared.codeStart = prepareVnextCodeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1206
+ }
1207
+ if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
1208
+ prepared.codeReviewStart = prepareVnextCodeReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
1209
+ }
1210
+ if (![prepared.specifyStart, prepared.protocolizeStart, prepared.planStart, prepared.planReviewStart, prepared.codeStart, prepared.codeReviewStart, prepared.mergeStart, prepared.legacyStageStart].some(Boolean)) {
1211
+ if (prepared.stageStart?.externalContext)
1212
+ throw new AppError("usage", "--context-file is currently supported only by vNext flow stages", 2);
1213
+ prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) });
1214
+ }
1215
+ }
1216
+ if (family === "stage" && (command === "pause" || command === "resume")) {
1217
+ requiredPosition(parsed, 0, "run-id");
1218
+ requiredOption(parsed, "project-root");
1219
+ requiredOption(parsed, "stage");
1220
+ requiredOption(parsed, "work");
1221
+ const fileParameter = command === "pause" ? "question-file" : "answer-file";
1222
+ const stdinParameter = command === "pause" ? "question-stdin" : "answer-stdin";
1223
+ const inputFile = optionalOption(parsed, fileParameter);
1224
+ const fromStdin = hasOption(parsed, stdinParameter);
1225
+ if (Boolean(inputFile) === fromStdin)
1226
+ throw new AppError("usage", `stage ${command} requires exactly one of --${fileParameter} or --${stdinParameter}`, 2);
1227
+ if (fromStdin && io.stdin?.isTTY)
1228
+ throw new AppError("usage", `--${stdinParameter} requires piped or redirected input`, 2);
1229
+ prepared.interactionText = inputFile ? prepareTextFile(inputFile, command === "pause" ? "User question" : "User answer").text : await readStdin(io.stdin);
1230
+ if (!prepared.interactionText.trim())
1231
+ return prepareError(new AppError("validation", `stage ${command} requires non-empty input`, 2), { parameter: inputFile ? fileParameter : stdinParameter });
1232
+ const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
1233
+ try {
1234
+ if (command === "pause")
1235
+ prepareStagePause(context, { ...input, question: prepared.interactionText });
1236
+ else
1237
+ prepared.resumeEvidence = prepareStageResume(context, { ...input, answer: prepared.interactionText }).evidence;
1238
+ }
1239
+ catch (error) {
1240
+ return prepareError(error, { parameter: "stage/work" });
1241
+ }
1242
+ }
1243
+ if (family === "work" && command === "finish") {
1244
+ const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
1245
+ if (Boolean(retryReceiptId) !== Boolean(retryReason))
1246
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
1247
+ const resultFile = optionalOption(parsed, "result-file");
1248
+ const resultStdin = hasOption(parsed, "result-stdin");
1249
+ if (Boolean(resultFile) === resultStdin)
1250
+ throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
1251
+ prepared.workFinish = { result: resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin) };
1252
+ prepareWorkResult(context, requiredPosition(parsed, 0, "work-id"), prepared.workFinish.result);
1253
+ }
1254
+ if (family === "stage" && command === "fanout" && requiredPosition(parsed, 0, "fanout action") === "reconcile") {
1255
+ prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
1256
+ }
1257
+ if (family === "stage" && command === "native" && requiredPosition(parsed, 0, "native action") === "observe") {
1258
+ prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
1259
+ }
1260
+ if (family !== "stage" || command !== "finish")
1261
+ return prepared;
1262
+ const projectRoot = requiredOption(parsed, "project-root");
1263
+ const runId = requiredPosition(parsed, 0, "run-id");
1264
+ const stage = requiredOption(parsed, "stage");
1265
+ if (hasOption(parsed, "status"))
1266
+ throw new AppError("usage", "Stage status belongs in the semantic input file", 2);
1267
+ if (["result-file", "semantic-file", "data", "result-stdin"].filter(key => hasOption(parsed, key)).length > 1)
1268
+ throw new AppError("usage", "Use exactly one result input: --result-file, --semantic-file, --data or --result-stdin", 2);
1269
+ if (Boolean(optionalOption(parsed, "retry-check")) !== Boolean(prepared.retryReason))
1270
+ throw new AppError("usage", "--retry-check requires recovery evidence", 2);
1271
+ const resultStdin = hasOption(parsed, "result-stdin");
1272
+ const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
1273
+ const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
1274
+ const runtime = run(projectRoot, runId);
1275
+ const terminalReplay = (name) => {
1276
+ let index;
1277
+ try {
1278
+ index = JSON.parse(runtime.indexJson);
1279
+ }
1280
+ catch (error) {
1281
+ 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) });
1282
+ }
1283
+ return index.stage_runs?.some(entry => entry.stage === name && entry.status === "done") === true
1284
+ && fs.existsSync(path.join(runtime.runRoot, vnextStageDirectory(name), "stage-report.json"));
1285
+ };
1286
+ const acceptStageInputs = (allowed) => {
1287
+ const options = allowed.split(" ");
1288
+ for (const name of ["dir", "semantic-file", "data", "result-file", "result-stdin", "outcome", "decision-file", "verification-file", "retry-check", "reason", "reason-file", "request", "work"]) {
1289
+ if (hasOption(parsed, name) && !options.includes(name === "reason-file" ? "reason" : name))
1290
+ throw new AppError("usage", `stage finish ${stage} does not accept --${name} for this flow`, 2, { parameter: name });
1291
+ }
1292
+ };
1293
+ const fromStdin = async (label, schema) => {
1294
+ const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label });
1295
+ try {
1296
+ validateSchema({ ...schema, file: value.file, data: value.value });
1297
+ }
1298
+ catch (error) {
1299
+ return prepareError(error, { file: value.file, label });
1300
+ }
1301
+ return value;
1302
+ };
1303
+ if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
1304
+ acceptStageInputs("result-file semantic-file data result-stdin outcome");
1305
+ const outcome = optionalOption(parsed, "outcome");
1306
+ if (!outcome || !["specified", "failed", "cancelled"].includes(outcome))
1307
+ throw new AppError("usage", "vNext SPECIFY requires --outcome specified|failed|cancelled; use stage pause for a user question", 2);
1308
+ if (Boolean(resultFile) === resultStdin)
1309
+ throw new AppError("usage", "vNext SPECIFY finish requires exactly one result input", 2);
1310
+ const schema = { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot };
1311
+ const input = resultStdin ? await fromStdin("SPECIFY result", schema) : json(resultFile, false, "SPECIFY result", schema);
1312
+ prepared.stageFinish = { value: input.value, text: input.text };
1313
+ }
1314
+ else if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
1315
+ acceptStageInputs("result-file semantic-file data");
1316
+ if (!resultFile || resultStdin || hasOption(parsed, "outcome"))
1317
+ throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
1318
+ let absolute;
1319
+ try {
1320
+ absolute = prepareProtocolizeResultPath(runtime.runRoot, resultFile);
1321
+ }
1322
+ catch (error) {
1323
+ return prepareError(error, { file: resultFile });
1324
+ }
1325
+ const input = json(absolute, false, "PROTOCOLIZE result", { schemaName: "vnext-protocolize-result", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1326
+ prepared.stageFinish = { value: input.value, text: input.text };
1327
+ }
1328
+ else if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
1329
+ acceptStageInputs("");
1330
+ if (hasOption(parsed, "code-work-batch-file"))
1331
+ throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
1332
+ try {
1333
+ prepared.planFinish = prepareVnextPlanFinish(context, { projectRoot, runId });
1334
+ }
1335
+ catch (error) {
1336
+ return prepareError(error, { parameter: "PLAN artifacts" });
1337
+ }
1338
+ }
1339
+ else if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
1340
+ acceptStageInputs("request work retry-check reason");
1341
+ if (resultStdin || resultFile || hasOption(parsed, "outcome"))
1342
+ throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
1343
+ requiredOption(parsed, "request");
1344
+ requiredOption(parsed, "work");
1345
+ try {
1346
+ prepared.mergeFinish = prepareVnextMergeFinish(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(optionalOption(parsed, "retry-check") ? { retryCheckId: optionalOption(parsed, "retry-check"), retryReason: prepared.retryReason } : {}) });
1347
+ }
1348
+ catch (error) {
1349
+ return prepareError(error, { parameter: "MERGE finish" });
1350
+ }
1351
+ }
1352
+ else if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
1353
+ acceptStageInputs("decision-file");
1354
+ if (resultStdin || hasOption(parsed, "outcome"))
1355
+ throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
1356
+ const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "04-plan-review", "decision.json");
1357
+ const input = json(decision, false, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1358
+ prepared.stageFinish = { value: input.value, text: input.text };
1359
+ }
1360
+ else if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
1361
+ acceptStageInputs("verification-file retry-check reason");
1362
+ if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome"))
1363
+ throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
1364
+ const verification = optionalOption(parsed, "verification-file");
1365
+ if (!verification)
1366
+ throw new AppError("usage", "vNext CODE finish requires --verification-file", 2);
1367
+ if (!terminalReplay(stage)) {
1368
+ const input = json(verification, false, "CODE verification", { schemaName: "code-verification", projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1369
+ prepared.stageFinish = { value: input.value, text: input.text };
1370
+ }
1371
+ }
1372
+ else if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
1373
+ acceptStageInputs("decision-file retry-check reason");
1374
+ if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
1375
+ throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
1376
+ const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "06-code-review", "decision.json");
1377
+ if (!terminalReplay(stage)) {
1378
+ const input = prepareCodeReviewDecision(runtime.runRoot, optionalOption(parsed, "decision-file"));
1379
+ try {
1380
+ validateSchema({ schemaName: "code-review-decision", file: decision, data: input.value, projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
1381
+ }
1382
+ catch (error) {
1383
+ return prepareError(error, { file: decision, label: "CODE-REVIEW decision" });
1384
+ }
1385
+ prepared.stageFinish = input;
1386
+ }
1387
+ }
1388
+ else {
1389
+ acceptStageInputs("dir semantic-file data");
1390
+ if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin)
1391
+ throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
1392
+ prepared.legacyStageFinish = prepareStageFinish(context, { projectRoot, runId, stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}), ...(semanticFile ? { semanticFile } : {}) });
1393
+ }
1394
+ return prepared;
1395
+ }
1396
+ async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args), prepared = {}) {
1397
+ if (!prepared.route)
1398
+ throw new AppError("command_not_prepared", "Command dispatch requires prepared arguments", 1);
1399
+ const { family, command, parsed } = prepared.route;
1400
+ if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
1401
+ const payload = prepared.lifecycleIssuance;
1402
+ if (!payload)
1403
+ throw new AppError("command_not_prepared", "Invocation issuance requires prepared input", 1);
1404
+ assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
1405
+ return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
1406
+ }
1407
+ if (family === "storage" && command === "migrate-writer") {
1408
+ if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1409
+ throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
1410
+ if (parsed.options.get("offline")?.[0] !== flagValue)
1411
+ throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
1412
+ return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
1413
+ }
506
1414
  if (family === "engine") {
507
- return dispatchEngine(context, command, parsed, classification);
1415
+ return dispatchEngine(context, command, parsed, classification, prepared);
508
1416
  }
509
1417
  if (family === "status") {
510
- const rootParsed = parseArgs([command ?? "", ...rest]);
511
1418
  return getRuntimeStatus(context, {
512
- projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root"),
513
- checkRegistry: hasOption(rootParsed, "check-registry")
1419
+ projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
1420
+ checkRegistry: hasOption(parsed, "check-registry")
514
1421
  });
515
1422
  }
516
1423
  if (family === "version") {
517
1424
  return getCliVersionReport();
518
1425
  }
519
1426
  if (family === "runtime") {
520
- return await dispatchRuntime(context, command, parsed);
1427
+ return await dispatchRuntime(context, command, parsed, commandProgress(io, output), prepared);
521
1428
  }
522
1429
  if (family === "canon") {
523
- return dispatchCanon(context, command, parsed);
1430
+ return dispatchCanon(context, command, parsed, prepared);
524
1431
  }
525
1432
  if (family === "project" && command === "register") {
526
1433
  return registerProject(context, { root: requiredOption(parsed, "root") });
@@ -597,16 +1504,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
597
1504
  return readyForMerge(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol ready-for-merge") });
598
1505
  }
599
1506
  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
- });
1507
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
610
1508
  }
611
1509
  if (family === "protocol" && command === "sync-from-run") {
612
1510
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -631,17 +1529,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
631
1529
  });
632
1530
  }
633
1531
  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
- });
1532
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
645
1533
  }
646
1534
  if (family === "plan" && command === "status") {
647
1535
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -660,12 +1548,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
660
1548
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
661
1549
  if (action === "status")
662
1550
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
1551
+ if (action === "reconcile")
1552
+ return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: prepared.observations ?? prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value });
663
1553
  if (action === "dispatch") {
664
1554
  if (stage === "plan-review")
665
1555
  return dispatchVnextPlanReview(context, { projectRoot, runId });
666
1556
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
667
1557
  }
668
- throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
1558
+ 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);
1559
+ }
1560
+ if (family === "stage" && command === "native") {
1561
+ if (requiredPosition(parsed, 0, "native action") !== "observe")
1562
+ throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
1563
+ 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
1564
  }
670
1565
  if (family === "run" && command === "capacity") {
671
1566
  if (requiredPosition(parsed, 0, "capacity action") !== "record")
@@ -677,16 +1572,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
677
1572
  return dispatchPlanItem(context, parsed, requiredScopeProjectRoot(scopeProjectRoot, "plan item"));
678
1573
  }
679
1574
  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
- });
1575
+ return renderWorkerPrompt(context, prepared.workerPrompt?.input ?? workerPromptInput(parsed), prepared.workerPrompt?.value);
690
1576
  }
691
1577
  if (family === "work" && command === "status") {
692
1578
  return getVnextWorkStatus(context, {
@@ -695,7 +1581,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
695
1581
  });
696
1582
  }
697
1583
  if (family === "work" && command === "add-batch") {
698
- return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file") });
1584
+ return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file"), prepared: prepared.workBatch });
699
1585
  }
700
1586
  if (family === "work" && command === "ls") {
701
1587
  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 +1589,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
703
1589
  if (family === "work" && command === "show")
704
1590
  return showWork(context, requiredPosition(parsed, 0, "work-id"));
705
1591
  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
- });
1592
+ if (!prepared.workRepair)
1593
+ throw new AppError("cli_input_unprepared", "Repair input was not prepared", 1);
1594
+ return addVnextCodeRepair(context, prepared.workRepair.input, prepared.workRepair.value);
748
1595
  }
749
1596
  if (family === "work" && command === "deps") {
750
1597
  const on = parsed.options.get("on") ?? [];
@@ -752,21 +1599,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
752
1599
  }
753
1600
  if (family === "work" && command === "delete")
754
1601
  return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
1602
+ if (family === "work" && command === "launch")
1603
+ return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
755
1604
  if (family === "work" && command === "start") {
756
1605
  const hookEventId = optionalOption(parsed, "hook-event-id");
1606
+ const recoveryId = optionalOption(parsed, "recovery-id");
757
1607
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
758
- return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
1608
+ return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
759
1609
  }
760
1610
  if (family === "work" && command === "finish") {
761
1611
  const resultFile = optionalOption(parsed, "result-file");
762
1612
  const resultStdin = hasOption(parsed, "result-stdin");
763
1613
  if (Boolean(resultFile) === resultStdin)
764
1614
  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));
1615
+ const workId = requiredPosition(parsed, 0, "work-id");
1616
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
1617
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1618
+ const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
1619
+ if (Boolean(retryReceiptId) !== Boolean(retryReason))
1620
+ throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
1621
+ const result = prepared.workFinish?.result ?? (resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin));
1622
+ return await finishWork(context, workId, result, commandProgress(io, output), retryReceiptId ? { receiptId: retryReceiptId, reason: retryReason } : undefined);
1623
+ }
1624
+ if (family === "work" && command === "fail") {
1625
+ const workId = requiredPosition(parsed, 0, "work-id");
1626
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
1627
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1628
+ return failWork(context, workId, requiredOption(parsed, "reason"));
767
1629
  }
768
- if (family === "work" && command === "fail")
769
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
770
1630
  if (family === "work" && command === "cancel")
771
1631
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
772
1632
  if (family === "work" && command === "retry")
@@ -788,12 +1648,12 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
788
1648
  const requireSessionBinding = hasOption(parsed, "require-session-binding");
789
1649
  const projectRoot = requiredOption(parsed, "project-root");
790
1650
  const stage = requiredOption(parsed, "stage");
791
- const externalContext = contextFile && contextSha256 ? loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) : undefined;
1651
+ const externalContext = prepared.stageStart?.externalContext;
792
1652
  const claimVnextStageStart = (runId) => {
793
1653
  registerProject(context, { root: projectRoot });
794
1654
  return findRecentMatchingHookEvent(context, {
795
1655
  projectId: requireProjectByRoot(context, resolveProjectRoot(projectRoot)).id,
796
- matchKey: stageStartMatchKey(runId, stage, projectRoot, contextSha256),
1656
+ matchKey: entityReferenceVariants(runId).map((reference) => stageStartMatchKey(reference, stage, projectRoot, contextSha256)),
797
1657
  errorCode: "trusted_session_binding_required",
798
1658
  operation: `vNext ${stage.toUpperCase()} start`
799
1659
  }).eventKey;
@@ -819,37 +1679,37 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
819
1679
  }
820
1680
  const result = launchVnextSpecify(context, {
821
1681
  projectRoot,
822
- ...(externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
1682
+ ...(prepared.stageStart?.intakeMarkdown !== undefined ? { intakeMarkdown: prepared.stageStart.intakeMarkdown } : externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
823
1683
  hookEventId,
824
1684
  ...(subject ? { slug: subject } : {}),
825
1685
  ...(externalContext ? { externalContext } : {}),
826
1686
  ...(!bootstrap ? { runId: requiredPosition(parsed, 0, "run-id") } : {})
827
- });
1687
+ }, prepared.specifyStart);
828
1688
  return attachExternalStageContext(result, externalContext);
829
1689
  }
830
1690
  if (!bootstrap && stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
831
1691
  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);
1692
+ return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.protocolizeStart), externalContext);
833
1693
  }
834
1694
  if (!bootstrap && stage === "plan" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
835
1695
  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);
1696
+ return attachExternalStageContext(startVnextPlan(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planStart), externalContext);
837
1697
  }
838
1698
  if (!bootstrap && stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
839
1699
  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);
1700
+ return attachExternalStageContext(startVnextPlanReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planReviewStart), externalContext);
841
1701
  }
842
1702
  if (!bootstrap && stage === "code" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
843
1703
  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);
1704
+ 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
1705
  }
846
1706
  if (!bootstrap && stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
847
1707
  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);
1708
+ return attachExternalStageContext(startVnextCodeReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.codeReviewStart), externalContext);
849
1709
  }
850
1710
  if (!bootstrap && stage === "merge" && isVnextMergeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
851
1711
  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);
1712
+ 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
1713
  }
854
1714
  if (intakeStdin) {
855
1715
  throw new AppError("usage", "--intake-stdin is only available for vNext SPECIFY", 2);
@@ -864,7 +1724,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
864
1724
  ...(hookEventId ? { hookEventId } : {}),
865
1725
  ...(requireSessionBinding ? { requireSessionBinding: true } : {}),
866
1726
  ...(dir ? { dir } : {})
867
- });
1727
+ }, prepared.legacyStageStart);
868
1728
  }
869
1729
  if (family === "stage" && command === "finish") {
870
1730
  const dir = optionalOption(parsed, "dir");
@@ -874,9 +1734,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
874
1734
  const projectRoot = requiredOption(parsed, "project-root");
875
1735
  const runId = requiredPosition(parsed, 0, "run-id");
876
1736
  const stage = requiredOption(parsed, "stage");
1737
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
877
1738
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
878
1739
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
879
1740
  const resultStdin = hasOption(parsed, "result-stdin");
1741
+ const retryCheckId = optionalOption(parsed, "retry-check");
1742
+ const retryReason = prepared.retryReason;
1743
+ const hookEventId = optionalOption(parsed, "hook-event-id");
1744
+ if (retryCheckId && !retryReason?.trim())
1745
+ throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
1746
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1747
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1748
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1749
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1750
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1751
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1752
+ if (protectedLifecycle)
1753
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
880
1754
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
881
1755
  const outcome = optionalOption(parsed, "outcome");
882
1756
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -888,84 +1762,92 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
888
1762
  if (!resultFile && !resultStdin) {
889
1763
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
890
1764
  }
891
- return finishVnextSpecifyStage(context, {
1765
+ return controlledReply(await finishVnextSpecifyStage(context, {
892
1766
  projectRoot,
893
1767
  runId,
894
- ...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
1768
+ ...(resultFile ? { resultFile } : prepared.stageFinish?.value ? {} : { resultJson: await readStdin(io.stdin) }),
1769
+ ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}),
895
1770
  outcome: outcome
896
- });
1771
+ }));
897
1772
  }
898
1773
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
899
1774
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
900
1775
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
901
1776
  }
902
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
1777
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile, ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}) }));
903
1778
  }
904
1779
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
905
1780
  if (optionalOption(parsed, "code-work-batch-file"))
906
1781
  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 });
1782
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId, ...(prepared.planFinish ? { prepared: prepared.planFinish } : {}) }));
908
1783
  }
909
1784
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
910
1785
  if (resultStdin || hasOption(parsed, "outcome"))
911
1786
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
912
1787
  const decisionFile = optionalOption(parsed, "decision-file");
913
- return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
1788
+ return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}), ...(prepared.stageFinish?.value ? { decision: prepared.stageFinish.value, decisionText: prepared.stageFinish.text } : {}) }));
914
1789
  }
915
1790
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
916
1791
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
917
1792
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
918
1793
  }
919
- return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
1794
+ 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
1795
  }
921
1796
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
922
1797
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
923
1798
  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) });
1799
+ 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
1800
  }
926
1801
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
927
1802
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
928
1803
  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) });
1804
+ 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
1805
  }
931
1806
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
932
1807
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
933
1808
  }
934
- return finishStage(context, {
1809
+ return controlledReply(await finishStage(context, {
935
1810
  projectRoot,
936
1811
  runId,
937
1812
  stage,
938
1813
  ...(dir ? { dir } : {}),
939
1814
  ...(semanticFile ? { semanticFile } : {})
940
- });
1815
+ }, prepared.legacyStageFinish));
941
1816
  }
942
1817
  if (family === "stage" && command === "pause") {
943
- if (!hasOption(parsed, "question-stdin"))
944
- throw new AppError("usage", "stage pause requires --question-stdin", 2);
1818
+ const projectRoot = requiredOption(parsed, "project-root");
1819
+ const runId = requiredPosition(parsed, 0, "run-id");
1820
+ const stage = requiredOption(parsed, "stage");
1821
+ const workId = requiredOption(parsed, "work");
1822
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
1823
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
1824
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
1825
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
1826
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
1827
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
1828
+ if (protectedLifecycle)
1829
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
945
1830
  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)
1831
+ projectRoot,
1832
+ runId,
1833
+ stage,
1834
+ workId,
1835
+ question: prepared.interactionText ?? await readStdin(io.stdin)
951
1836
  });
952
1837
  }
953
1838
  if (family === "stage" && command === "resume") {
954
- if (!hasOption(parsed, "answer-stdin"))
955
- throw new AppError("usage", "stage resume requires --answer-stdin", 2);
956
1839
  const hookEventId = optionalOption(parsed, "hook-event-id");
957
1840
  return resumeStageAfterUser(context, {
958
1841
  projectRoot: requiredOption(parsed, "project-root"),
959
1842
  runId: requiredPosition(parsed, 0, "run-id"),
960
1843
  stage: requiredOption(parsed, "stage"),
961
1844
  workId: requiredOption(parsed, "work"),
962
- answer: await readStdin(io.stdin),
1845
+ answer: prepared.interactionText ?? await readStdin(io.stdin),
1846
+ evidence: prepared.resumeEvidence,
963
1847
  ...(hookEventId ? { hookEventId } : {})
964
1848
  });
965
1849
  }
966
1850
  if (family === "stage" && command === "block") {
967
- if (!hasOption(parsed, "summary-stdin"))
968
- throw new AppError("usage", "stage block requires --summary-stdin", 2);
969
1851
  const kind = requiredOption(parsed, "kind");
970
1852
  if (kind !== "engine" && kind !== "harness" && kind !== "environment")
971
1853
  throw new AppError("usage", "--kind must be engine, harness or environment", 2);
@@ -976,7 +1858,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
976
1858
  workId: requiredOption(parsed, "work"),
977
1859
  kind,
978
1860
  code: requiredOption(parsed, "code"),
979
- summary: await readStdin(io.stdin),
1861
+ summary: prepared.interactionText ?? await readStdin(io.stdin),
980
1862
  retryable: hasOption(parsed, "retryable")
981
1863
  });
982
1864
  }
@@ -1001,13 +1883,55 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1001
1883
  return dispatchMergeWorker(context, command, parsed);
1002
1884
  }
1003
1885
  if (family === "cleanup") {
1004
- return dispatchCleanup(context, command, parsed);
1886
+ return dispatchCleanup(context, command, parsed, prepared.cleanup);
1005
1887
  }
1006
1888
  if (family === "session") {
1007
- return dispatchSession(context, command, parsed);
1889
+ return dispatchSession(context, command, parsed, prepared.sessionPayload);
1008
1890
  }
1009
1891
  if (family === "run") {
1010
- return dispatchRun(context, command, parsed);
1892
+ if (command === "control") {
1893
+ const action = requiredPosition(parsed, 0, "control action");
1894
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1895
+ if (action === "status")
1896
+ return runControlStatus(context, scope);
1897
+ if (action === "reconcile") {
1898
+ if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1899
+ throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
1900
+ return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
1901
+ }
1902
+ if (action === "resume") {
1903
+ if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
1904
+ throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
1905
+ return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
1906
+ }
1907
+ if (action === "serve")
1908
+ return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
1909
+ if (action === "pause" || action === "stop")
1910
+ return await requestRunControl(context, {
1911
+ ...scope, action, requestId: requiredOption(parsed, "request-id"),
1912
+ ...(hasOption(parsed, "force") ? { force: true } : {}),
1913
+ ...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
1914
+ ...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
1915
+ ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
1916
+ });
1917
+ throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
1918
+ }
1919
+ if (command === "drive") {
1920
+ const action = requiredPosition(parsed, 0, "drive action");
1921
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1922
+ if (action === "status")
1923
+ return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
1924
+ if (action === "launch")
1925
+ return await launchRunController(context, controllerLaunchInput(parsed), prepared.controllerLaunch);
1926
+ if (action === "serve")
1927
+ return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
1928
+ if (action === "answer")
1929
+ return answerRunController(context, controllerAnswerInput(parsed), prepared.controllerAnswer);
1930
+ if (action === "context")
1931
+ return supplyRunControllerContext(context, controllerContextInput(parsed), prepared.controllerContext);
1932
+ throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
1933
+ }
1934
+ return dispatchRun(context, command, parsed, prepared);
1011
1935
  }
1012
1936
  if (family === "stat") {
1013
1937
  return dispatchStat(context, command, parsed);
@@ -1022,23 +1946,26 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1022
1946
  return dispatchDashboard(context, command, parsed);
1023
1947
  }
1024
1948
  if (family === "schema") {
1025
- return dispatchSchema(context, command, parsed);
1949
+ return prepared.schemaValidation;
1026
1950
  }
1027
1951
  if (family === "memory") {
1028
1952
  return dispatchMemory(command, parsed);
1029
1953
  }
1030
1954
  if (family === "migration") {
1031
- return dispatchMigration(context, command, parsed, classification);
1955
+ return dispatchMigration(context, command, parsed, classification, prepared);
1032
1956
  }
1033
1957
  if (family === "codex" && command === "hooks") {
1034
- return dispatchCodexHooks(context, parsed);
1958
+ return dispatchCodexHooks(context, parsed, prepared);
1035
1959
  }
1036
1960
  if (family === "codex" && command === "home") {
1037
- return dispatchCodexHome(context, parsed);
1961
+ return dispatchCodexHome(context, parsed, prepared);
1038
1962
  }
1039
1963
  if (family === "codex" && command === "hook") {
1040
1964
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
1041
1965
  }
1966
+ if (family === "zcode" && command === "hook") {
1967
+ return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
1968
+ }
1042
1969
  if (family === "zcode" && command === "event") {
1043
1970
  return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
1044
1971
  }
@@ -1063,6 +1990,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1063
1990
  throw new AppError("usage", "Unknown opencode usage action", 2);
1064
1991
  return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1065
1992
  }
1993
+ if (family === "droid" && command === "event") {
1994
+ if (requiredPosition(parsed, 0, "droid event action") !== "handle")
1995
+ throw new AppError("usage", "Unknown droid event action", 2);
1996
+ return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1997
+ }
1998
+ if (family === "droid" && command === "usage") {
1999
+ if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
2000
+ throw new AppError("usage", "Unknown droid usage action", 2);
2001
+ return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
2002
+ }
1066
2003
  if (family === "agy" && command === "event")
1067
2004
  return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
1068
2005
  if (family === "agy" && command === "usage") {
@@ -1071,11 +2008,14 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1071
2008
  return ingestAgyUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1072
2009
  }
1073
2010
  if (family === "worktree") {
1074
- return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
2011
+ return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`), prepared);
1075
2012
  }
1076
2013
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
1077
2014
  }
1078
- function dispatchEngine(context, command, parsed, classification) {
2015
+ function engineBindInput(parsed) {
2016
+ 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") };
2017
+ }
2018
+ function dispatchEngine(context, command, parsed, classification, prepared) {
1079
2019
  if (command === "install") {
1080
2020
  return installCurrentEngine(context, { force: hasOption(parsed, "force") });
1081
2021
  }
@@ -1100,18 +2040,11 @@ function dispatchEngine(context, command, parsed, classification) {
1100
2040
  });
1101
2041
  }
1102
2042
  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
- });
2043
+ return bindRunEngine(context, engineBindInput(parsed), prepared.engineBind);
1111
2044
  }
1112
2045
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
1113
2046
  }
1114
- function dispatchMigration(context, command, parsed, classification) {
2047
+ function dispatchMigration(context, command, parsed, classification, prepared) {
1115
2048
  if (command === "impact")
1116
2049
  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
2050
  if (command === "plan" || command === "report") {
@@ -1127,26 +2060,42 @@ function dispatchMigration(context, command, parsed, classification) {
1127
2060
  });
1128
2061
  }
1129
2062
  if (command === "verify") {
1130
- return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
2063
+ return prepared.migrationVerification ?? verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
1131
2064
  }
1132
2065
  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
- });
2066
+ return applyMigration(context, migrationApplyInput(parsed, classification), prepared.migrationApply);
1144
2067
  }
1145
2068
  throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
1146
2069
  }
1147
- function dispatchCanon(context, command, parsed) {
2070
+ function protocolTransitionInput(parsed, scopeProjectRoot) {
2071
+ const reason = optionalOption(parsed, "reason");
2072
+ 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 } : {}) };
2073
+ }
2074
+ function controllerAnswerInput(parsed) {
2075
+ 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") };
2076
+ }
2077
+ function controllerLaunchInput(parsed) {
2078
+ 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") } : {}) };
2079
+ }
2080
+ function controllerContextInput(parsed) {
2081
+ 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") };
2082
+ }
2083
+ function migrationApplyInput(parsed, classification) {
2084
+ return {
2085
+ projectRoot: requiredOption(parsed, "project-root"),
2086
+ sourceVersion: optionalOption(parsed, "source-version"),
2087
+ targetVersion: optionalOption(parsed, "target-version"),
2088
+ runId: optionalOption(parsed, "run"),
2089
+ backupPath: optionalOption(parsed, "backup-path"),
2090
+ backupCreatedAt: optionalOption(parsed, "backup-created-at"),
2091
+ reportFile: optionalOption(parsed, "file"),
2092
+ allowActive: false,
2093
+ classification
2094
+ };
2095
+ }
2096
+ function dispatchCanon(context, command, parsed, prepared) {
1148
2097
  if (command === "register") {
1149
- return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
2098
+ return registerCanonRoot(context, { root: requiredOption(parsed, "root") }, prepared.canonRegistration);
1150
2099
  }
1151
2100
  if (command === "status") {
1152
2101
  return getCanonStatus(context, { root: optionalOption(parsed, "root") });
@@ -1156,6 +2105,15 @@ function dispatchCanon(context, command, parsed) {
1156
2105
  }
1157
2106
  throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
1158
2107
  }
2108
+ function mergeJobTarget(parsed, scopeProjectRoot) {
2109
+ return { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue"), workerId: requiredWorkerId(parsed), workspacePath: workspacePathOption(parsed) };
2110
+ }
2111
+ function mergeCancellationInput(parsed, scopeProjectRoot) {
2112
+ 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") };
2113
+ }
2114
+ function mergeWaitInput(parsed, scopeProjectRoot) {
2115
+ 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 };
2116
+ }
1159
2117
  function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1160
2118
  if (hasOption(parsed, "session-id")) {
1161
2119
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
@@ -1179,32 +2137,17 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1179
2137
  });
1180
2138
  }
1181
2139
  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
- });
2140
+ return waitNextMergeJob(context, mergeWaitInput(parsed, scopeProjectRoot));
1190
2141
  }
1191
2142
  if (command === "complete") {
1192
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1193
2143
  return completeMergeJob(context, {
1194
- protocolId,
1195
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
1196
- workerId: requiredWorkerId(parsed),
1197
- workspacePath: workspacePathOption(parsed),
2144
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1198
2145
  summary: requiredOption(parsed, "summary")
1199
2146
  });
1200
2147
  }
1201
2148
  if (command === "fail") {
1202
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1203
2149
  return failMergeJob(context, {
1204
- protocolId,
1205
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
1206
- workerId: requiredWorkerId(parsed),
1207
- workspacePath: workspacePathOption(parsed),
2150
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1208
2151
  reason: requiredOption(parsed, "reason"),
1209
2152
  requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
1210
2153
  });
@@ -1219,15 +2162,7 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1219
2162
  });
1220
2163
  }
1221
2164
  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
- });
2165
+ return cancelMergeQueueJob(context, mergeCancellationInput(parsed, scopeProjectRoot));
1231
2166
  }
1232
2167
  throw new AppError("usage", `Unknown merge-queue command: ${command ?? "<empty>"}`, 2);
1233
2168
  }
@@ -1247,6 +2182,8 @@ async function dispatchMerge(context, command, parsed, progress) {
1247
2182
  }
1248
2183
  if (command === "apply")
1249
2184
  return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
2185
+ if (command === "repair")
2186
+ return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
1250
2187
  if (command === "request") {
1251
2188
  const action = requiredPosition(parsed, 0, "request action");
1252
2189
  const requestId = requiredPosition(parsed, 1, "merge-request-id");
@@ -1343,7 +2280,7 @@ function completedCommandExitCode(payload) {
1343
2280
  const exitCode = payload.exit_code;
1344
2281
  return typeof exitCode === "number" && Number.isInteger(exitCode) ? exitCode : 0;
1345
2282
  }
1346
- function dispatchCleanup(context, command, parsed) {
2283
+ function dispatchCleanup(context, command, parsed, prepared) {
1347
2284
  if (command === "scan") {
1348
2285
  return cleanupScan(context, { projectRoot: requiredOption(parsed, "project-root") });
1349
2286
  }
@@ -1351,6 +2288,7 @@ function dispatchCleanup(context, command, parsed) {
1351
2288
  return cleanupApply(context, {
1352
2289
  projectRoot: requiredOption(parsed, "project-root"),
1353
2290
  planFile: requiredOption(parsed, "plan-file"),
2291
+ prepared,
1354
2292
  reason: requiredOption(parsed, "reason"),
1355
2293
  force: hasOption(parsed, "force")
1356
2294
  });
@@ -1371,12 +2309,14 @@ function dispatchProjectConfig(context, parsed) {
1371
2309
  }
1372
2310
  throw new AppError("usage", `Unknown project config action: ${action}`, 2);
1373
2311
  }
1374
- function dispatchSession(context, command, parsed) {
2312
+ function dispatchSession(context, command, parsed, preparedPayload) {
1375
2313
  if (command === "register") {
1376
2314
  return registerFlowSession(context, {
2315
+ ...(preparedPayload ? { preparedPayload } : {}),
1377
2316
  payloadBase64: optionalOption(parsed, "payload-base64"),
1378
2317
  payloadJson: optionalOption(parsed, "payload-json"),
1379
2318
  payloadFile: optionalOption(parsed, "payload-file"),
2319
+ hookEventId: optionalOption(parsed, "hook-event-id"),
1380
2320
  sessionId: optionalOption(parsed, "session-id")
1381
2321
  });
1382
2322
  }
@@ -1409,7 +2349,45 @@ function dispatchSession(context, command, parsed) {
1409
2349
  }
1410
2350
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1411
2351
  }
1412
- function dispatchRun(context, command, parsed) {
2352
+ function snapshotCreateInput(parsed) {
2353
+ const recoveryId = optionalOption(parsed, "recovery-id"), stageEntry = optionalOption(parsed, "stage-entry");
2354
+ 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 } : {}) };
2355
+ }
2356
+ function snapshotRestoreInput(parsed) {
2357
+ 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") } : {}) };
2358
+ }
2359
+ function runStartInput(parsed) {
2360
+ const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
2361
+ 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")) };
2362
+ }
2363
+ function runForkInput(parsed) {
2364
+ 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") } : {}) };
2365
+ }
2366
+ function recoveryCaptureInput(parsed) {
2367
+ return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") };
2368
+ }
2369
+ function recoveryResumeInput(parsed) {
2370
+ 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") };
2371
+ }
2372
+ function dispatchRun(context, command, parsed, prepared = {}) {
2373
+ if (command === "recovery") {
2374
+ const action = requiredPosition(parsed, 0, "recovery action");
2375
+ const projectRoot = requiredOption(parsed, "project-root");
2376
+ const runId = requiredPosition(parsed, 1, "run-id");
2377
+ if (action === "inspect")
2378
+ return inspectRunRecovery(context, { projectRoot, runId });
2379
+ if (action === "begin")
2380
+ return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") }, prepared.recoveryPayload);
2381
+ if (action === "seal")
2382
+ return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") }, prepared.recoveryPayload);
2383
+ if (action === "capture")
2384
+ return recordRecoveryCapture(context, recoveryCaptureInput(parsed), prepared.recoveryCapture);
2385
+ if (action === "resume")
2386
+ return resumeRunRecovery(context, recoveryResumeInput(parsed), prepared.recoveryResume);
2387
+ if (action === "accept")
2388
+ return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
2389
+ throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
2390
+ }
1413
2391
  if (command === "snapshot") {
1414
2392
  const action = requiredPosition(parsed, 0, "snapshot action");
1415
2393
  if (action === "bootstrap") {
@@ -1420,56 +2398,27 @@ function dispatchRun(context, command, parsed) {
1420
2398
  output: requiredOption(parsed, "output")
1421
2399
  });
1422
2400
  if (bootstrapAction === "restore")
1423
- return restoreEvalBootstrapSnapshot(context, {
1424
- snapshot: requiredOption(parsed, "snapshot"),
1425
- projectRoot: requiredOption(parsed, "project-root")
1426
- });
2401
+ return restoreEvalBootstrapSnapshot(context, snapshotRestoreInput(parsed), prepared.bootstrapRestore);
1427
2402
  throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore", 2);
1428
2403
  }
1429
2404
  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 });
2405
+ return createEvalRunSnapshot(context, snapshotCreateInput(parsed));
1444
2406
  }
1445
2407
  if (action === "restore")
1446
- return restoreEvalRunSnapshot(context, {
1447
- snapshot: requiredOption(parsed, "snapshot"),
1448
- projectRoot: requiredOption(parsed, "project-root")
1449
- });
2408
+ return restoreEvalRunSnapshot(context, snapshotRestoreInput(parsed), prepared.snapshotRestore);
1450
2409
  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
2410
  }
2411
+ if (command === "fork") {
2412
+ return forkRunFromSnapshot(context, runForkInput(parsed), prepared.runFork);
2413
+ }
1452
2414
  if (command === "prepare-vnext-specify") {
1453
- return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
2415
+ return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
1454
2416
  }
1455
2417
  if (command === "start") {
1456
2418
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
1457
2419
  assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
1458
2420
  }
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
- });
2421
+ return startFlowRun(context, prepared.runStart?.input ?? runStartInput(parsed), prepared.runStart?.value);
1473
2422
  }
1474
2423
  if (command === "status") {
1475
2424
  return getFlowRunStatus(context, {
@@ -1539,83 +2488,163 @@ function dispatchRun(context, command, parsed) {
1539
2488
  throw new AppError("usage", `Unknown run flags command: ${action}`, 2);
1540
2489
  }
1541
2490
  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
- });
2491
+ return attachFlowRunStage(context, stageAttachmentInput(parsed));
1550
2492
  }
1551
2493
  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
- });
2494
+ return completeFlowRunStage(context, stageCompletionInput(parsed));
1564
2495
  }
1565
2496
  if (command === "complete") {
1566
2497
  throw new AppError("usage", "run complete is retired; only an operator may use run override", 2);
1567
2498
  }
1568
2499
  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
- });
2500
+ return completeFlowRun(context, runOverrideInput(parsed));
1581
2501
  }
1582
2502
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1583
2503
  }
1584
- async function dispatchRuntime(context, command, parsed) {
2504
+ function stageAttachmentInput(parsed) {
2505
+ 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") };
2506
+ }
2507
+ function runOverrideInput(parsed) {
2508
+ 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") };
2509
+ }
2510
+ function stageCompletionInput(parsed) {
2511
+ 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") };
2512
+ }
2513
+ function runtimeServiceInput(parsed) {
2514
+ 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 };
2515
+ }
2516
+ function runtimeProcessStopInput(parsed) {
2517
+ const graceMs = optionalNumber(parsed, "grace-ms");
2518
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
2519
+ }
2520
+ function runtimeScopeResumeInput(parsed) {
2521
+ return { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") };
2522
+ }
2523
+ function runtimeProcessRegistrationInput(context, parsed) {
2524
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2525
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
2526
+ const kind = requiredOption(parsed, "kind");
2527
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
2528
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
2529
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
2530
+ return {
2531
+ kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
2532
+ ...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
2533
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
2534
+ workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
2535
+ operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
2536
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
2537
+ 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") } : {}) }
2538
+ };
2539
+ }
2540
+ function runtimeProcessConfirmationInput(parsed) {
2541
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
2542
+ 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 } : {}) };
2543
+ }
2544
+ function runtimeProcessHeartbeatInput(parsed) {
2545
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2546
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
2547
+ }
2548
+ function runtimeProcessFinishInput(parsed) {
2549
+ const state = requiredOption(parsed, "state");
2550
+ if (state !== "stopped" && state !== "failed")
2551
+ throw new AppError("validation", "--state must be stopped or failed", 2);
2552
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
2553
+ }
2554
+ async function dispatchRuntime(context, command, parsed, progress, prepared = {}) {
2555
+ if (command === "scope") {
2556
+ if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
2557
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2558
+ throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
2559
+ return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
2560
+ }
2561
+ if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
2562
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2563
+ throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
2564
+ return { ok: true, ...await launchRuntimeScopeResume(context, runtimeScopeResumeInput(parsed)) };
2565
+ }
2566
+ if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
2567
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2568
+ throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
2569
+ const mode = requiredOption(parsed, "mode");
2570
+ if (mode !== "pause" && mode !== "stop")
2571
+ throw new AppError("usage", "Scope control mode must be pause or stop", 2);
2572
+ return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, undefined, undefined, undefined, prepared.runtimeManifest) };
2573
+ }
2574
+ if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
2575
+ if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2576
+ throw new AppError("usage", "Invalid internal scope worker arguments", 2);
2577
+ return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
2578
+ }
2579
+ if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
2580
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
2581
+ throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
2582
+ return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
2583
+ }
2584
+ if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
2585
+ if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
2586
+ throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
2587
+ return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
2588
+ }
2589
+ 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))
2590
+ throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
2591
+ return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
2592
+ }
1585
2593
  if (command !== "process")
1586
- throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
2594
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
1587
2595
  const action = requiredPosition(parsed, 0, "runtime process action");
2596
+ if (action === "check-admission")
2597
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
2598
+ if (action === "start")
2599
+ return await startRuntimeService(context, { ...runtimeServiceInput(parsed), progress });
2600
+ if (action === "stop")
2601
+ return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
1588
2602
  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
- });
2603
+ const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
1597
2604
  return { ok: true, process };
1598
2605
  }
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 } : {}) }) };
2606
+ if (action === "confirm")
2607
+ return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
2608
+ if (action === "heartbeat")
2609
+ return { ok: heartbeatManagedProcess(context, runtimeProcessHeartbeatInput(parsed)) };
2610
+ if (action === "admission") {
2611
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2612
+ 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))
2613
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
2614
+ const metadata = JSON.parse(record.metadata_json);
2615
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
2616
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
2617
+ if (record.project_id && record.run_id) {
2618
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2619
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
2620
+ 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") });
2621
+ if (!recovering) {
2622
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
2623
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
2624
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
2625
+ }
2626
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
2627
+ 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")]);
2628
+ 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") });
2629
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
2630
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
2631
+ }
2632
+ 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") });
2633
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
1607
2634
  }
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 }) };
2635
+ if (action === "release-turn") {
2636
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2637
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2638
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
2639
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
1613
2640
  }
2641
+ if (action === "finish")
2642
+ return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
1614
2643
  if (action === "status")
1615
2644
  return { ok: true, processes: managedProcessStatus(context) };
1616
2645
  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);
2646
+ return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
2647
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
1619
2648
  }
1620
2649
  function dispatchStat(context, command, parsed) {
1621
2650
  if (command === "usage") {
@@ -1802,7 +2831,9 @@ function dispatchSchema(context, command, parsed) {
1802
2831
  // searching project directories: run_id is a structural field in the
1803
2832
  // submitted artifact and the database owns its materialized run_root.
1804
2833
  const explicitRunId = optionalOption(parsed, "run");
1805
- const declaredRunId = explicitRunId ?? readRunId(file);
2834
+ const data = prepareJsonFile(file, "schema input").value;
2835
+ const declared = data && typeof data === "object" && "run_id" in data ? data.run_id : undefined;
2836
+ const declaredRunId = explicitRunId ?? (typeof declared === "string" && declared.trim() ? declared : undefined);
1806
2837
  const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
1807
2838
  const bound = projectRoot
1808
2839
  ? 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 +2841,7 @@ function dispatchSchema(context, command, parsed) {
1810
2841
  return validateSchema({
1811
2842
  schemaName: requiredOption(parsed, "schema"),
1812
2843
  file,
2844
+ data,
1813
2845
  ...(schemaDir ? { schemaDir } : {}),
1814
2846
  ...(projectRoot ? { projectRoot } : {}),
1815
2847
  ...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
@@ -1819,14 +2851,11 @@ function dispatchSchema(context, command, parsed) {
1819
2851
  }
1820
2852
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
1821
2853
  }
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
- }
2854
+ function laneWorkspaceInput(parsed) {
2855
+ return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workspacePath: requiredOption(parsed, "path"), branch: optionalOption(parsed, "branch") };
2856
+ }
2857
+ function laneWaiterCancelInput(parsed) {
2858
+ return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"), reason: requiredOption(parsed, "reason") };
1830
2859
  }
1831
2860
  async function dispatchLane(context, command, parsed) {
1832
2861
  if (command === "status") {
@@ -1838,24 +2867,14 @@ async function dispatchLane(context, command, parsed) {
1838
2867
  if (command === "waiter") {
1839
2868
  const action = requiredPosition(parsed, 0, "lane waiter action");
1840
2869
  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
- });
2870
+ return cancelLaneWaiter(context, laneWaiterCancelInput(parsed));
1847
2871
  }
1848
2872
  throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
1849
2873
  }
1850
2874
  if (command === "workspace") {
1851
2875
  const action = requiredPosition(parsed, 0, "lane workspace action");
1852
2876
  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
- });
2877
+ return setLaneWorkspace(context, laneWorkspaceInput(parsed));
1859
2878
  }
1860
2879
  if (action === "check") {
1861
2880
  return checkLaneWorkspace(context, {
@@ -1927,7 +2946,7 @@ async function dispatchLane(context, command, parsed) {
1927
2946
  }
1928
2947
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
1929
2948
  }
1930
- function dispatchCodexHooks(context, parsed) {
2949
+ function dispatchCodexHooks(context, parsed, prepared) {
1931
2950
  const action = requiredPosition(parsed, 0, "codex hooks action");
1932
2951
  if (action === "print") {
1933
2952
  return printCodexHooks(context, {
@@ -1949,7 +2968,7 @@ function dispatchCodexHooks(context, parsed) {
1949
2968
  target: optionalOption(parsed, "target"),
1950
2969
  profile: optionalOption(parsed, "profile"),
1951
2970
  yes: hasOption(parsed, "yes")
1952
- });
2971
+ }, prepared.codexHooks);
1953
2972
  }
1954
2973
  if (action === "remove") {
1955
2974
  return removeCodexHooks(context, {
@@ -1957,11 +2976,14 @@ function dispatchCodexHooks(context, parsed) {
1957
2976
  target: optionalOption(parsed, "target"),
1958
2977
  profile: optionalOption(parsed, "profile"),
1959
2978
  yes: hasOption(parsed, "yes")
1960
- });
2979
+ }, prepared.codexHooks);
1961
2980
  }
1962
2981
  throw new AppError("usage", `Unknown codex hooks action: ${action}`, 2);
1963
2982
  }
1964
- function dispatchCodexHome(context, parsed) {
2983
+ function codexHomeInput(parsed) {
2984
+ return { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), sourceHome: optionalOption(parsed, "source-home"), targetHome: optionalOption(parsed, "target-home") };
2985
+ }
2986
+ function dispatchCodexHome(context, parsed, prepared) {
1965
2987
  const action = requiredPosition(parsed, 0, "codex home action");
1966
2988
  if (action === "plan") {
1967
2989
  return planCodexHome(context, {
@@ -1972,12 +2994,7 @@ function dispatchCodexHome(context, parsed) {
1972
2994
  });
1973
2995
  }
1974
2996
  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
- });
2997
+ return initCodexHome(context, codexHomeInput(parsed), prepared.codexHome);
1981
2998
  }
1982
2999
  if (action === "status") {
1983
3000
  return getCodexHomeStatus(context, {
@@ -2000,11 +3017,14 @@ function dispatchCodexHome(context, parsed) {
2000
3017
  }
2001
3018
  throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
2002
3019
  }
2003
- function dispatchCodexHook(context, parsed, stdin) {
3020
+ async function dispatchCodexHook(context, parsed, stdin) {
2004
3021
  const action = requiredPosition(parsed, 0, "codex hook action");
2005
3022
  if (action === "handle") {
2006
3023
  const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
2007
- const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }) : context;
3024
+ const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }, "hook") : context;
3025
+ const proof = await verifyCodexHookDelivery(hookContext.env, JSON.parse(stdin || "{}"));
3026
+ if (proof)
3027
+ hookContext.codexHookProof = proof;
2008
3028
  const result = handleCodexHook(hookContext, {
2009
3029
  event: requiredOption(parsed, "event"),
2010
3030
  projectRoot: optionalOption(parsed, "project-root"),
@@ -2027,6 +3047,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
2027
3047
  }
2028
3048
  throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
2029
3049
  }
3050
+ function dispatchZcodeHook(context, parsed, stdin) {
3051
+ if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
3052
+ throw new AppError("usage", "Unknown zcode hook action", 2);
3053
+ const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
3054
+ if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
3055
+ return { hookSpecificOutput: result.hookSpecificOutput };
3056
+ }
3057
+ return {};
3058
+ }
2030
3059
  function dispatchGrokEvent(context, parsed, stdin) {
2031
3060
  const action = requiredPosition(parsed, 0, "grok event action");
2032
3061
  if (action === "handle") {
@@ -2050,7 +3079,7 @@ function dispatchAgyEvent(context, parsed, stdin) {
2050
3079
  return handleAgyEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
2051
3080
  throw new AppError("usage", `Unknown agy event action: ${action}`, 2);
2052
3081
  }
2053
- function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
3082
+ function dispatchWorktree(context, command, parsed, scopeProjectRoot, prepared) {
2054
3083
  if (command === "plan") {
2055
3084
  return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
2056
3085
  }
@@ -2061,13 +3090,13 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
2061
3090
  branch: requiredOption(parsed, "branch"),
2062
3091
  base: requiredOption(parsed, "base"),
2063
3092
  path: requiredOption(parsed, "path")
2064
- });
3093
+ }, prepared.worktreeCreate);
2065
3094
  }
2066
3095
  if (command === "status") {
2067
3096
  return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
2068
3097
  }
2069
3098
  if (command === "bootstrap") {
2070
- return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
3099
+ return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot }, prepared.worktreeBootstrap);
2071
3100
  }
2072
3101
  if (command === "close") {
2073
3102
  return closeWorktree(context, {
@@ -2079,52 +3108,50 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
2079
3108
  throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
2080
3109
  }
2081
3110
  function dispatchPlanItem(context, parsed, projectRoot) {
3111
+ return updatePlanItem(context, planItemInput(parsed, projectRoot));
3112
+ }
3113
+ function planItemInput(parsed, projectRoot) {
2082
3114
  const action = requiredPosition(parsed, 0, "plan item action");
2083
3115
  const protocolId = requiredPosition(parsed, 1, "protocol-id");
2084
3116
  const itemId = requiredPosition(parsed, 2, "item-id");
2085
3117
  if (action === "start") {
2086
- return startPlanItem(context, { protocolId, projectRoot, itemId });
3118
+ return { protocolId, projectRoot, itemId, action };
2087
3119
  }
2088
3120
  if (action === "done") {
2089
- return completePlanItem(context, {
3121
+ return {
3122
+ action,
2090
3123
  protocolId,
2091
3124
  projectRoot,
2092
3125
  itemId,
2093
3126
  summary: requiredOption(parsed, "summary"),
2094
3127
  evidence: parsed.options.get("evidence") ?? []
2095
- });
3128
+ };
2096
3129
  }
2097
3130
  if (action === "block") {
2098
- return blockPlanItem(context, {
3131
+ return {
3132
+ action,
2099
3133
  protocolId,
2100
3134
  projectRoot,
2101
3135
  itemId,
2102
3136
  reason: requiredOption(parsed, "reason"),
2103
3137
  userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
2104
- });
3138
+ };
2105
3139
  }
2106
3140
  if (action === "skip") {
2107
- return skipPlanItem(context, {
3141
+ return {
3142
+ action,
2108
3143
  protocolId,
2109
3144
  projectRoot,
2110
3145
  itemId,
2111
3146
  reason: requiredOption(parsed, "reason")
2112
- });
3147
+ };
2113
3148
  }
2114
3149
  throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
2115
3150
  }
2116
3151
  function readOptionalJsonFile(file) {
2117
3152
  if (!file)
2118
3153
  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
- }
3154
+ return prepareJsonFile(file, "JSON input").value;
2128
3155
  }
2129
3156
  function parseFlowFlagOverrides(values) {
2130
3157
  const result = {};
@@ -2159,7 +3186,11 @@ function requiredPosition(parsed, index, label) {
2159
3186
  function optionalOption(parsed, key) {
2160
3187
  const values = parsed.options.get(key);
2161
3188
  const value = values?.[values.length - 1];
2162
- return value === flagValue ? undefined : value;
3189
+ if (value === flagValue)
3190
+ throw new AppError("usage", `--${key} requires a value`, 2);
3191
+ if (value !== undefined && !value.trim())
3192
+ throw new AppError("usage", `--${key} requires a non-empty value`, 2);
3193
+ return value;
2163
3194
  }
2164
3195
  function allOptions(parsed, key) {
2165
3196
  return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
@@ -2324,8 +3355,8 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2324
3355
  if (family === "merge-queue") {
2325
3356
  return projectRootForMergeQueueMutation(context, command, parsed);
2326
3357
  }
2327
- if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
2328
- return requiredOption(parsed, "project-root");
3358
+ if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
3359
+ return resolvedScope ?? requiredOption(parsed, "project-root");
2329
3360
  }
2330
3361
  if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
2331
3362
  return requiredOption(parsed, "project-root");
@@ -2343,7 +3374,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2343
3374
  return requiredOption(parsed, "project-root");
2344
3375
  }
2345
3376
  if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? "")) {
2346
- return requiredOption(parsed, "project-root");
3377
+ return resolvedScope ?? requiredOption(parsed, "project-root");
2347
3378
  }
2348
3379
  if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
2349
3380
  return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
@@ -2357,7 +3388,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2357
3388
  if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
2358
3389
  return requiredOption(parsed, "project-root");
2359
3390
  }
2360
- if (family === "agy" && ((command === "event" && requiredPosition(parsed, 0, "agy event action") === "handle") || command === "usage"))
3391
+ if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
2361
3392
  return requiredOption(parsed, "project-root");
2362
3393
  if (family === "codex" && command === "hooks") {
2363
3394
  const action = requiredPosition(parsed, 0, "codex hooks action");
@@ -2394,14 +3425,36 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
2394
3425
  function protocolProjectRoot(context, protocolId) {
2395
3426
  return requireProtocol(context, protocolId).project_root;
2396
3427
  }
3428
+ function prepareCliWorkRepair(context, parsed, objective, scopeProjectRoot) {
3429
+ 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") ?? [];
3430
+ if ([findings.length > 0, unresolved.length > 0, receipts.length > 0].filter(Boolean).length !== 1)
3431
+ throw new AppError("usage", "Repair requires exactly one source: --from-check, --from-finding or --from-unresolved", 2);
3432
+ const scope = { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"), runId: requiredOption(parsed, "run"), objective };
3433
+ if (findings.length) {
3434
+ if (origins.length || hasOption(parsed, "verification-file"))
3435
+ throw new AppError("usage", "Finding repair derives its origins and cannot accept --origin-work or --verification-file", 2);
3436
+ return prepareVnextCodeReviewRepair(context, { ...scope, findingIds: findings, checkRefsByFinding: Object.fromEntries(findings.map(finding => [finding, parsed.options.get("check-ref") ?? []])) });
3437
+ }
3438
+ if (!origins.length)
3439
+ throw new AppError("usage", "Repair requires at least one --origin-work", 2);
3440
+ if (hasOption(parsed, "check-ref"))
3441
+ throw new AppError("usage", "--check-ref is only supported with --from-finding", 2);
3442
+ if (receipts.length > 1)
3443
+ throw new AppError("usage", "Create one repair Work per --from-check receipt", 2);
3444
+ if (!unresolved.length && hasOption(parsed, "verification-file"))
3445
+ throw new AppError("usage", "--verification-file is only supported with --from-unresolved", 2);
3446
+ const input = { ...scope, originWorkIds: origins, ...(unresolved.length ? { semanticUnresolved: unresolved, verificationPath: requiredOption(parsed, "verification-file") } : { checkReceiptId: receipts[0] }) };
3447
+ return { input, value: prepareVnextCodeRepair(context, input) };
3448
+ }
3449
+ function workerPromptInput(parsed) {
3450
+ const workspaceRoot = optionalOption(parsed, "workspace-root"), taskFile = optionalOption(parsed, "task-file");
3451
+ 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 } : {}) };
3452
+ }
2397
3453
  function runnerIntake(context) {
2398
3454
  const input = context.value.task_input?.[0];
2399
3455
  if (!input?.path)
2400
3456
  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");
3457
+ return prepareTextFile(input.path, "Runner SPECIFY task input").text;
2405
3458
  }
2406
3459
  /** Stage services install context before their lifecycle transition. Keeping
2407
3460
  * this check here makes an incomplete service migration fail closed instead of