@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,155 @@
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 { readRegisteredInteractiveWork } from "./interactive-work-registration.js";
6
+ import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
7
+ import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
8
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
+ import { stageGraphScope } from "./stage-work-graph.js";
10
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
11
+ import { readBoundedReleaseFile } from "./release-source.js";
12
+ const event = "interactive.work_start";
13
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
14
+ function refuse(reason) {
15
+ throw new AppError("interactive_work_start_invalid", "Interactive Work requires its issued input and actual physical start", 2, { reason, effect: "no_effect" });
16
+ }
17
+ export function readInteractiveWorkGit(bundle) {
18
+ const git = JSON.parse(bundle.blob("runtime/git-policy.json").toString("utf8"));
19
+ if (!git || typeof git.git_executable !== "string" || !/^[a-f0-9]{64}$/.test(git.git_sha256)
20
+ || hash(readBoundedReleaseFile(git.git_executable, 64 * 1024 * 1024)) !== git.git_sha256)
21
+ refuse("selected_git_changed");
22
+ return { git_executable: git.git_executable, git_sha256: git.git_sha256 };
23
+ }
24
+ /** Historical SQL-only start custody. Never recapture already edited sources. */
25
+ export function readInteractiveWorkStart(context, scope, bundle, workSessionId) {
26
+ const assignment = readRegisteredInteractiveWork(context, scope, bundle, scope.workId);
27
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 2", [scope.projectId, event, workSessionId]);
28
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 33 * 1024 * 1024)
29
+ refuse("start_custody_missing");
30
+ const receipt = JSON.parse(rows[0].payload_json);
31
+ if (!receipt || Object.keys(receipt).sort().join() !== "actor,attempt,bundle_sha256,hook_event_id,hook_event_key,packet_sha256,run_id,schema_id,snapshot,snapshot_sha256,stage,work_id,work_session_id"
32
+ || receipt.schema_id !== "dd-flow/interactive-work-start@1" || receipt.run_id !== scope.runId || receipt.work_id !== scope.workId
33
+ || receipt.work_session_id !== workSessionId || receipt.stage !== assignment.packet.stage || receipt.attempt !== assignment.receipt.attempt
34
+ || receipt.packet_sha256 !== assignment.packet.packet_sha256 || receipt.bundle_sha256 !== bundle.sha256
35
+ || !receipt.actor || Object.keys(receipt.actor).sort().join() !== "harness,provider_session_id,session_id")
36
+ refuse("start_binding_changed");
37
+ const actor = receipt.actor;
38
+ if (!context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id 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 w.work_id=? AND ws.id=? AND ws.session_id=? AND ws.hook_event_id=? AND h.id=? AND h.status='claimed' AND h.session_id=ws.session_id AND h.harness=? AND h.provider_session_id=?", [scope.projectId, scope.runId, scope.workId, workSessionId, actor.session_id, receipt.hook_event_key, receipt.hook_event_id, actor.harness, actor.provider_session_id]))
39
+ refuse("physical_start_custody_changed");
40
+ const baseline = PreparedInteractiveGitBaseline.restore(Buffer.from(JSON.stringify(receipt.snapshot)), receipt.snapshot_sha256);
41
+ const start = context.db.get("SELECT start_receipt_json FROM work_sessions WHERE id=? AND work_id=?", [workSessionId, scope.workId]);
42
+ if (!start?.start_receipt_json || Buffer.byteLength(start.start_receipt_json) > 33 * 1024 * 1024)
43
+ refuse("start_packet_custody_missing");
44
+ const packetReceipt = JSON.parse(start.start_receipt_json);
45
+ if (typeof packetReceipt.context !== "string" || hash(Buffer.from(packetReceipt.context)) !== packetReceipt.context_sha256)
46
+ refuse("start_packet_changed");
47
+ const startContext = JSON.parse(packetReceipt.context);
48
+ if (startContext.run_id !== scope.runId || startContext.work_id !== scope.workId
49
+ || hybridDigest(startContext.interactive_start) !== hybridDigest({ schema_id: "dd-flow/interactive-work-start-binding@1",
50
+ packet_sha256: assignment.packet.packet_sha256, snapshot_sha256: baseline.sha256 }))
51
+ refuse("start_snapshot_binding_changed");
52
+ const frozen = bundle.execution.snapshot().scope;
53
+ if (baseline.snapshot().project_root !== frozen.projectRoot || baseline.snapshot().workspace !== frozen.workspace)
54
+ refuse("physical_start_scope_changed");
55
+ return { receipt, baseline, assignment };
56
+ }
57
+ /** Physical preparation precedes the ordinary hook/Session claim writer.
58
+ * The original Work snapshot is retained with that claim, never after edits. */
59
+ export class PreparedInteractiveWorkStart {
60
+ #scope;
61
+ #bundle;
62
+ #assignment;
63
+ #changes;
64
+ #baseline;
65
+ #git;
66
+ constructor(scope, bundle, assignment, changes, git) {
67
+ this.#scope = { ...scope };
68
+ this.#bundle = bundle;
69
+ this.#assignment = assignment;
70
+ this.#changes = changes;
71
+ const bytes = Buffer.from(JSON.stringify(changes.currentSnapshot()));
72
+ this.#baseline = PreparedInteractiveGitBaseline.restore(bytes, hash(bytes));
73
+ this.#git = { ...git };
74
+ Object.freeze(this);
75
+ }
76
+ static prepare(context, input) {
77
+ if (context.db.isOwnedWriteTransaction?.())
78
+ refuse("preparation_outside_writer_required");
79
+ const bundle = readFlowRunExecutionBundle(context, input);
80
+ if (!bundle)
81
+ refuse("bundle_required");
82
+ const assignment = readRegisteredInteractiveWork(context, input, bundle, input.workId), frozen = bundle.execution.snapshot().scope;
83
+ const git = readInteractiveWorkGit(bundle);
84
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, input, bundle, { projectRoot: frozen.projectRoot,
85
+ workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
86
+ const prepared = new PreparedInteractiveWorkStart(input, bundle, assignment, changes, git);
87
+ const patch = changes.snapshot(), packet = assignment.packet;
88
+ if (hybridDigest(patch.delta.changes) !== packet.file_changes_sha256 || patch.scope_revision !== packet.scope_revision
89
+ || patch.user_inputs_sha256 !== packet.user_inputs_sha256)
90
+ refuse("issued_context_changed_before_start");
91
+ // Unchanged files are also selected reviewer/writer inputs, not just patch paths.
92
+ const files = new Map(changes.currentSnapshot().entries.filter(item => item.kind === "file").map(item => [item.path, item.sha256]));
93
+ if (packet.read_inputs.some(ref => ref.root === "workspace" && files.get(ref.path) !== ref.sha256))
94
+ refuse("issued_source_changed_before_start");
95
+ context.db.readSnapshot(() => prepared.assertSelection(context));
96
+ prepared.revalidate();
97
+ return prepared;
98
+ }
99
+ revalidate() {
100
+ if (hash(readBoundedReleaseFile(this.#git.git_executable, 64 * 1024 * 1024)) !== this.#git.git_sha256)
101
+ refuse("selected_git_changed");
102
+ this.#changes.revalidate();
103
+ }
104
+ binding() {
105
+ return { schema_id: "dd-flow/interactive-work-start-binding@1", packet_sha256: this.#assignment.packet.packet_sha256, snapshot_sha256: this.#baseline.sha256 };
106
+ }
107
+ assertSelection(context) {
108
+ const { projectId, runId, workId } = this.#scope, packet = this.#assignment.packet;
109
+ const current = readRegisteredInteractiveWork(context, this.#scope, this.#bundle, workId);
110
+ if (hybridDigest(current.receipt) !== hybridDigest(this.#assignment.receipt)
111
+ || hybridDigest(current.custody) !== hybridDigest(this.#assignment.custody)
112
+ || current.packet.packet_sha256 !== this.#assignment.packet.packet_sha256)
113
+ refuse("assignment_changed_before_start");
114
+ const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle), index = JSON.parse(run.index_json);
115
+ const graph = stageGraphScope(context, projectId, runId, packet.owner_stage), stage = readDescriptorStageInput(context, { ...this.#scope, stage: packet.owner_stage }, this.#bundle);
116
+ if (run.status !== "running" || index.current_stage !== packet.owner_stage
117
+ || !index.stage_runs?.some((item) => item.stage === packet.owner_stage && item.status === "running" && item.attempt === graph.attempt)
118
+ || graph.attempt !== current.custody.scope.attempt || graph.cycle !== current.custody.scope.cycle
119
+ || stage.attempt !== graph.attempt || stage.inputSha256 !== packet.input_sha256)
120
+ refuse("active_assignment_required");
121
+ assertRunMutationAllowed(context, projectId, runId);
122
+ }
123
+ assertCustody(context) {
124
+ if (!context.db.isOwnedWriteTransaction?.())
125
+ refuse("owned_writer_required");
126
+ this.assertSelection(context);
127
+ this.#changes.assertCustody(context);
128
+ }
129
+ recordStart(context, workSessionId, hookEventKey, identity) {
130
+ this.assertCustody(context);
131
+ const { projectId, runId, workId } = this.#scope;
132
+ const link = context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id 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 w.work_id=? AND w.status='running' AND ws.id=? AND ws.status='running' AND ws.session_id=? AND ws.hook_event_id=? AND h.status='claimed' AND h.id=? AND h.session_id=ws.session_id AND s.harness=? AND s.provider_session_id=? AND h.harness=s.harness AND h.provider_session_id=s.provider_session_id", [projectId, runId, workId, workSessionId, identity.sessionId, hookEventKey, identity.hookEventId, identity.harness, identity.providerSessionId]);
133
+ if (!link)
134
+ refuse("claimed_work_session_required");
135
+ if (this.#assignment.retained.body.work_history.works.some(prior => prior.actor?.harness === identity.harness
136
+ && prior.actor.provider_session_id === identity.providerSessionId))
137
+ refuse("independent_physical_session_required");
138
+ if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.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 ws.id<>? AND s.harness=? AND s.provider_session_id=? LIMIT 1", [projectId, runId, workSessionId, identity.harness, identity.providerSessionId]))
139
+ refuse("independent_physical_session_required");
140
+ if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.work_session_id')=?", [projectId, event, workSessionId]))
141
+ refuse("start_already_retained");
142
+ const packet = this.#assignment.packet;
143
+ const receipt = { schema_id: "dd-flow/interactive-work-start@1", run_id: runId, work_id: workId, work_session_id: workSessionId,
144
+ hook_event_key: hookEventKey, hook_event_id: identity.hookEventId, stage: packet.stage, attempt: this.#assignment.receipt.attempt,
145
+ packet_sha256: packet.packet_sha256, bundle_sha256: this.#bundle.sha256,
146
+ actor: { session_id: identity.sessionId, harness: identity.harness, provider_session_id: identity.providerSessionId },
147
+ snapshot_sha256: this.#baseline.sha256, snapshot: this.#baseline.snapshot() };
148
+ // Same bounded snapshot as the RUN baseline; SQL custody is not worker transport.
149
+ const json = JSON.stringify(receipt);
150
+ if (Buffer.byteLength(json) > 33 * 1024 * 1024)
151
+ refuse("unbounded_start_receipt");
152
+ context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [projectId, event, json, context.now()]);
153
+ readInteractiveWorkStart(context, this.#scope, this.#bundle, workSessionId);
154
+ }
155
+ }
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { checkRepairDisposition } from "./check-runner-evidence.js";
2
+ import { checkDiagnosisDisposition } from "./check-runner-evidence.js";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { quote } from "shell-quote";
@@ -21,6 +21,12 @@ import { stageAttempt } from "./stage-work-graph.js";
21
21
  import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
22
22
  import { canonicalPath } from "../storage/paths.js";
23
23
  import { runtimeCommand, shellArgument } from "./runtime-command.js";
24
+ import { assignedLifecycleBinding, claimAssignedLifecycle, currentAssignedLifecycle, bindAssignedCoordinatorPacket, inheritAssignedPackets, retainControllerStageAssignment, admitAssignedAuxiliary, bindAssignedStageResume, readAcceptedAssignedWork, assertTerminalSettlementUnreserved } from "./assigned-lifecycle.js";
25
+ import { appendAudit } from "./audit.js";
26
+ import { recordTechnicalNote } from "./technical-notes.js";
27
+ import { ObservationClock } from "../harness-runtime/lib/observation-clock.mjs";
28
+ export { currentAssignedLifecycle, assignedSessionIdentity, assertAssignedSettlementCurrent } from "./assigned-lifecycle.js";
29
+ export const bindControllerStageAssignment = retainControllerStageAssignment;
24
30
  // Process-local custody: an executing SQL row or ambient variable alone cannot
25
31
  // recreate admission after a CLI restart, or in another resource database.
26
32
  const currentAdmissions = new WeakMap();
@@ -48,6 +54,11 @@ export function assertAuxiliaryCommandScope(context, command, projectRoot) {
48
54
  const daemonId = context.env.DD_FLOW_DAEMON_ID;
49
55
  if (!auxiliary || !daemonId)
50
56
  return;
57
+ const assignedScope = cliManagedInvocationScope(context);
58
+ if (assignedScope && admitAssignedAuxiliary(context, command, assignedScope, projectRoot)) {
59
+ assertRunMutationAllowed(context, requireProjectByRoot(context, assignedScope.projectRoot).id, assignedScope.runId, assignedScope.generation);
60
+ return;
61
+ }
51
62
  const event = observedLifecycleNativeEvent(context, command, daemonId);
52
63
  if (!event)
53
64
  throw new AppError("invocation_receipt_missing", "Auxiliary command has no exact native receipt", 1, { phase: "prepare", effect: "no_effect" });
@@ -96,8 +107,8 @@ export function isRetryableLifecycleRejection(code, details = {}) {
96
107
  if (["invocation_command_mismatch", "invocation_argument_mismatch"].includes(code))
97
108
  return details.recoverable !== false;
98
109
  if (["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(code))
99
- return Array.isArray(details.failures) && details.failures.length > 0 && details.failures.every(receipt => checkRepairDisposition(receipt) === "source_repair");
100
- return details.recoverable === true || ["review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
110
+ return Array.isArray(details.failures) && details.failures.length > 0 && details.failures.every(receipt => checkDiagnosisDisposition(receipt) === "diagnosis_required");
111
+ return details.recoverable === true || ["work_contract_incomplete", "review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
101
112
  "review_evidence_invalid", "evidence_obligation_unknown", "invalid_evidence_ref",
102
113
  "evidence_ref_missing", "evidence_line_out_of_range", "document_update_missing",
103
114
  "document_update_not_materialized", "review_repair_no_change"].includes(code)
@@ -289,6 +300,7 @@ export function bindLifecyclePublishedPacket(context, prompt) {
289
300
  const issued = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status = 'issued' ORDER BY rowid", [scope]);
290
301
  const ids = issued.filter(row => prompt.includes(publicInvocationCommand(context, row, retainedCommandWithoutInvocationId(row)))).map(row => row.id);
291
302
  bindLifecyclePacketAssignments(context, prompt, ids);
303
+ bindAssignedCoordinatorPacket(context, prompt);
292
304
  }
293
305
  function retainedCommandWithoutInvocationId(row) {
294
306
  const marker = ` --invocation-id ${row.id}`;
@@ -391,6 +403,24 @@ export function assertLifecycleOutcomes(context, scope) {
391
403
  /** Hooks already own native identity. Retain command outcomes there, with an
392
404
  * atomic controller notification; missing notifications reconcile from the row. */
393
405
  export function observeLifecycleCommand(context, command, nativeEvent) {
406
+ const assigned = currentAssignedLifecycle(context);
407
+ if (assigned)
408
+ return outcome => {
409
+ try {
410
+ const project = requireProjectByRoot(context, assigned.scope.projectRoot);
411
+ try {
412
+ appendAudit(context, { projectId: project.id, eventType: "lifecycle.assigned_outcome", payload: { run_id: assigned.scope.runId,
413
+ generation: assigned.scope.generation, invocation_id: assigned.invocation_id, operation: assigned.operation,
414
+ work_id: assigned.work_id, ...(outcome.error ? { error: { code: outcome.error.code, message: outcome.error.message } } : { accepted: true }) } });
415
+ }
416
+ catch {
417
+ recordTechnicalNote(context, project.id, assigned.scope.runId, { code: "assigned_outcome_audit_unavailable", message: "Optional assigned-operation audit could not be written; invocation outcome remains authoritative", phase: "settlement", generation: assigned.scope.generation, source_operation_id: assigned.invocation_id, consequence: "none" });
418
+ }
419
+ }
420
+ catch {
421
+ process.stderr.write(`${JSON.stringify({ event: "assigned_outcome_audit_unavailable", invocation_id: assigned.invocation_id })}\n`);
422
+ }
423
+ };
394
424
  const parsed = parseLifecycleCommand(command);
395
425
  const auxiliary = continuationCommand(command);
396
426
  if (parsed.kind !== "standalone" && !auxiliary)
@@ -677,9 +707,41 @@ export function lifecycleRetryCommands(context, stage) {
677
707
  }
678
708
  }))];
679
709
  }
680
- function currentIssuedCommand(context, scope, command) {
710
+ /** A resumed coordinator retains intent, not the old route's mutation authority. */
711
+ export function rebindAssignedCoordinatorCommands(context) {
712
+ const assigned = currentAssignedLifecycle(context);
713
+ if (assigned?.operation !== "stage_resume" || !assigned.work_session_id)
714
+ return [];
715
+ const rows = context.db.all(`SELECT DISTINCT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id
716
+ WHERE li.status='issued' AND (pa.packet_sha256=(SELECT json_extract(start_receipt_json,'$.prompt_sha256') FROM work_sessions WHERE id=?)
717
+ OR EXISTS (SELECT 1 FROM audit_events a WHERE a.event_type='lifecycle.coordinator_packet'
718
+ AND json_extract(a.payload_json,'$.work_session_id')=? AND json_extract(a.payload_json,'$.packet_sha256')=pa.packet_sha256))`, [assigned.work_session_id, assigned.work_session_id]);
719
+ const commands = [];
720
+ context.db.writeTransaction(() => {
721
+ for (const row of rows) {
722
+ const parsed = invocation(row.command), scope = JSON.parse(row.scope_json);
723
+ if (!["stage_finish", "stage_pause"].includes(parsed.operation) || commandOption(parsed, "stage") !== assigned.stage
724
+ || scope.projectRoot !== assigned.scope.projectRoot || scope.runId !== assigned.scope.runId || scope.generation > assigned.scope.generation)
725
+ continue;
726
+ if (row.scope_json === JSON.stringify(assigned.scope)) {
727
+ commands.push(publicInvocationCommand(context, row, row.command));
728
+ continue;
729
+ }
730
+ const command = canonicalManagedCommand(context, retainedCommandWithoutInvocationId(row), assigned.scope);
731
+ const next = managedLifecycleCommand(context, command);
732
+ const successor = currentIssuedCommand(context, JSON.stringify(assigned.scope), next);
733
+ if (!successor)
734
+ throw new AppError("invocation_assignment_unproven", "Resume has no exact current coordinator successor", 1, { effect: "no_effect", reconciliation_required: true });
735
+ inheritAssignedPackets(context, row.id, successor.id);
736
+ context.db.run("UPDATE lifecycle_invocations SET status='expired',updated_at=? WHERE id=? AND status='issued'", [context.now(), row.id]);
737
+ commands.push(next);
738
+ }
739
+ });
740
+ return [...new Set(commands)];
741
+ }
742
+ function currentIssuedCommand(context, scope, command, excludeId) {
681
743
  const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND status = 'issued'", [scope, scopedCommandFingerprint(context, scope, command)])
682
- .filter(row => row.deadline === null || row.deadline > Date.now());
744
+ .filter(row => row.id !== excludeId && (row.deadline === null || row.deadline > Date.now()));
683
745
  if (rows.length > 1)
684
746
  throw new AppError("invocation_ambiguous", "Multiple current lifecycle successors match this command", 1);
685
747
  return rows[0] ?? null;
@@ -703,18 +765,23 @@ function invocationRecoverySnapshot(context, scope) {
703
765
  }
704
766
  /** Resolve authority, never reconstruct it from an old prompt or issue on read. */
705
767
  export function currentStageFinishCommand(context, stage) {
768
+ return currentStageFinishAuthority(context, stage)?.command ?? null;
769
+ }
770
+ /** Observation of an existing attempt is not permission to dispatch it again. */
771
+ export function currentStageFinishAuthority(context, stage) {
706
772
  const scope = context.env.DD_FLOW_INVOCATION_SCOPE;
707
773
  if (!scope || !context.db.get("SELECT 1 FROM sqlite_master WHERE name='lifecycle_invocations'"))
708
774
  return null;
709
- const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status = 'issued'", [scope])
775
+ const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status IN ('issued','observed','executing')", [scope])
710
776
  .filter(row => {
711
777
  const parsed = invocation(row.command);
712
- return parsed.operation === "stage_finish" && commandOption(parsed, "stage") === stage
713
- && (row.deadline === null || row.deadline > Date.now());
778
+ return parsed.operation === "stage_finish" && commandOption(parsed, "stage") === stage;
714
779
  });
715
780
  if (rows.length > 1)
716
781
  throw new AppError("invocation_ambiguous", "Stage has multiple current finish authorities", 1, { stage });
717
- return rows[0] ? publicInvocationCommand(context, rows[0], rows[0].command) : null;
782
+ const row = rows[0];
783
+ return row ? { id: row.id, status: row.status, command: row.status === "issued" && (row.deadline === null || row.deadline > Date.now())
784
+ ? publicInvocationCommand(context, row, row.command) : null } : null;
718
785
  }
719
786
  export function managedInvocationContext(context, input) {
720
787
  const env = { ...context.env, DD_FLOW_CURRENT_INVOCATION: undefined, DD_FLOW_INVOCATION_SCOPE: undefined };
@@ -805,6 +872,7 @@ export function managedLifecycleCommand(context, command, options = {}) {
805
872
  if (context.env.DD_FLOW_INVOCATION_READONLY === "1")
806
873
  throw new AppError("invocation_command_unprepared", "No prepared lifecycle command is available; read-only inspection cannot issue one", 1);
807
874
  const issued = issueLifecycleInvocation(context, command, scope);
875
+ bindAssignedStageResume(context, load(context, issued.id));
808
876
  return publicInvocationCommand(context, load(context, issued.id), command);
809
877
  });
810
878
  }
@@ -841,9 +909,13 @@ function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
841
909
  throw new AppError("invocation_command_invalid", "Retained retry option cannot be rewritten unambiguously", 1);
842
910
  retained = retained.replace(fragments[0], "");
843
911
  }
844
- command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after candidate-source correction")}`);
912
+ command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after diagnosis and correction of the failed check")}`);
845
913
  }
846
- return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
914
+ const successor = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
915
+ const next = stagedContinuations.get(context.db)?.get(prior.id) ?? currentIssuedCommand(context, prior.scope_json, successor, prior.id);
916
+ if (next && context.db.get("SELECT 1 FROM lifecycle_invocations WHERE id=?", [next.id]))
917
+ inheritAssignedPackets(context, prior.id, next.id);
918
+ return successor;
847
919
  }
848
920
  // Caller holds the write transaction and has checked authority. Issued (or
849
921
  // legacy expired) attempts have never admitted an effect; observed/executing
@@ -862,6 +934,16 @@ const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "
862
934
  * A crash must leave either the old attempt executing, or both the settled
863
935
  * outcome and its successor; never a usable successor beside an unknown effect. */
864
936
  export function settleLifecycleRejection(context, id, error) {
937
+ const accepted = readAcceptedAssignedWork(context, id);
938
+ if (accepted) {
939
+ const row = load(context, id), scope = JSON.parse(row.scope_json);
940
+ settleLifecycleInvocation(context, id, { result: accepted });
941
+ Object.assign(error.details, { effect: "committed", recoverable: false, work_result_accepted: true, reconciliation_required: true });
942
+ const project = requireProjectByRoot(context, scope.projectRoot);
943
+ recordTechnicalNote(context, project.id, scope.runId, { code: "work_post_commit_pending", message: "Work result was accepted; derivative publication or projection needs reconciliation", phase: "publication", generation: scope.generation,
944
+ source_operation_id: id, work_id: String(accepted.work_id), consequence: "dependency_blocked" });
945
+ return;
946
+ }
865
947
  storage(context);
866
948
  // Only this settlement may publish a successor; inherited help is not authority.
867
949
  delete error.details.retry_command;
@@ -935,24 +1017,33 @@ export function hasLifecyclePacketAssignments(context, originalPrompt) {
935
1017
  }
936
1018
  export function recoverLifecycleCommands(context, originalPrompt, scope) {
937
1019
  const digest = crypto.createHash("sha256").update(originalPrompt).digest("hex");
938
- const ids = context.db.all("SELECT invocation_id FROM lifecycle_packet_assignments WHERE packet_sha256 = ? ORDER BY rowid", [digest]).map(row => row.invocation_id);
939
- if (!ids.length)
1020
+ const packet = context.db.all("SELECT li.* FROM lifecycle_packet_assignments pa JOIN lifecycle_invocations li ON li.id=pa.invocation_id WHERE pa.packet_sha256 = ? ORDER BY pa.rowid", [digest]);
1021
+ if (!packet.length)
940
1022
  throw new AppError("invocation_recovery_unbound", "Original packet has no verified lifecycle assignment binding; reconcile it before continuing", 1);
1023
+ const origins = new Map();
1024
+ for (const row of packet) {
1025
+ const priorScope = JSON.parse(row.scope_json);
1026
+ if (priorScope.projectRoot !== scope.projectRoot || priorScope.runId !== scope.runId || priorScope.rootSessionId !== scope.rootSessionId
1027
+ || priorScope.generation > scope.generation || priorScope.generation === scope.generation && row.scope_json !== JSON.stringify(scope))
1028
+ throw new AppError("invocation_recovery_scope_mismatch", "Retained command does not belong to the recovered root and an earlier generation", 1, { invocation_id: row.id });
1029
+ // Recovery inherits the immutable packet binding. Those new rows are
1030
+ // successors of its original commands, not additional commands to recover.
1031
+ if (priorScope.generation < scope.generation && !origins.has(row.fingerprint))
1032
+ origins.set(row.fingerprint, row);
1033
+ }
1034
+ if (!origins.size)
1035
+ throw new AppError("invocation_recovery_scope_mismatch", "Recovery requires an earlier packet generation", 1);
941
1036
  const recovered = [];
942
- for (const id of ids) {
943
- let prior = load(context, id);
944
- const oldScope = JSON.parse(prior.scope_json);
945
- if (oldScope.projectRoot !== scope.projectRoot || oldScope.runId !== scope.runId
946
- || oldScope.rootSessionId !== scope.rootSessionId || oldScope.generation >= scope.generation) {
947
- throw new AppError("invocation_recovery_scope_mismatch", "Retained command does not belong to the recovered root and an earlier generation", 1, { invocation_id: id });
948
- }
1037
+ for (const origin of origins.values()) {
1038
+ const id = origin.id;
949
1039
  // The immutable prompt can predate several recoveries. Follow the newest
950
1040
  // retained attempt, including an unknown successor, rather than reviving
951
1041
  // an older unexecuted command after the successor already produced effects.
952
- prior = context.db.get(`SELECT * FROM lifecycle_invocations WHERE fingerprint = ?
953
- AND json_extract(scope_json, '$.projectRoot') = ? AND json_extract(scope_json, '$.runId') IS ?
954
- AND json_extract(scope_json, '$.rootSessionId') = ? AND json_extract(scope_json, '$.generation') < ?
955
- ORDER BY json_extract(scope_json, '$.generation') DESC, rowid DESC LIMIT 1`, [prior.fingerprint, scope.projectRoot, scope.runId, scope.rootSessionId, scope.generation]);
1042
+ const prior = context.db.get(`SELECT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id
1043
+ WHERE pa.packet_sha256=? AND li.fingerprint = ?
1044
+ AND json_extract(li.scope_json, '$.projectRoot') = ? AND json_extract(li.scope_json, '$.runId') IS ?
1045
+ AND json_extract(li.scope_json, '$.rootSessionId') = ? AND (json_extract(li.scope_json, '$.generation') < ? OR li.scope_json = ?)
1046
+ ORDER BY json_extract(li.scope_json, '$.generation') DESC, li.rowid DESC LIMIT 1`, [digest, origin.fingerprint, scope.projectRoot, scope.runId, scope.rootSessionId, scope.generation, JSON.stringify(scope)]);
956
1047
  if (prior.status === "executing" || prior.status === "settled") {
957
1048
  const outcome = prior.outcome_json ? JSON.parse(prior.outcome_json) : null;
958
1049
  recovered.push({ invocation_id: id, status: prior.status, outcome,
@@ -968,6 +1059,9 @@ export function recoverLifecycleCommands(context, originalPrompt, scope) {
968
1059
  const env = { ...parsed.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope) };
969
1060
  const command = `${Object.entries(env).map(([key, value]) => `${key}=${quote([value])}`).join(" ")} ${launchExpression(parsed)} ${quote(argv)} ${prior.command.slice(shellSuffixOffset(prior.command))}`.trim();
970
1061
  const prepared = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope), DD_FLOW_CURRENT_INVOCATION: undefined } }, command);
1062
+ const successor = currentIssuedCommand(context, JSON.stringify(scope), prepared);
1063
+ if (successor)
1064
+ inheritAssignedPackets(context, prior.id, successor.id);
971
1065
  recovered.push({ invocation_id: id, status: "replaced_unexecuted", command: prepared });
972
1066
  }
973
1067
  return recovered;
@@ -1029,6 +1123,28 @@ function checkCommand(row, command) {
1029
1123
  throw new AppError("invocation_command_mismatch", "Lifecycle attempt belongs to another command or project", 1, { invocation_id: row.id });
1030
1124
  }
1031
1125
  }
1126
+ /** Match a daemon-attested native tool against its exact issued command.
1127
+ * This is only a correlation check: claim/owner/generation are verified again
1128
+ * by the normal invocation path before the command can mutate anything. */
1129
+ export function matchesIssuedNativeCommand(context, id, command) {
1130
+ const row = load(context, id);
1131
+ const analysis = parseLifecycleCommand(command);
1132
+ if (analysis.kind === "none" || analysis.supported === false)
1133
+ return false;
1134
+ try {
1135
+ const scope = JSON.parse(row.scope_json);
1136
+ const parsed = invocation(canonicalManagedCommand(context, analysis.invocation.command, scope));
1137
+ assertSuppliedLifecycleTarget(row, parsed);
1138
+ // Public Work packets deliberately omit the internal UUID. The caller
1139
+ // has already selected the unique issued assignment in its managed scope;
1140
+ // this comparison supplies no authority and must still pass native proof/CAS.
1141
+ const supplied = commandOption(parsed, "invocation-id");
1142
+ return (supplied === id || supplied === undefined && parsed.operation === "work_start") && fingerprint(parsed) === row.fingerprint;
1143
+ }
1144
+ catch {
1145
+ return false;
1146
+ }
1147
+ }
1032
1148
  /** Public commands may omit the coordinator RUN. A supplied target remains
1033
1149
  * input to validate, not text to silently overwrite with retained authority. */
1034
1150
  function assertSuppliedLifecycleTarget(row, actual) {
@@ -1480,6 +1596,32 @@ export function observeLifecycleInvocation(context, input) {
1480
1596
  }
1481
1597
  function observeLifecycleInvocationReceipt(context, input) {
1482
1598
  storage(context);
1599
+ const delivered = parseLifecycleCommand(input.command);
1600
+ if (delivered.kind === "standalone") {
1601
+ const id = commandOption(delivered.invocation, "invocation-id");
1602
+ const direct = context.db.all("SELECT * FROM lifecycle_invocations WHERE admission_json IS NOT NULL AND status IN ('executing','settled')")
1603
+ .filter(row => {
1604
+ const scope = JSON.parse(row.scope_json);
1605
+ if (id && row.id !== id || scope.daemonId !== input.identity.daemonId || scope.rootSessionId !== input.identity.rootSessionId)
1606
+ return false;
1607
+ // A fresh native call must bind the explicitly committed successor,
1608
+ // not short-circuit to its predecessor's successful repair response.
1609
+ // Exact delivery/explicit-ID replay remains historical read-only.
1610
+ const successor = row.status === "settled" && row.outcome_json ? committedSuccessor(context, row, JSON.parse(row.outcome_json)) : null;
1611
+ if (!id && row.identity_json && successor && ["issued", "observed"].includes(successor.status)
1612
+ && !sameNativeCall(row.identity_json, JSON.stringify([input.identity.daemonId, input.identity.rootSessionId, input.identity.sessionId, input.identity.parentSessionId, input.identity.toolCallId])))
1613
+ return false;
1614
+ try {
1615
+ return fingerprint(invocation(canonicalManagedCommand(context, delivered.invocation.command, scope))) === row.fingerprint;
1616
+ }
1617
+ catch {
1618
+ return false;
1619
+ }
1620
+ });
1621
+ if (direct.length === 1)
1622
+ return { eventKey: direct[0].event_key, duplicate: true,
1623
+ disposition: direct[0].status === "settled" ? "settled_read" : "in_flight", invocationId: direct[0].id };
1624
+ }
1483
1625
  const projectRoot = input.projectId ? context.db.get("SELECT root FROM projects WHERE id = ?", [input.projectId])?.root : undefined;
1484
1626
  const provenScope = projectRoot && input.harness ? managedNativeInvocationScope(context, projectRoot, input.identity, input.harness) : null;
1485
1627
  const owner = provenScope && input.harness && input.projectId ? managedNativeOwner(context, input.identity, input.harness, input.projectId) : null;
@@ -1587,6 +1729,10 @@ function observeLifecycleInvocationReceipt(context, input) {
1587
1729
  // public ID is corrected only by an exact operation/target match inside the
1588
1730
  // trusted native root. Never fuzzy-match UUID text or select a global latest.
1589
1731
  const initial = supplied ?? (candidates.length === 1 ? candidates[0] : undefined);
1732
+ // A live observation arriving after direct admission is optional correlation,
1733
+ // never a second authority or an executing -> observed transition.
1734
+ if (initial?.admission_json && ["executing", "settled"].includes(initial.status))
1735
+ return { eventKey: initial.event_key, duplicate: true, disposition: initial.status === "settled" ? "settled_read" : "in_flight", invocationId: initial.id };
1590
1736
  if (!supplied && candidates.length > 1) {
1591
1737
  input.recordReceipt();
1592
1738
  throw new AppError("invocation_ambiguous", "More than one issued lifecycle attempt matches this native command", 1, { effect: "no_effect", matches: candidates.map(row => row.id), ...(provenScope ? { lifecycle_assignment: { issuer: "dd-flow", scope: provenScope } } : {}) });
@@ -1735,7 +1881,8 @@ export function observedLifecycleInvocation(context, command, scopeConstraint, c
1735
1881
  const retainedId = suppliedId && context.db.get("SELECT id FROM lifecycle_invocations WHERE id = ?", [suppliedId]) ? suppliedId : undefined;
1736
1882
  // `issued` is a rendezvous, not authority: the CLI still blocks in
1737
1883
  // awaitLifecycleInvocation until the native observer commits the receipt.
1738
- const matching = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing','settled') ORDER BY rowid DESC")
1884
+ const history = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing','settled') ORDER BY rowid DESC");
1885
+ const matching = history
1739
1886
  .filter(row => {
1740
1887
  try {
1741
1888
  const explicitId = retainedId;
@@ -1755,7 +1902,7 @@ export function observedLifecycleInvocation(context, command, scopeConstraint, c
1755
1902
  }
1756
1903
  });
1757
1904
  const identity = correlation?.identity;
1758
- const matches = lifecycleCandidates(matching, identity ? JSON.stringify([identity.daemonId, identity.rootSessionId, identity.sessionId, identity.parentSessionId, identity.toolCallId]) : undefined, true);
1905
+ const matches = lifecycleCandidates(matching, identity ? JSON.stringify([identity.daemonId, identity.rootSessionId, identity.sessionId, identity.parentSessionId, identity.toolCallId]) : undefined, true, history);
1759
1906
  if (!matches.length)
1760
1907
  return null;
1761
1908
  if (matches.length !== 1)
@@ -1764,8 +1911,9 @@ export function observedLifecycleInvocation(context, command, scopeConstraint, c
1764
1911
  }
1765
1912
  /** A new native call may consume a successor, but never a historical result.
1766
1913
  * Exact native delivery replays its immutable assignment. CLI lookup without
1767
- * native correlation remains ambiguous when history and a successor coexist. */
1768
- function lifecycleCandidates(rows, identity, lookup = false) {
1914
+ * native correlation may use a uniquely bound committed successor, not an
1915
+ * arbitrary newest/active row. Unrelated history remains ambiguous. */
1916
+ function lifecycleCandidates(rows, identity, lookup = false, history = rows) {
1769
1917
  if (identity) {
1770
1918
  const replay = rows.filter(row => {
1771
1919
  if (!row.identity_json)
@@ -1786,6 +1934,26 @@ function lifecycleCandidates(rows, identity, lookup = false) {
1786
1934
  return replay;
1787
1935
  }
1788
1936
  const active = rows.filter(row => row.status !== "settled");
1937
+ if (lookup && !identity && active.length === 1 && (active[0].status === "issued" || active[0].identity_json && active[0].event_key)) {
1938
+ const current = active[0];
1939
+ const leadsToCurrent = (row, seen = new Set()) => {
1940
+ if (row.id === current.id)
1941
+ return true;
1942
+ if (row.status !== "settled" || seen.has(row.id))
1943
+ return false;
1944
+ seen.add(row.id);
1945
+ try {
1946
+ const next = JSON.parse(row.outcome_json ?? "null")?.successor_invocation_id;
1947
+ const successor = history.find(candidate => candidate.id === next && candidate.scope_json === current.scope_json);
1948
+ return Boolean(successor && leadsToCurrent(successor, seen));
1949
+ }
1950
+ catch {
1951
+ return false;
1952
+ }
1953
+ };
1954
+ if (rows.every(row => leadsToCurrent(row)))
1955
+ return active;
1956
+ }
1789
1957
  return (!lookup || identity) && active.length ? active : rows;
1790
1958
  }
1791
1959
  /** Restore private assignment arguments without discarding the provider's
@@ -2024,13 +2192,50 @@ export async function awaitLifecycleInvocation(context, input) {
2024
2192
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60000)
2025
2193
  throw new AppError("invocation_timeout_invalid", "Receipt timeout must be within 1..60000 ms", 1);
2026
2194
  context.db.run("UPDATE lifecycle_invocations SET deadline = COALESCE(deadline, ?), updated_at = ? WHERE id = ? AND status = 'issued'", [Date.now() + timeoutMs, context.now(), input.id]);
2195
+ const initialClock = new ObservationClock({ timeoutMs }), seenProgress = new Set();
2196
+ const receiptClock = new ObservationClock({ timeoutMs, snapshotIsLive: true, saved: { ...initialClock.state(),
2197
+ elapsed_ms: Math.max(0, timeoutMs - Math.max(0, (load(context, input.id).deadline ?? Date.now()) - Date.now())) } });
2027
2198
  for (;;) {
2028
- const row = load(context, input.id);
2199
+ let row = load(context, input.id);
2200
+ if (row.status === "issued" && invocation(row.command).operation === "work_start" && input.reconcileNativeObservation) {
2201
+ const scope = JSON.parse(row.scope_json);
2202
+ input.assertCurrent(scope);
2203
+ await input.reconcileNativeObservation(scope, progress => {
2204
+ input.assertCurrent(scope);
2205
+ if (seenProgress.has(progress.cursor))
2206
+ return;
2207
+ const current = load(context, input.id);
2208
+ if (current.status !== 'issued' || current.scope_json !== row.scope_json || current.deadline === null
2209
+ || !Number.isFinite(progress.observed_at) || progress.observed_at >= current.deadline || progress.observed_at > Date.now())
2210
+ return;
2211
+ const expired = receiptClock.sample(progress);
2212
+ const state = receiptClock.state();
2213
+ if (expired || state.cursor !== progress.cursor)
2214
+ return;
2215
+ seenProgress.add(progress.cursor);
2216
+ context.db.run("UPDATE lifecycle_invocations SET deadline = ?, updated_at = ? WHERE id = ? AND status = 'issued' AND scope_json = ?", [Date.now() + Math.max(1, timeoutMs - state.elapsed_ms), context.now(), input.id, row.scope_json]);
2217
+ input.progress?.("native_verification_progress");
2218
+ });
2219
+ // A native receipt can arrive while it is being read. Reload it before
2220
+ // deciding whether this exact invocation is eligible for admission.
2221
+ row = load(context, input.id);
2222
+ if (row.status !== "issued")
2223
+ continue;
2224
+ }
2029
2225
  if (row.status === "settled")
2030
2226
  return { outcome: JSON.parse(row.outcome_json), replay: true };
2031
2227
  if (row.status === "executing") {
2032
2228
  const scope = JSON.parse(row.scope_json);
2033
2229
  const parsed = invocation(row.command);
2230
+ if (row.admission_json && ["work_finish", "work_fail"].includes(parsed.operation)) {
2231
+ const { recoverCommittedAssignedWork } = await import("./work-registry.js");
2232
+ const result = recoverCommittedAssignedWork(context, row.id);
2233
+ if (result) {
2234
+ const outcome = { result };
2235
+ settleLifecycleInvocation(context, row.id, outcome);
2236
+ return { outcome, replay: true };
2237
+ }
2238
+ }
2034
2239
  if (parsed.operation === "stage_resume" && scope.runId) {
2035
2240
  input.assertCurrent(scope);
2036
2241
  const { recoverCommittedStageResume } = await import("./stage-pause.js");
@@ -2073,6 +2278,14 @@ export async function awaitLifecycleInvocation(context, input) {
2073
2278
  throw new AppError("invocation_outcome_pending", "This attempt already started; inspect its retained operation outcome before retrying", 1, { invocation_id: input.id, effect: "unknown" });
2074
2279
  }
2075
2280
  input.assertCurrent(JSON.parse(row.scope_json));
2281
+ if (["issued", "observed"].includes(row.status) && assignedLifecycleBinding(context, row)) {
2282
+ if (claimAssignedLifecycle(context, row))
2283
+ return { kind: "managed_assignment", replay: false };
2284
+ continue;
2285
+ }
2286
+ if (row.status === "issued" && context.env.DD_FLOW_DAEMON_ID && ["work_finish", "work_fail", "stage_finish", "stage_pause", "stage_resume", "stage_start"].includes(invocation(row.command).operation)) {
2287
+ throw new AppError("invocation_assignment_unproven", "Current managed assignment is unavailable; reconcile this operation without repeating Work", 1, { invocation_id: row.id, effect: "no_effect", recoverable: true, reconciliation_required: true });
2288
+ }
2076
2289
  if (row.status === "observed") {
2077
2290
  if (row.identity_json) {
2078
2291
  // Receipt display text is bounded/redacted and may not be valid JSON.
@@ -2086,11 +2299,13 @@ export async function awaitLifecycleInvocation(context, input) {
2086
2299
  }
2087
2300
  // Another native observation can replace an unclaimed receipt. Claim
2088
2301
  // exactly the identity/route checked above, or reload and validate it.
2089
- const workStart = invocation(row.command).operation === "work_start";
2090
- const birth = workStart ? processStartedAt(process.pid) : null, cwd = workStart ? process.cwd() : "";
2302
+ const operation = invocation(row.command).operation;
2303
+ const physicalAdmission = operation === "work_start" || operation.startsWith("delivery_");
2304
+ const birth = physicalAdmission ? processStartedAt(process.pid) : null, cwd = physicalAdmission ? process.cwd() : "";
2091
2305
  input.assertCurrent(JSON.parse(row.scope_json));
2092
2306
  let admittedAt = "";
2093
2307
  const claimed = context.db.writeTransaction(() => {
2308
+ assertTerminalSettlementUnreserved(context, row);
2094
2309
  // Acquire the writer before taking the anchor; waiting for SQLite or
2095
2310
  // physical birth observation must not make an old Hook appear fresh.
2096
2311
  admittedAt = context.now();
@@ -2107,7 +2322,7 @@ export async function awaitLifecycleInvocation(context, input) {
2107
2322
  owned.delete(owned.keys().next().value);
2108
2323
  owned.set(input.id, { eventKey: row.event_key, identity: row.identity_json, scope: row.scope_json, command: row.command, cwd, pid: process.pid, birth, admittedAt });
2109
2324
  }
2110
- return { eventKey: row.event_key, replay: false };
2325
+ return { kind: "native_hook", eventKey: row.event_key, replay: false };
2111
2326
  }
2112
2327
  continue;
2113
2328
  }
@@ -2139,8 +2354,10 @@ export function settleLifecycleInvocation(context, id, outcome) {
2139
2354
  }
2140
2355
  if (row.status !== "executing")
2141
2356
  throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
2142
- if (successor)
2357
+ if (successor) {
2143
2358
  insertIssuedInvocation(context, successor);
2359
+ inheritAssignedPackets(context, id, successor.id);
2360
+ }
2144
2361
  if (context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]).changes !== 1)
2145
2362
  throw new AppError("invocation_settlement_conflict", "Lifecycle outcome changed before commit", 1, { invocation_id: id });
2146
2363
  });