@deksden-com/dd-flow-cli 0.9.0-beta.145 → 0.9.0-beta.149

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 (101) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/build-info.json +7 -7
  3. package/dist/cli/command-inputs.js +6 -2
  4. package/dist/cli/command-metadata.js +18 -2
  5. package/dist/cli/diagnostic-invocation.js +46 -0
  6. package/dist/cli/diagnostic.js +120 -0
  7. package/dist/cli/help.js +35 -0
  8. package/dist/cli/memory-flow-admission.js +4 -2
  9. package/dist/cli/memory-init.js +5 -2
  10. package/dist/cli/run-cli.js +16 -15
  11. package/dist/cli.js +6 -0
  12. package/dist/domain/diagnostic-aspects.js +103 -0
  13. package/dist/domain/diagnostic-flows.js +8 -0
  14. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  15. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  16. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
  19. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  20. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  21. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  22. package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
  23. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
  24. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  25. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  26. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  27. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  28. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  29. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
  30. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  31. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  32. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  33. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  34. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  35. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
  36. package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
  37. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  38. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  39. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  40. package/dist/schemas/execution-bundle.schema.json +7 -1
  41. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  42. package/dist/services/check-environment.js +21 -0
  43. package/dist/services/check-runner-evidence.js +267 -15
  44. package/dist/services/code-checks.js +23 -15
  45. package/dist/services/controller-execution.js +10 -4
  46. package/dist/services/controller-fanout.js +13 -6
  47. package/dist/services/controller-memory-worker.js +3 -2
  48. package/dist/services/dashboard.js +2 -2
  49. package/dist/services/descriptor-run-settlement.js +5 -3
  50. package/dist/services/descriptor-run-start.js +22 -3
  51. package/dist/services/descriptor-stage-coordinator.js +37 -4
  52. package/dist/services/descriptor-stage-settlement.js +2 -1
  53. package/dist/services/diagnostic-evidence.js +73 -0
  54. package/dist/services/diagnostic-report-publication.js +72 -0
  55. package/dist/services/diagnostic-report.js +67 -0
  56. package/dist/services/diagnostic-run-bundle.js +74 -0
  57. package/dist/services/diagnostic-snapshot.js +159 -0
  58. package/dist/services/diagnostic-stage-handlers.js +108 -0
  59. package/dist/services/diagnostic-stage-results.js +132 -0
  60. package/dist/services/diagnostic-start.js +84 -0
  61. package/dist/services/diagnostic-work-batch.js +34 -0
  62. package/dist/services/diagnostic-work-collection.js +97 -0
  63. package/dist/services/diagnostic-work-registration.js +101 -0
  64. package/dist/services/diagnostic-work-results.js +93 -0
  65. package/dist/services/eval-snapshots.js +85 -29
  66. package/dist/services/execution-bundles.js +19 -7
  67. package/dist/services/flow-catalog.js +6 -1
  68. package/dist/services/frozen-engine.js +37 -2
  69. package/dist/services/hybrid-contracts.js +2 -0
  70. package/dist/services/installed-flow-source.js +25 -3
  71. package/dist/services/lifecycle-invocations.js +147 -34
  72. package/dist/services/memory-init-contexts.js +16 -0
  73. package/dist/services/memory-init-controller-start.js +12 -4
  74. package/dist/services/memory-init-lint-capability.js +5 -5
  75. package/dist/services/memory-init-native-capacity.js +4 -4
  76. package/dist/services/memory-init-preflight.js +10 -16
  77. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  78. package/dist/services/memory-layer.js +4 -3
  79. package/dist/services/memory-tool-probe.js +25 -0
  80. package/dist/services/memory-verification-checks.js +15 -4
  81. package/dist/services/native-daemon-history.js +2 -1
  82. package/dist/services/provider-capacity.js +43 -38
  83. package/dist/services/release-cache.js +8 -0
  84. package/dist/services/run-control.js +2 -1
  85. package/dist/services/run-controller-adapter.js +2 -1
  86. package/dist/services/run-controller-process.js +2 -1
  87. package/dist/services/run-controller-recovery.js +2 -1
  88. package/dist/services/run-controller.js +85 -15
  89. package/dist/services/run-fork.js +1 -1
  90. package/dist/services/runs.js +6 -1
  91. package/dist/services/runtime-scope-resume.js +12 -6
  92. package/dist/services/runtime-scope-stop.js +3 -2
  93. package/dist/services/runtime-service.js +2 -1
  94. package/dist/services/stage-handlers.js +16 -3
  95. package/dist/services/stage-pause.js +4 -1
  96. package/dist/services/version-status.js +1 -1
  97. package/dist/services/vnext-code.js +2 -2
  98. package/dist/services/vnext-fanout.js +31 -1
  99. package/dist/services/work-registry.js +37 -6
  100. package/dist/services/workspace-bootstrap.js +2 -1
  101. package/package.json +6 -3
