@deksden-com/dd-flow-cli 0.9.0-beta.1 → 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 (153) hide show
  1. package/CHANGELOG.md +561 -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 +91 -15
  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 +1506 -334
  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/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,55 +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";
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";
53
80
  const defaultIo = {
54
81
  stdout: process.stdout,
55
82
  stderr: process.stderr,
@@ -57,9 +84,36 @@ const defaultIo = {
57
84
  };
58
85
  const flagValue = "__dd_flow_flag__";
59
86
  export async function runCli(args, io = defaultIo, env = process.env) {
60
- 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
+ }
61
112
  const progress = progressForCommand(output.args);
62
113
  let heartbeat;
114
+ let invocationSettlement;
115
+ let settleHook;
116
+ let diagnosticHome;
63
117
  try {
64
118
  if (output.args.length === 1 && output.args[0] === "--version") {
65
119
  const payload = getCliVersionReport();
@@ -74,6 +128,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
74
128
  io.stdout.write(`${help}\n`);
75
129
  return 0;
76
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
+ }
77
141
  const classification = classifyCliOperation(output.args, env);
78
142
  if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
79
143
  throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
@@ -82,24 +146,143 @@ export async function runCli(args, io = defaultIo, env = process.env) {
82
146
  });
83
147
  }
84
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;
85
193
  emitHumanProgress(io, output, progress, "start");
86
194
  if (output.progressJsonl) {
87
195
  writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
88
196
  heartbeat = setInterval(() => writeProgressJsonl(io, "running", "Command is still running"), 15_000);
89
197
  }
90
- const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
91
- const routed = await routeArgsThroughEngine(routerContext, output.json ? [...output.args, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : output.args, io, io.stdin, env, scopeProjectRoot);
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;
204
+ const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
92
205
  if (routed?.routed && !routed.inProcess) {
93
206
  if (heartbeat)
94
207
  clearInterval(heartbeat);
95
- 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 });
96
211
  return routed.exitCode;
97
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
+ }
98
224
  const dispatchContext = routed?.inProcess
99
- ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
100
- : createContext(env, databaseModeForCommand(output.args, classification));
101
- preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
102
- 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
+ }
103
286
  const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
104
287
  const resultWithWarnings = addWarnings(result, refreshWarnings);
105
288
  if (heartbeat)
@@ -108,26 +291,87 @@ export async function runCli(args, io = defaultIo, env = process.env) {
108
291
  writeProgressJsonl(io, "done", "Command finished");
109
292
  emitHumanProgress(io, output, progress, "done");
110
293
  const payload = attachProgress(resultWithWarnings, progress, output);
294
+ const visible = writeResponseFile(payload, output);
111
295
  if (output.json)
112
- writeJson(io.stdout, payload);
296
+ writeJson(io.stdout, visible);
113
297
  else
114
298
  io.stdout.write(renderHumanResult(output.args, payload));
115
299
  return completedCommandExitCode(payload);
116
300
  }
117
- 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
+ }
118
332
  if (heartbeat)
119
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
+ }
120
353
  const payload = isAppError(error)
121
354
  ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
122
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() });
123
359
  emitHumanProgress(io, output, progress, "failed");
124
360
  if (output.progressJsonl)
125
361
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
126
362
  const errorPayload = attachProgress(payload, progress, 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
+ }
127
371
  if (output.json && output.progressJsonl)
128
372
  writeJson(io.stdout, errorPayload);
129
373
  else if (output.json)
130
- writeJson(io.stderr, errorPayload);
374
+ writeJson(io.stderr, visible);
131
375
  else
132
376
  io.stderr.write(renderHumanError(payload));
133
377
  return isAppError(error) ? error.exitCode : 1;
@@ -147,7 +391,22 @@ function resolveProjectRootBeforeRouting(args, env) {
147
391
  }
