@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,142 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { appendAudit } from "./audit.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
6
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
8
+ const eventType = "interactive.user_input";
9
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
10
+ function refuse(reason) {
11
+ throw new AppError("interactive_user_input_invalid", "Interactive input requires its exact RUN and ordered delivered user bytes", 1, { reason, effect: "no_effect" });
12
+ }
13
+ function validDelivery(value) {
14
+ if (value === null)
15
+ return true;
16
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join() !== "controller_id,pause_id,request_id")
17
+ return false;
18
+ return Object.values(value).every(item => typeof item === "string" && item.length > 0 && item.length <= 256 && !/[\0\r\n]/.test(item));
19
+ }
20
+ function state(runId, bundle, initial, turns) {
21
+ const value = { schema_id: "dd-flow/interactive-user-turns@1", run_id: runId, bundle_sha256: bundle.sha256,
22
+ initial_request: initial, turns, finish_requested: turns.at(-1)?.kind === "finish" };
23
+ return { ...value, sha256: hybridDigest(value) };
24
+ }
25
+ /** Custody only: a delivered turn does not certify applicability, approve an
26
+ * agent's scope expansion or authorize Git/delivery. No files or RUNs allocated.
27
+ * Stage owners must fence this full revision before accepting their results. */
28
+ function readInteractiveUserTurns(context, scope, bundle) {
29
+ return context.db.readSnapshot(() => {
30
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
31
+ if (bundle.manifest.flow_id !== "interactive")
32
+ refuse("foreign_flow");
33
+ const bytes = bundle.blob("runtime/source-request.md"), reference = bundle.manifest.inventory.find(item => item.path === "runtime/source-request.md");
34
+ if (!bytes.length || bytes.length > 64 * 1024 || !reference || hash(bytes) !== reference.sha256)
35
+ refuse("original_request_missing");
36
+ const initial = { sha256: reference.sha256, base64: bytes.toString("base64") }, turns = [];
37
+ const rows = context.db.all("SELECT id,payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id LIMIT 129", [scope.projectId, eventType, scope.runId]);
38
+ if (rows.length > 128)
39
+ refuse("too_many_turns");
40
+ let total = bytes.length;
41
+ const requests = new Set();
42
+ for (const row of rows) {
43
+ if (Buffer.byteLength(row.payload_json) > 2 * 1024 * 1024)
44
+ refuse("unbounded_turn");
45
+ let payload;
46
+ try {
47
+ payload = JSON.parse(row.payload_json);
48
+ }
49
+ catch {
50
+ return refuse("invalid_turn_custody");
51
+ }
52
+ const turn = payload?.turn;
53
+ if (!payload || !["bundle_sha256,previous_sha256,run_id,turn", "bundle_sha256,delivery,previous_sha256,run_id,turn"].includes(Object.keys(payload).sort().join())
54
+ || Object.hasOwn(payload, "delivery") && !validDelivery(payload.delivery)
55
+ || payload.run_id !== scope.runId || payload.bundle_sha256 !== bundle.sha256
56
+ || payload.previous_sha256 !== state(scope.runId, bundle, initial, turns).sha256
57
+ || !turn || Object.keys(turn).sort().join() !== "base64,inputs_sha256,kind,request_id,sequence,sha256"
58
+ || turn.sequence !== turns.length + 1 || !validRequest(turn.request_id) || requests.has(turn.request_id)
59
+ || typeof turn.inputs_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(turn.inputs_sha256)
60
+ || !["turn", "finish"].includes(turn.kind) || typeof turn.base64 !== "string" || turns.at(-1)?.kind === "finish")
61
+ refuse("invalid_turn_custody");
62
+ const content = Buffer.from(turn.base64, "base64");
63
+ total += content.length;
64
+ if (!content.length || content.length > 1024 * 1024 || total > 16 * 1024 * 1024
65
+ || content.toString("base64") !== turn.base64 || hash(content) !== turn.sha256)
66
+ refuse("turn_bytes_changed");
67
+ requests.add(turn.request_id);
68
+ turns.push(turn);
69
+ }
70
+ return { ...state(scope.runId, bundle, initial, turns), event_ids: rows.map(row => row.id) };
71
+ });
72
+ }
73
+ /** Free turns and actual HITL answers share one SQL snapshot/revision. Later
74
+ * answers cannot disappear behind a previously frozen free-turn digest. */
75
+ export function readInteractiveUserInputs(context, scope, bundle) {
76
+ return context.db.readSnapshot(() => {
77
+ const turns = readInteractiveUserTurns(context, scope, bundle);
78
+ const answers = readPreparedMemoryFlowUserInputs(context, scope, "interactive", bundle);
79
+ const messages = [...answers.sources.filter(source => source.role !== "request"),
80
+ ...turns.turns.map((turn, index) => ({ role: turn.kind, event_id: turns.event_ids[index],
81
+ request_id: turn.request_id, sequence: turn.sequence, sha256: turn.sha256, base64: turn.base64 }))]
82
+ .sort((a, b) => a.event_id - b.event_id);
83
+ const value = { schema_id: "dd-flow/interactive-user-inputs@1", run_id: scope.runId,
84
+ bundle_sha256: bundle.sha256, initial_request: turns.initial_request,
85
+ initial_request_basis: { role: "request", event_id: 0, sha256: turns.initial_request.sha256 }, turns: turns.turns,
86
+ turns_sha256: turns.sha256, messages, finish_requested: turns.finish_requested };
87
+ return { ...value, sha256: hybridDigest(value) };
88
+ });
89
+ }
90
+ /** Historical routing metadata, not another message or fresh pause selection. */
91
+ export function readInteractiveInputDelivery(context, scope, bundle, requestId) {
92
+ return context.db.readSnapshot(() => {
93
+ const inputs = readInteractiveUserInputs(context, scope, bundle);
94
+ if (!inputs.turns.some(turn => turn.request_id === requestId))
95
+ refuse("original_turn_required");
96
+ const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.turn.request_id')=?", [scope.projectId, eventType, scope.runId, requestId]);
97
+ return structuredClone(JSON.parse(row.payload_json).delivery ?? null);
98
+ });
99
+ }
100
+ function validRequest(value) {
101
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
102
+ }
103
+ /** Internal ingress for a future bound CLI turn/finish command, not an agent
104
+ * result field. Explicit kind=finish closes intake; text is never interpreted
105
+ * as a finish trigger. Exact-request replay remains possible after Stage move. */
106
+ export function retainInteractiveUserInput(context, scope, bundle, input) {
107
+ if (!validRequest(input.requestId) || !["turn", "finish"].includes(input.kind)
108
+ || !/^[a-f0-9]{64}$/.test(input.expectedInputsSha256) || !(input.bytes instanceof Uint8Array)
109
+ || !input.bytes.length || input.bytes.length > 1024 * 1024 || Object.hasOwn(input, "delivery") && !validDelivery(input.delivery))
110
+ refuse("invalid_delivery");
111
+ const bytes = Buffer.from(input.bytes), checksum = hash(bytes);
112
+ return context.db.writeTransaction(() => {
113
+ const current = readInteractiveUserInputs(context, scope, bundle);
114
+ const prior = current.turns.find(turn => turn.request_id === input.requestId);
115
+ if (prior) {
116
+ if (prior.kind !== input.kind || prior.sha256 !== checksum || prior.base64 !== bytes.toString("base64")
117
+ || prior.inputs_sha256 !== input.expectedInputsSha256
118
+ || Object.hasOwn(input, "delivery") && hybridDigest(input.delivery) !== hybridDigest(readInteractiveInputDelivery(context, scope, bundle, input.requestId)))
119
+ refuse("request_conflict");
120
+ return { changed: false, turn: prior, inputs: current };
121
+ }
122
+ assertRunMutationAllowed(context, scope.projectId, scope.runId);
123
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
124
+ if (current.sha256 !== input.expectedInputsSha256)
125
+ refuse("inputs_changed");
126
+ if (current.finish_requested)
127
+ refuse("finish_already_requested");
128
+ const active = index.stage_runs?.findLast((stage) => stage.stage === "interactive");
129
+ const hitlPaused = run.status === "paused" && active?.status === "paused" && active.pause?.reason === "waiting_for_user"
130
+ && typeof active.pause.id === "string" && active.pause.id.length > 0 && typeof active.pause.work_id === "string" && active.pause.work_id.length > 0;
131
+ if (run.status !== "running" && run.status !== "waiting_for_user" && !hitlPaused || index.current_stage !== "interactive"
132
+ || !index.stage_runs?.some((stage) => stage.stage === "interactive" && ["running", "waiting_for_user", "paused"].includes(stage.status)))
133
+ refuse("interactive_stage_required");
134
+ const turn = { sequence: current.turns.length + 1, request_id: input.requestId, kind: input.kind,
135
+ inputs_sha256: current.sha256, sha256: checksum, base64: bytes.toString("base64") };
136
+ appendAudit(context, { projectId: scope.projectId, eventType, payload: { run_id: scope.runId,
137
+ bundle_sha256: bundle.sha256, previous_sha256: current.turns_sha256, turn,
138
+ ...(Object.hasOwn(input, "delivery") ? { delivery: input.delivery } : {}) } });
139
+ // Re-reading within the same writer validates bounds/custody before commit.
140
+ return { changed: true, turn, inputs: readInteractiveUserInputs(context, scope, bundle) };
141
+ }, "interactive.retainUserInput");
142
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { localEnvironmentAllowed } from "./workspace-files.js";
5
+ export const interactiveWorkResultSchema = "dd-flow/interactive-work-result@1";
6
+ function refuse(reason) {
7
+ throw new AppError("interactive_work_batch_invalid", "Interactive Work requires selected task data, scope and worker instruction", 2, { reason, effect: "no_effect" });
8
+ }
9
+ export function interactiveWorkPathAllowed(name) {
10
+ return typeof name === "string" && !!name && name.length <= 4096 && !path.posix.isAbsolute(name) && !path.win32.isAbsolute(name)
11
+ && !name.includes("\\") && !name.includes("\0") && name.split("/").every(part => !!part && ![".", "..", ".git", ".dd-flow", "node_modules"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
12
+ }
13
+ const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
14
+ /** Deterministic batch data only. Registration, actor custody, workspace
15
+ * observation, source delivery and dispatch remain with existing runtime owners.
16
+ * One writer avoids overlapping shared-workspace hardening writes. */
17
+ export function interactiveWorkBatch(input) {
18
+ if (!input || Object.keys(input).filter(key => key !== "owner_stage").sort().join() !== "baseline_sha256,context,file_changes_sha256,input_sha256,instruction,read_inputs,run_id,scope_revision,stage,user_inputs_sha256,write_paths"
19
+ || !/^RUN-[0-9]+-[a-z0-9-]+$/.test(input.run_id) || !["hardening", "review"].includes(input.stage)
20
+ || ![input.input_sha256, input.baseline_sha256, input.user_inputs_sha256, input.scope_revision, input.file_changes_sha256].every(digest)
21
+ || typeof input.instruction !== "string" || !input.instruction.trim() || Buffer.byteLength(input.instruction) > 1024 * 1024
22
+ || !input.context || input.context.root !== "run" || !interactiveWorkPathAllowed(input.context.path) || !digest(input.context.sha256)
23
+ || !Array.isArray(input.read_inputs) || !input.read_inputs.length || input.read_inputs.length > 20_000
24
+ || input.read_inputs.some(ref => !ref || !["run", "workspace"].includes(ref.root) || !interactiveWorkPathAllowed(ref.path) || !digest(ref.sha256))
25
+ || new Set(input.read_inputs.map(ref => `${ref.root}:${ref.path}`)).size !== input.read_inputs.length
26
+ || !input.read_inputs.some(ref => hybridDigest(ref) === hybridDigest(input.context))
27
+ || !Array.isArray(input.write_paths) || input.write_paths.length > 20_000 || input.write_paths.some(name => !interactiveWorkPathAllowed(name))
28
+ || new Set(input.write_paths).size !== input.write_paths.length || hybridDigest(input.write_paths) !== hybridDigest([...input.write_paths].sort()))
29
+ refuse("invalid_selected_input");
30
+ if (input.stage === "review" && input.write_paths.length)
31
+ refuse("review_write_scope_forbidden");
32
+ if (input.owner_stage !== undefined && !["hardening", "review"].includes(input.owner_stage))
33
+ refuse("invalid_work_owner_role");
34
+ const ownerStage = input.owner_stage ?? input.stage;
35
+ if (!["hardening", "review"].includes(ownerStage) || ownerStage === "hardening" && input.stage !== "hardening")
36
+ refuse("invalid_work_owner_role");
37
+ const packet = { schema_id: "dd-flow/interactive-work-input@1", flow_id: "interactive",
38
+ ...structuredClone(input), owner_stage: ownerStage, key: input.stage, result_schema: interactiveWorkResultSchema };
39
+ return { works: [{ key: input.stage, depends_on: [], launch_policy: "fresh_agent_required",
40
+ result_schema: interactiveWorkResultSchema, task: input.instruction,
41
+ payload: { kind: `interactive-${input.stage}`, read_only: input.stage === "review",
42
+ interactive: { ...packet, packet_sha256: hybridDigest(packet) } } }] };
43
+ }
@@ -0,0 +1,119 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
5
+ import { readDescriptorRootWork } from "./descriptor-root-work.js";
6
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
7
+ import { stageGraphScope } from "./stage-work-graph.js";
8
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
9
+ import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
10
+ import { PreparedInteractiveWorkspaceChanges, readInteractiveMutationScope } from "./interactive-mutation-scope.js";
11
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
12
+ import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
13
+ function refuse(reason) {
14
+ throw new AppError("interactive_work_collection_invalid", "Interactive Stage requires its latest settled graph and unchanged actual output", 2, { reason, effect: "no_effect" });
15
+ }
16
+ function collectSql(context, scope, bundle, role) {
17
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
18
+ const graph = stageGraphScope(context, scope.projectId, scope.runId, scope.stage), root = readDescriptorRootWork(context, scope, bundle);
19
+ const input = readDescriptorStageInput(context, scope, bundle), selected = readInteractiveMutationScope(context, scope, bundle);
20
+ if (run.status !== "running" || root.status !== "running" || index.current_stage !== scope.stage || selected.needs_scope_refresh
21
+ || !selected.inputs.finish_requested || input.attempt !== graph.attempt
22
+ || !index.stage_runs?.some((item) => item.stage === scope.stage && item.status === "running" && item.attempt === graph.attempt))
23
+ refuse("active_stage_required");
24
+ if (context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId, root.workId]))
25
+ refuse("active_child_work");
26
+ // A lost newest family receipt must not silently select an older successful
27
+ // round. Generic allocation custody is independently retained by the registry.
28
+ if (context.db.get("SELECT 1 FROM audit_events b WHERE b.project_id=? AND b.event_type='work.batch_registration' AND json_extract(b.payload_json,'$.scope.run_id')=? AND json_extract(b.payload_json,'$.scope.stage')=? AND json_extract(b.payload_json,'$.scope.attempt')=? AND json_extract(b.payload_json,'$.scope.parent_work_id')=? AND json_extract(b.payload_json,'$.scope.request_key') LIKE 'interactive-%' AND NOT EXISTS (SELECT 1 FROM audit_events f WHERE f.project_id=b.project_id AND f.event_type='interactive.work_registration' AND json_extract(f.payload_json,'$.run_id')=? AND json_extract(f.payload_json,'$.stage')=? AND json_extract(f.payload_json,'$.attempt')=? AND json_extract(f.payload_json,'$.request_id')=substr(json_extract(b.payload_json,'$.scope.request_key'),13)) LIMIT 1", [scope.projectId, scope.runId, scope.stage, graph.attempt, root.workId, scope.runId, scope.stage, graph.attempt]))
29
+ refuse("round_registration_lost");
30
+ if (context.db.get("SELECT 1 FROM works w JOIN stage_work_memberships m ON m.project_id=w.project_id AND m.run_id=w.run_id AND m.work_id=w.work_id WHERE w.project_id=? AND w.run_id=? AND w.parent_work_id=? AND m.stage=? AND m.attempt=? AND m.cycle=? AND NOT EXISTS (SELECT 1 FROM audit_events f WHERE f.project_id=w.project_id AND f.event_type='interactive.work_registration' AND json_extract(f.payload_json,'$.run_id')=w.run_id AND json_extract(f.payload_json,'$.stage')=? AND json_extract(f.payload_json,'$.attempt')=? AND json_extract(f.payload_json,'$.work_ids.'||json_extract(w.payload_json,'$._dd_flow_batch_request.item_key'))=w.work_id) LIMIT 1", [scope.projectId, scope.runId, root.workId, scope.stage, graph.attempt, graph.cycle, scope.stage, graph.attempt]))
31
+ refuse("work_registration_lost");
32
+ const latest = context.db.get("SELECT payload_json 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')=? AND json_extract(payload_json,'$.attempt')=? ORDER BY id DESC LIMIT 1", [scope.projectId, scope.runId, scope.stage, graph.attempt]);
33
+ if (!latest || Buffer.byteLength(latest.payload_json) > 16384)
34
+ refuse("registered_graph_required");
35
+ const ids = Object.values(JSON.parse(latest.payload_json)?.work_ids ?? {});
36
+ const workId = ids[0];
37
+ if (ids.length !== 1 || typeof workId !== "string")
38
+ refuse("registered_graph_required");
39
+ const work = readCompletedInteractiveWork(context, { ...scope, workId }, bundle), packet = work.start.assignment.packet;
40
+ if (hybridDigest(JSON.parse(latest.payload_json)) !== hybridDigest(work.start.assignment.receipt))
41
+ refuse("latest_round_replaced");
42
+ if (packet.owner_stage !== scope.stage || work.start.assignment.custody.scope.attempt !== graph.attempt
43
+ || work.start.assignment.custody.scope.cycle !== graph.cycle || packet.input_sha256 !== input.inputSha256
44
+ || packet.user_inputs_sha256 !== selected.inputs.sha256 || packet.scope_revision !== selected.revision
45
+ || packet.baseline_sha256 !== selected.baseline.sha256)
46
+ refuse("issued_stage_input_changed");
47
+ if (packet.stage !== role)
48
+ refuse(role === "review" ? "final_review_work_required" : "correction_author_work_required");
49
+ const storedActors = context.db.all("SELECT DISTINCT COALESCE(h.harness,s.harness) AS harness,COALESCE(h.provider_session_id,s.provider_session_id) AS provider_session_id FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id LEFT JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id LEFT JOIN hook_events h ON h.project_id=w.project_id AND h.event_key=ws.hook_event_id WHERE w.project_id=? AND w.run_id=? AND ws.id<>? ORDER BY 1,2", [scope.projectId, scope.runId, work.completed.session.id]);
50
+ const actors = [...storedActors, ...work.start.assignment.retained.body.work_history.works.flatMap(prior => prior.actor ? [prior.actor] : [])];
51
+ const excluded = [...new Map(actors.map(actor => [`${actor.harness}\0${actor.provider_session_id}`, actor])).entries()]
52
+ .sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, actor]) => actor);
53
+ if (excluded.some(actor => !actor.harness || !actor.provider_session_id))
54
+ refuse("prior_physical_owner_unknown");
55
+ // Includes every prior writer round, failed/cancelled attempts and root
56
+ // coordinators, not just the latest hardening author selected by a caller.
57
+ try {
58
+ assertDiagnosticSessionIndependence([work.completed.session], excluded);
59
+ }
60
+ catch (error) {
61
+ if (error instanceof AppError && error.code === "diagnostic_work_collection_invalid")
62
+ refuse("physical_session_reused");
63
+ throw error;
64
+ }
65
+ const binding = { schema_id: "dd-flow/interactive-work-collection@1", run_id: scope.runId, stage: scope.stage, attempt: graph.attempt,
66
+ bundle_sha256: bundle.sha256, input_sha256: input.inputSha256, baseline_sha256: selected.baseline.sha256,
67
+ user_inputs_sha256: selected.inputs.sha256, scope_revision: selected.revision, work_id: workId,
68
+ registration_sha256: hybridDigest(work.start.assignment.receipt), settlement_sha256: hybridDigest(work.receipt),
69
+ result_sha256: work.receipt.result_sha256, actor: work.start.receipt.actor, excluded_actors: excluded };
70
+ return { binding, work };
71
+ }
72
+ /** Latest actual round selection, not a semantic review verdict or check gate.
73
+ * Stage handlers still own issue dispositions, corrections and verification. */
74
+ export class PreparedInteractiveWorkCollection {
75
+ sha256;
76
+ #scope;
77
+ #bundle;
78
+ #sql;
79
+ #changes;
80
+ #role;
81
+ constructor(scope, bundle, sql, changes, role) {
82
+ this.#scope = { ...scope };
83
+ this.#bundle = bundle;
84
+ this.#sql = sql;
85
+ this.#changes = changes;
86
+ this.#role = role;
87
+ this.sha256 = hybridDigest(sql.binding);
88
+ Object.freeze(this);
89
+ }
90
+ static prepare(context, input) {
91
+ return this.prepareRole(context, input, input.stage);
92
+ }
93
+ /** Handoff only. A correction author is never final review acceptance. */
94
+ static prepareCorrectionAuthor(context, input) {
95
+ return this.prepareRole(context, { ...input, stage: "review" }, "hardening");
96
+ }
97
+ static prepareRole(context, input, role) {
98
+ if (context.db.isOwnedWriteTransaction?.())
99
+ refuse("physical_preparation_outside_writer_required");
100
+ if (!["hardening", "review"].includes(input.stage))
101
+ refuse("work_stage_required");
102
+ const selected = readInteractiveStageScope(context, input), scope = { ...selected.scope, stage: input.stage }, bundle = selected.bundle;
103
+ const sql = context.db.readSnapshot(() => collectSql(context, scope, bundle, role)), frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
104
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot, workspace: frozen.workspace,
105
+ gitExecutable: git.git_executable, env: context.env }, sql.work.start.baseline.snapshot().entries.map(item => item.path));
106
+ if (createHash("sha256").update(JSON.stringify(changes.currentSnapshot())).digest("hex") !== sql.work.receipt.observed_snapshot_sha256)
107
+ refuse("settled_output_changed");
108
+ const prepared = new PreparedInteractiveWorkCollection(scope, bundle, sql, changes, role);
109
+ prepared.revalidate(context);
110
+ return prepared;
111
+ }
112
+ snapshot() { return structuredClone(this.#sql.binding); }
113
+ results() { return [{ workId: this.#sql.work.completed.workId, result: structuredClone(this.#sql.work.result) }]; }
114
+ revalidate(context) { readInteractiveWorkGit(this.#bundle); this.#changes.revalidate(); this.assertCustody(context); }
115
+ assertCustody(context) {
116
+ if (hybridDigest(context.db.readSnapshot(() => collectSql(context, this.#scope, this.#bundle, this.#role)).binding) !== this.sha256)
117
+ refuse("collection_changed");
118
+ }
119
+ }
@@ -0,0 +1,180 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
5
+ import { readInteractiveWorkGit, readInteractiveWorkStart } from "./interactive-work-start.js";
6
+ import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
7
+ import { PreparedInteractiveWorkspaceChanges, readInteractiveMutationScope } from "./interactive-mutation-scope.js";
8
+ import { validateInteractiveWorkResult, pendingInteractiveIssues } from "./interactive-work-results.js";
9
+ import { interactiveWorkPathAllowed } from "./interactive-work-batch.js";
10
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
11
+ import { stageGraphScope } from "./stage-work-graph.js";
12
+ import { readCompletedResearchWork } from "./diagnostic-work-collection.js";
13
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
14
+ function refuse(reason) {
15
+ throw new AppError("interactive_work_completion_invalid", "Interactive completion requires its original start and actual bounded changes", 2, { reason, effect: "no_effect" });
16
+ }
17
+ function workChanges(delta) {
18
+ return delta.changes.map(item => {
19
+ if (item.before && item.before.kind !== "file" || item.after && item.after.kind !== "file")
20
+ refuse("regular_file_delta_required");
21
+ return { path: item.path, before_sha256: item.before?.sha256 ?? null, after_sha256: item.after?.sha256 ?? null,
22
+ before_mode: item.before?.mode ?? null, after_mode: item.after?.mode ?? null };
23
+ });
24
+ }
25
+ /** SQL-only completed output custody, independent of mutable workspace/results
26
+ * projections. Semantic verdicts are returned intact, not turned into PASS. */
27
+ export function readCompletedInteractiveWork(context, scope, bundle) {
28
+ return context.db.readSnapshot(() => readCompletedInteractiveWorkSql(context, scope, bundle));
29
+ }
30
+ function readCompletedInteractiveWorkSql(context, scope, bundle) {
31
+ const completed = readCompletedResearchWork(context, scope, scope.workId);
32
+ const start = readInteractiveWorkStart(context, scope, bundle, completed.session.id);
33
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='interactive.work_settlement' AND json_extract(payload_json,'$.work_session_id')=? LIMIT 2", [scope.projectId, completed.session.id]);
34
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 8 * 1024 * 1024)
35
+ refuse("settlement_custody_missing");
36
+ const receipt = JSON.parse(rows[0].payload_json);
37
+ if (!receipt || Object.keys(receipt).sort().join() !== "bundle_sha256,changes,observed_snapshot_sha256,packet_sha256,result_sha256,run_id,schema_id,start_snapshot_sha256,work_id,work_session_id"
38
+ || receipt.schema_id !== "dd-flow/interactive-work-settlement@1" || receipt.run_id !== scope.runId || receipt.work_id !== scope.workId
39
+ || receipt.work_session_id !== completed.session.id || receipt.bundle_sha256 !== bundle.sha256
40
+ || receipt.packet_sha256 !== start.assignment.packet.packet_sha256 || receipt.start_snapshot_sha256 !== start.baseline.sha256
41
+ || receipt.result_sha256 !== hash(Buffer.from(completed.result)) || !/^[a-f0-9]{64}$/.test(receipt.observed_snapshot_sha256)
42
+ || !Array.isArray(receipt.changes))
43
+ refuse("settlement_binding_changed");
44
+ const link = context.db.get("SELECT result_receipt_json FROM work_sessions WHERE id=?", [completed.session.id]);
45
+ if (!link?.result_receipt_json || Buffer.byteLength(link.result_receipt_json) > 16384)
46
+ refuse("common_result_custody_missing");
47
+ const shared = JSON.parse(link.result_receipt_json);
48
+ if (shared.work_id !== scope.workId || shared.work_session_id !== completed.session.id || shared.status !== "completed"
49
+ || shared.result_sha256 !== receipt.result_sha256)
50
+ refuse("common_result_binding_changed");
51
+ const entries = new Map(start.baseline.snapshot().entries.map(item => [item.path, item]));
52
+ for (const change of receipt.changes) {
53
+ const before = entries.get(change?.path);
54
+ if (!change || change.before_sha256 !== (before?.kind === "file" ? before.sha256 : null)
55
+ || change.before_mode !== (before?.kind === "file" ? before.mode : null))
56
+ refuse("settled_before_version_changed");
57
+ }
58
+ const result = validateInteractiveWorkResult(Buffer.from(completed.result), bundle.blob("schemas/interactive-work-result.schema.json"), {
59
+ workId: scope.workId, packet: start.assignment.packet, actualChanges: receipt.changes, checks: [], checksSatisfied: true,
60
+ priorIssues: pendingInteractiveIssues(start.assignment.retained.body.work_history, start.assignment.packet),
61
+ observedSources: receipt.changes.filter(item => item.after_sha256 !== null).map(item => ({ root: "workspace", path: item.path, sha256: item.after_sha256 }))
62
+ });
63
+ return { completed, start, receipt, result };
64
+ }
65
+ /** Physical result preparation for common Work settlement, not Stage acceptance.
66
+ * No caller-provided packet, observation, actor or check receipt is authoritative. */
67
+ export class PreparedInteractiveWorkCompletion {
68
+ workId;
69
+ sha256;
70
+ #scope;
71
+ #bundle;
72
+ #start;
73
+ #changes;
74
+ #delta;
75
+ #bytes;
76
+ constructor(scope, bundle, start, changes, bytes) {
77
+ this.#scope = { ...scope };
78
+ this.workId = scope.workId;
79
+ this.#bundle = bundle;
80
+ this.#start = start;
81
+ this.#changes = changes;
82
+ const currentBytes = Buffer.from(JSON.stringify(changes.currentSnapshot()));
83
+ const current = PreparedInteractiveGitBaseline.restore(currentBytes, hash(currentBytes));
84
+ this.#delta = start.baseline.changes(current);
85
+ if (this.#delta.head_changed || this.#delta.index_changed)
86
+ refuse("work_git_movement");
87
+ const actualChanges = workChanges(this.#delta);
88
+ let proposal;
89
+ try {
90
+ proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
91
+ }
92
+ catch {
93
+ refuse("invalid_proposal");
94
+ }
95
+ // Existing content-change proposals may omit mode metadata. Never turn an
96
+ // unchanged-hash placeholder into a later, unreported permission change.
97
+ if (proposal && typeof proposal === "object" && !Array.isArray(proposal) && Array.isArray(proposal.changes)) {
98
+ const value = proposal;
99
+ const observations = new Map(actualChanges.map(item => [item.path, item]));
100
+ value.changes = value.changes.map(item => {
101
+ const observed = observations.get(item?.path ?? "");
102
+ return item && observed && item.before_sha256 !== item.after_sha256 && !Object.hasOwn(item, "before_mode") && !Object.hasOwn(item, "after_mode")
103
+ ? { ...item, before_mode: observed.before_mode, after_mode: observed.after_mode } : item;
104
+ });
105
+ }
106
+ const schema = bundle.blob("schemas/interactive-work-result.schema.json");
107
+ if (!schema.equals(bundle.engine().schema("interactive-work-result")))
108
+ refuse("selected_schema_changed");
109
+ const result = validateInteractiveWorkResult(Buffer.from(JSON.stringify(proposal)), schema, { workId: scope.workId, packet: start.assignment.packet,
110
+ actualChanges, observedSources: current.snapshot().entries.filter(item => item.kind === "file" && interactiveWorkPathAllowed(item.path))
111
+ .map(item => ({ root: "workspace", path: item.path, sha256: item.sha256 })),
112
+ // This Work contract assigns no command checks. Stage hardening owns the
113
+ // flow's required verification; [] does not assert those gates passed.
114
+ checks: [], checksSatisfied: true, priorIssues: pendingInteractiveIssues(start.assignment.retained.body.work_history, start.assignment.packet) });
115
+ this.#bytes = Buffer.from(JSON.stringify(result));
116
+ this.sha256 = hash(this.#bytes);
117
+ Object.freeze(this);
118
+ }
119
+ static prepare(context, input) {
120
+ if (context.db.isOwnedWriteTransaction?.())
121
+ refuse("preparation_outside_writer_required");
122
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
123
+ refuse("unbounded_result");
124
+ const work = context.db.get("SELECT w.project_id,w.run_id,r.project_root FROM works w JOIN runs r ON r.project_id=w.project_id AND r.id=w.run_id WHERE w.work_id=?", [input.workId]);
125
+ const link = context.db.get("SELECT id,session_id FROM work_sessions WHERE work_id=? AND status='running'", [input.workId]);
126
+ if (!work || !link)
127
+ refuse("active_work_required");
128
+ const scope = { projectId: work.project_id, runId: work.run_id, workId: input.workId };
129
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: work.project_root, runId: work.run_id });
130
+ if (!bundle)
131
+ refuse("bundle_required");
132
+ const start = readInteractiveWorkStart(context, scope, bundle, link.id), frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
133
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot, workspace: frozen.workspace,
134
+ gitExecutable: git.git_executable, env: context.env }, start.baseline.snapshot().entries.map(item => item.path));
135
+ const prepared = new PreparedInteractiveWorkCompletion(scope, bundle, start, changes, Buffer.from(input.bytes));
136
+ prepared.assertSettlementScope(context, link.session_id, link.id);
137
+ prepared.revalidate(context);
138
+ return prepared;
139
+ }
140
+ bytes() { return Buffer.from(this.#bytes); }
141
+ revalidate(context) {
142
+ readInteractiveWorkGit(this.#bundle);
143
+ this.#changes.revalidate();
144
+ this.assertSettlementScope(context, this.#start.receipt.actor.session_id, this.#start.receipt.work_session_id);
145
+ }
146
+ assertSettlementScope(context, sessionId, workSessionId) {
147
+ const start = readInteractiveWorkStart(context, this.#scope, this.#bundle, workSessionId), packet = start.assignment.packet;
148
+ if (hybridDigest(start.receipt) !== hybridDigest(this.#start.receipt) || sessionId !== start.receipt.actor.session_id)
149
+ refuse("start_owner_changed");
150
+ const selected = readInteractiveMutationScope(context, this.#scope, this.#bundle), observed = this.#changes.snapshot();
151
+ if (selected.needs_scope_refresh || selected.revision !== packet.scope_revision || selected.inputs.sha256 !== packet.user_inputs_sha256
152
+ || selected.baseline.sha256 !== packet.baseline_sha256 || observed.scope_revision !== selected.revision)
153
+ refuse("issued_inputs_changed");
154
+ const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle), index = JSON.parse(run.index_json);
155
+ const graph = stageGraphScope(context, this.#scope.projectId, this.#scope.runId, packet.owner_stage);
156
+ const stage = readDescriptorStageInput(context, { ...this.#scope, stage: packet.owner_stage }, this.#bundle);
157
+ if (run.status !== "running" || index.current_stage !== packet.owner_stage || graph.attempt !== start.assignment.custody.scope.attempt
158
+ || graph.cycle !== start.assignment.custody.scope.cycle || stage.attempt !== graph.attempt || stage.inputSha256 !== packet.input_sha256
159
+ || !index.stage_runs?.some((item) => item.stage === packet.owner_stage && item.status === "running" && item.attempt === graph.attempt)
160
+ || !context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND work_id=? AND status='running'", [this.#scope.projectId, this.#scope.runId, start.assignment.receipt.parent_work_id])
161
+ || !context.db.get("SELECT 1 FROM works w JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND w.status='running' AND ws.id=? AND ws.session_id=? AND ws.status='running' AND s.worker_id=w.work_id AND s.run_id=w.run_id AND s.status='active' AND s.harness=? AND s.provider_session_id=?", [this.#scope.projectId, this.#scope.runId, this.workId, workSessionId, sessionId, start.receipt.actor.harness, start.receipt.actor.provider_session_id]))
162
+ refuse("active_settlement_owner_required");
163
+ }
164
+ recordSettlement(context, sessionId, workSessionId) {
165
+ if (!context.db.isOwnedWriteTransaction?.())
166
+ refuse("owned_writer_required");
167
+ this.assertSettlementScope(context, sessionId, workSessionId);
168
+ this.#changes.assertCustody(context);
169
+ if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='interactive.work_settlement' AND json_extract(payload_json,'$.work_session_id')=?", [this.#scope.projectId, workSessionId]))
170
+ refuse("settlement_already_retained");
171
+ const receipt = { schema_id: "dd-flow/interactive-work-settlement@1", run_id: this.#scope.runId, work_id: this.workId, work_session_id: workSessionId,
172
+ bundle_sha256: this.#bundle.sha256, packet_sha256: this.#start.assignment.packet.packet_sha256,
173
+ start_snapshot_sha256: this.#start.baseline.sha256, observed_snapshot_sha256: this.#delta.current_sha256,
174
+ result_sha256: this.sha256, changes: workChanges(this.#delta) };
175
+ const json = JSON.stringify(receipt);
176
+ if (Buffer.byteLength(json) > 8 * 1024 * 1024)
177
+ refuse("unbounded_settlement");
178
+ context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,'interactive.work_settlement',?,?)", [this.#scope.projectId, json, context.now()]);
179
+ }
180
+ }