@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.
- package/CHANGELOG.md +561 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +136 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +569 -16
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- package/tools/repair-paused-run-status.mjs +59 -0
package/dist/cli/run-cli.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
52
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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(
|
|
100
|
-
: createContext(env, databaseModeForCommand(
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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(
|
|
471
|
-
checkRegistry: hasOption(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
662
|
-
throw new AppError("
|
|
663
|
-
|
|
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
|
|
704
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
882
|
-
|
|
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
|
|
885
|
-
runId
|
|
886
|
-
stage
|
|
887
|
-
workId
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3118
|
+
return { protocolId, projectRoot, itemId, action };
|
|
1968
3119
|
}
|
|
1969
3120
|
if (action === "done") {
|
|
1970
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|