148
392
  function databaseModeForCommand(args, classification) {
149
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
+ }
150
408
  if (family === "engine" ||
409
+ family === "storage" && command === "migrate-writer" ||
151
410
  family === "version" ||
152
411
  family === "schema" ||
153
412
  family === "canon" && command === "resolve" ||
@@ -189,7 +448,11 @@ function parseOutputOptions(args) {
189
448
  const stripped = [];
190
449
  let json = false;
191
450
  let progressJsonl = false;
192
- for (const arg of args) {
451
+ let responseFile = null;
452
+ for (let index = 0; index < args.length; index += 1) {
453
+ const arg = args[index];
454
+ if (arg === undefined)
455
+ continue;
193
456
  if (arg === "--json") {
194
457
  json = true;
195
458
  continue;
@@ -198,10 +461,73 @@ function parseOutputOptions(args) {
198
461
  progressJsonl = true;
199
462
  continue;
200
463
  }
464
+ if (arg === "--response-file") {
465
+ if (responseFile !== null)
466
+ throw new AppError("usage", "--response-file may be supplied only once", 2);
467
+ const file = args[++index];
468
+ if (!file?.trim() || file.startsWith("--"))
469
+ throw new AppError("usage", "--response-file requires a path", 2);
470
+ responseFile = file;
471
+ continue;
472
+ }
201
473
  stripped.push(arg);
202
474
  }
475
+ if (responseFile && !json)
476
+ throw new AppError("usage", "--response-file requires --json", 2);
203
477
  const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
204
- return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly };
478
+ return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
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
+ }
507
+ /** Persist the complete machine response for harnesses that cap tool output.
508
+ * The visible response keeps routing fields but omits the potentially large
509
+ * rendered prompt; an agent can safely read the named file without rerunning
510
+ * the lifecycle command. */
511
+ function writeResponseFile(payload, output) {
512
+ if (!output.responseFile)
513
+ return payload;
514
+ const file = path.resolve(output.responseFile);
515
+ try {
516
+ fs.mkdirSync(path.dirname(file), { recursive: true });
517
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`;
518
+ fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
519
+ fs.renameSync(temporary, file);
520
+ }
521
+ catch (error) {
522
+ // The lifecycle mutation may already be accepted. Do not retry it just
523
+ // because its optional full-response file could not be written.
524
+ return { ...(payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, result: payload }), response_file: file, response_file_error: String(error) };
525
+ }
526
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
527
+ return { ok: true, response_file: file };
528
+ const visible = { ...payload, response_file: file };
529
+ delete visible.worker_prompt_markdown;
530
+ return visible;
205
531
  }
206
532
  function progressForCommand(args) {
207
533
  const [family, command, subcommand] = args;
@@ -458,24 +784,650 @@ function stringValue(value) {
458
784
  function shortSha(value) {
459
785
  return value ? value.slice(0, 12) : "unknown";
460
786
  }
461
- async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
462
- const [family, command, ...rest] = args;
463
- 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
+ }
464
1414
  if (family === "engine") {
465
- return dispatchEngine(context, command, parsed, classification);
1415
+ return dispatchEngine(context, command, parsed, classification, prepared);
466
1416
  }
467
1417
  if (family === "status") {
468
- const rootParsed = parseArgs([command ?? "", ...rest]);
469
1418
  return getRuntimeStatus(context, {
470
- projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root"),
471
- checkRegistry: hasOption(rootParsed, "check-registry")
1419
+ projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
1420
+ checkRegistry: hasOption(parsed, "check-registry")
472
1421
  });
473
1422
  }
474
1423
  if (family === "version") {
475
1424
  return getCliVersionReport();
476
1425
  }
1426
+ if (family === "runtime") {
1427
+ return await dispatchRuntime(context, command, parsed, commandProgress(io, output), prepared);
1428
+ }
477
1429
  if (family === "canon") {
478
- return dispatchCanon(context, command, parsed);
1430
+ return dispatchCanon(context, command, parsed, prepared);
479
1431
  }
480
1432
  if (family === "project" && command === "register") {
481
1433
  return registerProject(context, { root: requiredOption(parsed, "root") });
@@ -552,16 +1504,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
552
1504
  return readyForMerge(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol ready-for-merge") });
553
1505
  }
554
1506
  if (family === "protocol" && command === "transition") {
555
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
556
- const reason = optionalOption(parsed, "reason");
557
- return transitionProtocol(context, {
558
- protocolId,
559
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"),
560
- to: requiredOption(parsed, "to"),
561
- jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
562
- force: hasOption(parsed, "force"),
563
- ...(reason ? { reason } : {})
564
- });
1507
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
565
1508
  }
566
1509
  if (family === "protocol" && command === "sync-from-run") {
567
1510
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -586,17 +1529,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
586
1529
  });
587
1530
  }
588
1531
  if (family === "transition") {
589
- const transitionParsed = parseArgs([command ?? "", ...rest]);
590
- const protocolId = requiredPosition(transitionParsed, 0, "protocol-id");
591
- const reason = optionalOption(transitionParsed, "reason");
592
- return transitionProtocol(context, {
593
- protocolId,
594
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "transition"),
595
- to: requiredOption(transitionParsed, "to"),
596
- jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
597
- force: hasOption(transitionParsed, "force"),
598
- ...(reason ? { reason } : {})
599
- });
1532
+ return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
600
1533
  }
601
1534
  if (family === "plan" && command === "status") {
602
1535
  const protocolId = requiredPosition(parsed, 0, "protocol-id");
@@ -615,12 +1548,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
615
1548
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
616
1549
  if (action === "status")
617
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 });
618
1553
  if (action === "dispatch") {
619
1554
  if (stage === "plan-review")
620
1555
  return dispatchVnextPlanReview(context, { projectRoot, runId });
621
1556
  return getVnextFanoutStatus(context, { projectRoot, runId, stage });
622
1557
  }
623
- 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 });
624
1564
  }
625
1565
  if (family === "run" && command === "capacity") {
626
1566
  if (requiredPosition(parsed, 0, "capacity action") !== "record")
@@ -632,16 +1572,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
632
1572
  return dispatchPlanItem(context, parsed, requiredScopeProjectRoot(scopeProjectRoot, "plan item"));
633
1573
  }
634
1574
  if (family === "prompt" && command === "render") {
635
- const workspaceRoot = optionalOption(parsed, "workspace-root");
636
- const taskFile = optionalOption(parsed, "task-file");
637
- return renderWorkerPrompt(context, {
638
- projectRoot: requiredOption(parsed, "project-root"),
639
- runId: requiredOption(parsed, "run"),
640
- stage: requiredOption(parsed, "stage"),
641
- profile: requiredOption(parsed, "profile"),
642
- ...(taskFile ? { taskFile } : { planItemId: requiredOption(parsed, "plan-item") }),
643
- ...(workspaceRoot ? { workspaceRoot } : {})
644
- });
1575
+ return renderWorkerPrompt(context, prepared.workerPrompt?.input ?? workerPromptInput(parsed), prepared.workerPrompt?.value);
645
1576
  }
646
1577
  if (family === "work" && command === "status") {
647
1578
  return getVnextWorkStatus(context, {
@@ -650,7 +1581,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
650
1581
  });
651
1582
  }
652
1583
  if (family === "work" && command === "add-batch") {
653
- 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 });
654
1585
  }
655
1586
  if (family === "work" && command === "ls") {
656
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")) } : {}) });
@@ -658,31 +1589,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
658
1589
  if (family === "work" && command === "show")
659
1590
  return showWork(context, requiredPosition(parsed, 0, "work-id"));
660
1591
  if (family === "work" && command === "repair" && requiredPosition(parsed, 0, "action") === "add") {
661
- if (!hasOption(parsed, "task-stdin"))
662
- throw new AppError("usage", "work repair add requires --task-stdin", 2);
663
- const origins = parsed.options.get("origin-work") ?? [];
664
- const findings = parsed.options.get("from-finding") ?? [];
665
- if (!origins.length && !findings.length)
666
- throw new AppError("usage", "work repair add requires --from-check with --origin-work or --from-finding", 2);
667
- if (findings.length)
668
- return addVnextCodeReviewRepair(context, {
669
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
670
- runId: requiredOption(parsed, "run"),
671
- findingIds: findings,
672
- checkRefsByFinding: Object.fromEntries(findings.map((finding) => [finding, parsed.options.get("check-ref") ?? []])),
673
- objective: await readStdin(io.stdin)
674
- });
675
- if (!hasOption(parsed, "from-check"))
676
- throw new AppError("usage", "check repair requires --from-check", 2);
677
- if (!origins.length)
678
- throw new AppError("usage", "check repair requires at least one --origin-work", 2);
679
- return addVnextCodeRepair(context, {
680
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
681
- runId: requiredOption(parsed, "run"),
682
- checkReceiptId: requiredOption(parsed, "from-check"),
683
- originWorkIds: origins,
684
- objective: await readStdin(io.stdin)
685
- });
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);
686
1595
  }
687
1596
  if (family === "work" && command === "deps") {
688
1597
  const on = parsed.options.get("on") ?? [];
@@ -690,21 +1599,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
690
1599
  }
691
1600
  if (family === "work" && command === "delete")
692
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) });
693
1604
  if (family === "work" && command === "start") {
694
1605
  const hookEventId = optionalOption(parsed, "hook-event-id");
1606
+ const recoveryId = optionalOption(parsed, "recovery-id");
695
1607
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
696
- 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 });
697
1609
  }
698
1610
  if (family === "work" && command === "finish") {
699
1611
  const resultFile = optionalOption(parsed, "result-file");
700
1612
  const resultStdin = hasOption(parsed, "result-stdin");
701
1613
  if (Boolean(resultFile) === resultStdin)
702
1614
  throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
703
- const result = resultFile ? fs.readFileSync(path.resolve(resultFile), "utf8") : await readStdin(io.stdin);
704
- 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"));
705
1629
  }
706
- if (family === "work" && command === "fail")
707
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
708
1630
  if (family === "work" && command === "cancel")
709
1631
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
710
1632
  if (family === "work" && command === "retry")
@@ -726,12 +1648,12 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
726
1648
  const requireSessionBinding = hasOption(parsed, "require-session-binding");
727
1649
  const projectRoot = requiredOption(parsed, "project-root");
728
1650
  const stage = requiredOption(parsed, "stage");
729
- const externalContext = contextFile && contextSha256 ? loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) : undefined;
1651
+ const externalContext = prepared.stageStart?.externalContext;
730
1652
  const claimVnextStageStart = (runId) => {
731
1653
  registerProject(context, { root: projectRoot });
732
1654
  return findRecentMatchingHookEvent(context, {
733
1655
  projectId: requireProjectByRoot(context, resolveProjectRoot(projectRoot)).id,
734
- matchKey: stageStartMatchKey(runId, stage, projectRoot, contextSha256),
1656
+ matchKey: entityReferenceVariants(runId).map((reference) => stageStartMatchKey(reference, stage, projectRoot, contextSha256)),
735
1657
  errorCode: "trusted_session_binding_required",
736
1658
  operation: `vNext ${stage.toUpperCase()} start`
737
1659
  }).eventKey;
@@ -757,37 +1679,37 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
757
1679
  }
758
1680
  const result = launchVnextSpecify(context, {
759
1681
  projectRoot,
760
- ...(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) }),
761
1683
  hookEventId,
762
1684
  ...(subject ? { slug: subject } : {}),
763
1685
  ...(externalContext ? { externalContext } : {}),
764
1686
  ...(!bootstrap ? { runId: requiredPosition(parsed, 0, "run-id") } : {})
765
- });
1687
+ }, prepared.specifyStart);
766
1688
  return attachExternalStageContext(result, externalContext);
767
1689
  }
768
1690
  if (!bootstrap && stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
769
1691
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
770
- 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);
771
1693
  }
772
1694
  if (!bootstrap && stage === "plan" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
773
1695
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
774
- 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);
775
1697
  }
776
1698
  if (!bootstrap && stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
777
1699
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
778
- 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);
779
1701
  }
780
1702
  if (!bootstrap && stage === "code" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
781
1703
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
782
- 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);
783
1705
  }
784
1706
  if (!bootstrap && stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
785
1707
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
786
- 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);
787
1709
  }
788
1710
  if (!bootstrap && stage === "merge" && isVnextMergeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
789
1711
  hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
790
- 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);
791
1713
  }
792
1714
  if (intakeStdin) {
793
1715
  throw new AppError("usage", "--intake-stdin is only available for vNext SPECIFY", 2);
@@ -802,7 +1724,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
802
1724
  ...(hookEventId ? { hookEventId } : {}),
803
1725
  ...(requireSessionBinding ? { requireSessionBinding: true } : {}),
804
1726
  ...(dir ? { dir } : {})
805
- });
1727
+ }, prepared.legacyStageStart);
806
1728
  }
807
1729
  if (family === "stage" && command === "finish") {
808
1730
  const dir = optionalOption(parsed, "dir");
@@ -812,9 +1734,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
812
1734
  const projectRoot = requiredOption(parsed, "project-root");
813
1735
  const runId = requiredPosition(parsed, 0, "run-id");
814
1736
  const stage = requiredOption(parsed, "stage");
1737
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
815
1738
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
816
1739
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
817
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 } : {}) });
818
1754
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
819
1755
  const outcome = optionalOption(parsed, "outcome");
820
1756
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -826,84 +1762,92 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
826
1762
  if (!resultFile && !resultStdin) {
827
1763
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
828
1764
  }
829
- return finishVnextSpecifyStage(context, {
1765
+ return controlledReply(await finishVnextSpecifyStage(context, {
830
1766
  projectRoot,
831
1767
  runId,
832
- ...(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 } : {}),
833
1770
  outcome: outcome
834
- });
1771
+ }));
835
1772
  }
836
1773
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
837
1774
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
838
1775
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
839
1776
  }
840
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
1777
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile, ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}) }));
841
1778
  }
842
1779
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
843
1780
  if (optionalOption(parsed, "code-work-batch-file"))
844
1781
  throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
845
- return finishVnextPlan(context, { projectRoot, runId });
1782
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId, ...(prepared.planFinish ? { prepared: prepared.planFinish } : {}) }));
846
1783
  }
847
1784
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
848
1785
  if (resultStdin || hasOption(parsed, "outcome"))
849
1786
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
850
1787
  const decisionFile = optionalOption(parsed, "decision-file");
851
- 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 } : {}) }));
852
1789
  }
853
1790
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
854
1791
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
855
1792
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
856
1793
  }
857
- 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) }));
858
1795
  }
859
1796
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
860
1797
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
861
1798
  throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
862
- 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) }));
863
1800
  }
864
1801
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
865
1802
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
866
1803
  throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
867
- 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));
868
1805
  }
869
1806
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
870
1807
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
871
1808
  }
872
- return finishStage(context, {
1809
+ return controlledReply(await finishStage(context, {
873
1810
  projectRoot,
874
1811
  runId,
875
1812
  stage,
876
1813
  ...(dir ? { dir } : {}),
877
1814
  ...(semanticFile ? { semanticFile } : {})
878
- });
1815
+ }, prepared.legacyStageFinish));
879
1816
  }
880
1817
  if (family === "stage" && command === "pause") {
881
- if (!hasOption(parsed, "question-stdin"))
882
- 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") } : {}) });
883
1830
  return pauseStageForUser(context, {
884
- projectRoot: requiredOption(parsed, "project-root"),
885
- runId: requiredPosition(parsed, 0, "run-id"),
886
- stage: requiredOption(parsed, "stage"),
887
- workId: requiredOption(parsed, "work"),
888
- question: await readStdin(io.stdin)
1831
+ projectRoot,
1832
+ runId,
1833
+ stage,
1834
+ workId,
1835
+ question: prepared.interactionText ?? await readStdin(io.stdin)
889
1836
  });
890
1837
  }
891
1838
  if (family === "stage" && command === "resume") {
892
- if (!hasOption(parsed, "answer-stdin"))
893
- throw new AppError("usage", "stage resume requires --answer-stdin", 2);
894
1839
  const hookEventId = optionalOption(parsed, "hook-event-id");
895
1840
  return resumeStageAfterUser(context, {
896
1841
  projectRoot: requiredOption(parsed, "project-root"),
897
1842
  runId: requiredPosition(parsed, 0, "run-id"),
898
1843
  stage: requiredOption(parsed, "stage"),
899
1844
  workId: requiredOption(parsed, "work"),
900
- answer: await readStdin(io.stdin),
1845
+ answer: prepared.interactionText ?? await readStdin(io.stdin),
1846
+ evidence: prepared.resumeEvidence,
901
1847
  ...(hookEventId ? { hookEventId } : {})
902
1848
  });
903
1849
  }
904
1850
  if (family === "stage" && command === "block") {
905
- if (!hasOption(parsed, "summary-stdin"))
906
- throw new AppError("usage", "stage block requires --summary-stdin", 2);
907
1851
  const kind = requiredOption(parsed, "kind");
908
1852
  if (kind !== "engine" && kind !== "harness" && kind !== "environment")
909
1853
  throw new AppError("usage", "--kind must be engine, harness or environment", 2);
@@ -914,7 +1858,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
914
1858
  workId: requiredOption(parsed, "work"),
915
1859
  kind,
916
1860
  code: requiredOption(parsed, "code"),
917
- summary: await readStdin(io.stdin),
1861
+ summary: prepared.interactionText ?? await readStdin(io.stdin),
918
1862
  retryable: hasOption(parsed, "retryable")
919
1863
  });
920
1864
  }
@@ -939,13 +1883,55 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
939
1883
  return dispatchMergeWorker(context, command, parsed);
940
1884
  }
941
1885
  if (family === "cleanup") {
942
- return dispatchCleanup(context, command, parsed);
1886
+ return dispatchCleanup(context, command, parsed, prepared.cleanup);
943
1887
  }
944
1888
  if (family === "session") {
945
- return dispatchSession(context, command, parsed);
1889
+ return dispatchSession(context, command, parsed, prepared.sessionPayload);
946
1890
  }
947
1891
  if (family === "run") {
948
- 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);
949
1935
  }
950
1936
  if (family === "stat") {
951
1937
  return dispatchStat(context, command, parsed);
@@ -960,23 +1946,26 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
960
1946
  return dispatchDashboard(context, command, parsed);
961
1947
  }
962
1948
  if (family === "schema") {
963
- return dispatchSchema(context, command, parsed);
1949
+ return prepared.schemaValidation;
964
1950
  }
965
1951
  if (family === "memory") {
966
1952
  return dispatchMemory(command, parsed);
967
1953
  }
968
1954
  if (family === "migration") {
969
- return dispatchMigration(context, command, parsed, classification);
1955
+ return dispatchMigration(context, command, parsed, classification, prepared);
970
1956
  }
971
1957
  if (family === "codex" && command === "hooks") {
972
- return dispatchCodexHooks(context, parsed);
1958
+ return dispatchCodexHooks(context, parsed, prepared);
973
1959
  }
974
1960
  if (family === "codex" && command === "home") {
975
- return dispatchCodexHome(context, parsed);
1961
+ return dispatchCodexHome(context, parsed, prepared);
976
1962
  }
977
1963
  if (family === "codex" && command === "hook") {
978
1964
  return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
979
1965
  }
1966
+ if (family === "zcode" && command === "hook") {
1967
+ return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
1968
+ }
980
1969
  if (family === "zcode" && command === "event") {
981
1970
  return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
982
1971
  }
@@ -1001,6 +1990,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1001
1990
  throw new AppError("usage", "Unknown opencode usage action", 2);
1002
1991
  return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1003
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
+ }
1004
2003
  if (family === "agy" && command === "event")
1005
2004
  return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
1006
2005
  if (family === "agy" && command === "usage") {
@@ -1009,11 +2008,14 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1009
2008
  return ingestAgyUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1010
2009
  }
1011
2010
  if (family === "worktree") {
1012
- return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
2011
+ return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`), prepared);
1013
2012
  }
1014
2013
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
1015
2014
  }
