@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  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 +214 -24
  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 -4
  190. package/dist/services/retained-native-hook.js +245 -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 +12 -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 +209 -43
  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 +9 -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 +252 -66
  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 +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -57,7 +57,8 @@ export function prepareManagedWorktree(input) {
57
57
  if (typeof input[key] !== "string" || !input[key].trim() || input[key].includes("\0"))
58
58
  invalid(`--${key} must be a non-empty value without NUL`);
59
59
  const git = (args) => {
60
- const result = spawnSync("git", ["-C", projectRoot, ...args], { encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
60
+ const result = spawnSync("git", ["-C", projectRoot, ...args], { encoding: "utf8",
61
+ env: { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0" } });
61
62
  if (result.error)
62
63
  throw result.error;
63
64
  return result;
@@ -90,7 +91,9 @@ export async function createManagedWorktree(context, input, prepared = prepareMa
90
91
  // Worktrunk owns creation; the CLI supplies its project-scoped service path
91
92
  // as a one-shot config override rather than relying on a developer's user config.
92
93
  const command = [worktrunk.bin, "-C", projectRoot, "--config-set", `worktree-path = ${JSON.stringify(prepared.path)}`, "switch", "--create", prepared.branch, "--base", prepared.base, "--no-cd", "--format=json", "-y"];
93
- const result = await runExternal(command);
94
+ // commandResult overlays process.env, so explicitly clear inherited Git
95
+ // redirection instead of merely omitting it from the override object.
96
+ const result = await runExternal(command, Object.fromEntries(Object.keys(process.env).filter(key => key.startsWith("GIT_")).map(key => [key, undefined])));
94
97
  if (result.exit_code !== 0)
95
98
  throw new AppError("worktrunk_command_unavailable", "Worktrunk switch failed", 1, { command: sanitizeCommand(command), result });
96
99
  const response = parseWorktrunkResponse(result.stdout, command);
@@ -256,12 +259,12 @@ export function worktreeRecord(context, projectId, protocolId) {
256
259
  function activeWorktreeRecord(context, projectId, protocolId) {
257
260
  return context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ? AND status = 'active'", [projectId, protocolId]);
258
261
  }
259
- async function runExternal(command) {
262
+ async function runExternal(command, env = {}) {
260
263
  const [bin, ...args] = command;
261
264
  if (!bin) {
262
265
  return { exit_code: null, signal: null, stdout: "", stderr: "Missing executable." };
263
266
  }
264
- const result = await commandResult(bin, args, { startupMs: SETUP_STARTUP_MS });
267
+ const result = await commandResult(bin, args, { startupMs: SETUP_STARTUP_MS, env });
265
268
  const evidence = processEvidence(result);
266
269
  return {
267
270
  exit_code: result.exit_code,
@@ -106,7 +106,7 @@ export function ensureLifecycleInvocationStorage(db) {
106
106
  CREATE TABLE IF NOT EXISTS lifecycle_invocations (
107
107
  id TEXT PRIMARY KEY, scope_json TEXT NOT NULL, command TEXT NOT NULL,
108
108
  fingerprint TEXT NOT NULL, status TEXT NOT NULL,
109
- event_key TEXT UNIQUE, identity_json TEXT, deadline INTEGER, outcome_json TEXT,
109
+ event_key TEXT UNIQUE, identity_json TEXT, admission_json TEXT, deadline INTEGER, outcome_json TEXT,
110
110
  created_at TEXT NOT NULL, updated_at TEXT NOT NULL
111
111
  );
112
112
  CREATE INDEX IF NOT EXISTS idx_lifecycle_invocations_scope
@@ -117,6 +117,7 @@ export function ensureLifecycleInvocationStorage(db) {
117
117
  PRIMARY KEY(packet_sha256, invocation_id)
118
118
  );
119
119
  `);
120
+ ensureColumn(db, "lifecycle_invocations", "admission_json", "ALTER TABLE lifecycle_invocations ADD COLUMN admission_json TEXT");
120
121
  }
121
122
  /** RUN authority stays in its existing store; process leases stay in runtime.sqlite. */
122
123
  export function ensureRunControlStorage(db) {
@@ -18,11 +18,17 @@
18
18
  "cli/input-preparation.js",
19
19
  "cli/native-hook-preparation.js",
20
20
  "domain/contracts.js",
21
+ "domain/delivery-flows.js",
21
22
  "domain/diagnostic-aspects.js",
22
23
  "domain/diagnostic-flows.js",
24
+ "domain/distill-flow.js",
23
25
  "domain/entity-ids.js",
24
26
  "domain/flow-contract.js",
27
+ "domain/interactive-flow.js",
28
+ "domain/memory-fix-flow.js",
29
+ "domain/review-fix-flow.js",
25
30
  "domain/session-coverage.js",
31
+ "domain/source-snapshot-flows.js",
26
32
  "domain/stage-catalog.js",
27
33
  "domain/stage-handler-identities.js",
28
34
  "domain/validation.js",
@@ -45,6 +51,7 @@
45
51
  "schemas/stage-report.schema.json",
46
52
  "schemas/stage-start-response.schema.json",
47
53
  "services/accepted-intent.js",
54
+ "services/assigned-lifecycle.js",
48
55
  "services/audit.js",
49
56
  "services/branch-context.js",
50
57
  "services/build-info.js",
@@ -57,6 +64,20 @@
57
64
  "services/code-checks.js",
58
65
  "services/command-context.js",
59
66
  "services/config.js",
67
+ "services/controller-result-authority.js",
68
+ "services/delivery-access.js",
69
+ "services/delivery-approval.js",
70
+ "services/delivery-artifact.js",
71
+ "services/delivery-checks.js",
72
+ "services/delivery-execution.js",
73
+ "services/delivery-intents.js",
74
+ "services/delivery-policy.js",
75
+ "services/delivery-procedures.js",
76
+ "services/delivery-request.js",
77
+ "services/delivery-result.js",
78
+ "services/delivery-stage-entry.js",
79
+ "services/delivery-stage-handlers.js",
80
+ "services/delivery-stage-results.js",
60
81
  "services/descriptor-coordinator-session.js",
61
82
  "services/descriptor-root-work.js",
62
83
  "services/descriptor-run-settlement.js",
@@ -65,15 +86,28 @@
65
86
  "services/descriptor-stage-settlement.js",
66
87
  "services/descriptor-stage-start.js",
67
88
  "services/diagnostic-evidence.js",
89
+ "services/diagnostic-repair-intake.js",
90
+ "services/diagnostic-repair-source-intake.js",
68
91
  "services/diagnostic-report-publication.js",
69
92
  "services/diagnostic-report.js",
93
+ "services/diagnostic-run-bundle.js",
70
94
  "services/diagnostic-snapshot.js",
71
95
  "services/diagnostic-stage-handlers.js",
72
96
  "services/diagnostic-stage-results.js",
97
+ "services/diagnostic-start.js",
73
98
  "services/diagnostic-work-batch.js",
74
99
  "services/diagnostic-work-collection.js",
75
100
  "services/diagnostic-work-registration.js",
76
101
  "services/diagnostic-work-results.js",
102
+ "services/distill-document-publication.js",
103
+ "services/distill-publication-custody.js",
104
+ "services/distill-stage-handlers.js",
105
+ "services/distill-stage-results.js",
106
+ "services/distill-user-inputs.js",
107
+ "services/distill-work-batch.js",
108
+ "services/distill-work-collection.js",
109
+ "services/distill-work-registration.js",
110
+ "services/distill-work-results.js",
77
111
  "services/engines.js",
78
112
  "services/eval-snapshots.js",
79
113
  "services/execution-bundles.js",
@@ -93,11 +127,36 @@
93
127
  "services/ids.js",
94
128
  "services/installation-lock.js",
95
129
  "services/installed-flow-source.js",
130
+ "services/interactive-attached-session.js",
131
+ "services/interactive-baseline-custody.js",
132
+ "services/interactive-before-versions.js",
133
+ "services/interactive-bundle-data.js",
134
+ "services/interactive-checks.js",
135
+ "services/interactive-finish-integration.js",
136
+ "services/interactive-git-baseline.js",
137
+ "services/interactive-git-dispatch.js",
138
+ "services/interactive-mutation-scope.js",
139
+ "services/interactive-request.js",
140
+ "services/interactive-run-bundle.js",
141
+ "services/interactive-stage-entry.js",
142
+ "services/interactive-stage-handlers.js",
143
+ "services/interactive-stage-observation.js",
144
+ "services/interactive-stage-results.js",
145
+ "services/interactive-user-inputs.js",
146
+ "services/interactive-work-batch.js",
147
+ "services/interactive-work-collection.js",
148
+ "services/interactive-work-completion.js",
149
+ "services/interactive-work-context.js",
150
+ "services/interactive-work-history.js",
151
+ "services/interactive-work-registration.js",
152
+ "services/interactive-work-results.js",
153
+ "services/interactive-work-start.js",
96
154
  "services/lanes.js",
97
155
  "services/lifecycle-command.js",
98
156
  "services/lifecycle-contract.js",
99
157
  "services/lifecycle-invocations.js",
100
158
  "services/managed-daemon-binding.js",
159
+ "services/managed-lease-monitor.js",
101
160
  "services/managed-processes.js",
102
161
  "services/memory-accepted-composition.js",
103
162
  "services/memory-baseline-preservation.js",
@@ -123,6 +182,19 @@
123
182
  "services/memory-finish-handler.js",
124
183
  "services/memory-finish-integration.js",
125
184
  "services/memory-finish-readiness.js",
185
+ "services/memory-fix-finish-integration.js",
186
+ "services/memory-fix-intake-custody.js",
187
+ "services/memory-fix-intake-settlement.js",
188
+ "services/memory-fix-publication.js",
189
+ "services/memory-fix-repair-admission.js",
190
+ "services/memory-fix-request.js",
191
+ "services/memory-fix-run-bundle.js",
192
+ "services/memory-fix-scope.js",
193
+ "services/memory-fix-sources.js",
194
+ "services/memory-fix-stage-entry.js",
195
+ "services/memory-fix-stage-handlers.js",
196
+ "services/memory-fix-stage-results.js",
197
+ "services/memory-flow-user-inputs.js",
126
198
  "services/memory-git-integration.js",
127
199
  "services/memory-historical-corrected-review.js",
128
200
  "services/memory-historical-original-review.js",
@@ -210,8 +282,11 @@
210
282
  "services/migrations.js",
211
283
  "services/native-session-control.js",
212
284
  "services/operator-profiles.js",
285
+ "services/physical-startup.js",
213
286
  "services/plan-runtime.js",
214
287
  "services/portable-refs.js",
288
+ "services/project-flow-source.js",
289
+ "services/project-git-integration.js",
215
290
  "services/project-registry.js",
216
291
  "services/project-summary.js",
217
292
  "services/projects.js",
@@ -231,6 +306,11 @@
231
306
  "services/repair-receipt.js",
232
307
  "services/review-copy.js",
233
308
  "services/review-deferral.js",
309
+ "services/review-fix-handoff.js",
310
+ "services/review-fix-intake-custody.js",
311
+ "services/review-fix-stage-entry.js",
312
+ "services/review-fix-stage-handlers.js",
313
+ "services/review-fix-stage-results.js",
234
314
  "services/run-control-receipt.js",
235
315
  "services/run-control.js",
236
316
  "services/run-controller-state.js",
@@ -240,6 +320,7 @@
240
320
  "services/run-recovery-guard.js",
241
321
  "services/run-recovery.js",
242
322
  "services/run-start-custody.js",
323
+ "services/run-timeline.js",
243
324
  "services/runs.js",
244
325
  "services/runtime-budget.js",
245
326
  "services/runtime-command.js",
@@ -254,6 +335,7 @@
254
335
  "services/stage-pause.js",
255
336
  "services/stage-report-renderer.js",
256
337
  "services/stage-work-graph.js",
338
+ "services/technical-notes.js",
257
339
  "services/tooling.js",
258
340
  "services/usage.js",
259
341
  "services/verification-matrix.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.151",
3
+ "version": "0.9.0-beta.155",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -30,13 +30,14 @@
30
30
  "test": "node scripts/verification.mjs run",
31
31
  "gate:merge": "node scripts/verification.mjs run",
32
32
  "verify:plan": "node scripts/verification.mjs plan",
33
- "test:verification": "node --test --test-concurrency=1 scripts/verification-policy.test.mjs scripts/generate-verification-components.test.mjs",
33
+ "test:verification": "node --test --test-concurrency=1 scripts/verification-policy.test.mjs scripts/generate-verification-components.test.mjs scripts/verification-jobs.test.mjs",
34
34
  "test:hybrid": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/hybrid-contracts.test.mjs",
35
35
  "test:hybrid:canon": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/hybrid-canon.test.mjs scripts/hybrid-inventory.test.mjs scripts/hybrid-consumers.test.mjs scripts/hybrid-schema-copies.test.mjs scripts/verify-e3-canon.test.mjs",
36
36
  "test:e2": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e2-*.test.mjs",
37
37
  "test:e2:installed": "node --test scripts/verify-e2-built-cli.test.mjs scripts/verify-e2-installed-cli.test.mjs",
38
- "test:e3": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e3-*.test.mjs",
38
+ "test:e3": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e3-*.test.mjs scripts/verify-e3-engine-storage.test.mjs",
39
39
  "test:e4": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e4-*.test.mjs",
40
+ "test:e5": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e5-*.test.mjs",
40
41
  "test:e3:canon": "node --import ./scripts/source-test-loader.mjs --test scripts/verify-e3-canon.test.mjs",
41
42
  "test:release": "node --test scripts/verify-release-build.test.mjs && vitest run --project=release",
42
43
  "test:release:build": "node --test --test-concurrency=1 scripts/verify-release-build.test.mjs scripts/verify-e2-built-cli.test.mjs scripts/verify-e2-installed-cli.test.mjs",
@@ -0,0 +1,342 @@
1
+ // SSOT for the cooperating verification entrypoints. Standalone consumers vendor
2
+ // this file byte-for-byte; contract catalogs and report validators stay local.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { spawn, execFileSync } from 'node:child_process';
8
+ import { setTimeout as delay } from 'node:timers/promises';
9
+ import { setTimeout, clearTimeout } from 'node:timers';
10
+ import process from 'node:process';
11
+
12
+ const realHome = os.userInfo().homedir; // OS identity, never a fixture's HOME.
13
+ const providerCredentials = ['OPENAI_API_KEY','OPENROUTER_API_KEY','OPENAI_DECISIONS_API_KEY','ANTHROPIC_API_KEY','GEMINI_API_KEY','GOOGLE_API_KEY','XAI_API_KEY','ZAI_API_KEY','NODE_AUTH_TOKEN','NPM_TOKEN','NPM_ACCESS_TOKEN'];
14
+ const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
15
+ const optional = file => { try { return read(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } };
16
+ const blocked = message => Object.assign(new Error(message), { code: 'verification_ownership_unknown' });
17
+ export function atomicJson(file, value) {
18
+ fs.mkdirSync(path.dirname(file), { recursive: true });
19
+ const temporary = `${file}.${randomUUID()}.tmp`;
20
+ try { fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' }); fs.renameSync(temporary, file); }
21
+ catch (error) {
22
+ try { fs.unlinkSync(temporary); } catch (cleanup) { if (cleanup.code !== 'ENOENT') error.cleanup_error = cleanup; }
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ // Same physical identity fields as EVAL's process-snapshot helper. Observation
28
+ // failure is never interpreted as an empty process table.
29
+ function snapshot() {
30
+ return execFileSync('ps', ['-axo', 'pid=,ppid=,pgid=,lstart=,stat='], {
31
+ encoding: 'utf8', timeout: 5000, env: { PATH: process.env.PATH, LC_ALL: 'C', TZ: 'UTC' },
32
+ }).trim().split('\n').flatMap(line => {
33
+ const p = line.trim().split(/\s+/);
34
+ return p.length >= 9 ? [{ pid: +p[0], ppid: +p[1], pgid: +p[2], birth: p.slice(3, 8).join(' '), zombie: p[8].startsWith('Z') }] : [];
35
+ });
36
+ }
37
+ const same = (a, b) => a && b && a.pid === b.pid && a.birth === b.birth && !b.zombie;
38
+ function self() {
39
+ const identity = snapshot().find(p => p.pid === process.pid && !p.zombie);
40
+ if (!identity) throw blocked('Cannot establish verification owner identity');
41
+ return identity;
42
+ }
43
+ function alive(owner, processes = snapshot()) { return same(owner, processes.find(p => p.pid === owner?.pid)); }
44
+ function producerSettled(attempt) {
45
+ const owner = attempt.owner, processes = snapshot();
46
+ return !alive(owner, processes) && Number.isSafeInteger(owner?.pgid) && owner.pgid > 0
47
+ && Array.isArray(owner.groups) && !processes.some(p => !p.zombie && p.pgid === owner.pgid)
48
+ && owner.groups.every(group => Number.isSafeInteger(group.pgid) && group.pgid > 0
49
+ && !processes.some(p => !p.zombie && p.pgid === group.pgid));
50
+ }
51
+ function release(lock, owner) {
52
+ // Only one reclaimer can remove this unique fence. A late reclaimer cannot
53
+ // unlink the next owner's record after the directory has been reacquired.
54
+ fs.unlinkSync(path.join(lock, `fence-${owner.token}`));
55
+ fs.unlinkSync(path.join(lock, 'owner.json')); fs.rmdirSync(lock);
56
+ }
57
+ async function claim(lock, signal) {
58
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
59
+ const identity = self();
60
+ for (;;) {
61
+ signal?.throwIfAborted();
62
+ try {
63
+ fs.mkdirSync(lock);
64
+ const owner = { ...identity, token: randomUUID(), groups: [] };
65
+ fs.writeFileSync(path.join(lock, `fence-${owner.token}`), '', { flag: 'wx' });
66
+ atomicJson(path.join(lock, 'owner.json'), owner);
67
+ return owner;
68
+ } catch (e) { if (e.code !== 'EEXIST') throw e; }
69
+ const owner = optional(path.join(lock, 'owner.json'));
70
+ if (owner) {
71
+ const processes = snapshot();
72
+ if (!alive(owner, processes)) {
73
+ // A dead coordinator is NOT cleanup. A surviving process group keeps
74
+ // the whole lease until its physical absence is positively observed.
75
+ if (!Array.isArray(owner.groups)) throw blocked(`Unconfirmed lease groups: ${lock}`);
76
+ if (!owner.groups.some(g => !g.pgid || processes.some(p => p.pgid === g.pgid && !p.zombie))) {
77
+ // Recheck the immutable owner token before removing its record.
78
+ if (optional(path.join(lock, 'owner.json'))?.token === owner.token) {
79
+ try { release(lock, owner); } catch (e) { if (!['ENOENT', 'ENOTEMPTY'].includes(e.code)) throw e; }
80
+ }
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ await delay(50, undefined, { signal });
86
+ }
87
+ }
88
+
89
+ /** One immutable producer attempt. The index is scheduling state, not proof;
90
+ * callers must validate the returned report bytes and meaningful inputs. */
91
+ export async function withContractAttempt(index, { rerun = false, failedOnly = false, reason, signal, validate, validationKey } = {}, action) {
92
+ if (rerun && !reason) throw new Error('Explicit rerun requires a reason');
93
+ const lock = `${index}.claim`;
94
+ const initial = optional(index);
95
+ if (initial?.status === 'running' && (!rerun || failedOnly)) {
96
+ try {
97
+ const result = await joinAttempt(initial, signal);
98
+ await validate?.(result.result, initial);
99
+ signal?.throwIfAborted();
100
+ return result;
101
+ } catch (error) {
102
+ signal?.throwIfAborted();
103
+ if (!rerun || !producerSettled(initial)) throw error;
104
+ }
105
+ }
106
+ let joined = null;
107
+ const owner = await claim(lock, signal);
108
+ let attempt;
109
+ try {
110
+ let current = optional(index); // Mandatory reread under claim.
111
+ if (rerun && current?.status === 'running' && producerSettled(current)) {
112
+ const terminal = optional(current.report);
113
+ if (terminal && (terminal.attemptId !== current.attemptId || !['passed', 'failed', 'interrupted'].includes(terminal.status))) throw blocked('Invalid terminal producer evidence');
114
+ signal?.throwIfAborted();
115
+ if (!terminal) atomicJson(current.report, { attemptId: current.attemptId, status: 'interrupted', error: 'Producer exited without terminal evidence; owner and owned process groups are absent', reason });
116
+ current = { ...current, status: terminal?.status ?? 'interrupted' };
117
+ atomicJson(index, current);
118
+ }
119
+ if (current?.status === 'running' && (!rerun || failedOnly)) { joined = current; }
120
+ else if (current && (!rerun || failedOnly && current.status === 'passed')) {
121
+ if (current.status !== 'passed') throw blocked(`Retained ${current.status} attempt requires explicit rerun: ${index}`);
122
+ try {
123
+ const terminal = read(current.report);
124
+ if (terminal.attemptId !== current.attemptId || terminal.status !== 'passed') throw new Error('Invalid terminal attempt');
125
+ await validate?.(terminal.result, current);
126
+ signal?.throwIfAborted();
127
+ return { ...terminal, reused: true, joined: false };
128
+ } catch (error) {
129
+ signal?.throwIfAborted(); // A cancelled reader never invalidates its producer.
130
+ atomicJson(index, { ...current, status: 'evidence_invalid', reason: String(error.message) });
131
+ reason = `Invalid evidence: ${error.message}`;
132
+ }
133
+ }
134
+ if (!joined) {
135
+ if (current?.status === 'running') throw blocked('Cannot supersede an active producer');
136
+ const attemptId = randomUUID();
137
+ const root = path.join(`${index}.attempts`, attemptId);
138
+ fs.mkdirSync(root, { recursive: true });
139
+ attempt = { attemptId, root, report: path.join(root, 'terminal.json'), owner, status: 'running', reason: reason ?? null, validationKey };
140
+ // Accepted rerun supersedes an old PASS before dispatch.
141
+ atomicJson(index, attempt);
142
+ }
143
+ } finally { release(lock, owner); }
144
+ if (joined) {
145
+ const result = await joinAttempt(joined, signal);
146
+ await validate?.(result.result, joined);
147
+ signal?.throwIfAborted();
148
+ return result;
149
+ }
150
+ try {
151
+ signal?.throwIfAborted();
152
+ const result = await action({ attemptId: attempt.attemptId, root: attempt.root });
153
+ await validate?.(result, attempt);
154
+ signal?.throwIfAborted();
155
+ const terminal = { attemptId: attempt.attemptId, result, status: 'passed' };
156
+ atomicJson(attempt.report, terminal);
157
+ atomicJson(index, { ...attempt, status: 'passed' });
158
+ return { ...terminal, reused: false, joined: false };
159
+ } catch (error) {
160
+ const terminal = { attemptId: attempt.attemptId, status: signal?.aborted ? 'interrupted' : 'failed', error: String(error.stack ?? error) };
161
+ atomicJson(attempt.report, terminal);
162
+ atomicJson(index, { ...attempt, status: terminal.status });
163
+ throw error;
164
+ }
165
+ }
166
+ async function joinAttempt(attempt, signal) {
167
+ for (;;) {
168
+ signal?.throwIfAborted(); // Cancels only this waiter.
169
+ const terminal = optional(attempt.report);
170
+ if (terminal) {
171
+ if (terminal.attemptId !== attempt.attemptId || terminal.status !== 'passed') throw blocked(`Producer attempt did not pass: ${attempt.attemptId}`);
172
+ return { ...terminal, reused: false, joined: true };
173
+ }
174
+ if (!alive(attempt.owner)) throw blocked(`Producer died without terminal evidence: ${attempt.attemptId}`);
175
+ await delay(50, undefined, { signal });
176
+ }
177
+ }
178
+
179
+ function isAncestor(identity, processes) {
180
+ let cursor = processes.find(p => p.pid === process.pid);
181
+ const seen = new Set();
182
+ while (cursor && !seen.has(cursor.pid)) {
183
+ if (same(identity, cursor)) return true;
184
+ seen.add(cursor.pid); cursor = processes.find(p => p.pid === cursor.ppid);
185
+ }
186
+ return false;
187
+ }
188
+ async function inheritedSlot(env, home, signal) {
189
+ if (!env.DD_FLOW_VERIFICATION_SLOT) return false;
190
+ let context;
191
+ try { context = JSON.parse(env.DD_FLOW_VERIFICATION_SLOT); } catch { throw blocked('Malformed inherited slot'); }
192
+ for (;;) {
193
+ const owner = optional(path.join(home, 'lease', 'owner.json')), processes = snapshot();
194
+ const group = owner?.groups?.find(g => g.token === context.token);
195
+ if (!group || owner.token !== context.owner || !alive(owner, processes)) break;
196
+ // A child may reach its first import before spawn() publishes its identity.
197
+ if (!group.pid) { await delay(10, undefined, { signal }); continue; }
198
+ if (isAncestor(group, processes)) return { owner, group };
199
+ break;
200
+ }
201
+ throw blocked('Inherited slot is not owned by an ancestor job');
202
+ }
203
+ async function nestedScope(env, home, outer, signal) {
204
+ if (!env.DD_FLOW_VERIFICATION_NESTED_SLOT) return outer ? `${outer.owner.token}-${outer.group.token}` : null;
205
+ let context;
206
+ try { context = JSON.parse(env.DD_FLOW_VERIFICATION_NESTED_SLOT); } catch { throw blocked('Malformed nested slot'); }
207
+ if (!outer || !/^[a-f0-9-]+$/.test(context.scope ?? '')) throw blocked('Invalid nested slot scope');
208
+ for (;;) {
209
+ const owner = optional(path.join(home, 'nested-slots', context.scope, 'owner.json')), processes = snapshot();
210
+ const child = owner?.groups?.find(group => group.token === context.child);
211
+ if (!child || owner.token !== context.owner || !alive(owner, processes)) break;
212
+ if (!child.pid) { await delay(10, undefined, { signal }); continue; }
213
+ if (isAncestor(child, processes)) return `${owner.token}-${child.token}`;
214
+ break;
215
+ }
216
+ throw blocked('Nested slot is not owned by an ancestor dispatch');
217
+ }
218
+
219
+ /** FIFO host-wide pool; descriptors are child commands, never callbacks sharing
220
+ * process.env. Exclusive drains both slots; inherited runners use one slot. */
221
+ export async function runJobs(jobs, { jobs: budget = 2, signal, env = process.env, coordinationHome } = {}) {
222
+ const queuedAt = Date.now();
223
+ if (![1, 2].includes(budget)) throw new Error('Verification budget must be 1 or 2');
224
+ if (Array.isArray(jobs) && jobs.some(job => job?.credentialKeys !== undefined && (!Array.isArray(job.credentialKeys) || job.credentialKeys.some(name => typeof name !== 'string' || !providerCredentials.includes(name))))) throw new Error('Invalid credential allowlist');
225
+ if (!Array.isArray(jobs) || jobs.some(job=>!job || typeof job.id!=='string' || !job.id || typeof job.command!=='string' || !job.command || job.args && (!Array.isArray(job.args)||job.args.some(arg=>typeof arg!=='string')) || job.executionClass!==undefined && !['isolated','exclusive'].includes(job.executionClass)) || new Set(jobs.map(job=>job.id)).size!==jobs.length) throw new Error('Invalid/duplicate verification job descriptor');
226
+ const home = coordinationHome ?? env.DD_FLOW_VERIFICATION_COORDINATION_HOME ?? path.join(realHome, '.dd-flow', 'verification-coordinator');
227
+ const nested = await inheritedSlot(env, home, signal);
228
+ const scope = await nestedScope(env, home, nested, signal);
229
+ const lock = scope ? path.join(home, 'nested-slots', scope) : path.join(home, 'lease');
230
+ const owner = await claim(lock, signal);
231
+ // A dead nested coordinator cannot free this slot while its outer group lives.
232
+ if (nested) { owner.groups = [{ ...nested.group, token: 'outer-group' }]; atomicJson(path.join(lock, 'owner.json'), owner); }
233
+ const results = new Array(jobs.length);
234
+ const active = new Set();
235
+ let failure;
236
+ const save = () => { if (owner) atomicJson(path.join(lock, 'owner.json'), owner); };
237
+ const start = (job, index) => {
238
+ signal?.throwIfAborted();
239
+ if (failure) throw failure;
240
+ const promise = execute(job, { home, owner, nested, scope, signal, env, save, queuedAt }).then(result => { results[index] = result; }, error => { failure ??= error; }).finally(() => active.delete(promise));
241
+ active.add(promise); return promise;
242
+ };
243
+ try {
244
+ for (let i = 0; i < jobs.length; i++) {
245
+ signal?.throwIfAborted();
246
+ if (failure) throw failure;
247
+ if (jobs[i].executionClass === 'exclusive') { await Promise.all(active); await start(jobs[i], i); }
248
+ else { while (active.size >= (nested ? 1 : budget)) await Promise.race(active); start(jobs[i], i); }
249
+ }
250
+ await Promise.all(active);
251
+ if (failure) throw failure;
252
+ return results;
253
+ } finally {
254
+ await Promise.allSettled(active);
255
+ // Never release a lease after unknown cleanup.
256
+ if (nested && owner.groups.every(group => group.token === 'outer-group')) owner.groups = [];
257
+ if (owner.groups.length === 0) release(lock, owner);
258
+ }
259
+ }
260
+
261
+ async function execute(job, { home, owner, nested, scope, signal, env, save, queuedAt }) {
262
+ const started = Date.now();
263
+ const attemptId = randomUUID();
264
+ const root = path.join(home, 'jobs', attemptId);
265
+ fs.mkdirSync(root, { recursive: true });
266
+ const childEnv = { ...env, ...job.env };
267
+ for (const name of providerCredentials) if (!job.credentialKeys?.includes(name)) delete childEnv[name];
268
+ childEnv.DD_FLOW_VERIFICATION_COORDINATION_HOME = home;
269
+ // node:test sets this private flag for its own children; a separately managed
270
+ // test command must produce a standalone report, not use its parent's IPC.
271
+ delete childEnv.NODE_TEST_CONTEXT;
272
+ for (const name of ['HOME', 'XDG_CACHE_HOME', 'XDG_CONFIG_HOME', 'XDG_RUNTIME_DIR', 'CODEX_HOME', 'AGY_HOME', 'DD_FLOW_HOME', 'DD_FLOW_RESOURCE_HOME', 'DD_FLOW_CONFIG_HOME']) {
273
+ childEnv[name] = path.join(root, name.toLowerCase()); fs.mkdirSync(childEnv[name], { recursive: true });
274
+ }
275
+ // Unix socket paths have a small OS limit. Keep the per-attempt temporary
276
+ // world short even when the durable coordinator/report home is deeply nested.
277
+ const tempBase = process.platform === 'win32' ? os.tmpdir() : fs.realpathSync('/tmp');
278
+ childEnv.TMPDIR = fs.mkdtempSync(path.join(tempBase, 'ddv-'));
279
+ atomicJson(path.join(root, 'temporary.json'), { tmpdir: childEnv.TMPDIR });
280
+ // Only an explicit native/delivery descriptor may opt into provider credentials.
281
+ const token = randomUUID();
282
+ if (!nested) {
283
+ childEnv.DD_FLOW_VERIFICATION_SLOT = JSON.stringify({ owner: owner.token, token });
284
+ delete childEnv.DD_FLOW_VERIFICATION_NESTED_SLOT;
285
+ } else {
286
+ childEnv.DD_FLOW_VERIFICATION_SLOT = env.DD_FLOW_VERIFICATION_SLOT;
287
+ childEnv.DD_FLOW_VERIFICATION_NESTED_SLOT = JSON.stringify({ scope, owner: owner.token, child: token });
288
+ }
289
+ // Unknown spawn outcome must retain the lease after coordinator death.
290
+ owner.groups.push({ token, attemptId, root, pgid: null }); save();
291
+ const child = spawn(job.command, job.args ?? [], { cwd: job.cwd, env: childEnv, stdio: job.stdio ?? 'inherit', detached: !nested });
292
+ let spawnError;
293
+ const closed = new Promise(resolve => child.once('close', resolve));
294
+ const completion = new Promise(resolve => { child.once('error', error => { spawnError = error; resolve({ code: null, signal: null }); }); child.once('exit', (code, exitSignal) => resolve({ code, signal: exitSignal })); });
295
+ // A pipe is owned by this helper, not exposed to callers. Drain it while
296
+ // preserving output; an orphan holding it open must not hide direct exit.
297
+ for (const [name, stream] of [['stdout', child.stdout], ['stderr', child.stderr]]) stream?.on('data', bytes => fs.appendFileSync(path.join(root, `${name}.log`), bytes));
298
+ let identity;
299
+ if (child.pid) identity = snapshot().find(p => p.pid === child.pid && !p.zombie);
300
+ if (identity && owner) { identity = { ...identity, token, attemptId, root }; owner.groups = owner.groups.map(g => g.token === token ? identity : g); save(); }
301
+ let cleanupError;
302
+ const stop = (killSignal = 'SIGTERM') => {
303
+ if (!identity) return;
304
+ try {
305
+ const current = snapshot().find(p => p.pid === identity.pid);
306
+ if (same(identity, current)) process.kill(nested ? identity.pid : -identity.pgid, killSignal);
307
+ } catch (error) { if (error.code !== 'ESRCH') cleanupError = error; }
308
+ };
309
+ let forceTimer;
310
+ const cancel = () => { stop(); forceTimer = setTimeout(() => stop('SIGKILL'), 1000); };
311
+ signal?.addEventListener('abort', cancel, { once: true });
312
+ if (signal?.aborted) cancel();
313
+ const result = await completion;
314
+ clearTimeout(forceTimer);
315
+ signal?.removeEventListener('abort', cancel);
316
+ try {
317
+ if (cleanupError) throw cleanupError;
318
+ if (identity && !nested) {
319
+ for (let n = 0; n < 100 && snapshot().some(p => p.pgid === identity.pgid && !p.zombie); n++) {
320
+ stop(); await delay(50);
321
+ }
322
+ if (snapshot().some(p => p.pgid === identity.pgid && !p.zombie)) throw blocked(`Owned group cleanup unconfirmed: ${identity.pgid}`);
323
+ }
324
+ if (child.pid && !identity && snapshot().some(p => !p.zombie && (nested ? p.pid === child.pid : p.pgid === child.pid))) throw blocked(`Spawned process identity unconfirmed: ${child.pid}`);
325
+ const ioClosed = await Promise.race([closed.then(() => true), delay(5000, undefined, { ref: false }).then(() => false)]);
326
+ if (!ioClosed) {
327
+ owner.groups = owner.groups.map(group => group.token === token ? { ...group, pgid: null, reason: 'stdio_cleanup_unconfirmed' } : group); save();
328
+ throw blocked(`Owned stdio cleanup unconfirmed: ${child.pid}`);
329
+ }
330
+ // Missing startup identity is not an eternal unknown after actual exit plus
331
+ // positive absence of the exact spawned group. Never kill by an inferred PID.
332
+ owner.groups = owner.groups.filter(g => g.token !== token); save();
333
+ if (spawnError) throw spawnError;
334
+ const finished = Date.now();
335
+ const terminal = { id: job.id, attemptId, root, ...result, started_at: new Date(started).toISOString(), finished_at: new Date(finished).toISOString(), queue_ms: started - queuedAt, elapsed_ms: finished - started, status: signal?.aborted ? 'interrupted' : result.code === 0 ? 'passed' : 'failed' };
336
+ atomicJson(path.join(root, 'terminal.json'), terminal);
337
+ return terminal;
338
+ } catch (error) {
339
+ child.stdout?.destroy(); child.stderr?.destroy();
340
+ throw error;
341
+ }
342
+ }