@@ -0,0 +1,101 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
3
+ import { requireProjectByRoot } from "./projects.js";
4
+ import { resolveProjectRoot } from "../storage/paths.js";
5
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
6
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
7
+ import { readDescriptorRootWork } from "./descriptor-root-work.js";
8
+ import { diagnosticSnapshotPaths, readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
9
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
10
+ import { validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
11
+ import { diagnosticWorkBatch } from "./diagnostic-work-batch.js";
12
+ import { addWorkBatch, prepareWorkBatch, readWorkBatchCustody } from "./work-registry.js";
13
+ import { stageGraphScope } from "./stage-work-graph.js";
14
+ import { hybridDigest } from "./hybrid-contracts.js";
15
+ import { appendAudit } from "./audit.js";
16
+ const event = "diagnostic.work_registration";
17
+ function refuse(reason) { throw new AppError("diagnostic_work_registration_invalid", "Diagnostic graph lacks its assigned stage authority", 2, { reason, effect: "no_effect" }); }
18
+ /** SQL-only reader. An explicitly registered empty graph is distinct from no graph. */
19
+ export function readRegisteredDiagnosticGraph(context, scope) {
20
+ const graph = stageGraphScope(context, scope.projectId, scope.runId, scope.stage);
21
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage, graph.attempt]);
22
+ if (!rows.length)
23
+ return null;
24
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
25
+ refuse("ambiguous_registration");
26
+ const receipt = JSON.parse(rows[0].payload_json);
27
+ if (receipt.schema_id !== "dd-flow/diagnostic-work-registration@1" || receipt.run_id !== scope.runId
28
+ || receipt.stage !== scope.stage || receipt.attempt !== graph.attempt || !receipt.work_ids
29
+ || receipt.input.run_id !== scope.runId || receipt.input.stage !== scope.stage
30
+ || receipt.input.input_sha256 !== receipt.input_sha256)
31
+ refuse("foreign_registration");
32
+ const batch = diagnosticWorkBatch(receipt.input);
33
+ if (receipt.batch_sha256 !== hybridDigest(batch.works) || batch.works.length !== Object.keys(receipt.work_ids).length
34
+ || new Set(Object.values(receipt.work_ids)).size !== batch.works.length)
35
+ refuse("changed_batch");
36
+ for (const item of batch.works) {
37
+ const id = receipt.work_ids[item.key];
38
+ if (!id)
39
+ refuse("missing_work");
40
+ const custody = readWorkBatchCustody(context, id);
41
+ if (custody.item_key !== item.key || custody.batch_sha256 !== receipt.batch_sha256
42
+ || hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids)
43
+ || hybridDigest(custody.scope) !== hybridDigest({ ...graph, stage: scope.stage,
44
+ parent_work_id: receipt.parent_work_id, request_key: "diagnostic-graph" }))
45
+ refuse("changed_work_custody");
46
+ }
47
+ return receipt;
48
+ }
49
+ /** The registered handler calls this only after generic Stage entry. Applicability
50
+ * and candidates come from the accepted predecessor, not a caller-owned batch. */
51
+ export function registerDiagnosticStageGraph(context, input) {
52
+ const bundle = readFlowRunExecutionBundle(context, input), project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
53
+ if (!bundle || !isDiagnosticFlow(bundle.manifest.flow_id) || !["audit", "analyze", "review"].includes(input.stage))
54
+ refuse("diagnostic_stage_required");
55
+ const flowId = bundle.manifest.flow_id, frozen = bundle.execution.snapshot().scope;
56
+ const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
57
+ const selection = readDescriptorStageInput(context, scope, bundle), root = readDescriptorRootWork(context, scope, bundle);
58
+ const snapshotSelection = { projectId: project.id, projectRoot: frozen.projectRoot, sourceRoot: frozen.projectRoot, requestId: frozen.requestId, flowId };
59
+ const snapshot = readDiagnosticSnapshot(context, snapshotSelection, input.runId);
60
+ if (!snapshot || snapshot.root !== frozen.workspace)
61
+ refuse("snapshot_required");
62
+ const previousStage = input.stage === "review" ? flowId === "mb-audit" ? "audit" : "analyze" : "preflight";
63
+ if (selection.input.predecessor?.stage !== previousStage)
64
+ refuse("predecessor_required");
65
+ const previousScope = { ...scope, stage: previousStage };
66
+ const accepted = readAcceptedStageData(context, previousScope), previousInput = readDescriptorStageInput(context, previousScope, bundle, true, true);
67
+ const previous = validateDiagnosticStageResult(accepted.bytes, bundle.descriptor.stageBlob(previousStage, "result_schema"), {
68
+ flowId, runId: input.runId, stage: previousStage, inputSha256: previousInput.inputSha256,
69
+ snapshotRoot: diagnosticSnapshotPaths(context, snapshot, input.runId).root, snapshotSha256: snapshot.source_sha256,
70
+ predecessors: previousInput.input.predecessor ? [{ stage: previousInput.input.predecessor.stage,
71
+ path: previousInput.input.predecessor.result.path, sha256: previousInput.input.predecessor.result.sha256 }] : []
72
+ });
73
+ const workInput = { flow_id: flowId, run_id: input.runId, stage: input.stage,
74
+ input_sha256: selection.inputSha256, snapshot_sha256: snapshot.source_sha256,
75
+ instruction: bundle.descriptor.stageBlob(input.stage, "instruction").toString("utf8"),
76
+ coverage: previous.details.coverage, candidates: input.stage === "review" ? previous.details.findings : [],
77
+ support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")).map(ref => ({ path: `execution-bundle/${ref.path}`, sha256: ref.sha256 })) };
78
+ const batch = diagnosticWorkBatch(workInput), batchInput = { parentWorkId: root.workId, stage: input.stage, requestKey: "diagnostic-graph", value: batch };
79
+ const prepared = batch.works.length ? prepareWorkBatch(context, batchInput) : [];
80
+ return context.db.writeTransaction(() => {
81
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
82
+ if (hybridDigest(readDiagnosticSnapshotCustody(context, snapshotSelection, input.runId)) !== hybridDigest(snapshot)
83
+ || hybridDigest(readDescriptorStageInput(context, scope, bundle)) !== hybridDigest(selection)
84
+ || hybridDigest(readAcceptedStageData(context, previousScope)) !== hybridDigest(accepted)
85
+ || readDescriptorRootWork(context, scope, bundle).workId !== root.workId)
86
+ refuse("stage_authority_changed");
87
+ const expected = { schema_id: "dd-flow/diagnostic-work-registration@1", run_id: input.runId, stage: input.stage,
88
+ attempt: selection.attempt, parent_work_id: root.workId, bundle_sha256: bundle.sha256,
89
+ input_sha256: selection.inputSha256, input: workInput, batch_sha256: hybridDigest(batch.works) };
90
+ const prior = readRegisteredDiagnosticGraph(context, scope);
91
+ if (prior) {
92
+ if (hybridDigest({ ...prior, work_ids: undefined }) !== hybridDigest(expected))
93
+ refuse("registration_changed");
94
+ return prior;
95
+ }
96
+ const workIds = batch.works.length ? addWorkBatch(context, { ...batchInput, prepared }).work_ids : {};
97
+ const receipt = { ...expected, work_ids: workIds };
98
+ appendAudit(context, { projectId: project.id, eventType: event, payload: receipt });
99
+ return readRegisteredDiagnosticGraph(context, scope);
100
+ });
101
+ }
@@ -0,0 +1,93 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { verifyDiagnosticSources, verifyDiagnosticCritic } from "./diagnostic-evidence.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ const refuse = (reason) => {
6
+ throw new AppError("diagnostic_work_result_invalid", "Diagnostic Work result does not match its assigned task", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
7
+ };
8
+ /** Accepts semantic data only after the caller has established completed Work,
9
+ * immutable batch custody and physical Session independence. */
10
+ export function validateDiagnosticWorkResult(bytes, schemaBytes, snapshotRoot, input, assignment) {
11
+ if (!bytes.length || bytes.length > 1024 * 1024 || schemaBytes.length > 1024 * 1024)
12
+ refuse("result_exceeds_bound");
13
+ const validate = (() => {
14
+ try {
15
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
16
+ if (schema.$id !== "dd-flow/diagnostic-work-result@1")
17
+ refuse("frozen_schema_invalid");
18
+ return new Ajv2020({ strict: false, validateFormats: false }).compile(schema);
19
+ }
20
+ catch {
21
+ return refuse("frozen_schema_invalid");
22
+ }
23
+ })();
24
+ let result;
25
+ try {
26
+ result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
27
+ }
28
+ catch {
29
+ return refuse("invalid_schema");
30
+ }
31
+ if (!validate(result))
32
+ refuse("invalid_schema");
33
+ if (result.flow_id !== input.flow_id || result.run_id !== input.run_id || result.stage !== input.stage
34
+ || result.work_id !== assignment.workId || result.aspect_id !== assignment.aspectId)
35
+ refuse("foreign_assignment");
36
+ verifyDiagnosticSources(snapshotRoot, result.sources);
37
+ if (assignment.role === "critic") {
38
+ if (input.stage !== "review" || result.findings.length)
39
+ refuse("critic_cannot_replace_candidates");
40
+ verifyDiagnosticCritic(snapshotRoot, input.candidates, result.decisions);
41
+ }
42
+ else {
43
+ if (input.stage === "review" || result.decisions.length
44
+ || !input.coverage.some(value => value.id === assignment.aspectId && value.applicability === "applicable"))
45
+ refuse("foreign_aspect");
46
+ const ids = new Set();
47
+ for (const finding of result.findings) {
48
+ if (ids.has(finding.id) || finding.aspect_id !== assignment.aspectId || !finding.evidence.length)
49
+ refuse("foreign_or_duplicate_finding");
50
+ ids.add(finding.id);
51
+ verifyDiagnosticSources(snapshotRoot, finding.evidence);
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+ /** Work-local IDs are not globally unique. Keep their origin when assigning
57
+ * stable report IDs, so one author's FIND-1 cannot overwrite another's. */
58
+ export function collectDiagnosticCandidates(results) {
59
+ const findings = [], origins = [];
60
+ const seen = new Set();
61
+ for (const [workIndex, item] of results.entries()) {
62
+ if (seen.has(item.workId) || item.result.work_id !== item.workId || item.result.stage === "review")
63
+ refuse("duplicate_or_foreign_work");
64
+ seen.add(item.workId);
65
+ for (const [findingIndex, finding] of item.result.findings.entries()) {
66
+ const id = `FIND-${String(workIndex + 1).padStart(3, "0")}-${String(findingIndex + 1).padStart(3, "0")}`;
67
+ findings.push({ ...structuredClone(finding), id });
68
+ origins.push({ id, work_id: item.workId, original_id: finding.id });
69
+ }
70
+ }
71
+ return { findings, origins };
72
+ }
73
+ /** Assign CLI-owned global IDs only to an exact, ordered copy of admitted Work
74
+ * findings. Any other proposal still goes through the ordinary strict guards. */
75
+ export function sealDiagnosticCandidateIds(bytes, results) {
76
+ if (!bytes.length || bytes.length > 1024 * 1024)
77
+ return bytes;
78
+ let value;
79
+ try {
80
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
81
+ }
82
+ catch {
83
+ return bytes;
84
+ }
85
+ if (!value || !Array.isArray(value.details?.findings)
86
+ || hybridDigest(value.details.findings) !== hybridDigest(results.flatMap(item => item.result.findings)))
87
+ return bytes;
88
+ const canonical = collectDiagnosticCandidates(results).findings;
89
+ if (hybridDigest(value.details.findings) === hybridDigest(canonical))
90
+ return bytes;
91
+ value.details.findings = canonical;
92
+ return Buffer.from(JSON.stringify(value));
93
+ }
@@ -23,12 +23,13 @@ import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-f
23
23
  import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
24
24
  import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
25
25
  import { readSetupStatus } from "./setup.js";
26
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
26
27
  import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
27
28
  /** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
28
29
  export function prepareEvalBootstrapSnapshot(context, input) {
29
30
  let source;
30
31
  if (input.flow !== undefined && input.flow !== "sdlc") {
31
- if (input.flow !== "mb-init" && input.flow !== "mb-upgrade")
32
+ if (input.flow !== "mb-init" && input.flow !== "mb-upgrade" && !isDiagnosticFlow(input.flow))
32
33
  throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
33
34
  const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
34
35
  const selected = readSetupStatus(home);
@@ -70,7 +71,7 @@ export function createEvalBootstrapSnapshot(context, input) {
70
71
  const manifest = {
71
72
  schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
72
73
  stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
73
- ...(source ? { flow: { id: input.flow === "mb-upgrade" ? "mb-upgrade" : "mb-init", source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
74
+ ...(source ? { flow: { id: source.resolveRegisteredFlow().descriptor.id, source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
74
75
  project_root: projectRoot,
75
76
  project_sha256: treeHash(path.join(temporary, "project")),
76
77
  project_git: snapshotProjectGit(projectRoot, temporary)
@@ -222,7 +223,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
222
223
  throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
223
224
  prepareOutputFile(output);
224
225
  prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
225
- prepareSnapshotWorkspace(workspaceRoot, projectRoot);
226
+ prepareSnapshotWorkspace(workspaceRoot, projectRoot, isDiagnosticFlow(status.run.flow_kind));
226
227
  return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
227
228
  }
228
229
  function captureEvalRunSnapshot(context, input) {
@@ -240,7 +241,7 @@ function captureEvalRunSnapshot(context, input) {
240
241
  }
241
242
  }
242
243
  else
243
- copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome), input.observationCuts);
244
+ copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions, ...(isDiagnosticFlow(status.run.flow_kind) ? [workspaceRoot] : [])], grokProviderHomes(context.ddFlowHome), input.observationCuts);
244
245
  for (const [file, cut] of input.journalCuts ?? []) {
245
246
  const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
246
247
  fs.mkdirSync(path.dirname(target), { recursive: true });
@@ -280,7 +281,7 @@ function captureEvalRunSnapshot(context, input) {
280
281
  filter: projectPayloadFilter(projectRoot)
281
282
  });
282
283
  const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
283
- const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
284
+ const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output, isDiagnosticFlow(status.run.flow_kind));
284
285
  // Accepted Work results are captured in SQLite. Their file representations
285
286
  // are retained evidence, not a second authority or a checkpoint gate.
286
287
  if (recovery) {
@@ -293,7 +294,7 @@ function captureEvalRunSnapshot(context, input) {
293
294
  }
294
295
  }
295
296
  const manifest = {
296
- schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
297
+ schema_id: workspace.kind === "git_repository" ? "dd-flow/eval-run-snapshot@7" : projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
297
298
  run_id: input.runId,
298
299
  project_id: project.id,
299
300
  project_root: projectRoot,
@@ -310,7 +311,7 @@ function captureEvalRunSnapshot(context, input) {
310
311
  ...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
311
312
  project_git: projectGit,
312
313
  workspace,
313
- payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind === "git_worktree" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
314
+ payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind !== "project" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
314
315
  source_status: status
315
316
  };
316
317
  writeJson(path.join(output, "snapshot.json"), manifest);
@@ -436,7 +437,7 @@ export function prepareSnapshotOperationalDecision(input) {
436
437
  }
437
438
  function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
438
439
  assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
439
- if (manifest.workspace.kind === "git_worktree") {
440
+ if (manifest.workspace.kind !== "project") {
440
441
  const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
441
442
  assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
442
443
  if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
@@ -760,27 +761,29 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
760
761
  function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
761
762
  if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
762
763
  return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
763
- function prepareSnapshotWorkspace(workspaceRoot, projectRoot) {
764
+ function prepareSnapshotWorkspace(workspaceRoot, projectRoot, allowUnborn = false) {
764
765
  if (samePath(workspaceRoot, projectRoot))
765
766
  return null;
766
767
  const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
767
768
  if (!branch)
768
769
  throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
769
770
  const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
770
- if (!baseRef)
771
+ if (!baseRef && !(allowUnborn && git(workspaceRoot, ['for-each-ref', '--format=%(refname)']) === ''))
771
772
  throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
772
773
  return { branch, baseRef };
773
774
  }
774
- function snapshotWorkspace(workspaceRoot, projectRoot, output) {
775
- const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot);
775
+ function snapshotWorkspace(workspaceRoot, projectRoot, output, independent = false) {
776
+ const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot, independent);
776
777
  if (!prepared)
777
778
  return { kind: "project" };
778
779
  const { branch, baseRef } = prepared;
779
780
  const workspace = path.join(output, "workspace");
780
781
  const bundle = path.join(output, "workspace.bundle");
781
782
  fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
782
- runGit(projectRoot, ["bundle", "create", bundle, branch]);
783
- return { kind: "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
783
+ if (baseRef === null)
784
+ return { kind: 'git_repository', source_root: workspaceRoot, branch, base_ref: null, bundle_sha256: null, state: 'unborn', sha256: treeHash(workspace), ...snapshotIndex(workspaceRoot, output, 'workspace') };
785
+ runGit(independent ? workspaceRoot : projectRoot, ["bundle", "create", bundle, branch]);
786
+ return { kind: independent ? "git_repository" : "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
784
787
  }
785
788
  function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
786
789
  if (manifest.workspace.kind === "project")
@@ -789,15 +792,28 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
789
792
  const bundle = path.join(snapshot, "workspace.bundle");
790
793
  if (treeHash(source) !== manifest.workspace.sha256)
791
794
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
792
- if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
795
+ if (manifest.workspace.state === 'unborn' ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
793
796
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
794
797
  const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
795
798
  if (fs.existsSync(workspaceRoot))
796
799
  throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
797
- runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
798
- if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
799
- throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
800
- runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
800
+ if (manifest.workspace.kind === 'git_repository') {
801
+ fs.mkdirSync(path.dirname(workspaceRoot), { recursive: true });
802
+ if (manifest.workspace.state === 'unborn') {
803
+ fs.mkdirSync(workspaceRoot);
804
+ runGit(workspaceRoot, ['-c', 'init.templateDir=', 'init', '--initial-branch', manifest.workspace.branch]);
805
+ }
806
+ else
807
+ runGit(projectRoot, ['clone', '--no-checkout', '--branch', manifest.workspace.branch, bundle, workspaceRoot]);
808
+ }
809
+ else {
810
+ runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
811
+ if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
812
+ throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
813
+ runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
814
+ }
815
+ if (git(workspaceRoot, ["rev-parse", "HEAD"]) !== manifest.workspace.base_ref)
816
+ throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen workspace base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
801
817
  replaceProjectTree(workspaceRoot, source);
802
818
  restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
803
819
  return fs.realpathSync(workspaceRoot);
@@ -847,7 +863,7 @@ function restoreProjectGit(snapshot, source, projectRoot) {
847
863
  restoreIndex(snapshot, "project", source, projectRoot);
848
864
  }
849
865
  const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
850
- function indexEntries(root) {
866
+ function indexEntries(root, includeExclude = true) {
851
867
  const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
852
868
  .toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
853
869
  .map(entry => `${entry}\0`).join(""));
@@ -860,7 +876,22 @@ function indexEntries(root) {
860
876
  if (fs.existsSync(resolved))
861
877
  merge[name] = fs.readFileSync(resolved).toString("base64");
862
878
  }
863
- return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
879
+ if (!includeExclude)
880
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
881
+ const excludePath = git(root, ["rev-parse", "--git-path", "info/exclude"]);
882
+ if (!excludePath)
883
+ throw new AppError("snapshot_index_invalid", "Could not locate Git local excludes", 1);
884
+ const excludeFile = path.resolve(root, excludePath);
885
+ let exclude = null;
886
+ if (fs.existsSync(excludeFile)) {
887
+ const stat = fs.lstatSync(excludeFile);
888
+ if (!stat.isFile() || stat.size > 1024 * 1024)
889
+ throw new AppError("snapshot_index_invalid", "Git local excludes must be a bounded regular file", 1);
890
+ exclude = fs.readFileSync(excludeFile).toString("base64");
891
+ }
892
+ // Local excludes affect review-visible inputs, but are not part of a Git
893
+ // bundle/index. Keep them in the existing hashed, non-executable Git payload.
894
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge, exclude }));
864
895
  }
865
896
  function snapshotIndex(root, output, name) {
866
897
  const bytes = indexEntries(root);
@@ -911,6 +942,8 @@ function restoreIndex(snapshot, name, metadata, root) {
911
942
  if (!metadata.index_objects_sha256 || fileHash(pack) !== metadata.index_objects_sha256)
912
943
  throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index object checksum does not match", 1);
913
944
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
945
+ if (value.exclude !== undefined && value.exclude !== null && (typeof value.exclude !== "string" || Buffer.byteLength(value.exclude) > 1400000))
946
+ throw new AppError("snapshot_index_invalid", "Snapshot Git local excludes are invalid", 1);
914
947
  gitBytes(root, ["unpack-objects"], fs.readFileSync(pack));
915
948
  runGit(root, ["read-tree", "--empty"]);
916
949
  gitBytes(root, ["update-index", "-z", "--index-info"], Buffer.from(value.entries, "base64"));
@@ -923,18 +956,36 @@ function restoreIndex(snapshot, name, metadata, root) {
923
956
  else
924
957
  fs.rmSync(path.resolve(root, target), { force: true });
925
958
  }
959
+ if (value.exclude !== undefined) {
960
+ const target = git(root, ["rev-parse", "--git-path", "info/exclude"]);
961
+ if (!target)
962
+ throw new AppError("snapshot_index_invalid", "Could not locate restored Git local excludes", 1);
963
+ const destination = path.resolve(root, target);
964
+ if (fs.existsSync(destination) && !fs.lstatSync(destination).isFile())
965
+ throw new AppError("snapshot_index_invalid", "Restored Git local excludes are not a regular file", 1);
966
+ if (value.exclude === null)
967
+ fs.rmSync(destination, { force: true });
968
+ else {
969
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
970
+ fs.writeFileSync(destination, Buffer.from(value.exclude, "base64"));
971
+ }
972
+ }
926
973
  return;
927
974
  }
928
975
  if (fs.statSync(file).size)
929
976
  runGit(root, ["apply", "--cached", "--binary", file]);
930
977
  }
978
+ function capturedIndexEntries(snapshot, name, root) {
979
+ const captured = JSON.parse(fs.readFileSync(path.join(snapshot, `${name}.index.patch`), "utf8"));
980
+ return indexEntries(root, Object.hasOwn(captured, "exclude"));
981
+ }
931
982
  export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true) {
932
983
  const manifest = readManifest(snapshot);
933
984
  if (manifest.purpose !== "recovery" || manifest.recovery_id !== input.recoveryId || manifest.run_id !== input.runId)
934
985
  throw new AppError("recovery_capture_invalid", "Snapshot does not identify this recovery", 1);
935
986
  const entries = [
936
987
  { name: "project", root: input.projectRoot, hash: manifest.project_sha256, git: manifest.project_git, head: manifest.project_git.head },
937
- ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
988
+ ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
938
989
  ];
939
990
  verifySnapshotPayload(snapshot, manifest);
940
991
  for (const entry of entries) {
@@ -944,7 +995,7 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
944
995
  throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
945
996
  if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
946
997
  || entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
947
- || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
998
+ || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? capturedIndexEntries(snapshot, entry.name, entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
948
999
  throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
949
1000
  }
950
1001
  }
@@ -964,7 +1015,7 @@ function verifySnapshotPayload(snapshot, manifest) {
964
1015
  if (treeHash(path.join(snapshot, "runtime")) !== manifest.runtime_sha256)
965
1016
  throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
966
1017
  verifySnapshotModes(snapshot, manifest);
967
- const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
1018
+ const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
968
1019
  for (const payload of payloads)
969
1020
  verifyGitSnapshotPayload(snapshot, payload);
970
1021
  if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
@@ -1433,7 +1484,7 @@ function treeModesHash(root, filtered = false) {
1433
1484
  function verifySnapshotModes(snapshot, manifest) {
1434
1485
  if (!manifest.payload_modes)
1435
1486
  return; // Legacy snapshots did not attest modes.
1436
- for (const name of ["project", "runtime", ...(manifest.workspace.kind === "git_worktree" ? ["workspace"] : [])]) {
1487
+ for (const name of ["project", "runtime", ...(manifest.workspace.kind !== "project" ? ["workspace"] : [])]) {
1437
1488
  if (treeModesHash(path.join(snapshot, name)) !== manifest.payload_modes[name])
1438
1489
  throw new AppError("snapshot_checksum_mismatch", "Snapshot executable modes do not match manifest", 1, { payload: name });
1439
1490
  }
@@ -1465,7 +1516,7 @@ function readManifest(snapshot) {
1465
1516
  if (!fs.existsSync(file))
1466
1517
  throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
1467
1518
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1468
- if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1519
+ if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6", "dd-flow/eval-run-snapshot@7"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1469
1520
  throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
1470
1521
  if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
1471
1522
  throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
@@ -1480,8 +1531,13 @@ function readManifest(snapshot) {
1480
1531
  || spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
1481
1532
  : value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
1482
1533
  throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
1483
- if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
1484
- throw new AppError("snapshot_invalid", "Snapshot worktree manifest is incomplete", 1, { snapshot });
1534
+ if (!['project', 'git_worktree', 'git_repository'].includes(value.workspace.kind)
1535
+ || (value.schema_id === 'dd-flow/eval-run-snapshot@7') !== (value.workspace.kind === 'git_repository')
1536
+ || value.workspace.kind !== 'project' && (!value.workspace.branch || (value.workspace.state === 'unborn'
1537
+ ? value.workspace.kind !== 'git_repository' || value.workspace.base_ref !== null || value.workspace.bundle_sha256 !== null
1538
+ || value.workspace.index_format !== 'entries@1' || !value.workspace.index_sha256 || !value.workspace.index_objects_sha256
1539
+ : value.workspace.state !== undefined || !value.workspace.base_ref || !value.workspace.bundle_sha256)))
1540
+ throw new AppError("snapshot_invalid", "Snapshot workspace manifest is incomplete or incompatible", 1, { snapshot });
1485
1541
  return value;
1486
1542
  }
1487
1543
  function readBootstrapManifest(snapshot) {
@@ -1490,7 +1546,7 @@ function readBootstrapManifest(snapshot) {
1490
1546
  throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
1491
1547
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1492
1548
  const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
1493
- : value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade") && value.stage === "preflight"
1549
+ : value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade" || isDiagnosticFlow(value.flow?.id)) && value.stage === "preflight"
1494
1550
  && Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
1495
1551
  && /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
1496
1552
  if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
@@ -9,6 +9,7 @@ import { FrozenGenericExecution } from "./generic-execution.js";
9
9
  import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
10
10
  import { readBoundedReleaseFile } from "./release-source.js";
11
11
  import { acquireInstallationLock } from "./installation-lock.js";
12
+ import { projectPacketLocators } from "./stage-context.js";
12
13
  const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
13
14
  const limit = 1024 * 1024;
14
15
  function refuse(reason, issues) {
@@ -54,7 +55,8 @@ export class RetainedExecutionBundle {
54
55
  #blobs;
55
56
  #admission;
56
57
  #engine;
57
- constructor(bytes, manifest, blobs, admission, descriptor, execution) {
58
+ #engineRelocation;
59
+ constructor(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation) {
58
60
  frozen(manifest);
59
61
  this.manifest = manifest;
60
62
  this.sha256 = hash(bytes);
@@ -63,9 +65,10 @@ export class RetainedExecutionBundle {
63
65
  this.#admission = admission;
64
66
  this.descriptor = descriptor;
65
67
  this.execution = execution;
68
+ this.#engineRelocation = engineRelocation && structuredClone(engineRelocation);
66
69
  Object.freeze(this);
67
70
  }
68
- static admit(input, expectedSha256, sourceBlobs, selected) {
71
+ static admit(input, expectedSha256, sourceBlobs, selected, engineRelocation) {
69
72
  if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
70
73
  refuse("invalid_bundle_identity");
71
74
  const bytes = Buffer.from(input);
@@ -113,12 +116,12 @@ export class RetainedExecutionBundle {
113
116
  const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
114
117
  settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
115
118
  const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
116
- const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
119
+ const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation);
117
120
  admitted.engine();
118
121
  return admitted;
119
122
  }
120
123
  /** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
121
- static read(root, expectedSha256, admission) {
124
+ static read(root, expectedSha256, admission, engineRelocation) {
122
125
  try {
123
126
  directory(root);
124
127
  const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
@@ -130,7 +133,7 @@ export class RetainedExecutionBundle {
130
133
  const manifest = value, blobs = new Map();
131
134
  for (const ref of manifest.inventory)
132
135
  blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
133
- return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
136
+ return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission, engineRelocation);
134
137
  }
135
138
  catch (error) {
136
139
  if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
@@ -153,8 +156,17 @@ export class RetainedExecutionBundle {
153
156
  }
154
157
  #admitEngine() {
155
158
  const binding = json(this.blob(this.manifest.engine_binding.path));
156
- const engine = AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
157
- if (!engine.manifestBytes().equals(this.blob(this.manifest.engine_manifest.path)))
159
+ const retainedManifest = this.blob(this.manifest.engine_manifest.path), relocation = this.#engineRelocation;
160
+ if (relocation) {
161
+ const root = JSON.parse(projectPacketLocators(JSON.stringify({ root: binding.engine.snapshot_root }), relocation.relocations)).root;
162
+ if (!relocation.relocations.length || root === binding.engine.snapshot_root || root !== relocation.binding.engine.snapshot_root
163
+ || relocation.binding.run_id !== this.manifest.run_id)
164
+ refuse("foreign_engine_relocation");
165
+ }
166
+ const engine = relocation
167
+ ? AdmittedFrozenEngine.admitRelocation(binding, retainedManifest, this.manifest.engine_manifest.sha256, relocation.binding)
168
+ : AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
169
+ if (!relocation && !engine.manifestBytes().equals(retainedManifest))
158
170
  refuse("engine_manifest_bytes_changed");
159
171
  const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
160
172
  for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {
@@ -1,6 +1,7 @@
1
1
  import { vnextStages } from "../domain/stage-catalog.js";
2
2
  import { sdlcHandlerIdentities, memoryInitHandlerIdentities, memoryUpgradeHandlerIdentities } from "../domain/stage-handler-identities.js";
3
3
  import { AppError } from "../shared/errors.js";
4
+ import { diagnosticFlowStages } from "../domain/diagnostic-flows.js";
4
5
  /** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
5
6
  export function listBundledFlows() {
6
7
  return [
@@ -14,7 +15,11 @@ export function listBundledFlows() {
14
15
  stages: stages.map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
15
16
  edges: stages.slice(0, -1).flatMap((handler, index) => [{ from: handler.stage, to: stages[index + 1].stage, on: "accepted" },
16
17
  ...(handler.stage === "verify" ? [{ from: handler.stage, to: "review", on: "needs_repair" }] : [])]) };
17
- })
18
+ }),
19
+ ...Object.entries(diagnosticFlowStages).map(([id, stages]) => ({ id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["report"],
20
+ stages: stages.map((stage, index) => ({ id: stage, directory: `${String(index + 1).padStart(2, "0")}-${stage}`,
21
+ entry_handler: "diagnostic-stage-entry", finish_handler: "diagnostic-stage-finish" })),
22
+ edges: stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" })) }))
18
23
  ];
19
24
  }
20
25
  export function showBundledFlow(id) {