@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155

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 (227) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +214 -24
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -4
  190. package/dist/services/retained-native-hook.js +245 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +12 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +209 -43
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +9 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +252 -66
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -17,6 +17,7 @@ import { appendAudit } from "./audit.js";
17
17
  import { prepareStagePacket, acceptStagePacket, publishStagePacket, readAcceptedStageData } from "./stage-report-renderer.js";
18
18
  import { assertStageSettlementAllowed } from "./run-recovery.js";
19
19
  import { refreshRunSessionProjection } from "./run-projection.js";
20
+ import { currentAssignedLifecycle, assertAssignedSettlementCurrent, hasAssignedCoordinatorLaunch } from "./assigned-lifecycle.js";
20
21
  const event = "flow_run.descriptor_stage_settlement";
21
22
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
22
23
  function refuse(reason) { throw new AppError("descriptor_stage_settlement_invalid", "Stage settlement lacks exact coordinator and result custody", 1, { reason, effect: "no_effect" }); }
@@ -26,10 +27,10 @@ function receipts(context, scope, kind) {
26
27
  refuse("ambiguous_receipt");
27
28
  return rows[0] ? JSON.parse(rows[0].payload_json) : null;
28
29
  }
29
- function owner(context, scope, bundle, finishKey, matchKeys) {
30
+ function coordinatorOwner(context, scope, bundle, historicalRead = false) {
30
31
  const prior = receipts(context, scope, event);
31
32
  const input = readDescriptorStageInput(context, scope, bundle, true, prior !== null), root = readDescriptorRootWork(context, scope, bundle);
32
- const closedTerminal = prior && root.status === "completed" && bundle.descriptor.descriptor.terminal_stages.includes(scope.stage)
33
+ const closedTerminal = prior && root.status === "completed" && (historicalRead || bundle.descriptor.descriptor.terminal_stages.includes(scope.stage))
33
34
  && context.db.get("SELECT 1 FROM runs WHERE project_id=? AND id=? AND status='done'", [scope.projectId, scope.runId]);
34
35
  if (root.status !== "running" && !closedTerminal || !stageWorkIds(context, scope.projectId, scope.runId, scope.stage).has(root.workId))
35
36
  refuse("root_not_active");
@@ -67,13 +68,50 @@ function owner(context, scope, bundle, finishKey, matchKeys) {
67
68
  const promptPath = path.join(input.runRoot, bundle.descriptor.stage(scope.stage).directory, "coordinator-prompt.md");
68
69
  if (link.prompt_path !== promptPath)
69
70
  refuse("prompt_link_changed");
70
- const prompts = 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, promptPath]);
71
- if (prompts.length !== 1 || Buffer.byteLength(prompts[0].payload_json) > 6 * 1024 * 1024)
71
+ const prompts = 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')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=?", [scope.projectId, scope.runId, scope.stage, promptPath, input.attempt, association.binding.prompt_sha256]);
72
+ if (!prompts.length)
72
73
  refuse("prompt_custody_missing");
73
- const prompt = JSON.parse(prompts[0].payload_json), promptBytes = Buffer.from(prompt.base64, "base64");
74
- if (prompt.attempt !== input.attempt || prompt.sha256 !== association.binding.prompt_sha256 || hash(promptBytes) !== prompt.sha256 || promptBytes.toString("base64") !== prompt.base64)
75
- refuse("prompt_custody_changed");
74
+ let promptBytes;
75
+ for (const row of prompts) {
76
+ const prompt = JSON.parse(row.payload_json), bytes = Buffer.from(prompt.base64, "base64");
77
+ if (hash(bytes) !== prompt.sha256 || bytes.toString("base64") !== prompt.base64 || promptBytes && !promptBytes.equals(bytes))
78
+ refuse("prompt_custody_changed");
79
+ promptBytes = bytes;
80
+ }
76
81
  const start = context.db.get("SELECT daemon_id,session_id,harness,provider_session_id FROM hook_events WHERE project_id=? AND event_key=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, link.hook_event_id]);
82
+ if (start ? start.session_id !== link.session_id || start.harness !== session.harness || start.provider_session_id !== session.provider_session_id
83
+ : !hasAssignedCoordinatorLaunch(context, { ...scope, workSessionId: link.id, sessionId: link.session_id }))
84
+ refuse("start_owner_mismatch");
85
+ return { input, root, link, session, binding, association, promptPath, prior, start };
86
+ }
87
+ /** SQL-only current author identity. This proves the registered coordinator,
88
+ * exact prompt, physical binding and claimed or assigned start, not output approval,
89
+ * permission to publish, or Stage completion. No Session is allocated here. */
90
+ export function readActiveDescriptorStageCoordinator(context, input, bundle) {
91
+ const scope = { projectId: input.projectId, runId: input.runId, stage: input.stage };
92
+ const selected = coordinatorOwner(context, scope, bundle);
93
+ if (selected.prior)
94
+ refuse("coordinator_already_settled");
95
+ if (!selected.session.harness || !selected.session.provider_session_id)
96
+ refuse("physical_owner_missing");
97
+ return { work_id: selected.root.workId, work_session_id: selected.link.id, session_id: selected.link.session_id,
98
+ harness: selected.session.harness, provider_session_id: selected.session.provider_session_id,
99
+ start_hook: selected.link.hook_event_id, association_sha256: hybridDigest(selected.association),
100
+ physical_binding_sha256: hybridDigest(selected.binding), prompt_path: selected.promptPath };
101
+ }
102
+ function owner(context, scope, bundle, finishKey, matchKeys, historicalRead = false) {
103
+ const { input, root, link, session, binding, association, promptPath, prior, start } = coordinatorOwner(context, scope, bundle, historicalRead);
104
+ const assignedId = prior?.binding?.assignedInvocationId;
105
+ const saved = assignedId && context.db.get("SELECT admission_json,status FROM lifecycle_invocations WHERE id=?", [assignedId]);
106
+ const assigned = prior
107
+ ? saved && saved.status === "settled" && saved.admission_json ? JSON.parse(saved.admission_json) : null
108
+ : currentAssignedLifecycle(context);
109
+ if (assigned && assigned.work_session_id === link.id && assigned.operation === "stage_finish" && assigned.scope.runId === scope.runId && assigned.stage === scope.stage) {
110
+ return { input, workId: root.workId, workSessionId: link.id, sessionId: link.session_id,
111
+ startHook: link.hook_event_id, finishKey: null, finishId: null, finishDaemon: assigned.scope.daemonId,
112
+ assignedInvocationId: assigned.invocation_id, associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
113
+ promptPath, prior, linkResultPath: link.result_path };
114
+ }
77
115
  const finish = context.db.get("SELECT id,daemon_id,session_id,harness,provider_session_id,match_key,status FROM hook_events WHERE project_id=? AND event_key=? AND event_name='PreToolUse'", [scope.projectId, finishKey]);
78
116
  if (!start || !finish || finish.status !== (prior ? "claimed" : "observed")
79
117
  || start.session_id !== link.session_id || finish.session_id !== link.session_id
@@ -83,13 +121,14 @@ function owner(context, scope, bundle, finishKey, matchKeys) {
83
121
  refuse("finish_owner_mismatch");
84
122
  return { input, workId: root.workId, workSessionId: link.id, sessionId: link.session_id,
85
123
  startHook: link.hook_event_id, finishKey, finishId: finish.id, finishDaemon: finish.daemon_id,
86
- associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
124
+ assignedInvocationId: undefined, associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
87
125
  promptPath, prior, linkResultPath: link.result_path };
88
126
  }
89
127
  function stableOwner(value) {
90
128
  return { input: value.input, workId: value.workId, workSessionId: value.workSessionId, sessionId: value.sessionId,
91
129
  startHook: value.startHook, finishKey: value.finishKey, finishId: value.finishId, finishDaemon: value.finishDaemon,
92
- associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath };
130
+ associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath,
131
+ ...(value.assignedInvocationId ? { assignedInvocationId: value.assignedInvocationId } : {}) };
93
132
  }
94
133
  /** SQL-only accepted terminal custody reader, also after root closure. Match
95
134
  * keys must be prepared outside the writer because lifecycle key preparation
@@ -100,16 +139,17 @@ export function readAcceptedDescriptorStageSettlement(context, input, bundle, ma
100
139
  // the retained coordinator binding; source/policy owners are not scope keys.
101
140
  const scope = { projectId: input.projectId, runId: input.runId, stage: input.stage };
102
141
  const receipt = receipts(context, scope, event);
103
- if (!receipt || typeof receipt.binding?.finishKey !== "string")
142
+ if (!receipt || typeof receipt.binding?.finishKey !== "string" && typeof receipt.binding?.assignedInvocationId !== "string")
104
143
  refuse("settlement_receipt_missing");
105
- const selected = owner(context, scope, bundle, receipt.binding.finishKey, matchKeys);
144
+ const selected = owner(context, scope, bundle, receipt.binding.finishKey, matchKeys, true);
106
145
  const data = readAcceptedStageData(context, scope);
107
146
  const expected = { run_id: scope.runId, stage: scope.stage, work_id: selected.workId, work_session_id: selected.workSessionId, binding: {
108
147
  workId: selected.workId, workSessionId: selected.workSessionId, sessionId: selected.sessionId, startHook: selected.startHook,
109
148
  finishKey: selected.finishKey, finishId: selected.finishId, finishDaemon: selected.finishDaemon,
110
149
  associationSha256: selected.associationSha256, physicalBindingSha256: selected.physicalBindingSha256, promptPath: selected.promptPath,
111
150
  scope, attempt: selected.input.attempt, input_sha256: selected.input.inputSha256, bundle_sha256: bundle.sha256,
112
- result_sha256: data.result.sha256, result_path: selected.linkResultPath, data_schema_id: data.data_schema_id
151
+ result_sha256: data.result.sha256, result_path: selected.linkResultPath, data_schema_id: data.data_schema_id,
152
+ ...(selected.assignedInvocationId ? { assignedInvocationId: selected.assignedInvocationId } : {})
113
153
  } };
114
154
  if (hybridDigest(expected) !== hybridDigest(receipt) || data.input_sha256 !== selected.input.inputSha256)
115
155
  refuse("settlement_changed");
@@ -168,8 +208,12 @@ export class PreparedDescriptorStageSettlement {
168
208
  finishKey: value.finishKey, finishId: value.finishId, finishDaemon: value.finishDaemon,
169
209
  associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath,
170
210
  scope: this.#scope, attempt: value.input.attempt, input_sha256: value.input.inputSha256, bundle_sha256: this.#bundle.sha256,
171
- result_sha256: this.#packet.sha256, result_path: this.#packet.file, data_schema_id: this.#schemaId };
211
+ result_sha256: this.#packet.sha256, result_path: this.#packet.file, data_schema_id: this.#schemaId,
212
+ ...(value.assignedInvocationId ? { assignedInvocationId: value.assignedInvocationId } : {}) };
172
213
  }
214
+ /** Identity already admitted against physical hook/coordinator custody.
215
+ * A family may compare it with a prior writer; this grants no settlement. */
216
+ coordinatorSessionId() { return this.#owner.sessionId; }
173
217
  /** The optional handler fence must be SQL-only, prepared before this call.
174
218
  * It keeps bootstrap/check/reviewer custody in the same acceptance writer.
175
219
  */
@@ -197,7 +241,8 @@ export class PreparedDescriptorStageSettlement {
197
241
  const activeChildren = [...stageWorkIds(context, this.#scope.projectId, this.#scope.runId, this.#scope.stage)].filter(id => id !== this.#owner.workId && context.db.get("SELECT 1 FROM works WHERE work_id=? AND status IN ('created','running','paused')", [id]));
198
242
  if (activeChildren.length)
199
243
  refuse("stage_children_active");
200
- if (context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND status='observed'", [context.now(), this.#owner.finishId]).changes !== 1)
244
+ assertAssignedSettlementCurrent(context, this.#owner.workSessionId);
245
+ if (this.#owner.finishId !== null && context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND status='observed'", [context.now(), this.#owner.finishId]).changes !== 1)
201
246
  refuse("finish_claim_changed");
202
247
  acceptStagePacket(context, { ...this.#scope, prepared: this.#packet });
203
248
  completeFlowRunStage(context, { projectRoot: this.#completion.run.project_root, runId: this.#scope.runId, stage: this.#scope.stage, status: "done", data: "result.json", dataSchemaId: this.#schemaId, dataSha256: this.#packet.sha256 }, this.#completion);
@@ -0,0 +1,123 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
5
+ import { resolveProjectRoot } from "../storage/paths.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { readFlowRunExecutionBundle, resolveRun } from "./runs.js";
8
+ import { readAcceptedStageData, resolveStagePublicationPath } from "./stage-report-renderer.js";
9
+ import { selectDiagnosticFindings } from "./diagnostic-report.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ import { assertMemoryRepairWritePaths } from "./memory-review-correction-plan.js";
12
+ import { readMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
13
+ function refuse(reason) {
14
+ throw new AppError("repair_source_invalid", "Repair intake requires a completed diagnostic RUN and its accepted report", 2, { reason, effect: "no_effect" });
15
+ }
16
+ export function readDiagnosticRepairIntake(context, input) {
17
+ return context.db.readSnapshot(() => {
18
+ try {
19
+ return readIntake(context, input);
20
+ }
21
+ catch (error) {
22
+ if (error instanceof SyntaxError || error instanceof TypeError)
23
+ refuse("malformed_source_custody");
24
+ throw error;
25
+ }
26
+ });
27
+ }
28
+ export function readMemoryFixUserInputs(context, input, retainedBundle) {
29
+ return readMemoryFlowUserInputs(context, input, "mb-fix", retainedBundle);
30
+ }
31
+ /** Semantic intake proposal only: no write, closure, migration or SDLC launch
32
+ * authority. The registered flow must separately bind actual user approval,
33
+ * observe current source drift and admit its writer/independent verifier.
34
+ * Never classify prose by keywords or infer selection from severity. */
35
+ export function prepareMemoryFixIntake(context, input) {
36
+ const source = readDiagnosticRepairIntake(context, input);
37
+ const routing = validateMemoryFixRouting(source.selected_findings.map(finding => finding.id), input.decisions);
38
+ return { schema_id: "dd-flow/mb-fix-intake@1", ...source, routing,
39
+ repair_finding_ids: routing.filter(decision => decision.route === "memory_repair").map(decision => decision.finding_id) };
40
+ }
41
+ /** Shared semantic routing checks; this does not admit a source RUN or user approval. */
42
+ export function validateMemoryFixRouting(findingIds, decisions) {
43
+ const invalid = (reason) => {
44
+ throw new AppError("memory_fix_intake_invalid", "Memory repair intake requires one bounded routing decision per selected finding", 2, { reason, effect: "no_effect" });
45
+ };
46
+ if (!Array.isArray(findingIds) || !findingIds.length || findingIds.length > 4096
47
+ || findingIds.some(id => typeof id !== "string" || !id.trim()) || new Set(findingIds).size !== findingIds.length
48
+ || !Array.isArray(decisions) || decisions.length !== findingIds.length)
49
+ invalid("complete_routing_required");
50
+ const selected = new Set(findingIds);
51
+ const byId = new Map();
52
+ for (const value of decisions) {
53
+ if (!value || typeof value !== "object" || Array.isArray(value)
54
+ || Object.keys(value).sort().join() !== "finding_id,reason,route,write_paths")
55
+ invalid("invalid_routing_decision");
56
+ const decision = value;
57
+ if (typeof decision.finding_id !== "string" || !selected.has(decision.finding_id) || byId.has(decision.finding_id))
58
+ invalid("foreign_or_duplicate_finding");
59
+ if (!["memory_repair", "review_fix", "upgrade", "defer"].includes(decision.route)
60
+ || typeof decision.reason !== "string" || !decision.reason.trim() || decision.reason.includes("\0")
61
+ || Buffer.byteLength(decision.reason) > 16 * 1024 || !Array.isArray(decision.write_paths))
62
+ invalid("invalid_routing_decision");
63
+ if (decision.route === "memory_repair")
64
+ assertMemoryRepairWritePaths(decision.write_paths);
65
+ else if (decision.write_paths.length)
66
+ invalid("non_memory_write_scope");
67
+ byId.set(decision.finding_id, structuredClone(decision));
68
+ }
69
+ return findingIds.map(id => byId.get(id));
70
+ }
71
+ function readIntake(context, input) {
72
+ const projectRoot = resolveProjectRoot(input.projectRoot), project = requireProjectByRoot(context, projectRoot);
73
+ const run = resolveRun(context, project.id, input.sourceRunId);
74
+ if (run.status !== "done" || !isDiagnosticFlow(run.flow_kind))
75
+ refuse("completed_diagnostic_required");
76
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot, runId: run.id });
77
+ if (!bundle || bundle.manifest.flow_id !== run.flow_kind)
78
+ refuse("diagnostic_bundle_required");
79
+ const scope = { projectId: project.id, runId: run.id, stage: "report" };
80
+ const accepted = readAcceptedStageData(context, scope);
81
+ if (accepted.data_schema_id !== "dd-flow/diagnostic-stage-result@1")
82
+ refuse("unsupported_source_result");
83
+ const result = JSON.parse(accepted.bytes.toString("utf8"));
84
+ if (result.schema_id !== accepted.data_schema_id || result.run_id !== run.id || result.flow_id !== run.flow_kind
85
+ || result.stage !== "report" || result.input_sha256 !== accepted.input_sha256)
86
+ refuse("foreign_source_result");
87
+ const stage = JSON.parse(run.index_json).stage_runs.find((item) => item.stage === "report");
88
+ const rows = context.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", [project.id, run.id, stage.attempt]);
89
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
90
+ refuse("report_publication_missing");
91
+ const publication = JSON.parse(rows[0].payload_json);
92
+ const reportPath = `${stage.dir}/diagnostic-report.json`;
93
+ if (publication.schema_id !== "dd-flow/diagnostic-report-publication@1" || publication.input_sha256 !== accepted.input_sha256
94
+ || !Array.isArray(publication.artifacts))
95
+ refuse("foreign_report_publication");
96
+ const artifacts = publication.artifacts.filter((item) => item.path === reportPath);
97
+ if (artifacts.length !== 1 || !/^[a-f0-9]{64}$/.test(artifacts[0].sha256))
98
+ refuse("report_artifact_missing");
99
+ const packets = 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')='report' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=? LIMIT 2", [project.id, run.id, stage.attempt, artifacts[0].sha256]);
100
+ if (packets.length !== 1 || Buffer.byteLength(packets[0].payload_json) > 6 * 1024 * 1024)
101
+ refuse("report_packet_missing");
102
+ const packet = JSON.parse(packets[0].payload_json);
103
+ const destination = path.resolve(run.run_root ?? run.run_dir, reportPath);
104
+ if (typeof packet.file !== "string" || resolveStagePublicationPath(context, project.id, run.id, packet.file) !== destination
105
+ || typeof packet.base64 !== "string")
106
+ refuse("foreign_report_packet");
107
+ const bytes = Buffer.from(packet.base64, "base64");
108
+ if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== packet.base64
109
+ || createHash("sha256").update(bytes).digest("hex") !== artifacts[0].sha256)
110
+ refuse("report_packet_changed");
111
+ const report = JSON.parse(bytes.toString("utf8"));
112
+ if (report.schema_id !== "dd-flow/diagnostic-report@1" || report.run.id !== run.id || report.run.flow_id !== run.flow_kind
113
+ || report.run.snapshot_sha256 !== result.snapshot_sha256 || report.summary !== result.summary
114
+ || hybridDigest(report.review_focus) !== hybridDigest(result.details.focus)
115
+ || hybridDigest(report.aspect_coverage) !== hybridDigest(result.details.coverage)
116
+ || hybridDigest(report.limitations) !== hybridDigest(result.details.limitations)
117
+ || !Array.isArray(report.findings_register)
118
+ || hybridDigest(report.findings_register.map(({ origin: _origin, resolution: _resolution, critic_decisions: _critics, ...finding }) => finding)) !== hybridDigest(result.details.findings)
119
+ || hybridDigest(report.findings_register.map(finding => finding.resolution)) !== hybridDigest(result.details.decisions))
120
+ refuse("report_result_changed");
121
+ return { ...selectDiagnosticFindings(report, input.findingIds),
122
+ source_stage: { ...accepted.result, authority_sha256: accepted.authority_sha256 } };
123
+ }
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
4
+ import { readDiagnosticRepairIntake } from "./diagnostic-repair-intake.js";
5
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { resolveProjectRoot } from "../storage/paths.js";
8
+ const logical = "runtime/repair-intake.json", limit = 4 * 1024 * 1024;
9
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
10
+ const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
11
+ const targets = {
12
+ "mb-fix": { schema: "dd-flow/mb-fix-source-intake@1", error: "memory_fix_intake_custody_invalid", message: "Memory repair requires its own frozen selected source intake", bundle: "memory_fix_bundle_required" },
13
+ "review-fix": { schema: "dd-flow/review-fix-source-intake@1", error: "review_fix_intake_custody_invalid", message: "Review-fix requires its own frozen selected source intake", bundle: "review_fix_bundle_required" }
14
+ };
15
+ function refuse(flowId, reason) {
16
+ const owner = Object.hasOwn(targets, flowId) ? targets[flowId] : targets["mb-fix"];
17
+ throw new AppError(owner.error, owner.message, 2, { reason, effect: "no_effect" });
18
+ }
19
+ /** SQL/in-memory fence for generic Stage writers; performs no filesystem lookup. */
20
+ export function assertDiagnosticRepairSourceIntakeCustody(context, scope, bundle, expectedSha256, flowId) {
21
+ if (!Object.hasOwn(targets, flowId))
22
+ refuse(flowId, "unsupported_flow");
23
+ if (bundle.manifest.flow_id !== flowId || bundle.manifest.run_id !== scope.runId)
24
+ refuse(flowId, targets[flowId].bundle);
25
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
26
+ const references = bundle.manifest.inventory.filter(ref => ref.path === logical);
27
+ if (references.length !== 1 || references[0].sha256 !== expectedSha256 || hash(bundle.blob(logical)) !== expectedSha256)
28
+ refuse(flowId, "source_intake_changed");
29
+ }
30
+ /** Initial selection comes from the completed diagnostic's SQL custody, never
31
+ * an agent-authored report. The RUN bundle factory must retain these exact bytes.
32
+ * No RUN allocation, write approval or mutation happens here.
33
+ */
34
+ export function prepareDiagnosticRepairSourceIntake(context, input, flowId) {
35
+ if (!Object.hasOwn(targets, flowId))
36
+ refuse(flowId, "unsupported_flow");
37
+ const intake = readDiagnosticRepairIntake(context, input);
38
+ const bytes = Buffer.from(JSON.stringify({ schema_id: targets[flowId].schema, intake }));
39
+ if (bytes.length > limit)
40
+ refuse(flowId, "intake_exceeds_bound");
41
+ return { path: logical, sha256: hash(bytes), bytes };
42
+ }
43
+ /** Existing RUNs read their own retained input, not neighboring audit storage.
44
+ * Original source-stage paths are provenance only, never paths in this RUN.
45
+ * Frozen-bundle SQL authority still belongs to the existing generic runtime.
46
+ */
47
+ export function readDiagnosticRepairSourceIntake(context, input, flowId, retainedBundle) {
48
+ if (!Object.hasOwn(targets, flowId))
49
+ refuse(flowId, "unsupported_flow");
50
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
51
+ const bundle = retainedBundle ?? readFlowRunExecutionBundle(context, input);
52
+ if (!bundle || bundle.manifest.flow_id !== flowId || bundle.manifest.run_id !== input.runId)
53
+ refuse(flowId, targets[flowId].bundle);
54
+ return context.db.readSnapshot(() => {
55
+ assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: input.runId }, bundle);
56
+ const references = bundle.manifest.inventory.filter(ref => ref.path === logical);
57
+ if (references.length !== 1)
58
+ refuse(flowId, "source_intake_reference_missing");
59
+ const bytes = bundle.blob(logical);
60
+ if (!bytes.length || bytes.length > limit || hash(bytes) !== references[0].sha256)
61
+ refuse(flowId, "source_intake_changed");
62
+ let value;
63
+ try {
64
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
65
+ }
66
+ catch {
67
+ return refuse(flowId, "invalid_source_intake");
68
+ }
69
+ try {
70
+ const intake = value.intake;
71
+ if (value.schema_id !== targets[flowId].schema || Object.keys(value).sort().join() !== "intake,schema_id"
72
+ || intake.authorization !== "not_granted" || !/^RUN-[0-9]+-[a-z0-9-]+$/.test(intake.source.id)
73
+ || !isDiagnosticFlow(intake.source.flow_id) || !digest(intake.source.snapshot_sha256) || !digest(intake.source.report_sha256)
74
+ || !digest(intake.source_stage.sha256) || !digest(intake.source_stage.authority_sha256)
75
+ || typeof intake.source_stage.path !== "string" || !intake.source_stage.path
76
+ || !Array.isArray(intake.selected_findings) || !intake.selected_findings.length || intake.selected_findings.length > 4096
77
+ || !Array.isArray(intake.unselected_ids) || intake.unselected_ids.length > 4096
78
+ || !Array.isArray(intake.limitations) || intake.limitations.some(item => typeof item !== "string"))
79
+ refuse(flowId, "invalid_source_intake");
80
+ const ids = intake.selected_findings.map(finding => finding.id);
81
+ if (ids.some(id => typeof id !== "string" || !id.trim()) || new Set(ids).size !== ids.length
82
+ || new Set(intake.unselected_ids).size !== intake.unselected_ids.length
83
+ || intake.unselected_ids.some(id => typeof id !== "string" || !id.trim() || ids.includes(id)))
84
+ refuse(flowId, "invalid_source_selection");
85
+ for (const finding of intake.selected_findings) {
86
+ if (finding.origin.id !== finding.id || typeof finding.origin.work_id !== "string"
87
+ || finding.resolution.finding_id !== finding.id
88
+ || !["accepted", "downgraded", "def_candidate", "review_fix_candidate"].includes(finding.resolution.disposition)
89
+ || !Array.isArray(finding.evidence) || !Array.isArray(finding.resolution.evidence) || !Array.isArray(finding.critic_decisions)
90
+ || finding.critic_decisions.some(critic => critic.decision.finding_id !== finding.id || !Array.isArray(critic.decision.evidence)))
91
+ refuse(flowId, "invalid_source_selection");
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (error instanceof AppError)
96
+ throw error;
97
+ return refuse(flowId, "invalid_source_intake");
98
+ }
99
+ return { sha256: references[0].sha256, reference: { root: "run", path: `execution-bundle/${logical}`, sha256: references[0].sha256 },
100
+ intake: structuredClone(value.intake) };
101
+ });
102
+ }
@@ -55,6 +55,36 @@ export function prepareDiagnosticReport(result, analysis, critics) {
55
55
  dashboard_links: critics.map(work => ({ work_id: work.workId, result: `run://${result.run_id}/works/${work.workId}/result.json` }))
56
56
  };
57
57
  }
58
+ /** Pure repair-intake selection, not acceptance or mutation authority. The
59
+ * caller must obtain report bytes from the completed RUN's committed custody,
60
+ * then separately bind the user's approved write scope. Never infer selection
61
+ * from severity, recommendations or a non-empty findings register. */
62
+ export function selectDiagnosticFindings(report, findingIds) {
63
+ const invalid = (reason) => {
64
+ throw new AppError("repair_selection_invalid", "Select exact reviewed finding IDs before preparing repair scope", 2, { reason, effect: "no_effect" });
65
+ };
66
+ if (!Array.isArray(findingIds) || !findingIds.length || findingIds.length > 4096
67
+ || findingIds.some(id => typeof id !== "string" || !id.trim()) || new Set(findingIds).size !== findingIds.length)
68
+ invalid("explicit_unique_selection_required");
69
+ const register = new Map(report.findings_register.map(finding => [finding.id, finding]));
70
+ if (register.size !== report.findings_register.length)
71
+ invalid("ambiguous_finding_register");
72
+ const selected = findingIds.map(id => {
73
+ const finding = register.get(id);
74
+ if (!finding)
75
+ return invalid("unknown_finding");
76
+ if (!["accepted", "downgraded", "def_candidate", "review_fix_candidate"].includes(finding.resolution.disposition))
77
+ invalid("finding_not_accepted");
78
+ return structuredClone(finding);
79
+ });
80
+ return {
81
+ source: { ...structuredClone(report.run), report_sha256: hybridDigest(report) },
82
+ selected_findings: selected,
83
+ unselected_ids: report.findings_register.filter(finding => !findingIds.includes(finding.id)).map(finding => finding.id),
84
+ limitations: [...report.limitations],
85
+ authorization: "not_granted"
86
+ };
87
+ }
58
88
  /** HTML embeds exactly the authoritative JSON value; untrusted prose is text,
59
89
  * never HTML or executable script. Markdown remains a compact companion. */
60
90
  export function renderDiagnosticReport(report) {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AppError } from "../shared/errors.js";
3
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
3
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
4
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
4
5
  import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
5
6
  import { FrozenGenericExecution } from "./generic-execution.js";
6
7
  import { AdmittedFrozenEngine } from "./frozen-engine.js";
@@ -10,34 +11,65 @@ import { requireProjectByRoot } from "./projects.js";
10
11
  import { nextDescriptorRunId } from "./descriptor-run-start.js";
11
12
  import { bundledHandlerContext } from "./stage-handlers.js";
12
13
  import { hybridDigest } from "./hybrid-contracts.js";
14
+ import { readDiagnosticRequest } from "./diagnostic-start.js";
13
15
  const bytes = (value) => Buffer.from(JSON.stringify(value));
14
16
  const hash = (value) => createHash("sha256").update(value).digest("hex");
15
17
  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
18
  /** Initial construction only. Retry after allocation restores the retained RUN
17
19
  * bundle, never current operator settings/canon. boundAt belongs to the request. */
18
20
  export function prepareDiagnosticRunBundle(context, input) {
21
+ return prepareRequestRunBundle(context, input, "source_snapshot");
22
+ }
23
+ /** Delivery shares immutable request/engine/profile retention, not diagnosis's
24
+ * read-only cloned workspace or its permission model. No effect is admitted. */
25
+ export function prepareDeliveryRunBundle(context, input) {
26
+ return prepareRequestRunBundle(context, input, "delivery");
27
+ }
28
+ function prepareRequestRunBundle(context, input, mode) {
19
29
  if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
20
30
  || !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
21
31
  refuse("unprepared_selection");
22
32
  const frozen = input.execution.snapshot(), flowId = frozen.settings.flow_id;
23
- if (!isDiagnosticFlow(flowId))
24
- refuse("diagnostic_flow_required");
33
+ if (mode === "delivery" ? !isDeliveryFlow(flowId) : !isSourceSnapshotFlow(flowId))
34
+ refuse("registered_request_flow_required");
35
+ if (mode === "delivery" && frozen.settings.stage_session_mode === "same_session")
36
+ refuse("independent_delivery_verifier_session_required");
37
+ if (flowId === "review-fix" && frozen.settings.stage_session_mode === "same_session")
38
+ refuse("independent_scope_review_session_required");
39
+ if ((mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix") && (typeof input.requestText !== "string" || !input.requestText.trim()
40
+ || Buffer.byteLength(input.requestText) > 64 * 1024))
41
+ refuse(mode === "delivery" ? "original_delivery_request_required" : flowId === "mb-distill" ? "original_distill_request_required" : "original_review_fix_request_required");
42
+ const request = flowId === "review-fix" ? readDiagnosticRequest(context, { projectRoot: frozen.scope.projectRoot,
43
+ requestId: frozen.scope.requestId, flowId }) : null;
44
+ if (flowId === "review-fix" && (!request?.repair_source || request.created_at !== input.boundAt
45
+ || request.objective !== input.requestText || request.source_sha256 !== input.source.sha256
46
+ || request.execution_sha256 !== input.execution.sha256))
47
+ refuse("original_repair_request_required");
25
48
  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)
49
+ const snapshot = isSourceSnapshotFlow(flowId) ? readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
50
+ sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId }) : null;
51
+ if (mode === "source_snapshot" && (!snapshot || snapshot.root !== frozen.scope.workspace))
29
52
  refuse("snapshot_selection_changed");
30
53
  input.source.revalidate();
31
54
  const flow = input.source.resolveRegisteredFlow();
32
55
  if (flow.descriptor.id !== flowId || flow.sha256 !== frozen.settings.descriptor_sha256)
33
56
  refuse("flow_selection_changed");
34
- input.engine.admitDiagnosticFlow(input.source, flowId);
57
+ if (isDeliveryFlow(flowId))
58
+ input.engine.admitDeliveryFlow(input.source, flowId);
59
+ else if (flowId === "review-fix")
60
+ input.engine.admitReviewFixFlow(input.source);
61
+ else if (flowId === "mb-distill")
62
+ input.engine.admitDistillFlow(input.source);
63
+ else if (flowId === "mb-audit" || flowId === "project-review")
64
+ input.engine.admitDiagnosticFlow(input.source, flowId);
65
+ else
66
+ refuse("registered_request_flow_required");
35
67
  const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
36
68
  if (typeof checksum !== "string")
37
69
  refuse("engine_identity_missing");
38
70
  const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: flowId });
39
71
  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",
72
+ project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: isDeliveryFlow(flowId) ? "Request-bound delivery; startup grants no productive mutation approval" : flowId === "review-fix" ? "Request-bound selected finding scope and explicit formalization handoff" : flowId === "mb-distill" ? "Request-bound practice distillation with separate destination authority" : "Request-bound read-only diagnostic",
41
73
  engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
42
74
  integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
43
75
  const blobs = input.source.blobs();
@@ -47,6 +79,12 @@ export function prepareDiagnosticRunBundle(context, input) {
47
79
  blobs.set(logical, content);
48
80
  return { path: logical, sha256: hash(content) };
49
81
  };
82
+ // Use the retained request verbatim, not the coordinator's interpretation or
83
+ // a later mutable intake file. It is task data, never implicit write scope.
84
+ if (mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix")
85
+ put("runtime/source-request.md", Buffer.from(input.requestText));
86
+ if (request?.repair_source)
87
+ put(request.repair_source.path, Buffer.from(request.repair_source.base64, "base64"));
50
88
  for (const ref of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
51
89
  if (!ref)
52
90
  continue;
@@ -60,7 +98,8 @@ export function prepareDiagnosticRunBundle(context, input) {
60
98
  const execution = put("runtime/execution.json", bytes(frozen.settings));
61
99
  const engineBinding = put("runtime/engine-binding.json", bytes(binding));
62
100
  const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
63
- put("runtime/diagnostic-snapshot.json", bytes(snapshot));
101
+ if (snapshot)
102
+ put("runtime/diagnostic-snapshot.json", bytes(snapshot));
64
103
  const provenance = { canon: input.source.canonProvenance(), project_pack: null };
65
104
  const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: flowId,
66
105
  descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
5
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
7
  import { appendAudit } from "./audit.js";
8
8
  import { reviewSourceHash, reviewSourceFilter, reviewExecutionCopyHash } from "./review-copy.js";
@@ -32,7 +32,7 @@ export function diagnosticSnapshotPaths(context, value, runId) {
32
32
  }
33
33
  function validate(context, input, runId) {
34
34
  const physical = physicalSnapshot(context, input, runId);
35
- if (!isDiagnosticFlow(input.flowId) || !input.requestId?.trim() || input.requestId.length > 200
35
+ if (!isSourceSnapshotFlow(input.flowId) || !input.requestId?.trim() || input.requestId.length > 200
36
36
  || !path.isAbsolute(physical.projectRoot) || physical.projectRoot !== physical.sourceRoot
37
37
  || fs.realpathSync(physical.sourceRoot) !== physical.sourceRoot)
38
38
  refuse("invalid_selection");
@@ -8,7 +8,7 @@ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
8
8
  import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
9
  import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
10
10
  import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
11
- import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
11
+ import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, materializeDiagnosticRetainedFields, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
12
12
  import { readAcceptedStageData } from "./stage-report-renderer.js";
13
13
  import { registerDiagnosticStageGraph } from "./diagnostic-work-registration.js";
14
14
  import { PreparedDiagnosticWorkCollection } from "./diagnostic-work-collection.js";
@@ -53,7 +53,8 @@ export function finishDiagnosticStage(context, input) {
53
53
  const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
54
54
  const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
55
55
  const collection = ["audit", "analyze", "review"].includes(input.stage) ? PreparedDiagnosticWorkCollection.prepare(context, input) : null;
56
- const candidates = ["audit", "analyze"].includes(input.stage) ? sealDiagnosticCandidateIds(input.bytes, collection.results()) : input.bytes;
56
+ const proposal = materializeDiagnosticRetainedFields(input.bytes, prior, input.stage);
57
+ const candidates = ["audit", "analyze"].includes(input.stage) ? sealDiagnosticCandidateIds(proposal, collection.results()) : proposal;
57
58
  const bytes = sealDiagnosticStageLimitations(candidates, [
58
59
  ...(prior?.details.limitations ?? []), ...(collection?.results() ?? []).flatMap(work => work.result.limitations)
59
60
  ], prior?.details.missing_expected_sources ?? []);
@@ -7,6 +7,32 @@ import { verifyDiagnosticCoverage, verifyDiagnosticSources } from "./diagnostic-
7
7
  const refuse = (reason) => {
8
8
  throw new AppError("diagnostic_stage_result_invalid", "Diagnostic result does not match its frozen stage", 2, { reason, effect: "no_effect", ...(reason === "result_schema_invalid" ? { phase: "prepare", recoverable: true } : {}) });
9
9
  };
10
+ /** Retained fields are CLI data, not a transcription task for the coordinator. */
11
+ export function materializeDiagnosticRetainedFields(bytes, prior, stage) {
12
+ if (!prior || !bytes.length || bytes.length > 1024 * 1024)
13
+ return bytes;
14
+ let value;
15
+ try {
16
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
17
+ }
18
+ catch {
19
+ return bytes;
20
+ }
21
+ if (!value || typeof value !== "object" || !value.details || typeof value.details !== "object" || Array.isArray(value.details))
22
+ return bytes;
23
+ const retained = stage === "report" ? Object.entries(prior.details).filter(([key]) => key !== "limitations")
24
+ : ["focus", "coverage"].map(key => [key, prior.details[key]]);
25
+ for (const [key, expected] of retained) {
26
+ if (Object.hasOwn(value.details, key) && hybridDigest(value.details[key]) !== hybridDigest(expected))
27
+ throw new AppError("diagnostic_stage_invalid", "A proposal cannot replace CLI-retained predecessor data", 2, { reason: `accepted_${key}_changed`, effect: "no_effect", phase: "prepare", recoverable: true });
28
+ value.details[key] = structuredClone(expected);
29
+ }
30
+ if (!Object.hasOwn(value.details, "limitations"))
31
+ value.details.limitations = [];
32
+ if (!Object.hasOwn(value.details, "missing_expected_sources"))
33
+ value.details.missing_expected_sources = structuredClone(prior.details.missing_expected_sources);
34
+ return Buffer.from(JSON.stringify(value));
35
+ }
10
36
  /** Accepted limitations and missing-source observations belong to predecessor/Work custody, not a coordinator's
11
37
  * transcription. Keep its annotations too; validate the complete sealed result
12
38
  * with the frozen schema afterwards, without truncating overflowing data. */