1016
- 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) {
1017
2019
  if (command === "install") {
1018
2020
  return installCurrentEngine(context, { force: hasOption(parsed, "force") });
1019
2021
  }
@@ -1038,18 +2040,11 @@ function dispatchEngine(context, command, parsed, classification) {
1038
2040
  });
1039
2041
  }
1040
2042
  if (command === "bind-run") {
1041
- return bindRunEngine(context, {
1042
- projectRoot: requiredOption(parsed, "project-root"),
1043
- runId: requiredPosition(parsed, 0, "run-id"),
1044
- packageName: optionalOption(parsed, "package"),
1045
- engineVersion: requiredOption(parsed, "engine-version"),
1046
- integrityChecksum: optionalOption(parsed, "integrity-checksum"),
1047
- reason: requiredOption(parsed, "reason")
1048
- });
2043
+ return bindRunEngine(context, engineBindInput(parsed), prepared.engineBind);
1049
2044
  }
1050
2045
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
1051
2046
  }
1052
- function dispatchMigration(context, command, parsed, classification) {
2047
+ function dispatchMigration(context, command, parsed, classification, prepared) {
1053
2048
  if (command === "impact")
1054
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") } : {}) });
1055
2050
  if (command === "plan" || command === "report") {
@@ -1065,26 +2060,42 @@ function dispatchMigration(context, command, parsed, classification) {
1065
2060
  });
1066
2061
  }
1067
2062
  if (command === "verify") {
1068
- return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
2063
+ return prepared.migrationVerification ?? verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
1069
2064
  }
1070
2065
  if (command === "apply") {
1071
- return applyMigration(context, {
1072
- projectRoot: requiredOption(parsed, "project-root"),
1073
- sourceVersion: optionalOption(parsed, "source-version"),
1074
- targetVersion: optionalOption(parsed, "target-version"),
1075
- runId: optionalOption(parsed, "run"),
1076
- backupPath: optionalOption(parsed, "backup-path"),
1077
- backupCreatedAt: optionalOption(parsed, "backup-created-at"),
1078
- reportFile: optionalOption(parsed, "file"),
1079
- allowActive: false,
1080
- classification
1081
- });
2066
+ return applyMigration(context, migrationApplyInput(parsed, classification), prepared.migrationApply);
1082
2067
  }
1083
2068
  throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
1084
2069
  }
