@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134

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 (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -2,7 +2,9 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { spawn, spawnSync } from "node:child_process";
5
+ import { spawn } from "node:child_process";
6
+ import { commandText } from '../harness-runtime/lib/process-json.mjs';
7
+ import { SETUP_STARTUP_MS, publishSetup } from '../harness-runtime/lib/setup-progress.mjs';
6
8
  import { createRequire } from "node:module";
7
9
  import { getCliBuildInfo } from "./build-info.js";
8
10
  import { AppError } from "../shared/errors.js";
@@ -12,6 +14,7 @@ import { requireProtocol } from "./protocols.js";
12
14
  import { resolveCanonRootBeforeContext } from "./canon.js";
13
15
  import { appendAudit } from "./audit.js";
14
16
  import { findProjectByRoot } from "./projects.js";
17
+ import { parseCommandArgs } from "./lifecycle-command.js";
15
18
  import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
16
19
  export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
17
20
  const routerNativeFamilies = new Set(["engine", "version", "schema"]);
@@ -31,6 +34,24 @@ export function isRouterNativeCommand(args) {
31
34
  return routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
32
35
  }
33
36
  export function installCurrentEngine(context, input = {}) {
37
+ const installation = prepareInstallation(context, input);
38
+ const first = installation.next();
39
+ if (first.done && input.forRouting)
40
+ return first.value;
41
+ const manifest = first.done ? requireManifest(path.join(String(first.value.path), 'engine.json')) : first.value;
42
+ return probeEntrypoint(manifest, undefined, context.ddFlowHome, process.env).then(() => first.done ? first.value : installation.next().value)
43
+ .catch(error => {
44
+ let cleanup_error;
45
+ try {
46
+ installation.return({});
47
+ }
48
+ catch (cleanup) {
49
+ cleanup_error = { message: String(cleanup).slice(0, 2000) };
50
+ }
51
+ throw new AppError('engine_install_failed', 'Staged engine admission failed', 1, { ...error.details, cause: error.code, probe_error: { code: error.code, details: error.details }, ...(cleanup_error ? { cleanup_error } : {}) });
52
+ });
53
+ }
54
+ function* prepareInstallation(context, input) {
34
55
  const build = getCliBuildInfo();
35
56
  const packageRoot = findPackageRoot(path.dirname(sourceFile));
36
57
  if (!packageRoot) {
@@ -66,6 +87,7 @@ export function installCurrentEngine(context, input = {}) {
66
87
  const tmp = path.join(parent, `.${path.basename(target)}.tmp-${process.pid}-${Date.now()}`);
67
88
  fs.rmSync(tmp, { recursive: true, force: true });
68
89
  ensureDir(tmp);
90
+ publishSetup('probe_started', `engine-staging:${tmp}`);
69
91
  copyPackageSnapshot(packageRoot, tmp);
70
92
  const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
71
93
  const stagedChecksum = stagedManifest.integrity.checksum;
@@ -81,7 +103,7 @@ export function installCurrentEngine(context, input = {}) {
81
103
  packageVersion: build.version,
82
104
  integrityChecksum: stagedChecksum
83
105
  }) : null;
84
- if (duplicate) {
106
+ if (duplicate && manifestDiagnostics(duplicate, { checkEntrypoint: false }).length === 0) {
85
107
  fs.rmSync(tmp, { recursive: true, force: true });
86
108
  return {
87
109
  ok: true,
@@ -94,13 +116,15 @@ export function installCurrentEngine(context, input = {}) {
94
116
  const manifest = { ...stagedManifest, package_root: activationTarget, snapshot_root: activationTarget };
95
117
  fs.writeFileSync(path.join(tmp, "engine.json"), `${JSON.stringify(manifest, null, 2)}\n`);
96
118
  const checkedManifest = requireManifest(path.join(tmp, "engine.json"));
97
- const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
119
+ const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: false });
98
120
  if (stagedDiagnostics.length > 0) {
99
121
  throw new AppError("engine_install_failed", "Staged dd-flow engine failed its integrity or dependency health check", 1, {
100
122
  diagnostics: stagedDiagnostics,
101
123
  path: tmp
102
124
  });
103
125
  }
126
+ if (!input.forRouting)
127
+ yield { ...checkedManifest, package_root: tmp, snapshot_root: tmp };
104
128
  activateEngineAtomically(tmp, activationTarget);
105
129
  const installed = requireManifest(path.join(activationTarget, "engine.json"));
106
130
  return {
@@ -117,7 +141,7 @@ export function installCurrentEngine(context, input = {}) {
117
141
  }
118
142
  export function listEngines(context) {
119
143
  const root = engineStoreRoot(context.ddFlowHome);
120
- const engines = readInstalledManifests(root).map(manifestSummary);
144
+ const engines = readInstalledManifests(root).map(manifest => manifestSummary(manifest));
121
145
  return { ok: true, schema_id: "dd-flow/engine-list@1", engine_store: root, engines };
122
146
  }
123
147
  export function engineInfo(context, input) {
@@ -135,14 +159,23 @@ export function resolveEngine(context, input = {}) {
135
159
  }
136
160
  return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection, contexts: compatibilityContexts(context, { ...input, classification }) };
137
161
  }
138
- export function doctorEngines(context, input = {}) {
162
+ export async function doctorEngines(context, input = {}) {
139
163
  const root = engineStoreRoot(context.ddFlowHome);
140
164
  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
- }));
165
+ const checks = [];
166
+ for (const manifest of installed) {
167
+ const diagnostics = manifestDiagnostics(manifest);
168
+ let probe_error;
169
+ if (!diagnostics.length)
170
+ try {
171
+ await probeEntrypoint(manifest, undefined, context.ddFlowHome, context.env);
172
+ }
173
+ catch (error) {
174
+ probe_error = error instanceof AppError ? error.details : { message: String(error) };
175
+ diagnostics.push(error instanceof AppError && error.code === 'engine_probe_failed' ? 'entrypoint_dependency_load_failed' : 'entrypoint_probe_unavailable');
176
+ }
177
+ checks.push({ engine: manifestSummary(manifest, diagnostics), status: diagnostics.includes("entrypoint_probe_unavailable") ? "unverified" : diagnostics.length ? "corrupt" : "ok", diagnostics, ...(probe_error ? { probe_error } : {}) });
178
+ }
146
179
  const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
147
180
  const selection = selectEngine(context, input, {}, classification);
148
181
  return {
@@ -155,13 +188,14 @@ export function doctorEngines(context, input = {}) {
155
188
  exit_code: checks.every((check) => check.status === "ok") && selection.status === "selected" ? 0 : 1
156
189
  };
157
190
  }
158
- export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
191
+ export async function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
159
192
  if (isEngineMode(env) || isRouterNativeCommand(args))
160
193
  return Promise.resolve(null);
161
194
  const classification = classifyCliOperation(args, env);
162
195
  const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
163
196
  if (bound) {
164
197
  const entrypoint = resolveManifestEntrypoint(bound.manifest);
198
+ await probeEntrypoint(bound.manifest, undefined, context.ddFlowHome, process.env);
165
199
  return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
166
200
  }
167
201
  const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
@@ -174,13 +208,25 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
174
208
  if (!selection.selected)
175
209
  return Promise.resolve(null);
176
210
  const current = getCliBuildInfo();
177
- if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version) {
211
+ const currentRoot = findPackageRoot(path.dirname(sourceFile));
212
+ if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version
213
+ && currentRoot !== null && fs.realpathSync(selection.selected.snapshot_root) === fs.realpathSync(currentRoot)) {
178
214
  return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
179
215
  }
180
216
  const entrypoint = resolveManifestEntrypoint(selection.selected);
217
+ try {
218
+ await probeEntrypoint(selection.selected, undefined, context.ddFlowHome, process.env);
219
+ }
220
+ catch (cause) {
221
+ const error = cause;
222
+ if (error.code !== 'engine_probe_failed')
223
+ throw error;
224
+ throw new AppError('missing_engine', 'Selected engine cannot load its runtime dependencies', 1, { probe: error.details });
225
+ }
181
226
  return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
182
227
  }
183
- export function bindRunEngine(context, input) {
228
+ export async function prepareRunEngineBind(context, input) {
229
+ const reason = requiredText(input.reason, "reason");
184
230
  const projectRoot = resolveProjectRoot(input.projectRoot);
185
231
  const located = locateRunRoot(context.db, projectRoot, input.runId);
186
232
  if (!located)
@@ -190,40 +236,43 @@ export function bindRunEngine(context, input) {
190
236
  packageName: input.packageName ?? getCliBuildInfo().package_name,
191
237
  packageVersion: input.engineVersion,
192
238
  ...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
193
- });
239
+ }, { checkEntrypoint: false });
194
240
  const requested = engineIdentity(manifest);
195
241
  if (existing) {
196
242
  if (sameEngineIdentity(existing.engine, requested))
197
- return { ok: true, action: "run_engine_bind", changed: false, binding: existing };
243
+ return { kind: "replay", binding: existing };
198
244
  throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, {
199
245
  run_id: located.run_id,
200
246
  current: existing.engine,
201
247
  requested
202
248
  });
203
249
  }
