@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
3
|
+
import { getFlowRunVariables } from "./runs.js";
|
|
4
|
+
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
|
+
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
|
+
/** Stage entry acknowledges the packet before the controller issues child commands. */
|
|
7
|
+
export function controllerStageEntryPrompt(stage, command, responseFile) {
|
|
8
|
+
return [
|
|
9
|
+
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
10
|
+
command,
|
|
11
|
+
`Read the complete authoritative packet saved to ${JSON.stringify(responseFile)}, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.`,
|
|
12
|
+
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
13
|
+
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
14
|
+
"Copy runtime-issued lifecycle commands verbatim as standalone calls. Never generate an invocation ID, substitute a shell variable for it, or borrow one from another command. If a required command is missing or rejected without a returned retry command, report the exact error and end the Turn so the controller can handle it."
|
|
15
|
+
].join("\n\n");
|
|
16
|
+
}
|
|
17
|
+
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
18
|
+
export async function nextControllerFanout(context, input) {
|
|
19
|
+
const children = input.children.filter(child => child.status !== "settled_by_root");
|
|
20
|
+
const observations = { harness_id: input.harness, parent_session_id: input.sessionId, children };
|
|
21
|
+
if (children.length) {
|
|
22
|
+
const observed = observeVnextNativeChildren(context, { ...input, observations });
|
|
23
|
+
const issues = observed.issues;
|
|
24
|
+
if (issues.length)
|
|
25
|
+
throw new AppError("fanout_reconciliation_required", "Native children have unresolved lifecycle ownership", 1, { issues });
|
|
26
|
+
}
|
|
27
|
+
let fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
28
|
+
if (!fanout)
|
|
29
|
+
return { kind: "none" };
|
|
30
|
+
if (children.length) {
|
|
31
|
+
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
32
|
+
const issues = reconciled.issues.filter(issue => issue.code !== "child_check_in_progress");
|
|
33
|
+
if (issues.length)
|
|
34
|
+
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
35
|
+
if (children.some(child => child.status === "running" || child.status === "unknown"))
|
|
36
|
+
return { kind: "wait", reason: "native_children_unsettled" };
|
|
37
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
38
|
+
}
|
|
39
|
+
if (fanout.state === "dispatch_required") {
|
|
40
|
+
dispatchVnextPlanReview(context, input);
|
|
41
|
+
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
42
|
+
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
43
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
44
|
+
}
|
|
45
|
+
const prompt = (text) => ({ kind: "prompt", prompt: text, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, works: fanout.works, text }) });
|
|
46
|
+
if (fanout.state === "coordinator_required")
|
|
47
|
+
return prompt(`Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
48
|
+
if (fanout.works.failed || fanout.works.cancelled)
|
|
49
|
+
return prompt(`Some ${input.stage} child Work failed or was cancelled. Inspect its recorded receipts. Choose a legal scoped repair/retry or block the Stage with the concrete reason. Do not relaunch settled siblings or infer success from a child that omitted work finish.`);
|
|
50
|
+
if (fanout.works.ready.length) {
|
|
51
|
+
const choices = fanout.works.ready.map(work => {
|
|
52
|
+
const payload = context.db.get("SELECT payload_json FROM works WHERE work_id = ?", [work.work_id])?.payload_json;
|
|
53
|
+
const override = payload ? JSON.parse(payload).execution_override : undefined;
|
|
54
|
+
return { work, resolved: resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: input.policy, profiles: input.profiles, stage: input.stage, ...(override ? { work: override } : {}) }) };
|
|
55
|
+
});
|
|
56
|
+
const external = choices.filter(choice => choice.resolved.delegation.mode === "external");
|
|
57
|
+
if (external.length) {
|
|
58
|
+
const maximum = Math.min(...external.map(choice => choice.resolved.delegation.max_parallel ?? 1));
|
|
59
|
+
return { kind: "external", works: external.slice(0, maximum).map(choice => choice.work) };
|
|
60
|
+
}
|
|
61
|
+
const variables = getFlowRunVariables(context, input);
|
|
62
|
+
const capacity = variables.variables[subagentCapacityKey];
|
|
63
|
+
if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1)
|
|
64
|
+
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
65
|
+
const wave = choices.slice(0, capacity).map(choice => choice.work);
|
|
66
|
+
return prompt([
|
|
67
|
+
`Launch exactly ${wave.length} direct native children for the current ${input.stage} Stage; the recorded capacity is ${capacity}.`,
|
|
68
|
+
"Use this harness's native child mechanism, not external roots or forks. Wait for these children to settle; do not cancel siblings because one failed. Do not finish this Stage or start a successor in this Turn.",
|
|
69
|
+
...wave.map(work => {
|
|
70
|
+
if (!work.work_id || !work.start_command)
|
|
71
|
+
throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
|
|
72
|
+
return `Assign one leaf child Work ${work.work_id}. ${work.launch_policy === "fresh_agent_required" ? "Use empty, non-inherited context when supported; a new Session id alone does not prove empty context." : "Inherit context only through the harness's declared mechanism."}\nIts first technical action must be this exact standalone command:\n${work.start_command}\nUse only the Work packet returned by it. Complete its declared result and exact work finish command, then stop. Do not create children or start another Work. Missing facts go through the Work's failure/result contract; only the coordinator handles Stage HITL.`;
|
|
73
|
+
})
|
|
74
|
+
].join("\n\n"));
|
|
75
|
+
}
|
|
76
|
+
if (fanout.works.running)
|
|
77
|
+
return { kind: "wait", reason: "work_receipts_pending" };
|
|
78
|
+
if (fanout.works.created)
|
|
79
|
+
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
80
|
+
return prompt(`All declared ${input.stage} child Work has settled (${fanout.works.completed} completed). Read the accepted result receipts and continue this Stage's semantic decision under its original packet. Execute an exact repair command returned by a rejected stage finish; a rejection alone does not create repair Work. Return after finish or after declaring a repair graph. Do not start a successor Stage.`);
|
|
81
|
+
}
|
|
82
|
+
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
83
|
+
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
84
|
+
const candidates = [];
|
|
85
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
86
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
87
|
+
const text = (value, keys) => { for (const key of keys)
|
|
88
|
+
if (typeof value[key] === "string" && value[key])
|
|
89
|
+
return value[key]; return null; };
|
|
90
|
+
const settledTree = new Set(list(receipt.tree_observations)
|
|
91
|
+
.filter(value => object(value).settled === true)
|
|
92
|
+
.map(value => text(object(value), ["provider_session_id", "session_id"]))
|
|
93
|
+
.filter((id) => id !== null));
|
|
94
|
+
const add = (raw, source, scopedParent = null, fallback = "unknown") => {
|
|
95
|
+
const value = object(raw);
|
|
96
|
+
const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
|
|
97
|
+
const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
|
|
98
|
+
if (!id || parent !== rootSessionId)
|
|
99
|
+
return;
|
|
100
|
+
const state = String(value.status ?? value.state ?? fallback).toLowerCase();
|
|
101
|
+
const status = ["completed", "complete", "done", "idle", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
|
|
102
|
+
candidates.push({ session_id: id, parent_session_id: parent, status: status === "unknown" && settledTree.has(id) ? "settled_by_root" : status, source });
|
|
103
|
+
};
|
|
104
|
+
for (const value of list(receipt.descendants))
|
|
105
|
+
add(value, "adapter.descendants");
|
|
106
|
+
for (const value of list(receipt.children))
|
|
107
|
+
add(value, "opencode.session.children");
|
|
108
|
+
const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
|
|
109
|
+
for (const value of list(subagents.running))
|
|
110
|
+
add(value, "zcode/session/subagents", rootSessionId, "running");
|
|
111
|
+
for (const value of [...list(subagents.completed), ...list(object(subagents.ended).items)])
|
|
112
|
+
add(value, "zcode/session/subagents", rootSessionId, "completed");
|
|
113
|
+
const unique = new Map();
|
|
114
|
+
for (const child of candidates) {
|
|
115
|
+
const previous = unique.get(child.session_id);
|
|
116
|
+
if (!previous || previous.status === "unknown" || previous.status === "running" && child.status !== "running")
|
|
117
|
+
unique.set(child.session_id, child);
|
|
118
|
+
}
|
|
119
|
+
return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
|
|
120
|
+
}
|
|
@@ -19,6 +19,8 @@ export function getCmuxStatus(context, input) {
|
|
|
19
19
|
}
|
|
20
20
|
export function renderDashboard(context, input) {
|
|
21
21
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
22
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
23
|
+
throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
|
|
22
24
|
const format = parseDashboardFormat(input.format);
|
|
23
25
|
if (format === "html") {
|
|
24
26
|
if (input.protocol) {
|
|
@@ -170,6 +172,10 @@ export function refreshAllDashboards(context, input = {}) {
|
|
|
170
172
|
}
|
|
171
173
|
export function autoRefreshDashboards(context, input) {
|
|
172
174
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
175
|
+
// Hooks and usage ingestion continue to record receipts after capture.
|
|
176
|
+
// Their derived timestamp must not mutate the captured workspace.
|
|
177
|
+
if (projectRecoveryGuarded(context, project.id))
|
|
178
|
+
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
173
179
|
const config = readProjectConfig(context, project.id);
|
|
174
180
|
if (!config.dashboard.auto_refresh) {
|
|
175
181
|
return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
|
|
@@ -196,6 +202,9 @@ export function autoRefreshDashboards(context, input) {
|
|
|
196
202
|
}
|
|
197
203
|
return result;
|
|
198
204
|
}
|
|
205
|
+
function projectRecoveryGuarded(context, projectId) {
|
|
206
|
+
return Boolean(context.db.get("SELECT 1 FROM run_recovery_guards WHERE project_id = ? AND status IN ('draining', 'sealed', 'resuming') LIMIT 1", [projectId]));
|
|
207
|
+
}
|
|
199
208
|
export function dashboardSummary(context, projectId, projectRoot) {
|
|
200
209
|
const config = readProjectConfig(context, projectId);
|
|
201
210
|
return {
|
package/dist/services/engines.js
CHANGED
|
@@ -12,7 +12,7 @@ import { requireProtocol } from "./protocols.js";
|
|
|
12
12
|
import { resolveCanonRootBeforeContext } from "./canon.js";
|
|
13
13
|
import { appendAudit } from "./audit.js";
|
|
14
14
|
import { findProjectByRoot } from "./projects.js";
|
|
15
|
-
import { allRunEngineBindings,
|
|
15
|
+
import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
|
|
16
16
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
17
17
|
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
18
18
|
const sourceFile = fileURLToPath(import.meta.url);
|
|
@@ -117,7 +117,7 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
export function listEngines(context) {
|
|
119
119
|
const root = engineStoreRoot(context.ddFlowHome);
|
|
120
|
-
const engines = readInstalledManifests(root).map(manifestSummary);
|
|
120
|
+
const engines = readInstalledManifests(root).map(manifest => manifestSummary(manifest));
|
|
121
121
|
return { ok: true, schema_id: "dd-flow/engine-list@1", engine_store: root, engines };
|
|
122
122
|
}
|
|
123
123
|
export function engineInfo(context, input) {
|
|
@@ -138,11 +138,10 @@ export function resolveEngine(context, input = {}) {
|
|
|
138
138
|
export function doctorEngines(context, input = {}) {
|
|
139
139
|
const root = engineStoreRoot(context.ddFlowHome);
|
|
140
140
|
const installed = readInstalledManifests(root);
|
|
141
|
-
const checks = installed.map(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}));
|
|
141
|
+
const checks = installed.map(manifest => {
|
|
142
|
+
const diagnostics = manifestDiagnostics(manifest);
|
|
143
|
+
return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.length ? "corrupt" : "ok", diagnostics };
|
|
144
|
+
});
|
|
146
145
|
const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
|
|
147
146
|
const selection = selectEngine(context, input, {}, classification);
|
|
148
147
|
return {
|
|
@@ -174,7 +173,9 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
|
|
|
174
173
|
if (!selection.selected)
|
|
175
174
|
return Promise.resolve(null);
|
|
176
175
|
const current = getCliBuildInfo();
|
|
177
|
-
|
|
176
|
+
const currentRoot = findPackageRoot(path.dirname(sourceFile));
|
|
177
|
+
if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version
|
|
178
|
+
&& currentRoot !== null && fs.realpathSync(selection.selected.snapshot_root) === fs.realpathSync(currentRoot)) {
|
|
178
179
|
return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
|
|
179
180
|
}
|
|
180
181
|
const entrypoint = resolveManifestEntrypoint(selection.selected);
|
|
@@ -182,7 +183,7 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
|
|
|
182
183
|
}
|
|
183
184
|
export function bindRunEngine(context, input) {
|
|
184
185
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
185
|
-
const located =
|
|
186
|
+
const located = locateRunRoot(context.db, projectRoot, input.runId);
|
|
186
187
|
if (!located)
|
|
187
188
|
throw new AppError("not_found", `RUN is not registered in router storage: ${input.runId}`, 1, { run_id: input.runId, project_root: projectRoot });
|
|
188
189
|
const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
@@ -250,13 +251,45 @@ export function bindCurrentEngineToRun(context, input) {
|
|
|
250
251
|
engine: engineIdentity(manifest),
|
|
251
252
|
probe: { status: "not_required", command: null }
|
|
252
253
|
};
|
|
253
|
-
return writeRunEngineBinding(path.join(input.
|
|
254
|
+
return writeRunEngineBinding(path.join(input.runRoot, "engine-binding.json"), binding).binding;
|
|
255
|
+
}
|
|
256
|
+
/** Forks are a new execution authority. The source binding stays immutable;
|
|
257
|
+
* this replaces only the copied binding after proving the selected engine can
|
|
258
|
+
* read the copied RUN. */
|
|
259
|
+
export function upgradeForkedRunEngine(context, input) {
|
|
260
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
261
|
+
const located = locateRunRoot(context.db, projectRoot, input.runId);
|
|
262
|
+
if (!located)
|
|
263
|
+
throw new AppError("not_found", `Forked RUN is not registered: ${input.runId}`, 1);
|
|
264
|
+
const manifest = requireBoundManifest(context.ddFlowHome, {
|
|
265
|
+
packageName: getCliBuildInfo().package_name,
|
|
266
|
+
packageVersion: input.engineVersion,
|
|
267
|
+
...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
|
|
268
|
+
});
|
|
269
|
+
const requested = engineIdentity(manifest);
|
|
270
|
+
readRunEngineBinding(located.binding_path);
|
|
271
|
+
const lineage = path.join(located.run_root, "fork-lineage.json");
|
|
272
|
+
if (!fs.existsSync(lineage))
|
|
273
|
+
throw new AppError("fork_lineage_missing", "Engine upgrade is permitted only for a derived RUN", 1, { run_id: located.run_id });
|
|
274
|
+
const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
|
|
275
|
+
const probe = spawnSync(process.execPath, [resolveManifestEntrypoint(manifest), ...probeArgs], {
|
|
276
|
+
cwd: manifest.snapshot_root,
|
|
277
|
+
env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
|
|
278
|
+
encoding: "utf8", timeout: 30_000
|
|
279
|
+
});
|
|
280
|
+
if (probe.status !== 0)
|
|
281
|
+
throw new AppError("fork_engine_probe_failed", "Selected engine cannot read the forked RUN", 1, { run_id: located.run_id, engine: manifestSummary(manifest), stderr: probe.stderr.slice(0, 2000) });
|
|
282
|
+
const binding = { schema_id: runEngineBindingSchemaId, run_id: located.run_id, project_root: projectRoot, bound_at: context.now(), source: "fork_upgrade", reason: requiredText(input.reason, "reason"), engine: requested, probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` } };
|
|
283
|
+
const temporary = `${located.binding_path}.fork-${process.pid}-${Date.now()}.tmp`;
|
|
284
|
+
fs.writeFileSync(temporary, `${JSON.stringify(binding, null, 2)}\n`);
|
|
285
|
+
fs.renameSync(temporary, located.binding_path);
|
|
286
|
+
return binding;
|
|
254
287
|
}
|
|
255
288
|
function boundRunEngineForArgs(context, args, projectRoot) {
|
|
256
289
|
const runId = runIdForArgs(args);
|
|
257
290
|
if (!runId || !projectRoot)
|
|
258
291
|
return null;
|
|
259
|
-
const located =
|
|
292
|
+
const located = locateRunRoot(context.db, projectRoot, runId);
|
|
260
293
|
if (!located)
|
|
261
294
|
return null;
|
|
262
295
|
const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
@@ -274,6 +307,8 @@ function runIdForArgs(args) {
|
|
|
274
307
|
const [family, command, ...rest] = args;
|
|
275
308
|
const parsed = parseLightArgs(rest);
|
|
276
309
|
if (family === "run" && command !== "start" && command !== "list") {
|
|
310
|
+
if (["drive", "control", "recovery", "capacity"].includes(command ?? ""))
|
|
311
|
+
return option(parsed, "run");
|
|
277
312
|
if (command === "flags")
|
|
278
313
|
return positional(parsed, 1);
|
|
279
314
|
return positional(parsed, 0);
|
|
@@ -383,11 +418,7 @@ export function selectEngine(context, input, options = {}, classification = inpu
|
|
|
383
418
|
const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
|
|
384
419
|
.filter((manifest) => manifest.package_name === packageName)
|
|
385
420
|
.filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
|
|
386
|
-
.filter((manifest) =>
|
|
387
|
-
&& manifest.package_name === build.package_name
|
|
388
|
-
&& manifest.package_version === build.version
|
|
389
|
-
? true
|
|
390
|
-
: manifestHealthy(manifest))
|
|
421
|
+
.filter((manifest) => manifestHealthy(manifest))
|
|
391
422
|
.sort((a, b) => {
|
|
392
423
|
const versionOrder = compareSemverStrings(b.package_version, a.package_version);
|
|
393
424
|
if (versionOrder !== 0)
|
|
@@ -847,7 +878,7 @@ function manifestDiagnostics(manifest, options = {}) {
|
|
|
847
878
|
}
|
|
848
879
|
return diagnostics;
|
|
849
880
|
}
|
|
850
|
-
function manifestSummary(manifest) {
|
|
881
|
+
function manifestSummary(manifest, diagnostics = manifestDiagnostics(manifest, { checkEntrypoint: false })) {
|
|
851
882
|
return {
|
|
852
883
|
schema_id: manifest.schema_id,
|
|
853
884
|
package_name: manifest.package_name,
|
|
@@ -859,8 +890,8 @@ function manifestSummary(manifest) {
|
|
|
859
890
|
snapshot_root: manifest.snapshot_root,
|
|
860
891
|
resolved_entrypoint: safeResolveManifestEntrypoint(manifest),
|
|
861
892
|
installed_at: manifest.installed_at,
|
|
862
|
-
healthy:
|
|
863
|
-
diagnostics
|
|
893
|
+
healthy: diagnostics.length === 0,
|
|
894
|
+
diagnostics
|
|
864
895
|
};
|
|
865
896
|
}
|
|
866
897
|
function resolveManifestEntrypoint(manifest) {
|