1085
- 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) {
1086
2097
  if (command === "register") {
1087
- return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
2098
+ return registerCanonRoot(context, { root: requiredOption(parsed, "root") }, prepared.canonRegistration);
1088
2099
  }
1089
2100
  if (command === "status") {
1090
2101
  return getCanonStatus(context, { root: optionalOption(parsed, "root") });
@@ -1094,6 +2105,15 @@ function dispatchCanon(context, command, parsed) {
1094
2105
  }
1095
2106
  throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
1096
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
+ }
1097
2117
  function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1098
2118
  if (hasOption(parsed, "session-id")) {
1099
2119
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
@@ -1117,32 +2137,17 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1117
2137
  });
1118
2138
  }
1119
2139
  if (command === "wait-next") {
1120
- return waitNextMergeJob(context, {
1121
- projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
1122
- workerId: requiredWorkerId(parsed),
1123
- timeoutSeconds: optionalNumber(parsed, "timeout"),
1124
- pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
1125
- workspacePath: workspacePathOption(parsed),
1126
- acquireLock: parseOptionalBoolean(optionalOption(parsed, "acquire-lock"), "acquire-lock") ?? false
1127
- });
2140
+ return waitNextMergeJob(context, mergeWaitInput(parsed, scopeProjectRoot));
1128
2141
  }
