@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134

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 (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,121 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
4
+ export const recoveryObservationBudgetMs = 120_000;
5
+ const activeObservation = new AsyncLocalStorage();
6
+ export const withRecoveryObservation = (budget, action, retainActive = false) => activeObservation.run(retainActive ? activeObservation.getStore() ?? budget : budget, action);
7
+ export const recoveryRpcTimeout = (maximum) => {
8
+ if (maximum != null)
9
+ validateObservationDuration(maximum);
10
+ const observation = activeObservation.getStore();
11
+ // An unbounded productive transport wait is still bounded while recovery is
12
+ // actively reconciling. Outside recovery, its owner policy decides when to
13
+ // stop rather than a hidden adapter timer.
14
+ if (maximum === null)
15
+ return observation ? observation.timeout() : null;
16
+ return observation?.timeout(maximum ?? 30_000) ?? maximum ?? 30_000;
17
+ };
18
+ /** Call only while holding the existing owner-claim transaction. A replay of
19
+ * an explicit request never replenishes time, even after another request. */
20
+ export function renewRecoveryObservation(snapshot, requestId) {
21
+ if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 200)
22
+ throw new AppError("usage", "Reconciliation requires a nonempty request ID of at most 200 characters", 2);
23
+ const previous = snapshot.recovery_observation;
24
+ const ids = previous?.reconciliation_ids ?? [];
25
+ if (!Array.isArray(ids) || ids.some(id => typeof id !== "string"))
26
+ throw new AppError("recovery_observation_invalid", "Retained reconciliation identities are malformed", 1);
27
+ if (ids.includes(requestId))
28
+ return snapshot;
29
+ return { ...snapshot, recovery_observation: { policy_id: "settlement-inactivity@1", observer_started: false, budget_ms: recoveryObservationBudgetMs, remaining_ms: recoveryObservationBudgetMs, progress_markers: [], observation_gaps: 0, reconciliation_ids: [...ids, requestId], poll_index: 0 } };
30
+ }
31
+ /**
32
+ * New episodes measure silence between confirmed retirement facts; legacy
33
+ * receipts keep cumulative active-observer accounting. A gap never proves
34
+ * native death, and repeated gaps exhaust observation rather than grant time.
35
+ * Remaining time and seen proofs survive owner replacement.
36
+ */
37
+ export function recoveryObservationBudget(saved, monotonic = () => performance.now()) {
38
+ const prior = saved && typeof saved === "object" ? saved.recovery_observation : null;
39
+ if (prior?.policy_id !== undefined && prior.policy_id !== "settlement-inactivity@1")
40
+ throw new AppError("recovery_observation_invalid", "Unknown retained observation policy", 1);
41
+ const sliding = !prior || prior.policy_id === "settlement-inactivity@1";
42
+ if (prior?.progress_markers !== undefined && (!Array.isArray(prior.progress_markers) || prior.progress_markers.some(marker => typeof marker !== "string"))
43
+ || prior?.observation_gaps !== undefined && (!Number.isSafeInteger(prior.observation_gaps) || prior.observation_gaps < 0)
44
+ || prior?.observer_started !== undefined && typeof prior.observer_started !== "boolean")
45
+ throw new AppError("recovery_observation_invalid", "Retained observation progress is malformed", 1);
46
+ const markers = new Set(prior?.progress_markers ?? []);
47
+ let gaps = prior?.observation_gaps ?? 0;
48
+ let remaining = typeof prior?.remaining_ms === "number" && Number.isFinite(prior.remaining_ms)
49
+ ? Math.max(0, Math.min(recoveryObservationBudgetMs, prior.remaining_ms))
50
+ : prior == null ? recoveryObservationBudgetMs : 0;
51
+ // A restored observer cannot know what happened while it was absent. Retain
52
+ // one bounded uncertainty episode; repeated reconstruction never grants time.
53
+ if (sliding && prior && prior.observer_started !== false && ++gaps > 1)
54
+ remaining = 0;
55
+ let last = monotonic();
56
+ let pollIndex = Number.isSafeInteger(prior?.poll_index) && prior.poll_index >= 0 ? prior.poll_index : 0;
57
+ const observation = {
58
+ exhausted: () => {
59
+ const current = monotonic();
60
+ const elapsed = current - last;
61
+ last = current;
62
+ // A gap means this process was not observing; it never proves progress
63
+ // or death and therefore does not consume the reconciliation budget.
64
+ if (elapsed >= 0 && elapsed <= 60_000)
65
+ remaining = Math.max(0, remaining - elapsed);
66
+ else if (sliding && ++gaps > 1)
67
+ remaining = 0;
68
+ return remaining === 0;
69
+ },
70
+ observe: snapshot => {
71
+ observation.exhausted();
72
+ if (!sliding || remaining === 0)
73
+ return;
74
+ const proof = settlementProgressMarkers(snapshot);
75
+ const advanced = proof.some(marker => !markers.has(marker));
76
+ proof.forEach(marker => markers.add(marker));
77
+ if (advanced) {
78
+ remaining = recoveryObservationBudgetMs;
79
+ gaps = 0;
80
+ }
81
+ },
82
+ state: () => ({ ...(sliding ? { policy_id: "settlement-inactivity@1", observer_started: true, progress_markers: [...markers].sort(), observation_gaps: gaps } : {}), budget_ms: recoveryObservationBudgetMs, remaining_ms: Math.ceil(remaining), reconciliation_ids: prior?.reconciliation_ids ?? [], poll_index: pollIndex }),
83
+ timeout: (maximum = 30_000) => {
84
+ validateObservationDuration(maximum);
85
+ if (observation.exhausted())
86
+ throw new AppError("recovery_observation_budget_exhausted", "Recovery observation budget exhausted; explicit reconciliation is required", 1);
87
+ return Math.max(1, Math.min(maximum, Math.ceil(remaining)));
88
+ },
89
+ nextDelay: () => Math.min([1000, 2000, 5000, 10000][Math.min(pollIndex++, 3)], Math.ceil(remaining)),
90
+ };
91
+ return observation;
92
+ }
93
+ /** Only confirmed settlement facts advance recovery. Polls, errors, timestamps,
94
+ * and native model activity cannot replenish physical retirement observation. */
95
+ export function settlementProgressMarkers(snapshot) {
96
+ const result = [];
97
+ const visit = (value, prefix, depth) => {
98
+ if (!value || typeof value !== "object" || depth > 12)
99
+ return;
100
+ if (Array.isArray(value)) {
101
+ for (const item of value)
102
+ visit(item, prefix, depth + 1);
103
+ return;
104
+ }
105
+ const object = value;
106
+ const identity = ["operation_id", "process_id", "session_id", "run_id", "control_id", "scope_id", "id"].map(key => typeof object[key] === "string" ? `${key}:${object[key]}` : "").filter(Boolean).join("|");
107
+ const location = `${prefix}/${identity}`;
108
+ for (const key of ["physical_settled", "native_settled", "settled", "retired", "published", "committed"])
109
+ if (object[key] === true)
110
+ result.push(`${location}/${key}`);
111
+ if (["completed", "released", "sealed", "verified", "retired"].includes(String(object.status)))
112
+ result.push(`${location}/status:${String(object.status)}`);
113
+ if (object.state === "settled")
114
+ result.push(`${location}/state:settled`);
115
+ for (const [key, item] of Object.entries(object))
116
+ if (key !== "recovery_observation" && key !== "error" && key !== "metadata" && key !== "capture_error")
117
+ visit(item, `${location}/${key}`, depth + 1);
118
+ };
119
+ visit(snapshot, "", 0);
120
+ return [...new Set(result)].sort();
121
+ }
@@ -0,0 +1,145 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError, errorRecord } from "../shared/errors.js";
5
+ import { registerStoreWriter } from "../storage/writer-contract.js";
6
+ import { managedProcessRuntimeHome } from "./managed-processes.js";
7
+ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
8
+ /** Export one RUN's relational evidence, never mutate the live authority. */
9
+ export function captureRecoveryDatabase(source, output, projectId, runId, protocolIds = []) {
10
+ source.exec(`VACUUM INTO '${output.replace(/'/g, "''")}'`);
11
+ const db = new DatabaseSync(output);
12
+ let complete = false, held = false, failed = false, primary;
13
+ try {
14
+ registerStoreWriter(db);
15
+ db.exec("PRAGMA foreign_keys = OFF; PRAGMA secure_delete = ON; BEGIN IMMEDIATE");
16
+ held = true;
17
+ db.exec("CREATE TEMP TABLE capture_scope (project_id TEXT, run_id TEXT)");
18
+ db.prepare("INSERT INTO capture_scope VALUES (?, ?)").run(projectId, runId);
19
+ db.exec("CREATE TEMP TABLE capture_protocols (id TEXT)");
20
+ for (const id of protocolIds)
21
+ db.prepare("INSERT INTO capture_protocols VALUES (?)").run(id);
22
+ if (!db.prepare("SELECT 1 FROM runs WHERE (project_id, id) IN (SELECT * FROM capture_scope)").get())
23
+ throw new AppError("snapshot_run_missing", "Recovery export requires the selected RUN", 1);
24
+ const ownRun = "(project_id, run_id) IN (SELECT * FROM capture_scope)";
25
+ const ownProject = "project_id IN (SELECT project_id FROM capture_scope)";
26
+ const works = `SELECT work_id FROM works WHERE ${ownRun}`;
27
+ if (db.prepare(`SELECT 1 FROM sessions WHERE ${ownProject} AND run_id IS NOT NULL AND run_id NOT IN (SELECT run_id FROM capture_scope) AND session_id IN (SELECT session_id FROM work_sessions WHERE work_id IN (${works})) LIMIT 1`).get())
28
+ throw new AppError("snapshot_scope_conflict", "Selected Work refers to a Session currently bound to another RUN", 1);
29
+ const sessions = `SELECT session_id FROM sessions WHERE ${ownProject} AND (run_id IN (SELECT run_id FROM capture_scope) OR session_id IN (SELECT session_id FROM work_sessions WHERE work_id IN (${works})))`;
30
+ const nativeSessions = `SELECT harness, provider_session_id FROM sessions WHERE ${ownProject} AND session_id IN (${sessions})`;
31
+ const invocations = "json_extract(scope_json, '$.runId') IN (SELECT run_id FROM capture_scope) AND json_extract(scope_json, '$.projectRoot') IN (SELECT root FROM projects WHERE id IN (SELECT project_id FROM capture_scope))";
32
+ const invocationReceipts = db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'lifecycle_invocations'").get()
33
+ ? ` OR event_key IN (SELECT event_key FROM lifecycle_invocations WHERE ${invocations})` : "";
34
+ const protocols = `SELECT id FROM capture_protocols UNION SELECT subject_id FROM runs WHERE (project_id, id) IN (SELECT * FROM capture_scope) UNION SELECT value FROM merge_requests, json_each(merge_requests.protocol_ids_json) WHERE ${ownRun} UNION SELECT protocol_id FROM flow_jobs WHERE ${ownRun}`;
35
+ const rules = {
36
+ dd_flow_writer_contract: "1",
37
+ dd_flow_writer_migrations: "0",
38
+ projects: "id IN (SELECT project_id FROM capture_scope)",
39
+ runs: "(project_id, id) IN (SELECT * FROM capture_scope)",
40
+ sessions: `${ownProject} AND session_id IN (${sessions})`,
41
+ work_sessions: `work_id IN (${works})`, work_launches: `work_id IN (${works})`,
42
+ work_recovery_segments: `work_id IN (${works})`,
43
+ run_recovery_bindings: `recovery_id IN (SELECT recovery_id FROM run_recovery_guards WHERE ${ownRun})`,
44
+ lifecycle_invocations: invocations,
45
+ lifecycle_packet_assignments: `invocation_id IN (SELECT id FROM lifecycle_invocations WHERE ${invocations})`,
46
+ hook_events: `${ownProject} AND (session_id IN (${sessions}) OR (harness, provider_session_id) IN (${nativeSessions})${invocationReceipts})`,
47
+ harness_usage_snapshots: `${ownProject} AND (harness, provider_session_id) IN (${nativeSessions})`,
48
+ protocols: `${ownProject} AND id IN (${protocols})`,
49
+ project_config: ownProject,
50
+ // Host installations, caches, queue owners and leases are not portable RUN authority.
51
+ runtime_config: "0", hook_installations: "0", codex_home_profiles: "0", codex_session_bindings: "0",
52
+ merge_servers: "0", merge_sessions: "0", merge_queue: "0", lanes: "0", lane_locks: "0", lane_waiters: "0",
53
+ sqlite_sequence: "0"
54
+ };
55
+ for (const table of ["works", "stage_work_memberships", "repair_intents", "check_receipts", "merge_requests", "flow_run_flag_mutations", "usage", "flow_session_segments", "flow_jobs", "run_recovery_guards", "run_controllers", "run_controls"])
56
+ rules[table] = ownRun;
57
+ for (const table of ["plan_bindings", "plan_progress", "worktree_records", "audit_events"])
58
+ rules[table] = `${ownProject} AND protocol_id IN (${protocols})`;
59
+ // Relocation proofs are RUN-owned provenance needed to map unchanged context
60
+ // bytes through subsequent recovery generations, not portable live authority.
61
+ rules.audit_events = `${ownProject} AND (protocol_id IN (${protocols}) OR (event_type = 'flow_run.publication_relocation' AND json_extract(payload_json, '$.run_id') IN (SELECT run_id FROM capture_scope)))`;
62
+ for (const table of ["run_controller_events", "run_controller_operations"])
63
+ rules[table] = `controller_id IN (SELECT controller_id FROM run_controllers WHERE ${ownRun})`;
64
+ for (const table of ["run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers"])
65
+ rules[table] = `control_id IN (SELECT control_id FROM run_controls WHERE ${ownRun})`;
66
+ const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all();
67
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
68
+ // Evaluate every relationship before deleting parents, including historical bindings.
69
+ for (const { name } of tables) {
70
+ const predicate = rules[name];
71
+ if (predicate === undefined && db.prepare(`SELECT 1 FROM ${quote(name)} LIMIT 1`).get())
72
+ throw new AppError("snapshot_scope_unknown", "Recovery export has no scope rule for a populated table", 1, { table: name });
73
+ db.exec(`CREATE TEMP TABLE ${quote(`keep_${name}`)} AS SELECT rowid AS retained_id FROM ${quote(name)} WHERE ${predicate ?? "0"}`);
74
+ }
75
+ for (const { name } of tables)
76
+ db.exec(`DELETE FROM ${quote(name)} WHERE rowid NOT IN (SELECT retained_id FROM ${quote(`keep_${name}`)})`);
77
+ const issues = db.prepare("PRAGMA foreign_key_check").all();
78
+ if (issues.length)
79
+ throw new AppError("snapshot_scope_inconsistent", "Scoped recovery export has unresolved relational references", 1, { issues });
80
+ db.exec("COMMIT");
81
+ held = false;
82
+ db.exec("VACUUM");
83
+ complete = true;
84
+ }
85
+ catch (error) {
86
+ primary = error;
87
+ failed = true;
88
+ }
89
+ const cleanup = cleanupScopeWriters([{ name: output, db, rollback: held, close: "always" }]);
90
+ if (!complete) {
91
+ try {
92
+ fs.rmSync(output, { force: true });
93
+ }
94
+ catch (error) {
95
+ cleanup.push({ target: output, phase: "remove_incomplete_output", message: String(error), error: errorRecord(error) });
96
+ }
97
+ }
98
+ if (failed || cleanup.length) {
99
+ if (primary instanceof AppError) {
100
+ if (cleanup.length)
101
+ primary.details.snapshot_cleanup_errors = cleanup;
102
+ throw primary;
103
+ }
104
+ throw new AppError("snapshot_database_failed", primary instanceof Error ? primary.message : "Snapshot database cleanup failed", 1, { ...(failed ? { cause: errorRecord(primary) } : {}), snapshot_cleanup_errors: cleanup });
105
+ }
106
+ }
107
+ /** Physical leases are evidence, not authority to resurrect on another host. */
108
+ export function readRecoveryResourceEvidence(resourceHome, projectId, runId, authority) {
109
+ const file = path.join(resourceHome, "runtime.sqlite");
110
+ const scope = { schema_id: "dd-flow/recovery-resource-evidence@1", source_home: resourceHome, dd_flow_home: authority.ddFlowHome, project_id: projectId, run_id: runId };
111
+ if (!fs.existsSync(file))
112
+ return { ...scope, database_present: false, processes: [], resources: [] };
113
+ const db = new DatabaseSync(file, { readOnly: true });
114
+ let held = false, failed = false, primary, result;
115
+ try {
116
+ db.exec("PRAGMA busy_timeout = 4000; BEGIN");
117
+ held = true;
118
+ const candidates = db.prepare("SELECT * FROM managed_processes WHERE project_id = ? AND run_id = ? ORDER BY id").all(projectId, runId).map(record => ({ record, home: managedProcessRuntimeHome(record, authority) }));
119
+ if (candidates.some(item => item.home === null))
120
+ throw new AppError("snapshot_resource_scope_unproven", "A retained process has no proven runtime-home identity", 1, { resource_home: resourceHome });
121
+ const processes = candidates.filter(item => item.home === authority.ddFlowHome).map(item => item.record);
122
+ const ids = new Set(processes.map(record => record.id)), owners = new Set(processes.map(record => record.owner_id));
123
+ const resources = db.prepare("SELECT r.* FROM managed_resources r JOIN managed_processes p ON p.id = r.process_id WHERE p.project_id = ? AND p.run_id = ? ORDER BY r.resource_kind, r.resource_key").all(projectId, runId).filter(row => typeof row.process_id === "string" && ids.has(row.process_id));
124
+ const unbound = db.prepare("SELECT owner_id FROM managed_resources WHERE process_id IS NULL AND owner_id IN (SELECT owner_id FROM managed_processes WHERE project_id = ? AND run_id = ?)").all(projectId, runId).some(row => typeof row.owner_id === "string" && owners.has(row.owner_id));
125
+ if (unbound)
126
+ throw new AppError("snapshot_resource_scope_unproven", "A retained resource has no process binding proving its RUN scope", 1, { resource_home: resourceHome });
127
+ db.exec("COMMIT");
128
+ held = false;
129
+ result = { ...scope, database_present: true, processes, resources };
130
+ }
131
+ catch (error) {
132
+ primary = error;
133
+ failed = true;
134
+ }
135
+ const cleanup = cleanupScopeWriters([{ name: file, db, rollback: held, close: "always" }]);
136
+ if (failed || cleanup.length) {
137
+ if (primary instanceof AppError) {
138
+ if (cleanup.length)
139
+ primary.details.snapshot_cleanup_errors = cleanup;
140
+ throw primary;
141
+ }
142
+ throw new AppError("snapshot_resource_evidence_failed", primary instanceof Error ? primary.message : "Resource evidence cleanup failed", 1, { ...(failed ? { cause: errorRecord(primary) } : {}), snapshot_cleanup_errors: cleanup });
143
+ }
144
+ return result;
145
+ }
@@ -0,0 +1,42 @@
1
+ import crypto from "node:crypto";
2
+ import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
3
+ import { assertRunMutationAllowed } from "./run-recovery.js";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { assertSourceRepairEligible } from "./code-checks.js";
6
+ /** Checks finish before entering this synchronous registration transaction. */
7
+ export function ensureAggregateRepair(context, input, create) {
8
+ if (!input.failures.length)
9
+ throw new AppError("repair_intent_invalid", "Repair requires assertion-backed failed receipts", 1);
10
+ assertSourceRepairEligible(input.failures);
11
+ const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
12
+ return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
13
+ }
14
+ export function ensureRepairIntent(context, input, create) {
15
+ return context.db.writeTransaction(() => {
16
+ assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
17
+ const attempt = stageAttempt(context, input.projectId, input.runId, input.stage);
18
+ const payload = JSON.stringify({ project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, cycle: stageCycle(context, input.projectId, input.runId, input.stage), causes: input.causes });
19
+ const identity = crypto.createHash("sha256").update(payload).digest("hex");
20
+ const prior = context.db.get("SELECT work_id, payload_json FROM repair_intents WHERE identity = ?", [identity]);
21
+ if (prior && prior.payload_json !== payload)
22
+ throw new AppError("repair_intent_conflict", "Repair identity has different causes", 1);
23
+ const workId = prior?.work_id ?? create();
24
+ if (!prior) {
25
+ bindStageWork(context, { projectId: input.projectId, runId: input.runId, workId, stage: input.stage, attempt });
26
+ context.db.run("INSERT INTO repair_intents (identity, project_id, run_id, stage, attempt, payload_hash, payload_json, work_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [identity, input.projectId, input.runId, input.stage, attempt, identity, payload, workId, context.now()]);
27
+ }
28
+ return { schema_id: "dd-flow/continuation@1", kind: "repair_required", project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, identity, work_id: workId };
29
+ });
30
+ }
31
+ export function isRegisteredRepair(context, value, scope) {
32
+ if (!value || typeof value !== "object")
33
+ return false;
34
+ const receipt = value;
35
+ if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
36
+ return false;
37
+ if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
38
+ return false;
39
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
40
+ return false;
41
+ return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
42
+ }
@@ -0,0 +1,157 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { ignoredWorkspacePaths } from "./code-checks.js";
7
+ import { localEnvironmentAllowed, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
8
+ export function reviewerSourceInputs(root, aspectIds) {
9
+ const criteria = [...new Set(aspectIds)].sort().map(id => {
10
+ if (!/^[a-z][a-z0-9_-]*$/.test(id))
11
+ throw new AppError("review_criterion_invalid", "Assigned aspect has an invalid identity", 1, { aspect_id: id });
12
+ return `.memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects/${id}.md`;
13
+ });
14
+ const orientation = [".memory-bank/index.md", ".memory-bank/spec/engineering/index.md", ".memory-bank/spec/engineering/coding-standards.md"].filter(relative => fs.existsSync(path.join(root, relative)));
15
+ return [...criteria, ...orientation].map(relative => {
16
+ const file = path.join(root, relative);
17
+ if (!fs.existsSync(file))
18
+ throw new AppError("review_criterion_missing", "Assigned reviewer criterion is missing", 1, { path: relative });
19
+ const inside = path.relative(fs.realpathSync(root), fs.realpathSync(file));
20
+ if (inside === ".." || inside.startsWith(`..${path.sep}`) || !fs.statSync(file).isFile())
21
+ throw new AppError("review_criterion_invalid", "Reviewer source must be a file in its workspace", 1, { path: relative });
22
+ return { path: relative, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") };
23
+ });
24
+ }
25
+ export function reviewerSourceInstruction(root, aspectIds) {
26
+ const inputs = reviewerSourceInputs(root, aspectIds);
27
+ return ["Read the assigned canonical criterion files and the available project orientation below before independent review. Apply each criterion's scope; a small behavior change is not by itself a reason to omit testing or evidence. These starting inputs do not limit relevant project investigation. Do not edit them.", ...inputs.map(input => `- ${path.join(root, input.path)} (SHA256 ${input.sha256})`)].join("\n");
28
+ }
29
+ export function reviewEnvironmentAllowed(file) {
30
+ return localEnvironmentAllowed(file);
31
+ }
32
+ export function reviewSourcePaths(root, explicitInputs = [], base = "HEAD") {
33
+ const inventory = workspaceGitFiles(root, ["--cached", "--others", "--exclude-standard"]);
34
+ if (inventory === null)
35
+ return null;
36
+ const serviceAllowed = projectServicePathFilter(root, explicitInputs);
37
+ const hasBase = base !== "HEAD" || spawnSync("git", ["-C", root, "rev-parse", "--verify", "HEAD"], { encoding: "utf8" }).status === 0;
38
+ const changes = hasBase ? spawnSync("git", ["-C", root, "diff", "--name-status", "-z", "--find-renames", base, "--"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : null;
39
+ if (changes && (changes.status !== 0 || changes.error))
40
+ throw new AppError("review_git_evidence_failed", "Could not classify renamed review paths", 1, { workspace_root: root, cause: changes.error?.message ?? changes.stderr });
41
+ const fields = changes?.stdout.split("\0").filter(Boolean) ?? [], tainted = new Set();
42
+ for (let index = 0; index < fields.length;) {
43
+ const status = fields[index++];
44
+ if (/^[RC]/.test(status)) {
45
+ const before = fields[index++], after = fields[index++];
46
+ if (!reviewEnvironmentAllowed(before) || !reviewEnvironmentAllowed(after)) {
47
+ tainted.add(before);
48
+ tainted.add(after);
49
+ }
50
+ }
51
+ else
52
+ index += 1;
53
+ }
54
+ return [...new Set([...inventory, ...explicitInputs])]
55
+ .filter(relative => reviewEnvironmentAllowed(relative) && !tainted.has(relative) && serviceAllowed(path.join(root, relative)))
56
+ .sort();
57
+ }
58
+ export function reviewSourceFilter(root, explicitInputs = [], base = "HEAD") {
59
+ const selected = reviewSourcePaths(root, explicitInputs, base);
60
+ const serviceAllowed = projectServicePathFilter(root, explicitInputs);
61
+ const retained = new Set(selected?.map(relative => path.resolve(root, relative)) ?? []);
62
+ const branches = new Set();
63
+ for (const file of retained)
64
+ for (let directory = path.dirname(file); directory !== path.dirname(directory); directory = path.dirname(directory))
65
+ branches.add(directory);
66
+ return file => {
67
+ const parts = path.relative(root, file).split(path.sep);
68
+ if (parts.some(part => [".git", "node_modules", ".dd-flow"].includes(part)) || !reviewEnvironmentAllowed(file))
69
+ return false;
70
+ return selected === null ? serviceAllowed(file) : retained.has(path.resolve(file)) || branches.has(path.resolve(file));
71
+ };
72
+ }
73
+ export function reviewSourceHash(root, explicitInputs = [], base = "HEAD") {
74
+ const allowed = reviewSourceFilter(root, explicitInputs, base), ownedHook = ownedWorkspaceHookPath(root);
75
+ return reviewCopyHash(root, file => path.relative(root, file) !== ownedHook && allowed(file));
76
+ }
77
+ /** The copied project has no Git index. Apply the source's exact service-file
78
+ * policy to it, while retaining only our frozen Git evidence under .dd-flow. */
79
+ export function reviewExecutionCopyHash(copyRoot, sourceRoot, includeEvidence = true, explicitInputs = [], base = "HEAD") {
80
+ if (ownedWorkspaceHookPath(sourceRoot) && !ownedWorkspaceHookPath(copyRoot))
81
+ throw new AppError("read_only_input_changed", "Qualified review hook differs between execution copy and source", 2, { copy_root: copyRoot });
82
+ const sourceAllowed = reviewSourceFilter(sourceRoot, explicitInputs, base);
83
+ const serviceAllowed = projectServicePathFilter(sourceRoot, explicitInputs);
84
+ const ownedHook = ownedWorkspaceHookPath(sourceRoot);
85
+ const verifySelection = (directory) => {
86
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
87
+ const file = path.join(directory, entry.name), relative = path.relative(copyRoot, file);
88
+ const parts = relative.split(path.sep);
89
+ if (parts[0] === ".dd-flow" || parts.some(part => [".git", "node_modules"].includes(part)))
90
+ continue;
91
+ if (!reviewEnvironmentAllowed(file))
92
+ throw new AppError("read_only_input_changed", "Read-only execution copy contains a local environment file", 2, { path: relative });
93
+ if (!serviceAllowed(path.join(sourceRoot, relative)))
94
+ continue;
95
+ if (!sourceAllowed(path.join(sourceRoot, relative)) && ignoredWorkspacePaths(sourceRoot, [relative]).includes(relative.split(path.sep).join("/")))
96
+ continue;
97
+ if (!sourceAllowed(path.join(sourceRoot, relative)))
98
+ throw new AppError("read_only_input_changed", "Read-only execution copy contains an unselected product input", 2, { path: relative });
99
+ if (entry.isDirectory())
100
+ verifySelection(file);
101
+ }
102
+ };
103
+ verifySelection(copyRoot);
104
+ return reviewCopyHash(copyRoot, file => {
105
+ const relative = path.relative(copyRoot, file);
106
+ const parts = relative.split(path.sep);
107
+ if (parts[0] === ".dd-flow")
108
+ return includeEvidence && (parts.length === 1 || parts[1] === "review-evidence");
109
+ return relative !== ownedHook && sourceAllowed(path.join(sourceRoot, relative));
110
+ });
111
+ }
112
+ /** Exact bytes visible in a read-only execution copy, including symlink targets. */
113
+ export function reviewCopyHash(root, allowed = () => true) {
114
+ const hash = crypto.createHash("sha256");
115
+ let canonicalRoot;
116
+ try {
117
+ canonicalRoot = fs.realpathSync(root);
118
+ }
119
+ catch {
120
+ throw new AppError("read_only_input_changed", "Read-only execution copy is missing", 2, { root });
121
+ }
122
+ const visit = (directory) => {
123
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
124
+ const file = path.join(directory, entry.name);
125
+ if (!allowed(file))
126
+ continue;
127
+ const relative = path.relative(root, file).split(path.sep).join("/");
128
+ const stat = fs.lstatSync(file);
129
+ const record = (kind, value = "") => hash.update(`${JSON.stringify([relative, kind, stat.mode & 0o111, value])}\n`);
130
+ if (entry.isDirectory()) {
131
+ record("directory");
132
+ visit(file);
133
+ continue;
134
+ }
135
+ if (entry.isSymbolicLink()) {
136
+ const link = fs.readlinkSync(file);
137
+ let target;
138
+ try {
139
+ target = fs.realpathSync(file);
140
+ }
141
+ catch {
142
+ throw new AppError("read_only_workspace_escape", "Read-only copy contains an unresolved symlink", 1, { path: relative });
143
+ }
144
+ const inside = path.relative(canonicalRoot, target);
145
+ if (path.isAbsolute(link) || inside === ".." || inside.startsWith(`..${path.sep}`))
146
+ throw new AppError("read_only_workspace_escape", "Read-only copy contains a symlink outside its workspace", 1, { path: relative });
147
+ record("symlink", link);
148
+ }
149
+ else if (entry.isFile())
150
+ record("file", crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"));
151
+ else
152
+ throw new AppError("read_only_workspace_unsupported", "Read-only copy contains an unsupported file type", 1, { path: relative });
153
+ }
154
+ };
155
+ visit(root);
156
+ return hash.digest("hex");
157
+ }
@@ -0,0 +1,23 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { assertPortableArtifactRef } from "./portable-refs.js";
6
+ /** A deferral names one project-local file, never an arbitrary path. */
7
+ export function reviewDefEvidence(workspaceRoot, defId) {
8
+ try {
9
+ if (!defId || !/^DEF-[A-Za-z0-9][A-Za-z0-9_-]*$/.test(defId))
10
+ throw new Error("invalid DEF identifier");
11
+ const relative = `.memory-bank/defs/${defId}.md`;
12
+ const file = assertPortableArtifactRef(relative, { workspaceRoot, runHome: workspaceRoot, runId: "" });
13
+ const directory = fs.realpathSync(path.join(workspaceRoot, ".memory-bank", "defs"));
14
+ const root = fs.realpathSync(workspaceRoot);
15
+ const directoryRelative = path.relative(root, directory);
16
+ if (directoryRelative === ".." || directoryRelative.startsWith(`..${path.sep}`) || path.isAbsolute(directoryRelative) || path.dirname(file) !== directory || !fs.statSync(file).isFile())
17
+ throw new Error("DEF must be a regular file in the project defs directory");
18
+ return { def_id: defId, path: relative, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") };
19
+ }
20
+ catch (error) {
21
+ throw new AppError("deferral_invalid", "A deferral requires a readable project-local named DEF", 2, { def_id: defId ?? null, cause: error instanceof Error ? error.message : String(error) });
22
+ }
23
+ }
@@ -0,0 +1,56 @@
1
+ import crypto from "node:crypto";
2
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
+ const records = (value) => Array.isArray(value) ? value.map(object) : [];
4
+ const text = (value) => typeof value === "string" && value.length ? value : null;
5
+ /** Pure projection of durable evidence. Never infer quiescence from an ACK,
6
+ * a terminal RUN label, or a settlement captured before a subsequent resume. */
7
+ export function controlStatusReceipt(observation, request = {}) {
8
+ const control = object(observation.control), worker = object(observation.worker), resume = object(observation.resume_operation);
9
+ const present = Boolean(control.control_id), current = control.current === true;
10
+ const activeResume = Boolean(resume.operation_id);
11
+ const historical = !current || activeResume || ["resuming", "resumed"].includes(String(control.status));
12
+ const settlement = object(control.settlement);
13
+ const snapshot = historical ? {} : control.admission === "sealed" ? settlement : object(worker.snapshot);
14
+ const nodes = records(snapshot.nodes).map((node, index) => {
15
+ const identity = Object.fromEntries(["operation_id", "controller_id", "process_id", "check_id", "merge_request_id", "work_id", "work_session_id", "session_id", "harness", "state_dir", "resource_home"].filter(key => node[key] !== undefined).map(key => [key, node[key]]));
16
+ const settled = node.status === "completed";
17
+ const role = ["subject", "worker", "merge", "judge", "probe", "check", "process"].includes(String(node.role)) ? node.role : node.check_id ? "check" : node.merge_request_id ? "merge" : node.process_id ? "process" : "unknown";
18
+ return { id: `node:${crypto.createHash("sha256").update(JSON.stringify(Object.keys(identity).length ? identity : { index, reason: node.reason })).digest("hex")}`, role, status: settled ? "completed" : node.status === "outcome_unknown" ? "unknown" : "pending", acknowledged: settled || object(node.receipt).cancelled === true, settled, reason: settled ? null : text(node.reason) ?? "node_settlement_required", details: node };
19
+ });
20
+ const settled = present && current && !historical && control.status === "settled" && control.admission === "sealed" && Boolean(control.capture_path) && settlement.settled === true && Array.isArray(settlement.nodes) && nodes.every(node => node.settled);
21
+ let state;
22
+ if (!present)
23
+ state = request.action && ["done", "failed", "cancelled"].includes(String(observation.run_status)) ? "terminal_noop" : "not_requested";
24
+ else if (!current)
25
+ state = "superseded";
26
+ else if (resume.status === "failed" || worker.status === "failed")
27
+ state = "recovery_required";
28
+ else if (control.status === "resumed" && control.admission === "recovered" && resume.status === "completed")
29
+ state = "running";
30
+ else if (activeResume || control.status === "resuming")
31
+ state = "resuming";
32
+ else if (settled)
33
+ state = control.requested_mode === "pause" ? "paused" : "stopped";
34
+ else
35
+ state = control.requested_mode === "pause" ? "pause_requested" : "stop_requested";
36
+ const reasons = settled ? [] : [...new Set([
37
+ ...nodes.filter(node => !node.settled).map(node => node.reason),
38
+ ...(Array.isArray(snapshot.pending_reasons) ? snapshot.pending_reasons.filter((reason) => typeof reason === "string" && Boolean(reason)) : []),
39
+ ...(!present ? [state === "terminal_noop" ? "terminal_settlement_not_observed" : "control_not_requested"]
40
+ : !current ? ["control_superseded"]
41
+ : state === "running" ? ["run_active"]
42
+ : state === "resuming" ? ["recovery_acceptance_pending"]
43
+ : state === "recovery_required" ? [text(object(resume.error).code) ?? text(object(worker.error).code) ?? "control_recovery_required"]
44
+ : control.admission === "sealed" ? ["recovery_capture_required"] : ["settlement_observation_required"])
45
+ ])];
46
+ return {
47
+ ...observation, schema_id: "dd-flow/run-control-receipt@1", ok: true,
48
+ request_id: request.requestId ?? text(resume.request_id) ?? text(control.request_id), control_id: text(control.control_id),
49
+ generation: Number.isSafeInteger(control.generation) ? control.generation : 0,
50
+ requested_mode: request.action ?? (activeResume ? "resume" : text(control.requested_mode)),
51
+ applied_mode: settled ? control.requested_mode : state === "running" ? "resume" : null,
52
+ state, settled, nodes, unsettled_reasons: reasons,
53
+ resumability: ["done", "failed", "cancelled"].includes(String(observation.run_status)) ? "not_applicable" : settled || state === "recovery_required" ? "recovery_required" : ["running", "not_requested", "terminal_noop", "superseded"].includes(state) ? "not_applicable" : "pending",
54
+ recovery_id: text(control.recovery_id)
55
+ };
56
+ }