@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 +31 -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 +274 -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 +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  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 +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  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 +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  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 +250 -33
  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 -7
  190. package/dist/services/retained-native-hook.js +274 -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 +25 -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 +257 -44
  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 +8 -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 +255 -69
  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 +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,99 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
3
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
4
+ import { FrozenGenericExecution } from "./generic-execution.js";
5
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { readRunStartRequest, runStartPayloadLimit } from "./run-start-custody.js";
8
+ import { PreparedDescriptorRunStart } from "./descriptor-run-start.js";
9
+ import { prepareDeliveryRunBundle } from "./diagnostic-run-bundle.js";
10
+ import { appendAudit } from "./audit.js";
11
+ import { hybridDigest } from "./hybrid-contracts.js";
12
+ const event = "delivery.request";
13
+ function refuse(reason) {
14
+ throw new AppError("delivery_request_invalid", "Delivery requires its original request, workspace and frozen selection", 2, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
15
+ }
16
+ /** SQL/data only: retries never consult a newer operator selection. */
17
+ export function readDeliveryRequest(context, scope) {
18
+ if (!isDeliveryFlow(scope.flowId) || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId))
19
+ refuse("invalid_scope");
20
+ const project = requireProjectByRoot(context, scope.projectRoot);
21
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope.requestId')=? LIMIT 2", [project.id, event, scope.requestId]);
22
+ if (!rows.length)
23
+ return null;
24
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > runStartPayloadLimit)
25
+ refuse("ambiguous_request");
26
+ const value = JSON.parse(rows[0].payload_json);
27
+ if (value.schema_id !== "dd-flow/delivery-request@1" || value.project_id !== project.id
28
+ || hybridDigest(value.scope) !== hybridDigest(scope) || !Number.isFinite(Date.parse(value.created_at))
29
+ || typeof value.objective !== "string" || !value.objective.trim() || Buffer.byteLength(value.objective) > 64 * 1024
30
+ || !value.execution || hybridDigest(value.execution) !== value.execution_sha256)
31
+ refuse("request_changed");
32
+ return value;
33
+ }
34
+ export function retainDeliveryRequest(context, input) {
35
+ if (context.db.isOwnedWriteTransaction?.())
36
+ refuse("prepare_outside_writer");
37
+ if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
38
+ || !(input.engine instanceof AdmittedFrozenEngine) || !isDeliveryFlow(input.scope.flowId))
39
+ refuse("prepared_selection_required");
40
+ const frozen = input.execution.snapshot(), flow = input.source.resolveRegisteredFlow(), project = requireProjectByRoot(context, input.scope.projectRoot);
41
+ if (frozen.scope.projectRoot !== input.scope.projectRoot || frozen.scope.requestId !== input.scope.requestId
42
+ || frozen.settings.flow_id !== input.scope.flowId || flow.descriptor.id !== input.scope.flowId
43
+ || frozen.settings.descriptor_sha256 !== flow.sha256 || frozen.settings.stage_session_mode === "same_session"
44
+ || frozen.settings.attached_session_receipt || frozen.settings.delegation.native_capacity_receipt
45
+ || !input.objective.trim() || Buffer.byteLength(input.objective) > 64 * 1024)
46
+ refuse("selection_changed");
47
+ input.source.revalidate();
48
+ input.engine.admitDeliveryFlow(input.source, input.scope.flowId);
49
+ const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
50
+ if (typeof checksum !== "string")
51
+ refuse("engine_identity_required");
52
+ const value = { schema_id: "dd-flow/delivery-request@1", project_id: project.id, scope: structuredClone(input.scope),
53
+ created_at: context.now(), objective: input.objective, selection: structuredClone(input.selection),
54
+ source: input.source.snapshot(), source_sha256: input.source.sha256, execution: frozen, execution_sha256: input.execution.sha256,
55
+ engine: { identity: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
56
+ integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: input.engine.manifestSha256 } };
57
+ if (Buffer.byteLength(JSON.stringify(value)) > runStartPayloadLimit)
58
+ refuse("request_exceeds_bound");
59
+ return context.db.writeTransaction(() => {
60
+ const prior = readDeliveryRequest(context, input.scope);
61
+ if (prior) {
62
+ if (hybridDigest({ ...value, created_at: prior.created_at }) !== hybridDigest(prior))
63
+ refuse("request_conflict");
64
+ return prior;
65
+ }
66
+ if (readRunStartRequest(context, project.id, input.scope.requestId))
67
+ refuse("foreign_run_request");
68
+ appendAudit(context, { projectId: project.id, eventType: event, payload: value });
69
+ return value;
70
+ });
71
+ }
72
+ /** Called inside the common RUN allocation writer: retained data only. */
73
+ export function assertDeliveryRequestBundle(context, projectId, bundle) {
74
+ const frozen = bundle.execution.snapshot(), flowId = bundle.manifest.flow_id;
75
+ if (!isDeliveryFlow(flowId))
76
+ refuse("delivery_bundle_required");
77
+ const request = readDeliveryRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
78
+ if (!request || request.project_id !== projectId || request.execution_sha256 !== bundle.execution.sha256
79
+ || request.source.descriptor.sha256 !== bundle.manifest.descriptor.sha256 || bundle.manifest.bootstrap_receipts.length
80
+ || request.engine.manifest_sha256 !== bundle.manifest.engine_manifest.sha256
81
+ || !bundle.blob("runtime/source-request.md").equals(Buffer.from(request.objective)))
82
+ refuse("original_request_bundle_required");
83
+ for (const item of request.source.blobs)
84
+ if (!bundle.blob(item.path).equals(Buffer.from(item.base64, "base64")))
85
+ refuse("source_bytes_changed");
86
+ }
87
+ export function startDeliveryRun(context, scope) {
88
+ const request = readDeliveryRequest(context, scope);
89
+ if (!request)
90
+ refuse("original_request_required");
91
+ const project = requireProjectByRoot(context, scope.projectRoot);
92
+ if (readRunStartRequest(context, project.id, scope.requestId))
93
+ return PreparedDescriptorRunStart.restore(context, scope).apply(context);
94
+ const source = PreparedInstalledFlowSource.restoreRetained(request.source, request.source_sha256);
95
+ const execution = FrozenGenericExecution.restore(request.execution, request.execution_sha256, source.resolveRegisteredFlow(), request.execution.scope);
96
+ const engine = AdmittedFrozenEngine.admitSelection(request.engine.identity, request.engine.manifest_sha256);
97
+ const bundle = prepareDeliveryRunBundle(context, { source, execution, engine, boundAt: request.created_at, requestText: request.objective });
98
+ return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
99
+ }
@@ -0,0 +1,144 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { types } from "node:util";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { resolveDeliveryBinding } from "./delivery-policy.js";
6
+ import { executeDeliveryStep } from "./delivery-execution.js";
7
+ import { readDeliveryIntent } from "./delivery-intents.js";
8
+ import { appendAudit } from "./audit.js";
9
+ import { managedProcessStatus, managedProcessBinding, processTreeIsAlive, resourceHome } from "./managed-processes.js";
10
+ const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
+ const text = (value) => typeof value === "string" && Boolean(value.trim()) && value.length <= 4096 && !value.includes("\0");
12
+ const reference = (value) => value === null || record(value) && Object.keys(value).sort().join() === "id,kind" && text(value.id) && text(value.kind);
13
+ function validRollback(value, operation) {
14
+ if (operation !== "deploy")
15
+ return !Object.hasOwn(value, "rollback");
16
+ const rollback = value.rollback;
17
+ return record(rollback) && Object.keys(rollback).sort().join() === "reason,reference,status,strategy"
18
+ && typeof rollback.strategy === "string" && ["rollback", "roll_forward", "operator_handoff"].includes(rollback.strategy)
19
+ && typeof rollback.status === "string" && ["not_needed", "required", "completed", "failed", "unknown"].includes(rollback.status)
20
+ && text(rollback.reason) && reference(rollback.reference)
21
+ && (rollback.status !== "completed" || rollback.reference !== null)
22
+ && (value.status === "complete" ? rollback.status === "not_needed" : rollback.status !== "not_needed");
23
+ }
24
+ function refuse(reason) {
25
+ throw new AppError("delivery_result_unproven", "Read back the original delivery; do not repeat its mutation", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
26
+ }
27
+ /** Settlement reads the latest owned observation, never a supplied filename
28
+ * or an older optimistic answer after a newer uncertain readback. A changed
29
+ * operation log needs a new observation; this is not a retry authorization. */
30
+ export function readDeliveryResult(context, scope) {
31
+ const intent = readDeliveryIntent(context, scope);
32
+ if (!intent)
33
+ refuse("original_intent_required");
34
+ const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.operation_readback' AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, scope.runId]);
35
+ if (!row || Buffer.byteLength(row.payload_json) > 131072)
36
+ refuse("retained_readback_required");
37
+ let receipt;
38
+ try {
39
+ receipt = JSON.parse(row.payload_json);
40
+ }
41
+ catch {
42
+ return refuse("retained_readback_invalid");
43
+ }
44
+ const grant = intent.plan.approval.scope, observed = receipt?.observation;
45
+ if (receipt?.schema_id !== "dd-flow/delivery-result@1" || receipt.run_id !== scope.runId || receipt.intent_sha256 !== intent.identity
46
+ || receipt.operation_head_sha256 !== intent.head || !observed || observed.schema_id !== "dd-flow/delivery-result-observation@1"
47
+ || observed.operation_id !== grant.operation_id || observed.binding_id !== grant.binding_id
48
+ || hybridDigest(observed.artifact) !== hybridDigest(grant.artifact) || hybridDigest(observed.selector) !== hybridDigest(grant.selector)
49
+ || !["complete", "absent", "partial", "unknown"].includes(observed.status)
50
+ || !reference(observed.reference) || observed.status === "complete" && observed.reference === null
51
+ || !validRollback(observed, grant.selector.operation)
52
+ || !Number.isFinite(Date.parse(receipt.observed_at)) || !Array.isArray(receipt.processes) || !receipt.processes.length
53
+ || receipt.processes.length > 16 || receipt.processes.some(id => typeof id !== "string" || !id.startsWith("PROC-")))
54
+ refuse("retained_readback_changed");
55
+ return { sha256: hybridDigest(receipt), receipt: structuredClone(receipt) };
56
+ }
57
+ /** Private result/reconciliation observation. The caller proves the actual
58
+ * Stage/coordinator authority and project readback safety. No new approval,
59
+ * login, retry, rollback or terminal Stage/RUN decision is inferred here.
60
+ * Reconciliation intentionally does not require unexpired mutation approval
61
+ * or local artifact availability: an earlier effect must remain observable. */
62
+ export async function observeDeliveryResult(context, input) {
63
+ if (context.db.isOwnedWriteTransaction?.())
64
+ refuse("observation_outside_writer_required");
65
+ const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [input.binding] }, input.binding.selector);
66
+ const original = readDeliveryIntent(context, input);
67
+ if (!original)
68
+ refuse("original_intent_required");
69
+ const grant = original.plan.approval.scope;
70
+ if (hybridDigest(binding) !== grant.binding_sha256)
71
+ refuse("original_binding_required");
72
+ const assertOwner = () => {
73
+ const checked = input.assertCurrent();
74
+ if (types.isPromise(checked)) {
75
+ void checked.catch(() => { });
76
+ refuse("synchronous_owner_fence_required");
77
+ }
78
+ const current = readDeliveryIntent(context, input);
79
+ if (!current || current.identity !== original.identity || current.head !== original.head)
80
+ refuse("original_operation_changed");
81
+ return current;
82
+ };
83
+ const assertCurrent = () => {
84
+ const current = assertOwner();
85
+ // The old command may lack a result event after interruption. Readback is
86
+ // allowed only after every originally admitted process is physically quiet.
87
+ const processes = managedProcessStatus(context);
88
+ for (const event of current.events) {
89
+ if (event.kind !== "admitted")
90
+ continue;
91
+ const process = processes.find(item => item.id === event.process.process_id);
92
+ if (event.process.resource_home !== resourceHome(context) || !process || process.state !== "stopped"
93
+ || managedProcessBinding(process, resourceHome(context)).registration_sha256 !== event.process.registration_sha256
94
+ || processTreeIsAlive(process))
95
+ refuse("original_process_not_settled");
96
+ }
97
+ };
98
+ assertCurrent();
99
+ const procedure = binding.procedures.result_readback, deadline = Date.now() + procedure.timeout_ms, processes = [];
100
+ let observed;
101
+ for (const step of procedure.procedure_steps) {
102
+ let value;
103
+ try {
104
+ const result = await executeDeliveryStep(context, { projectId: input.projectId, runId: input.runId,
105
+ operationId: `result-${randomUUID()}`, stepId: step.id, workspace: original.plan.artifact.workspace,
106
+ procedure, deadlineAt: deadline, assertCurrent, admit() { }, consumeOutput(bytes) {
107
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
108
+ } });
109
+ processes.push(result.process_id);
110
+ }
111
+ catch {
112
+ return refuse("readback_failed");
113
+ }
114
+ const keys = binding.selector.operation === "deploy" ? "artifact,binding_id,operation_id,reference,rollback,schema_id,selector,status"
115
+ : "artifact,binding_id,operation_id,reference,schema_id,selector,status";
116
+ if (!record(value) || Object.keys(value).sort().join() !== keys
117
+ || value.schema_id !== "dd-flow/delivery-result-observation@1" || value.operation_id !== grant.operation_id || value.binding_id !== binding.id
118
+ || hybridDigest(value.artifact) !== hybridDigest(grant.artifact) || hybridDigest(value.selector) !== hybridDigest(grant.selector)
119
+ || !["complete", "absent", "partial", "unknown"].includes(value.status)
120
+ || !reference(value.reference) || !validRollback(value, binding.selector.operation)
121
+ || value.status === "complete" && value.reference === null)
122
+ refuse("result_context_mismatch_or_unsafe_output");
123
+ const next = value;
124
+ // Ordered steps may confirm the same result; a conflicting earlier answer
125
+ // is not silently overwritten by a more optimistic later command.
126
+ if (observed && hybridDigest(observed) !== hybridDigest(next))
127
+ refuse("conflicting_readback");
128
+ observed = structuredClone(next);
129
+ }
130
+ assertCurrent();
131
+ if (!observed || Date.now() >= deadline)
132
+ refuse("readback_incomplete");
133
+ const receipt = { schema_id: "dd-flow/delivery-result@1", run_id: input.runId, intent_sha256: original.identity,
134
+ operation_head_sha256: original.head, observed_at: context.now(), observation: observed, processes };
135
+ if (!Number.isFinite(Date.parse(receipt.observed_at)))
136
+ refuse("observation_clock_invalid");
137
+ return context.db.writeTransaction(() => {
138
+ // Physical observation is already settled. Only current SQL/owner fences
139
+ // run in this short writer; receipt persistence does not close the intent.
140
+ assertOwner();
141
+ appendAudit(context, { projectId: input.projectId, eventType: "delivery.operation_readback", payload: receipt });
142
+ return structuredClone(receipt);
143
+ });
144
+ }
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import { deliveryFlowStages } from "../domain/delivery-flows.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { runtimeCommand } from "./runtime-command.js";
5
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
6
+ /** Packet candidates are not invocation admission or mutation approval. */
7
+ export function deliveryStageCommands(context, input) {
8
+ if (!deliveryFlowStages[input.flowId].includes(input.stage))
9
+ throw new AppError("delivery_stage_invalid", "Unknown delivery Stage", 2, { effect: "no_effect" });
10
+ const command = (operation, file) => runtimeCommand(context, ["delivery", operation, input.runId,
11
+ "--project-root", input.projectRoot, "--stage", input.stage, ...(file ? ["--result-file", file] : []), "--json"]);
12
+ return {
13
+ inputs: command("inputs"),
14
+ ...(["prepare", "verify"].includes(input.stage) ? { checks: command("checks", path.join(input.stageRoot, "delivery-artifact.json")) } : {}),
15
+ ...(["publish", "deploy"].includes(input.stage) ? { execute: command("execute", path.join(input.stageRoot, "delivery-approval.json")) } : {}),
16
+ ...(["publish", "deploy", "verify", "finish"].includes(input.stage) ? { reconcile: command("reconcile") } : {})
17
+ };
18
+ }
19
+ /** Issue only after the native Stage coordinator is bound. Keep assignments
20
+ * out of immutable semantic packets and include literal commands for custody. */
21
+ export function issueDeliveryStageCommands(context, input) {
22
+ const commands = Object.fromEntries(Object.entries(deliveryStageCommands(context, input))
23
+ .map(([operation, candidate]) => [operation, operation === "inputs" ? candidate : managedLifecycleCommand(context, candidate)]));
24
+ const instructions = `\n## Bound delivery command candidates\n\n\`\`\`json\n${JSON.stringify(commands, null, 2)}\n\`\`\`\n\nWrite the exact artifact/approval JSON to the issued result-file before checks/execute. Starting this Stage grants no mutation approval. Reconcile the original intent; never retry an unknown effect.\n\n## Current issued delivery commands\n\n${Object.values(commands).join("\n\n")}\n`;
25
+ return { commands, instructions };
26
+ }
@@ -0,0 +1,56 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { deliveryFlowStages } from "../domain/delivery-flows.js";
3
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
4
+ import { requireProjectByRoot } from "./projects.js";
5
+ import { resolveProjectRoot } from "../storage/paths.js";
6
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
7
+ import { hybridDigest } from "./hybrid-contracts.js";
8
+ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
9
+ import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
10
+ import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
11
+ import { currentLifecycleAdmission, lifecycleInvocationScope, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
12
+ function refuse(reason) {
13
+ throw new AppError("delivery_stage_invalid", "Delivery requires its registered flow, original request and current Stage owner", 2, { reason, effect: "no_effect" });
14
+ }
15
+ /** Internal family entry, not a public launcher or permission to publish.
16
+ * The ordinary descriptor runtime owns attempt, coordinator Work and Session.
17
+ * Delivery uses its actual workspace, not a read-only diagnostic clone. */
18
+ export function readDeliveryStageScope(context, input) {
19
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
20
+ const bundle = readFlowRunExecutionBundle(context, input), flow = bundle?.manifest.flow_id;
21
+ if (!bundle || !flow || !Object.hasOwn(deliveryFlowStages, flow)
22
+ || !deliveryFlowStages[flow].includes(input.stage))
23
+ refuse("registered_delivery_stage_required");
24
+ const flowId = flow, scope = { projectId: project.id, runId: input.runId, stage: input.stage };
25
+ bundle.descriptor.stage(input.stage);
26
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
27
+ const users = readPreparedMemoryFlowUserInputs(context, scope, flowId, bundle), userInputsSha256 = hybridDigest(users);
28
+ return { scope, bundle, run, flowId, users, userInputsSha256,
29
+ assertCustody(writer) {
30
+ assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
31
+ if (hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, flowId, bundle)) !== userInputsSha256)
32
+ refuse("user_inputs_changed_during_entry");
33
+ }
34
+ };
35
+ }
36
+ export function startDeliveryStage(context, input) {
37
+ const selected = readDeliveryStageScope(context, input);
38
+ const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, selected.assertCustody);
39
+ const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, selected.assertCustody);
40
+ return { ...start, ...coordinator, flow_id: selected.flowId, user_inputs_sha256: selected.userInputsSha256 };
41
+ }
42
+ /** A matching SQL coordinator or ambient scope is not a command capability.
43
+ * The ordinary lifecycle gate must already have admitted this exact CLI process. */
44
+ export function assertDeliveryCommandOwner(context, input) {
45
+ const id = context.env.DD_FLOW_CURRENT_INVOCATION;
46
+ if (!id || !currentLifecycleAdmission(context, id))
47
+ refuse("issued_coordinator_command_required");
48
+ const native = lifecycleInvocationScope(context, id);
49
+ assertLifecycleInvocationCurrent(context, native, input.command);
50
+ const selected = readDeliveryStageScope(context, input), actor = readActiveDescriptorStageCoordinator(context, selected.scope, selected.bundle);
51
+ const hook = context.db.get("SELECT daemon_id FROM hook_events WHERE project_id=? AND event_key=?", [selected.scope.projectId, actor.start_hook]);
52
+ if (native.projectRoot !== input.projectRoot || native.runId !== input.runId || native.rootSessionId !== actor.provider_session_id
53
+ || !hook?.daemon_id || hook.daemon_id !== native.daemonId)
54
+ refuse("current_physical_coordinator_required");
55
+ return selected;
56
+ }
@@ -0,0 +1,135 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { deliveryFlowStages } from "../domain/delivery-flows.js";
4
+ import { readDeliveryStageScope, startDeliveryStage } from "./delivery-stage-entry.js";
5
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
6
+ import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
7
+ import { readAcceptedStageData, prepareStageReport, acceptStageReport, publishStageReport } from "./stage-report-renderer.js";
8
+ import { prepareDeliveryStageResult } from "./delivery-stage-results.js";
9
+ import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ import { prepareDeliveryChecks, prepareDeliveryHistoricalChecks } from "./delivery-checks.js";
12
+ import { deliveryApprovalScope } from "./delivery-approval.js";
13
+ import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
14
+ import { readDeliveryIntent } from "./delivery-intents.js";
15
+ import { readDeliveryResult } from "./delivery-result.js";
16
+ import { appendAudit } from "./audit.js";
17
+ export const deliveryStageHandlers = Object.freeze(Object.entries(deliveryFlowStages).flatMap(([flowId, stages]) => stages.map(stage => Object.freeze({ flowId, stage, entryId: "delivery-stage-entry", finishId: "delivery-stage-finish",
18
+ entry: startDeliveryStage, finish: finishDeliveryStage }))));
19
+ function refuse(reason) {
20
+ throw new AppError("delivery_stage_invalid", "Delivery Stage requires observed project policy and owned evidence", 2, { reason, effect: "no_effect", phase: "prepare", recoverable: true });
21
+ }
22
+ /** Family settlement never runs the protected command. Only the operation
23
+ * owner can retain its original effect/readback; Stage reports cannot invent it.
24
+ * Public family admission/launch still requires its separate installed gates. */
25
+ export function finishDeliveryStage(context, input) {
26
+ const selected = readDeliveryStageScope(context, input), { scope, bundle, flowId } = selected;
27
+ const stages = deliveryFlowStages[flowId], index = stages.indexOf(input.stage);
28
+ const packet = readDescriptorStageInput(context, scope, bundle, true, true);
29
+ const previous = index ? readAcceptedStageData(context, { ...scope, stage: stages[index - 1] }) : null;
30
+ let decision = null;
31
+ let policy;
32
+ if (index === 1) {
33
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
34
+ refuse("result_exceeds_bound");
35
+ try {
36
+ decision = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)).details?.decision;
37
+ }
38
+ catch {
39
+ return refuse("invalid_result_json");
40
+ }
41
+ if (!decision?.policy || typeof decision.policy.path !== "string" || typeof decision.policy.sha256 !== "string"
42
+ || !decision.binding?.selector || typeof decision.version !== "string" || !decision.version.trim())
43
+ refuse("project_policy_required");
44
+ const workspace = bundle.execution.snapshot().scope.workspace;
45
+ policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
46
+ if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
47
+ refuse("project_policy_binding_mismatch");
48
+ }
49
+ const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
50
+ let checks;
51
+ let verification;
52
+ let artifact = null;
53
+ let historical;
54
+ if (index > 1) {
55
+ decision = prior?.details?.decision;
56
+ artifact = prior?.details?.artifact;
57
+ }
58
+ if (["prepare", "verify"].includes(input.stage)) {
59
+ decision = prior?.details?.decision;
60
+ if (input.stage === "prepare") {
61
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
62
+ refuse("result_exceeds_bound");
63
+ try {
64
+ artifact = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)).details?.artifact;
65
+ }
66
+ catch {
67
+ return refuse("invalid_result_json");
68
+ }
69
+ }
70
+ else
71
+ artifact = prior?.details?.artifact;
72
+ if (!artifact || !decision)
73
+ refuse("accepted_artifact_required");
74
+ checks = prepareDeliveryChecks(context, { ...input, artifact });
75
+ verification = checks.evidence();
76
+ }
77
+ const afterEffect = index >= stages.indexOf(flowId === "deploy" ? "deploy" : "publish");
78
+ const intent = afterEffect ? readDeliveryIntent(context, scope) : null;
79
+ const readback = afterEffect ? readDeliveryResult(context, scope) : null;
80
+ const operation = decision && artifact ? deliveryApprovalScope(scope, bundle, { operationId: decision.operation_id, binding: decision.binding,
81
+ artifact: { subject_ref: artifact.subject_ref, version: artifact.version, sha256: artifact.sha256 } }) : null;
82
+ if (afterEffect && (!intent || hybridDigest(intent.plan.approval.scope) !== hybridDigest(operation)))
83
+ refuse("original_delivery_scope_required");
84
+ if (afterEffect && !checks)
85
+ historical = prepareDeliveryHistoricalChecks(context, { ...input,
86
+ sourceStage: input.stage === "deploy" ? "prepare" : "verify", operationScopeSha256: hybridDigest(operation) });
87
+ const binding = { flowId, runId: input.runId, stage: input.stage,
88
+ inputSha256: packet.inputSha256, userInputsSha256: selected.userInputsSha256,
89
+ predecessors: packet.input.predecessor ? [{ stage: packet.input.predecessor.stage, ...packet.input.predecessor.result }] : [],
90
+ decision, artifact, deliveryScopeSha256: operation ? hybridDigest(operation) : null,
91
+ verificationSha256: verification ? hybridDigest(verification.receipt) : historical?.sha256 ?? null, intentSha256: intent?.identity ?? null,
92
+ readbackSha256: readback?.sha256 ?? null, readbackStatus: readback?.receipt.observation.status ?? null,
93
+ rollback: readback?.receipt.observation.rollback ?? null, retainedLimitations: prior?.details?.limitations ?? [] };
94
+ const bytes = prepareDeliveryStageResult(input.bytes, bundle.descriptor.stageBlob(input.stage, "result_schema"), binding);
95
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes });
96
+ policy?.assertCurrent();
97
+ checks?.revalidate();
98
+ verification?.proof.revalidate(context);
99
+ historical?.revalidate();
100
+ const reportRoot = input.stage === "finish" ? path.join(packet.runRoot, bundle.descriptor.stage(input.stage).directory) : "";
101
+ const report = input.stage === "finish" ? prepareStageReport({ run_id: input.runId, stage: "finish", verdict: "completed",
102
+ summary: JSON.parse(bytes.toString("utf8")).summary, delivery: { result: JSON.parse(bytes.toString("utf8")), readback: readback?.receipt,
103
+ verification: historical?.receipt }, semantic: { next_action: "The exact operation was observed; this report grants no new effect or retry." } }) : null;
104
+ const fence = (writer) => {
105
+ selected.assertCustody(writer);
106
+ if (previous && hybridDigest(readAcceptedStageData(writer, { ...scope, stage: stages[index - 1] })) !== hybridDigest(previous))
107
+ refuse("accepted_predecessor_changed");
108
+ checks?.assertCustody(writer);
109
+ verification?.proof.assertCustody(writer);
110
+ historical?.assertCustody(writer);
111
+ if (intent && readDeliveryIntent(writer, scope)?.identity !== intent.identity || readback && readDeliveryResult(writer, scope).sha256 !== readback.sha256)
112
+ refuse("delivery_readback_changed");
113
+ if (verification) {
114
+ const payload = { run_id: input.runId, stage: input.stage, sha256: hybridDigest(verification.receipt), receipt: verification.receipt };
115
+ const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.stage_checks' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, input.stage]);
116
+ if (rows.length > 1 || rows.length && hybridDigest(JSON.parse(rows[0].payload_json)) !== hybridDigest(payload))
117
+ refuse("accepted_verification_changed");
118
+ if (!rows.length)
119
+ appendAudit(writer, { projectId: scope.projectId, eventType: "delivery.stage_checks", payload });
120
+ }
121
+ if (report) {
122
+ const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='finish' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, packet.attempt]);
123
+ if (rows.length > 1 || rows.length && JSON.parse(rows[0].payload_json).sha256 !== report.sha256)
124
+ refuse("terminal_report_changed");
125
+ if (!rows.length)
126
+ acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
127
+ }
128
+ };
129
+ const accepted = settlement.apply(context, fence);
130
+ if (!report)
131
+ return accepted;
132
+ const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
133
+ publishStageReport(context, scope.projectId, input.runId, "finish", reportRoot);
134
+ return { ...accepted, ...complete, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
135
+ }
@@ -0,0 +1,94 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { deliveryFlowStages } from "../domain/delivery-flows.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { resolveDeliveryBinding } from "./delivery-policy.js";
6
+ function refuse(reason, parameter, message = "Delivery Stage requires its accepted selection and observed operation evidence") {
7
+ throw new AppError("delivery_stage_result_invalid", message, 2, { reason, ...(parameter ? { parameter } : {}), effect: "no_effect", phase: "prepare", recoverable: true });
8
+ }
9
+ /** Projection only. Bindings must be independently read from accepted Stage,
10
+ * check, approval, intent and readback owners, not copied from a model result.
11
+ * This validator does not authorize execution or prove semantic sufficiency. */
12
+ export function prepareDeliveryStageResult(bytes, schemaBytes, binding) {
13
+ if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
14
+ refuse("result_exceeds_bound");
15
+ let proposal, validate;
16
+ try {
17
+ proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
18
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
19
+ if (schema.$id !== "dd-flow/delivery-stage-result@1")
20
+ refuse("frozen_schema_invalid");
21
+ validate = new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
22
+ }
23
+ catch (error) {
24
+ if (error instanceof AppError)
25
+ throw error;
26
+ return refuse("invalid_schema");
27
+ }
28
+ if (!proposal || typeof proposal !== "object" || Array.isArray(proposal) || !proposal.details
29
+ || typeof proposal.details !== "object" || Array.isArray(proposal.details))
30
+ refuse("invalid_schema");
31
+ const stages = deliveryFlowStages[binding.flowId], index = stages.indexOf(binding.stage);
32
+ if (index < 0 || binding.predecessors.length !== (index ? 1 : 0)
33
+ || index && binding.predecessors[0]?.stage !== stages[index - 1])
34
+ refuse("foreign_stage_predecessor");
35
+ const owned = { schema_id: "dd-flow/delivery-stage-result@1", flow_id: binding.flowId, run_id: binding.runId,
36
+ stage: binding.stage, input_sha256: binding.inputSha256, user_inputs_sha256: binding.userInputsSha256, predecessors: binding.predecessors };
37
+ const details = proposal.details;
38
+ const proof = { decision: binding.decision, artifact: binding.artifact, delivery_scope_sha256: binding.deliveryScopeSha256,
39
+ intent_sha256: binding.intentSha256, readback_sha256: binding.readbackSha256, verification_sha256: binding.verificationSha256,
40
+ rollback: binding.rollback };
41
+ for (const [target, values] of [[proposal, owned], [details, proof]]) {
42
+ for (const [key, value] of Object.entries(values)) {
43
+ if (Object.hasOwn(target, key) && hybridDigest(target[key]) !== hybridDigest(value)) {
44
+ const parameter = target === proposal ? key : `details.${key}`;
45
+ refuse("unowned_stage_claim", parameter, value === null
46
+ ? `${binding.stage} has no accepted ${parameter}. Omit this CLI-owned field or set it to null; do not advance selection or operation evidence into this Stage.`
47
+ : `${parameter} differs from the CLI-owned Stage evidence. Use the current Stage and project-policy values, or omit a projected field; do not invent or copy a different binding.`);
48
+ }
49
+ target[key] = structuredClone(value);
50
+ }
51
+ }
52
+ if (!Array.isArray(details.limitations) || details.limitations.some(value => typeof value !== "string" || !value.trim()))
53
+ refuse("invalid_schema");
54
+ details.limitations = [...new Set([...binding.retainedLimitations, ...details.limitations])];
55
+ if (!validate(proposal) || typeof proposal.summary !== "string" || !proposal.summary.trim())
56
+ refuse("invalid_schema");
57
+ if (binding.stage === "preflight") {
58
+ if (Object.values(proof).some(value => value !== null) || binding.readbackStatus !== null)
59
+ refuse("preflight_cannot_grant_delivery");
60
+ }
61
+ else {
62
+ const decision = binding.decision;
63
+ if (!decision)
64
+ refuse("accepted_decision_required");
65
+ const normalized = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [decision.binding] }, decision.binding.selector);
66
+ if (normalized.selector.operation !== binding.flowId)
67
+ refuse("foreign_delivery_decision");
68
+ if (binding.flowId === "deploy" ? !decision.rollback : decision.rollback !== undefined)
69
+ refuse("deployment_rollback_plan_required");
70
+ if (decision.rollback && (!decision.rollback.reason.trim()
71
+ || decision.rollback.strategy !== "operator_handoff" && !decision.rollback.procedure_ref?.trim()))
72
+ refuse("deployment_rollback_plan_required");
73
+ if (index >= 2) {
74
+ const artifact = binding.artifact;
75
+ if (!artifact || artifact.subject_ref !== normalized.selector.subject_ref || artifact.version !== decision.version
76
+ || !binding.deliveryScopeSha256 || !binding.verificationSha256)
77
+ refuse("verified_artifact_required");
78
+ }
79
+ else if (binding.artifact !== null || binding.deliveryScopeSha256 !== null || binding.verificationSha256 !== null)
80
+ refuse("premature_artifact_claim");
81
+ const mutationIndex = stages.indexOf(binding.flowId === "deploy" ? "deploy" : "publish");
82
+ if (index >= mutationIndex) {
83
+ if (!binding.intentSha256 || !binding.readbackSha256 || binding.readbackStatus !== "complete")
84
+ refuse("completed_delivery_readback_required");
85
+ if (binding.flowId === "deploy" && binding.rollback?.status !== "not_needed")
86
+ refuse("deployment_recovery_unresolved");
87
+ }
88
+ else if (binding.intentSha256 !== null || binding.readbackSha256 !== null || binding.readbackStatus !== null)
89
+ refuse("premature_delivery_claim");
90
+ if ((binding.flowId !== "deploy" || index < mutationIndex) && binding.rollback !== null)
91
+ refuse("premature_rollback_claim");
92
+ }
93
+ return Buffer.from(JSON.stringify(proposal));
94
+ }
@@ -4,6 +4,7 @@ import { readDescriptorStageInput, readDescriptorStageInputForRecovery } from ".
4
4
  import { runtimeHarness } from "./execution-policy.js";
5
5
  import { stageGraphScope } from "./stage-work-graph.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { hasAssignedCoordinatorLaunch } from "./assigned-lifecycle.js";
7
8
  /** SQL-only physical coordinator proof, independent of allocation/prompt
8
9
  * custody. It creates no Session, hook claim or provider operation.
9
10
  */
@@ -20,7 +21,8 @@ export function readDescriptorCoordinatorSession(context, input, refuse = reason
20
21
  || session.result_path !== path.join(selected.runRoot, directory, "result-input.json"))
21
22
  refuse("coordinator_packet_link_changed");
22
23
  if (session.harness !== runtimeHarness(route.profile)
23
- || !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id]))
24
+ || !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id])
25
+ && !hasAssignedCoordinatorLaunch(context, { projectId: scope.projectId, runId: scope.runId, stage: scope.stage, workSessionId: session.id, sessionId: session.session_id }))
24
26
  refuse("coordinator_start_custody_missing");
25
27
  const bindings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='stage.coordinator_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, session.id]);
26
28
  if (!bindings.length || bindings.length > 32 || bindings.some(row => Buffer.byteLength(row.payload_json) > 32768))