1129
2142
  if (command === "complete") {
1130
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1131
2143
  return completeMergeJob(context, {
1132
- protocolId,
1133
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
1134
- workerId: requiredWorkerId(parsed),
1135
- workspacePath: workspacePathOption(parsed),
2144
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1136
2145
  summary: requiredOption(parsed, "summary")
1137
2146
  });
1138
2147
  }
1139
2148
  if (command === "fail") {
1140
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1141
2149
  return failMergeJob(context, {
1142
- protocolId,
1143
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
1144
- workerId: requiredWorkerId(parsed),
1145
- workspacePath: workspacePathOption(parsed),
2150
+ ...mergeJobTarget(parsed, scopeProjectRoot),
1146
2151
  reason: requiredOption(parsed, "reason"),
1147
2152
  requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
1148
2153
  });
@@ -1157,15 +2162,7 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
1157
2162
  });
1158
2163
  }
1159
2164
  if (command === "cancel") {
1160
- const protocolId = requiredPosition(parsed, 0, "protocol-id");
1161
- return cancelMergeQueueJob(context, {
1162
- protocolId,
1163
- projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"),
1164
- reason: requiredOption(parsed, "reason"),
1165
- workerId: optionalOption(parsed, "worker-id"),
1166
- workspacePath: optionalOption(parsed, "path"),
1167
- force: hasOption(parsed, "force")
1168
- });
2165
+ return cancelMergeQueueJob(context, mergeCancellationInput(parsed, scopeProjectRoot));
1169
2166
  }
1170
2167
  throw new AppError("usage", `Unknown merge-queue command: ${command ?? "<empty>"}`, 2);
1171
2168
  }
@@ -1185,6 +2182,8 @@ async function dispatchMerge(context, command, parsed, progress) {
1185
2182
  }
1186
2183
  if (command === "apply")
