@deksden-com/dd-flow-cli 0.9.0-beta.152 → 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 (226) 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 +114 -16
  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 +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 +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-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,289 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { appendAudit } from "./audit.js";
5
+ import { readInteractiveStageScope, startInteractiveStage } from "./interactive-stage-entry.js";
6
+ import { interactiveStages } from "../domain/interactive-flow.js";
7
+ import { PreparedInteractiveStageObservation } from "./interactive-stage-observation.js";
8
+ import { PreparedInteractiveChecks } from "./interactive-checks.js";
9
+ import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
10
+ import { entityReferenceVariants } from "../shared/entity-references.js";
11
+ import { stageLifecycleMatchKey } from "./hooks.js";
12
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
13
+ import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
14
+ import { PreparedRetainedCheckEvidence } from "./code-checks.js";
15
+ import { prepareInteractiveStageResult, assertInteractiveStageContinuation } from "./interactive-stage-results.js";
16
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
17
+ import { readInteractiveWorkHistory } from "./interactive-work-history.js";
18
+ import { registerInteractiveStageGraph, readRegisteredInteractiveWork } from "./interactive-work-registration.js";
19
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
20
+ import { PreparedInteractiveWorkCollection } from "./interactive-work-collection.js";
21
+ import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
22
+ import { prepareInteractiveFinishIntegration, prepareInteractiveIntegrationCustody } from "./interactive-git-dispatch.js";
23
+ const event = "interactive.stage_acceptance";
24
+ export const interactiveStageHandlers = Object.freeze(interactiveStages.map(stage => Object.freeze({ flowId: "interactive", stage,
25
+ entryId: "interactive-stage-entry", finishId: "interactive-stage-finish", entry: startInteractiveStage, finish: finishInteractiveStage })));
26
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
27
+ function refuse(reason) {
28
+ throw new AppError("interactive_stage_invalid", "Interactive completion requires actual reviewed work and owned checks", 2, { reason, effect: "no_effect" });
29
+ }
30
+ function receipt(context, scope) {
31
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage]);
32
+ if (rows.length > 1 || rows.some(row => Buffer.byteLength(row.payload_json) > 2 * 1024 * 1024))
33
+ refuse("ambiguous_stage_acceptance");
34
+ return rows[0] ? JSON.parse(rows[0].payload_json) : null;
35
+ }
36
+ function assertRetainedPlan(context, scope, proof) {
37
+ if (!proof.check_plan_sha256)
38
+ return;
39
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='interactive.check_plan' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.sha256')=? LIMIT 2", [scope.projectId, scope.runId, proof.check_plan_sha256]);
40
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
41
+ refuse("accepted_check_plan_required");
42
+ const value = JSON.parse(rows[0].payload_json), plan = value?.plan, binding = proof.binding;
43
+ if (value.sha256 !== proof.check_plan_sha256 || hybridDigest(plan) !== value.sha256 || plan.run_id !== scope.runId || plan.stage !== scope.stage
44
+ || plan.bundle_sha256 !== proof.bundle_sha256 || plan.input_sha256 !== binding.inputSha256
45
+ || plan.user_inputs_sha256 !== binding.userInputsSha256 || plan.scope_revision !== binding.scopeRevision
46
+ || plan.file_changes_sha256 !== binding.fileChangesSha256 || plan.no_checks_reason !== proof.no_checks_reason
47
+ || !Array.isArray(plan.checks) || Boolean(plan.checks.length) !== Boolean(proof.checks.length))
48
+ refuse("accepted_check_plan_changed");
49
+ }
50
+ /** Historical replay uses accepted SQL/result/check custody, never today's
51
+ * workspace or today's scope to manufacture a replacement successful result. */
52
+ function acceptedStage(context, input, selected) {
53
+ const { scope, bundle } = selected, accepted = readAcceptedStageData(context, scope), proof = receipt(context, scope);
54
+ const value = JSON.parse(accepted.bytes.toString("utf8"));
55
+ if (!proof || proof.schema_id !== "dd-flow/interactive-stage-acceptance@1" || proof.run_id !== input.runId || proof.stage !== input.stage
56
+ || proof.bundle_sha256 !== bundle.sha256 || proof.result_sha256 !== accepted.result.sha256 || accepted.data_schema_id !== "dd-flow/interactive-stage-result@1"
57
+ || proof.binding.runId !== input.runId || proof.binding.stage !== input.stage || proof.binding.inputSha256 !== accepted.input_sha256
58
+ || value.details.review?.verdict === "revise" || !Array.isArray(proof.checks)
59
+ || (["hardening", "review"].includes(input.stage)
60
+ ? !/^[a-f0-9]{64}$/.test(proof.check_plan_sha256 ?? "") || !proof.checks.length && !proof.no_checks_reason?.trim()
61
+ : proof.check_plan_sha256 !== null || proof.checks.length !== 0 || proof.no_checks_reason !== null))
62
+ refuse("accepted_stage_proof_required");
63
+ const prepared = prepareInteractiveStageResult(accepted.bytes, bundle.descriptor.stageBlob(input.stage, "result_schema"), proof.binding);
64
+ if (!prepared.bytes.equals(accepted.bytes))
65
+ refuse("accepted_stage_result_changed");
66
+ const checks = proof.checks.length ? PreparedRetainedCheckEvidence.prepare(context, { projectId: scope.projectId, runId: input.runId, references: proof.checks }) : null;
67
+ const integration = input.stage === "integration" ? prepareInteractiveIntegrationCustody(context, input) : null;
68
+ if (integration ? !proof.integration?.binding || hybridDigest(proof.integration.binding) !== hybridDigest(integration.binding)
69
+ || proof.integration.observation?.schema_id !== "dd-flow/interactive-finish-integration@1"
70
+ || proof.integration.observation.operation_sha256 !== integration.binding.operation_sha256
71
+ || proof.integration.observation.no_op !== integration.reviewed.empty
72
+ || Boolean(proof.integration.observation.git) === integration.reviewed.empty
73
+ : proof.integration !== undefined)
74
+ refuse("accepted_integration_proof_required");
75
+ const assertProof = (writer) => {
76
+ if (hybridDigest(receipt(writer, scope)) !== hybridDigest(proof)
77
+ || hybridDigest(readAcceptedStageData(writer, scope)) !== hybridDigest(accepted))
78
+ refuse("accepted_stage_proof_changed");
79
+ assertRetainedPlan(writer, scope, proof);
80
+ checks?.assertCustody(writer);
81
+ integration?.assertCustody(writer);
82
+ const collection = proof.collection;
83
+ if (collection) {
84
+ if (hybridDigest(collection) !== proof.binding.workCollectionSha256 || collection.run_id !== input.runId || collection.stage !== input.stage
85
+ || collection.bundle_sha256 !== bundle.sha256 || collection.input_sha256 !== accepted.input_sha256)
86
+ refuse("accepted_work_collection_changed");
87
+ const work = readCompletedInteractiveWork(writer, { ...scope, workId: collection.work_id }, bundle);
88
+ if (hybridDigest(work.receipt) !== collection.settlement_sha256 || work.receipt.result_sha256 !== collection.result_sha256
89
+ || hybridDigest(work.start.assignment.receipt) !== collection.registration_sha256
90
+ || hybridDigest(work.start.receipt.actor) !== hybridDigest(collection.actor))
91
+ refuse("accepted_work_collection_changed");
92
+ }
93
+ else if (proof.binding.workCollectionSha256 !== null)
94
+ refuse("accepted_work_collection_changed");
95
+ };
96
+ context.db.readSnapshot(() => assertProof(context));
97
+ return { accepted, proof, result: prepared.result, assertCustody: assertProof };
98
+ }
99
+ /** Historical semantic and physical Stage custody for terminal integration.
100
+ * Reading an accepted proposal alone is not proof of coordinator settlement.
101
+ * Lifecycle keys are prepared outside the SQL writer; no current source capture
102
+ * or new Stage/Work allocation occurs here. */
103
+ export function readAcceptedInteractiveStage(context, input) {
104
+ if (context.db.isOwnedWriteTransaction?.())
105
+ refuse("physical_preparation_outside_writer_required");
106
+ const selected = readInteractiveStageScope(context, input, true);
107
+ if (!selected.replay)
108
+ refuse("accepted_stage_required");
109
+ const retained = acceptedStage(context, input, selected);
110
+ const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
111
+ const settlement = readAcceptedDescriptorStageSettlement(context, selected.scope, selected.bundle, matchKeys);
112
+ const assertCustody = (writer) => {
113
+ retained.assertCustody(writer);
114
+ if (hybridDigest(readAcceptedDescriptorStageSettlement(writer, selected.scope, selected.bundle, matchKeys)) !== hybridDigest(settlement))
115
+ refuse("accepted_stage_settlement_changed");
116
+ };
117
+ context.db.readSnapshot(() => assertCustody(context));
118
+ return { result: structuredClone(retained.result), proof: structuredClone(retained.proof),
119
+ bytes: Buffer.from(retained.accepted.bytes), reference: structuredClone(retained.accepted.result), assertCustody };
120
+ }
121
+ function prepareReplay(context, input, selected) {
122
+ const retained = acceptedStage(context, input, selected);
123
+ const prepared = prepareInteractiveStageResult(input.bytes, selected.bundle.descriptor.stageBlob(input.stage, "result_schema"), retained.proof.binding);
124
+ if (!prepared.bytes.equals(retained.accepted.bytes))
125
+ refuse("accepted_stage_result_changed");
126
+ return { settlement: PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes }),
127
+ resultSha256: retained.proof.result_sha256, assertCustody: retained.assertCustody };
128
+ }
129
+ function retainAcceptance(context, scope, proof) {
130
+ if (Buffer.byteLength(JSON.stringify(proof)) > 2 * 1024 * 1024)
131
+ refuse("unbounded_stage_acceptance");
132
+ const prior = receipt(context, scope);
133
+ if (prior) {
134
+ if (hybridDigest(prior) !== hybridDigest(proof))
135
+ refuse("accepted_stage_proof_changed");
136
+ }
137
+ else
138
+ appendAudit(context, { projectId: scope.projectId, eventType: event, payload: proof });
139
+ }
140
+ function pendingAcceptedIntegration(context, input, resultSha256, cause) {
141
+ let committed = false;
142
+ try {
143
+ const selected = readInteractiveStageScope(context, input, true);
144
+ committed = selected.replay && receipt(context, selected.scope)?.result_sha256 === resultSha256
145
+ && readAcceptedStageData(context, selected.scope).result.sha256 === resultSha256;
146
+ }
147
+ catch { /* No independently retained terminal acceptance. */ }
148
+ if (!committed)
149
+ throw cause;
150
+ throw new AppError("interactive_finish_pending", "Accepted integration requires projection or RUN settlement reconciliation", 1, { effect: "committed", recoverable: true, run_id: input.runId, next_action: "reconcile_accepted_integration",
151
+ cause: cause instanceof AppError ? cause.code : "finish_settlement_failed" });
152
+ }
153
+ /** Git effects are issued separately by the scoped operation owner. This
154
+ * handler consumes observed integration, never an agent's commit claim. A
155
+ * committed Stage can be followed by an interrupted RUN closure; replay seals
156
+ * that original result without revisiting today's mutable workspace. */
157
+ function finishIntegration(context, input) {
158
+ const selected = readInteractiveStageScope(context, input, true), { scope, bundle } = selected;
159
+ if (selected.replay) {
160
+ const replay = prepareReplay(context, input, selected);
161
+ try {
162
+ const stage = replay.settlement.apply(context, replay.assertCustody), accepted = readAcceptedInteractiveStage(context, input);
163
+ return { ...stage, ...PreparedDescriptorRunSettlement.prepare(context, input).apply(context, accepted.assertCustody) };
164
+ }
165
+ catch (error) {
166
+ return pendingAcceptedIntegration(context, input, replay.resultSha256, error);
167
+ }
168
+ }
169
+ if (receipt(context, scope))
170
+ refuse("orphan_stage_acceptance");
171
+ const review = readAcceptedInteractiveStage(context, { ...input, stage: "review" });
172
+ const git = prepareInteractiveFinishIntegration(context, input);
173
+ const packet = readDescriptorStageInput(context, scope, bundle);
174
+ if (packet.input.predecessor?.stage !== "review" || hybridDigest(packet.input.predecessor.result) !== hybridDigest(review.reference))
175
+ refuse("accepted_review_predecessor_required");
176
+ const original = git.reviewed;
177
+ const binding = { runId: input.runId, stage: "integration", inputSha256: packet.inputSha256,
178
+ baselineSha256: original.baseline_sha256, userInputsSha256: original.user_inputs_sha256, scopeRevision: original.scope_revision,
179
+ fileChangesSha256: original.file_changes_sha256, finishRequested: true, workCollectionSha256: null, review: review.result.details.review,
180
+ retainedLimitations: review.result.details.limitations,
181
+ predecessors: [{ stage: "review", ...review.reference }] };
182
+ const prepared = prepareInteractiveStageResult(input.bytes, bundle.descriptor.stageBlob("integration", "result_schema"), binding);
183
+ assertInteractiveStageContinuation(prepared.result, review.result);
184
+ const proof = { schema_id: "dd-flow/interactive-stage-acceptance@1", run_id: input.runId, stage: "integration", bundle_sha256: bundle.sha256,
185
+ result_sha256: hash(prepared.bytes), binding, collection: null, check_plan_sha256: null, checks: [], no_checks_reason: null,
186
+ integration: { binding: git.binding, observation: git.value() } };
187
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes });
188
+ review.assertCustody(context);
189
+ git.revalidate();
190
+ try {
191
+ const stage = settlement.apply(context, writer => {
192
+ review.assertCustody(writer);
193
+ git.assertCustody(writer);
194
+ retainAcceptance(writer, scope, proof);
195
+ });
196
+ const accepted = readAcceptedInteractiveStage(context, input);
197
+ return { ...stage, ...PreparedDescriptorRunSettlement.prepare(context, input).apply(context, accepted.assertCustody) };
198
+ }
199
+ catch (error) {
200
+ return pendingAcceptedIntegration(context, input, proof.result_sha256, error);
201
+ }
202
+ }
203
+ /** Internal handlers. Public admission still requires registered descriptors,
204
+ * controller/command wiring and installed family qualification. */
205
+ export function finishInteractiveStage(context, input) {
206
+ if (input.stage === "integration")
207
+ return finishIntegration(context, input);
208
+ const selected = readInteractiveStageScope(context, input, true), { scope, bundle } = selected;
209
+ if (selected.replay) {
210
+ const replay = prepareReplay(context, input, selected);
211
+ return replay.settlement.apply(context, replay.assertCustody);
212
+ }
213
+ if (receipt(context, scope))
214
+ refuse("orphan_stage_acceptance");
215
+ const observation = PreparedInteractiveStageObservation.prepare(context, input), proposal = observation.prepareResult(input.bytes);
216
+ if (proposal.result.details.review?.verdict === "revise")
217
+ throw new AppError("interactive_review_corrections_required", "Independent review has unresolved work; the Stage remains open", 2, { phase: "prepare", effect: "no_effect", recoverable: true, projection: { outcome: "needs_repair", review: proposal.result.details.review,
218
+ review_work_id: observation.collectionProof()?.work_id, work_collection_sha256: proposal.result.work_collection_sha256 }, continuation: { kind: "correction_required" } });
219
+ const checked = ["hardening", "review"].includes(input.stage) ? observation.prepareCheckedResult(context, input.bytes, PreparedInteractiveChecks.restore(context, { projectRoot: input.projectRoot, runId: input.runId, stage: input.stage })) : null;
220
+ const prepared = checked ?? proposal;
221
+ const proof = { schema_id: "dd-flow/interactive-stage-acceptance@1", run_id: input.runId, stage: input.stage,
222
+ bundle_sha256: bundle.sha256, result_sha256: hash(prepared.bytes), binding: observation.binding(), collection: observation.collectionProof(),
223
+ check_plan_sha256: checked?.check_plan_sha256 ?? null, checks: checked?.checks ?? [], no_checks_reason: checked?.no_checks_reason ?? null };
224
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes });
225
+ observation.revalidate(context);
226
+ checked?.revalidate(context);
227
+ return settlement.apply(context, writer => {
228
+ observation.assertCustody(writer);
229
+ checked?.assertCustody(writer);
230
+ retainAcceptance(writer, scope, proof);
231
+ });
232
+ }
233
+ /** A negative review permits a separate scoped author, not reviewer writes or
234
+ * Stage rewind. Replaying allocation returns its original Work even if active. */
235
+ export function registerInteractiveReviewCorrection(context, input) {
236
+ if (typeof input.reviewWorkId !== "string" || !input.reviewWorkId || input.reviewWorkId.length > 256 || /[\0\r\n]/.test(input.reviewWorkId))
237
+ refuse("review_work_id_required");
238
+ const stage = "review", selected = readInteractiveStageScope(context, { ...input, stage }), { scope, bundle } = selected;
239
+ const packet = readDescriptorStageInput(context, scope, bundle), review = readCompletedInteractiveWork(context, { ...scope, workId: input.reviewWorkId }, bundle);
240
+ if (review.start.assignment.packet.stage !== "review" || review.start.assignment.packet.owner_stage !== "review"
241
+ || review.start.assignment.receipt.attempt !== packet.attempt || review.result.verdict === "ready")
242
+ refuse("negative_review_required_for_correction");
243
+ const requestId = `correction-${hybridDigest({ work_id: input.reviewWorkId, result_sha256: review.receipt.result_sha256, input_sha256: packet.inputSha256 }).slice(0, 40)}`;
244
+ const args = { projectRoot: input.projectRoot, runId: input.runId, stage, role: "hardening",
245
+ requestId, gitExecutable: readInteractiveWorkGit(bundle).git_executable };
246
+ const prior = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='interactive.work_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='review' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 1", [scope.projectId, input.runId, packet.attempt, requestId]);
247
+ if (prior)
248
+ return registerInteractiveStageGraph(context, args);
249
+ const history = readInteractiveWorkHistory(context, scope, bundle);
250
+ if (history.works.at(-1)?.work_id !== input.reviewWorkId)
251
+ refuse("latest_negative_review_required");
252
+ return registerInteractiveStageGraph(context, args, writer => {
253
+ if (hybridDigest(readInteractiveWorkHistory(writer, scope, bundle)) !== hybridDigest(history))
254
+ refuse("correction_history_changed");
255
+ });
256
+ }
257
+ /** Hand off a settled correction and its current check evidence to a fresh
258
+ * read-only critic. An author result never becomes accepted review itself. */
259
+ export function registerInteractiveCorrectionReview(context, input) {
260
+ if (typeof input.correctionWorkId !== "string" || !input.correctionWorkId || input.correctionWorkId.length > 256 || /[\0\r\n]/.test(input.correctionWorkId))
261
+ refuse("correction_work_id_required");
262
+ const stage = "review", { scope, bundle } = readInteractiveStageScope(context, { ...input, stage });
263
+ const packet = readDescriptorStageInput(context, scope, bundle), author = readCompletedInteractiveWork(context, { ...scope, workId: input.correctionWorkId }, bundle);
264
+ if (author.start.assignment.packet.owner_stage !== "review" || author.start.assignment.packet.stage !== "hardening"
265
+ || author.start.assignment.receipt.attempt !== packet.attempt || author.result.verdict !== "ready")
266
+ refuse("ready_correction_author_required");
267
+ const requestId = `recheck-${hybridDigest({ work_id: input.correctionWorkId, result_sha256: author.receipt.result_sha256, input_sha256: packet.inputSha256 }).slice(0, 40)}`;
268
+ const args = { projectRoot: input.projectRoot, runId: input.runId, stage, requestId, gitExecutable: readInteractiveWorkGit(bundle).git_executable };
269
+ const prior = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='interactive.work_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='review' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 1", [scope.projectId, input.runId, packet.attempt, requestId]);
270
+ if (prior) {
271
+ const registered = registerInteractiveStageGraph(context, args);
272
+ const retained = readRegisteredInteractiveWork(context, scope, bundle, registered.work_ids.review).retained.body;
273
+ const originalAuthor = retained.work_history.works.at(-1);
274
+ if (!retained.stage_checks || originalAuthor?.work_id !== input.correctionWorkId
275
+ || originalAuthor.result_sha256 !== author.receipt.result_sha256)
276
+ refuse("recheck_registration_changed");
277
+ return registered;
278
+ }
279
+ const collection = PreparedInteractiveWorkCollection.prepareCorrectionAuthor(context, input);
280
+ if (collection.snapshot().work_id !== input.correctionWorkId)
281
+ refuse("latest_correction_author_required");
282
+ const checks = PreparedInteractiveChecks.restore(context, { ...input, stage }), evidence = checks.evidence(context);
283
+ collection.revalidate(context);
284
+ return registerInteractiveStageGraph(context, { ...args, checks }, writer => {
285
+ collection.assertCustody(writer);
286
+ checks.assertCustody(writer);
287
+ evidence?.assertCustody(writer);
288
+ });
289
+ }
@@ -0,0 +1,136 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { interactiveStages } from "../domain/interactive-flow.js";
3
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
6
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
7
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
8
+ import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
9
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
10
+ import { PreparedInteractiveWorkCollection } from "./interactive-work-collection.js";
11
+ import { readInteractiveWorkHistory } from "./interactive-work-history.js";
12
+ import { pendingInteractiveIssues } from "./interactive-work-results.js";
13
+ import { prepareInteractiveStageResult, assertInteractiveStageContinuation } from "./interactive-stage-results.js";
14
+ import { PreparedInteractiveChecks } from "./interactive-checks.js";
15
+ function refuse(reason) {
16
+ throw new AppError("interactive_stage_observation_invalid", "Interactive result requires the current task, actual patch and independently settled Work", 2, { reason, effect: "no_effect" });
17
+ }
18
+ /** Actual task/patch/Work observation for Stage handlers. This grants neither
19
+ * successful command checks nor Git integration or Stage/RUN acceptance. */
20
+ export class PreparedInteractiveStageObservation {
21
+ #selected;
22
+ #packet;
23
+ #previous;
24
+ #prior;
25
+ #changes;
26
+ #history;
27
+ #collection;
28
+ #binding;
29
+ constructor(selected, packet, previous, changes, history, collection, binding) {
30
+ this.#selected = selected;
31
+ this.#packet = packet;
32
+ this.#previous = previous;
33
+ this.#prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
34
+ this.#changes = changes;
35
+ this.#history = history;
36
+ this.#collection = collection;
37
+ this.#binding = binding;
38
+ Object.freeze(this);
39
+ }
40
+ static prepare(context, input) {
41
+ if (context.db.isOwnedWriteTransaction?.())
42
+ refuse("preparation_outside_writer_required");
43
+ // Integration must observe accepted reviewed bytes through the Git owner,
44
+ // not reuse a pre-commit workspace observation as integration evidence.
45
+ if (input.stage === "integration")
46
+ refuse("integration_observation_required");
47
+ const selected = readInteractiveStageScope(context, input), { scope, bundle, run } = selected, stage = input.stage;
48
+ const packet = readDescriptorStageInput(context, scope, bundle), index = JSON.parse(run.index_json);
49
+ if (run.status !== "running" || index.current_stage !== stage
50
+ || !index.stage_runs?.some((item) => item.stage === stage && item.attempt === packet.attempt && item.status === "running")
51
+ || selected.selected.needs_scope_refresh)
52
+ refuse("active_current_scope_required");
53
+ const previousStage = interactiveStages[interactiveStages.indexOf(stage) - 1];
54
+ const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
55
+ if (previous && previous.data_schema_id !== "dd-flow/interactive-stage-result@1")
56
+ refuse("accepted_predecessor_required");
57
+ if (previous ? packet.input.predecessor?.stage !== previousStage || hybridDigest(packet.input.predecessor.result) !== hybridDigest(previous.result)
58
+ : packet.input.predecessor != null)
59
+ refuse("accepted_predecessor_required");
60
+ const frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
61
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot,
62
+ workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
63
+ const patch = changes.snapshot(), workStage = stage === "hardening" || stage === "review";
64
+ const history = workStage ? readInteractiveWorkHistory(context, scope, bundle) : null;
65
+ const registered = history?.works.some(work => work.owner_stage === stage && work.attempt === packet.attempt);
66
+ const collection = workStage && (stage === "review" || patch.delta.changes.length || registered)
67
+ ? PreparedInteractiveWorkCollection.prepare(context, { ...input, stage }) : null;
68
+ const work = collection?.results()[0]?.result;
69
+ const pending = history ? pendingInteractiveIssues(history, { file_changes_sha256: hybridDigest(patch.delta.changes),
70
+ user_inputs_sha256: selected.selected.inputs.sha256, scope_revision: selected.selected.revision }) : [];
71
+ const review = stage === "review" && work ? { verdict: work.verdict === "ready" && !pending.some(issue => issue.severity !== "minor")
72
+ ? "accept" : "revise", summary: work.summary } : null;
73
+ const binding = { runId: input.runId, stage, inputSha256: packet.inputSha256,
74
+ baselineSha256: selected.selected.baseline.sha256, userInputsSha256: selected.selected.inputs.sha256,
75
+ scopeRevision: selected.selected.revision, fileChangesSha256: hybridDigest(patch.delta.changes),
76
+ finishRequested: selected.selected.inputs.finish_requested, workCollectionSha256: collection?.sha256 ?? null, review,
77
+ retainedLimitations: work?.limitations ?? [],
78
+ predecessors: previous && previousStage ? [{ stage: previousStage, ...previous.result }] : [] };
79
+ const prepared = new PreparedInteractiveStageObservation(selected, packet, previous, changes, history, collection, binding);
80
+ prepared.revalidate(context);
81
+ return prepared;
82
+ }
83
+ binding() { return structuredClone(this.#binding); }
84
+ collectionProof() { return this.#collection?.snapshot() ?? null; }
85
+ prepareResult(bytes) {
86
+ const verdict = this.#collection?.results()[0]?.result.verdict;
87
+ if (this.#binding.stage === "hardening" && verdict !== undefined && verdict !== "ready")
88
+ refuse("hardening_work_not_ready");
89
+ const prepared = prepareInteractiveStageResult(bytes, this.#selected.bundle.descriptor.stageBlob(this.#binding.stage, "result_schema"), this.#binding);
90
+ // Projection retains the latest Work's limitations; original history stays
91
+ // separate so a later independent Work can reassess earlier limitations.
92
+ if (this.#prior)
93
+ assertInteractiveStageContinuation(prepared.result, this.#prior);
94
+ return prepared;
95
+ }
96
+ /** A handler must consume this joint fence with ordinary Stage settlement.
97
+ * No-check decisions remain explicit, never a fabricated passed receipt. */
98
+ prepareCheckedResult(context, bytes, checks) {
99
+ if (!(checks instanceof PreparedInteractiveChecks) || !["hardening", "review"].includes(this.#binding.stage))
100
+ refuse("owned_stage_checks_required");
101
+ const plan = checks.value(), binding = this.#binding;
102
+ if (plan.run_id !== binding.runId || plan.stage !== binding.stage || plan.bundle_sha256 !== this.#selected.bundle.sha256
103
+ || plan.input_sha256 !== binding.inputSha256 || plan.user_inputs_sha256 !== binding.userInputsSha256
104
+ || plan.scope_revision !== binding.scopeRevision || plan.file_changes_sha256 !== binding.fileChangesSha256)
105
+ refuse("foreign_stage_checks");
106
+ this.revalidate(context);
107
+ const evidence = checks.evidence(context), prepared = this.prepareResult(bytes);
108
+ return { ...prepared, check_plan_sha256: checks.sha256, checks: evidence?.retainedReferences(context) ?? [], no_checks_reason: plan.no_checks_reason,
109
+ assertCustody: (writer) => { this.assertCustody(writer); checks.assertCustody(writer); evidence?.assertCustody(writer); },
110
+ revalidate: (reader) => { this.revalidate(reader); checks.revalidate(reader); evidence?.revalidate(reader); } };
111
+ }
112
+ revalidate(context) {
113
+ readInteractiveWorkGit(this.#selected.bundle);
114
+ this.#changes.revalidate();
115
+ this.#collection?.revalidate(context);
116
+ context.db.readSnapshot(() => this.assertSelection(context));
117
+ }
118
+ assertCustody(context) {
119
+ this.#changes.assertCustody(context);
120
+ this.assertSelection(context);
121
+ }
122
+ assertSelection(context) {
123
+ this.#selected.assertCustody(context);
124
+ this.#collection?.assertCustody(context);
125
+ const { scope, bundle } = this.#selected, packet = readDescriptorStageInput(context, scope, bundle);
126
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
127
+ if (run.status !== "running" || index.current_stage !== scope.stage || !index.stage_runs?.some((item) => item.stage === scope.stage && item.attempt === packet.attempt && item.status === "running"))
128
+ refuse("active_current_scope_required");
129
+ if (hybridDigest(packet) !== hybridDigest(this.#packet))
130
+ refuse("stage_input_changed");
131
+ if (this.#previous && hybridDigest(readAcceptedStageData(context, { ...scope, stage: this.#binding.predecessors[0].stage })) !== hybridDigest(this.#previous))
132
+ refuse("predecessor_changed");
133
+ if (this.#history && hybridDigest(readInteractiveWorkHistory(context, scope, bundle)) !== hybridDigest(this.#history))
134
+ refuse("work_history_changed");
135
+ }
136
+ }
@@ -0,0 +1,111 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { interactiveStages } from "../domain/interactive-flow.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ function refuse(reason) {
6
+ throw new AppError("interactive_stage_result_invalid", "Interactive Stage requires its actual inputs, scoped changes and independent review", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
7
+ }
8
+ /** Pure proposal validation. A valid JSON result does not prove semantic
9
+ * correctness, physical reviewer independence, successful checks or Git effects.
10
+ * Those bindings come from ordinary Stage/Work/operation owners at settlement. */
11
+ export function validateInteractiveStageResult(bytes, schemaBytes, binding) {
12
+ if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
13
+ refuse("result_exceeds_bound");
14
+ let result;
15
+ try {
16
+ result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
17
+ }
18
+ catch {
19
+ return refuse("invalid_schema");
20
+ }
21
+ const validate = (() => {
22
+ try {
23
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
24
+ if (schema.$id !== "dd-flow/interactive-stage-result@1")
25
+ refuse("frozen_schema_invalid");
26
+ return new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
27
+ }
28
+ catch {
29
+ return refuse("frozen_schema_invalid");
30
+ }
31
+ })();
32
+ if (!validate(result) || result.schema_id !== "dd-flow/interactive-stage-result@1" || result.flow_id !== "interactive"
33
+ || !result.summary.trim() || !result.details.change_summary.trim() || result.details.limitations.some(item => !item.trim())
34
+ || result.details.review && !result.details.review.summary.trim())
35
+ refuse("invalid_schema");
36
+ if (result.run_id !== binding.runId || result.stage !== binding.stage || result.input_sha256 !== binding.inputSha256
37
+ || result.baseline_sha256 !== binding.baselineSha256 || result.user_inputs_sha256 !== binding.userInputsSha256
38
+ || result.scope_revision !== binding.scopeRevision || result.file_changes_sha256 !== binding.fileChangesSha256
39
+ || hybridDigest(result.predecessors) !== hybridDigest(binding.predecessors))
40
+ refuse("foreign_stage_binding");
41
+ if (result.work_collection_sha256 !== binding.workCollectionSha256 || hybridDigest(result.details.review) !== hybridDigest(binding.review))
42
+ refuse("unowned_work_or_review");
43
+ const preflight = binding.stage === "preflight", empty = binding.fileChangesSha256 === hybridDigest([]);
44
+ if (preflight ? result.predecessors.length !== 0 : result.predecessors.length !== 1
45
+ || result.predecessors[0]?.stage !== interactiveStages[interactiveStages.indexOf(binding.stage) - 1])
46
+ refuse("foreign_stage_predecessor");
47
+ if (preflight && (!empty || binding.finishRequested))
48
+ refuse("preflight_changed_or_already_finished");
49
+ if (!preflight && !binding.finishRequested)
50
+ refuse("explicit_finish_required");
51
+ if (binding.stage === "review" || binding.stage === "hardening" && !empty) {
52
+ if (!binding.workCollectionSha256)
53
+ refuse("completed_work_collection_required");
54
+ }
55
+ if (["review", "integration"].includes(binding.stage) ? binding.review === null : binding.review !== null)
56
+ refuse("review_stage_required");
57
+ if (binding.stage === "integration" && binding.review?.verdict !== "accept")
58
+ refuse("accepted_review_required");
59
+ return result;
60
+ }
61
+ /** Agents supply semantic summary/details, not manually calculated custody
62
+ * hashes. Binding must come from CLI-issued context/Work selection and be
63
+ * fenced against current SQL at settlement, never inferred from this proposal
64
+ * or silently refreshed to newer ambient inputs. Conflicting supplied claims
65
+ * refuse rather than being overwritten with valid-looking current metadata. */
66
+ export function prepareInteractiveStageResult(bytes, schemaBytes, binding) {
67
+ if (!bytes.length || bytes.length > 1024 * 1024)
68
+ refuse("result_exceeds_bound");
69
+ let proposal;
70
+ try {
71
+ proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
72
+ }
73
+ catch {
74
+ return refuse("invalid_schema");
75
+ }
76
+ if (!proposal || typeof proposal !== "object" || Array.isArray(proposal)
77
+ || !proposal.details || typeof proposal.details !== "object" || Array.isArray(proposal.details))
78
+ refuse("invalid_schema");
79
+ const owned = { schema_id: "dd-flow/interactive-stage-result@1", flow_id: "interactive", run_id: binding.runId, stage: binding.stage,
80
+ input_sha256: binding.inputSha256, baseline_sha256: binding.baselineSha256, user_inputs_sha256: binding.userInputsSha256,
81
+ scope_revision: binding.scopeRevision, file_changes_sha256: binding.fileChangesSha256, work_collection_sha256: binding.workCollectionSha256,
82
+ predecessors: binding.predecessors };
83
+ for (const [key, value] of Object.entries(owned))
84
+ if (Object.hasOwn(proposal, key) && hybridDigest(proposal[key]) !== hybridDigest(value))
85
+ refuse("foreign_stage_binding");
86
+ const details = proposal.details;
87
+ if (Object.hasOwn(details, "review") && hybridDigest(details.review) !== hybridDigest(binding.review))
88
+ refuse("unowned_work_or_review");
89
+ const supplied = details.limitations;
90
+ const limitations = Array.isArray(supplied)
91
+ ? [...supplied, ...(binding.retainedLimitations ?? []).filter(item => !supplied.includes(item))] : supplied;
92
+ const encoded = Buffer.from(JSON.stringify({ ...proposal, ...owned, details: { ...details, review: binding.review, limitations } }));
93
+ return { result: validateInteractiveStageResult(encoded, schemaBytes, binding), bytes: encoded };
94
+ }
95
+ /** Later HITL answers/hardening may legitimately revise an earlier scope. Once
96
+ * review is accepted, integration cannot change that reviewed task or patch. */
97
+ export function assertInteractiveStageContinuation(result, prior) {
98
+ if (result.run_id !== prior.run_id || result.baseline_sha256 !== prior.baseline_sha256
99
+ || interactiveStages.indexOf(result.stage) !== interactiveStages.indexOf(prior.stage) + 1)
100
+ refuse("foreign_stage_predecessor");
101
+ if (result.stage === "integration" && (prior.details.review?.verdict !== "accept"
102
+ || result.user_inputs_sha256 !== prior.user_inputs_sha256 || result.scope_revision !== prior.scope_revision
103
+ || result.file_changes_sha256 !== prior.file_changes_sha256
104
+ || result.details.change_summary !== prior.details.change_summary || hybridDigest(result.details.review) !== hybridDigest(prior.details.review)
105
+ || prior.details.limitations.some(item => !result.details.limitations.includes(item))))
106
+ refuse("accepted_review_changed");
107
+ }
108
+ /** A real revise verdict must never be projected as successful completion. */
109
+ export function interactiveStageDisposition(result) {
110
+ return result.details.review?.verdict === "revise" ? "needs_work" : "ready";
111
+ }