@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
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 +526 -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 +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -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/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -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 +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -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 +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -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 +71 -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/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- 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 +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- 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 +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,56 +1,83 @@
|
|
|
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, bindLifecyclePublishedPacket, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, retainObservedLifecycleSemantics, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
|
|
3
10
|
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
11
|
+
import { dispatchNativeHook, isNativeHookIngress } from "./native-hook-ingress.js";
|
|
12
|
+
import { migrateStoreWriter } from "../storage/writer-migration.js";
|
|
4
13
|
import { helpForArgs } from "./help.js";
|
|
5
14
|
import { AppError, isAppError } from "../shared/errors.js";
|
|
15
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
6
16
|
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";
|
|
17
|
+
import { archiveProject, prepareProjectArchive, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
|
|
18
|
+
import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, prepareProtocolRegistration, requireProtocol, syncProtocolFromRun, prepareProtocolTransition, transitionProtocol } from "../services/protocols.js";
|
|
19
|
+
import { getPlanStatus, preparePlanItemMutation, updatePlanItem } from "../services/plans.js";
|
|
20
|
+
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
21
|
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";
|
|
22
|
+
import { cleanupApply, cleanupScan, prepareCleanupApply } from "../services/cleanup.js";
|
|
23
|
+
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";
|
|
24
|
+
import { bootstrapWorktree, closeWorktree, createWorktreeRecord, prepareWorktreeCreate, prepareWorktreeBootstrap, prepareWorktreeClose, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
|
|
25
|
+
import { acquireLaneLock, cancelLaneWaiter, prepareLaneWaiterCancellation, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, prepareLaneWorkspace, prepareLaneLock, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
|
|
26
|
+
import { getProjectConfigStatus, prepareProjectConfigValue, setProjectConfigValue } from "../services/config.js";
|
|
27
|
+
import { getCanonStatus, registerCanonRoot, resolveCanonRoot, prepareCanonRegistration } from "../services/canon.js";
|
|
18
28
|
import { getRuntimeStatus } from "../services/status.js";
|
|
19
29
|
import { getCliVersionReport } from "../services/build-info.js";
|
|
20
30
|
import { parseCommandArgs } from "../services/lifecycle-command.js";
|
|
21
|
-
import {
|
|
31
|
+
import { prepareError, prepareJsonFile, prepareJsonSchema, prepareJsonText, prepareTextFile } from "./input-preparation.js";
|
|
32
|
+
import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, prepareDashboardRefresh, prepareDashboardRender, prepareGlobalDashboardRender, prepareDashboardOptions, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
|
|
22
33
|
import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
|
|
23
|
-
import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
|
|
34
|
+
import { getFlowSessionStatus, prepareFlowSessionPayload, registerFlowSession, stopFlowSession, stopMergeWorker, syncFlowSessionUsage } from "../services/sessions.js";
|
|
24
35
|
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";
|
|
36
|
+
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
37
|
import { previewNextEntityId } from "../services/ids.js";
|
|
27
38
|
import { validateSchema } from "../services/schema-validation.js";
|
|
28
39
|
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";
|
|
40
|
+
import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
|
|
41
|
+
import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, prepareMigrationApply, verifyMigrationReport } from "../services/migrations.js";
|
|
31
42
|
import { publishProjectSummary } from "../services/project-summary.js";
|
|
32
43
|
import { requireProjectByRoot } from "../services/projects.js";
|
|
33
44
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
34
|
-
import { doctorEngines, bindRunEngine, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
45
|
+
import { doctorEngines, bindRunEngine, prepareRunEngineBind, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
35
46
|
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";
|
|
47
|
+
import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify, prepareVnextSpecifyStart } from "../services/vnext-specify.js";
|
|
48
|
+
import { finishVnextProtocolize, isVnextProtocolizeRun, prepareProtocolizeResultPath, prepareVnextProtocolizeStart, startVnextProtocolize } from "../services/vnext-protocolize.js";
|
|
49
|
+
import { finishVnextPlan, isVnextPlanRun, startVnextPlan, prepareVnextPlanStart, prepareVnextPlanFinish } from "../services/vnext-plan.js";
|
|
50
|
+
import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, prepareVnextPlanReviewStart, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
|
|
51
|
+
import { addVnextCodeRepair, finishVnextCode, prepareVnextCodeRepair, prepareVnextCodeStart, startVnextCode } from "../services/vnext-code.js";
|
|
52
|
+
import { finishVnextCodeReview, isVnextCodeReviewRun, prepareCodeReviewDecision, prepareVnextCodeReviewRepair, prepareVnextCodeReviewStart, startVnextCodeReview } from "../services/vnext-code-review.js";
|
|
53
|
+
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, prepareVnextMergeFinish, prepareVnextMergeStart, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
43
54
|
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";
|
|
55
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, prepareNativeObservations, reconcileVnextFanout } from "../services/vnext-fanout.js";
|
|
56
|
+
import { pauseStageForUser, resumeStageAfterUser, prepareStagePause, prepareStageResume } from "../services/stage-pause.js";
|
|
57
|
+
import { blockStageForRuntime, unblockStageAfterRuntimeRepair, prepareStageRuntimeBlock, prepareStageRuntimeUnblock } from "../services/stage-blocker.js";
|
|
47
58
|
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 {
|
|
53
|
-
import {
|
|
59
|
+
import { prepareWorkerPrompt, renderWorkerPrompt } from "../services/prompts.js";
|
|
60
|
+
import { finishStage, prepareStageFinish, startStage, prepareLegacyStageStart } from "../services/stage-lifecycle.js";
|
|
61
|
+
import { addWorkBatch, prepareWorkBatch, prepareWorkDeps, prepareWorkDelete, prepareWorkSettlement, prepareWorkRetry, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, prepareWorkResult, retryWork, showWork, startWork } from "../services/work-registry.js";
|
|
62
|
+
import { launchExternalWork } from "../services/external-work-launch.js";
|
|
63
|
+
import { createEvalBootstrapSnapshot, prepareEvalBootstrapSnapshot, createEvalRunSnapshot, prepareEvalRunSnapshotCreation, prepareVnextSpecifyRun, prepareEvalRunRestore, prepareEvalBootstrapRestore, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
|
|
64
|
+
import { forkRunFromSnapshot, prepareRunFork } from "../services/run-fork.js";
|
|
65
|
+
import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
|
|
66
|
+
import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
|
|
67
|
+
import { loadExternalStageContext, prepareExternalStageContext } from "../services/stage-context.js";
|
|
68
|
+
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
69
|
+
import { prepareCommandRoute } from "./command-inputs.js";
|
|
70
|
+
import { parseLifecycleCommand } from "../services/lifecycle-command.js";
|
|
71
|
+
import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
72
|
+
import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, prepareRuntimeScopeControl, prepareRuntimeScopeFence, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
|
|
73
|
+
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
74
|
+
import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
75
|
+
import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
|
|
76
|
+
import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
|
|
77
|
+
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
78
|
+
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
79
|
+
import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
80
|
+
import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
|
|
54
81
|
const defaultIo = {
|
|
55
82
|
stdout: process.stdout,
|
|
56
83
|
stderr: process.stderr,
|
|
@@ -58,9 +85,36 @@ const defaultIo = {
|
|
|
58
85
|
};
|
|
59
86
|
const flagValue = "__dd_flow_flag__";
|
|
60
87
|
export async function runCli(args, io = defaultIo, env = process.env) {
|
|
61
|
-
|
|
88
|
+
let output;
|
|
89
|
+
try {
|
|
90
|
+
output = parseOutputOptions(args);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const rejection = isAppError(error) ? error : new AppError("usage", error instanceof Error ? error.message : String(error), 2);
|
|
94
|
+
Object.assign(rejection.details, { phase: "prepare", effect: "no_effect", recoverable: true, ...rejection.details });
|
|
95
|
+
try {
|
|
96
|
+
const command = quote(["dd-flow", ...args]), event = observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID);
|
|
97
|
+
if (event) {
|
|
98
|
+
const context = createContext({ ...env }, "hook");
|
|
99
|
+
try {
|
|
100
|
+
observeLifecycleCommand(context, command, event)({ error: rejection });
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
context.db.close?.();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (persistence) {
|
|
108
|
+
rejection.details.lifecycle_diagnostic_error = String(persistence);
|
|
109
|
+
}
|
|
110
|
+
writeJson(io.stderr, { ok: false, error: { code: rejection.code, message: rejection.message, details: rejection.details } });
|
|
111
|
+
return rejection.exitCode;
|
|
112
|
+
}
|
|
62
113
|
const progress = progressForCommand(output.args);
|
|
63
114
|
let heartbeat;
|
|
115
|
+
let invocationSettlement;
|
|
116
|
+
let settleHook;
|
|
117
|
+
let diagnosticHome;
|
|
64
118
|
try {
|
|
65
119
|
if (output.args.length === 1 && output.args[0] === "--version") {
|
|
66
120
|
const payload = getCliVersionReport();
|
|
@@ -75,6 +129,20 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
75
129
|
io.stdout.write(`${help}\n`);
|
|
76
130
|
return 0;
|
|
77
131
|
}
|
|
132
|
+
const suppliedInvocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
|
|
133
|
+
if (isNativeHookIngress(output.args)) {
|
|
134
|
+
writeJson(io.stdout, await dispatchNativeHook(output.args, await readStdin(io.stdin), env));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
|
|
138
|
+
const controlArgs = parseArgs(output.args.slice(2));
|
|
139
|
+
requiredOption(controlArgs, "run");
|
|
140
|
+
requiredOption(controlArgs, "project-root");
|
|
141
|
+
}
|
|
142
|
+
if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
|
|
143
|
+
&& hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
|
|
144
|
+
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);
|
|
145
|
+
}
|
|
78
146
|
const classification = classifyCliOperation(output.args, env);
|
|
79
147
|
if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
|
|
80
148
|
throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
|
|
@@ -83,25 +151,158 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
83
151
|
});
|
|
84
152
|
}
|
|
85
153
|
const routerContext = createRouterContext(env);
|
|
154
|
+
const publicCommand = quote(["dd-flow", ...output.args]);
|
|
155
|
+
const nativeCommand = quote(["dd-flow", ...args]);
|
|
156
|
+
const lifecycleCommand = parseLifecycleCommand(publicCommand);
|
|
157
|
+
if (lifecycleCommand.kind === "standalone")
|
|
158
|
+
validateLifecyclePathAliases(output.args, lifecycleCommand.invocation.operation);
|
|
159
|
+
const observedInvocation = observedLifecycleInvocation(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID);
|
|
160
|
+
const nativeEvent = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
|
|
161
|
+
? observedLifecycleNativeEvent(routerContext, nativeCommand, env.DD_FLOW_DAEMON_ID) : null;
|
|
162
|
+
const diagnostic = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
|
|
163
|
+
? observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID, observedInvocation?.id) : null;
|
|
164
|
+
if (diagnostic && (!nativeEvent || Number(diagnostic.hook_event_id) > nativeEvent.id)) {
|
|
165
|
+
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 });
|
|
166
|
+
}
|
|
167
|
+
if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
|
|
168
|
+
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 });
|
|
169
|
+
}
|
|
170
|
+
const invocationId = observedInvocation?.id ?? suppliedInvocationId;
|
|
171
|
+
let boundInvocationArgs;
|
|
172
|
+
try {
|
|
173
|
+
boundInvocationArgs = observedInvocation
|
|
174
|
+
? resolveObservedLifecycleArgs(routerContext, observedInvocation.id, output.args)
|
|
175
|
+
: invocationId && invocationId !== suppliedInvocationId
|
|
176
|
+
? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
|
|
177
|
+
: output.args;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (!(observedInvocation && error instanceof AppError))
|
|
181
|
+
throw error;
|
|
182
|
+
const rejectionContext = createContext({ ...env }, "hook");
|
|
183
|
+
try {
|
|
184
|
+
Object.assign(error.details, settleLifecyclePreparationRejection(rejectionContext, observedInvocation.id, error));
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
rejectionContext.db.close?.();
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
const observedOperation = lifecycleCommand.kind === "standalone" ? lifecycleCommand.invocation.operation : null;
|
|
192
|
+
if (observedInvocation && !observedOperation)
|
|
193
|
+
throw new AppError("invocation_command_invalid", "Observed lifecycle command could not be parsed", 1);
|
|
194
|
+
const invocationScope = invocationId ? lifecycleInvocationScope({ ...routerContext, env }, invocationId) : undefined;
|
|
195
|
+
const expandedInvocationArgs = invocationScope && observedOperation
|
|
196
|
+
? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
|
|
197
|
+
: boundInvocationArgs;
|
|
198
|
+
const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
|
|
199
|
+
if (retainedOutput?.responseFile)
|
|
200
|
+
output.responseFile = retainedOutput.responseFile;
|
|
201
|
+
let invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
|
|
86
202
|
emitHumanProgress(io, output, progress, "start");
|
|
87
203
|
if (output.progressJsonl) {
|
|
88
204
|
writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
|
|
89
205
|
heartbeat = setInterval(() => writeProgressJsonl(io, "running", "Command is still running"), 15_000);
|
|
90
206
|
}
|
|
91
|
-
|
|
92
|
-
|
|
207
|
+
// Route by retained authority, so a mistyped project path reaches the CLI
|
|
208
|
+
// validator instead of selecting a different engine or failing preflight.
|
|
209
|
+
const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env));
|
|
210
|
+
assertAuxiliaryCommandScope({ ...routerContext, env }, publicCommand, scopeProjectRoot);
|
|
211
|
+
invocationArgs = resolveCommandPaths(routerContext, invocationArgs, scopeProjectRoot);
|
|
212
|
+
const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
|
|
93
213
|
const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
|
|
94
214
|
if (routed?.routed && !routed.inProcess) {
|
|
95
215
|
if (heartbeat)
|
|
96
216
|
clearInterval(heartbeat);
|
|
97
|
-
emitHumanProgress(io, output, progress, "done");
|
|
217
|
+
emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
|
|
218
|
+
if (output.progressJsonl && routed.exitCode !== 0)
|
|
219
|
+
writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
|
|
98
220
|
return routed.exitCode;
|
|
99
221
|
}
|
|
222
|
+
let preparedInput;
|
|
223
|
+
let preparedRoute;
|
|
224
|
+
if (!invocationId) {
|
|
225
|
+
const preparationContext = createContext({ ...env }, "read_existing");
|
|
226
|
+
try {
|
|
227
|
+
preparedInput = await prepareCliInput(invocationArgs, preparationContext, io, scopeProjectRoot, output.responseFile);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
preparationContext.db.close?.();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
100
233
|
const dispatchContext = routed?.inProcess
|
|
101
|
-
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(
|
|
102
|
-
: createContext(env, databaseModeForCommand(
|
|
103
|
-
|
|
104
|
-
|
|
234
|
+
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification))
|
|
235
|
+
: createContext({ ...env }, databaseModeForCommand(invocationArgs, classification));
|
|
236
|
+
diagnosticHome = dispatchContext.ddFlowHome;
|
|
237
|
+
dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
|
|
238
|
+
let dispatchArgs = invocationArgs;
|
|
239
|
+
let retainedOutcome;
|
|
240
|
+
// Issued transports may deliver their receipt asynchronously. Observe only
|
|
241
|
+
// after admission; retained outcomes must not need a fresh hook receipt.
|
|
242
|
+
if (invocationId) {
|
|
243
|
+
const command = quote(["dd-flow", ...invocationArgs]);
|
|
244
|
+
const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
|
|
245
|
+
validateArgs: () => {
|
|
246
|
+
try {
|
|
247
|
+
preparedRoute = prepareCommandRoute(invocationArgs);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error instanceof AppError && error.code === "usage" && ["invocation-id", "hook-event-id"].includes(String(error.details.parameter ?? ""))) {
|
|
251
|
+
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 });
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
prepare: async () => { preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile, preparedRoute); },
|
|
257
|
+
assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
|
|
258
|
+
if (admission.replay) {
|
|
259
|
+
retainedOutcome = admission.outcome;
|
|
260
|
+
settleHook = undefined;
|
|
261
|
+
delete dispatchContext.observeLifecycleHook;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
invocationSettlement = { context: dispatchContext, id: invocationId };
|
|
265
|
+
if (observedInvocation)
|
|
266
|
+
retainObservedLifecycleSemantics(dispatchContext, invocationId, invocationArgs);
|
|
267
|
+
dispatchArgs = [...invocationArgs, "--hook-event-id", admission.eventKey];
|
|
268
|
+
preparedInput?.route?.parsed.options.set("hook-event-id", [admission.eventKey]);
|
|
269
|
+
dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
|
|
270
|
+
dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (retainedOutcome?.error) {
|
|
274
|
+
const error = retainedOutcome.error;
|
|
275
|
+
throw new AppError(error.code, error.message, error.exitCode, error.details);
|
|
276
|
+
}
|
|
277
|
+
if (!retainedOutcome && !preparedInput)
|
|
278
|
+
preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile);
|
|
279
|
+
if (!retainedOutcome)
|
|
280
|
+
preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
|
|
281
|
+
if (!retainedOutcome) {
|
|
282
|
+
const command = !invocationId ? nativeCommand : quote(["dd-flow", ...dispatchArgs]);
|
|
283
|
+
const event = !invocationId ? nativeEvent : null;
|
|
284
|
+
settleHook = observeLifecycleCommand(dispatchContext, command, event ?? undefined);
|
|
285
|
+
}
|
|
286
|
+
const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output, preparedInput);
|
|
287
|
+
if (!retainedOutcome && invocationArgs[0] === "stage" && invocationArgs[1] === "start"
|
|
288
|
+
&& result && typeof result === "object" && "prompt_path" in result && typeof result.prompt_path === "string") {
|
|
289
|
+
try {
|
|
290
|
+
bindLifecyclePublishedPacket(dispatchContext, fs.readFileSync(result.prompt_path, "utf8"));
|
|
291
|
+
}
|
|
292
|
+
catch (cause) {
|
|
293
|
+
throw new AppError("invocation_packet_binding_failed", "Stage started but its published lifecycle assignments could not be retained", 1, { effect: "committed", cause: String(cause) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const completedHook = settleHook;
|
|
297
|
+
settleHook = undefined;
|
|
298
|
+
completedHook?.({ result });
|
|
299
|
+
if (invocationSettlement) {
|
|
300
|
+
const completed = invocationSettlement;
|
|
301
|
+
invocationSettlement = undefined;
|
|
302
|
+
// A failure to persist an already completed result is an unknown reply,
|
|
303
|
+
// not a failed operation eligible for a new productive retry.
|
|
304
|
+
settleLifecycleInvocation(completed.context, completed.id, { result });
|
|
305
|
+
}
|
|
105
306
|
const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
|
|
106
307
|
const resultWithWarnings = addWarnings(result, refreshWarnings);
|
|
107
308
|
if (heartbeat)
|
|
@@ -117,17 +318,82 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
117
318
|
io.stdout.write(renderHumanResult(output.args, payload));
|
|
118
319
|
return completedCommandExitCode(payload);
|
|
119
320
|
}
|
|
120
|
-
catch (
|
|
321
|
+
catch (caught) {
|
|
322
|
+
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 } });
|
|
323
|
+
if (!settleHook && !invocationSettlement) {
|
|
324
|
+
let earlyContext;
|
|
325
|
+
try {
|
|
326
|
+
const command = quote(["dd-flow", ...args]);
|
|
327
|
+
const router = createRouterContext(env);
|
|
328
|
+
const event = observedLifecycleNativeEvent(router, command, env.DD_FLOW_DAEMON_ID);
|
|
329
|
+
// A correctable prepare rejection precedes the terminal command's
|
|
330
|
+
// single-use claim. Correlate its unique hook by the same lifecycle
|
|
331
|
+
// match key used at dispatch, including compound provider tool shells;
|
|
332
|
+
// otherwise a repaired retry sees two fresh, unclaimed events.
|
|
333
|
+
if (event || (error.details.phase === "prepare" && error.details.effect === "no_effect"
|
|
334
|
+
&& fs.existsSync(path.join(router.ddFlowHome, "db.sqlite")))) {
|
|
335
|
+
earlyContext = createContext({ ...env }, "hook");
|
|
336
|
+
observeLifecycleCommand(earlyContext, command, event ?? undefined)({ error });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (persistence) {
|
|
340
|
+
error.details.lifecycle_diagnostic_error = String(persistence);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
earlyContext?.db.close?.();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
|
|
347
|
+
try {
|
|
348
|
+
const directory = path.join(diagnosticHome, "diagnostics", "cli");
|
|
349
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
350
|
+
const artifact = path.join(directory, `${randomUUID()}.json`);
|
|
351
|
+
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 });
|
|
352
|
+
error.details.diagnostic_artifact = artifact;
|
|
353
|
+
}
|
|
354
|
+
catch (secondary) {
|
|
355
|
+
io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
121
358
|
if (heartbeat)
|
|
122
359
|
clearInterval(heartbeat);
|
|
360
|
+
if (invocationSettlement && error.details.effect !== "committed") {
|
|
361
|
+
try {
|
|
362
|
+
if (isAppError(error))
|
|
363
|
+
settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
|
|
364
|
+
else
|
|
365
|
+
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
366
|
+
}
|
|
367
|
+
catch (persistenceError) {
|
|
368
|
+
io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (settleHook) {
|
|
372
|
+
try {
|
|
373
|
+
settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
|
|
374
|
+
}
|
|
375
|
+
catch (persistence) {
|
|
376
|
+
io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
123
379
|
const payload = isAppError(error)
|
|
124
380
|
? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
|
|
125
381
|
: { ok: false, error: { code: "unexpected", message: String(error) } };
|
|
382
|
+
progress.splice(0, progress.length, ...progress.filter(event => event.phase !== "done"));
|
|
383
|
+
if (progress.length)
|
|
384
|
+
progress.push({ phase: "failed", message: payload.error.message, at: new Date().toISOString() });
|
|
126
385
|
emitHumanProgress(io, output, progress, "failed");
|
|
127
386
|
if (output.progressJsonl)
|
|
128
387
|
writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
|
|
129
388
|
const errorPayload = attachProgress(payload, progress, output);
|
|
130
|
-
|
|
389
|
+
let visible = errorPayload;
|
|
390
|
+
try {
|
|
391
|
+
if (error.details.parameter !== "response-file")
|
|
392
|
+
visible = writeResponseFile(errorPayload, output);
|
|
393
|
+
}
|
|
394
|
+
catch (publication) {
|
|
395
|
+
error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
|
|
396
|
+
}
|
|
131
397
|
if (output.json && output.progressJsonl)
|
|
132
398
|
writeJson(io.stdout, errorPayload);
|
|
133
399
|
else if (output.json)
|
|
@@ -150,8 +416,25 @@ function resolveProjectRootBeforeRouting(args, env) {
|
|
|
150
416
|
}
|
|
151
417
|
}
|
|
152
418
|
function databaseModeForCommand(args, classification) {
|
|
419
|
+
if (isNativeHookIngress(args))
|
|
420
|
+
return "hook";
|
|
153
421
|
const [family, command] = args;
|
|
422
|
+
// Managed attempts are issued only from a prepared store. Opening that
|
|
423
|
+
// existing writer must not run migrations before their input is admitted.
|
|
424
|
+
if (parseArgs(args.slice(2)).options.has("invocation-id"))
|
|
425
|
+
return "hook";
|
|
426
|
+
// Native hook ingress must remain synchronous, but it must not run schema
|
|
427
|
+
// migration/VACUUM while the provider is waiting for its decision. Startup
|
|
428
|
+
// prepares the store; the hook mode only verifies the prepared contract and
|
|
429
|
+
// records the receipt.
|
|
430
|
+
if (family === "codex" && command === "hook" && args[2] === "handle") {
|
|
431
|
+
// Only the participating PreToolUse ingress needs the prepared hot-path
|
|
432
|
+
// contract. SessionStart/Stop/PostToolUse are observational no-ops here
|
|
433
|
+
// and retain the ordinary startup path for backwards compatibility.
|
|
434
|
+
return optionalOption(parseArgs(args.slice(3)), "event") === "PreToolUse" ? "hook" : "initialize";
|
|
435
|
+
}
|
|
154
436
|
if (family === "engine" ||
|
|
437
|
+
family === "storage" && command === "migrate-writer" ||
|
|
155
438
|
family === "version" ||
|
|
156
439
|
family === "schema" ||
|
|
157
440
|
family === "canon" && command === "resolve" ||
|
|
@@ -207,8 +490,10 @@ function parseOutputOptions(args) {
|
|
|
207
490
|
continue;
|
|
208
491
|
}
|
|
209
492
|
if (arg === "--response-file") {
|
|
493
|
+
if (responseFile !== null)
|
|
494
|
+
throw new AppError("usage", "--response-file may be supplied only once", 2);
|
|
210
495
|
const file = args[++index];
|
|
211
|
-
if (!file)
|
|
496
|
+
if (!file?.trim() || file.startsWith("--"))
|
|
212
497
|
throw new AppError("usage", "--response-file requires a path", 2);
|
|
213
498
|
responseFile = file;
|
|
214
499
|
continue;
|
|
@@ -220,6 +505,33 @@ function parseOutputOptions(args) {
|
|
|
220
505
|
const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
|
|
221
506
|
return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
|
|
222
507
|
}
|
|
508
|
+
/** Validate the response destination without creating it. The response is an
|
|
509
|
+
* optional publication, but a known-bad destination should never be found
|
|
510
|
+
* only after a successful lifecycle mutation. */
|
|
511
|
+
function prepareResponseDestination(value) {
|
|
512
|
+
const file = path.resolve(value);
|
|
513
|
+
const parent = path.dirname(file);
|
|
514
|
+
try {
|
|
515
|
+
let ancestor = parent;
|
|
516
|
+
for (;;) {
|
|
517
|
+
try {
|
|
518
|
+
if (!fs.statSync(ancestor).isDirectory())
|
|
519
|
+
throw new AppError("response_file_parent_invalid", "--response-file parent must be a directory", 2, { file, parent });
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (error.code !== "ENOENT" || path.dirname(ancestor) === ancestor)
|
|
524
|
+
throw error;
|
|
525
|
+
ancestor = path.dirname(ancestor);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (fs.existsSync(file) && !fs.statSync(file).isFile())
|
|
529
|
+
throw new AppError("response_file_invalid", "--response-file must name a regular file", 2, { file });
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
return prepareError(error, { parameter: "response-file", file, parent });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
223
535
|
/** Persist the complete machine response for harnesses that cap tool output.
|
|
224
536
|
* The visible response keeps routing fields but omits the potentially large
|
|
225
537
|
* rendered prompt; an agent can safely read the named file without rerunning
|
|
@@ -500,27 +812,650 @@ function stringValue(value) {
|
|
|
500
812
|
function shortSha(value) {
|
|
501
813
|
return value ? value.slice(0, 12) : "unknown";
|
|
502
814
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
815
|
+
/** All caller supplied file/stdin data is consumed here, before a managed
|
|
816
|
+
* invocation is allowed to claim execution or a stage claims its hook event. */
|
|
817
|
+
async function prepareCliInput(...args) {
|
|
818
|
+
try {
|
|
819
|
+
return await prepareCliInputValue(...args);
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
return prepareError(error);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function prepareCliInputValue(args, context, io, scopeProjectRoot, responseFile = null, route = prepareCommandRoute(args)) {
|
|
826
|
+
const { family, command, parsed } = route;
|
|
827
|
+
const prepared = { route };
|
|
828
|
+
if (optionalOption(parsed, "reason-file")) {
|
|
829
|
+
const value = prepareJsonFile(requiredOption(parsed, "reason-file"), "Check recovery evidence").value;
|
|
830
|
+
if (!value || typeof value.reason !== "string" || !value.reason.trim() || Object.keys(value).some(key => key !== "reason"))
|
|
831
|
+
throw new AppError("validation", "Recovery evidence must be a JSON object with one nonempty reason", 2);
|
|
832
|
+
prepared.retryReason = value.reason;
|
|
833
|
+
}
|
|
834
|
+
else if (optionalOption(parsed, "retry-check"))
|
|
835
|
+
prepared.retryReason = optionalOption(parsed, "reason");
|
|
836
|
+
if (route.key === "project register")
|
|
837
|
+
resolveProjectRoot(requiredOption(parsed, "root"));
|
|
838
|
+
if (route.key === "project archive")
|
|
839
|
+
prepareProjectArchive(context, { idOrAlias: parsed.positional[0], root: optionalOption(parsed, "root"), reason: requiredOption(parsed, "reason") });
|
|
840
|
+
if (route.key === "protocol register")
|
|
841
|
+
prepareProtocolRegistration(context, { handshakeId: requiredPosition(parsed, 0, "handshake-id"), projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "workspace-path") });
|
|
842
|
+
if (route.key === "canon register")
|
|
843
|
+
prepared.canonRegistration = prepareCanonRegistration(context, { root: requiredOption(parsed, "root") });
|
|
844
|
+
if (route.key === "run attach-stage")
|
|
845
|
+
prepareFlowRunStageAttachment(context, stageAttachmentInput(parsed));
|
|
846
|
+
if (route.key === "run complete-stage")
|
|
847
|
+
prepareFlowRunStageCompletion(context, stageCompletionInput(parsed));
|
|
848
|
+
if (route.key === "run override")
|
|
849
|
+
prepareFlowRunCompletion(context, runOverrideInput(parsed));
|
|
850
|
+
if (route.key === "engine bind-run")
|
|
851
|
+
prepared.engineBind = prepareRunEngineBind(context, engineBindInput(parsed));
|
|
852
|
+
if (route.key === "worktree create")
|
|
853
|
+
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") });
|
|
854
|
+
if (route.key === "worktree bootstrap")
|
|
855
|
+
prepared.worktreeBootstrap = prepareWorktreeBootstrap(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree bootstrap") });
|
|
856
|
+
if (route.key === "worktree close")
|
|
857
|
+
prepareWorktreeClose(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree close"), mode: requiredOption(parsed, "mode") });
|
|
858
|
+
if (route.key.startsWith("plan item "))
|
|
859
|
+
preparePlanItemMutation(context, planItemInput(parsed, requiredScopeProjectRoot(scopeProjectRoot, route.key)));
|
|
860
|
+
if (route.key === "lane workspace set")
|
|
861
|
+
prepareLaneWorkspace(context, laneWorkspaceInput(parsed));
|
|
862
|
+
if (route.key === "lane waiter cancel")
|
|
863
|
+
prepareLaneWaiterCancellation(context, laneWaiterCancelInput(parsed));
|
|
864
|
+
if (route.key === "merge-queue complete")
|
|
865
|
+
prepareMergeJobCompletion(context, { ...mergeJobTarget(parsed, scopeProjectRoot), summary: requiredOption(parsed, "summary") });
|
|
866
|
+
if (route.key === "merge-queue cancel")
|
|
867
|
+
prepareMergeQueueCancellation(context, mergeCancellationInput(parsed, scopeProjectRoot));
|
|
868
|
+
if (route.key === "merge-queue wait-next")
|
|
869
|
+
prepareMergeQueueWait(context, mergeWaitInput(parsed, scopeProjectRoot));
|
|
870
|
+
if (route.key === "merge-queue next" || route.key === "merge-queue claim")
|
|
871
|
+
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") } : {}) });
|
|
872
|
+
if (route.key === "merge-queue note")
|
|
873
|
+
prepareMergeJobNote(context, { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue note"), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
|
|
874
|
+
if (route.key === "merge bundle complete")
|
|
875
|
+
prepareMergeBundleCompletion(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), summary: requiredOption(parsed, "summary") });
|
|
876
|
+
if (route.key === "merge bundle claim")
|
|
877
|
+
prepareMergeBundleClaim(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed) });
|
|
878
|
+
if (route.key === "merge bundle fail")
|
|
879
|
+
prepareMergeBundleFailure(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed), workerId: requiredWorkerId(parsed), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
|
|
880
|
+
if (route.key === "merge-queue fail")
|
|
881
|
+
prepareMergeJobFailure(context, { ...mergeJobTarget(parsed, scopeProjectRoot), reason: requiredOption(parsed, "reason"), requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue") });
|
|
882
|
+
if (route.key.startsWith("lane lock ") && route.key !== "lane lock status") {
|
|
883
|
+
const action = requiredPosition(parsed, 0, "lane lock action");
|
|
884
|
+
prepareLaneLock(context, {
|
|
885
|
+
projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"),
|
|
886
|
+
workspacePath: workspacePathOption(parsed), leaseToken: optionalOption(parsed, "lease-token"), reason: optionalOption(parsed, "reason"),
|
|
887
|
+
ttlSeconds: optionalNumber(parsed, "ttl"), timeoutSeconds: optionalNumber(parsed, "timeout"), pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
|
|
888
|
+
}, action);
|
|
889
|
+
}
|
|
890
|
+
if (route.key === "run recovery begin")
|
|
891
|
+
prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "interruption-json"), "interruption");
|
|
892
|
+
if (route.key === "run recovery seal")
|
|
893
|
+
prepared.recoveryPayload = prepareRecoveryPayload(requiredOption(parsed, "settlement-json"), "settlement");
|
|
894
|
+
if (route.key === "run recovery capture")
|
|
895
|
+
prepared.recoveryCapture = prepareRecoveryCapture(context, recoveryCaptureInput(parsed));
|
|
896
|
+
if (route.key === "run recovery resume")
|
|
897
|
+
prepared.recoveryResume = prepareRunRecoveryResume(context, recoveryResumeInput(parsed));
|
|
898
|
+
if (route.key === "run drive answer")
|
|
899
|
+
prepared.controllerAnswer = prepareRunControllerAnswer(context, controllerAnswerInput(parsed));
|
|
900
|
+
if (route.key === "run drive launch")
|
|
901
|
+
prepared.controllerLaunch = prepareRunControllerLaunch(context, controllerLaunchInput(parsed));
|
|
902
|
+
if (route.key === "run drive context")
|
|
903
|
+
prepared.controllerContext = prepareRunControllerContext(context, controllerContextInput(parsed));
|
|
904
|
+
if (route.key === "protocol transition" || route.key === "transition")
|
|
905
|
+
prepared.protocolTransition = prepareProtocolTransition(context, protocolTransitionInput(parsed, scopeProjectRoot ?? resolveOperationProjectRoot(context, args)));
|
|
906
|
+
if (route.key === "codex home init")
|
|
907
|
+
prepared.codexHome = prepareCodexHome(context, codexHomeInput(parsed));
|
|
908
|
+
if (route.key === "codex home remove")
|
|
909
|
+
prepareCodexHomeRemoval(context, { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), mode: requiredOption(parsed, "mode") });
|
|
910
|
+
if (["codex hooks install", "codex hooks remove"].includes(route.key))
|
|
911
|
+
prepared.codexHooks = prepareCodexHooks(context, { projectRoot: requiredOption(parsed, "project-root"), target: optionalOption(parsed, "target"), profile: optionalOption(parsed, "profile"), yes: hasOption(parsed, "yes") });
|
|
912
|
+
if (route.key === "migration apply")
|
|
913
|
+
prepared.migrationApply = prepareMigrationApply(context, migrationApplyInput(parsed, classifyCliOperation(args, context.env)));
|
|
914
|
+
if (route.key === "migration verify")
|
|
915
|
+
prepared.migrationVerification = verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
|
|
916
|
+
if (responseFile)
|
|
917
|
+
prepareResponseDestination(responseFile);
|
|
918
|
+
if (family === "dashboard") {
|
|
919
|
+
const action = dashboardAction(command);
|
|
920
|
+
prepareDashboardOptions({ format: optionalOption(parsed, "format"), viewer: optionalOption(parsed, "viewer"), open: optionalOption(parsed, "open") });
|
|
921
|
+
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") });
|
|
922
|
+
if (action === "refresh" && target.kind !== "global" && optionalOption(parsed, "output") !== undefined) {
|
|
923
|
+
throw new AppError("validation", "--output is supported only for global dashboard refresh; use dashboard render for a custom project/protocol output", 2, { parameter: "output" });
|
|
924
|
+
}
|
|
925
|
+
if (target.kind === "protocol")
|
|
926
|
+
requireProtocol(context, target.protocolId, target.project.id);
|
|
927
|
+
if (action !== "data") {
|
|
928
|
+
if (target.kind === "global" || target.kind === "all_projects") {
|
|
929
|
+
prepareGlobalDashboardRender(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? (command?.endsWith("-global") ? undefined : "html") });
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
const format = optionalOption(parsed, "format") ?? (action === "open" || hasOption(parsed, "project") ? "html" : undefined);
|
|
933
|
+
const input = { projectRoot: target.project.root, format, ...(target.kind === "protocol" ? { protocol: target.protocolId } : {}) };
|
|
934
|
+
if (action === "refresh")
|
|
935
|
+
prepareDashboardRefresh(context, { ...input, open: optionalOption(parsed, "open") });
|
|
936
|
+
else if (action === "render")
|
|
937
|
+
prepareDashboardRender(context, { ...input, output: optionalOption(parsed, "output") });
|
|
938
|
+
else if (target.kind === "protocol") {
|
|
939
|
+
if (format !== "html")
|
|
940
|
+
throw new AppError("validation", "Protocol dashboard open supports --format html", 2, { protocol: target.protocolId });
|
|
941
|
+
prepareDashboardRender(context, input);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (route.key === "prompt render") {
|
|
947
|
+
const input = workerPromptInput(parsed);
|
|
948
|
+
prepared.workerPrompt = { input, value: prepareWorkerPrompt(context, input) };
|
|
949
|
+
}
|
|
950
|
+
if (route.key === "run snapshot restore" || route.key === "run snapshot bootstrap restore") {
|
|
951
|
+
try {
|
|
952
|
+
const input = snapshotRestoreInput(parsed);
|
|
953
|
+
if (route.key === "run snapshot restore")
|
|
954
|
+
prepared.snapshotRestore = prepareEvalRunRestore(context, input);
|
|
955
|
+
else
|
|
956
|
+
prepared.bootstrapRestore = prepareEvalBootstrapRestore(input);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
return prepareError(error, { parameter: "snapshot/project-root" });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (route.key === "run snapshot bootstrap create") {
|
|
963
|
+
try {
|
|
964
|
+
prepareEvalBootstrapSnapshot(context, { projectRoot: requiredOption(parsed, "project-root"), output: requiredOption(parsed, "output") });
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
return prepareError(error, { parameter: "project-root/output" });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (route.key === "run snapshot create") {
|
|
971
|
+
prepareEvalRunSnapshotCreation(context, snapshotCreateInput(parsed));
|
|
972
|
+
}
|
|
973
|
+
if (route.key === "run start") {
|
|
974
|
+
try {
|
|
975
|
+
const input = runStartInput(parsed);
|
|
976
|
+
prepared.runStart = { input, value: prepareFlowRunStart(context, input) };
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
return prepareError(error, { parameter: "run start" });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (route.key === "run fork") {
|
|
983
|
+
try {
|
|
984
|
+
prepared.runFork = prepareRunFork(runForkInput(parsed));
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
return prepareError(error, { parameter: "from/output" });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (["runtime scope fence", "runtime scope stop"].includes(route.key))
|
|
991
|
+
prepareRuntimeScopeFence(requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"));
|
|
992
|
+
if (route.key === "runtime scope resume")
|
|
993
|
+
prepareRuntimeScopeResumeRequest(context, runtimeScopeResumeInput(parsed));
|
|
994
|
+
if (route.key === "runtime scope reconcile")
|
|
995
|
+
prepareRuntimeScopeReconciliation(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id"));
|
|
996
|
+
if (route.key === "runtime scope serve")
|
|
997
|
+
prepareRuntimeScopeServing(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")));
|
|
998
|
+
if (route.key === "runtime scope control") {
|
|
999
|
+
try {
|
|
1000
|
+
const raw = optionalOption(parsed, "manifest-json");
|
|
1001
|
+
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);
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
return prepareError(error, { parameter: "scope-id/request-id/manifest-json" });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (route.key === "runtime process register") {
|
|
1008
|
+
prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
|
|
1009
|
+
}
|
|
1010
|
+
if (route.key === "runtime process heartbeat")
|
|
1011
|
+
prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
|
|
1012
|
+
if (route.key === "runtime process finish")
|
|
1013
|
+
prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
|
|
1014
|
+
if (route.key === "runtime process confirm")
|
|
1015
|
+
prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
|
|
1016
|
+
if (route.key === "runtime process stop")
|
|
1017
|
+
prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
|
|
1018
|
+
if (route.key === "runtime process reconcile")
|
|
1019
|
+
prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
|
|
1020
|
+
if (route.key === "runtime process release-turn") {
|
|
1021
|
+
prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
|
|
1022
|
+
}
|
|
1023
|
+
if (route.key === "runtime process start") {
|
|
1024
|
+
try {
|
|
1025
|
+
prepareRuntimeService(context, runtimeServiceInput(parsed));
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
return prepareError(error, { parameter: "runtime process start" });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (family === "work" && command === "retry") {
|
|
1032
|
+
try {
|
|
1033
|
+
prepareWorkRetry(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
return prepareError(error, { parameter: "work-id/reason" });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (family === "work" && (command === "fail" || command === "cancel")) {
|
|
1040
|
+
try {
|
|
1041
|
+
prepareWorkSettlement(context, requiredPosition(parsed, 0, "work-id"), command === "fail" ? "failed" : "cancelled", requiredOption(parsed, "reason"));
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
return prepareError(error, { parameter: "work-id/reason" });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (family === "work" && (command === "deps" || command === "delete")) {
|
|
1048
|
+
try {
|
|
1049
|
+
if (command === "delete")
|
|
1050
|
+
prepareWorkDelete(context, requiredPosition(parsed, 0, "work-id"));
|
|
1051
|
+
else
|
|
1052
|
+
prepareWorkDeps(context, { workId: requiredPosition(parsed, 1, "work-id"), action: requiredPosition(parsed, 0, "action"), on: parsed.options.get("on") ?? [] });
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
return prepareError(error, { parameter: "work-id/on" });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (route.key === "cleanup apply") {
|
|
1059
|
+
const planFile = requiredOption(parsed, "plan-file");
|
|
1060
|
+
try {
|
|
1061
|
+
prepared.cleanup = prepareCleanupApply(context, { projectRoot: requiredOption(parsed, "project-root"), planFile, reason: requiredOption(parsed, "reason"), force: hasOption(parsed, "force") });
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
return prepareError(error, { parameter: "plan-file", file: planFile });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (route.key === "stage block" || route.key === "stage unblock") {
|
|
1068
|
+
const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
|
|
1069
|
+
try {
|
|
1070
|
+
if (route.key === "stage block") {
|
|
1071
|
+
const summaryFile = optionalOption(parsed, "summary-file"), summaryStdin = hasOption(parsed, "summary-stdin");
|
|
1072
|
+
if (Boolean(summaryFile) === summaryStdin)
|
|
1073
|
+
throw new AppError("usage", "stage block requires exactly one of --summary-file or --summary-stdin", 2);
|
|
1074
|
+
prepared.interactionText = summaryFile ? prepareTextFile(summaryFile, "Runtime block summary").text : await readStdin(io.stdin);
|
|
1075
|
+
prepareStageRuntimeBlock(context, { ...input, kind: requiredOption(parsed, "kind"), code: requiredOption(parsed, "code"), summary: prepared.interactionText, retryable: hasOption(parsed, "retryable") });
|
|
1076
|
+
}
|
|
1077
|
+
else
|
|
1078
|
+
prepareStageRuntimeUnblock(context, input);
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
return prepareError(error, { parameter: "stage/work/summary" });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (route.key === "zcode invocation prepare") {
|
|
1085
|
+
const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label: "invocation issuance" }).value;
|
|
1086
|
+
const payload = value && typeof value === "object" ? value : {};
|
|
1087
|
+
try {
|
|
1088
|
+
prepared.lifecycleIssuance = prepareLifecycleIssuance(payload.command, payload.scope);
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
if (error instanceof AppError && ["invocation_command_invalid", "invocation_scope_invalid"].includes(error.code))
|
|
1092
|
+
return prepareError(new AppError("usage", error.message, 2, { cause_code: error.code }), { parameter: "stdin" });
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
assertLifecycleInvocationCurrent(context, prepared.lifecycleIssuance.scope, prepared.lifecycleIssuance.command);
|
|
1096
|
+
}
|
|
1097
|
+
if (route.key === "work add-batch") {
|
|
1098
|
+
const file = requiredOption(parsed, "file");
|
|
1099
|
+
try {
|
|
1100
|
+
prepared.workBatch = prepareWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file });
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
return prepareError(error, { parameter: "file/parent", file });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (route.key === "work repair add") {
|
|
1107
|
+
const taskFile = optionalOption(parsed, "task-file"), taskStdin = hasOption(parsed, "task-stdin");
|
|
1108
|
+
if (Boolean(taskFile) === taskStdin)
|
|
1109
|
+
throw new AppError("usage", "work repair add requires exactly one of --task-file or --task-stdin", 2);
|
|
1110
|
+
if (taskStdin && io.stdin?.isTTY)
|
|
1111
|
+
throw new AppError("usage", "--task-stdin requires piped or redirected input", 2);
|
|
1112
|
+
const task = taskFile ? prepareTextFile(taskFile, "Repair task").text : await readStdin(io.stdin);
|
|
1113
|
+
prepared.workRepair = prepareCliWorkRepair(context, parsed, task, scopeProjectRoot);
|
|
1114
|
+
}
|
|
1115
|
+
if (route.key === "run config set" || route.key === "run vars set") {
|
|
1116
|
+
const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), key: requiredOption(parsed, "key"), value: requiredOption(parsed, "value") };
|
|
1117
|
+
try {
|
|
1118
|
+
if (route.key === "run config set")
|
|
1119
|
+
prepareFlowRunConfig(context, { ...input, reason: requiredOption(parsed, "reason") });
|
|
1120
|
+
else
|
|
1121
|
+
prepareFlowRunVariable(context, input);
|
|
1122
|
+
}
|
|
1123
|
+
catch (error) {
|
|
1124
|
+
return prepareError(error, { parameter: "run config/vars" });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (route.key === "schema validate") {
|
|
1128
|
+
try {
|
|
1129
|
+
prepared.schemaValidation = dispatchSchema(context, command, parsed);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
return prepareError(error, { parameter: "file", file: requiredOption(parsed, "file") });
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (route.key === "project config set") {
|
|
1136
|
+
try {
|
|
1137
|
+
prepareProjectConfigValue(requiredOption(parsed, "key"), requiredOption(parsed, "value"));
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
return prepareError(error, { parameter: "key/value" });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (family === "session" && command === "register") {
|
|
1144
|
+
try {
|
|
1145
|
+
prepared.sessionPayload = prepareFlowSessionPayload({ payloadBase64: optionalOption(parsed, "payload-base64"), payloadFile: optionalOption(parsed, "payload-file"), payloadJson: optionalOption(parsed, "payload-json") });
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
return prepareError(error, { parameter: "session payload" });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const run = (projectRoot, runId) => {
|
|
1152
|
+
try {
|
|
1153
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
|
|
1154
|
+
const row = context.db.get("SELECT run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
|
|
1155
|
+
if (!row)
|
|
1156
|
+
throw new AppError("not_found", "RUN is not registered", 2, { run_id: runId });
|
|
1157
|
+
if (!row.run_root)
|
|
1158
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId });
|
|
1159
|
+
return { runRoot: row.run_root, workspaceRoot: row.workspace_root, indexJson: row.index_json };
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
return prepareError(error, { parameter: "run-id", run_id: runId });
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
const json = (file, stdin, label, schema) => {
|
|
1166
|
+
if (stdin) {
|
|
1167
|
+
// The caller awaits this helper; the synchronous facade below is only
|
|
1168
|
+
// used for file inputs. Keep stdin handling in the stage branch.
|
|
1169
|
+
throw new AppError("usage", `${label} stdin preparation must be awaited`, 2);
|
|
1170
|
+
}
|
|
1171
|
+
if (!file)
|
|
1172
|
+
throw new AppError("usage", `${label} input is required`, 2);
|
|
1173
|
+
return prepareJsonSchema(file, label, schema);
|
|
1174
|
+
};
|
|
1175
|
+
if (family === "stage" && command === "start") {
|
|
1176
|
+
const stage = requiredOption(parsed, "stage");
|
|
1177
|
+
if (stage !== "specify" && ["bootstrap", "intake-file", "intake-stdin"].some(option => hasOption(parsed, option)))
|
|
1178
|
+
throw new AppError("usage", "--bootstrap and intake inputs are only supported for SPECIFY", 2);
|
|
1179
|
+
requiredOption(parsed, "project-root");
|
|
1180
|
+
if (!hasOption(parsed, "bootstrap"))
|
|
1181
|
+
requiredPosition(parsed, 0, "run-id");
|
|
1182
|
+
if (hasOption(parsed, "session-id"))
|
|
1183
|
+
throw new AppError("usage", "--session-id is managed by the harness lifecycle adapter and is not accepted", 2);
|
|
1184
|
+
const contextFile = optionalOption(parsed, "context-file"), contextSha256 = optionalOption(parsed, "context-sha256");
|
|
1185
|
+
if (Boolean(contextFile) !== Boolean(contextSha256))
|
|
1186
|
+
throw new AppError("usage", "stage start requires --context-file together with --context-sha256", 2);
|
|
1187
|
+
if ([hasOption(parsed, "intake-file"), hasOption(parsed, "intake-stdin"), Boolean(contextFile)].filter(Boolean).length > 1)
|
|
1188
|
+
throw new AppError("usage", "Use exactly one of --intake-file, --intake-stdin or --context-file", 2);
|
|
1189
|
+
if (contextFile && contextSha256) {
|
|
1190
|
+
try {
|
|
1191
|
+
prepared.stageStart = { externalContext: loadExternalStageContext({ file: contextFile, expectedSha256: contextSha256, stage }) };
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
return prepareError(error, { file: contextFile, parameter: "context-file" });
|
|
1195
|
+
}
|
|
1196
|
+
if (!hasOption(parsed, "bootstrap")) {
|
|
1197
|
+
const { runRoot } = run(requiredOption(parsed, "project-root"), requiredPosition(parsed, 0, "run-id"));
|
|
1198
|
+
prepareExternalStageContext({ stageRoot: path.join(runRoot, vnextStageDirectory(stage)), loaded: prepared.stageStart.externalContext });
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (stage === "specify") {
|
|
1202
|
+
const intakeFile = optionalOption(parsed, "intake-file");
|
|
1203
|
+
if (intakeFile)
|
|
1204
|
+
prepared.stageStart = { intakeMarkdown: prepareTextFile(intakeFile, "SPECIFY intake").text };
|
|
1205
|
+
else if (hasOption(parsed, "intake-stdin"))
|
|
1206
|
+
prepared.stageStart = { intakeMarkdown: await readStdin(io.stdin) };
|
|
1207
|
+
else if (prepared.stageStart?.externalContext)
|
|
1208
|
+
prepared.stageStart.intakeMarkdown = runnerIntake(prepared.stageStart.externalContext);
|
|
1209
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1210
|
+
const bootstrap = hasOption(parsed, "bootstrap");
|
|
1211
|
+
if (isVnextSpecifyFlow(projectRoot) && (bootstrap || isVnextSpecifyRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") }))) {
|
|
1212
|
+
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") } : {}) });
|
|
1213
|
+
}
|
|
1214
|
+
else if (bootstrap) {
|
|
1215
|
+
if (hasOption(parsed, "intake-stdin") || contextFile)
|
|
1216
|
+
throw new AppError("usage", "Legacy SPECIFY bootstrap supports --intake-file, not stdin or runner context", 2);
|
|
1217
|
+
prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot, stage, bootstrap: { subject: requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) }, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) }, prepared.stageStart?.intakeMarkdown);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (stage === "merge" && isVnextMergeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1221
|
+
prepared.mergeStart = prepareVnextMergeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1222
|
+
}
|
|
1223
|
+
if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1224
|
+
prepared.protocolizeStart = prepareVnextProtocolizeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1225
|
+
}
|
|
1226
|
+
if (stage === "plan" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1227
|
+
prepared.planStart = prepareVnextPlanStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1228
|
+
}
|
|
1229
|
+
if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1230
|
+
prepared.planReviewStart = prepareVnextPlanReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1231
|
+
}
|
|
1232
|
+
if (stage === "code" && isVnextPlanRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1233
|
+
prepared.codeStart = prepareVnextCodeStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1234
|
+
}
|
|
1235
|
+
if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1236
|
+
prepared.codeReviewStart = prepareVnextCodeReviewStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), ...(prepared.stageStart?.externalContext ? { externalContext: prepared.stageStart.externalContext } : {}) });
|
|
1237
|
+
}
|
|
1238
|
+
if (![prepared.specifyStart, prepared.protocolizeStart, prepared.planStart, prepared.planReviewStart, prepared.codeStart, prepared.codeReviewStart, prepared.mergeStart, prepared.legacyStageStart].some(Boolean)) {
|
|
1239
|
+
if (prepared.stageStart?.externalContext)
|
|
1240
|
+
throw new AppError("usage", "--context-file is currently supported only by vNext flow stages", 2);
|
|
1241
|
+
prepared.legacyStageStart = prepareLegacyStageStart(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}) });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (family === "stage" && (command === "pause" || command === "resume")) {
|
|
1245
|
+
requiredPosition(parsed, 0, "run-id");
|
|
1246
|
+
requiredOption(parsed, "project-root");
|
|
1247
|
+
requiredOption(parsed, "stage");
|
|
1248
|
+
requiredOption(parsed, "work");
|
|
1249
|
+
const fileParameter = command === "pause" ? "question-file" : "answer-file";
|
|
1250
|
+
const stdinParameter = command === "pause" ? "question-stdin" : "answer-stdin";
|
|
1251
|
+
const inputFile = optionalOption(parsed, fileParameter);
|
|
1252
|
+
const fromStdin = hasOption(parsed, stdinParameter);
|
|
1253
|
+
if (Boolean(inputFile) === fromStdin)
|
|
1254
|
+
throw new AppError("usage", `stage ${command} requires exactly one of --${fileParameter} or --${stdinParameter}`, 2);
|
|
1255
|
+
if (fromStdin && io.stdin?.isTTY)
|
|
1256
|
+
throw new AppError("usage", `--${stdinParameter} requires piped or redirected input`, 2);
|
|
1257
|
+
prepared.interactionText = inputFile ? prepareTextFile(inputFile, command === "pause" ? "User question" : "User answer").text : await readStdin(io.stdin);
|
|
1258
|
+
if (!prepared.interactionText.trim())
|
|
1259
|
+
return prepareError(new AppError("validation", `stage ${command} requires non-empty input`, 2), { parameter: inputFile ? fileParameter : stdinParameter });
|
|
1260
|
+
const input = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 0, "run-id"), stage: requiredOption(parsed, "stage"), workId: requiredOption(parsed, "work") };
|
|
1261
|
+
try {
|
|
1262
|
+
if (command === "pause")
|
|
1263
|
+
prepareStagePause(context, { ...input, question: prepared.interactionText });
|
|
1264
|
+
else
|
|
1265
|
+
prepared.resumeEvidence = prepareStageResume(context, { ...input, answer: prepared.interactionText }).evidence;
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
return prepareError(error, { parameter: "stage/work" });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (family === "work" && command === "finish") {
|
|
1272
|
+
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
|
|
1273
|
+
if (Boolean(retryReceiptId) !== Boolean(retryReason))
|
|
1274
|
+
throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
|
|
1275
|
+
const resultFile = optionalOption(parsed, "result-file");
|
|
1276
|
+
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1277
|
+
if (Boolean(resultFile) === resultStdin)
|
|
1278
|
+
throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
|
|
1279
|
+
prepared.workFinish = { result: resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin) };
|
|
1280
|
+
prepareWorkResult(context, requiredPosition(parsed, 0, "work-id"), prepared.workFinish.result);
|
|
1281
|
+
}
|
|
1282
|
+
if (family === "stage" && command === "fanout" && requiredPosition(parsed, 0, "fanout action") === "reconcile") {
|
|
1283
|
+
prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
|
|
1284
|
+
}
|
|
1285
|
+
if (family === "stage" && command === "native" && requiredPosition(parsed, 0, "native action") === "observe") {
|
|
1286
|
+
prepared.observations = prepareNativeObservations(prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value);
|
|
1287
|
+
}
|
|
1288
|
+
if (family !== "stage" || command !== "finish")
|
|
1289
|
+
return prepared;
|
|
1290
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1291
|
+
const runId = requiredPosition(parsed, 0, "run-id");
|
|
1292
|
+
const stage = requiredOption(parsed, "stage");
|
|
1293
|
+
if (hasOption(parsed, "status"))
|
|
1294
|
+
throw new AppError("usage", "Stage status belongs in the semantic input file", 2);
|
|
1295
|
+
if (["result-file", "semantic-file", "data", "result-stdin"].filter(key => hasOption(parsed, key)).length > 1)
|
|
1296
|
+
throw new AppError("usage", "Use exactly one result input: --result-file, --semantic-file, --data or --result-stdin", 2);
|
|
1297
|
+
if (Boolean(optionalOption(parsed, "retry-check")) !== Boolean(prepared.retryReason))
|
|
1298
|
+
throw new AppError("usage", "--retry-check requires recovery evidence", 2);
|
|
1299
|
+
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1300
|
+
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
1301
|
+
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
1302
|
+
const runtime = run(projectRoot, runId);
|
|
1303
|
+
const terminalReplay = (name) => {
|
|
1304
|
+
let index;
|
|
1305
|
+
try {
|
|
1306
|
+
index = JSON.parse(runtime.indexJson);
|
|
1307
|
+
}
|
|
1308
|
+
catch (error) {
|
|
1309
|
+
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) });
|
|
1310
|
+
}
|
|
1311
|
+
return index.stage_runs?.some(entry => entry.stage === name && entry.status === "done") === true
|
|
1312
|
+
&& fs.existsSync(path.join(runtime.runRoot, vnextStageDirectory(name), "stage-report.json"));
|
|
1313
|
+
};
|
|
1314
|
+
const acceptStageInputs = (allowed) => {
|
|
1315
|
+
const options = allowed.split(" ");
|
|
1316
|
+
for (const name of ["dir", "semantic-file", "data", "result-file", "result-stdin", "outcome", "decision-file", "verification-file", "retry-check", "reason", "reason-file", "request", "work"]) {
|
|
1317
|
+
if (hasOption(parsed, name) && !options.includes(name === "reason-file" ? "reason" : name))
|
|
1318
|
+
throw new AppError("usage", `stage finish ${stage} does not accept --${name} for this flow`, 2, { parameter: name });
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
const fromStdin = async (label, schema) => {
|
|
1322
|
+
const value = prepareJsonText(await readStdin(io.stdin), { file: "<stdin>", label });
|
|
1323
|
+
try {
|
|
1324
|
+
validateSchema({ ...schema, file: value.file, data: value.value });
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
return prepareError(error, { file: value.file, label });
|
|
1328
|
+
}
|
|
1329
|
+
return value;
|
|
1330
|
+
};
|
|
1331
|
+
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
1332
|
+
acceptStageInputs("result-file semantic-file data result-stdin outcome");
|
|
1333
|
+
const outcome = optionalOption(parsed, "outcome");
|
|
1334
|
+
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome))
|
|
1335
|
+
throw new AppError("usage", "vNext SPECIFY requires --outcome specified|failed|cancelled; use stage pause for a user question", 2);
|
|
1336
|
+
if (Boolean(resultFile) === resultStdin)
|
|
1337
|
+
throw new AppError("usage", "vNext SPECIFY finish requires exactly one result input", 2);
|
|
1338
|
+
const schema = { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot };
|
|
1339
|
+
const input = resultStdin ? await fromStdin("SPECIFY result", schema) : json(resultFile, false, "SPECIFY result", schema);
|
|
1340
|
+
prepared.stageFinish = { value: input.value, text: input.text };
|
|
1341
|
+
}
|
|
1342
|
+
else if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
|
|
1343
|
+
acceptStageInputs("result-file semantic-file data");
|
|
1344
|
+
if (!resultFile || resultStdin || hasOption(parsed, "outcome"))
|
|
1345
|
+
throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
|
|
1346
|
+
let absolute;
|
|
1347
|
+
try {
|
|
1348
|
+
absolute = prepareProtocolizeResultPath(runtime.runRoot, resultFile);
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
return prepareError(error, { file: resultFile });
|
|
1352
|
+
}
|
|
1353
|
+
const input = json(absolute, false, "PROTOCOLIZE result", { schemaName: "vnext-protocolize-result", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
|
|
1354
|
+
prepared.stageFinish = { value: input.value, text: input.text };
|
|
1355
|
+
}
|
|
1356
|
+
else if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
1357
|
+
acceptStageInputs("");
|
|
1358
|
+
if (hasOption(parsed, "code-work-batch-file"))
|
|
1359
|
+
throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
|
|
1360
|
+
try {
|
|
1361
|
+
prepared.planFinish = prepareVnextPlanFinish(context, { projectRoot, runId });
|
|
1362
|
+
}
|
|
1363
|
+
catch (error) {
|
|
1364
|
+
return prepareError(error, { parameter: "PLAN artifacts" });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
else if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
|
|
1368
|
+
acceptStageInputs("request work retry-check reason");
|
|
1369
|
+
if (resultStdin || resultFile || hasOption(parsed, "outcome"))
|
|
1370
|
+
throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
|
|
1371
|
+
requiredOption(parsed, "request");
|
|
1372
|
+
requiredOption(parsed, "work");
|
|
1373
|
+
try {
|
|
1374
|
+
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 } : {}) });
|
|
1375
|
+
}
|
|
1376
|
+
catch (error) {
|
|
1377
|
+
return prepareError(error, { parameter: "MERGE finish" });
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
else if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
|
|
1381
|
+
acceptStageInputs("decision-file");
|
|
1382
|
+
if (resultStdin || hasOption(parsed, "outcome"))
|
|
1383
|
+
throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
|
|
1384
|
+
const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "04-plan-review", "decision.json");
|
|
1385
|
+
const input = json(decision, false, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
|
|
1386
|
+
prepared.stageFinish = { value: input.value, text: input.text };
|
|
1387
|
+
}
|
|
1388
|
+
else if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
1389
|
+
acceptStageInputs("verification-file retry-check reason");
|
|
1390
|
+
if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome"))
|
|
1391
|
+
throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
|
|
1392
|
+
const verification = optionalOption(parsed, "verification-file");
|
|
1393
|
+
if (!verification)
|
|
1394
|
+
throw new AppError("usage", "vNext CODE finish requires --verification-file", 2);
|
|
1395
|
+
if (!terminalReplay(stage)) {
|
|
1396
|
+
const input = json(verification, false, "CODE verification", { schemaName: "code-verification", projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
|
|
1397
|
+
prepared.stageFinish = { value: input.value, text: input.text };
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
else if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
|
|
1401
|
+
acceptStageInputs("decision-file retry-check reason");
|
|
1402
|
+
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
1403
|
+
throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
|
|
1404
|
+
const decision = optionalOption(parsed, "decision-file") ?? path.join(runtime.runRoot, "06-code-review", "decision.json");
|
|
1405
|
+
if (!terminalReplay(stage)) {
|
|
1406
|
+
const input = prepareCodeReviewDecision(runtime.runRoot, optionalOption(parsed, "decision-file"));
|
|
1407
|
+
try {
|
|
1408
|
+
validateSchema({ schemaName: "code-review-decision", file: decision, data: input.value, projectRoot: runtime.workspaceRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: runtime.runRoot });
|
|
1409
|
+
}
|
|
1410
|
+
catch (error) {
|
|
1411
|
+
return prepareError(error, { file: decision, label: "CODE-REVIEW decision" });
|
|
1412
|
+
}
|
|
1413
|
+
prepared.stageFinish = input;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
acceptStageInputs("dir semantic-file data");
|
|
1418
|
+
if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin)
|
|
1419
|
+
throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
|
|
1420
|
+
prepared.legacyStageFinish = prepareStageFinish(context, { projectRoot, runId, stage, ...(optionalOption(parsed, "dir") ? { dir: optionalOption(parsed, "dir") } : {}), ...(semanticFile ? { semanticFile } : {}) });
|
|
1421
|
+
}
|
|
1422
|
+
return prepared;
|
|
1423
|
+
}
|
|
1424
|
+
async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args), prepared = {}) {
|
|
1425
|
+
if (!prepared.route)
|
|
1426
|
+
throw new AppError("command_not_prepared", "Command dispatch requires prepared arguments", 1);
|
|
1427
|
+
const { family, command, parsed } = prepared.route;
|
|
1428
|
+
if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
|
|
1429
|
+
const payload = prepared.lifecycleIssuance;
|
|
1430
|
+
if (!payload)
|
|
1431
|
+
throw new AppError("command_not_prepared", "Invocation issuance requires prepared input", 1);
|
|
1432
|
+
assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
|
|
1433
|
+
return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
|
|
1434
|
+
}
|
|
1435
|
+
if (family === "storage" && command === "migrate-writer") {
|
|
1436
|
+
if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1437
|
+
throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
|
|
1438
|
+
if (parsed.options.get("offline")?.[0] !== flagValue)
|
|
1439
|
+
throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
|
|
1440
|
+
return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
|
|
1441
|
+
}
|
|
506
1442
|
if (family === "engine") {
|
|
507
|
-
return dispatchEngine(context, command, parsed, classification);
|
|
1443
|
+
return dispatchEngine(context, command, parsed, classification, prepared);
|
|
508
1444
|
}
|
|
509
1445
|
if (family === "status") {
|
|
510
|
-
const rootParsed = parseArgs([command ?? "", ...rest]);
|
|
511
1446
|
return getRuntimeStatus(context, {
|
|
512
|
-
projectRoot: optionalOption(
|
|
513
|
-
checkRegistry: hasOption(
|
|
1447
|
+
projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
|
|
1448
|
+
checkRegistry: hasOption(parsed, "check-registry")
|
|
514
1449
|
});
|
|
515
1450
|
}
|
|
516
1451
|
if (family === "version") {
|
|
517
1452
|
return getCliVersionReport();
|
|
518
1453
|
}
|
|
519
1454
|
if (family === "runtime") {
|
|
520
|
-
return await dispatchRuntime(context, command, parsed);
|
|
1455
|
+
return await dispatchRuntime(context, command, parsed, commandProgress(io, output), prepared);
|
|
521
1456
|
}
|
|
522
1457
|
if (family === "canon") {
|
|
523
|
-
return dispatchCanon(context, command, parsed);
|
|
1458
|
+
return dispatchCanon(context, command, parsed, prepared);
|
|
524
1459
|
}
|
|
525
1460
|
if (family === "project" && command === "register") {
|
|
526
1461
|
return registerProject(context, { root: requiredOption(parsed, "root") });
|
|
@@ -597,16 +1532,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
597
1532
|
return readyForMerge(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol ready-for-merge") });
|
|
598
1533
|
}
|
|
599
1534
|
if (family === "protocol" && command === "transition") {
|
|
600
|
-
|
|
601
|
-
const reason = optionalOption(parsed, "reason");
|
|
602
|
-
return transitionProtocol(context, {
|
|
603
|
-
protocolId,
|
|
604
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "protocol transition"),
|
|
605
|
-
to: requiredOption(parsed, "to"),
|
|
606
|
-
jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
|
|
607
|
-
force: hasOption(parsed, "force"),
|
|
608
|
-
...(reason ? { reason } : {})
|
|
609
|
-
});
|
|
1535
|
+
return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
|
|
610
1536
|
}
|
|
611
1537
|
if (family === "protocol" && command === "sync-from-run") {
|
|
612
1538
|
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
@@ -631,17 +1557,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
631
1557
|
});
|
|
632
1558
|
}
|
|
633
1559
|
if (family === "transition") {
|
|
634
|
-
|
|
635
|
-
const protocolId = requiredPosition(transitionParsed, 0, "protocol-id");
|
|
636
|
-
const reason = optionalOption(transitionParsed, "reason");
|
|
637
|
-
return transitionProtocol(context, {
|
|
638
|
-
protocolId,
|
|
639
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "transition"),
|
|
640
|
-
to: requiredOption(transitionParsed, "to"),
|
|
641
|
-
jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
|
|
642
|
-
force: hasOption(transitionParsed, "force"),
|
|
643
|
-
...(reason ? { reason } : {})
|
|
644
|
-
});
|
|
1560
|
+
return transitionProtocol(context, protocolTransitionInput(parsed, scopeProjectRoot), prepared.protocolTransition);
|
|
645
1561
|
}
|
|
646
1562
|
if (family === "plan" && command === "status") {
|
|
647
1563
|
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
@@ -660,12 +1576,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
660
1576
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
|
|
661
1577
|
if (action === "status")
|
|
662
1578
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
1579
|
+
if (action === "reconcile")
|
|
1580
|
+
return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: prepared.observations ?? prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value });
|
|
663
1581
|
if (action === "dispatch") {
|
|
664
1582
|
if (stage === "plan-review")
|
|
665
1583
|
return dispatchVnextPlanReview(context, { projectRoot, runId });
|
|
666
1584
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
667
1585
|
}
|
|
668
|
-
throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
|
|
1586
|
+
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);
|
|
1587
|
+
}
|
|
1588
|
+
if (family === "stage" && command === "native") {
|
|
1589
|
+
if (requiredPosition(parsed, 0, "native action") !== "observe")
|
|
1590
|
+
throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
|
|
1591
|
+
return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: prepared.observations ?? prepareJsonFile(requiredOption(parsed, "observations-file"), "native observations").value });
|
|
669
1592
|
}
|
|
670
1593
|
if (family === "run" && command === "capacity") {
|
|
671
1594
|
if (requiredPosition(parsed, 0, "capacity action") !== "record")
|
|
@@ -677,16 +1600,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
677
1600
|
return dispatchPlanItem(context, parsed, requiredScopeProjectRoot(scopeProjectRoot, "plan item"));
|
|
678
1601
|
}
|
|
679
1602
|
if (family === "prompt" && command === "render") {
|
|
680
|
-
|
|
681
|
-
const taskFile = optionalOption(parsed, "task-file");
|
|
682
|
-
return renderWorkerPrompt(context, {
|
|
683
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
684
|
-
runId: requiredOption(parsed, "run"),
|
|
685
|
-
stage: requiredOption(parsed, "stage"),
|
|
686
|
-
profile: requiredOption(parsed, "profile"),
|
|
687
|
-
...(taskFile ? { taskFile } : { planItemId: requiredOption(parsed, "plan-item") }),
|
|
688
|
-
...(workspaceRoot ? { workspaceRoot } : {})
|
|
689
|
-
});
|
|
1603
|
+
return renderWorkerPrompt(context, prepared.workerPrompt?.input ?? workerPromptInput(parsed), prepared.workerPrompt?.value);
|
|
690
1604
|
}
|
|
691
1605
|
if (family === "work" && command === "status") {
|
|
692
1606
|
return getVnextWorkStatus(context, {
|
|
@@ -695,7 +1609,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
695
1609
|
});
|
|
696
1610
|
}
|
|
697
1611
|
if (family === "work" && command === "add-batch") {
|
|
698
|
-
return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file") });
|
|
1612
|
+
return addWorkBatch(context, { parentWorkId: requiredOption(parsed, "parent"), file: requiredOption(parsed, "file"), prepared: prepared.workBatch });
|
|
699
1613
|
}
|
|
700
1614
|
if (family === "work" && command === "ls") {
|
|
701
1615
|
return listWorks(context, { ...(optionalOption(parsed, "run") ? { runId: optionalOption(parsed, "run") } : {}), ...(optionalOption(parsed, "parent") ? { parentWorkId: optionalOption(parsed, "parent") } : {}), ...(optionalOption(parsed, "status") ? { status: optionalOption(parsed, "status") } : {}), ...(hasOption(parsed, "ready") ? { ready: true } : {}), ...(hasOption(parsed, "include-results") ? { includeResults: true } : {}), ...(optionalOption(parsed, "limit") ? { limit: Number(optionalOption(parsed, "limit")) } : {}) });
|
|
@@ -703,48 +1617,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
703
1617
|
if (family === "work" && command === "show")
|
|
704
1618
|
return showWork(context, requiredPosition(parsed, 0, "work-id"));
|
|
705
1619
|
if (family === "work" && command === "repair" && requiredPosition(parsed, 0, "action") === "add") {
|
|
706
|
-
if (!
|
|
707
|
-
throw new AppError("
|
|
708
|
-
|
|
709
|
-
const findings = parsed.options.get("from-finding") ?? [];
|
|
710
|
-
const unresolved = parsed.options.get("from-unresolved") ?? [];
|
|
711
|
-
if (!origins.length && !findings.length)
|
|
712
|
-
throw new AppError("usage", "work repair add requires --from-check with --origin-work, --from-finding, or --from-unresolved with --origin-work", 2);
|
|
713
|
-
if (findings.length)
|
|
714
|
-
return addVnextCodeReviewRepair(context, {
|
|
715
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
|
|
716
|
-
runId: requiredOption(parsed, "run"),
|
|
717
|
-
findingIds: findings,
|
|
718
|
-
checkRefsByFinding: Object.fromEntries(findings.map((finding) => [finding, parsed.options.get("check-ref") ?? []])),
|
|
719
|
-
objective: await readStdin(io.stdin)
|
|
720
|
-
});
|
|
721
|
-
if (unresolved.length) {
|
|
722
|
-
if (!origins.length)
|
|
723
|
-
throw new AppError("usage", "semantic repair requires at least one --origin-work", 2);
|
|
724
|
-
const verificationPath = requiredOption(parsed, "verification-file");
|
|
725
|
-
return addVnextCodeRepair(context, {
|
|
726
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
|
|
727
|
-
runId: requiredOption(parsed, "run"),
|
|
728
|
-
originWorkIds: origins,
|
|
729
|
-
semanticUnresolved: unresolved,
|
|
730
|
-
verificationPath,
|
|
731
|
-
objective: await readStdin(io.stdin)
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
if (!hasOption(parsed, "from-check"))
|
|
735
|
-
throw new AppError("usage", "check repair requires --from-check", 2);
|
|
736
|
-
if (!origins.length)
|
|
737
|
-
throw new AppError("usage", "check repair requires at least one --origin-work", 2);
|
|
738
|
-
const receipts = parsed.options.get("from-check") ?? [];
|
|
739
|
-
if (receipts.length !== 1)
|
|
740
|
-
throw new AppError("usage", "check repair accepts exactly one --from-check; create one causal repair Work per failed receipt", 2);
|
|
741
|
-
return addVnextCodeRepair(context, {
|
|
742
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"),
|
|
743
|
-
runId: requiredOption(parsed, "run"),
|
|
744
|
-
checkReceiptId: receipts[0],
|
|
745
|
-
originWorkIds: origins,
|
|
746
|
-
objective: await readStdin(io.stdin)
|
|
747
|
-
});
|
|
1620
|
+
if (!prepared.workRepair)
|
|
1621
|
+
throw new AppError("cli_input_unprepared", "Repair input was not prepared", 1);
|
|
1622
|
+
return addVnextCodeRepair(context, prepared.workRepair.input, prepared.workRepair.value);
|
|
748
1623
|
}
|
|
749
1624
|
if (family === "work" && command === "deps") {
|
|
750
1625
|
const on = parsed.options.get("on") ?? [];
|
|
@@ -752,21 +1627,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
752
1627
|
}
|
|
753
1628
|
if (family === "work" && command === "delete")
|
|
754
1629
|
return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
|
|
1630
|
+
if (family === "work" && command === "launch")
|
|
1631
|
+
return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
|
|
755
1632
|
if (family === "work" && command === "start") {
|
|
756
1633
|
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
1634
|
+
const recoveryId = optionalOption(parsed, "recovery-id");
|
|
757
1635
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
|
|
758
|
-
return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
|
|
1636
|
+
return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
|
|
759
1637
|
}
|
|
760
1638
|
if (family === "work" && command === "finish") {
|
|
761
1639
|
const resultFile = optionalOption(parsed, "result-file");
|
|
762
1640
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
763
1641
|
if (Boolean(resultFile) === resultStdin)
|
|
764
1642
|
throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
|
|
765
|
-
const
|
|
766
|
-
|
|
1643
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
1644
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
|
|
1645
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
1646
|
+
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
|
|
1647
|
+
if (Boolean(retryReceiptId) !== Boolean(retryReason))
|
|
1648
|
+
throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
|
|
1649
|
+
const result = prepared.workFinish?.result ?? (resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin));
|
|
1650
|
+
return await finishWork(context, workId, result, commandProgress(io, output), retryReceiptId ? { receiptId: retryReceiptId, reason: retryReason } : undefined);
|
|
1651
|
+
}
|
|
1652
|
+
if (family === "work" && command === "fail") {
|
|
1653
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
1654
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
|
|
1655
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
1656
|
+
return failWork(context, workId, requiredOption(parsed, "reason"));
|
|
767
1657
|
}
|
|
768
|
-
if (family === "work" && command === "fail")
|
|
769
|
-
return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
770
1658
|
if (family === "work" && command === "cancel")
|
|
771
1659
|
return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
772
1660
|
if (family === "work" && command === "retry")
|
|
@@ -788,12 +1676,12 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
788
1676
|
const requireSessionBinding = hasOption(parsed, "require-session-binding");
|
|
789
1677
|
const projectRoot = requiredOption(parsed, "project-root");
|
|
790
1678
|
const stage = requiredOption(parsed, "stage");
|
|
791
|
-
const externalContext =
|
|
1679
|
+
const externalContext = prepared.stageStart?.externalContext;
|
|
792
1680
|
const claimVnextStageStart = (runId) => {
|
|
793
1681
|
registerProject(context, { root: projectRoot });
|
|
794
1682
|
return findRecentMatchingHookEvent(context, {
|
|
795
1683
|
projectId: requireProjectByRoot(context, resolveProjectRoot(projectRoot)).id,
|
|
796
|
-
matchKey: stageStartMatchKey(
|
|
1684
|
+
matchKey: entityReferenceVariants(runId).map((reference) => stageStartMatchKey(reference, stage, projectRoot, contextSha256)),
|
|
797
1685
|
errorCode: "trusted_session_binding_required",
|
|
798
1686
|
operation: `vNext ${stage.toUpperCase()} start`
|
|
799
1687
|
}).eventKey;
|
|
@@ -819,37 +1707,37 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
819
1707
|
}
|
|
820
1708
|
const result = launchVnextSpecify(context, {
|
|
821
1709
|
projectRoot,
|
|
822
|
-
...(externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
|
|
1710
|
+
...(prepared.stageStart?.intakeMarkdown !== undefined ? { intakeMarkdown: prepared.stageStart.intakeMarkdown } : externalContext ? { intakeMarkdown: runnerIntake(externalContext) } : intakeFile ? { intakeFile } : { intakeMarkdown: await readStdin(io.stdin) }),
|
|
823
1711
|
hookEventId,
|
|
824
1712
|
...(subject ? { slug: subject } : {}),
|
|
825
1713
|
...(externalContext ? { externalContext } : {}),
|
|
826
1714
|
...(!bootstrap ? { runId: requiredPosition(parsed, 0, "run-id") } : {})
|
|
827
|
-
});
|
|
1715
|
+
}, prepared.specifyStart);
|
|
828
1716
|
return attachExternalStageContext(result, externalContext);
|
|
829
1717
|
}
|
|
830
1718
|
if (!bootstrap && stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
831
1719
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
832
|
-
return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1720
|
+
return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.protocolizeStart), externalContext);
|
|
833
1721
|
}
|
|
834
1722
|
if (!bootstrap && stage === "plan" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
835
1723
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
836
|
-
return attachExternalStageContext(startVnextPlan(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1724
|
+
return attachExternalStageContext(startVnextPlan(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planStart), externalContext);
|
|
837
1725
|
}
|
|
838
1726
|
if (!bootstrap && stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
839
1727
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
840
|
-
return attachExternalStageContext(startVnextPlanReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1728
|
+
return attachExternalStageContext(startVnextPlanReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.planReviewStart), externalContext);
|
|
841
1729
|
}
|
|
842
1730
|
if (!bootstrap && stage === "code" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
843
1731
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
844
|
-
return attachExternalStageContext(await startVnextCode(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1732
|
+
return attachExternalStageContext(await startVnextCode(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.codeStart), externalContext);
|
|
845
1733
|
}
|
|
846
1734
|
if (!bootstrap && stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
847
1735
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
848
|
-
return attachExternalStageContext(startVnextCodeReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1736
|
+
return attachExternalStageContext(startVnextCodeReview(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.codeReviewStart), externalContext);
|
|
849
1737
|
}
|
|
850
1738
|
if (!bootstrap && stage === "merge" && isVnextMergeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
851
1739
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
852
|
-
return attachExternalStageContext(await startVnextMerge(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }), externalContext);
|
|
1740
|
+
return attachExternalStageContext(await startVnextMerge(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, progress: commandProgress(io, output), ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.mergeStart), externalContext);
|
|
853
1741
|
}
|
|
854
1742
|
if (intakeStdin) {
|
|
855
1743
|
throw new AppError("usage", "--intake-stdin is only available for vNext SPECIFY", 2);
|
|
@@ -864,7 +1752,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
864
1752
|
...(hookEventId ? { hookEventId } : {}),
|
|
865
1753
|
...(requireSessionBinding ? { requireSessionBinding: true } : {}),
|
|
866
1754
|
...(dir ? { dir } : {})
|
|
867
|
-
});
|
|
1755
|
+
}, prepared.legacyStageStart);
|
|
868
1756
|
}
|
|
869
1757
|
if (family === "stage" && command === "finish") {
|
|
870
1758
|
const dir = optionalOption(parsed, "dir");
|
|
@@ -874,9 +1762,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
874
1762
|
const projectRoot = requiredOption(parsed, "project-root");
|
|
875
1763
|
const runId = requiredPosition(parsed, 0, "run-id");
|
|
876
1764
|
const stage = requiredOption(parsed, "stage");
|
|
1765
|
+
const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
|
|
877
1766
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
878
1767
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
879
1768
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1769
|
+
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
1770
|
+
const retryReason = prepared.retryReason;
|
|
1771
|
+
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
1772
|
+
if (retryCheckId && !retryReason?.trim())
|
|
1773
|
+
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
1774
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
1775
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
1776
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1777
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
1778
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1779
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
1780
|
+
if (protectedLifecycle)
|
|
1781
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
|
|
880
1782
|
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
881
1783
|
const outcome = optionalOption(parsed, "outcome");
|
|
882
1784
|
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
|
|
@@ -888,84 +1790,92 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
888
1790
|
if (!resultFile && !resultStdin) {
|
|
889
1791
|
throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
|
|
890
1792
|
}
|
|
891
|
-
return finishVnextSpecifyStage(context, {
|
|
1793
|
+
return controlledReply(await finishVnextSpecifyStage(context, {
|
|
892
1794
|
projectRoot,
|
|
893
1795
|
runId,
|
|
894
|
-
...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
|
|
1796
|
+
...(resultFile ? { resultFile } : prepared.stageFinish?.value ? {} : { resultJson: await readStdin(io.stdin) }),
|
|
1797
|
+
...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}),
|
|
895
1798
|
outcome: outcome
|
|
896
|
-
});
|
|
1799
|
+
}));
|
|
897
1800
|
}
|
|
898
1801
|
if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
|
|
899
1802
|
if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
900
1803
|
throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
|
|
901
1804
|
}
|
|
902
|
-
return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
|
|
1805
|
+
return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile, ...(prepared.stageFinish?.value ? { result: prepared.stageFinish.value } : {}) }));
|
|
903
1806
|
}
|
|
904
1807
|
if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
905
1808
|
if (optionalOption(parsed, "code-work-batch-file"))
|
|
906
1809
|
throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
|
|
907
|
-
return finishVnextPlan(context, { projectRoot, runId });
|
|
1810
|
+
return controlledReply(await finishVnextPlan(context, { projectRoot, runId, ...(prepared.planFinish ? { prepared: prepared.planFinish } : {}) }));
|
|
908
1811
|
}
|
|
909
1812
|
if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
|
|
910
1813
|
if (resultStdin || hasOption(parsed, "outcome"))
|
|
911
1814
|
throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
|
|
912
1815
|
const decisionFile = optionalOption(parsed, "decision-file");
|
|
913
|
-
return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
|
|
1816
|
+
return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}), ...(prepared.stageFinish?.value ? { decision: prepared.stageFinish.value, decisionText: prepared.stageFinish.text } : {}) }));
|
|
914
1817
|
}
|
|
915
1818
|
if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
916
1819
|
if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
917
1820
|
throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
|
|
918
1821
|
}
|
|
919
|
-
return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
|
|
1822
|
+
return controlledReply(await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(prepared.stageFinish?.value ? { verification: prepared.stageFinish.value } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
|
|
920
1823
|
}
|
|
921
1824
|
if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
|
|
922
1825
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
923
1826
|
throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
|
|
924
|
-
return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), progress: commandProgress(io, output) });
|
|
1827
|
+
return controlledReply(await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(prepared.stageFinish?.value ? { decision: prepared.stageFinish.value, decisionText: prepared.stageFinish.text } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
|
|
925
1828
|
}
|
|
926
1829
|
if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
|
|
927
1830
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
928
1831
|
throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
|
|
929
|
-
return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), progress: commandProgress(io, output) });
|
|
1832
|
+
return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }, prepared.mergeFinish));
|
|
930
1833
|
}
|
|
931
1834
|
if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
|
|
932
1835
|
throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
|
|
933
1836
|
}
|
|
934
|
-
return finishStage(context, {
|
|
1837
|
+
return controlledReply(await finishStage(context, {
|
|
935
1838
|
projectRoot,
|
|
936
1839
|
runId,
|
|
937
1840
|
stage,
|
|
938
1841
|
...(dir ? { dir } : {}),
|
|
939
1842
|
...(semanticFile ? { semanticFile } : {})
|
|
940
|
-
});
|
|
1843
|
+
}, prepared.legacyStageFinish));
|
|
941
1844
|
}
|
|
942
1845
|
if (family === "stage" && command === "pause") {
|
|
943
|
-
|
|
944
|
-
|
|
1846
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1847
|
+
const runId = requiredPosition(parsed, 0, "run-id");
|
|
1848
|
+
const stage = requiredOption(parsed, "stage");
|
|
1849
|
+
const workId = requiredOption(parsed, "work");
|
|
1850
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
1851
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
1852
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1853
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
1854
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1855
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
1856
|
+
if (protectedLifecycle)
|
|
1857
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
945
1858
|
return pauseStageForUser(context, {
|
|
946
|
-
projectRoot
|
|
947
|
-
runId
|
|
948
|
-
stage
|
|
949
|
-
workId
|
|
950
|
-
question: await readStdin(io.stdin)
|
|
1859
|
+
projectRoot,
|
|
1860
|
+
runId,
|
|
1861
|
+
stage,
|
|
1862
|
+
workId,
|
|
1863
|
+
question: prepared.interactionText ?? await readStdin(io.stdin)
|
|
951
1864
|
});
|
|
952
1865
|
}
|
|
953
1866
|
if (family === "stage" && command === "resume") {
|
|
954
|
-
if (!hasOption(parsed, "answer-stdin"))
|
|
955
|
-
throw new AppError("usage", "stage resume requires --answer-stdin", 2);
|
|
956
1867
|
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
957
1868
|
return resumeStageAfterUser(context, {
|
|
958
1869
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
959
1870
|
runId: requiredPosition(parsed, 0, "run-id"),
|
|
960
1871
|
stage: requiredOption(parsed, "stage"),
|
|
961
1872
|
workId: requiredOption(parsed, "work"),
|
|
962
|
-
answer: await readStdin(io.stdin),
|
|
1873
|
+
answer: prepared.interactionText ?? await readStdin(io.stdin),
|
|
1874
|
+
evidence: prepared.resumeEvidence,
|
|
963
1875
|
...(hookEventId ? { hookEventId } : {})
|
|
964
1876
|
});
|
|
965
1877
|
}
|
|
966
1878
|
if (family === "stage" && command === "block") {
|
|
967
|
-
if (!hasOption(parsed, "summary-stdin"))
|
|
968
|
-
throw new AppError("usage", "stage block requires --summary-stdin", 2);
|
|
969
1879
|
const kind = requiredOption(parsed, "kind");
|
|
970
1880
|
if (kind !== "engine" && kind !== "harness" && kind !== "environment")
|
|
971
1881
|
throw new AppError("usage", "--kind must be engine, harness or environment", 2);
|
|
@@ -976,7 +1886,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
976
1886
|
workId: requiredOption(parsed, "work"),
|
|
977
1887
|
kind,
|
|
978
1888
|
code: requiredOption(parsed, "code"),
|
|
979
|
-
summary: await readStdin(io.stdin),
|
|
1889
|
+
summary: prepared.interactionText ?? await readStdin(io.stdin),
|
|
980
1890
|
retryable: hasOption(parsed, "retryable")
|
|
981
1891
|
});
|
|
982
1892
|
}
|
|
@@ -1001,13 +1911,55 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1001
1911
|
return dispatchMergeWorker(context, command, parsed);
|
|
1002
1912
|
}
|
|
1003
1913
|
if (family === "cleanup") {
|
|
1004
|
-
return dispatchCleanup(context, command, parsed);
|
|
1914
|
+
return dispatchCleanup(context, command, parsed, prepared.cleanup);
|
|
1005
1915
|
}
|
|
1006
1916
|
if (family === "session") {
|
|
1007
|
-
return dispatchSession(context, command, parsed);
|
|
1917
|
+
return dispatchSession(context, command, parsed, prepared.sessionPayload);
|
|
1008
1918
|
}
|
|
1009
1919
|
if (family === "run") {
|
|
1010
|
-
|
|
1920
|
+
if (command === "control") {
|
|
1921
|
+
const action = requiredPosition(parsed, 0, "control action");
|
|
1922
|
+
const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
|
|
1923
|
+
if (action === "status")
|
|
1924
|
+
return runControlStatus(context, scope);
|
|
1925
|
+
if (action === "reconcile") {
|
|
1926
|
+
if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1927
|
+
throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
|
|
1928
|
+
return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
|
|
1929
|
+
}
|
|
1930
|
+
if (action === "resume") {
|
|
1931
|
+
if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
|
|
1932
|
+
throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
|
|
1933
|
+
return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
|
|
1934
|
+
}
|
|
1935
|
+
if (action === "serve")
|
|
1936
|
+
return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
|
|
1937
|
+
if (action === "pause" || action === "stop")
|
|
1938
|
+
return await requestRunControl(context, {
|
|
1939
|
+
...scope, action, requestId: requiredOption(parsed, "request-id"),
|
|
1940
|
+
...(hasOption(parsed, "force") ? { force: true } : {}),
|
|
1941
|
+
...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
|
|
1942
|
+
...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
|
|
1943
|
+
...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
|
|
1944
|
+
});
|
|
1945
|
+
throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
|
|
1946
|
+
}
|
|
1947
|
+
if (command === "drive") {
|
|
1948
|
+
const action = requiredPosition(parsed, 0, "drive action");
|
|
1949
|
+
const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
|
|
1950
|
+
if (action === "status")
|
|
1951
|
+
return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
|
|
1952
|
+
if (action === "launch")
|
|
1953
|
+
return await launchRunController(context, controllerLaunchInput(parsed), prepared.controllerLaunch);
|
|
1954
|
+
if (action === "serve")
|
|
1955
|
+
return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
|
|
1956
|
+
if (action === "answer")
|
|
1957
|
+
return answerRunController(context, controllerAnswerInput(parsed), prepared.controllerAnswer);
|
|
1958
|
+
if (action === "context")
|
|
1959
|
+
return supplyRunControllerContext(context, controllerContextInput(parsed), prepared.controllerContext);
|
|
1960
|
+
throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
|
|
1961
|
+
}
|
|
1962
|
+
return dispatchRun(context, command, parsed, prepared);
|
|
1011
1963
|
}
|
|
1012
1964
|
if (family === "stat") {
|
|
1013
1965
|
return dispatchStat(context, command, parsed);
|
|
@@ -1022,23 +1974,26 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1022
1974
|
return dispatchDashboard(context, command, parsed);
|
|
1023
1975
|
}
|
|
1024
1976
|
if (family === "schema") {
|
|
1025
|
-
return
|
|
1977
|
+
return prepared.schemaValidation;
|
|
1026
1978
|
}
|
|
1027
1979
|
if (family === "memory") {
|
|
1028
1980
|
return dispatchMemory(command, parsed);
|
|
1029
1981
|
}
|
|
1030
1982
|
if (family === "migration") {
|
|
1031
|
-
return dispatchMigration(context, command, parsed, classification);
|
|
1983
|
+
return dispatchMigration(context, command, parsed, classification, prepared);
|
|
1032
1984
|
}
|
|
1033
1985
|
if (family === "codex" && command === "hooks") {
|
|
1034
|
-
return dispatchCodexHooks(context, parsed);
|
|
1986
|
+
return dispatchCodexHooks(context, parsed, prepared);
|
|
1035
1987
|
}
|
|
1036
1988
|
if (family === "codex" && command === "home") {
|
|
1037
|
-
return dispatchCodexHome(context, parsed);
|
|
1989
|
+
return dispatchCodexHome(context, parsed, prepared);
|
|
1038
1990
|
}
|
|
1039
1991
|
if (family === "codex" && command === "hook") {
|
|
1040
1992
|
return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
|
|
1041
1993
|
}
|
|
1994
|
+
if (family === "zcode" && command === "hook") {
|
|
1995
|
+
return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
|
|
1996
|
+
}
|
|
1042
1997
|
if (family === "zcode" && command === "event") {
|
|
1043
1998
|
return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
|
|
1044
1999
|
}
|
|
@@ -1063,6 +2018,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1063
2018
|
throw new AppError("usage", "Unknown opencode usage action", 2);
|
|
1064
2019
|
return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1065
2020
|
}
|
|
2021
|
+
if (family === "droid" && command === "event") {
|
|
2022
|
+
if (requiredPosition(parsed, 0, "droid event action") !== "handle")
|
|
2023
|
+
throw new AppError("usage", "Unknown droid event action", 2);
|
|
2024
|
+
return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
2025
|
+
}
|
|
2026
|
+
if (family === "droid" && command === "usage") {
|
|
2027
|
+
if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
|
|
2028
|
+
throw new AppError("usage", "Unknown droid usage action", 2);
|
|
2029
|
+
return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
2030
|
+
}
|
|
1066
2031
|
if (family === "agy" && command === "event")
|
|
1067
2032
|
return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
|
|
1068
2033
|
if (family === "agy" && command === "usage") {
|
|
@@ -1071,11 +2036,14 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1071
2036
|
return ingestAgyUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1072
2037
|
}
|
|
1073
2038
|
if (family === "worktree") {
|
|
1074
|
-
return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`));
|
|
2039
|
+
return dispatchWorktree(context, command, parsed, requiredScopeProjectRoot(scopeProjectRoot, `worktree ${command ?? ""}`), prepared);
|
|
1075
2040
|
}
|
|
1076
2041
|
throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
|
|
1077
2042
|
}
|
|
1078
|
-
function
|
|
2043
|
+
function engineBindInput(parsed) {
|
|
2044
|
+
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") };
|
|
2045
|
+
}
|
|
2046
|
+
function dispatchEngine(context, command, parsed, classification, prepared) {
|
|
1079
2047
|
if (command === "install") {
|
|
1080
2048
|
return installCurrentEngine(context, { force: hasOption(parsed, "force") });
|
|
1081
2049
|
}
|
|
@@ -1100,18 +2068,11 @@ function dispatchEngine(context, command, parsed, classification) {
|
|
|
1100
2068
|
});
|
|
1101
2069
|
}
|
|
1102
2070
|
if (command === "bind-run") {
|
|
1103
|
-
return bindRunEngine(context,
|
|
1104
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1105
|
-
runId: requiredPosition(parsed, 0, "run-id"),
|
|
1106
|
-
packageName: optionalOption(parsed, "package"),
|
|
1107
|
-
engineVersion: requiredOption(parsed, "engine-version"),
|
|
1108
|
-
integrityChecksum: optionalOption(parsed, "integrity-checksum"),
|
|
1109
|
-
reason: requiredOption(parsed, "reason")
|
|
1110
|
-
});
|
|
2071
|
+
return bindRunEngine(context, engineBindInput(parsed), prepared.engineBind);
|
|
1111
2072
|
}
|
|
1112
2073
|
throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
|
|
1113
2074
|
}
|
|
1114
|
-
function dispatchMigration(context, command, parsed, classification) {
|
|
2075
|
+
function dispatchMigration(context, command, parsed, classification, prepared) {
|
|
1115
2076
|
if (command === "impact")
|
|
1116
2077
|
return assessMigrationImpact(context, { projectRoot: requiredOption(parsed, "project-root"), ...(optionalOption(parsed, "target-version") ? { targetVersion: optionalOption(parsed, "target-version") } : {}), ...(optionalOption(parsed, "mode") ? { mode: optionalOption(parsed, "mode") } : {}) });
|
|
1117
2078
|
if (command === "plan" || command === "report") {
|
|
@@ -1127,26 +2088,42 @@ function dispatchMigration(context, command, parsed, classification) {
|
|
|
1127
2088
|
});
|
|
1128
2089
|
}
|
|
1129
2090
|
if (command === "verify") {
|
|
1130
|
-
return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
|
|
2091
|
+
return prepared.migrationVerification ?? verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
|
|
1131
2092
|
}
|
|
1132
2093
|
if (command === "apply") {
|
|
1133
|
-
return applyMigration(context,
|
|
1134
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1135
|
-
sourceVersion: optionalOption(parsed, "source-version"),
|
|
1136
|
-
targetVersion: optionalOption(parsed, "target-version"),
|
|
1137
|
-
runId: optionalOption(parsed, "run"),
|
|
1138
|
-
backupPath: optionalOption(parsed, "backup-path"),
|
|
1139
|
-
backupCreatedAt: optionalOption(parsed, "backup-created-at"),
|
|
1140
|
-
reportFile: optionalOption(parsed, "file"),
|
|
1141
|
-
allowActive: false,
|
|
1142
|
-
classification
|
|
1143
|
-
});
|
|
2094
|
+
return applyMigration(context, migrationApplyInput(parsed, classification), prepared.migrationApply);
|
|
1144
2095
|
}
|
|
1145
2096
|
throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
|
|
1146
2097
|
}
|
|
1147
|
-
function
|
|
2098
|
+
function protocolTransitionInput(parsed, scopeProjectRoot) {
|
|
2099
|
+
const reason = optionalOption(parsed, "reason");
|
|
2100
|
+
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 } : {}) };
|
|
2101
|
+
}
|
|
2102
|
+
function controllerAnswerInput(parsed) {
|
|
2103
|
+
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") };
|
|
2104
|
+
}
|
|
2105
|
+
function controllerLaunchInput(parsed) {
|
|
2106
|
+
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") } : {}) };
|
|
2107
|
+
}
|
|
2108
|
+
function controllerContextInput(parsed) {
|
|
2109
|
+
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") };
|
|
2110
|
+
}
|
|
2111
|
+
function migrationApplyInput(parsed, classification) {
|
|
2112
|
+
return {
|
|
2113
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
2114
|
+
sourceVersion: optionalOption(parsed, "source-version"),
|
|
2115
|
+
targetVersion: optionalOption(parsed, "target-version"),
|
|
2116
|
+
runId: optionalOption(parsed, "run"),
|
|
2117
|
+
backupPath: optionalOption(parsed, "backup-path"),
|
|
2118
|
+
backupCreatedAt: optionalOption(parsed, "backup-created-at"),
|
|
2119
|
+
reportFile: optionalOption(parsed, "file"),
|
|
2120
|
+
allowActive: false,
|
|
2121
|
+
classification
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function dispatchCanon(context, command, parsed, prepared) {
|
|
1148
2125
|
if (command === "register") {
|
|
1149
|
-
return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
|
|
2126
|
+
return registerCanonRoot(context, { root: requiredOption(parsed, "root") }, prepared.canonRegistration);
|
|
1150
2127
|
}
|
|
1151
2128
|
if (command === "status") {
|
|
1152
2129
|
return getCanonStatus(context, { root: optionalOption(parsed, "root") });
|
|
@@ -1156,6 +2133,15 @@ function dispatchCanon(context, command, parsed) {
|
|
|
1156
2133
|
}
|
|
1157
2134
|
throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
|
|
1158
2135
|
}
|
|
2136
|
+
function mergeJobTarget(parsed, scopeProjectRoot) {
|
|
2137
|
+
return { protocolId: requiredPosition(parsed, 0, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue"), workerId: requiredWorkerId(parsed), workspacePath: workspacePathOption(parsed) };
|
|
2138
|
+
}
|
|
2139
|
+
function mergeCancellationInput(parsed, scopeProjectRoot) {
|
|
2140
|
+
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") };
|
|
2141
|
+
}
|
|
2142
|
+
function mergeWaitInput(parsed, scopeProjectRoot) {
|
|
2143
|
+
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 };
|
|
2144
|
+
}
|
|
1159
2145
|
function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
|
|
1160
2146
|
if (hasOption(parsed, "session-id")) {
|
|
1161
2147
|
throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
|
|
@@ -1179,32 +2165,17 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
|
|
|
1179
2165
|
});
|
|
1180
2166
|
}
|
|
1181
2167
|
if (command === "wait-next") {
|
|
1182
|
-
return waitNextMergeJob(context,
|
|
1183
|
-
projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
|
|
1184
|
-
workerId: requiredWorkerId(parsed),
|
|
1185
|
-
timeoutSeconds: optionalNumber(parsed, "timeout"),
|
|
1186
|
-
pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
|
|
1187
|
-
workspacePath: workspacePathOption(parsed),
|
|
1188
|
-
acquireLock: parseOptionalBoolean(optionalOption(parsed, "acquire-lock"), "acquire-lock") ?? false
|
|
1189
|
-
});
|
|
2168
|
+
return waitNextMergeJob(context, mergeWaitInput(parsed, scopeProjectRoot));
|
|
1190
2169
|
}
|
|
1191
2170
|
if (command === "complete") {
|
|
1192
|
-
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
1193
2171
|
return completeMergeJob(context, {
|
|
1194
|
-
|
|
1195
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue complete"),
|
|
1196
|
-
workerId: requiredWorkerId(parsed),
|
|
1197
|
-
workspacePath: workspacePathOption(parsed),
|
|
2172
|
+
...mergeJobTarget(parsed, scopeProjectRoot),
|
|
1198
2173
|
summary: requiredOption(parsed, "summary")
|
|
1199
2174
|
});
|
|
1200
2175
|
}
|
|
1201
2176
|
if (command === "fail") {
|
|
1202
|
-
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
1203
2177
|
return failMergeJob(context, {
|
|
1204
|
-
|
|
1205
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue fail"),
|
|
1206
|
-
workerId: requiredWorkerId(parsed),
|
|
1207
|
-
workspacePath: workspacePathOption(parsed),
|
|
2178
|
+
...mergeJobTarget(parsed, scopeProjectRoot),
|
|
1208
2179
|
reason: requiredOption(parsed, "reason"),
|
|
1209
2180
|
requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
|
|
1210
2181
|
});
|
|
@@ -1219,15 +2190,7 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
|
|
|
1219
2190
|
});
|
|
1220
2191
|
}
|
|
1221
2192
|
if (command === "cancel") {
|
|
1222
|
-
|
|
1223
|
-
return cancelMergeQueueJob(context, {
|
|
1224
|
-
protocolId,
|
|
1225
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "merge-queue cancel"),
|
|
1226
|
-
reason: requiredOption(parsed, "reason"),
|
|
1227
|
-
workerId: optionalOption(parsed, "worker-id"),
|
|
1228
|
-
workspacePath: optionalOption(parsed, "path"),
|
|
1229
|
-
force: hasOption(parsed, "force")
|
|
1230
|
-
});
|
|
2193
|
+
return cancelMergeQueueJob(context, mergeCancellationInput(parsed, scopeProjectRoot));
|
|
1231
2194
|
}
|
|
1232
2195
|
throw new AppError("usage", `Unknown merge-queue command: ${command ?? "<empty>"}`, 2);
|
|
1233
2196
|
}
|
|
@@ -1247,6 +2210,8 @@ async function dispatchMerge(context, command, parsed, progress) {
|
|
|
1247
2210
|
}
|
|
1248
2211
|
if (command === "apply")
|
|
1249
2212
|
return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
|
|
2213
|
+
if (command === "repair")
|
|
2214
|
+
return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
|
|
1250
2215
|
if (command === "request") {
|
|
1251
2216
|
const action = requiredPosition(parsed, 0, "request action");
|
|
1252
2217
|
const requestId = requiredPosition(parsed, 1, "merge-request-id");
|
|
@@ -1343,7 +2308,7 @@ function completedCommandExitCode(payload) {
|
|
|
1343
2308
|
const exitCode = payload.exit_code;
|
|
1344
2309
|
return typeof exitCode === "number" && Number.isInteger(exitCode) ? exitCode : 0;
|
|
1345
2310
|
}
|
|
1346
|
-
function dispatchCleanup(context, command, parsed) {
|
|
2311
|
+
function dispatchCleanup(context, command, parsed, prepared) {
|
|
1347
2312
|
if (command === "scan") {
|
|
1348
2313
|
return cleanupScan(context, { projectRoot: requiredOption(parsed, "project-root") });
|
|
1349
2314
|
}
|
|
@@ -1351,6 +2316,7 @@ function dispatchCleanup(context, command, parsed) {
|
|
|
1351
2316
|
return cleanupApply(context, {
|
|
1352
2317
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
1353
2318
|
planFile: requiredOption(parsed, "plan-file"),
|
|
2319
|
+
prepared,
|
|
1354
2320
|
reason: requiredOption(parsed, "reason"),
|
|
1355
2321
|
force: hasOption(parsed, "force")
|
|
1356
2322
|
});
|
|
@@ -1371,12 +2337,14 @@ function dispatchProjectConfig(context, parsed) {
|
|
|
1371
2337
|
}
|
|
1372
2338
|
throw new AppError("usage", `Unknown project config action: ${action}`, 2);
|
|
1373
2339
|
}
|
|
1374
|
-
function dispatchSession(context, command, parsed) {
|
|
2340
|
+
function dispatchSession(context, command, parsed, preparedPayload) {
|
|
1375
2341
|
if (command === "register") {
|
|
1376
2342
|
return registerFlowSession(context, {
|
|
2343
|
+
...(preparedPayload ? { preparedPayload } : {}),
|
|
1377
2344
|
payloadBase64: optionalOption(parsed, "payload-base64"),
|
|
1378
2345
|
payloadJson: optionalOption(parsed, "payload-json"),
|
|
1379
2346
|
payloadFile: optionalOption(parsed, "payload-file"),
|
|
2347
|
+
hookEventId: optionalOption(parsed, "hook-event-id"),
|
|
1380
2348
|
sessionId: optionalOption(parsed, "session-id")
|
|
1381
2349
|
});
|
|
1382
2350
|
}
|
|
@@ -1409,7 +2377,45 @@ function dispatchSession(context, command, parsed) {
|
|
|
1409
2377
|
}
|
|
1410
2378
|
throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
|
|
1411
2379
|
}
|
|
1412
|
-
function
|
|
2380
|
+
function snapshotCreateInput(parsed) {
|
|
2381
|
+
const recoveryId = optionalOption(parsed, "recovery-id"), stageEntry = optionalOption(parsed, "stage-entry");
|
|
2382
|
+
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 } : {}) };
|
|
2383
|
+
}
|
|
2384
|
+
function snapshotRestoreInput(parsed) {
|
|
2385
|
+
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") } : {}) };
|
|
2386
|
+
}
|
|
2387
|
+
function runStartInput(parsed) {
|
|
2388
|
+
const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
|
|
2389
|
+
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")) };
|
|
2390
|
+
}
|
|
2391
|
+
function runForkInput(parsed) {
|
|
2392
|
+
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") } : {}) };
|
|
2393
|
+
}
|
|
2394
|
+
function recoveryCaptureInput(parsed) {
|
|
2395
|
+
return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") };
|
|
2396
|
+
}
|
|
2397
|
+
function recoveryResumeInput(parsed) {
|
|
2398
|
+
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") };
|
|
2399
|
+
}
|
|
2400
|
+
function dispatchRun(context, command, parsed, prepared = {}) {
|
|
2401
|
+
if (command === "recovery") {
|
|
2402
|
+
const action = requiredPosition(parsed, 0, "recovery action");
|
|
2403
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
2404
|
+
const runId = requiredPosition(parsed, 1, "run-id");
|
|
2405
|
+
if (action === "inspect")
|
|
2406
|
+
return inspectRunRecovery(context, { projectRoot, runId });
|
|
2407
|
+
if (action === "begin")
|
|
2408
|
+
return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") }, prepared.recoveryPayload);
|
|
2409
|
+
if (action === "seal")
|
|
2410
|
+
return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") }, prepared.recoveryPayload);
|
|
2411
|
+
if (action === "capture")
|
|
2412
|
+
return recordRecoveryCapture(context, recoveryCaptureInput(parsed), prepared.recoveryCapture);
|
|
2413
|
+
if (action === "resume")
|
|
2414
|
+
return resumeRunRecovery(context, recoveryResumeInput(parsed), prepared.recoveryResume);
|
|
2415
|
+
if (action === "accept")
|
|
2416
|
+
return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
2417
|
+
throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
|
|
2418
|
+
}
|
|
1413
2419
|
if (command === "snapshot") {
|
|
1414
2420
|
const action = requiredPosition(parsed, 0, "snapshot action");
|
|
1415
2421
|
if (action === "bootstrap") {
|
|
@@ -1420,56 +2426,27 @@ function dispatchRun(context, command, parsed) {
|
|
|
1420
2426
|
output: requiredOption(parsed, "output")
|
|
1421
2427
|
});
|
|
1422
2428
|
if (bootstrapAction === "restore")
|
|
1423
|
-
return restoreEvalBootstrapSnapshot(context,
|
|
1424
|
-
snapshot: requiredOption(parsed, "snapshot"),
|
|
1425
|
-
projectRoot: requiredOption(parsed, "project-root")
|
|
1426
|
-
});
|
|
2429
|
+
return restoreEvalBootstrapSnapshot(context, snapshotRestoreInput(parsed), prepared.bootstrapRestore);
|
|
1427
2430
|
throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore", 2);
|
|
1428
2431
|
}
|
|
1429
2432
|
if (action === "create") {
|
|
1430
|
-
|
|
1431
|
-
const stageEntry = optionalOption(parsed, "stage-entry");
|
|
1432
|
-
if (candidate && stageEntry !== undefined)
|
|
1433
|
-
throw new AppError("usage", "Use either --candidate or --stage-entry, not both", 2);
|
|
1434
|
-
if (!candidate && stageEntry === undefined)
|
|
1435
|
-
throw new AppError("usage", "Snapshot create requires --candidate or --stage-entry", 2);
|
|
1436
|
-
const input = {
|
|
1437
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1438
|
-
runId: requiredPosition(parsed, 1, "run-id"),
|
|
1439
|
-
output: requiredOption(parsed, "output")
|
|
1440
|
-
};
|
|
1441
|
-
return candidate
|
|
1442
|
-
? createEvalRunSnapshot(context, { ...input, candidate: true })
|
|
1443
|
-
: createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
|
|
2433
|
+
return createEvalRunSnapshot(context, snapshotCreateInput(parsed));
|
|
1444
2434
|
}
|
|
1445
2435
|
if (action === "restore")
|
|
1446
|
-
return restoreEvalRunSnapshot(context,
|
|
1447
|
-
snapshot: requiredOption(parsed, "snapshot"),
|
|
1448
|
-
projectRoot: requiredOption(parsed, "project-root")
|
|
1449
|
-
});
|
|
2436
|
+
return restoreEvalRunSnapshot(context, snapshotRestoreInput(parsed), prepared.snapshotRestore);
|
|
1450
2437
|
throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore | dd-flow run snapshot create <RUN> (--stage-entry <stage>|--candidate) --project-root <path> --output <directory> | dd-flow run snapshot restore --snapshot <directory> --project-root <path>", 2);
|
|
1451
2438
|
}
|
|
2439
|
+
if (command === "fork") {
|
|
2440
|
+
return forkRunFromSnapshot(context, runForkInput(parsed), prepared.runFork);
|
|
2441
|
+
}
|
|
1452
2442
|
if (command === "prepare-vnext-specify") {
|
|
1453
|
-
return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
|
|
2443
|
+
return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
|
|
1454
2444
|
}
|
|
1455
2445
|
if (command === "start") {
|
|
1456
2446
|
if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
|
|
1457
2447
|
assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
|
|
1458
2448
|
}
|
|
1459
|
-
|
|
1460
|
-
return startFlowRun(context, {
|
|
1461
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1462
|
-
workspaceRoot: optionalOption(parsed, "workspace-root"),
|
|
1463
|
-
flowKind: requiredOption(parsed, "flow-kind"),
|
|
1464
|
-
subjectType: requiredOption(parsed, "subject-type"),
|
|
1465
|
-
subjectId: requiredOption(parsed, "subject-id"),
|
|
1466
|
-
slug: requiredOption(parsed, "slug"),
|
|
1467
|
-
nextAction: optionalOption(parsed, "next-action"),
|
|
1468
|
-
preset: optionalOption(parsed, "preset"),
|
|
1469
|
-
taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
|
|
1470
|
-
...(protocolOverrides ? { protocolOverrides } : {}),
|
|
1471
|
-
runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
|
|
1472
|
-
});
|
|
2449
|
+
return startFlowRun(context, prepared.runStart?.input ?? runStartInput(parsed), prepared.runStart?.value);
|
|
1473
2450
|
}
|
|
1474
2451
|
if (command === "status") {
|
|
1475
2452
|
return getFlowRunStatus(context, {
|
|
@@ -1539,83 +2516,163 @@ function dispatchRun(context, command, parsed) {
|
|
|
1539
2516
|
throw new AppError("usage", `Unknown run flags command: ${action}`, 2);
|
|
1540
2517
|
}
|
|
1541
2518
|
if (command === "attach-stage") {
|
|
1542
|
-
return attachFlowRunStage(context,
|
|
1543
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1544
|
-
runId: requiredPosition(parsed, 0, "run-id"),
|
|
1545
|
-
stage: requiredOption(parsed, "stage"),
|
|
1546
|
-
dir: requiredOption(parsed, "dir"),
|
|
1547
|
-
status: requiredOption(parsed, "status"),
|
|
1548
|
-
dataSchemaId: optionalOption(parsed, "data-schema-id")
|
|
1549
|
-
});
|
|
2519
|
+
return attachFlowRunStage(context, stageAttachmentInput(parsed));
|
|
1550
2520
|
}
|
|
1551
2521
|
if (command === "complete-stage") {
|
|
1552
|
-
|
|
1553
|
-
return completeFlowRunStage(context, {
|
|
1554
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1555
|
-
runId: requiredPosition(parsed, 0, "run-id"),
|
|
1556
|
-
stage: requiredOption(parsed, "stage"),
|
|
1557
|
-
status: requiredOption(parsed, "status"),
|
|
1558
|
-
stageReport,
|
|
1559
|
-
data: optionalOption(parsed, "data"),
|
|
1560
|
-
dataSchemaId: optionalOption(parsed, "data-schema-id"),
|
|
1561
|
-
report: optionalOption(parsed, "report"),
|
|
1562
|
-
aliases: allOptions(parsed, "alias")
|
|
1563
|
-
});
|
|
2522
|
+
return completeFlowRunStage(context, stageCompletionInput(parsed));
|
|
1564
2523
|
}
|
|
1565
2524
|
if (command === "complete") {
|
|
1566
2525
|
throw new AppError("usage", "run complete is retired; only an operator may use run override", 2);
|
|
1567
2526
|
}
|
|
1568
2527
|
if (command === "override") {
|
|
1569
|
-
|
|
1570
|
-
if (!['cancelled', 'failed'].includes(status)) {
|
|
1571
|
-
throw new AppError("usage", "run override status must be cancelled or failed", 2, { status });
|
|
1572
|
-
}
|
|
1573
|
-
return completeFlowRun(context, {
|
|
1574
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1575
|
-
runId: requiredPosition(parsed, 0, "run-id"),
|
|
1576
|
-
status,
|
|
1577
|
-
verdict: optionalOption(parsed, "verdict"),
|
|
1578
|
-
nextAction: optionalOption(parsed, "next-action"),
|
|
1579
|
-
manualOverrideReason: requiredOption(parsed, "reason")
|
|
1580
|
-
});
|
|
2528
|
+
return completeFlowRun(context, runOverrideInput(parsed));
|
|
1581
2529
|
}
|
|
1582
2530
|
throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
|
|
1583
2531
|
}
|
|
1584
|
-
|
|
2532
|
+
function stageAttachmentInput(parsed) {
|
|
2533
|
+
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") };
|
|
2534
|
+
}
|
|
2535
|
+
function runOverrideInput(parsed) {
|
|
2536
|
+
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") };
|
|
2537
|
+
}
|
|
2538
|
+
function stageCompletionInput(parsed) {
|
|
2539
|
+
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") };
|
|
2540
|
+
}
|
|
2541
|
+
function runtimeServiceInput(parsed) {
|
|
2542
|
+
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 };
|
|
2543
|
+
}
|
|
2544
|
+
function runtimeProcessStopInput(parsed) {
|
|
2545
|
+
const graceMs = optionalNumber(parsed, "grace-ms");
|
|
2546
|
+
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
|
|
2547
|
+
}
|
|
2548
|
+
function runtimeScopeResumeInput(parsed) {
|
|
2549
|
+
return { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") };
|
|
2550
|
+
}
|
|
2551
|
+
function runtimeProcessRegistrationInput(context, parsed) {
|
|
2552
|
+
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
2553
|
+
const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
|
|
2554
|
+
const kind = requiredOption(parsed, "kind");
|
|
2555
|
+
const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
|
|
2556
|
+
const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
|
|
2557
|
+
const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
|
|
2558
|
+
return {
|
|
2559
|
+
kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
|
|
2560
|
+
...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
|
|
2561
|
+
ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
|
|
2562
|
+
workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
|
|
2563
|
+
operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
|
|
2564
|
+
stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
|
|
2565
|
+
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") } : {}) }
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function runtimeProcessConfirmationInput(parsed) {
|
|
2569
|
+
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
|
|
2570
|
+
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 } : {}) };
|
|
2571
|
+
}
|
|
2572
|
+
function runtimeProcessHeartbeatInput(parsed) {
|
|
2573
|
+
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
2574
|
+
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
|
|
2575
|
+
}
|
|
2576
|
+
function runtimeProcessFinishInput(parsed) {
|
|
2577
|
+
const state = requiredOption(parsed, "state");
|
|
2578
|
+
if (state !== "stopped" && state !== "failed")
|
|
2579
|
+
throw new AppError("validation", "--state must be stopped or failed", 2);
|
|
2580
|
+
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
|
|
2581
|
+
}
|
|
2582
|
+
async function dispatchRuntime(context, command, parsed, progress, prepared = {}) {
|
|
2583
|
+
if (command === "scope") {
|
|
2584
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
|
|
2585
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2586
|
+
throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
|
|
2587
|
+
return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
|
|
2588
|
+
}
|
|
2589
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
|
|
2590
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2591
|
+
throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
|
|
2592
|
+
return { ok: true, ...await launchRuntimeScopeResume(context, runtimeScopeResumeInput(parsed)) };
|
|
2593
|
+
}
|
|
2594
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
|
|
2595
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2596
|
+
throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
|
|
2597
|
+
const mode = requiredOption(parsed, "mode");
|
|
2598
|
+
if (mode !== "pause" && mode !== "stop")
|
|
2599
|
+
throw new AppError("usage", "Scope control mode must be pause or stop", 2);
|
|
2600
|
+
return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, undefined, undefined, undefined, prepared.runtimeManifest) };
|
|
2601
|
+
}
|
|
2602
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
|
|
2603
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2604
|
+
throw new AppError("usage", "Invalid internal scope worker arguments", 2);
|
|
2605
|
+
return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
|
|
2606
|
+
}
|
|
2607
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
|
|
2608
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2609
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
|
|
2610
|
+
return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
|
|
2611
|
+
}
|
|
2612
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
|
|
2613
|
+
if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
|
|
2614
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
|
|
2615
|
+
return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
|
|
2616
|
+
}
|
|
2617
|
+
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))
|
|
2618
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
|
|
2619
|
+
return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
|
|
2620
|
+
}
|
|
1585
2621
|
if (command !== "process")
|
|
1586
|
-
throw new AppError("usage", "Usage: dd-flow runtime process
|
|
2622
|
+
throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
|
|
1587
2623
|
const action = requiredPosition(parsed, 0, "runtime process action");
|
|
2624
|
+
if (action === "check-admission")
|
|
2625
|
+
return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
|
|
2626
|
+
if (action === "start")
|
|
2627
|
+
return await startRuntimeService(context, { ...runtimeServiceInput(parsed), progress });
|
|
2628
|
+
if (action === "stop")
|
|
2629
|
+
return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
|
|
1588
2630
|
if (action === "register") {
|
|
1589
|
-
const
|
|
1590
|
-
const process = registerManagedProcess(context, {
|
|
1591
|
-
kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
|
|
1592
|
-
projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
|
|
1593
|
-
workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
|
|
1594
|
-
operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
|
|
1595
|
-
stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
|
|
1596
|
-
});
|
|
2631
|
+
const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
|
|
1597
2632
|
return { ok: true, process };
|
|
1598
2633
|
}
|
|
1599
|
-
if (action === "confirm")
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
return { ok:
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2634
|
+
if (action === "confirm")
|
|
2635
|
+
return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
|
|
2636
|
+
if (action === "heartbeat")
|
|
2637
|
+
return { ok: heartbeatManagedProcess(context, runtimeProcessHeartbeatInput(parsed)) };
|
|
2638
|
+
if (action === "admission") {
|
|
2639
|
+
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
2640
|
+
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))
|
|
2641
|
+
throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
|
|
2642
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
2643
|
+
if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
|
|
2644
|
+
throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
|
|
2645
|
+
if (record.project_id && record.run_id) {
|
|
2646
|
+
if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
2647
|
+
throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
|
|
2648
|
+
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") });
|
|
2649
|
+
if (!recovering) {
|
|
2650
|
+
if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
|
|
2651
|
+
throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
|
|
2652
|
+
assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
|
|
2653
|
+
}
|
|
2654
|
+
const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
|
|
2655
|
+
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")]);
|
|
2656
|
+
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") });
|
|
2657
|
+
if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
|
|
2658
|
+
throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
|
|
2659
|
+
}
|
|
2660
|
+
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") });
|
|
2661
|
+
return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
|
|
1607
2662
|
}
|
|
1608
|
-
if (action === "
|
|
1609
|
-
const
|
|
1610
|
-
if (
|
|
1611
|
-
throw new AppError("
|
|
1612
|
-
return
|
|
2663
|
+
if (action === "release-turn") {
|
|
2664
|
+
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
2665
|
+
if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
2666
|
+
throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
|
|
2667
|
+
return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
|
|
1613
2668
|
}
|
|
2669
|
+
if (action === "finish")
|
|
2670
|
+
return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
|
|
1614
2671
|
if (action === "status")
|
|
1615
2672
|
return { ok: true, processes: managedProcessStatus(context) };
|
|
1616
2673
|
if (action === "reconcile")
|
|
1617
|
-
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"),
|
|
1618
|
-
throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
2674
|
+
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
|
|
2675
|
+
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
|
|
1619
2676
|
}
|
|
1620
2677
|
function dispatchStat(context, command, parsed) {
|
|
1621
2678
|
if (command === "usage") {
|
|
@@ -1802,7 +2859,9 @@ function dispatchSchema(context, command, parsed) {
|
|
|
1802
2859
|
// searching project directories: run_id is a structural field in the
|
|
1803
2860
|
// submitted artifact and the database owns its materialized run_root.
|
|
1804
2861
|
const explicitRunId = optionalOption(parsed, "run");
|
|
1805
|
-
const
|
|
2862
|
+
const data = prepareJsonFile(file, "schema input").value;
|
|
2863
|
+
const declared = data && typeof data === "object" && "run_id" in data ? data.run_id : undefined;
|
|
2864
|
+
const declaredRunId = explicitRunId ?? (typeof declared === "string" && declared.trim() ? declared : undefined);
|
|
1806
2865
|
const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
|
|
1807
2866
|
const bound = projectRoot
|
|
1808
2867
|
? context.db.get("SELECT r.id, r.run_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE p.root = ? AND (r.id = ? OR r.run_root = ?) LIMIT 1", [fs.realpathSync(projectRoot), declaredRunId ?? "", fileRunRoot ?? ""])
|
|
@@ -1810,6 +2869,7 @@ function dispatchSchema(context, command, parsed) {
|
|
|
1810
2869
|
return validateSchema({
|
|
1811
2870
|
schemaName: requiredOption(parsed, "schema"),
|
|
1812
2871
|
file,
|
|
2872
|
+
data,
|
|
1813
2873
|
...(schemaDir ? { schemaDir } : {}),
|
|
1814
2874
|
...(projectRoot ? { projectRoot } : {}),
|
|
1815
2875
|
...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
|
|
@@ -1819,14 +2879,11 @@ function dispatchSchema(context, command, parsed) {
|
|
|
1819
2879
|
}
|
|
1820
2880
|
throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
|
|
1821
2881
|
}
|
|
1822
|
-
function
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
catch {
|
|
1828
|
-
return undefined;
|
|
1829
|
-
}
|
|
2882
|
+
function laneWorkspaceInput(parsed) {
|
|
2883
|
+
return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workspacePath: requiredOption(parsed, "path"), branch: optionalOption(parsed, "branch") };
|
|
2884
|
+
}
|
|
2885
|
+
function laneWaiterCancelInput(parsed) {
|
|
2886
|
+
return { projectRoot: requiredOption(parsed, "project-root"), lane: requiredOption(parsed, "lane"), workerId: requiredOption(parsed, "worker-id"), reason: requiredOption(parsed, "reason") };
|
|
1830
2887
|
}
|
|
1831
2888
|
async function dispatchLane(context, command, parsed) {
|
|
1832
2889
|
if (command === "status") {
|
|
@@ -1838,24 +2895,14 @@ async function dispatchLane(context, command, parsed) {
|
|
|
1838
2895
|
if (command === "waiter") {
|
|
1839
2896
|
const action = requiredPosition(parsed, 0, "lane waiter action");
|
|
1840
2897
|
if (action === "cancel") {
|
|
1841
|
-
return cancelLaneWaiter(context,
|
|
1842
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1843
|
-
lane: requiredOption(parsed, "lane"),
|
|
1844
|
-
workerId: requiredOption(parsed, "worker-id"),
|
|
1845
|
-
reason: requiredOption(parsed, "reason")
|
|
1846
|
-
});
|
|
2898
|
+
return cancelLaneWaiter(context, laneWaiterCancelInput(parsed));
|
|
1847
2899
|
}
|
|
1848
2900
|
throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
|
|
1849
2901
|
}
|
|
1850
2902
|
if (command === "workspace") {
|
|
1851
2903
|
const action = requiredPosition(parsed, 0, "lane workspace action");
|
|
1852
2904
|
if (action === "set") {
|
|
1853
|
-
return setLaneWorkspace(context,
|
|
1854
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1855
|
-
lane: requiredOption(parsed, "lane"),
|
|
1856
|
-
workspacePath: requiredOption(parsed, "path"),
|
|
1857
|
-
branch: optionalOption(parsed, "branch")
|
|
1858
|
-
});
|
|
2905
|
+
return setLaneWorkspace(context, laneWorkspaceInput(parsed));
|
|
1859
2906
|
}
|
|
1860
2907
|
if (action === "check") {
|
|
1861
2908
|
return checkLaneWorkspace(context, {
|
|
@@ -1927,7 +2974,7 @@ async function dispatchLane(context, command, parsed) {
|
|
|
1927
2974
|
}
|
|
1928
2975
|
throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
|
|
1929
2976
|
}
|
|
1930
|
-
function dispatchCodexHooks(context, parsed) {
|
|
2977
|
+
function dispatchCodexHooks(context, parsed, prepared) {
|
|
1931
2978
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|
|
1932
2979
|
if (action === "print") {
|
|
1933
2980
|
return printCodexHooks(context, {
|
|
@@ -1949,7 +2996,7 @@ function dispatchCodexHooks(context, parsed) {
|
|
|
1949
2996
|
target: optionalOption(parsed, "target"),
|
|
1950
2997
|
profile: optionalOption(parsed, "profile"),
|
|
1951
2998
|
yes: hasOption(parsed, "yes")
|
|
1952
|
-
});
|
|
2999
|
+
}, prepared.codexHooks);
|
|
1953
3000
|
}
|
|
1954
3001
|
if (action === "remove") {
|
|
1955
3002
|
return removeCodexHooks(context, {
|
|
@@ -1957,11 +3004,14 @@ function dispatchCodexHooks(context, parsed) {
|
|
|
1957
3004
|
target: optionalOption(parsed, "target"),
|
|
1958
3005
|
profile: optionalOption(parsed, "profile"),
|
|
1959
3006
|
yes: hasOption(parsed, "yes")
|
|
1960
|
-
});
|
|
3007
|
+
}, prepared.codexHooks);
|
|
1961
3008
|
}
|
|
1962
3009
|
throw new AppError("usage", `Unknown codex hooks action: ${action}`, 2);
|
|
1963
3010
|
}
|
|
1964
|
-
function
|
|
3011
|
+
function codexHomeInput(parsed) {
|
|
3012
|
+
return { projectRoot: requiredOption(parsed, "project-root"), profile: optionalOption(parsed, "profile"), sourceHome: optionalOption(parsed, "source-home"), targetHome: optionalOption(parsed, "target-home") };
|
|
3013
|
+
}
|
|
3014
|
+
function dispatchCodexHome(context, parsed, prepared) {
|
|
1965
3015
|
const action = requiredPosition(parsed, 0, "codex home action");
|
|
1966
3016
|
if (action === "plan") {
|
|
1967
3017
|
return planCodexHome(context, {
|
|
@@ -1972,12 +3022,7 @@ function dispatchCodexHome(context, parsed) {
|
|
|
1972
3022
|
});
|
|
1973
3023
|
}
|
|
1974
3024
|
if (action === "init") {
|
|
1975
|
-
return initCodexHome(context,
|
|
1976
|
-
projectRoot: requiredOption(parsed, "project-root"),
|
|
1977
|
-
profile: optionalOption(parsed, "profile"),
|
|
1978
|
-
sourceHome: optionalOption(parsed, "source-home"),
|
|
1979
|
-
targetHome: optionalOption(parsed, "target-home")
|
|
1980
|
-
});
|
|
3025
|
+
return initCodexHome(context, codexHomeInput(parsed), prepared.codexHome);
|
|
1981
3026
|
}
|
|
1982
3027
|
if (action === "status") {
|
|
1983
3028
|
return getCodexHomeStatus(context, {
|
|
@@ -2000,11 +3045,14 @@ function dispatchCodexHome(context, parsed) {
|
|
|
2000
3045
|
}
|
|
2001
3046
|
throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
|
|
2002
3047
|
}
|
|
2003
|
-
function dispatchCodexHook(context, parsed, stdin) {
|
|
3048
|
+
async function dispatchCodexHook(context, parsed, stdin) {
|
|
2004
3049
|
const action = requiredPosition(parsed, 0, "codex hook action");
|
|
2005
3050
|
if (action === "handle") {
|
|
2006
3051
|
const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
|
|
2007
|
-
const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }) : context;
|
|
3052
|
+
const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }, "hook") : context;
|
|
3053
|
+
const proof = await verifyCodexHookDelivery(hookContext.env, JSON.parse(stdin || "{}"));
|
|
3054
|
+
if (proof)
|
|
3055
|
+
hookContext.codexHookProof = proof;
|
|
2008
3056
|
const result = handleCodexHook(hookContext, {
|
|
2009
3057
|
event: requiredOption(parsed, "event"),
|
|
2010
3058
|
projectRoot: optionalOption(parsed, "project-root"),
|
|
@@ -2027,6 +3075,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
|
|
|
2027
3075
|
}
|
|
2028
3076
|
throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
|
|
2029
3077
|
}
|
|
3078
|
+
function dispatchZcodeHook(context, parsed, stdin) {
|
|
3079
|
+
if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
|
|
3080
|
+
throw new AppError("usage", "Unknown zcode hook action", 2);
|
|
3081
|
+
const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
|
|
3082
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
3083
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
3084
|
+
}
|
|
3085
|
+
return {};
|
|
3086
|
+
}
|
|
2030
3087
|
function dispatchGrokEvent(context, parsed, stdin) {
|
|
2031
3088
|
const action = requiredPosition(parsed, 0, "grok event action");
|
|
2032
3089
|
if (action === "handle") {
|
|
@@ -2050,7 +3107,7 @@ function dispatchAgyEvent(context, parsed, stdin) {
|
|
|
2050
3107
|
return handleAgyEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
|
|
2051
3108
|
throw new AppError("usage", `Unknown agy event action: ${action}`, 2);
|
|
2052
3109
|
}
|
|
2053
|
-
function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
|
|
3110
|
+
function dispatchWorktree(context, command, parsed, scopeProjectRoot, prepared) {
|
|
2054
3111
|
if (command === "plan") {
|
|
2055
3112
|
return planWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
|
|
2056
3113
|
}
|
|
@@ -2061,13 +3118,13 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
|
|
|
2061
3118
|
branch: requiredOption(parsed, "branch"),
|
|
2062
3119
|
base: requiredOption(parsed, "base"),
|
|
2063
3120
|
path: requiredOption(parsed, "path")
|
|
2064
|
-
});
|
|
3121
|
+
}, prepared.worktreeCreate);
|
|
2065
3122
|
}
|
|
2066
3123
|
if (command === "status") {
|
|
2067
3124
|
return getWorktreeStatus(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
|
|
2068
3125
|
}
|
|
2069
3126
|
if (command === "bootstrap") {
|
|
2070
|
-
return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot });
|
|
3127
|
+
return bootstrapWorktree(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: scopeProjectRoot }, prepared.worktreeBootstrap);
|
|
2071
3128
|
}
|
|
2072
3129
|
if (command === "close") {
|
|
2073
3130
|
return closeWorktree(context, {
|
|
@@ -2079,52 +3136,50 @@ function dispatchWorktree(context, command, parsed, scopeProjectRoot) {
|
|
|
2079
3136
|
throw new AppError("usage", `Unknown worktree command: ${command ?? "<empty>"}`, 2);
|
|
2080
3137
|
}
|
|
2081
3138
|
function dispatchPlanItem(context, parsed, projectRoot) {
|
|
3139
|
+
return updatePlanItem(context, planItemInput(parsed, projectRoot));
|
|
3140
|
+
}
|
|
3141
|
+
function planItemInput(parsed, projectRoot) {
|
|
2082
3142
|
const action = requiredPosition(parsed, 0, "plan item action");
|
|
2083
3143
|
const protocolId = requiredPosition(parsed, 1, "protocol-id");
|
|
2084
3144
|
const itemId = requiredPosition(parsed, 2, "item-id");
|
|
2085
3145
|
if (action === "start") {
|
|
2086
|
-
return
|
|
3146
|
+
return { protocolId, projectRoot, itemId, action };
|
|
2087
3147
|
}
|
|
2088
3148
|
if (action === "done") {
|
|
2089
|
-
return
|
|
3149
|
+
return {
|
|
3150
|
+
action,
|
|
2090
3151
|
protocolId,
|
|
2091
3152
|
projectRoot,
|
|
2092
3153
|
itemId,
|
|
2093
3154
|
summary: requiredOption(parsed, "summary"),
|
|
2094
3155
|
evidence: parsed.options.get("evidence") ?? []
|
|
2095
|
-
}
|
|
3156
|
+
};
|
|
2096
3157
|
}
|
|
2097
3158
|
if (action === "block") {
|
|
2098
|
-
return
|
|
3159
|
+
return {
|
|
3160
|
+
action,
|
|
2099
3161
|
protocolId,
|
|
2100
3162
|
projectRoot,
|
|
2101
3163
|
itemId,
|
|
2102
3164
|
reason: requiredOption(parsed, "reason"),
|
|
2103
3165
|
userRequired: parseBoolean(requiredOption(parsed, "user-required"), "user-required")
|
|
2104
|
-
}
|
|
3166
|
+
};
|
|
2105
3167
|
}
|
|
2106
3168
|
if (action === "skip") {
|
|
2107
|
-
return
|
|
3169
|
+
return {
|
|
3170
|
+
action,
|
|
2108
3171
|
protocolId,
|
|
2109
3172
|
projectRoot,
|
|
2110
3173
|
itemId,
|
|
2111
3174
|
reason: requiredOption(parsed, "reason")
|
|
2112
|
-
}
|
|
3175
|
+
};
|
|
2113
3176
|
}
|
|
2114
3177
|
throw new AppError("usage", `Unknown plan item action: ${action}`, 2);
|
|
2115
3178
|
}
|
|
2116
3179
|
function readOptionalJsonFile(file) {
|
|
2117
3180
|
if (!file)
|
|
2118
3181
|
return undefined;
|
|
2119
|
-
|
|
2120
|
-
throw new AppError("not_found", `JSON file does not exist: ${file}`, 1);
|
|
2121
|
-
}
|
|
2122
|
-
try {
|
|
2123
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2124
|
-
}
|
|
2125
|
-
catch {
|
|
2126
|
-
throw new AppError("validation", `JSON file is malformed: ${file}`, 2);
|
|
2127
|
-
}
|
|
3182
|
+
return prepareJsonFile(file, "JSON input").value;
|
|
2128
3183
|
}
|
|
2129
3184
|
function parseFlowFlagOverrides(values) {
|
|
2130
3185
|
const result = {};
|
|
@@ -2159,7 +3214,11 @@ function requiredPosition(parsed, index, label) {
|
|
|
2159
3214
|
function optionalOption(parsed, key) {
|
|
2160
3215
|
const values = parsed.options.get(key);
|
|
2161
3216
|
const value = values?.[values.length - 1];
|
|
2162
|
-
|
|
3217
|
+
if (value === flagValue)
|
|
3218
|
+
throw new AppError("usage", `--${key} requires a value`, 2);
|
|
3219
|
+
if (value !== undefined && !value.trim())
|
|
3220
|
+
throw new AppError("usage", `--${key} requires a non-empty value`, 2);
|
|
3221
|
+
return value;
|
|
2163
3222
|
}
|
|
2164
3223
|
function allOptions(parsed, key) {
|
|
2165
3224
|
return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
|
|
@@ -2324,8 +3383,8 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2324
3383
|
if (family === "merge-queue") {
|
|
2325
3384
|
return projectRootForMergeQueueMutation(context, command, parsed);
|
|
2326
3385
|
}
|
|
2327
|
-
if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
|
|
2328
|
-
return requiredOption(parsed, "project-root");
|
|
3386
|
+
if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
|
|
3387
|
+
return resolvedScope ?? requiredOption(parsed, "project-root");
|
|
2329
3388
|
}
|
|
2330
3389
|
if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
|
|
2331
3390
|
return requiredOption(parsed, "project-root");
|
|
@@ -2343,7 +3402,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2343
3402
|
return requiredOption(parsed, "project-root");
|
|
2344
3403
|
}
|
|
2345
3404
|
if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? "")) {
|
|
2346
|
-
return requiredOption(parsed, "project-root");
|
|
3405
|
+
return resolvedScope ?? requiredOption(parsed, "project-root");
|
|
2347
3406
|
}
|
|
2348
3407
|
if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
|
|
2349
3408
|
return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
|
|
@@ -2357,7 +3416,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2357
3416
|
if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
|
|
2358
3417
|
return requiredOption(parsed, "project-root");
|
|
2359
3418
|
}
|
|
2360
|
-
if (family
|
|
3419
|
+
if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
|
|
2361
3420
|
return requiredOption(parsed, "project-root");
|
|
2362
3421
|
if (family === "codex" && command === "hooks") {
|
|
2363
3422
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|
|
@@ -2394,14 +3453,36 @@ function projectRootForMergeQueueMutation(context, command, parsed) {
|
|
|
2394
3453
|
function protocolProjectRoot(context, protocolId) {
|
|
2395
3454
|
return requireProtocol(context, protocolId).project_root;
|
|
2396
3455
|
}
|
|
3456
|
+
function prepareCliWorkRepair(context, parsed, objective, scopeProjectRoot) {
|
|
3457
|
+
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") ?? [];
|
|
3458
|
+
if ([findings.length > 0, unresolved.length > 0, receipts.length > 0].filter(Boolean).length !== 1)
|
|
3459
|
+
throw new AppError("usage", "Repair requires exactly one source: --from-check, --from-finding or --from-unresolved", 2);
|
|
3460
|
+
const scope = { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "work repair add"), runId: requiredOption(parsed, "run"), objective };
|
|
3461
|
+
if (findings.length) {
|
|
3462
|
+
if (origins.length || hasOption(parsed, "verification-file"))
|
|
3463
|
+
throw new AppError("usage", "Finding repair derives its origins and cannot accept --origin-work or --verification-file", 2);
|
|
3464
|
+
return prepareVnextCodeReviewRepair(context, { ...scope, findingIds: findings, checkRefsByFinding: Object.fromEntries(findings.map(finding => [finding, parsed.options.get("check-ref") ?? []])) });
|
|
3465
|
+
}
|
|
3466
|
+
if (!origins.length)
|
|
3467
|
+
throw new AppError("usage", "Repair requires at least one --origin-work", 2);
|
|
3468
|
+
if (hasOption(parsed, "check-ref"))
|
|
3469
|
+
throw new AppError("usage", "--check-ref is only supported with --from-finding", 2);
|
|
3470
|
+
if (receipts.length > 1)
|
|
3471
|
+
throw new AppError("usage", "Create one repair Work per --from-check receipt", 2);
|
|
3472
|
+
if (!unresolved.length && hasOption(parsed, "verification-file"))
|
|
3473
|
+
throw new AppError("usage", "--verification-file is only supported with --from-unresolved", 2);
|
|
3474
|
+
const input = { ...scope, originWorkIds: origins, ...(unresolved.length ? { semanticUnresolved: unresolved, verificationPath: requiredOption(parsed, "verification-file") } : { checkReceiptId: receipts[0] }) };
|
|
3475
|
+
return { input, value: prepareVnextCodeRepair(context, input) };
|
|
3476
|
+
}
|
|
3477
|
+
function workerPromptInput(parsed) {
|
|
3478
|
+
const workspaceRoot = optionalOption(parsed, "workspace-root"), taskFile = optionalOption(parsed, "task-file");
|
|
3479
|
+
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 } : {}) };
|
|
3480
|
+
}
|
|
2397
3481
|
function runnerIntake(context) {
|
|
2398
3482
|
const input = context.value.task_input?.[0];
|
|
2399
3483
|
if (!input?.path)
|
|
2400
3484
|
throw new AppError("validation", "Runner SPECIFY context requires one ordered task_input file", 2);
|
|
2401
|
-
|
|
2402
|
-
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
2403
|
-
throw new AppError("not_found", "Runner SPECIFY task input is missing", 2, { task_input: file });
|
|
2404
|
-
return fs.readFileSync(file, "utf8");
|
|
3485
|
+
return prepareTextFile(input.path, "Runner SPECIFY task input").text;
|
|
2405
3486
|
}
|
|
2406
3487
|
/** Stage services install context before their lifecycle transition. Keeping
|
|
2407
3488
|
* this check here makes an incomplete service migration fail closed instead of
|