1187
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") });
1188
2187
  if (command === "request") {
1189
2188
  const action = requiredPosition(parsed, 0, "request action");
1190
2189
  const requestId = requiredPosition(parsed, 1, "merge-request-id");
@@ -1281,7 +2280,7 @@ function completedCommandExitCode(payload) {
1281
2280
  const exitCode = payload.exit_code;
1282
2281
  return typeof exitCode === "number" && Number.isInteger(exitCode) ? exitCode : 0;
1283
2282
  }
1284
- function dispatchCleanup(context, command, parsed) {
2283
+ function dispatchCleanup(context, command, parsed, prepared) {
1285
2284
  if (command === "scan") {
1286
2285
  return cleanupScan(context, { projectRoot: requiredOption(parsed, "project-root") });
1287
2286
  }
@@ -1289,6 +2288,7 @@ function dispatchCleanup(context, command, parsed) {
1289
2288
  return cleanupApply(context, {
1290
2289
  projectRoot: requiredOption(parsed, "project-root"),
1291
2290
  planFile: requiredOption(parsed, "plan-file"),
2291
+ prepared,
1292
2292
  reason: requiredOption(parsed, "reason"),
1293
2293
  force: hasOption(parsed, "force")
1294
2294
  });
@@ -1309,12 +2309,14 @@ function dispatchProjectConfig(context, parsed) {
1309
2309
  }
1310
2310
  throw new AppError("usage", `Unknown project config action: ${action}`, 2);
1311
2311
  }
1312
- function dispatchSession(context, command, parsed) {
2312
+ function dispatchSession(context, command, parsed, preparedPayload) {
1313
2313
  if (command === "register") {
1314
2314
  return registerFlowSession(context, {
2315
+ ...(preparedPayload ? { preparedPayload } : {}),
1315
2316
  payloadBase64: optionalOption(parsed, "payload-base64"),
1316
2317
  payloadJson: optionalOption(parsed, "payload-json"),
1317
2318
  payloadFile: optionalOption(parsed, "payload-file"),
2319
+ hookEventId: optionalOption(parsed, "hook-event-id"),
1318
2320
  sessionId: optionalOption(parsed, "session-id")
1319
2321
  });
1320
2322
  }
@@ -1347,7 +2349,45 @@ function dispatchSession(context, command, parsed) {
1347
2349
  }
1348
2350
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1349
2351
  }
1350
- 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
+ }
1351
2391
  if (command === "snapshot") {
1352
2392
  const action = requiredPosition(parsed, 0, "snapshot action");
1353
2393
  if (action === "bootstrap") {
@@ -1358,56 +2398,27 @@ function dispatchRun(context, command, parsed) {
1358
2398
  output: requiredOption(parsed, "output")
1359
2399
  });
1360
2400
  if (bootstrapAction === "restore")
1361
- return restoreEvalBootstrapSnapshot(context, {
1362
- snapshot: requiredOption(parsed, "snapshot"),
1363
- projectRoot: requiredOption(parsed, "project-root")
1364
- });
2401
+ return restoreEvalBootstrapSnapshot(context, snapshotRestoreInput(parsed), prepared.bootstrapRestore);
1365
2402
  throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore", 2);
1366
2403
  }
1367
2404
  if (action === "create") {
1368
- const candidate = parsed.options.has("candidate");
1369
- const stageEntry = optionalOption(parsed, "stage-entry");
1370
- if (candidate && stageEntry !== undefined)
1371
- throw new AppError("usage", "Use either --candidate or --stage-entry, not both", 2);
1372
- if (!candidate && stageEntry === undefined)
1373
- throw new AppError("usage", "Snapshot create requires --candidate or --stage-entry", 2);
1374
- const input = {
1375
- projectRoot: requiredOption(parsed, "project-root"),
1376
- runId: requiredPosition(parsed, 1, "run-id"),
1377
- output: requiredOption(parsed, "output")
1378
- };
1379
- return candidate
1380
- ? createEvalRunSnapshot(context, { ...input, candidate: true })
1381
- : createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
2405
+ return createEvalRunSnapshot(context, snapshotCreateInput(parsed));
1382
2406
  }
1383
2407
  if (action === "restore")
1384
- return restoreEvalRunSnapshot(context, {
1385
- snapshot: requiredOption(parsed, "snapshot"),
1386
- projectRoot: requiredOption(parsed, "project-root")
1387
- });
2408
+ return restoreEvalRunSnapshot(context, snapshotRestoreInput(parsed), prepared.snapshotRestore);
1388
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);
1389
2410
  }
2411
+ if (command === "fork") {
2412
+ return forkRunFromSnapshot(context, runForkInput(parsed), prepared.runFork);
2413
+ }
1390
2414
  if (command === "prepare-vnext-specify") {
1391
- 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") });
1392
2416
  }
1393
2417
  if (command === "start") {
1394
2418
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
1395
2419
  assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
1396
2420
  }
1397
- const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
1398
- return startFlowRun(context, {
1399
- projectRoot: requiredOption(parsed, "project-root"),
1400
- workspaceRoot: optionalOption(parsed, "workspace-root"),
1401
- flowKind: requiredOption(parsed, "flow-kind"),
1402
- subjectType: requiredOption(parsed, "subject-type"),
1403
- subjectId: requiredOption(parsed, "subject-id"),
1404
- slug: requiredOption(parsed, "slug"),
1405
- nextAction: optionalOption(parsed, "next-action"),
1406
- preset: optionalOption(parsed, "preset"),
1407
- taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
1408
- ...(protocolOverrides ? { protocolOverrides } : {}),
1409
- runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
1410
- });
2421
+ return startFlowRun(context, prepared.runStart?.input ?? runStartInput(parsed), prepared.runStart?.value);
1411
2422
  }
