@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
@@ -12,7 +12,8 @@ import { requireProjectByRoot } from "./projects.js";
12
12
  import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
13
13
  import { runtimeCommand } from "./runtime-command.js";
14
14
  import { promptDeliveryInstruction } from "./prompt-delivery.js";
15
- function semanticCompletionPrompt(stage, completed) {
15
+ import { currentStageFinishCommand } from "./lifecycle-invocations.js";
16
+ function semanticCompletionPrompt(stage, completed, command) {
16
17
  const task = stage === "plan-review"
17
18
  ? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
18
19
  : stage === "code-review"
@@ -21,7 +22,7 @@ function semanticCompletionPrompt(stage, completed) {
21
22
  return [
22
23
  `All declared ${stage} child Work has settled (${completed} completed).`,
23
24
  task,
24
- "Then invoke the exact Stage finish command from the original authoritative packet.",
25
+ `Then invoke this exact currently issued Stage finish command:\n\n${command}\n\nRefresh the authoritative verification evidence first; an earlier rejected or successful nonterminal command only reads its retained outcome.`,
25
26
  "If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
26
27
  "Do not create speculative repair Work or start a successor Stage."
27
28
  ].join(" ");
@@ -37,10 +38,13 @@ export function controllerStageEntryPrompt(stage, command) {
37
38
  stageLifecycleInstruction
38
39
  ].join("\n\n");
39
40
  }
40
- export function controllerStageResumePrompt(pauseId, command) {
41
+ export function controllerStageResumePrompt(packet, resumedPromptPath) {
41
42
  return [
42
- `The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
43
- command,
43
+ `The accepted canonical answer for pause ${packet.pause_id} (${packet.stage}, ${packet.work_id}) is retained at this absolute read-only path: ${JSON.stringify(packet.answer_file)} (sha256 ${packet.answer_sha256}). Do not replace it with a placeholder or ask the user to supply it again. A null pause.answer_path before resume does not mean the answer is missing.`,
44
+ resumedPromptPath
45
+ ? `This exact answer was already committed. Do not invoke stage resume again. Read the recovered authoritative continuation at ${JSON.stringify(resumedPromptPath)} and perform its current Stage instructions.`
46
+ : `Only while this exact pause still awaits its answer, your first tool call must be this exact standalone command. It applies the already accepted bound answer file:\n\n${packet.command}`,
47
+ ...(packet.resume_source_path ? [`If this pause has already resumed, do not invoke stage resume again. Read the complete recovered result.worker_prompt_markdown in ${JSON.stringify(packet.resume_source_path)} instead, and follow that authoritative continuation. The controller restores this committed packet before dispatch. A different pause requires its own accepted answer.`] : []),
44
48
  `Do not alter the answer. ${promptDeliveryInstruction} Follow the returned packet, then stop at this Stage boundary.`,
45
49
  stageLifecycleInstruction
46
50
  ].join("\n\n");
@@ -205,7 +209,10 @@ export async function nextControllerFanout(context, input) {
205
209
  ].join("\n\n"));
206
210
  }
207
211
  }
208
- return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
212
+ const finish = currentStageFinishCommand(context, input.stage);
213
+ if (!finish)
214
+ throw new AppError("controller_finish_authority_missing", "Settled child Work has no current issued Stage finish; reconcile lifecycle authority", 1, { stage: input.stage });
215
+ return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed, finish));
209
216
  }
210
217
  /** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
211
218
  export function controllerNativeChildren(receipt, rootSessionId) {
@@ -6,6 +6,7 @@ import { launchExternalWork } from "./external-work-launch.js";
6
6
  import { requireProjectByRoot } from "./projects.js";
7
7
  import { assertControllerLeaseAllowed } from "./run-controller-process.js";
8
8
  import { assertManagedProcessLease } from "./managed-processes.js";
9
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
9
10
  if (!parentPort)
10
11
  throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
11
12
  const port = parentPort, cancellation = new AbortController();
@@ -18,8 +19,8 @@ try {
18
19
  context = createContext(env, "prepared_existing");
19
20
  const project = requireProjectByRoot(context, task.input.projectRoot);
20
21
  const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
21
- if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
22
- throw new AppError("controller_memory_task_invalid", "Memory worker requires the selected memory RUN", 1, { effect: "no_effect" });
22
+ if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind) && !isDiagnosticFlow(run.flow_kind))
23
+ throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory or diagnostic RUN", 1, { effect: "no_effect" });
23
24
  if (task.operation === "external") {
24
25
  const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
25
26
  if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
@@ -216,10 +216,10 @@ export function autoRefreshDashboards(context, input) {
216
216
  if (projectRecoveryGuarded(context, project.id))
217
217
  return { ok: true, skipped: true, reason: "run_recovery_guarded" };
218
218
  const configured = readProjectConfig(context, project.id);
219
- // Automatic observation must not change either memory flow's source baseline.
219
+ // Automatic observation must not change a memory or diagnostic source baseline.
220
220
  // Keep projections in Home; do not hide runtime-created paths in Git or
221
221
  // weaken source preservation by excluding user-owned .tasks directories.
222
- const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
222
+ const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade','mb-audit','project-review') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
223
223
  const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
224
224
  markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
225
225
  if (!config.dashboard.auto_refresh) {
@@ -62,17 +62,19 @@ export class PreparedDescriptorRunSettlement {
62
62
  const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
63
63
  const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
64
64
  const snapshot = context.db.readSnapshot(() => ({ owner: selected(context, scope, bundle, matchKeys), generation: assertRunMutationAllowed(context, scope.projectId, scope.runId) }));
65
- const git = snapshot.owner.prior?.git ?? gitFacts(bundle.execution.snapshot().scope.workspace);
65
+ const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
66
+ const git = snapshot.owner.prior?.git ?? gitFacts(operational.workspace_root ?? operational.project_root);
66
67
  return new PreparedDescriptorRunSettlement(scope, bundle, snapshot.owner, git, snapshot.generation, matchKeys);
67
68
  }
68
69
  apply(context, assertHandlerCustody) {
69
70
  if (typeof assertHandlerCustody !== "function")
70
71
  refuse("handler_fence_required");
71
- const bundle = readFlowRunExecutionBundle(context, { projectRoot: this.#bundle.execution.snapshot().scope.projectRoot, runId: this.#scope.runId });
72
+ const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
73
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
72
74
  if (!bundle || bundle.sha256 !== this.#bundle.sha256)
73
75
  refuse("bundle_changed");
74
76
  const observed = context.db.readSnapshot(() => selected(context, this.#scope, this.#bundle, this.#matchKeys));
75
- if (!observed.prior && hybridDigest(gitFacts(this.#bundle.execution.snapshot().scope.workspace)) !== hybridDigest(this.#git))
77
+ if (!observed.prior && hybridDigest(gitFacts(operational.workspace_root ?? operational.project_root)) !== hybridDigest(this.#git))
76
78
  refuse("git_observation_changed");
77
79
  const receipt = { run_id: this.#scope.runId, work_id: this.#owner.rootId, binding: this.#owner.binding, git: this.#git };
78
80
  const changed = withStageSettlement(context, () => {
@@ -20,6 +20,8 @@ import { allocateDescriptorRootWork, readDescriptorRootWork } from "./descriptor
20
20
  import { nextRunId } from "./ids.js";
21
21
  import { readMemoryInitRunAllocation } from "./memory-init-run-allocation.js";
22
22
  import { readMemoryUpgradeEvidence, readMemoryUpgradeTargetEvidence, upgradeEvidenceFilePath, upgradeEvidenceIndexPath, upgradeTargetIndexPath } from "./memory-upgrade-evidence.js";
23
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
24
+ import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
23
25
  const encode = (value) => Buffer.from(JSON.stringify(value));
24
26
  const owner = (directory) => {
25
27
  const stat = fs.lstatSync(directory);
@@ -91,6 +93,13 @@ function memoryBootstrap(context, projectId, bundle) {
91
93
  if (bundle.descriptor.descriptor.id === "sdlc")
92
94
  return;
93
95
  const scope = bundle.execution.snapshot().scope;
96
+ if (isDiagnosticFlow(bundle.manifest.flow_id)) {
97
+ const snapshot = readDiagnosticSnapshotCustody(context, { projectId, projectRoot: scope.projectRoot,
98
+ sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
99
+ if (!snapshot || snapshot.root !== scope.workspace || bundle.manifest.bootstrap_receipts.length !== 0)
100
+ refuseRunStart("diagnostic_snapshot_authority_changed");
101
+ return;
102
+ }
94
103
  if (bundle.descriptor.descriptor.id === "mb-init") {
95
104
  const retained = readMemoryInitBootstrapReadiness(context, { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace });
96
105
  const reference = bundle.manifest.bootstrap_receipts[0];
@@ -113,6 +122,16 @@ function memoryBootstrap(context, projectId, bundle) {
113
122
  || !bundle.blob(reference.path).equals(encode(retained.authority.receipt)))
114
123
  refuseRunStart("memory_bootstrap_authority_changed");
115
124
  }
125
+ function observeRunWorkspace(context, bundle, projectId) {
126
+ const scope = bundle.execution.snapshot().scope;
127
+ if (!isDiagnosticFlow(bundle.manifest.flow_id))
128
+ return observeDescriptorWorkspace(context, scope.projectRoot, scope.workspace);
129
+ const snapshot = readDiagnosticSnapshot(context, { projectId, projectRoot: scope.projectRoot,
130
+ sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
131
+ if (!snapshot || snapshot.root !== scope.workspace)
132
+ refuseRunStart("diagnostic_snapshot_authority_changed");
133
+ return observeDescriptorWorkspace(context, snapshot.root, snapshot.root);
134
+ }
116
135
  /** RUN allocation and SQL bundle authority are one request-owned transaction.
117
136
  * Physical publication is separate, replayable and gates lifecycle writes.
118
137
  * This does not register missing handlers or qualify a provider/storage route.
@@ -164,11 +183,11 @@ export class PreparedDescriptorRunStart {
164
183
  const home = fs.realpathSync(context.ddFlowHome), runRoot = projectRunHome(home, selected.id, bundle.manifest.run_id);
165
184
  if (fs.existsSync(runRoot))
166
185
  refuseRunStart("foreign_run_root");
167
- const facts = observeDescriptorWorkspace(context, scope.projectRoot, scope.workspace), now = context.now(), artifacts = [
186
+ const facts = observeRunWorkspace(context, bundle, selected.id), now = context.now(), artifacts = [
168
187
  { path: "engine-binding.json", base64: bundle.blob(bundle.manifest.engine_binding.path).toString("base64") }
169
188
  ];
170
189
  const isSdlc = bundle.descriptor.descriptor.id === "sdlc", flowKind = isSdlc ? "vnext_specify" : bundle.descriptor.descriptor.id;
171
- if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind))
190
+ if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind) && !isDiagnosticFlow(flowKind))
172
191
  refuseRunStart("unsupported_flow_kind");
173
192
  if (flowKind === "mb-upgrade") {
174
193
  const upgradeScope = { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace };
@@ -250,7 +269,7 @@ export class PreparedDescriptorRunStart {
250
269
  revalidatePhysical(context) {
251
270
  this.revalidateDirectoryOwners(context);
252
271
  const p = this.#payload;
253
- if (hybridDigest(observeDescriptorWorkspace(context, p.project_root, p.workspace_root)) !== hybridDigest(p.git))
272
+ if (hybridDigest(observeRunWorkspace(context, this.#bundle, p.project_id)) !== hybridDigest(p.git))
254
273
  refuseRunStart("physical_scope_changed");
255
274
  this.#bundle.engine();
256
275
  }
@@ -8,11 +8,14 @@ import { hybridDigest } from "./hybrid-contracts.js";
8
8
  import { readDescriptorRootWork } from "./descriptor-root-work.js";
9
9
  import { bindStageWork, stageWorkIds } from "./stage-work-graph.js";
10
10
  import { appendAudit } from "./audit.js";
11
- import { acceptStagePacket, prepareStagePacket, publishStagePacket } from "./stage-report-renderer.js";
11
+ import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
12
12
  import { assertRunMutationAllowed } from "./run-recovery-guard.js";
13
13
  import { assertStageSettlementAllowed, PreparedRecoveryCoordinatorRead } from "./run-recovery.js";
14
14
  import { loadExternalStageContextBytes, renderExternalStageContext } from "./stage-context.js";
15
15
  import { runtimeCommand } from "./runtime-command.js";
16
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
17
+ import { diagnosticAspects } from "../domain/diagnostic-aspects.js";
18
+ import { readDiagnosticSnapshot } from "./diagnostic-snapshot.js";
16
19
  const event = "flow_run.descriptor_coordinator";
17
20
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
18
21
  function refuse(reason) {
@@ -45,7 +48,18 @@ function readDescriptorStageInputCustody(context, scope, bundle, run, settlement
45
48
  || typeof stage.attempt !== "string" || typeof stage.input_sha256 !== "string")
46
49
  refuse("inactive_stage");
47
50
  const file = path.join(run.run_root, declared.directory, "stage-input.json");
48
- const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.file')=? LIMIT 2", [scope.projectId, scope.runId, scope.stage, file]);
51
+ // Imported receipts retain their original locators and exact input bytes.
52
+ // Match their current publication through trusted RUN lineage, as accepted
53
+ // result readers do, rather than requiring rewritten historical SQL.
54
+ const candidates = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, stage.attempt]);
55
+ if (candidates.length > 32)
56
+ refuse("input_custody_missing");
57
+ const rows = candidates.filter(row => {
58
+ if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
59
+ refuse("input_custody_changed");
60
+ const candidate = JSON.parse(row.payload_json);
61
+ return typeof candidate?.file === "string" && resolveStagePublicationPath(context, scope.projectId, scope.runId, candidate.file) === file;
62
+ });
49
63
  if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
50
64
  refuse("input_custody_missing");
51
65
  const packet = JSON.parse(rows[0].payload_json), content = Buffer.from(packet.base64, "base64");
@@ -58,7 +72,8 @@ function readDescriptorStageInputCustody(context, scope, bundle, run, settlement
58
72
  || input.descriptor_sha256 !== bundle.descriptor.sha256 || input.execution_sha256 !== bundle.execution.sha256)
59
73
  refuse("foreign_input");
60
74
  const starts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.descriptor_stage_start' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, scope.stage]);
61
- if (starts.length !== 1 || hybridDigest(JSON.parse(starts[0].payload_json)) !== hybridDigest({ schema_id: "dd-flow/descriptor-stage-start@1", run_id: scope.runId, stage: scope.stage, attempt: stage.attempt, bundle_sha256: bundle.sha256, input_sha256: stage.input_sha256, file }))
75
+ const start = starts.length === 1 ? JSON.parse(starts[0].payload_json) : null;
76
+ if (!start || typeof start.file !== "string" || hybridDigest({ ...start, file: resolveStagePublicationPath(context, scope.projectId, scope.runId, start.file) }) !== hybridDigest({ schema_id: "dd-flow/descriptor-stage-start@1", run_id: scope.runId, stage: scope.stage, attempt: stage.attempt, bundle_sha256: bundle.sha256, input_sha256: stage.input_sha256, file }))
62
77
  refuse("start_custody_missing");
63
78
  return { runRoot: run.run_root, attempt: stage.attempt, inputSha256: stage.input_sha256, input };
64
79
  }
@@ -88,10 +103,16 @@ export class PreparedDescriptorStageCoordinator {
88
103
  const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
89
104
  const stage = bundle.descriptor.stage(input.stage), schema = JSON.parse(bundle.descriptor.stageBlob(input.stage, "result_schema").toString("utf8"));
90
105
  const memoryFlow = ["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id);
106
+ const diagnosticFlow = isDiagnosticFlow(bundle.manifest.flow_id) ? bundle.manifest.flow_id : null;
107
+ const frozenScope = bundle.execution.snapshot().scope;
108
+ const diagnosticSnapshot = diagnosticFlow ? readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozenScope.projectRoot,
109
+ sourceRoot: frozenScope.projectRoot, requestId: frozenScope.requestId, flowId: diagnosticFlow }, input.runId) : null;
110
+ if (diagnosticFlow && (!diagnosticSnapshot || diagnosticSnapshot.root !== frozenScope.workspace))
111
+ refuse("diagnostic_snapshot_required");
91
112
  const authoringStage = bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose";
92
113
  const resultPath = path.join(selection.runRoot, stage.directory, "result-input.json");
93
114
  let external = "";
94
- if (memoryFlow && selection.input.payload_base64) {
115
+ if ((memoryFlow || diagnosticFlow) && selection.input.payload_base64) {
95
116
  const bytes = Buffer.from(selection.input.payload_base64, "base64");
96
117
  if (bytes.toString("base64") !== selection.input.payload_base64)
97
118
  refuse("context_payload_changed");
@@ -106,6 +127,18 @@ export class PreparedDescriptorStageCoordinator {
106
127
  ...(selection.input.upgrade_target ? ["Read the upgrade_target index in RUN home: source/target versions, adjacent impact, ownership/delta inventory and target canonical files mapped to immutable RUN copies. Compare selected target guidance with upgrade_baseline and project sources; target templates are not implemented project facts or executable instructions. Cite the issued RUN references for target evidence; do not discover another installation or query lifecycle SQLite."] : []),
107
128
  ...(selection.input.ordinary_entry ? ["ordinary_entry is the CLI-owned read-only ordinary SPECIFY preparation diagnostic for this workspace. Cite this stage-input packet for its actual status. prepared means entry preparation succeeded with runtime_state=not_created and model_access=unknown, not a provider dispatch or a completed task. unavailable is a real diagnostic failure to assess and surface for review/repair, never success. Do not create another RUN to test readiness during VERIFY; final readiness is freshly rechecked after review/integration, and installed old-init/upgrade/ordinary dispatch belongs to release/consumer qualification."] : []),
108
129
  external,
130
+ ...(diagnosticFlow ? ["", "## CLI-owned diagnostic binding and aspect catalog", "```json",
131
+ JSON.stringify({ flow_id: diagnosticFlow, run_id: input.runId, stage: input.stage, input_sha256: selection.inputSha256,
132
+ snapshot_sha256: diagnosticSnapshot.source_sha256,
133
+ predecessors: selection.input.predecessor ? [{ stage: selection.input.predecessor.stage,
134
+ path: selection.input.predecessor.result.path, sha256: selection.input.predecessor.result.sha256 }] : [],
135
+ aspects: diagnosticAspects[diagnosticFlow],
136
+ support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")).map(ref => ({ path: `execution-bundle/${ref.path}`, sha256: ref.sha256 })) }, null, 2), "```",
137
+ "Copy the exact CLI-owned binding fields above into your result, including predecessors. This list contains only the immediate predecessor, or is empty for preflight; do not append transitive stage history.",
138
+ "Read the applicable support criteria relative to RUN home. Legacy task/subagent snippets in those reference files do not authorize manual lifecycle, repair or dispatch. Project facts must come from the assigned snapshot, not the canon criteria.",
139
+ "Every source/evidence path in the result is relative to the assigned snapshot workspace root, with forward slashes: src/index.ts or .memory-bank/index.md, never an absolute filesystem path. Hash the exact snapshot file bytes. Intake transport files, this RUN's packets and canonical support references are not snapshot sources: use the frozen input to interpret the request, but do not insert them into sources or evidence. Do not fabricate a snapshot-relative spelling for a file outside the snapshot.",
140
+ "Files under .dd-flow/review-evidence are CLI-generated observations, not original project sources: do not put them in sources/evidence or infer the original repository's history or unborn state from the working copy's Git metadata. For missing linked targets cite the existing referring project document and describe the absence as a limitation, not as a source file.",
141
+ "Every baseline aspect needs an explicit applicability row. The runtime registers applicable analytical Work and independent critics from the accepted predecessor; do not allocate or dispatch your own Work. Do not edit either the original project or snapshot. Read completed assigned Work results before proposing analysis/review. Keep all candidates, rejected findings, critic disagreement and limitations; a completed diagnostic is not a claim that the project is clean."] : []),
109
142
  ...(selection.input.ordinary_defaults ? ["", "## Ordinary SDLC technical defaults", "Use ordinary_defaults.proposals from the frozen input verbatim as project-owned JSON. Assign each missing configuration to an applicable unit with its exact workspace write scope and reviewed output artifact. Preserve valid existing configs. These defaults do not authorize future work, push, deletion or standing project policy. Do not invent a bootstrap command. If ordinary_defaults.missing is nonempty, resolve that actual choice through the existing stage pause/answer path before composing; do not guess."] : []),
110
143
  ...(memoryFlow ? ["", "## Request support reference", "```json",
111
144
  JSON.stringify({ root: "run", path: `${stage.directory}/stage-input.json`, sha256: selection.inputSha256 }), "```",
@@ -174,7 +174,8 @@ export class PreparedDescriptorStageSettlement {
174
174
  * It keeps bootstrap/check/reviewer custody in the same acceptance writer.
175
175
  */
176
176
  apply(context, assertHandlerCustody) {
177
- const current = readFlowRunExecutionBundle(context, { projectRoot: this.#bundle.execution.snapshot().scope.projectRoot, runId: this.#scope.runId });
177
+ const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
178
+ const current = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
178
179
  if (!current || current.sha256 !== this.#bundle.sha256)
179
180
  refuse("bundle_changed");
180
181
  const receipt = { run_id: this.#scope.runId, stage: this.#scope.stage, work_id: this.#owner.workId, work_session_id: this.#owner.workSessionId, binding: this.binding() };
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ const refuse = (reason, details = {}) => {
6
+ const correctable = ["invalid_source_reference", "source_hash_changed", "source_missing"].includes(reason);
7
+ const message = reason === "source_hash_changed"
8
+ ? "Citation sha256 does not match its assigned snapshot file; correct the result using source_path and actual_source_sha256, then retry. No result was accepted."
9
+ : reason === "source_missing" ? "Citation is absent from the original project snapshot. Cite an existing project source instead; generated .dd-flow/review-evidence observations are not project sources. For a missing link target cite the existing referring document and record the absence as a limitation. Correct the result, then retry; no result was accepted."
10
+ : correctable ? "Use snapshot-relative evidence paths with forward slashes (for example src/index.ts), without absolute paths, dot segments or .git; sha256 must be 64 lowercase hex characters"
11
+ : "Diagnostic evidence does not match the assigned snapshot and coverage";
12
+ throw new AppError("diagnostic_evidence_invalid", message, 2, { ...details, reason, effect: "no_effect", ...(correctable ? { phase: "prepare", recoverable: true } : {}) });
13
+ };
14
+ /** Run after schema validation; schema-shaped hashes are not evidence by themselves. */
15
+ export function verifyDiagnosticSources(root, sources) {
16
+ const canonicalRoot = fs.realpathSync(root), seen = new Map();
17
+ for (const source of sources) {
18
+ if (!source.path || source.path.length > 4096 || source.path.includes("\\") || path.posix.isAbsolute(source.path) || path.win32.isAbsolute(source.path)
19
+ || source.path.split("/").some(part => !part || part === "." || part === ".." || part === ".git")
20
+ || !/^[a-f0-9]{64}$/.test(source.sha256))
21
+ refuse("invalid_source_reference");
22
+ if (seen.has(source.path) && seen.get(source.path) !== source.sha256)
23
+ refuse("conflicting_source_reference");
24
+ seen.set(source.path, source.sha256);
25
+ let bytes;
26
+ try {
27
+ const file = fs.realpathSync(path.join(canonicalRoot, source.path)), relative = path.relative(canonicalRoot, file);
28
+ if (relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative))
29
+ refuse("escaping_source_reference");
30
+ const stat = fs.statSync(file);
31
+ if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
32
+ refuse("unbounded_source_reference");
33
+ bytes = fs.readFileSync(file);
34
+ }
35
+ catch (error) {
36
+ if (error instanceof AppError)
37
+ throw error;
38
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
39
+ return refuse("source_missing", { source_path: source.path });
40
+ }
41
+ return refuse("source_unavailable");
42
+ }
43
+ const actual = createHash("sha256").update(bytes).digest("hex");
44
+ if (actual !== source.sha256)
45
+ refuse("source_hash_changed", {
46
+ source_path: source.path, provided_source_sha256: source.sha256, actual_source_sha256: actual
47
+ });
48
+ }
49
+ }
50
+ export function verifyDiagnosticCoverage(root, baseline, coverage) {
51
+ const ids = coverage.map(value => value.id);
52
+ if (new Set(ids).size !== ids.length || ids.length !== baseline.length || baseline.some(id => !ids.includes(id)))
53
+ refuse("incomplete_aspect_coverage");
54
+ for (const value of coverage) {
55
+ if (!value.reason.trim())
56
+ refuse("missing_applicability_reason");
57
+ verifyDiagnosticSources(root, value.evidence);
58
+ }
59
+ }
60
+ /** Each independent critic must account for every candidate, including rejected findings. */
61
+ export function verifyDiagnosticCritic(root, candidates, decisions) {
62
+ const ids = candidates.map(value => value.id), reviewed = decisions.map(value => value.finding_id);
63
+ if (new Set(ids).size !== ids.length || new Set(reviewed).size !== reviewed.length
64
+ || ids.length !== reviewed.length || ids.some(id => !reviewed.includes(id)))
65
+ refuse("incomplete_critic_coverage");
66
+ for (const candidate of candidates) {
67
+ if (!candidate.evidence.length)
68
+ refuse("finding_without_evidence");
69
+ verifyDiagnosticSources(root, candidate.evidence);
70
+ }
71
+ for (const decision of decisions)
72
+ verifyDiagnosticSources(root, decision.evidence);
73
+ }
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { appendAudit } from "./audit.js";
6
+ import { prepareStagePacket, acceptStagePacket } from "./stage-report-renderer.js";
7
+ import { publishImmutableInput } from "../storage/immutable-input.js";
8
+ import { renderDiagnosticReport } from "./diagnostic-report.js";
9
+ function refuse(reason) { throw new AppError("diagnostic_report_publication_invalid", "Diagnostic report publication requires its owned stage and exact bytes", 2, { reason, effect: "no_effect" }); }
10
+ /** Files are immutable candidates until retain is called in the Stage acceptance
11
+ * writer. Interrupted publication can replay before any terminal acceptance. */
12
+ export function prepareDiagnosticReportPublication(context, scope, report) {
13
+ const run = context.db.get("SELECT run_root,index_json FROM runs WHERE project_id=? AND id=?", [scope.projectId, scope.runId]);
14
+ const stage = run ? JSON.parse(run.index_json).stage_runs.find((value) => value.stage === scope.stage) : null;
15
+ if (!run?.run_root || scope.stage !== "report" || stage?.attempt !== scope.attempt || stage.input_sha256 !== scope.inputSha256
16
+ || report.run.id !== scope.runId || !["running", "done"].includes(stage.status))
17
+ refuse("foreign_stage");
18
+ const directory = path.join(run.run_root, stage.dir);
19
+ if (fs.realpathSync(run.run_root) !== run.run_root || fs.realpathSync(directory) !== directory
20
+ || !directory.startsWith(run.run_root + path.sep))
21
+ refuse("unsafe_report_directory");
22
+ const rendered = renderDiagnosticReport(report);
23
+ const packets = Object.entries(rendered).map(([kind, bytes]) => {
24
+ if (bytes.length > 4 * 1024 * 1024)
25
+ refuse("report_exceeds_bound");
26
+ const suffix = kind === "markdown" ? "md" : kind;
27
+ const file = path.join(directory, `diagnostic-report.${suffix}`);
28
+ try {
29
+ if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())
30
+ refuse("unsafe_report_file");
31
+ }
32
+ catch (error) {
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ }
36
+ return prepareStagePacket(file, bytes);
37
+ });
38
+ const manifest = { schema_id: "dd-flow/diagnostic-report-publication@1", run_id: scope.runId, stage: scope.stage, attempt: scope.attempt,
39
+ input_sha256: scope.inputSha256, artifacts: packets.map(packet => ({ path: path.relative(run.run_root, packet.file).split(path.sep).join("/"), sha256: packet.sha256 })) };
40
+ const retained = (writer) => writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='diagnostic.report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, scope.attempt]);
41
+ return {
42
+ manifest,
43
+ publish() {
44
+ if (fs.realpathSync(directory) !== directory)
45
+ refuse("report_directory_changed");
46
+ for (const packet of packets)
47
+ publishImmutableInput(packet.file, packet.bytes, "diagnostic_report_conflict");
48
+ },
49
+ /** SQL-only. The existing packet ledger retains recoverable authoritative
50
+ * bytes in the same transaction as Stage completion. */
51
+ retain(writer) {
52
+ const rows = retained(writer);
53
+ if (rows.length) {
54
+ if (rows.length !== 1 || hybridDigest(JSON.parse(rows[0].payload_json)) !== hybridDigest(manifest))
55
+ refuse("report_custody_changed");
56
+ for (const packet of packets) {
57
+ const records = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.file')=? LIMIT 2", [scope.projectId, scope.runId, scope.stage, scope.attempt, packet.file]);
58
+ if (records.length !== 1 || Buffer.byteLength(records[0].payload_json) > 6 * 1024 * 1024
59
+ || hybridDigest(JSON.parse(records[0].payload_json)) !== hybridDigest({ run_id: scope.runId, stage: scope.stage,
60
+ attempt: scope.attempt, file: packet.file, sha256: packet.sha256, base64: packet.bytes.toString("base64") }))
61
+ refuse("report_packet_custody_changed");
62
+ }
63
+ return;
64
+ }
65
+ if (stage.status !== "running")
66
+ refuse("report_receipt_missing");
67
+ for (const packet of packets)
68
+ acceptStagePacket(writer, { ...scope, prepared: packet });
69
+ appendAudit(writer, { projectId: scope.projectId, eventType: "diagnostic.report", payload: manifest });
70
+ }
71
+ };
72
+ }
@@ -0,0 +1,67 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { hybridDigest } from "./hybrid-contracts.js";
3
+ import { collectDiagnosticCandidates } from "./diagnostic-work-results.js";
4
+ function refuse(reason) { throw new AppError("diagnostic_report_invalid", "Diagnostic report requires the reviewed candidate and critic registers", 2, { reason, effect: "no_effect" }); }
5
+ /** Projection only. The handler separately owns source checks, physical critic
6
+ * independence, SQL custody, publication and terminal RUN settlement. */
7
+ export function prepareDiagnosticReport(result, analysis, critics) {
8
+ if (result.stage !== "report" || !critics.length
9
+ || [...analysis, ...critics].some(work => work.workId !== work.result.work_id || work.result.run_id !== result.run_id || work.result.flow_id !== result.flow_id))
10
+ refuse("foreign_work");
11
+ const candidates = collectDiagnosticCandidates(analysis);
12
+ if (hybridDigest(candidates.findings) !== hybridDigest(result.details.findings))
13
+ refuse("candidate_register_changed");
14
+ for (const critic of critics) {
15
+ const ids = critic.result.decisions.map(value => value.finding_id);
16
+ if (critic.result.stage !== "review" || critic.result.findings.length || new Set(ids).size !== ids.length
17
+ || ids.length !== candidates.findings.length || candidates.findings.some(value => !ids.includes(value.id)))
18
+ refuse("incomplete_critic");
19
+ }
20
+ const decisions = new Map(result.details.decisions.map(value => [value.finding_id, value]));
21
+ if (decisions.size !== candidates.findings.length || result.details.decisions.length !== decisions.size
22
+ || candidates.findings.some(value => !decisions.has(value.id)))
23
+ refuse("incomplete_resolution");
24
+ const findings = candidates.findings.map((finding, index) => ({ ...finding, origin: candidates.origins[index],
25
+ resolution: decisions.get(finding.id),
26
+ critic_decisions: critics.map(work => ({ work_id: work.workId, decision: work.result.decisions.find(value => value.finding_id === finding.id) })) }));
27
+ const incomplete = result.details.coverage.some(value => value.applicability === "blocked" || value.applicability === "out_of_scope")
28
+ || result.details.missing_expected_sources.length > 0 || result.details.limitations.length > 0;
29
+ const accepted = findings.filter(value => !["rejected", "duplicate"].includes(value.resolution.disposition));
30
+ const sourceMap = new Map();
31
+ for (const source of [...result.details.sources, ...result.details.coverage.flatMap(value => value.evidence),
32
+ ...analysis.flatMap(value => value.result.sources), ...critics.flatMap(value => value.result.sources),
33
+ ...findings.flatMap(value => [...value.evidence, ...value.resolution.evidence, ...value.critic_decisions.flatMap(critic => critic.decision.evidence)])]) {
34
+ if (sourceMap.has(source.path) && sourceMap.get(source.path).sha256 !== source.sha256)
35
+ refuse("conflicting_source_versions");
36
+ sourceMap.set(source.path, structuredClone(source));
37
+ }
38
+ return {
39
+ schema_id: "dd-flow/diagnostic-report@1",
40
+ run: { id: result.run_id, flow_id: result.flow_id, snapshot_sha256: result.snapshot_sha256 },
41
+ summary: result.summary, review_focus: structuredClone(result.details.focus), source_map: [...sourceMap.values()],
42
+ aspect_coverage: structuredClone(result.details.coverage),
43
+ critic_pass: critics.map(work => ({ work_id: work.workId, role: work.result.aspect_id, summary: work.result.summary,
44
+ semantic_sha256: hybridDigest(work.result), decisions: structuredClone(work.result.decisions), limitations: [...work.result.limitations] })),
45
+ findings_register: findings,
46
+ conformance_summary: { assessment: incomplete ? "incomplete" : accepted.length ? "findings" : "no_accepted_findings_in_selected_scope",
47
+ accepted_findings: accepted.length, rejected_or_duplicate_findings: findings.length - accepted.length,
48
+ applicable_aspects: result.details.coverage.filter(value => value.applicability === "applicable").length,
49
+ blocked_aspects: result.details.coverage.filter(value => value.applicability === "blocked").length,
50
+ missing_expected_sources: [...result.details.missing_expected_sources] },
51
+ review_fix_recommendation: { authorization: "not_granted", candidate_ids: findings.filter(value => value.resolution.disposition === "review_fix_candidate").map(value => value.id) },
52
+ deferrals: { authorization: "not_granted", candidate_ids: findings.filter(value => value.resolution.disposition === "def_candidate").map(value => value.id),
53
+ observed_sources: [...sourceMap.values()].filter(value => /(?:^|\/)DEF(?:-MBU)?-[^/]+/.test(value.path)) },
54
+ limitations: [...result.details.limitations],
55
+ dashboard_links: critics.map(work => ({ work_id: work.workId, result: `run://${result.run_id}/works/${work.workId}/result.json` }))
56
+ };
57
+ }
58
+ /** HTML embeds exactly the authoritative JSON value; untrusted prose is text,
59
+ * never HTML or executable script. Markdown remains a compact companion. */
60
+ export function renderDiagnosticReport(report) {
61
+ const json = Buffer.from(JSON.stringify(report, null, 2) + "\n");
62
+ const escape = (text) => text.replace(/[&<>"']/g, value => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[value]);
63
+ const embedded = json.toString("utf8").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
64
+ const markdown = Buffer.from(`# ${report.run.flow_id} · ${report.run.id}\n\n${report.summary}\n\nAssessment: ${report.conformance_summary.assessment}\n\n${report.findings_register.length} findings retained; ${report.critic_pass.length} independent critic passes.\n\nAuthoritative data: diagnostic-report.json\n`);
65
+ const html = Buffer.from(`<!doctype html><html lang="en"><meta charset="utf-8"><title>${escape(report.run.flow_id)} · ${escape(report.run.id)}</title><body><h1>${escape(report.review_focus.title)}</h1><p>${escape(report.summary)}</p><p>${escape(report.conformance_summary.assessment)}</p><h2>Coverage</h2><ul>${report.aspect_coverage.map(value => `<li>${escape(value.id)}: ${escape(value.applicability)} — ${escape(value.reason)}</li>`).join("")}</ul><h2>Findings</h2><pre>${escape(JSON.stringify(report.findings_register, null, 2))}</pre><script id="diagnostic-report-data" type="application/json">${embedded}</script></body></html>\n`);
66
+ return { json, markdown, html };
67
+ }
@@ -0,0 +1,74 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
4
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
5
+ import { FrozenGenericExecution } from "./generic-execution.js";
6
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
7
+ import { RetainedExecutionBundle } from "./execution-bundles.js";
8
+ import { readDiagnosticSnapshot } from "./diagnostic-snapshot.js";
9
+ import { requireProjectByRoot } from "./projects.js";
10
+ import { nextDescriptorRunId } from "./descriptor-run-start.js";
11
+ import { bundledHandlerContext } from "./stage-handlers.js";
12
+ import { hybridDigest } from "./hybrid-contracts.js";
13
+ const bytes = (value) => Buffer.from(JSON.stringify(value));
14
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
15
+ function refuse(reason) { throw new AppError("diagnostic_run_bundle_invalid", "Diagnostic bundle requires its frozen source, execution and snapshot", 2, { reason, effect: "no_effect" }); }
16
+ /** Initial construction only. Retry after allocation restores the retained RUN
17
+ * bundle, never current operator settings/canon. boundAt belongs to the request. */
18
+ export function prepareDiagnosticRunBundle(context, input) {
19
+ if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
20
+ || !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
21
+ refuse("unprepared_selection");
22
+ const frozen = input.execution.snapshot(), flowId = frozen.settings.flow_id;
23
+ if (!isDiagnosticFlow(flowId))
24
+ refuse("diagnostic_flow_required");
25
+ const project = requireProjectByRoot(context, frozen.scope.projectRoot);
26
+ const snapshot = readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
27
+ sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
28
+ if (!snapshot || snapshot.root !== frozen.scope.workspace)
29
+ refuse("snapshot_selection_changed");
30
+ input.source.revalidate();
31
+ const flow = input.source.resolveRegisteredFlow();
32
+ if (flow.descriptor.id !== flowId || flow.sha256 !== frozen.settings.descriptor_sha256)
33
+ refuse("flow_selection_changed");
34
+ input.engine.admitDiagnosticFlow(input.source, flowId);
35
+ const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
36
+ if (typeof checksum !== "string")
37
+ refuse("engine_identity_missing");
38
+ const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: flowId });
39
+ const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
40
+ project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: "Request-bound read-only diagnostic",
41
+ engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
42
+ integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
43
+ const blobs = input.source.blobs();
44
+ const put = (logical, content) => {
45
+ if (blobs.has(logical))
46
+ refuse("bundle_path_collision");
47
+ blobs.set(logical, content);
48
+ return { path: logical, sha256: hash(content) };
49
+ };
50
+ for (const ref of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
51
+ if (!ref)
52
+ continue;
53
+ const content = input.receiptBytes?.get(ref.sha256);
54
+ if (!content || hash(content) !== ref.sha256 || !input.receipts?.has(ref.sha256))
55
+ refuse("execution_receipt_missing");
56
+ put(ref.path, Buffer.from(content));
57
+ }
58
+ const profiles = { schema_id: "dd-flow/profile-settings@1", profiles: frozen.profiles, aliases: {}, default_profile: null };
59
+ const profile = put("runtime/profiles.json", bytes({ ...profiles, revision: hybridDigest(profiles) }));
60
+ const execution = put("runtime/execution.json", bytes(frozen.settings));
61
+ const engineBinding = put("runtime/engine-binding.json", bytes(binding));
62
+ const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
63
+ put("runtime/diagnostic-snapshot.json", bytes(snapshot));
64
+ const provenance = { canon: input.source.canonProvenance(), project_pack: null };
65
+ const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: flowId,
66
+ descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
67
+ engine_binding: engineBinding, engine_manifest: engineManifest, bootstrap_receipts: [], ...provenance,
68
+ inventory: [...blobs].map(([logical, content]) => ({ path: logical, sha256: hash(content) })) };
69
+ const encoded = bytes(bundle);
70
+ return RetainedExecutionBundle.admit(encoded, hash(encoded), blobs, { runId, executionScope: frozen.scope,
71
+ bundleProjectRoot: frozen.scope.projectRoot, descriptorSha256: flow.sha256, engineChecksum: checksum,
72
+ engineBindingSha256: engineBinding.sha256, engineManifestSha256: engineManifest.sha256,
73
+ descriptorContext: bundledHandlerContext(new Map()), bundleProvenance: provenance, receipts: input.receipts ?? new Map() });
74
+ }