@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,249 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
6
+ import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
7
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
8
+ import { prepareStagePacket, acceptStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
9
+ import { putInteractiveBundleData, readInteractiveBundleData } from "./interactive-bundle-data.js";
10
+ import { interactiveWorkBatch, interactiveWorkPathAllowed } from "./interactive-work-batch.js";
11
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
12
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
13
+ import { readInteractiveWorkHistory, assertInteractiveWorkHistory, assertInteractiveCorrectionHistory } from "./interactive-work-history.js";
14
+ import { pendingInteractiveIssues } from "./interactive-work-results.js";
15
+ import { PreparedInteractiveChecks } from "./interactive-checks.js";
16
+ const event = "interactive.work_context", logical = "runtime/work-context.json";
17
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
18
+ function refuse(reason) {
19
+ throw new AppError("interactive_work_context_invalid", "Interactive Work requires its exact retained user data and original versions", 2, { reason, effect: "no_effect" });
20
+ }
21
+ function json(bytes) {
22
+ try {
23
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
24
+ }
25
+ catch {
26
+ return refuse("invalid_context_json");
27
+ }
28
+ }
29
+ function record(context, scope, bundle, reference) {
30
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
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')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.reference.path')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage, scope.attempt, reference.path]);
32
+ if (!rows.length)
33
+ return null;
34
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 4 * 1024 * 1024)
35
+ refuse("ambiguous_or_unbounded_context");
36
+ const value = json(Buffer.from(rows[0].payload_json));
37
+ if (!value || Object.keys(value).sort().join() !== "attempt,bundle_sha256,input,logical_sha256,packet_refs,reference,run_id,schema_id,stage"
38
+ || value.schema_id !== "dd-flow/interactive-work-context@1" || value.run_id !== scope.runId || value.stage !== scope.stage || value.attempt !== scope.attempt
39
+ || value.bundle_sha256 !== bundle.sha256 || !/^[a-f0-9]{64}$/.test(value.logical_sha256)
40
+ || hybridDigest(value.reference) !== hybridDigest(reference) || hybridDigest(value.input?.context) !== hybridDigest(reference)
41
+ || value.input?.run_id !== scope.runId || value.input.owner_stage !== scope.stage
42
+ || !Array.isArray(value.packet_refs) || !value.packet_refs.length || value.packet_refs.length > 33)
43
+ refuse("context_scope_changed");
44
+ interactiveWorkBatch(value.input);
45
+ return value;
46
+ }
47
+ /** Selected SQL packet bytes, never mutable projections or caller-owned JSON. */
48
+ export function readRetainedInteractiveWorkContext(context, scope, bundle, reference) {
49
+ const value = record(context, scope, bundle, reference);
50
+ if (!value)
51
+ refuse("context_custody_missing");
52
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
53
+ if (!run.run_root)
54
+ refuse("run_root_missing");
55
+ const prefix = `${bundle.descriptor.stage(scope.stage).directory}/work-contexts/${value.logical_sha256}/`;
56
+ const files = new Map(), inventory = [];
57
+ for (const ref of value.packet_refs) {
58
+ if (!ref || ref.root !== "run" || !ref.path.startsWith(prefix) || !/^[a-f0-9]{64}$/.test(ref.sha256))
59
+ refuse("foreign_context_part");
60
+ const name = ref.path.slice(prefix.length);
61
+ if (!interactiveWorkPathAllowed(name) || files.has(name))
62
+ refuse("invalid_context_inventory");
63
+ const suffix = `/${ref.path}`;
64
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=? AND substr(json_extract(payload_json,'$.file'),-?)=? LIMIT 2", [scope.projectId, scope.runId, scope.stage, scope.attempt, ref.sha256, suffix.length, suffix]);
65
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
66
+ refuse("context_part_custody_missing");
67
+ const packet = json(Buffer.from(rows[0].payload_json));
68
+ if (typeof packet.base64 !== "string" || resolveStagePublicationPath(context, scope.projectId, scope.runId, packet.file) !== path.join(run.run_root, ref.path))
69
+ refuse("context_part_scope_changed");
70
+ const bytes = Buffer.from(packet.base64, "base64");
71
+ if (!bytes.length || bytes.length > 1024 * 1024 || bytes.toString("base64") !== packet.base64 || hash(bytes) !== ref.sha256)
72
+ refuse("context_part_changed");
73
+ files.set(name, bytes);
74
+ inventory.push({ path: name, sha256: ref.sha256 });
75
+ }
76
+ const selected = readInteractiveBundleData({ manifest: { ...bundle.manifest, inventory }, blob: name => files.get(name) ?? Buffer.alloc(0) }, logical);
77
+ if (selected.sha256 !== value.logical_sha256 || selected.reference.sha256 !== reference.sha256 || reference.path !== `${prefix}${logical}`)
78
+ refuse("context_identity_changed");
79
+ const body = json(selected.bytes);
80
+ if (!body || body.schema_id !== "dd-flow/interactive-work-data@1" || body.run_id !== scope.runId || body.stage !== scope.stage
81
+ || body.work_role !== value.input.stage
82
+ || body.bundle_sha256 !== bundle.sha256 || body.input_sha256 !== value.input.input_sha256
83
+ || body.baseline_sha256 !== value.input.baseline_sha256
84
+ || body.user_inputs_sha256 !== value.input.user_inputs_sha256 || body.scope_revision !== value.input.scope_revision
85
+ || body.file_changes_sha256 !== value.input.file_changes_sha256 || !Array.isArray(body.file_changes)
86
+ || hybridDigest(body.file_changes) !== value.input.file_changes_sha256 || !Array.isArray(body.current_files)
87
+ || body.current_files.some(item => !item || item.kind !== "file" || !interactiveWorkPathAllowed(item.path) || !/^[a-f0-9]{64}$/.test(item.sha256))
88
+ || body.worker_instruction?.root !== "run" || body.worker_instruction.path !== `execution-bundle/interactive/hybrid/${value.input.stage}-worker.md`
89
+ || hash(Buffer.from(value.input.instruction)) !== body.worker_instruction.sha256
90
+ || hybridDigest(value.input.read_inputs) !== hybridDigest([...value.packet_refs, body.worker_instruction, ...(body.stage_checks?.sources ?? []),
91
+ ...body.current_files.map(item => ({ root: "workspace", path: item.path, sha256: item.sha256 }))]))
92
+ refuse("context_binding_changed");
93
+ if (body.stage_checks) {
94
+ const { plan, plan_sha256, sources, receipts } = body.stage_checks;
95
+ if (body.work_role !== "review" || plan_sha256 !== hybridDigest(plan) || plan.run_id !== scope.runId || plan.stage !== scope.stage
96
+ || plan.bundle_sha256 !== bundle.sha256 || plan.input_sha256 !== body.input_sha256 || plan.user_inputs_sha256 !== body.user_inputs_sha256
97
+ || plan.scope_revision !== body.scope_revision || plan.file_changes_sha256 !== body.file_changes_sha256
98
+ || !Array.isArray(receipts) || !Array.isArray(sources) || sources.some(ref => ref.root !== "run")
99
+ || receipts.some(ref => !sources.some(source => source.path === ref.path && source.sha256 === ref.sha256)))
100
+ refuse("stage_checks_changed");
101
+ }
102
+ assertInteractiveWorkHistory(body.work_history, scope.runId, bundle.sha256);
103
+ if (scope.stage === "review" && value.input.stage === "hardening")
104
+ assertInteractiveCorrectionHistory(body.work_history, scope.attempt);
105
+ if (hybridDigest(body.pending_issues) !== hybridDigest(pendingInteractiveIssues(body.work_history, body)))
106
+ refuse("pending_issues_changed");
107
+ return { record: value, bytes: selected.bytes, body };
108
+ }
109
+ /** Source preparation only, not a Work registration/launch permission. The
110
+ * graph owner pairs retainPreparedSql with its existing atomic batch writer. */
111
+ export class PreparedInteractiveWorkContext {
112
+ #selected;
113
+ #changes;
114
+ #scope;
115
+ #record;
116
+ #packets;
117
+ #history;
118
+ #verification;
119
+ constructor(selected, changes, scope, value, packets, history, verification) {
120
+ this.#selected = selected;
121
+ this.#changes = changes;
122
+ this.#scope = scope;
123
+ this.#record = structuredClone(value);
124
+ this.#packets = packets;
125
+ this.#history = structuredClone(history);
126
+ this.#verification = verification;
127
+ Object.freeze(this);
128
+ }
129
+ static prepare(context, input) {
130
+ if (context.db.isOwnedWriteTransaction?.())
131
+ refuse("preparation_outside_writer_required");
132
+ if (!["hardening", "review"].includes(input.stage))
133
+ refuse("work_stage_required");
134
+ const role = input.role ?? input.stage;
135
+ if (!["hardening", "review"].includes(role) || input.stage === "hardening" && role !== "hardening")
136
+ refuse("invalid_work_owner_role");
137
+ const selected = readInteractiveStageScope(context, input), { bundle, scope, run } = selected;
138
+ const stage = readDescriptorStageInput(context, scope, bundle);
139
+ if (!run.run_root)
140
+ refuse("run_root_missing");
141
+ const history = readInteractiveWorkHistory(context, scope, bundle);
142
+ if (input.stage === "review" && role === "hardening") {
143
+ assertInteractiveCorrectionHistory(history, stage.attempt);
144
+ bundle.execution.work(input.stage, undefined, role);
145
+ }
146
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: bundle.execution.snapshot().scope.projectRoot,
147
+ workspace: bundle.execution.snapshot().scope.workspace, gitExecutable: input.gitExecutable, env: context.env });
148
+ const workerPath = `interactive/hybrid/${role}-worker.md`, workerBytes = bundle.blob(workerPath);
149
+ let instruction;
150
+ try {
151
+ instruction = new TextDecoder("utf-8", { fatal: true }).decode(workerBytes);
152
+ }
153
+ catch {
154
+ return refuse("invalid_worker_instruction");
155
+ }
156
+ if (!bundle.blob("schemas/interactive-work-result.schema.json").equals(bundle.engine().schema("interactive-work-result")))
157
+ refuse("worker_schema_not_qualified");
158
+ const current = changes.currentSnapshot(), patch = changes.snapshot().delta.changes, entry = selected.selected.baseline.snapshot(), state = selected.selected;
159
+ let stageChecks = null;
160
+ let verificationOwner = null;
161
+ if (input.checks) {
162
+ if (!(input.checks instanceof PreparedInteractiveChecks) || role !== "review")
163
+ refuse("owned_review_checks_required");
164
+ const plan = input.checks.value(), evidence = input.checks.evidence(context), sources = new Map();
165
+ if (plan.run_id !== input.runId || plan.stage !== input.stage || plan.bundle_sha256 !== bundle.sha256 || plan.input_sha256 !== stage.inputSha256
166
+ || plan.user_inputs_sha256 !== state.inputs.sha256 || plan.scope_revision !== state.revision || plan.file_changes_sha256 !== hybridDigest(patch))
167
+ refuse("foreign_review_checks");
168
+ for (const source of evidence?.sourceReferences() ?? []) {
169
+ const prior = sources.get(source.path);
170
+ if (prior && prior.sha256 !== source.sha256)
171
+ refuse("conflicting_check_sources");
172
+ sources.set(source.path, source);
173
+ }
174
+ stageChecks = { plan_sha256: input.checks.sha256, plan, receipts: evidence?.retainedReferences(context) ?? [], sources: [...sources.values()] };
175
+ verificationOwner = { context, checks: input.checks, evidence };
176
+ }
177
+ const currentFiles = current.entries.filter(item => item.kind === "file" && interactiveWorkPathAllowed(item.path));
178
+ const body = { schema_id: "dd-flow/interactive-work-data@1", run_id: input.runId, stage: input.stage, work_role: role, bundle_sha256: bundle.sha256,
179
+ input_sha256: stage.inputSha256, baseline_sha256: state.baseline.sha256, user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision, file_changes_sha256: hybridDigest(patch),
180
+ delivered_user_inputs: { initial_request: state.inputs.initial_request, messages: state.inputs.messages, finish_requested: state.inputs.finish_requested },
181
+ entry_git: { head: entry.head, branch: entry.branch, index_sha256: entry.index_sha256, status_base64: entry.status_base64, visibility: entry.visibility },
182
+ scope: { paths: state.active_paths, decisions: state.records.map(item => ({ basis: item.basis, reason: item.reason, paths: item.paths, user_inputs_sha256: item.user_inputs_sha256 })) },
183
+ before_versions: state.before_versions.filter(item => state.active_paths.includes(item.path)), current_files: currentFiles, file_changes: patch,
184
+ work_history: history, stage_checks: stageChecks, pending_issues: pendingInteractiveIssues(history, { file_changes_sha256: hybridDigest(patch), user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision }),
185
+ worker_instruction: { root: "run", path: `execution-bundle/${workerPath}`, sha256: hash(workerBytes) } };
186
+ const bytes = Buffer.from(JSON.stringify(body)), blobs = new Map(), data = putInteractiveBundleData(blobs, logical, bytes);
187
+ const prefix = `${bundle.descriptor.stage(input.stage).directory}/work-contexts/${data.sha256}/`;
188
+ const packetRefs = [...blobs].map(([name, content]) => ({ root: "run", path: `${prefix}${name}`, sha256: hash(content) }));
189
+ const reference = { root: "run", path: `${prefix}${logical}`, sha256: data.reference.sha256 };
190
+ const workInput = { run_id: input.runId, stage: role, owner_stage: input.stage, input_sha256: stage.inputSha256,
191
+ baseline_sha256: state.baseline.sha256, user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision,
192
+ file_changes_sha256: hybridDigest(patch), context: reference, read_inputs: [...packetRefs, { ...body.worker_instruction, root: "run" }, ...(stageChecks?.sources ?? []),
193
+ ...currentFiles.map(item => ({ root: "workspace", path: item.path, sha256: item.sha256 }))],
194
+ write_paths: role === "hardening" ? [...state.active_paths] : [], instruction };
195
+ interactiveWorkBatch(workInput);
196
+ const ownedScope = { ...scope, stage: input.stage, attempt: stage.attempt };
197
+ const value = { schema_id: "dd-flow/interactive-work-context@1", run_id: input.runId, stage: input.stage,
198
+ attempt: stage.attempt, bundle_sha256: bundle.sha256, logical_sha256: data.sha256, reference, packet_refs: packetRefs, input: workInput };
199
+ if (Buffer.byteLength(JSON.stringify(value)) > 4 * 1024 * 1024)
200
+ refuse("unbounded_context_metadata");
201
+ const packets = [...blobs].map(([name, content]) => prepareStagePacket(path.join(run.run_root, prefix, name), content));
202
+ const prepared = new PreparedInteractiveWorkContext(selected, changes, ownedScope, value, packets, history, verificationOwner);
203
+ prepared.revalidate();
204
+ return prepared;
205
+ }
206
+ snapshot() { return structuredClone(this.#record); }
207
+ workInput() { return structuredClone(this.#record.input); }
208
+ revalidate() { this.#changes.revalidate(); if (this.#verification) {
209
+ this.#verification.checks.revalidate(this.#verification.context);
210
+ this.#verification.evidence?.revalidate(this.#verification.context);
211
+ } }
212
+ assertCustody(context) {
213
+ this.#selected.assertCustody(context);
214
+ this.#changes.assertCustody(context);
215
+ this.#verification?.checks.assertCustody(context);
216
+ this.#verification?.evidence?.assertCustody(context);
217
+ if (hybridDigest(readInteractiveWorkHistory(context, this.#scope, this.#selected.bundle)) !== hybridDigest(this.#history))
218
+ refuse("work_history_changed");
219
+ const current = readDescriptorStageInput(context, this.#scope, this.#selected.bundle);
220
+ if (current.attempt !== this.#scope.attempt || current.inputSha256 !== this.#record.input.input_sha256)
221
+ refuse("stage_input_changed");
222
+ }
223
+ retainPreparedSql(context) {
224
+ if (!context.db.isOwnedWriteTransaction?.())
225
+ refuse("owned_writer_required");
226
+ const prior = record(context, this.#scope, this.#selected.bundle, this.#record.reference);
227
+ if (prior) {
228
+ if (hybridDigest(prior) !== hybridDigest(this.#record))
229
+ refuse("context_changed");
230
+ return { changed: false };
231
+ }
232
+ this.assertCustody(context);
233
+ assertRunMutationAllowed(context, this.#scope.projectId, this.#scope.runId);
234
+ const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#selected.bundle), index = JSON.parse(run.index_json);
235
+ if (run.status !== "running" || index.current_stage !== this.#scope.stage || !index.stage_runs?.some((item) => item.stage === this.#scope.stage && item.status === "running" && item.attempt === this.#scope.attempt))
236
+ refuse("active_work_stage_required");
237
+ for (const prepared of this.#packets)
238
+ acceptStagePacket(context, { ...this.#scope, prepared });
239
+ context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [this.#scope.projectId, event, JSON.stringify(this.#record), context.now()]);
240
+ readRetainedInteractiveWorkContext(context, this.#scope, this.#selected.bundle, this.#record.reference);
241
+ return { changed: true };
242
+ }
243
+ publish(context) {
244
+ const retained = readRetainedInteractiveWorkContext(context, this.#scope, this.#selected.bundle, this.#record.reference);
245
+ const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#selected.bundle);
246
+ for (const ref of retained.record.packet_refs)
247
+ publishStagePacket(context, this.#scope.projectId, this.#scope.runId, this.#scope.stage, path.join(run.run_root, ref.path));
248
+ }
249
+ }
@@ -0,0 +1,112 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { readRegisteredInteractiveWork } from "./interactive-work-registration.js";
4
+ import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
5
+ import { readInteractiveWorkStart } from "./interactive-work-start.js";
6
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
+ const hash = (text) => createHash("sha256").update(text).digest("hex");
8
+ function refuse(reason) {
9
+ throw new AppError("interactive_work_history_invalid", "Interactive corrections require the original registered Work history", 2, { reason, effect: "no_effect" });
10
+ }
11
+ /** A reviewer remains read-only. A separate author can be registered inside
12
+ * that same Stage only after its independently completed negative review. */
13
+ export function assertInteractiveCorrectionHistory(history, attempt) {
14
+ const prior = history.works.findLast(work => work.owner_stage === "review" && work.stage === "review" && work.attempt === attempt && work.status === "completed");
15
+ if (!prior || typeof prior.result === "string" || prior.result.verdict === "ready")
16
+ refuse("negative_review_required_for_correction");
17
+ }
18
+ /** Retained task data validation only; never reconstruct custody from this JSON. */
19
+ export function assertInteractiveWorkHistory(value, runId, bundleSha256) {
20
+ if (!value || Object.keys(value).sort().join() !== "bundle_sha256,run_id,schema_id,works"
21
+ || value.schema_id !== "dd-flow/interactive-work-history@1" || value.run_id !== runId || value.bundle_sha256 !== bundleSha256
22
+ || !Array.isArray(value.works) || value.works.length > 20_000)
23
+ refuse("invalid_retained_history");
24
+ const ids = new Set();
25
+ let total = 0;
26
+ for (const item of value.works) {
27
+ if (!item || Object.keys(item).sort().join() !== "actor,attempt,owner_stage,request_id,result,result_sha256,review_binding,stage,status,work_id,work_session_id"
28
+ || typeof item.work_id !== "string" || !item.work_id || ids.has(item.work_id) || !["hardening", "review"].includes(item.stage)
29
+ || typeof item.attempt !== "string" || !item.attempt || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(item.request_id)
30
+ || !["hardening", "review"].includes(item.owner_stage) || item.owner_stage === "hardening" && item.stage !== "hardening"
31
+ || !["completed", "failed", "cancelled"].includes(item.status)
32
+ || (item.work_session_id === null ? item.actor !== null || item.status === "completed"
33
+ : typeof item.work_session_id !== "string" || !item.work_session_id || !item.actor
34
+ || Object.keys(item.actor).sort().join() !== "harness,provider_session_id"
35
+ || typeof item.actor.harness !== "string" || !item.actor.harness
36
+ || typeof item.actor.provider_session_id !== "string" || !item.actor.provider_session_id))
37
+ refuse("invalid_history_entry");
38
+ if (item.status === "completed" && item.stage === "review" ? !item.review_binding
39
+ || Object.keys(item.review_binding).sort().join() !== "file_changes_sha256,scope_revision,user_inputs_sha256"
40
+ || Object.values(item.review_binding).some(value => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value))
41
+ : item.review_binding !== null)
42
+ refuse("invalid_history_review_binding");
43
+ const text = typeof item.result === "string" ? item.result : JSON.stringify(item.result);
44
+ if (typeof text !== "string" || !text.length || Buffer.byteLength(text) > 1024 * 1024 || hash(text) !== item.result_sha256)
45
+ refuse("history_result_changed");
46
+ if (item.status === "completed" ? typeof item.result !== "object" || !item.result || item.result.schema_id !== "dd-flow/interactive-work-result@1"
47
+ || item.result.run_id !== runId || item.result.work_id !== item.work_id || item.result.stage !== item.stage : typeof item.result !== "string")
48
+ refuse("foreign_history_result");
49
+ ids.add(item.work_id);
50
+ total += Buffer.byteLength(JSON.stringify(item));
51
+ if (total > 32 * 1024 * 1024)
52
+ refuse("history_budget_exceeded");
53
+ }
54
+ }
55
+ /** Original SQL custody, in registration order, across all attempts and stages.
56
+ * Missing/active Works refuse; terminal failures remain data, never fake reviews. */
57
+ export function readInteractiveWorkHistory(context, scope, bundle) {
58
+ return context.db.readSnapshot(() => {
59
+ if (bundle.manifest.flow_id !== "interactive" || bundle.manifest.run_id !== scope.runId)
60
+ refuse("interactive_bundle_required");
61
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
62
+ const rows = context.db.all("SELECT j.value AS work_id FROM audit_events f,json_each(f.payload_json,'$.work_ids') j WHERE f.project_id=? AND f.event_type='interactive.work_registration' AND json_extract(f.payload_json,'$.run_id')=? ORDER BY f.id,j.key LIMIT 20001", [scope.projectId, scope.runId]);
63
+ const works = context.db.all("SELECT work_id,status FROM works WHERE project_id=? AND run_id=? AND result_schema='dd-flow/interactive-work-result@1' LIMIT 20001", [scope.projectId, scope.runId]);
64
+ const registered = new Set(rows.map(row => row.work_id));
65
+ if (rows.length > 20_000 || works.length !== rows.length || registered.size !== rows.length
66
+ || works.some(work => !registered.has(work.work_id)))
67
+ refuse("history_registration_missing");
68
+ const byId = new Map(works.map(work => [work.work_id, work])), entries = [];
69
+ let total = 0;
70
+ for (const row of rows) {
71
+ const work = byId.get(row.work_id);
72
+ if (!["completed", "failed", "cancelled"].includes(work.status))
73
+ refuse("prior_work_not_terminal");
74
+ const text = context.db.get("SELECT result FROM works WHERE project_id=? AND run_id=? AND work_id=? AND length(CAST(result AS BLOB))<=1048576", [scope.projectId, scope.runId, row.work_id])?.result;
75
+ if (!text)
76
+ refuse("unbounded_or_missing_history_result");
77
+ const assignment = readRegisteredInteractiveWork(context, scope, bundle, row.work_id);
78
+ const link = context.db.get("SELECT id,status,result_receipt_json FROM work_sessions WHERE work_id=? ORDER BY rowid DESC LIMIT 1", [row.work_id]);
79
+ let actor = null, result = text;
80
+ if (work.status === "completed") {
81
+ const completed = readCompletedInteractiveWork(context, { ...scope, workId: row.work_id }, bundle);
82
+ if (link?.id !== completed.completed.session.id)
83
+ refuse("history_latest_session_changed");
84
+ actor = { harness: completed.completed.session.harness, provider_session_id: completed.completed.session.provider_session_id };
85
+ result = completed.result;
86
+ }
87
+ else if (link) {
88
+ const start = readInteractiveWorkStart(context, { ...scope, workId: row.work_id }, bundle, link.id);
89
+ if (link.status !== work.status || !link.result_receipt_json || Buffer.byteLength(link.result_receipt_json) > 16384)
90
+ refuse("terminal_session_custody_missing");
91
+ const receipt = JSON.parse(link.result_receipt_json);
92
+ if (receipt.work_id !== row.work_id || receipt.work_session_id !== link.id || receipt.status !== work.status || receipt.result_sha256 !== hash(text))
93
+ refuse("terminal_result_changed");
94
+ actor = { harness: start.receipt.actor.harness, provider_session_id: start.receipt.actor.provider_session_id };
95
+ }
96
+ const entry = { work_id: row.work_id, stage: assignment.packet.stage, owner_stage: assignment.packet.owner_stage, attempt: assignment.receipt.attempt,
97
+ request_id: assignment.receipt.request_id, status: work.status, work_session_id: link?.id ?? null,
98
+ review_binding: work.status === "completed" && assignment.packet.stage === "review" ? {
99
+ file_changes_sha256: assignment.packet.file_changes_sha256, user_inputs_sha256: assignment.packet.user_inputs_sha256,
100
+ scope_revision: assignment.packet.scope_revision
101
+ } : null,
102
+ actor, result_sha256: hash(text), result };
103
+ total += Buffer.byteLength(JSON.stringify(entry));
104
+ if (total > 32 * 1024 * 1024)
105
+ refuse("history_budget_exceeded");
106
+ entries.push(entry);
107
+ }
108
+ const history = { schema_id: "dd-flow/interactive-work-history@1", run_id: scope.runId, bundle_sha256: bundle.sha256, works: entries };
109
+ assertInteractiveWorkHistory(history, scope.runId, bundle.sha256);
110
+ return history;
111
+ });
112
+ }
@@ -0,0 +1,124 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { hybridDigest } from "./hybrid-contracts.js";
3
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
4
+ import { readDescriptorRootWork } from "./descriptor-root-work.js";
5
+ import { stageGraphScope } from "./stage-work-graph.js";
6
+ import { PreparedInteractiveWorkContext, readRetainedInteractiveWorkContext } from "./interactive-work-context.js";
7
+ import { interactiveWorkBatch } from "./interactive-work-batch.js";
8
+ import { prepareWorkBatch, addWorkBatch, readWorkBatchCustody } from "./work-registry.js";
9
+ import { PreparedInteractiveChecks } from "./interactive-checks.js";
10
+ const event = "interactive.work_registration";
11
+ function refuse(reason) {
12
+ throw new AppError("interactive_work_registration_invalid", "Interactive Work must belong to its retained Stage context and atomic graph", 2, { reason, effect: "no_effect" });
13
+ }
14
+ /** SQL-only assignment custody, usable after the Stage or workspace advanced.
15
+ * A launch owner must separately fence current Stage/input/physical state. */
16
+ export function readRegisteredInteractiveWork(context, scope, bundle, workId) {
17
+ const custody = readWorkBatchCustody(context, workId), bound = custody.scope;
18
+ if (bundle.manifest.flow_id !== "interactive" || bound.project_id !== scope.projectId || bound.run_id !== scope.runId
19
+ || !["hardening", "review"].includes(bound.stage) || !bound.request_key.startsWith("interactive-"))
20
+ refuse("foreign_work_assignment");
21
+ const stage = bound.stage, requestId = bound.request_key.slice("interactive-".length);
22
+ const root = readDescriptorRootWork(context, scope, bundle);
23
+ 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')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 2", [scope.projectId, event, scope.runId, stage, bound.attempt, requestId]);
24
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
25
+ refuse("registration_custody_missing");
26
+ const receipt = JSON.parse(rows[0].payload_json);
27
+ if (!receipt || Object.keys(receipt).sort().join() !== "attempt,batch_sha256,bundle_sha256,context,parent_work_id,request_id,run_id,schema_id,stage,work_ids"
28
+ || receipt.schema_id !== "dd-flow/interactive-work-registration@1" || receipt.run_id !== scope.runId || receipt.stage !== stage
29
+ || receipt.attempt !== bound.attempt || receipt.request_id !== requestId || receipt.parent_work_id !== root.workId
30
+ || bound.parent_work_id !== root.workId || receipt.bundle_sha256 !== bundle.sha256
31
+ || hybridDigest(receipt.work_ids) !== hybridDigest(custody.work_ids))
32
+ refuse("registration_scope_changed");
33
+ const retained = readRetainedInteractiveWorkContext(context, { ...scope, stage, attempt: bound.attempt }, bundle, receipt.context);
34
+ const batch = interactiveWorkBatch(retained.record.input);
35
+ if (retained.record.input.owner_stage !== stage || custody.item_key !== retained.record.input.stage
36
+ || receipt.work_ids[custody.item_key] !== workId || Object.keys(receipt.work_ids).join() !== custody.item_key
37
+ || receipt.batch_sha256 !== hybridDigest(batch.works) || receipt.batch_sha256 !== custody.batch_sha256)
38
+ refuse("registration_batch_changed");
39
+ return { receipt, retained, packet: batch.works[0].payload.interactive, custody };
40
+ }
41
+ /** Registration replay returns original allocation metadata, not fresh launch authority. */
42
+ export function registerInteractiveStageGraph(context, input, assertHandlerCustody) {
43
+ const requestId = input.requestId ?? "initial";
44
+ const role = input.role ?? input.stage;
45
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(requestId))
46
+ refuse("invalid_request_id");
47
+ const selected = readInteractiveStageScope(context, input), { scope, bundle } = selected;
48
+ const graph = stageGraphScope(context, scope.projectId, scope.runId, input.stage);
49
+ const root = readDescriptorRootWork(context, scope, bundle);
50
+ const requestKey = `interactive-${requestId}`;
51
+ const requestScope = { ...graph, stage: input.stage, parent_work_id: root.workId, request_key: requestKey };
52
+ const requestSha = hybridDigest(requestScope);
53
+ const read = () => {
54
+ 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')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 2", [scope.projectId, event, scope.runId, input.stage, graph.attempt, requestId]);
55
+ if (!rows.length) {
56
+ if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='work.batch_registration' AND json_extract(payload_json,'$.request_sha256')=?", [scope.projectId, requestSha])
57
+ || context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND json_extract(payload_json,'$._dd_flow_batch_request.request_sha256')=?", [scope.projectId, scope.runId, requestSha]))
58
+ refuse("registration_receipt_missing");
59
+ return null;
60
+ }
61
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
62
+ refuse("ambiguous_registration");
63
+ const receipt = JSON.parse(rows[0].payload_json);
64
+ if (!receipt || Object.keys(receipt).sort().join() !== "attempt,batch_sha256,bundle_sha256,context,parent_work_id,request_id,run_id,schema_id,stage,work_ids"
65
+ || receipt.schema_id !== "dd-flow/interactive-work-registration@1" || receipt.run_id !== scope.runId
66
+ || receipt.stage !== input.stage || receipt.attempt !== graph.attempt || receipt.request_id !== requestId
67
+ || receipt.parent_work_id !== root.workId || receipt.bundle_sha256 !== bundle.sha256)
68
+ refuse("registration_scope_changed");
69
+ const retained = readRetainedInteractiveWorkContext(context, { ...scope, stage: input.stage, attempt: graph.attempt }, bundle, receipt.context);
70
+ if (retained.record.input.stage !== role)
71
+ refuse("registration_role_changed");
72
+ const batch = interactiveWorkBatch(retained.record.input);
73
+ if (receipt.batch_sha256 !== hybridDigest(batch.works)
74
+ || !receipt.work_ids || Object.keys(receipt.work_ids).sort().join() !== batch.works.map(item => item.key).sort().join()
75
+ || new Set(Object.values(receipt.work_ids)).size !== batch.works.length)
76
+ refuse("registration_batch_changed");
77
+ for (const [key, id] of Object.entries(receipt.work_ids)) {
78
+ const custody = readWorkBatchCustody(context, id);
79
+ if (custody.item_key !== key || custody.batch_sha256 !== receipt.batch_sha256 || hybridDigest(custody.scope) !== requestSha
80
+ || hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids))
81
+ refuse("registration_work_changed");
82
+ }
83
+ return receipt;
84
+ };
85
+ const prior = context.db.readSnapshot(read);
86
+ if (prior)
87
+ return { ...prior, replayed: true };
88
+ const noOverlap = () => {
89
+ if (context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id=? AND status IN ('created','running','paused') AND json_extract(payload_json,'$.kind') IN ('interactive-hardening','interactive-review')", [scope.projectId, scope.runId, root.workId]))
90
+ refuse("active_previous_round");
91
+ };
92
+ noOverlap();
93
+ const checks = input.checks ?? (role === "review" ? PreparedInteractiveChecks.prepareReview(context, input) ?? undefined : undefined);
94
+ if (checks && !input.checks) {
95
+ checks.retain(context);
96
+ checks.evidence(context);
97
+ }
98
+ const frame = PreparedInteractiveWorkContext.prepare(context, { ...input, ...(checks ? { checks } : {}) }), metadata = frame.snapshot();
99
+ if (metadata.attempt !== graph.attempt)
100
+ refuse("stage_attempt_changed");
101
+ const batchInput = { parentWorkId: root.workId, stage: input.stage, requestKey, value: interactiveWorkBatch(frame.workInput()) };
102
+ const prepared = prepareWorkBatch(context, batchInput);
103
+ frame.revalidate();
104
+ return context.db.writeTransaction(() => {
105
+ const racing = read();
106
+ if (racing)
107
+ return { ...racing, replayed: true };
108
+ assertHandlerCustody?.(context);
109
+ frame.assertCustody(context);
110
+ noOverlap();
111
+ if (readDescriptorRootWork(context, scope, bundle).workId !== root.workId)
112
+ refuse("root_changed");
113
+ frame.retainPreparedSql(context);
114
+ const allocated = addWorkBatch(context, { ...batchInput, prepared });
115
+ const receipt = { schema_id: "dd-flow/interactive-work-registration@1", run_id: scope.runId, stage: input.stage,
116
+ attempt: graph.attempt, request_id: requestId, parent_work_id: root.workId, bundle_sha256: bundle.sha256,
117
+ context: metadata.reference, batch_sha256: hybridDigest(prepared), work_ids: allocated.work_ids };
118
+ context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [scope.projectId, event, JSON.stringify(receipt), context.now()]);
119
+ const verified = read();
120
+ if (!verified)
121
+ refuse("registration_not_retained");
122
+ return { ...verified, replayed: false };
123
+ });
124
+ }
@@ -0,0 +1,115 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { interactiveWorkResultSchema } from "./interactive-work-batch.js";
5
+ const issueKey = (workId, issueId) => JSON.stringify([workId, issueId]);
6
+ /** Only independently settled review decisions close earlier items. Author
7
+ * decisions are proposals. Null issue_id tracks an unfinished Work outcome,
8
+ * not an invented product finding. Failed/cancelled attempts remain explicit. */
9
+ export function pendingInteractiveIssues(history, binding) {
10
+ const pending = new Map();
11
+ for (const work of history.works) {
12
+ if (work.status !== "completed" || typeof work.result === "string") {
13
+ pending.set(issueKey(work.work_id, null), { work_id: work.work_id, issue_id: null, severity: "major", summary: String(work.result) });
14
+ continue;
15
+ }
16
+ const result = work.result;
17
+ if (work.stage === "review" && work.review_binding?.file_changes_sha256 === binding.file_changes_sha256
18
+ && work.review_binding.user_inputs_sha256 === binding.user_inputs_sha256 && work.review_binding.scope_revision === binding.scope_revision)
19
+ for (const decision of result.issue_decisions ?? [])
20
+ if (decision.outcome !== "still_open")
21
+ pending.delete(issueKey(decision.work_id, decision.issue_id));
22
+ for (const issue of result.issues)
23
+ pending.set(issueKey(work.work_id, issue.id), {
24
+ work_id: work.work_id, issue_id: issue.id, severity: issue.severity, summary: issue.summary
25
+ });
26
+ if (result.verdict !== "ready" && !result.issues.length && !(result.issue_decisions ?? []).some(decision => decision.outcome === "still_open"))
27
+ pending.set(issueKey(work.work_id, null), { work_id: work.work_id, issue_id: null, severity: "major", summary: result.summary });
28
+ }
29
+ return [...pending.values()];
30
+ }
31
+ function refuse(reason) {
32
+ throw new AppError("interactive_work_result_invalid", "Interactive Work requires its exact assignment, observed changes and checks", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
33
+ }
34
+ /** Proposal checks only. Physical source/check/Session custody must be prepared
35
+ * independently and fenced again in Work completion and Stage settlement. */
36
+ export function validateInteractiveWorkResult(bytes, schemaBytes, binding) {
37
+ if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
38
+ refuse("result_exceeds_bound");
39
+ let result;
40
+ try {
41
+ result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
42
+ }
43
+ catch {
44
+ return refuse("invalid_schema");
45
+ }
46
+ const validate = (() => {
47
+ try {
48
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
49
+ if (schema.$id !== interactiveWorkResultSchema)
50
+ refuse("frozen_schema_invalid");
51
+ return new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
52
+ }
53
+ catch {
54
+ return refuse("frozen_schema_invalid");
55
+ }
56
+ })();
57
+ if (!validate(result) || result.schema_id !== interactiveWorkResultSchema || !result.summary.trim()
58
+ || result.limitations.some(item => !item.trim()))
59
+ refuse("invalid_schema");
60
+ const packet = binding.packet;
61
+ if (result.run_id !== packet.run_id || result.work_id !== binding.workId || result.stage !== packet.stage
62
+ || result.packet_sha256 !== packet.packet_sha256)
63
+ refuse("foreign_work_binding");
64
+ if (hybridDigest(result.changes) !== hybridDigest(binding.actualChanges))
65
+ refuse("unobserved_change_claim");
66
+ const paths = result.changes.map(item => item.path);
67
+ if (new Set(paths).size !== paths.length || result.changes.some(item => !packet.write_paths.includes(item.path)
68
+ || item.before_sha256 === item.after_sha256 && item.before_mode === item.after_mode
69
+ || Object.hasOwn(item, "before_mode") && ((item.before_sha256 === null) !== (item.before_mode === null)
70
+ || (item.after_sha256 === null) !== (item.after_mode === null))))
71
+ refuse("change_outside_write_scope");
72
+ if (packet.stage === "review" && (result.changes.length || packet.write_paths.length))
73
+ refuse("review_changed_sources");
74
+ if (hybridDigest(result.checks) !== hybridDigest(binding.checks))
75
+ refuse("unowned_check_claim");
76
+ const sources = new Map(packet.read_inputs.map(ref => [`${ref.root}:${ref.path}`, ref.sha256]));
77
+ for (const change of result.changes)
78
+ sources.delete(`workspace:${change.path}`);
79
+ for (const ref of [...binding.observedSources, ...binding.checks]) {
80
+ const key = `${ref.root}:${ref.path}`;
81
+ // Changed workspace inputs are superseded only by independently observed
82
+ // post-Work sources. Agent references never extend this allowed evidence.
83
+ const change = ref.root === "workspace" ? result.changes.find(item => item.path === ref.path) : null;
84
+ if (change && change.after_sha256 !== ref.sha256)
85
+ refuse("stale_observed_output");
86
+ if (ref.root === "run" && sources.has(key) && sources.get(key) !== ref.sha256)
87
+ refuse("conflicting_run_evidence");
88
+ sources.set(key, ref.sha256);
89
+ }
90
+ const ids = new Set();
91
+ for (const issue of result.issues) {
92
+ if (!issue.id.trim() || !issue.summary.trim() || ids.has(issue.id))
93
+ refuse("invalid_issue");
94
+ ids.add(issue.id);
95
+ if (issue.evidence.some(ref => sources.get(`${ref.root}:${ref.path}`) !== ref.sha256))
96
+ refuse("foreign_issue_evidence");
97
+ }
98
+ const pending = new Map((binding.priorIssues ?? []).map(issue => [issueKey(issue.work_id, issue.issue_id), issue]));
99
+ const decisions = result.issue_decisions ?? [], decided = new Set();
100
+ if (decisions.length !== pending.size)
101
+ refuse("incomplete_prior_issue_decisions");
102
+ for (const decision of decisions) {
103
+ const key = issueKey(decision.work_id, decision.issue_id), prior = pending.get(key);
104
+ if (!prior || decided.has(key) || !decision.summary.trim())
105
+ refuse("foreign_or_duplicate_issue_decision");
106
+ if (decision.evidence.some(ref => sources.get(`${ref.root}:${ref.path}`) !== ref.sha256))
107
+ refuse("foreign_issue_decision_evidence");
108
+ if (result.verdict === "ready" && decision.outcome === "still_open" && prior.severity !== "minor")
109
+ refuse("unsupported_ready_claim");
110
+ decided.add(key);
111
+ }
112
+ if (result.verdict === "ready" && (!binding.checksSatisfied || result.issues.some(issue => issue.severity !== "minor")))
113
+ refuse("unsupported_ready_claim");
114
+ return result;
115
+ }