@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.74
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 +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -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 +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-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/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +120 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +223 -215
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +592 -154
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { quote } from "shell-quote";
|
|
5
|
+
import { observeLifecycleCommand } from "../services/lifecycle-invocations.js";
|
|
6
|
+
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, issueLifecycleInvocation, lifecycleInvocationScope, settleLifecycleInvocation, settleLifecycleRejection } from "../services/lifecycle-invocations.js";
|
|
3
7
|
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
8
|
+
import { migrateStoreWriter } from "../storage/writer-migration.js";
|
|
4
9
|
import { helpForArgs } from "./help.js";
|
|
5
10
|
import { AppError, isAppError } from "../shared/errors.js";
|
|
6
11
|
import { writeJson } from "../shared/json.js";
|
|
@@ -10,7 +15,7 @@ import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlan
|
|
|
10
15
|
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
11
16
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
12
17
|
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";
|
|
18
|
+
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeHook, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
14
19
|
import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
|
|
15
20
|
import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
|
|
16
21
|
import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
|
|
@@ -26,7 +31,7 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunSe
|
|
|
26
31
|
import { previewNextEntityId } from "../services/ids.js";
|
|
27
32
|
import { validateSchema } from "../services/schema-validation.js";
|
|
28
33
|
import { preflightMemoryPermissions } from "../services/memory-permissions.js";
|
|
29
|
-
import { ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
|
|
34
|
+
import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
|
|
30
35
|
import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
|
|
31
36
|
import { publishProjectSummary } from "../services/project-summary.js";
|
|
32
37
|
import { requireProjectByRoot } from "../services/projects.js";
|
|
@@ -39,18 +44,30 @@ import { finishVnextPlan, isVnextPlanRun, startVnextPlan } from "../services/vne
|
|
|
39
44
|
import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
|
|
40
45
|
import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services/vnext-code.js";
|
|
41
46
|
import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
|
|
42
|
-
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
47
|
+
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
43
48
|
import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
|
|
44
|
-
import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
|
|
49
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout } from "../services/vnext-fanout.js";
|
|
45
50
|
import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
|
|
46
51
|
import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
|
|
47
52
|
import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
|
|
48
53
|
import { renderWorkerPrompt } from "../services/prompts.js";
|
|
49
54
|
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";
|
|
55
|
+
import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
|
|
56
|
+
import { launchExternalWork } from "../services/external-work-launch.js";
|
|
51
57
|
import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
|
|
58
|
+
import { forkRunFromSnapshot } from "../services/run-fork.js";
|
|
59
|
+
import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
|
|
60
|
+
import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
|
|
52
61
|
import { loadExternalStageContext } from "../services/stage-context.js";
|
|
53
|
-
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
62
|
+
import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
63
|
+
import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns } from "../services/runtime-budget.js";
|
|
64
|
+
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
65
|
+
import { launchRuntimeScopeControl, launchRuntimeScopeResume, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
66
|
+
import { launchRunController, resumeRunController, serveRunController, runControllerStatus, answerRunController, supplyRunControllerContext } from "../services/run-controller.js";
|
|
67
|
+
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
68
|
+
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
69
|
+
import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
70
|
+
import { startRuntimeService } from "../services/runtime-service.js";
|
|
54
71
|
const defaultIo = {
|
|
55
72
|
stdout: process.stdout,
|
|
56
73
|
stderr: process.stderr,
|
|
@@ -58,9 +75,20 @@ const defaultIo = {
|
|
|
58
75
|
};
|
|
59
76
|
const flagValue = "__dd_flow_flag__";
|
|
60
77
|
export async function runCli(args, io = defaultIo, env = process.env) {
|
|
61
|
-
|
|
78
|
+
let output;
|
|
79
|
+
try {
|
|
80
|
+
output = parseOutputOptions(args);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
writeJson(io.stderr, { ok: false, error: { code: "usage", message } });
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
62
87
|
const progress = progressForCommand(output.args);
|
|
63
88
|
let heartbeat;
|
|
89
|
+
let invocationSettlement;
|
|
90
|
+
let settleHook;
|
|
91
|
+
let diagnosticHome;
|
|
64
92
|
try {
|
|
65
93
|
if (output.args.length === 1 && output.args[0] === "--version") {
|
|
66
94
|
const payload = getCliVersionReport();
|
|
@@ -75,6 +103,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
75
103
|
io.stdout.write(`${help}\n`);
|
|
76
104
|
return 0;
|
|
77
105
|
}
|
|
106
|
+
assertSingletonOptions(output.args);
|
|
107
|
+
if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
|
|
108
|
+
const controlArgs = parseArgs(output.args.slice(2));
|
|
109
|
+
requiredOption(controlArgs, "run");
|
|
110
|
+
requiredOption(controlArgs, "project-root");
|
|
111
|
+
}
|
|
112
|
+
if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
|
|
113
|
+
&& hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
|
|
114
|
+
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);
|
|
115
|
+
}
|
|
78
116
|
const classification = classifyCliOperation(output.args, env);
|
|
79
117
|
if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
|
|
80
118
|
throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
|
|
@@ -94,14 +132,54 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
94
132
|
if (routed?.routed && !routed.inProcess) {
|
|
95
133
|
if (heartbeat)
|
|
96
134
|
clearInterval(heartbeat);
|
|
97
|
-
emitHumanProgress(io, output, progress, "done");
|
|
135
|
+
emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
|
|
136
|
+
if (output.progressJsonl && routed.exitCode !== 0)
|
|
137
|
+
writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
|
|
98
138
|
return routed.exitCode;
|
|
99
139
|
}
|
|
100
140
|
const dispatchContext = routed?.inProcess
|
|
101
141
|
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
|
|
102
|
-
: createContext(env, databaseModeForCommand(output.args, classification));
|
|
142
|
+
: createContext({ ...env }, databaseModeForCommand(output.args, classification));
|
|
143
|
+
diagnosticHome = dispatchContext.ddFlowHome;
|
|
103
144
|
preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
|
|
104
|
-
|
|
145
|
+
dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
|
|
146
|
+
let dispatchArgs = output.args;
|
|
147
|
+
settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
|
|
148
|
+
let retainedOutcome;
|
|
149
|
+
const invocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
|
|
150
|
+
if (invocationId) {
|
|
151
|
+
const command = quote(["dd-flow", ...output.args]);
|
|
152
|
+
const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
|
|
153
|
+
assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
|
|
154
|
+
if (admission.replay) {
|
|
155
|
+
retainedOutcome = admission.outcome;
|
|
156
|
+
settleHook = undefined;
|
|
157
|
+
delete dispatchContext.observeLifecycleHook;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
invocationSettlement = { context: dispatchContext, id: invocationId };
|
|
161
|
+
dispatchArgs = [...output.args, "--hook-event-id", admission.eventKey];
|
|
162
|
+
dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
|
|
163
|
+
dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (retainedOutcome?.error) {
|
|
167
|
+
const error = retainedOutcome.error;
|
|
168
|
+
throw new AppError(error.code, error.message, error.exitCode, error.details);
|
|
169
|
+
}
|
|
170
|
+
if (!retainedOutcome)
|
|
171
|
+
settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
|
|
172
|
+
const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output);
|
|
173
|
+
const completedHook = settleHook;
|
|
174
|
+
settleHook = undefined;
|
|
175
|
+
completedHook?.({ result });
|
|
176
|
+
if (invocationSettlement) {
|
|
177
|
+
const completed = invocationSettlement;
|
|
178
|
+
invocationSettlement = undefined;
|
|
179
|
+
// A failure to persist an already completed result is an unknown reply,
|
|
180
|
+
// not a failed operation eligible for a new productive retry.
|
|
181
|
+
settleLifecycleInvocation(completed.context, completed.id, { result });
|
|
182
|
+
}
|
|
105
183
|
const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
|
|
106
184
|
const resultWithWarnings = addWarnings(result, refreshWarnings);
|
|
107
185
|
if (heartbeat)
|
|
@@ -117,9 +195,41 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
117
195
|
io.stdout.write(renderHumanResult(output.args, payload));
|
|
118
196
|
return completedCommandExitCode(payload);
|
|
119
197
|
}
|
|
120
|
-
catch (
|
|
198
|
+
catch (caught) {
|
|
199
|
+
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 } });
|
|
200
|
+
if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
|
|
201
|
+
try {
|
|
202
|
+
const directory = path.join(diagnosticHome, "diagnostics", "cli");
|
|
203
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
204
|
+
const artifact = path.join(directory, `${randomUUID()}.json`);
|
|
205
|
+
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 });
|
|
206
|
+
error.details.diagnostic_artifact = artifact;
|
|
207
|
+
}
|
|
208
|
+
catch (secondary) {
|
|
209
|
+
io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
121
212
|
if (heartbeat)
|
|
122
213
|
clearInterval(heartbeat);
|
|
214
|
+
if (invocationSettlement && error.details.effect !== "committed") {
|
|
215
|
+
try {
|
|
216
|
+
if (isAppError(error))
|
|
217
|
+
settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
|
|
218
|
+
else
|
|
219
|
+
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
220
|
+
}
|
|
221
|
+
catch (persistenceError) {
|
|
222
|
+
io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (settleHook) {
|
|
226
|
+
try {
|
|
227
|
+
settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
|
|
228
|
+
}
|
|
229
|
+
catch (persistence) {
|
|
230
|
+
io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
123
233
|
const payload = isAppError(error)
|
|
124
234
|
? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
|
|
125
235
|
: { ok: false, error: { code: "unexpected", message: String(error) } };
|
|
@@ -127,7 +237,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
127
237
|
if (output.progressJsonl)
|
|
128
238
|
writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
|
|
129
239
|
const errorPayload = attachProgress(payload, progress, output);
|
|
130
|
-
|
|
240
|
+
let visible = errorPayload;
|
|
241
|
+
try {
|
|
242
|
+
visible = writeResponseFile(errorPayload, output);
|
|
243
|
+
}
|
|
244
|
+
catch (publication) {
|
|
245
|
+
error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
|
|
246
|
+
}
|
|
131
247
|
if (output.json && output.progressJsonl)
|
|
132
248
|
writeJson(io.stdout, errorPayload);
|
|
133
249
|
else if (output.json)
|
|
@@ -152,6 +268,7 @@ function resolveProjectRootBeforeRouting(args, env) {
|
|
|
152
268
|
function databaseModeForCommand(args, classification) {
|
|
153
269
|
const [family, command] = args;
|
|
154
270
|
if (family === "engine" ||
|
|
271
|
+
family === "storage" && command === "migrate-writer" ||
|
|
155
272
|
family === "version" ||
|
|
156
273
|
family === "schema" ||
|
|
157
274
|
family === "canon" && command === "resolve" ||
|
|
@@ -503,6 +620,20 @@ function shortSha(value) {
|
|
|
503
620
|
async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
|
|
504
621
|
const [family, command, ...rest] = args;
|
|
505
622
|
const parsed = parseArgs(rest);
|
|
623
|
+
if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
|
|
624
|
+
const payload = JSON.parse(await readStdin(io.stdin));
|
|
625
|
+
if (typeof payload.command !== "string" || !payload.scope || typeof payload.scope !== "object")
|
|
626
|
+
throw new AppError("usage", "Expected command and managed scope on stdin", 2);
|
|
627
|
+
assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
|
|
628
|
+
return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
|
|
629
|
+
}
|
|
630
|
+
if (family === "storage" && command === "migrate-writer") {
|
|
631
|
+
if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
632
|
+
throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
|
|
633
|
+
if (parsed.options.get("offline")?.[0] !== flagValue)
|
|
634
|
+
throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
|
|
635
|
+
return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
|
|
636
|
+
}
|
|
506
637
|
if (family === "engine") {
|
|
507
638
|
return dispatchEngine(context, command, parsed, classification);
|
|
508
639
|
}
|
|
@@ -517,7 +648,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
517
648
|
return getCliVersionReport();
|
|
518
649
|
}
|
|
519
650
|
if (family === "runtime") {
|
|
520
|
-
return await dispatchRuntime(context, command, parsed);
|
|
651
|
+
return await dispatchRuntime(context, command, parsed, commandProgress(io, output));
|
|
521
652
|
}
|
|
522
653
|
if (family === "canon") {
|
|
523
654
|
return dispatchCanon(context, command, parsed);
|
|
@@ -660,12 +791,19 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
660
791
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
|
|
661
792
|
if (action === "status")
|
|
662
793
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
794
|
+
if (action === "reconcile")
|
|
795
|
+
return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
|
|
663
796
|
if (action === "dispatch") {
|
|
664
797
|
if (stage === "plan-review")
|
|
665
798
|
return dispatchVnextPlanReview(context, { projectRoot, runId });
|
|
666
799
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
667
800
|
}
|
|
668
|
-
throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
|
|
801
|
+
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);
|
|
802
|
+
}
|
|
803
|
+
if (family === "stage" && command === "native") {
|
|
804
|
+
if (requiredPosition(parsed, 0, "native action") !== "observe")
|
|
805
|
+
throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
|
|
806
|
+
return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
|
|
669
807
|
}
|
|
670
808
|
if (family === "run" && command === "capacity") {
|
|
671
809
|
if (requiredPosition(parsed, 0, "capacity action") !== "record")
|
|
@@ -752,21 +890,34 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
752
890
|
}
|
|
753
891
|
if (family === "work" && command === "delete")
|
|
754
892
|
return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
|
|
893
|
+
if (family === "work" && command === "launch")
|
|
894
|
+
return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
|
|
755
895
|
if (family === "work" && command === "start") {
|
|
756
896
|
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
897
|
+
const recoveryId = optionalOption(parsed, "recovery-id");
|
|
757
898
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
|
|
758
|
-
return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
|
|
899
|
+
return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
|
|
759
900
|
}
|
|
760
901
|
if (family === "work" && command === "finish") {
|
|
761
902
|
const resultFile = optionalOption(parsed, "result-file");
|
|
762
903
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
763
904
|
if (Boolean(resultFile) === resultStdin)
|
|
764
905
|
throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
|
|
906
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
907
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
|
|
908
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
909
|
+
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = optionalOption(parsed, "reason");
|
|
910
|
+
if (Boolean(retryReceiptId) !== Boolean(retryReason))
|
|
911
|
+
throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
|
|
765
912
|
const result = resultFile ? fs.readFileSync(path.resolve(resultFile), "utf8") : await readStdin(io.stdin);
|
|
766
|
-
return await finishWork(context,
|
|
913
|
+
return await finishWork(context, workId, result, commandProgress(io, output), retryReceiptId ? { receiptId: retryReceiptId, reason: retryReason } : undefined);
|
|
914
|
+
}
|
|
915
|
+
if (family === "work" && command === "fail") {
|
|
916
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
917
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
|
|
918
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
919
|
+
return failWork(context, workId, requiredOption(parsed, "reason"));
|
|
767
920
|
}
|
|
768
|
-
if (family === "work" && command === "fail")
|
|
769
|
-
return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
770
921
|
if (family === "work" && command === "cancel")
|
|
771
922
|
return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
772
923
|
if (family === "work" && command === "retry")
|
|
@@ -874,9 +1025,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
874
1025
|
const projectRoot = requiredOption(parsed, "project-root");
|
|
875
1026
|
const runId = requiredPosition(parsed, 0, "run-id");
|
|
876
1027
|
const stage = requiredOption(parsed, "stage");
|
|
1028
|
+
const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
|
|
877
1029
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
878
1030
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
879
1031
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1032
|
+
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
1033
|
+
const retryReason = optionalOption(parsed, "reason");
|
|
1034
|
+
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
1035
|
+
if (retryCheckId && !retryReason?.trim())
|
|
1036
|
+
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
1037
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
1038
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
1039
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1040
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
1041
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1042
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
1043
|
+
if (protectedLifecycle)
|
|
1044
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
|
|
880
1045
|
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
881
1046
|
const outcome = optionalOption(parsed, "outcome");
|
|
882
1047
|
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
|
|
@@ -888,65 +1053,77 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
888
1053
|
if (!resultFile && !resultStdin) {
|
|
889
1054
|
throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
|
|
890
1055
|
}
|
|
891
|
-
return finishVnextSpecifyStage(context, {
|
|
1056
|
+
return controlledReply(await finishVnextSpecifyStage(context, {
|
|
892
1057
|
projectRoot,
|
|
893
1058
|
runId,
|
|
894
1059
|
...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
|
|
895
1060
|
outcome: outcome
|
|
896
|
-
});
|
|
1061
|
+
}));
|
|
897
1062
|
}
|
|
898
1063
|
if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
|
|
899
1064
|
if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
900
1065
|
throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
|
|
901
1066
|
}
|
|
902
|
-
return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
|
|
1067
|
+
return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile }));
|
|
903
1068
|
}
|
|
904
1069
|
if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
905
1070
|
if (optionalOption(parsed, "code-work-batch-file"))
|
|
906
1071
|
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 });
|
|
1072
|
+
return controlledReply(await finishVnextPlan(context, { projectRoot, runId }));
|
|
908
1073
|
}
|
|
909
1074
|
if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
|
|
910
1075
|
if (resultStdin || hasOption(parsed, "outcome"))
|
|
911
1076
|
throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
|
|
912
1077
|
const decisionFile = optionalOption(parsed, "decision-file");
|
|
913
|
-
return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
|
|
1078
|
+
return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) }));
|
|
914
1079
|
}
|
|
915
1080
|
if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
|
|
916
1081
|
if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
917
1082
|
throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
|
|
918
1083
|
}
|
|
919
|
-
return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
|
|
1084
|
+
return controlledReply(await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
|
|
920
1085
|
}
|
|
921
1086
|
if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
|
|
922
1087
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
923
1088
|
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) });
|
|
1089
|
+
return controlledReply(await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
|
|
925
1090
|
}
|
|
926
1091
|
if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
|
|
927
1092
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
928
1093
|
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) });
|
|
1094
|
+
return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
|
|
930
1095
|
}
|
|
931
1096
|
if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
|
|
932
1097
|
throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
|
|
933
1098
|
}
|
|
934
|
-
return finishStage(context, {
|
|
1099
|
+
return controlledReply(await finishStage(context, {
|
|
935
1100
|
projectRoot,
|
|
936
1101
|
runId,
|
|
937
1102
|
stage,
|
|
938
1103
|
...(dir ? { dir } : {}),
|
|
939
1104
|
...(semanticFile ? { semanticFile } : {})
|
|
940
|
-
});
|
|
1105
|
+
}));
|
|
941
1106
|
}
|
|
942
1107
|
if (family === "stage" && command === "pause") {
|
|
943
1108
|
if (!hasOption(parsed, "question-stdin"))
|
|
944
1109
|
throw new AppError("usage", "stage pause requires --question-stdin", 2);
|
|
1110
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1111
|
+
const runId = requiredPosition(parsed, 0, "run-id");
|
|
1112
|
+
const stage = requiredOption(parsed, "stage");
|
|
1113
|
+
const workId = requiredOption(parsed, "work");
|
|
1114
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
1115
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
1116
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1117
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
1118
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
1119
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
1120
|
+
if (protectedLifecycle)
|
|
1121
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
945
1122
|
return pauseStageForUser(context, {
|
|
946
|
-
projectRoot
|
|
947
|
-
runId
|
|
948
|
-
stage
|
|
949
|
-
workId
|
|
1123
|
+
projectRoot,
|
|
1124
|
+
runId,
|
|
1125
|
+
stage,
|
|
1126
|
+
workId,
|
|
950
1127
|
question: await readStdin(io.stdin)
|
|
951
1128
|
});
|
|
952
1129
|
}
|
|
@@ -1007,6 +1184,48 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1007
1184
|
return dispatchSession(context, command, parsed);
|
|
1008
1185
|
}
|
|
1009
1186
|
if (family === "run") {
|
|
1187
|
+
if (command === "control") {
|
|
1188
|
+
const action = requiredPosition(parsed, 0, "control action");
|
|
1189
|
+
const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
|
|
1190
|
+
if (action === "status")
|
|
1191
|
+
return runControlStatus(context, scope);
|
|
1192
|
+
if (action === "reconcile") {
|
|
1193
|
+
if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1194
|
+
throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
|
|
1195
|
+
return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
|
|
1196
|
+
}
|
|
1197
|
+
if (action === "resume") {
|
|
1198
|
+
if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
|
|
1199
|
+
throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
|
|
1200
|
+
return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
|
|
1201
|
+
}
|
|
1202
|
+
if (action === "serve")
|
|
1203
|
+
return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
|
|
1204
|
+
if (action === "pause" || action === "stop")
|
|
1205
|
+
return await requestRunControl(context, {
|
|
1206
|
+
...scope, action, requestId: requiredOption(parsed, "request-id"),
|
|
1207
|
+
...(hasOption(parsed, "force") ? { force: true } : {}),
|
|
1208
|
+
...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
|
|
1209
|
+
...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
|
|
1210
|
+
...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
|
|
1211
|
+
});
|
|
1212
|
+
throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
|
|
1213
|
+
}
|
|
1214
|
+
if (command === "drive") {
|
|
1215
|
+
const action = requiredPosition(parsed, 0, "drive action");
|
|
1216
|
+
const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
|
|
1217
|
+
if (action === "status")
|
|
1218
|
+
return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
|
|
1219
|
+
if (action === "launch")
|
|
1220
|
+
return await launchRunController(context, { ...scope, 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") } : {}) });
|
|
1221
|
+
if (action === "serve")
|
|
1222
|
+
return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
|
|
1223
|
+
if (action === "answer")
|
|
1224
|
+
return answerRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), pauseId: requiredOption(parsed, "pause-id"), answerFile: requiredOption(parsed, "answer-file") });
|
|
1225
|
+
if (action === "context")
|
|
1226
|
+
return supplyRunControllerContext(context, { ...scope, controllerId: requiredOption(parsed, "controller-id"), requestId: requiredOption(parsed, "request-id"), stage: requiredOption(parsed, "stage"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256") });
|
|
1227
|
+
throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
|
|
1228
|
+
}
|
|
1010
1229
|
return dispatchRun(context, command, parsed);
|
|
1011
1230
|
}
|
|
1012
1231
|
if (family === "stat") {
|
|
@@ -1039,6 +1258,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1039
1258
|
if (family === "codex" && command === "hook") {
|
|
1040
1259
|
return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
|
|
1041
1260
|
}
|
|
1261
|
+
if (family === "zcode" && command === "hook") {
|
|
1262
|
+
return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
|
|
1263
|
+
}
|
|
1042
1264
|
if (family === "zcode" && command === "event") {
|
|
1043
1265
|
return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
|
|
1044
1266
|
}
|
|
@@ -1063,6 +1285,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1063
1285
|
throw new AppError("usage", "Unknown opencode usage action", 2);
|
|
1064
1286
|
return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1065
1287
|
}
|
|
1288
|
+
if (family === "droid" && command === "event") {
|
|
1289
|
+
if (requiredPosition(parsed, 0, "droid event action") !== "handle")
|
|
1290
|
+
throw new AppError("usage", "Unknown droid event action", 2);
|
|
1291
|
+
return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1292
|
+
}
|
|
1293
|
+
if (family === "droid" && command === "usage") {
|
|
1294
|
+
if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
|
|
1295
|
+
throw new AppError("usage", "Unknown droid usage action", 2);
|
|
1296
|
+
return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1297
|
+
}
|
|
1066
1298
|
if (family === "agy" && command === "event")
|
|
1067
1299
|
return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
|
|
1068
1300
|
if (family === "agy" && command === "usage") {
|
|
@@ -1247,6 +1479,8 @@ async function dispatchMerge(context, command, parsed, progress) {
|
|
|
1247
1479
|
}
|
|
1248
1480
|
if (command === "apply")
|
|
1249
1481
|
return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
|
|
1482
|
+
if (command === "repair")
|
|
1483
|
+
return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
|
|
1250
1484
|
if (command === "request") {
|
|
1251
1485
|
const action = requiredPosition(parsed, 0, "request action");
|
|
1252
1486
|
const requestId = requiredPosition(parsed, 1, "merge-request-id");
|
|
@@ -1410,6 +1644,24 @@ function dispatchSession(context, command, parsed) {
|
|
|
1410
1644
|
throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
|
|
1411
1645
|
}
|
|
1412
1646
|
function dispatchRun(context, command, parsed) {
|
|
1647
|
+
if (command === "recovery") {
|
|
1648
|
+
const action = requiredPosition(parsed, 0, "recovery action");
|
|
1649
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1650
|
+
const runId = requiredPosition(parsed, 1, "run-id");
|
|
1651
|
+
if (action === "inspect")
|
|
1652
|
+
return inspectRunRecovery(context, { projectRoot, runId });
|
|
1653
|
+
if (action === "begin")
|
|
1654
|
+
return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") });
|
|
1655
|
+
if (action === "seal")
|
|
1656
|
+
return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") });
|
|
1657
|
+
if (action === "capture")
|
|
1658
|
+
return recordRecoveryCapture(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") });
|
|
1659
|
+
if (action === "resume")
|
|
1660
|
+
return resumeRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), daemonId: requiredOption(parsed, "daemon-id"), harness: requiredOption(parsed, "harness"), nativeSessionId: requiredOption(parsed, "native-session-id") });
|
|
1661
|
+
if (action === "accept")
|
|
1662
|
+
return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
1663
|
+
throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
|
|
1664
|
+
}
|
|
1413
1665
|
if (command === "snapshot") {
|
|
1414
1666
|
const action = requiredPosition(parsed, 0, "snapshot action");
|
|
1415
1667
|
if (action === "bootstrap") {
|
|
@@ -1428,16 +1680,20 @@ function dispatchRun(context, command, parsed) {
|
|
|
1428
1680
|
}
|
|
1429
1681
|
if (action === "create") {
|
|
1430
1682
|
const candidate = parsed.options.has("candidate");
|
|
1683
|
+
const incomplete = parsed.options.has("incomplete");
|
|
1684
|
+
const recoveryId = optionalOption(parsed, "recovery-id");
|
|
1431
1685
|
const stageEntry = optionalOption(parsed, "stage-entry");
|
|
1432
|
-
if (candidate
|
|
1433
|
-
throw new AppError("usage", "Use
|
|
1434
|
-
if (!candidate && stageEntry === undefined)
|
|
1435
|
-
throw new AppError("usage", "Snapshot create requires --candidate or --stage-entry", 2);
|
|
1686
|
+
if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) + Number(recoveryId !== undefined) !== 1)
|
|
1687
|
+
throw new AppError("usage", "Use exactly one of --candidate, --incomplete, --recovery-id or --stage-entry", 2);
|
|
1436
1688
|
const input = {
|
|
1437
1689
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
1438
1690
|
runId: requiredPosition(parsed, 1, "run-id"),
|
|
1439
1691
|
output: requiredOption(parsed, "output")
|
|
1440
1692
|
};
|
|
1693
|
+
if (incomplete)
|
|
1694
|
+
return createEvalRunSnapshot(context, { ...input, incomplete: true });
|
|
1695
|
+
if (recoveryId)
|
|
1696
|
+
return createEvalRunSnapshot(context, { ...input, recoveryId });
|
|
1441
1697
|
return candidate
|
|
1442
1698
|
? createEvalRunSnapshot(context, { ...input, candidate: true })
|
|
1443
1699
|
: createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
|
|
@@ -1445,12 +1701,23 @@ function dispatchRun(context, command, parsed) {
|
|
|
1445
1701
|
if (action === "restore")
|
|
1446
1702
|
return restoreEvalRunSnapshot(context, {
|
|
1447
1703
|
snapshot: requiredOption(parsed, "snapshot"),
|
|
1704
|
+
executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
|
|
1705
|
+
...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}),
|
|
1448
1706
|
projectRoot: requiredOption(parsed, "project-root")
|
|
1449
1707
|
});
|
|
1450
1708
|
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
1709
|
}
|
|
1710
|
+
if (command === "fork") {
|
|
1711
|
+
return forkRunFromSnapshot(context, {
|
|
1712
|
+
snapshot: requiredOption(parsed, "from"),
|
|
1713
|
+
output: requiredOption(parsed, "output"),
|
|
1714
|
+
engineVersion: requiredOption(parsed, "engine-version"),
|
|
1715
|
+
requestId: requiredOption(parsed, "request-id"),
|
|
1716
|
+
...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {})
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1452
1719
|
if (command === "prepare-vnext-specify") {
|
|
1453
|
-
return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
|
|
1720
|
+
return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
|
|
1454
1721
|
}
|
|
1455
1722
|
if (command === "start") {
|
|
1456
1723
|
if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
|
|
@@ -1467,6 +1734,7 @@ function dispatchRun(context, command, parsed) {
|
|
|
1467
1734
|
nextAction: optionalOption(parsed, "next-action"),
|
|
1468
1735
|
preset: optionalOption(parsed, "preset"),
|
|
1469
1736
|
taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
|
|
1737
|
+
executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
|
|
1470
1738
|
...(protocolOverrides ? { protocolOverrides } : {}),
|
|
1471
1739
|
runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
|
|
1472
1740
|
});
|
|
@@ -1581,30 +1849,113 @@ function dispatchRun(context, command, parsed) {
|
|
|
1581
1849
|
}
|
|
1582
1850
|
throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
|
|
1583
1851
|
}
|
|
1584
|
-
async function dispatchRuntime(context, command, parsed) {
|
|
1852
|
+
async function dispatchRuntime(context, command, parsed, progress) {
|
|
1853
|
+
if (command === "scope") {
|
|
1854
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
|
|
1855
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1856
|
+
throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
|
|
1857
|
+
return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
|
|
1858
|
+
}
|
|
1859
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
|
|
1860
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1861
|
+
throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
|
|
1862
|
+
return { ok: true, ...await launchRuntimeScopeResume(context, { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") }) };
|
|
1863
|
+
}
|
|
1864
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
|
|
1865
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1866
|
+
throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
|
|
1867
|
+
const mode = requiredOption(parsed, "mode");
|
|
1868
|
+
if (mode !== "pause" && mode !== "stop")
|
|
1869
|
+
throw new AppError("usage", "Scope control mode must be pause or stop", 2);
|
|
1870
|
+
return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, hasOption(parsed, "manifest-json") ? JSON.parse(requiredOption(parsed, "manifest-json")) : undefined) };
|
|
1871
|
+
}
|
|
1872
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
|
|
1873
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1874
|
+
throw new AppError("usage", "Invalid internal scope worker arguments", 2);
|
|
1875
|
+
return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
|
|
1876
|
+
}
|
|
1877
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
|
|
1878
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1879
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
|
|
1880
|
+
return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
|
|
1881
|
+
}
|
|
1882
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
|
|
1883
|
+
if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1884
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
|
|
1885
|
+
return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
|
|
1886
|
+
}
|
|
1887
|
+
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))
|
|
1888
|
+
throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
|
|
1889
|
+
return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
|
|
1890
|
+
}
|
|
1585
1891
|
if (command !== "process")
|
|
1586
|
-
throw new AppError("usage", "Usage: dd-flow runtime process
|
|
1892
|
+
throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
|
|
1587
1893
|
const action = requiredPosition(parsed, 0, "runtime process action");
|
|
1894
|
+
if (action === "check-admission")
|
|
1895
|
+
return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
|
|
1896
|
+
if (action === "start")
|
|
1897
|
+
return await startRuntimeService(context, { 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, progress });
|
|
1898
|
+
if (action === "stop")
|
|
1899
|
+
return { ok: true, process: await stopManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(optionalPositiveNumber(parsed, "grace-ms", true) ? { graceMs: optionalPositiveNumber(parsed, "grace-ms", true) } : {}) }) };
|
|
1588
1900
|
if (action === "register") {
|
|
1589
1901
|
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
1902
|
+
const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
|
|
1903
|
+
const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
|
|
1904
|
+
const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
|
|
1905
|
+
const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
|
|
1590
1906
|
const process = registerManagedProcess(context, {
|
|
1591
1907
|
kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
|
|
1592
|
-
|
|
1908
|
+
uniqueActiveOperation: requiredOption(parsed, "kind") === "eval-observer",
|
|
1909
|
+
...(requiredOption(parsed, "kind") === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
|
|
1910
|
+
ownerPid: optionalPositiveNumber(parsed, "owner-pid", true),
|
|
1911
|
+
projectId, runId,
|
|
1593
1912
|
workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
|
|
1594
1913
|
operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
|
|
1595
|
-
stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
|
|
1914
|
+
stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {}),
|
|
1915
|
+
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") } : {}) }
|
|
1596
1916
|
});
|
|
1597
1917
|
return { ok: true, process };
|
|
1598
1918
|
}
|
|
1599
1919
|
if (action === "confirm") {
|
|
1600
1920
|
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
1601
1921
|
const processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
|
|
1602
|
-
return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
|
|
1922
|
+
return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
|
|
1603
1923
|
}
|
|
1604
1924
|
if (action === "heartbeat") {
|
|
1605
1925
|
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
1606
1926
|
return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
|
|
1607
1927
|
}
|
|
1928
|
+
if (action === "admission") {
|
|
1929
|
+
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
1930
|
+
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))
|
|
1931
|
+
throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
|
|
1932
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
1933
|
+
if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
|
|
1934
|
+
throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
|
|
1935
|
+
if (record.project_id && record.run_id) {
|
|
1936
|
+
if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
1937
|
+
throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
|
|
1938
|
+
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") });
|
|
1939
|
+
if (!recovering) {
|
|
1940
|
+
if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
|
|
1941
|
+
throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
|
|
1942
|
+
assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
|
|
1943
|
+
}
|
|
1944
|
+
const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
|
|
1945
|
+
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")]);
|
|
1946
|
+
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") });
|
|
1947
|
+
if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
|
|
1948
|
+
throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
|
|
1949
|
+
}
|
|
1950
|
+
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") });
|
|
1951
|
+
return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
|
|
1952
|
+
}
|
|
1953
|
+
if (action === "release-turn") {
|
|
1954
|
+
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
1955
|
+
if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
1956
|
+
throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
|
|
1957
|
+
return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
|
|
1958
|
+
}
|
|
1608
1959
|
if (action === "finish") {
|
|
1609
1960
|
const state = requiredOption(parsed, "state");
|
|
1610
1961
|
if (state !== "stopped" && state !== "failed")
|
|
@@ -1615,7 +1966,7 @@ async function dispatchRuntime(context, command, parsed) {
|
|
|
1615
1966
|
return { ok: true, processes: managedProcessStatus(context) };
|
|
1616
1967
|
if (action === "reconcile")
|
|
1617
1968
|
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalPositiveNumber(parsed, "grace-ms", true)) };
|
|
1618
|
-
throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
1969
|
+
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
|
|
1619
1970
|
}
|
|
1620
1971
|
function dispatchStat(context, command, parsed) {
|
|
1621
1972
|
if (command === "usage") {
|
|
@@ -1797,16 +2148,37 @@ function dispatchSchema(context, command, parsed) {
|
|
|
1797
2148
|
if (command === "validate") {
|
|
1798
2149
|
const schemaDir = optionalOption(parsed, "schema-dir");
|
|
1799
2150
|
const projectRoot = optionalOption(parsed, "project-root");
|
|
2151
|
+
const file = requiredOption(parsed, "file");
|
|
2152
|
+
// A generic validation command can still bind to the exact RUN without
|
|
2153
|
+
// searching project directories: run_id is a structural field in the
|
|
2154
|
+
// submitted artifact and the database owns its materialized run_root.
|
|
2155
|
+
const explicitRunId = optionalOption(parsed, "run");
|
|
2156
|
+
const declaredRunId = explicitRunId ?? readRunId(file);
|
|
2157
|
+
const fileRunRoot = path.basename(file) === "run.json" ? path.dirname(path.resolve(file)) : undefined;
|
|
2158
|
+
const bound = projectRoot
|
|
2159
|
+
? 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 ?? ""])
|
|
2160
|
+
: undefined;
|
|
1800
2161
|
return validateSchema({
|
|
1801
2162
|
schemaName: requiredOption(parsed, "schema"),
|
|
1802
|
-
file
|
|
2163
|
+
file,
|
|
1803
2164
|
...(schemaDir ? { schemaDir } : {}),
|
|
1804
2165
|
...(projectRoot ? { projectRoot } : {}),
|
|
2166
|
+
...(declaredRunId || bound?.id ? { runId: declaredRunId ?? bound.id } : {}),
|
|
2167
|
+
...(bound?.run_root ? { runRoot: bound.run_root } : {}),
|
|
1805
2168
|
ddFlowHome: context.ddFlowHome
|
|
1806
2169
|
});
|
|
1807
2170
|
}
|
|
1808
2171
|
throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
|
|
1809
2172
|
}
|
|
2173
|
+
function readRunId(file) {
|
|
2174
|
+
try {
|
|
2175
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2176
|
+
return typeof value.run_id === "string" && value.run_id.trim() ? value.run_id : undefined;
|
|
2177
|
+
}
|
|
2178
|
+
catch {
|
|
2179
|
+
return undefined;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
1810
2182
|
async function dispatchLane(context, command, parsed) {
|
|
1811
2183
|
if (command === "status") {
|
|
1812
2184
|
return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
|
|
@@ -2006,6 +2378,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
|
|
|
2006
2378
|
}
|
|
2007
2379
|
throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
|
|
2008
2380
|
}
|
|
2381
|
+
function dispatchZcodeHook(context, parsed, stdin) {
|
|
2382
|
+
if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
|
|
2383
|
+
throw new AppError("usage", "Unknown zcode hook action", 2);
|
|
2384
|
+
const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
|
|
2385
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
2386
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
2387
|
+
}
|
|
2388
|
+
return {};
|
|
2389
|
+
}
|
|
2009
2390
|
function dispatchGrokEvent(context, parsed, stdin) {
|
|
2010
2391
|
const action = requiredPosition(parsed, 0, "grok event action");
|
|
2011
2392
|
if (action === "handle") {
|
|
@@ -2128,6 +2509,21 @@ function parseFlowFlagOverrides(values) {
|
|
|
2128
2509
|
function parseArgs(args) {
|
|
2129
2510
|
return parseCommandArgs(args);
|
|
2130
2511
|
}
|
|
2512
|
+
/** Most CLI options name one authority, path or target. Silently taking the
|
|
2513
|
+
* last duplicate can widen a mutation after an operator typo. Keep only the
|
|
2514
|
+
* options consumed as intentional arrays repeatable. */
|
|
2515
|
+
function assertSingletonOptions(args) {
|
|
2516
|
+
const repeatable = new Set(["alias", "flag", "hide", "check-ref", "evidence", "from-check", "from-finding", "from-unresolved", "on", "origin-work"]);
|
|
2517
|
+
const parsed = parseCommandArgs(args);
|
|
2518
|
+
if (args[0] === "stage" && args[1] === "finish"
|
|
2519
|
+
&& ["result-file", "semantic-file", "data", "result-stdin"].filter(name => parsed.options.has(name)).length > 1) {
|
|
2520
|
+
throw new AppError("usage", "Stage finish requires exactly one result input; result-file, semantic-file, data and result-stdin are mutually exclusive", 2);
|
|
2521
|
+
}
|
|
2522
|
+
for (const [name, values] of parsed.options) {
|
|
2523
|
+
if (!repeatable.has(name) && values.length > 1)
|
|
2524
|
+
throw new AppError("usage", `--${name} may be supplied only once`, 2);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2131
2527
|
function requiredPosition(parsed, index, label) {
|
|
2132
2528
|
const value = parsed.positional[index];
|
|
2133
2529
|
if (!value) {
|
|
@@ -2303,7 +2699,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2303
2699
|
if (family === "merge-queue") {
|
|
2304
2700
|
return projectRootForMergeQueueMutation(context, command, parsed);
|
|
2305
2701
|
}
|
|
2306
|
-
if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
|
|
2702
|
+
if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
|
|
2307
2703
|
return requiredOption(parsed, "project-root");
|
|
2308
2704
|
}
|
|
2309
2705
|
if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
|
|
@@ -2336,7 +2732,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2336
2732
|
if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
|
|
2337
2733
|
return requiredOption(parsed, "project-root");
|
|
2338
2734
|
}
|
|
2339
|
-
if (family
|
|
2735
|
+
if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
|
|
2340
2736
|
return requiredOption(parsed, "project-root");
|
|
2341
2737
|
if (family === "codex" && command === "hooks") {
|
|
2342
2738
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|