204
- const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
205
- const probe = spawnSync(process.execPath, [resolveManifestEntrypoint(manifest), ...probeArgs], {
206
- cwd: manifest.snapshot_root,
207
- env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
208
- encoding: "utf8",
209
- timeout: 30_000
210
- });
211
- if (probe.status !== 0) {
212
- throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
213
- run_id: located.run_id,
214
- engine: manifestSummary(manifest),
215
- command: `dd-flow ${probeArgs.join(" ")}`,
216
- exit_code: probe.status,
217
- stderr: probe.stderr.slice(0, 2000)
218
- });
250
+ await probeEntrypoint(manifest, ['run', 'status', located.run_id, '--project-root', projectRoot, '--json'], context.ddFlowHome, context.env, 'run_engine');
251
+ return { kind: "create", projectRoot, located, manifest, requested, reason, entrypoint: resolveManifestEntrypoint(manifest) };
252
+ }
253
+ export async function bindRunEngine(context, input, supplied) {
254
+ const prepared = supplied ?? await prepareRunEngineBind(context, input);
255
+ if (prepared.kind === "replay")
256
+ return { ok: true, action: "run_engine_bind", changed: false, binding: prepared.binding };
257
+ const { projectRoot, located, manifest, requested, reason } = prepared;
258
+ const current = locateRunRoot(context.db, projectRoot, input.runId);
259
+ if (!current || current.run_root !== located.run_root)
260
+ throw new AppError('run_engine_binding_conflict', 'RUN identity changed during probe', 1);
261
+ requireBoundManifest(context.ddFlowHome, { packageName: manifest.package_name, packageVersion: manifest.package_version, integrityChecksum: requested.integrity_checksum });
262
+ const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
263
+ if (existing) {
264
+ if (sameEngineIdentity(existing.engine, requested))
265
+ return { ok: true, action: "run_engine_bind", changed: false, binding: existing };
266
+ throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, { run_id: located.run_id, current: existing.engine, requested });
219
267
  }
