@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +396 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +81 -9
  5. package/dist/cli/run-cli.js +442 -46
  6. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  25. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  26. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  27. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  28. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  29. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  30. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  31. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  32. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  33. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  34. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  35. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  36. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  37. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  38. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  39. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  40. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  41. package/dist/runtime/context.js +3 -1
  42. package/dist/schemas/agent-profile.schema.json +1 -1
  43. package/dist/schemas/code-review-result.schema.json +2 -2
  44. package/dist/schemas/code-work-batch.schema.json +4 -3
  45. package/dist/schemas/code-work-result.schema.json +4 -4
  46. package/dist/schemas/harness-config.schema.json +23 -0
  47. package/dist/schemas/plan-review-result.schema.json +2 -2
  48. package/dist/schemas/run-control-receipt.schema.json +99 -0
  49. package/dist/schemas/run-control-request.schema.json +36 -0
  50. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  51. package/dist/services/cleanup.js +8 -5
  52. package/dist/services/cli-operation-classifier.js +11 -1
  53. package/dist/services/code-checks.js +543 -96
  54. package/dist/services/controller-fanout.js +120 -0
  55. package/dist/services/dashboard.js +9 -0
  56. package/dist/services/engines.js +50 -19
  57. package/dist/services/eval-snapshots.js +639 -60
  58. package/dist/services/execution-policy.js +177 -0
  59. package/dist/services/external-work-launch.js +110 -0
  60. package/dist/services/harness-adapter.js +147 -24
  61. package/dist/services/harness-config.js +68 -0
  62. package/dist/services/hooks.js +413 -168
  63. package/dist/services/lanes.js +1 -1
  64. package/dist/services/lifecycle-command.js +74 -6
  65. package/dist/services/lifecycle-invocations.js +637 -0
  66. package/dist/services/managed-daemon-binding.js +38 -0
  67. package/dist/services/managed-processes.js +138 -22
  68. package/dist/services/merge-queue.js +6 -6
  69. package/dist/services/merge-server.js +75 -29
  70. package/dist/services/migrations.js +1 -1
  71. package/dist/services/native-daemon-history.js +49 -0
  72. package/dist/services/native-session-control.js +39 -0
  73. package/dist/services/projects.js +7 -3
  74. package/dist/services/prompts.js +4 -2
  75. package/dist/services/protocols.js +11 -2
  76. package/dist/services/recovery-observation-budget.js +61 -0
  77. package/dist/services/recovery-snapshot-database.js +107 -0
  78. package/dist/services/run-control-receipt.js +56 -0
  79. package/dist/services/run-control-worker.js +363 -0
  80. package/dist/services/run-control.js +876 -0
  81. package/dist/services/run-controller-adapter.js +211 -0
  82. package/dist/services/run-controller-capture.js +134 -0
  83. package/dist/services/run-controller-process.js +189 -0
  84. package/dist/services/run-controller-recovery.js +222 -0
  85. package/dist/services/run-controller-state.js +31 -0
  86. package/dist/services/run-controller.js +733 -0
  87. package/dist/services/run-engine-bindings.js +20 -62
  88. package/dist/services/run-fork.js +124 -0
  89. package/dist/services/run-observations.js +112 -0
  90. package/dist/services/run-recovery-runtime.js +69 -0
  91. package/dist/services/run-recovery.js +327 -0
  92. package/dist/services/runs.js +280 -40
  93. package/dist/services/runtime-budget.js +362 -0
  94. package/dist/services/runtime-scope-capture.js +50 -0
  95. package/dist/services/runtime-scope-control.js +450 -0
  96. package/dist/services/runtime-scope-resume.js +543 -0
  97. package/dist/services/runtime-scope-stop.js +99 -0
  98. package/dist/services/runtime-scope-worker.js +210 -0
  99. package/dist/services/runtime-service.js +96 -0
  100. package/dist/services/schema-validation.js +12 -12
  101. package/dist/services/stage-context.js +2 -2
  102. package/dist/services/stage-lifecycle.js +32 -10
  103. package/dist/services/stage-pause.js +25 -13
  104. package/dist/services/usage.js +37 -93
  105. package/dist/services/vnext-code-review.js +149 -89
  106. package/dist/services/vnext-code.js +223 -215
  107. package/dist/services/vnext-execution-profile.js +5 -3
  108. package/dist/services/vnext-fanout.js +135 -21
  109. package/dist/services/vnext-merge.js +258 -89
  110. package/dist/services/vnext-plan-review.js +75 -64
  111. package/dist/services/vnext-plan.js +77 -29
  112. package/dist/services/vnext-protocolize.js +21 -14
  113. package/dist/services/vnext-specify.js +26 -16
  114. package/dist/services/work-registry.js +592 -154
  115. package/dist/services/workspace-bootstrap.js +76 -0
  116. package/dist/shared/errors.js +12 -0
  117. package/dist/storage/database.js +341 -31
  118. package/dist/storage/writer-contract.js +77 -0
  119. package/dist/storage/writer-migration.js +102 -0
  120. package/package.json +20 -13
  121. 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 {
@@ -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, findRunHome, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
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((manifest) => ({
142
- engine: manifestSummary(manifest),
143
- status: manifestHealthy(manifest) ? "ok" : "corrupt",
144
- diagnostics: manifestDiagnostics(manifest)
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
- if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version) {
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 = findRunHome(context.ddFlowHome, projectRoot, input.runId);
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.runHome, "engine-binding.json"), binding).binding;
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 = findRunHome(context.ddFlowHome, projectRoot, runId);
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) => options.allowCurrentInProcess
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: manifestHealthy(manifest),
863
- diagnostics: manifestDiagnostics(manifest)
893
+ healthy: diagnostics.length === 0,
894
+ diagnostics
864
895
  };
865
896
  }
866
897
  function resolveManifestEntrypoint(manifest) {