1412
2423
  if (command === "status") {
1413
2424
  return getFlowRunStatus(context, {
@@ -1477,48 +2488,164 @@ function dispatchRun(context, command, parsed) {
1477
2488
  throw new AppError("usage", `Unknown run flags command: ${action}`, 2);
1478
2489
  }
1479
2490
  if (command === "attach-stage") {
1480
- return attachFlowRunStage(context, {
1481
- projectRoot: requiredOption(parsed, "project-root"),
1482
- runId: requiredPosition(parsed, 0, "run-id"),
1483
- stage: requiredOption(parsed, "stage"),
1484
- dir: requiredOption(parsed, "dir"),
1485
- status: requiredOption(parsed, "status"),
1486
- dataSchemaId: optionalOption(parsed, "data-schema-id")
1487
- });
2491
+ return attachFlowRunStage(context, stageAttachmentInput(parsed));
1488
2492
  }
1489
2493
  if (command === "complete-stage") {
1490
- const stageReport = optionalOption(parsed, "stage-report") ?? optionalOption(parsed, "dashboard");
1491
- return completeFlowRunStage(context, {
1492
- projectRoot: requiredOption(parsed, "project-root"),
1493
- runId: requiredPosition(parsed, 0, "run-id"),
1494
- stage: requiredOption(parsed, "stage"),
1495
- status: requiredOption(parsed, "status"),
1496
- stageReport,
1497
- data: optionalOption(parsed, "data"),
1498
- dataSchemaId: optionalOption(parsed, "data-schema-id"),
1499
- report: optionalOption(parsed, "report"),
1500
- aliases: allOptions(parsed, "alias")
1501
- });
2494
+ return completeFlowRunStage(context, stageCompletionInput(parsed));
1502
2495
  }
1503
2496
  if (command === "complete") {
1504
2497
  throw new AppError("usage", "run complete is retired; only an operator may use run override", 2);
1505
2498
  }
1506
2499
  if (command === "override") {
1507
- const status = requiredOption(parsed, "status");
1508
- if (!['cancelled', 'failed'].includes(status)) {
1509
- throw new AppError("usage", "run override status must be cancelled or failed", 2, { status });
1510
- }
1511
- return completeFlowRun(context, {
1512
- projectRoot: requiredOption(parsed, "project-root"),
1513
- runId: requiredPosition(parsed, 0, "run-id"),
1514
- status,
1515
- verdict: optionalOption(parsed, "verdict"),
1516
- nextAction: optionalOption(parsed, "next-action"),
1517
- manualOverrideReason: requiredOption(parsed, "reason")
1518
- });
2500
+ return completeFlowRun(context, runOverrideInput(parsed));
1519
2501
  }
1520
2502
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1521
2503
  }
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
+ }
2593
+ if (command !== "process")
2594
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
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)) };
2602
+ if (action === "register") {
2603
+ const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
2604
+ return { ok: true, process };
2605
+ }
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 };
2634
+ }
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);
2640
+ }
2641
+ if (action === "finish")
2642
+ return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
2643
+ if (action === "status")
2644
+ return { ok: true, processes: managedProcessStatus(context) };
2645
+ if (action === "reconcile")
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);
2648
+ }
1522
2649
  function dispatchStat(context, command, parsed) {
1523
2650
  if (command === "usage") {
1524
2651
  return getStoredFlowRunUsage(context, {
@@ -1699,16 +2826,37 @@ function dispatchSchema(context, command, parsed) {
1699
2826
  if (command === "validate") {
1700
2827
  const schemaDir = optionalOption(parsed, "schema-dir");
1701
2828
  const projectRoot = optionalOption(parsed, "project-root");
2829
+ const file = requiredOption(parsed, "file");
2830
+ // A generic validation command can still bind to the exact RUN without
2831
+ // searching project directories: run_id is a structural field in the
2832
+ // submitted artifact and the database owns its materialized run_root.
2833
+ const explicitRunId = optionalOption(parsed, "run");
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);
2837
+ const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
2838
+ const bound = projectRoot
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 ?? ""])
2840
+ : undefined;
1702
2841
  return validateSchema({
1703
2842
  schemaName: requiredOption(parsed, "schema"),
1704
- file: requiredOption(parsed, "file"),
2843
+ file,
2844
+ data,
1705
2845
  ...(schemaDir ? { schemaDir } : {}),
1706
2846
  ...(projectRoot ? { projectRoot } : {}),
2847
+ ...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
2848
+ ...(bound?.run_root ? { runRoot: bound.run_root } : {}),
1707
2849
  ddFlowHome: context.ddFlowHome
1708
2850
  });
1709
2851
  }
1710
2852
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
1711
2853
  }
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") };
2859
+ }
1712
2860
  async function dispatchLane(context, command, parsed) {
1713
2861
  if (command === "status") {
1714
2862
  return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
@@ -1719,24 +2867,14 @@ async function dispatchLane(context, command, parsed) {
1719
2867
  if (command === "waiter") {
1720
2868
  const action = requiredPosition(parsed, 0, "lane waiter action");
1721
2869
  if (action === "cancel") {
1722
- return cancelLaneWaiter(context, {
1723
- projectRoot: requiredOption(parsed, "project-root"),
1724
- lane: requiredOption(parsed, "lane"),
1725
- workerId: requiredOption(parsed, "worker-id"),
1726
- reason: requiredOption(parsed, "reason")
1727
- });
2870
+ return cancelLaneWaiter(context, laneWaiterCancelInput(parsed));
1728
2871
  }
1729
2872
  throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
1730
2873
  }
1731
2874
  if (command === "workspace") {
1732
2875
  const action = requiredPosition(parsed, 0, "lane workspace action");
1733
2876
  if (action === "set") {
1734
- return setLaneWorkspace(context, {
1735
- projectRoot: requiredOption(parsed, "project-root"),
1736
- lane: requiredOption(parsed, "lane"),
1737
- workspacePath: requiredOption(parsed, "path"),
1738
- branch: optionalOption(parsed, "branch")
1739
- });
2877
+ return setLaneWorkspace(context, laneWorkspaceInput(parsed));
1740
2878
  }
1741
2879
  if (action === "check") {
1742
2880
  return checkLaneWorkspace(context, {
@@ -1808,7 +2946,7 @@ async function dispatchLane(context, command, parsed) {
1808
2946
  }
1809
2947
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
1810
2948
  }
1811
- function dispatchCodexHooks(context, parsed) {
2949
+ function dispatchCodexHooks(context, parsed, prepared) {
1812
2950
  const action = requiredPosition(parsed, 0, "codex hooks action");
1813
2951
  if (action === "print") {
1814
2952
  return printCodexHooks(context, {
@@ -1830,7 +2968,7 @@ function dispatchCodexHooks(context, parsed) {
1830
2968
  target: optionalOption(parsed, "target"),
1831
2969
  profile: optionalOption(parsed, "profile"),
1832
2970
  yes: hasOption(parsed, "yes")
1833
- });
2971
+ }, prepared.codexHooks);
1834
2972
  }
1835
2973
  if (action === "remove") {
1836
2974
  return removeCodexHooks(context, {
@@ -1838,11 +2976,14 @@ function dispatchCodexHooks(context, parsed) {
1838
2976
  target: optionalOption(parsed, "target"),
1839
2977
  profile: optionalOption(parsed, "profile"),
1840
2978
  yes: hasOption(parsed, "yes")
1841
- });
2979
+ }, prepared.codexHooks);
1842
2980
  }
1843
2981
  throw new AppError("usage", `Unknown codex hooks action: ${action}`, 2);
1844
2982
  }
1845
- 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) {
1846
2987
  const action = requiredPosition(parsed, 0, "codex home action");
1847
2988
  if (action === "plan") {
1848
2989
  return planCodexHome(context, {
@@ -1853,12 +2994,7 @@ function dispatchCodexHome(context, parsed) {
1853
2994
  });
1854
2995
  }
1855
2996
  if (action === "init") {
1856
- return initCodexHome(context, {
1857
- projectRoot: requiredOption(parsed, "project-root"),
1858
- profile: optionalOption(parsed, "profile"),
1859
- sourceHome: optionalOption(parsed, "source-home"),
1860
- targetHome: optionalOption(parsed, "target-home")
1861
- });
2997
+ return initCodexHome(context, codexHomeInput(parsed), prepared.codexHome);
1862
2998
  }
1863
2999
  if (action === "status") {
1864
3000
  return getCodexHomeStatus(context, {
@@ -1881,11 +3017,14 @@ function dispatchCodexHome(context, parsed) {
1881
3017
  }
1882
3018
  throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
1883
3019
  }
1884
- function dispatchCodexHook(context, parsed, stdin) {
3020
+ async function dispatchCodexHook(context, parsed, stdin) {
1885
3021
  const action = requiredPosition(parsed, 0, "codex hook action");
1886
3022
  if (action === "handle") {
1887
3023
  const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
1888
- 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;
1889
3028
  const result = handleCodexHook(hookContext, {
1890
3029
  event: requiredOption(parsed, "event"),
1891
3030
  projectRoot: optionalOption(parsed, "project-root"),
@@ -1908,6 +3047,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
1908
3047
  }
1909
3048
  throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
1910
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
+ }
1911
3059
  function dispatchGrokEvent(context, parsed, stdin) {
1912
3060
  const action = requiredPosition(parsed, 0, "grok event action");
1913
3061
  if (action === "handle") {
@@ -1931,7 +3079,7 @@ function dispatchAgyEvent(context, parsed, stdin) {
1931
3079
  return handleAgyEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
1932
3080
  throw new AppError("usage", `Unknown agy event action: ${action}`, 2);
1933
3081
  }
1934
- function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
3082
+ function dispatchWorktree(context, command, parsed, scopeProjectRoot, prepared) {
1935
3083
  if (command === "plan") {
1936
3084
  return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1937
3085
  }
@@ -1942,13 +3090,13 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
1942
3090
  branch: requiredOption(parsed, "branch"),
1943
3091
  base: requiredOption(parsed, "base"),
1944
3092
  path: requiredOption(parsed, "path")
1945
- });
3093
+ }, prepared.worktreeCreate);
1946
3094
  }
1947
3095
  if (command === "status") {
1948
3096
  return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
1949
3097
  }
1950
3098
  if (command === "bootstrap") {
1951
- return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
3099
+ return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot }, prepared.worktreeBootstrap);
1952
3100
  }
1953
3101
  if (command === "close") {
1954
3102
  return closeWorktree(context, {
@@ -1960,52 +3108,50 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
1960
3108
  throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
1961
3109
  }
1962
3110
  function dispatchPlanItem(context, parsed, projectRoot) {
3111
+ return updatePlanItem(context, planItemInput(parsed, projectRoot));
3112
+ }
3113
+ function planItemInput(parsed, projectRoot) {
1963
3114
  const action = requiredPosition(parsed, 0, "plan item action");
1964
3115
  const protocolId = requiredPosition(parsed, 1, "protocol-id");
1965
3116
  const itemId = requiredPosition(parsed, 2, "item-id");
1966
3117
  if (action === "start") {
1967
- return startPlanItem(context, { protocolId, projectRoot, itemId });
3118
+ return { protocolId, projectRoot, itemId, action };
1968
3119
  }
1969
3120
  if (action === "done") {
1970
- return completePlanItem(context, {
3121
+ return {
3122
+ action,
1971
3123
  protocolId,
1972
3124
  projectRoot,
1973
3125
  itemId,
1974
3126
  summary: requiredOption(parsed, "summary"),
1975
3127
  evidence: parsed.options.get("evidence") ?? []
1976
- });
3128
+ };
1977
3129
  }
1978
3130
  if (action === "block") {
1979
- return blockPlanItem(context, {
3131
+ return {
3132
+ action,
1980
3133
  protocolId,
1981
3134
  projectRoot,
1982
3135
  itemId,
1983
3136
  reason: requiredOption(parsed, "reason"),
1984
3137
  userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
1985
- });
3138
+ };
1986
3139
  }
1987
3140
  if (action === "skip") {
1988
- return skipPlanItem(context, {
3141
+ return {
3142
+ action,
1989
3143
  protocolId,
1990
3144
  projectRoot,
1991
3145
  itemId,
1992
3146
  reason: requiredOption(parsed, "reason")
1993
- });
3147
+ };
1994
3148
  }
1995
3149
  throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
1996
3150
  }
1997
3151
  function readOptionalJsonFile(file) {
1998
3152
  if (!file)
1999
3153
  return undefined;
2000
- if (!fs.existsSync(file)) {
2001
- throw new AppError("not_found", `JSON file does not exist: ${file}`, 1);
2002
- }
2003
- try {
2004
- return JSON.parse(fs.readFileSync(file, "utf8"));
2005
- }
2006
- catch {
2007
- throw new AppError("validation", `JSON file is malformed: ${file}`, 2);
2008
- }
3154
+ return prepareJsonFile(file, "JSON input").value;
2009
3155
  }
2010
3156
  function parseFlowFlagOverrides(values) {
2011
3157
  const result = {};
@@ -2040,7 +3186,11 @@ function requiredPosition(parsed, index, label) {
2040
3186
  function optionalOption(parsed, key) {
2041
3187
  const values = parsed.options.get(key);
2042
3188
  const value = values?.[values.length - 1];
2043
- 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;
2044
3194
  }
2045
3195
  function allOptions(parsed, key) {
2046
3196
  return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
@@ -2205,8 +3355,8 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2205
3355
  if (family === "merge-queue") {
2206
3356
  return projectRootForMergeQueueMutation(context, command, parsed);
2207
3357
  }
2208
- if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
2209
- return requiredOption(parsed, "project-root");
3358
+ if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
3359
+ return resolvedScope ?? requiredOption(parsed, "project-root");
2210
3360
  }
2211
3361
  if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
2212
3362
  return requiredOption(parsed, "project-root");
@@ -2224,7 +3374,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2224
3374
  return requiredOption(parsed, "project-root");
2225
3375
  }
2226
3376
  if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? "")) {
2227
- return requiredOption(parsed, "project-root");
3377
+ return resolvedScope ?? requiredOption(parsed, "project-root");
2228
3378
  }
2229
3379
  if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
2230
3380
  return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
@@ -2238,7 +3388,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2238
3388
  if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
2239
3389
  return requiredOption(parsed, "project-root");
2240
3390
  }
2241
- 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"))
2242
3392
  return requiredOption(parsed, "project-root");
2243
3393
  if (family === "codex" && command === "hooks") {
2244
3394
  const action = requiredPosition(parsed, 0, "codex hooks action");
@@ -2275,14 +3425,36 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
2275
3425
  function protocolProjectRoot(context, protocolId) {
2276
3426
  return requireProtocol(context, protocolId).project_root;
2277
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
+ }
2278
3453
  function runnerIntake(context) {
2279
3454
  const input = context.value.task_input?.[0];
2280
3455
  if (!input?.path)
2281
3456
  throw new AppError("validation", "Runner SPECIFY context requires one ordered task_input file", 2);
2282
- const file = path.resolve(input.path);
2283
- if (!fs.existsSync(file) || !fs.statSync(file).isFile())
2284
- throw new AppError("not_found", "Runner SPECIFY task input is missing", 2, { task_input: file });
2285
- return fs.readFileSync(file, "utf8");
3457
+ return prepareTextFile(input.path, "Runner SPECIFY task input").text;
2286
3458
  }
2287
3459
  /** Stage services install context before their lifecycle transition. Keeping
2288
3460
  * this check here makes an incomplete service migration fail closed instead of