268
+ const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
220
269
  const binding = {
221
270
  schema_id: runEngineBindingSchemaId,
222
271
  run_id: located.run_id,
223
272
  project_root: projectRoot,
224
273
  bound_at: context.now(),
225
274
  source: "legacy_recovery",
226
- reason: requiredText(input.reason, "reason"),
275
+ reason,
227
276
  engine: requested,
228
277
  probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` }
229
278
  };
@@ -252,6 +301,36 @@ export function bindCurrentEngineToRun(context, input) {
252
301
  };
253
302
  return writeRunEngineBinding(path.join(input.runRoot, "engine-binding.json"), binding).binding;
254
303
  }
304
+ /** Forks are a new execution authority. The source binding stays immutable;
305
+ * this replaces only the copied binding after proving the selected engine can
306
+ * read the copied RUN. */
307
+ export async function upgradeForkedRunEngine(context, input) {
308
+ const projectRoot = resolveProjectRoot(input.projectRoot);
309
+ const located = locateRunRoot(context.db, projectRoot, input.runId);
310
+ if (!located)
311
+ throw new AppError("not_found", `Forked RUN is not registered: ${input.runId}`, 1);
312
+ const manifest = requireBoundManifest(context.ddFlowHome, {
313
+ packageName: getCliBuildInfo().package_name,
314
+ packageVersion: input.engineVersion,
315
+ ...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
316
+ });
317
+ const requested = engineIdentity(manifest);
318
+ const priorBinding = fs.readFileSync(located.binding_path, 'utf8');
319
+ const lineage = path.join(located.run_root, "fork-lineage.json");
320
+ if (!fs.existsSync(lineage))
321
+ throw new AppError("fork_lineage_missing", "Engine upgrade is permitted only for a derived RUN", 1, { run_id: located.run_id });
322
+ const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
323
+ const priorLineage = fs.readFileSync(lineage, 'utf8');
324
+ await probeEntrypoint(manifest, probeArgs, context.ddFlowHome, context.env, 'fork_engine');
325
+ if (fs.readFileSync(located.binding_path, 'utf8') !== priorBinding || fs.readFileSync(lineage, 'utf8') !== priorLineage)
326
+ throw new AppError('fork_engine_binding_conflict', 'Fork identity changed during probe', 1);
327
+ requireBoundManifest(context.ddFlowHome, { packageName: manifest.package_name, packageVersion: manifest.package_version, integrityChecksum: manifest.integrity.checksum });
328
+ 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(" ")}` } };
329
+ const temporary = `${located.binding_path}.fork-${process.pid}-${Date.now()}.tmp`;
330
+ fs.writeFileSync(temporary, `${JSON.stringify(binding, null, 2)}\n`);
331
+ fs.renameSync(temporary, located.binding_path);
332
+ return binding;
333
+ }
255
334
  function boundRunEngineForArgs(context, args, projectRoot) {
256
335
  const runId = runIdForArgs(args);
257
336
  if (!runId || !projectRoot)
@@ -274,6 +353,8 @@ function runIdForArgs(args) {
274
353
  const [family, command, ...rest] = args;
275
354
  const parsed = parseLightArgs(rest);
276
355
  if (family === "run" && command !== "start" && command !== "list") {
356
+ if (["drive", "control", "recovery", "capacity"].includes(command ?? ""))
357
+ return option(parsed, "run");
277
358
  if (command === "flags")
278
359
  return positional(parsed, 1);
279
360
  return positional(parsed, 0);
@@ -290,7 +371,7 @@ function runIdForArgs(args) {
290
371
  return option(parsed, "run");
291
372
  return null;
292
373
  }
293
- function requireBoundManifest(ddFlowHome, input) {
374
+ function requireBoundManifest(ddFlowHome, input, options = {}) {
294
375
  const manifest = input.snapshotRoot
295
376
  ? requireManifest(path.join(input.snapshotRoot, "engine.json"))
296
377
  : findInstalledManifest(ddFlowHome, {
@@ -303,7 +384,7 @@ function requireBoundManifest(ddFlowHome, input) {
303
384
  || manifest.package_version !== input.packageVersion
304
385
  || input.integrityChecksum !== undefined && manifest.integrity.checksum !== input.integrityChecksum
305
386
  || input.snapshotRoot !== undefined && path.resolve(manifest.snapshot_root) !== path.resolve(input.snapshotRoot)
306
- || !manifestHealthy(manifest)) {
387
+ || manifestDiagnostics(manifest, options).length !== 0) {
307
388
  throw new AppError("run_engine_binding_invalid", "RUN engine binding points to a missing or unhealthy engine snapshot", 1, {
308
389
  package_name: input.packageName,
309
390
  package_version: input.packageVersion,
@@ -344,7 +425,7 @@ function executionManifest(context) {
344
425
  return requireManifest(path.join(String(installed.path), "engine.json"));
345
426
  }
346
427
  function requiredText(value, label) {
347
- const normalized = value.trim();
428
+ const normalized = typeof value === "string" ? value.trim() : "";
348
429
  if (!normalized)
349
430
  throw new AppError("usage", `Missing required value: ${label}`, 2);
350
431
  return normalized;
@@ -380,14 +461,14 @@ export function selectEngine(context, input, options = {}, classification = inpu
380
461
  const requiredRange = stringValue(compatibility?.engine?.version_range) ?? (classification.mode === "mb_upgrade" ? null : `>=${build.version} <9999.0.0`);
381
462
  const recommended = stringValue(compatibility?.engine?.recommended_version) ?? build.version;
382
463
  const installHintTemplate = stringValue(compatibility?.engine?.old_engine_install_hint) ?? `npx ${packageName}@<version> engine install`;
383
- const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
464
+ // An engine-mode caller already executes this module's build. Another
465
+ // installed engine cannot establish compatibility for its current writes.
466
+ // Keep full snapshot validation in routing/binding, not lease maintenance.
467
+ const executingCurrent = options.allowCurrentInProcess && isEngineMode(input.env ?? process.env);
468
+ const manifests = executingCurrent ? [] : readInstalledManifests(engineStoreRoot(context.ddFlowHome))
384
469
  .filter((manifest) => manifest.package_name === packageName)
385
470
  .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))
471
+ .filter((manifest) => manifestHealthy(manifest))
391
472
  .sort((a, b) => {
392
473
  const versionOrder = compareSemverStrings(b.package_version, a.package_version);
393
474
  if (versionOrder !== 0)
@@ -402,7 +483,7 @@ export function selectEngine(context, input, options = {}, classification = inpu
402
483
  requiredRange !== null &&
403
484
  packageName === build.package_name &&
404
485
  satisfiesRange(build.version, requiredRange)
405
- ? currentBuildManifest(context, build)
486
+ ? currentBuildManifest(context, build, options.verifyCurrentContent !== false)
406
487
  : null);
407
488
  const diagnostics = [
408
489
  ...targetBlockers,
@@ -420,9 +501,38 @@ export function selectEngine(context, input, options = {}, classification = inpu
420
501
  diagnostics
421
502
  };
422
503
  }
423
- function currentBuildManifest(context, build) {
504
+ function currentBuildManifest(context, build, verifyContent) {
424
505
  const packageRoot = findPackageRoot(path.dirname(sourceFile)) ?? process.cwd();
425
- return buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot);
506
+ const manifestFile = path.join(packageRoot, "engine.json");
507
+ const installedLayout = path.basename(path.dirname(path.dirname(packageRoot))) === "engines"
508
+ && path.basename(path.dirname(packageRoot)) === path.basename(path.dirname(engineVersionRoot(context.ddFlowHome, build.package_name, build.version)));
509
+ if (fs.existsSync(manifestFile) || installedLayout) {
510
+ const installed = readManifest(manifestFile);
511
+ // Use the executing module's physical root, never an environment alias.
512
+ // A damaged installed manifest must not become a development fallback.
513
+ if (!installed || installed.package_name !== build.package_name || installed.package_version !== build.version
514
+ || installed.engine_version !== build.version || installed.engine_id !== "dd-flow-engine"
515
+ || typeof installed.snapshot_root !== "string" || !fs.existsSync(installed.snapshot_root)
516
+ || fs.realpathSync(installed.snapshot_root) !== fs.realpathSync(packageRoot)
517
+ || typeof installed.entrypoint !== "string" || installed.integrity?.source !== "package_snapshot"
518
+ || typeof installed.integrity?.checksum !== "string" || !/^[a-f0-9]{64}$/.test(installed.integrity.checksum)
519
+ || (installed.schema_registry && (!Array.isArray(installed.schema_registry.entries)
520
+ || installed.schema_registry.entries.some(entry => !entry || typeof entry.path !== "string"
521
+ || typeof entry.id !== "string" || typeof entry.name !== "string" || !/^[a-f0-9]{64}$/.test(entry.checksum ?? ""))))) {
522
+ throw new AppError("unhealthy_engine", "Executing engine snapshot manifest does not match its build", 1, { manifest: manifestFile });
523
+ }
524
+ const diagnostics = manifestDiagnostics(installed, { checkEntrypoint: false, verifyContent });
525
+ if (diagnostics.length)
526
+ throw new AppError("unhealthy_engine", "Executing engine snapshot is unhealthy", 1, { manifest: manifestFile, diagnostics });
527
+ return installed;
528
+ }
529
+ // Only an unmaterialized package has no snapshot proof. Canon and
530
+ // package-manager aliases are expected in development checkouts.
531
+ const manifest = buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot, null);
532
+ // An ephemeral routing choice has no snapshot proof. Hash bytes only when
533
+ // materializing/validating an installed snapshot, never on every CLI call.
534
+ manifest.integrity.mode = "degraded_local_development";
535
+ return manifest;
426
536
  }
427
537
  export function compatibilityContexts(context, input = {}) {
428
538
  const env = input.env ?? process.env;
@@ -560,40 +670,16 @@ function canonCompatibilitySha(memorybankRoot) {
560
670
  return null;
561
671
  }
562
672
  }
563
- function parseLightArgs(args) {
564
- const positional = [];
565
- const options = new Map();
566
- for (let index = 0; index < args.length; index += 1) {
567
- const value = args[index];
568
- if (value?.startsWith("--")) {
569
- const equal = value.indexOf("=");
570
- if (equal > 2) {
571
- const key = value.slice(2, equal);
572
- options.set(key, [...(options.get(key) ?? []), value.slice(equal + 1)]);
573
- continue;
574
- }
575
- const key = value.slice(2);
576
- const next = args[index + 1];
577
- if (!next || next.startsWith("--"))
578
- options.set(key, [...(options.get(key) ?? []), ""]);
579
- else {
580
- options.set(key, [...(options.get(key) ?? []), next]);
581
- index += 1;
582
- }
583
- }
584
- else if (value)
585
- positional.push(value);
586
- }
587
- return { positional, options };
588
- }
673
+ const parseLightArgs = parseCommandArgs;
589
674
  function option(parsed, key) {
590
675
  const values = parsed.options.get(key);
591
- return values?.[values.length - 1] || null;
676
+ const value = values?.[values.length - 1];
677
+ return value && value !== "__dd_flow_flag__" ? value : null;
592
678
  }
593
679
  function positional(parsed, index) {
594
680
  return parsed.positional[index] ?? null;
595
681
  }
596
- function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot) {
682
+ function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot, checksum = engineSnapshotChecksum(checksumRoot)) {
597
683
  return {
598
684
  schema_id: engineManifestSchemaId,
599
685
  package_name: packageName,
@@ -614,29 +700,46 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
614
700
  schema_registry: buildSchemaRegistry(checksumRoot),
615
701
  integrity: {
616
702
  source: "package_snapshot",
617
- checksum: engineSnapshotChecksum(checksumRoot),
703
+ checksum,
618
704
  mode: "full_content"
619
705
  }
620
706
  };
621
707
  }
622
708
  function copyPackageSnapshot(packageRoot, target) {
709
+ let copied = 0;
710
+ for (const [relative, source] of packageSnapshotFiles(packageRoot)) {
711
+ const destination = path.join(target, relative);
712
+ ensureDir(path.dirname(destination));
713
+ fs.copyFileSync(source, destination);
714
+ if (++copied % 64 === 0)
715
+ publishSetup('transfer_progress', `${target}:${copied}`);
716
+ }
717
+ }
718
+ /** The same source inventory drives validation and copying; no staging writes. */
719
+ function packageSnapshotFiles(packageRoot) {
720
+ const files = new Map();
721
+ const collect = (source, relative, skipNodeModules = false) => {
722
+ if (!fs.statSync(source).isDirectory()) {
723
+ files.set(relative, source);
724
+ return;
725
+ }
726
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
727
+ if (skipNodeModules && entry.name === "node_modules")
728
+ continue;
729
+ const child = path.join(source, entry.name), name = path.join(relative, entry.name);
730
+ if (entry.isDirectory())
731
+ collect(child, name, skipNodeModules);
732
+ else if (entry.isFile())
733
+ files.set(name, child);
734
+ }
735
+ };
623
736
  for (const item of ["dist", "package.json", "README.md", "CHANGELOG.md", "tools"]) {
624
737
  const from = path.join(packageRoot, item);
625
738
  if (!fs.existsSync(from))
626
739
  continue;
627
- const to = path.join(target, item);
628
- const stat = fs.statSync(from);
629
- if (stat.isDirectory())
630
- copyDir(from, to);
631
- else {
632
- ensureDir(path.dirname(to));
633
- fs.copyFileSync(from, to);
634
- }
740
+ collect(from, item);
635
741
  }
636
- copyProductionDependencies(packageRoot, target);
637
- }
638
- function copyProductionDependencies(packageRoot, target) {
639
- const queue = [{ sourceRoot: packageRoot, targetRoot: target, ancestors: new Set() }];
742
+ const queue = [{ sourceRoot: packageRoot, targetRoot: "", ancestors: new Set() }];
640
743
  const copied = new Set();
641
744
  while (queue.length > 0) {
642
745
  const current = queue.shift();
@@ -651,12 +754,39 @@ function copyProductionDependencies(packageRoot, target) {
651
754
  if (copied.has(copyKey))
652
755
  continue;
653
756
  copied.add(copyKey);
654
- copyDir(sourceRoot, targetRoot, true);
757
+ collect(sourceRoot, targetRoot, true);
655
758
  if (current.ancestors.has(sourceRoot))
656
759
  continue;
657
760
  queue.push({ sourceRoot, targetRoot, ancestors: new Set([...current.ancestors, sourceRoot]) });
658
761
  }
659
762
  }
763
+ return files;
764
+ }
765
+ export function prepareForkEngine(engineVersion, integrityChecksum) {
766
+ const build = getCliBuildInfo();
767
+ if (engineVersion !== build.version)
768
+ throw new AppError("fork_engine_unavailable", "Fork installs the invoking CLI engine; invoke the requested version to upgrade the fork", 2, { requested: engineVersion, available: build.version });
769
+ const packageRoot = findPackageRoot(path.dirname(sourceFile));
770
+ if (!packageRoot || !fs.existsSync(path.join(packageRoot, "dist", "cli.js")))
771
+ throw new AppError("harness_runtime_unbuilt", "Build the CLI before preparing a fork", 1);
772
+ const files = packageSnapshotFiles(packageRoot);
773
+ if (integrityChecksum !== undefined) {
774
+ const checksum = snapshotFilesChecksum(files);
775
+ if (checksum !== integrityChecksum)
776
+ throw new AppError("fork_engine_unavailable", "Requested engine checksum differs from the invoking CLI artifact", 2, { requested: integrityChecksum });
777
+ }
778
+ }
779
+ function snapshotFilesChecksum(files) {
780
+ const checksum = crypto.createHash("sha256");
781
+ for (const [relative, source] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
782
+ if (path.basename(relative) === "engine.json")
783
+ continue;
784
+ checksum.update(relative);
785
+ checksum.update("\0");
786
+ checksum.update(fs.readFileSync(source));
787
+ checksum.update("\0");
788
+ }
789
+ return checksum.digest("hex");
660
790
  }
661
791
  function readProductionDependencies(packageRoot) {
662
792
  try {
@@ -713,19 +843,6 @@ function hasAncestorFile(start, name) {
713
843
  current = parent;
714
844
  }
715
845
  }
716
- function copyDir(from, to, skipNodeModules = false) {
717
- ensureDir(to);
718
- for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
719
- if (skipNodeModules && entry.name === "node_modules")
720
- continue;
721
- const source = path.join(from, entry.name);
722
- const target = path.join(to, entry.name);
723
- if (entry.isDirectory())
724
- copyDir(source, target, skipNodeModules);
725
- else if (entry.isFile())
726
- fs.copyFileSync(source, target);
727
- }
728
- }
729
846
  function acquireInstallLock(lockDir) {
730
847
  ensureDir(path.dirname(lockDir));
731
848
  try {
@@ -750,6 +867,9 @@ function readInstalledManifests(root) {
750
867
  if (!fs.statSync(packageRoot).isDirectory())
751
868
  continue;
752
869
  for (const versionDir of fs.readdirSync(packageRoot)) {
870
+ // Failed staging, activation backups and locks are not installations.
871
+ if (versionDir.startsWith(".") || /\.previous-\d+-\d+$/.test(versionDir) || versionDir.endsWith(".lock"))
872
+ continue;
753
873
  const manifest = readManifest(path.join(packageRoot, versionDir, "engine.json"));
754
874
  if (manifest)
755
875
  manifests.push(manifest);
@@ -802,7 +922,7 @@ function manifestDiagnostics(manifest, options = {}) {
802
922
  if (!manifest.integrity || manifest.integrity.mode !== "full_content" || !manifest.integrity.checksum) {
803
923
  diagnostics.push("content_checksum_missing");
804
924
  }
805
- else if (fs.existsSync(snapshotRoot)) {
925
+ else if (options.verifyContent !== false && fs.existsSync(snapshotRoot)) {
806
926
  try {
807
927
  if (engineSnapshotChecksum(snapshotRoot) !== manifest.integrity.checksum)
808
928
  diagnostics.push("content_checksum_mismatch");
@@ -816,6 +936,8 @@ function manifestDiagnostics(manifest, options = {}) {
816
936
  const schemaPath = safeResolveSnapshotPath(manifest, entry.path);
817
937
  if (!schemaPath || !fs.existsSync(schemaPath))
818
938
  diagnostics.push(`schema_registry_missing:${entry.name}`);
939
+ else if (options.verifyContent === false)
940
+ continue;
819
941
  else if (fileChecksum(schemaPath) !== entry.checksum)
820
942
  diagnostics.push(`schema_registry_checksum_mismatch:${entry.name}`);
821
943
  else {
@@ -829,25 +951,21 @@ function manifestDiagnostics(manifest, options = {}) {
829
951
  }
830
952
  }
831
953
  }
832
- if (options.checkEntrypoint !== false && entrypoint && fs.existsSync(entrypoint) && !diagnostics.includes("entrypoint_outside_snapshot")) {
833
- const health = spawnSync(process.execPath, [entrypoint, "--version", "--json"], {
834
- cwd: snapshotRoot,
835
- env: {
836
- ...process.env,
837
- DD_FLOW_ENGINE_MODE: "1",
838
- DD_FLOW_ENGINE_HOME: snapshotRoot,
839
- DD_FLOW_ENGINE_HEALTHCHECK: "1"
840
- },
841
- encoding: "utf8",
842
- stdio: ["ignore", "pipe", "pipe"],
843
- timeout: 15_000
844
- });
845
- if (health.status !== 0)
846
- diagnostics.push("entrypoint_dependency_load_failed");
847
- }
848
954
  return diagnostics;
849
955
  }
850
- function manifestSummary(manifest) {
956
+ async function probeEntrypoint(manifest, args = ['--version', '--json'], runtimeHome, env = {}, purpose = 'engine') {
957
+ publishSetup('probe_started', manifest.snapshot_root);
958
+ try {
959
+ await commandText(resolveManifestEntrypoint(manifest), args, { cwd: manifest.snapshot_root,
960
+ env: { ...env, DD_FLOW_ENGINE_MODE: '1', DD_FLOW_ENGINE_HOME: manifest.snapshot_root, ...(runtimeHome ? { DD_FLOW_HOME: runtimeHome } : {}), ...(args[0] === '--version' ? { DD_FLOW_ENGINE_HEALTHCHECK: '1' } : {}) }, startupMs: SETUP_STARTUP_MS, phase: 'engine-probe' });
961
+ publishSetup('probe_completed', manifest.snapshot_root);
962
+ }
963
+ catch (cause) {
964
+ const error = cause;
965
+ throw new AppError(error.details?.reason === 'child_exit' && error.details?.signal === null && error.details?.exit_code !== null ? `${purpose}_probe_failed` : `${purpose}_probe_unavailable`, 'Selected engine probe did not complete successfully', 1, { cause: error.code, message: error.message, ...error.details });
966
+ }
967
+ }
968
+ function manifestSummary(manifest, diagnostics = manifestDiagnostics(manifest, { checkEntrypoint: false })) {
851
969
  return {
852
970
  schema_id: manifest.schema_id,
853
971
  package_name: manifest.package_name,
@@ -859,8 +977,8 @@ function manifestSummary(manifest) {
859
977
  snapshot_root: manifest.snapshot_root,
860
978
  resolved_entrypoint: safeResolveManifestEntrypoint(manifest),
861
979
  installed_at: manifest.installed_at,
862
- healthy: manifestHealthy(manifest),
863
- diagnostics: manifestDiagnostics(manifest)
980
+ healthy: diagnostics.length === 0,
981
+ diagnostics
864
982
  };
865
983
  }
866
984
  function resolveManifestEntrypoint(manifest) {
@@ -1011,22 +1129,30 @@ function findPackageRoot(start) {
1011
1129
  }
1012
1130
  }
1013
1131
  export function engineSnapshotChecksum(root) {
1132
+ publishSetup('probe_started', `engine-digest:${root}`);
1014
1133
  const files = [];
1015
1134
  collectFiles(root, root, files);
1016
1135
  const checksum = crypto.createHash("sha256");
1136
+ let checked = 0;
1017
1137
  for (const file of files.sort()) {
1018
1138
  checksum.update(file);
1019
1139
  checksum.update("\0");
1020
1140
  checksum.update(fs.readFileSync(path.join(root, file)));
1141
+ if (++checked % 64 === 0)
1142
+ publishSetup('identity_digest_progress', `${root}:${checked}`);
1021
1143
  checksum.update("\0");
1022
1144
  }
1023
- return checksum.digest("hex");
1145
+ const result = checksum.digest("hex");
1146
+ publishSetup('identity_digest_completed', root);
1147
+ return result;
1024
1148
  }
1025
1149
  function collectFiles(root, current, files) {
1026
1150
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1027
1151
  if (entry.name === "engine.json")
1028
1152
  continue;
1029
1153
  const full = path.join(current, entry.name);
1154
+ if (entry.isSymbolicLink())
1155
+ throw new AppError("engine_integrity_symlink", "Pinned engine snapshot contains an unhashed symbolic link", 1, { path: path.relative(root, full) });
1030
1156
  if (entry.isDirectory())
1031
1157
  collectFiles(root, full, files);
1032
1158
  else if (entry.isFile())