ultra-builder-pro-cli 0.25.1 → 0.26.0

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 (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +40 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,903 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 8B.2 — Parallel orchestrator.
4
- //
5
- // Consumes the current Change-scoped plan.json (published by a Plan checkpoint) and
6
- // drives resumable per-wave session spawning. Each wave is either parallel
7
- // (batch Promise.all on proc exit) or serial (await per task) based on the
8
- // file-overlap detection already done by plan-builder.
9
- //
10
- // Responsibilities:
11
- // • resolve the canonical accepted Change plan
12
- // • for each ready wave: evaluate dispatch-rules per task → spawn sessions
13
- // via session-runner → wait for transport exit → preserve task-gate state
14
- // • stop at the first non-converged wave and resume safely on the next call
15
- // • emit wave_started / wave_paused|wave_completed and
16
- // plan_paused|plan_completed events
17
- //
18
- // Not responsible for:
19
- // • circuit breaker (Phase 5.2 recordTaskFailure owns that path)
20
- // • worktree lifecycle beyond what session-runner already does
21
- // • merge back (Phase 8B.4 layers on top of closeSession)
22
-
23
- const fs = require('node:fs');
24
- const path = require('node:path');
25
- const { isDeepStrictEqual } = require('node:util');
26
-
27
- const planStore = require('../mcp-server/lib/plan-store.cjs');
28
- const ops = require('../mcp-server/lib/state-ops.cjs');
29
- const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
30
- const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
31
- const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
32
- const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
33
- const runner = require('./session-runner.cjs');
34
- const dispatchRules = require('./dispatch-rules.cjs');
35
-
36
- const TERMINAL_TASK_STATUSES = new Set(['completed', 'expanded']);
37
-
38
- function resolvePlan({ db, plan, planPath, repoRoot, changeId = null }) {
39
- if (plan) {
40
- if (!plan.change_id) {
41
- throw planError(
42
- 'PLAN_CHANGE_SCOPED_REQUIRED',
43
- 'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
44
- );
45
- }
46
- return plan;
47
- }
48
- if (planPath) {
49
- throw planError(
50
- 'PLAN_LEGACY_RETIRED',
51
- 'explicit planPath execution is retired; migrate the project and use its accepted Change Plan',
52
- );
53
- }
54
- let candidates;
55
- if (changeId) {
56
- candidates = db.prepare(
57
- `SELECT id, artifact_root, status
58
- FROM changes
59
- WHERE id = ? AND status = 'active'`,
60
- ).all(changeId);
61
- } else {
62
- candidates = db.prepare(
63
- `SELECT id, artifact_root, status
64
- FROM changes WHERE status = 'active' ORDER BY updated_at DESC, id ASC`,
65
- ).all().filter((candidate) => checkpoints.currentCheckpoint(
66
- db,
67
- 'plan',
68
- { change_id: candidate.id },
69
- { includeDraft: false },
70
- ));
71
- }
72
- if (candidates.length > 1) {
73
- throw planError(
74
- 'PLAN_CHANGE_REQUIRED',
75
- 'more than one current Change has an executable plan; provide changeId',
76
- { change_ids: candidates.map((candidate) => candidate.id).sort() },
77
- );
78
- }
79
- const change = candidates[0] || null;
80
- const loaded = change
81
- ? planStore.loadChangePlanArtifact(repoRoot, change, { db, strict: true })
82
- : null;
83
- if (!loaded) {
84
- const err = new Error(
85
- changeId
86
- ? `runPlan: no current scoped plan found for change ${changeId}`
87
- : 'runPlan: no current accepted Change-scoped Plan found',
88
- );
89
- err.code = 'PLAN_NOT_FOUND';
90
- throw err;
91
- }
92
- return loaded;
93
- }
94
-
95
- function planError(code, message, details = null) {
96
- const error = new Error(message);
97
- error.code = code;
98
- if (details) error.details = details;
99
- return error;
100
- }
101
-
102
- function validatePlanShape(plan) {
103
- if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
104
- throw planError('PLAN_INVALID', 'execution plan must be an object');
105
- }
106
- if (plan.schema_version !== undefined && plan.schema_version !== '1.0') {
107
- throw planError(
108
- 'PLAN_INVALID',
109
- `unsupported execution plan schema_version: ${String(plan.schema_version)}`,
110
- );
111
- }
112
- if (plan.change_id !== undefined && plan.change_id !== null
113
- && (typeof plan.change_id !== 'string' || plan.change_id.trim() === '')) {
114
- throw planError('PLAN_INVALID', 'execution plan change_id must be null or a non-empty string');
115
- }
116
- if (!Array.isArray(plan.waves) || plan.waves.length === 0) {
117
- throw planError('PLAN_INVALID', 'execution plan must contain at least one dependency wave');
118
- }
119
- if (plan.cycles !== undefined && !Array.isArray(plan.cycles)) {
120
- throw planError('PLAN_INVALID', 'execution plan cycles must be an array');
121
- }
122
- if (Array.isArray(plan.cycles) && plan.cycles.length > 0) {
123
- throw planError('PLAN_INVALID', 'execution plan contains dependency cycles', {
124
- cycles: plan.cycles,
125
- });
126
- }
127
-
128
- const waveIds = new Set();
129
- const taskIds = new Set();
130
- for (const [index, wave] of plan.waves.entries()) {
131
- if (!wave || typeof wave !== 'object' || Array.isArray(wave)) {
132
- throw planError('PLAN_INVALID', `execution plan wave ${index + 1} must be an object`);
133
- }
134
- if ((typeof wave.id !== 'string' && typeof wave.id !== 'number')
135
- || String(wave.id).trim() === '') {
136
- throw planError('PLAN_INVALID', `execution plan wave ${index + 1} requires an id`);
137
- }
138
- const waveKey = String(wave.id);
139
- if (waveIds.has(waveKey)) {
140
- throw planError('PLAN_INVALID', `execution plan contains duplicate wave id ${waveKey}`);
141
- }
142
- waveIds.add(waveKey);
143
- if (!Array.isArray(wave.tasks) || wave.tasks.length === 0) {
144
- throw planError('PLAN_INVALID', `execution plan wave ${waveKey} must contain tasks`);
145
- }
146
- if (typeof wave.parallel !== 'boolean') {
147
- throw planError('PLAN_INVALID', `execution plan wave ${waveKey} requires parallel boolean`);
148
- }
149
- for (const taskId of wave.tasks) {
150
- if (typeof taskId !== 'string' || taskId.trim() === '') {
151
- throw planError('PLAN_INVALID', `execution plan wave ${waveKey} has an invalid task id`);
152
- }
153
- if (taskIds.has(taskId)) {
154
- throw planError('PLAN_INVALID', `execution plan contains duplicate task ${taskId}`);
155
- }
156
- taskIds.add(taskId);
157
- }
158
- }
159
- return plan;
160
- }
161
-
162
- function assertPlanAuthority(db, repoRoot, plan) {
163
- const taskIds = plan.waves.flatMap((wave) => wave.tasks);
164
- const taskRows = taskIds.map((id) => ops.readTask(db, id)).filter(Boolean);
165
- const taskChangeIds = new Set(taskRows.map((task) => task.change_id).filter(Boolean));
166
-
167
- if (!plan.change_id) {
168
- throw planError(
169
- 'PLAN_CHANGE_SCOPED_REQUIRED',
170
- 'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
171
- );
172
- }
173
- if (taskChangeIds.size > 1 || (taskChangeIds.size === 1 && !taskChangeIds.has(plan.change_id))) {
174
- throw planError(
175
- 'PLAN_CHANGE_MISMATCH',
176
- `execution plan ${plan.change_id} contains tasks owned by another change`,
177
- { task_change_ids: [...taskChangeIds].sort() },
178
- );
179
- }
180
-
181
- const currentPlan = checkpoints.currentCheckpoint(
182
- db,
183
- 'plan',
184
- { change_id: plan.change_id },
185
- { includeDraft: false },
186
- );
187
- if (!currentPlan || checkpoints.checkpointDigest(currentPlan) !== currentPlan.digest) {
188
- throw planError(
189
- 'PLAN_NOT_COMPLETED',
190
- `change ${plan.change_id} has no current accepted Plan checkpoint`,
191
- );
192
- }
193
- const context = currentPlan.context_envelope_id
194
- ? db.prepare('SELECT * FROM context_envelopes WHERE id = ?')
195
- .get(currentPlan.context_envelope_id)
196
- : null;
197
- if (!context || context.stage !== 'plan' || context.scope_type !== 'change'
198
- || context.scope_id !== plan.change_id) {
199
- throw planError(
200
- 'PLAN_STALE',
201
- `execution plan for ${plan.change_id} is not bound to its Context Envelope`,
202
- { authority_code: 'PLAN_CONTEXT_MISMATCH' },
203
- );
204
- }
205
- let contextPayload;
206
- try {
207
- contextPayload = JSON.parse(context.payload_json);
208
- } catch {
209
- throw planError(
210
- 'PLAN_STALE',
211
- `execution plan for ${plan.change_id} has a corrupt Context Envelope`,
212
- { authority_code: 'PLAN_CONTEXT_INVALID' },
213
- );
214
- }
215
- const baseline = db.prepare(
216
- `SELECT scope_json FROM baselines
217
- WHERE status <> 'superseded' ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
218
- ).get();
219
- const scope = baseline
220
- ? JSON.parse(baseline.scope_json || '["."]')
221
- : ['.'];
222
- const currentCheckout = require('../mcp-server/lib/baseline-workflow.cjs')
223
- .gitWorktreeSnapshot(repoRoot, scope);
224
- const plannedWorktreeDigest = contextPayload.git?.scoped_worktree_digest
225
- || contextPayload.git?.worktree_digest
226
- || null;
227
- if (plannedWorktreeDigest && currentCheckout.digest !== plannedWorktreeDigest) {
228
- throw planError(
229
- 'PLAN_STALE',
230
- `execution plan for ${plan.change_id} no longer matches the scoped checkout`,
231
- {
232
- authority_code: 'PLAN_CONTEXT_STALE',
233
- blockers: ['CONTEXT_WORKTREE_STALE'],
234
- },
235
- );
236
- }
237
- const staleTasks = taskRows.filter((task) => task.stale);
238
- if (staleTasks.length > 0) {
239
- throw planError(
240
- 'PLAN_STALE',
241
- `execution plan for ${plan.change_id} contains stale task contracts`,
242
- {
243
- authority_code: 'PLAN_TASK_CONTRACT_STALE',
244
- task_ids: staleTasks.map((task) => task.id),
245
- },
246
- );
247
- }
248
- const expected = planStore.buildPlan(taskRows, { changeId: plan.change_id });
249
- const { context: planContext, ...planCore } = plan;
250
- if (!isDeepStrictEqual(planCore, expected)) {
251
- throw planError(
252
- 'PLAN_STALE',
253
- `execution plan for ${plan.change_id} does not match the current DB task graph`,
254
- );
255
- }
256
- if (!planContext
257
- || planContext.snapshot_id !== context.id
258
- || planContext.manifest_path !== context.artifact_path
259
- || planContext.manifest_digest !== context.digest) {
260
- throw planError(
261
- 'PLAN_STALE',
262
- `execution plan for ${plan.change_id} does not match its planning context`,
263
- { authority_code: 'PLAN_CONTEXT_MISMATCH' },
264
- );
265
- }
266
- const change = db.prepare(
267
- 'SELECT id, artifact_root FROM changes WHERE id = ?',
268
- ).get(plan.change_id);
269
- const canonicalPaths = planStore.changePlanPaths(repoRoot, change);
270
- const expectedPlanPath = path.relative(repoRoot, canonicalPaths.json);
271
- const expectedPlanMdPath = path.relative(repoRoot, canonicalPaths.md);
272
- const canonicalPlan = planStore.loadChangePlanArtifact(
273
- repoRoot,
274
- change,
275
- { db, strict: true },
276
- );
277
- if (!canonicalPlan || !isDeepStrictEqual(canonicalPlan, plan)) {
278
- throw planError(
279
- 'PLAN_STALE',
280
- `execution plan for ${plan.change_id} differs from its canonical Change artifact`,
281
- );
282
- }
283
- return { mode: 'current-change', checkpoint_id: currentPlan.id };
284
- }
285
-
286
- function awaitExit(proc) {
287
- return new Promise((resolve) => {
288
- if (!proc) {
289
- resolve({ code: -1, signal: null, error: new Error('worker process was not created') });
290
- return;
291
- }
292
- if (proc.exitCode !== null && proc.exitCode !== undefined) {
293
- resolve({ code: proc.exitCode, signal: proc.signalCode || null, error: null });
294
- return;
295
- }
296
- let settled = false;
297
- const settle = (result) => {
298
- if (settled) return;
299
- settled = true;
300
- proc.removeListener('exit', onExit);
301
- proc.removeListener('error', onError);
302
- resolve(result);
303
- };
304
- const onExit = (code, signal) => settle({
305
- code: code === null ? -1 : code,
306
- signal,
307
- error: null,
308
- });
309
- const onError = (error) => settle({ code: -1, signal: null, error });
310
- proc.once('exit', onExit);
311
- proc.once('error', onError);
312
- });
313
- }
314
-
315
- function failureReason(execution) {
316
- if (execution.error) return `worker spawn or runtime error: ${execution.error.message}`;
317
- return `runtime process exited with code ${execution.code}`
318
- + `${execution.signal ? ` (${execution.signal})` : ''}`;
319
- }
320
-
321
- function authorityBlockedResult(error, taskId, {
322
- sid = null,
323
- reservationFailed = false,
324
- } = {}) {
325
- const gate = runner.findExpectedExecutionGate(error);
326
- if (!gate) return null;
327
- return {
328
- sid,
329
- task_id: taskId,
330
- code: -4,
331
- status: 'authority_blocked',
332
- retryable: true,
333
- authority_code: gate.code,
334
- error: error.message,
335
- ...(Array.isArray(gate.details?.blockers)
336
- ? { blockers: gate.details.blockers }
337
- : {}),
338
- ...(reservationFailed ? { _reservationFailed: true } : {}),
339
- };
340
- }
341
-
342
- function recordExecutionFailure(db, taskId, {
343
- sessionId = null,
344
- reason,
345
- } = {}) {
346
- const current = ops.readTask(db, taskId);
347
- if (current && !TERMINAL_TASK_STATUSES.has(current.status) && current.status !== 'blocked') {
348
- try { ops.patchTask(db, taskId, { status: 'blocked' }); }
349
- catch (error) {
350
- process.stderr.write(
351
- `parallel-orchestrator: patchTask(${taskId}→blocked) failed: ${error.message}\n`,
352
- );
353
- }
354
- }
355
- try {
356
- ops.recordTaskFailure(db, taskId, { reason, session_id: sessionId });
357
- } catch (error) {
358
- process.stderr.write(
359
- `parallel-orchestrator: recordTaskFailure(${taskId}) failed: ${error.message}\n`,
360
- );
361
- }
362
- }
363
-
364
- function finalizeSession(db, handle, execution, ctx = {}) {
365
- const { code } = execution;
366
- const sessionStatus = code === 0 ? 'completed' : 'crashed';
367
- const liveSession = ops.readSession(db, handle.sid);
368
- if (!liveSession || liveSession.status !== 'running') {
369
- return {
370
- sid: handle.sid,
371
- task_id: handle.task_id,
372
- code,
373
- signal: execution.signal || null,
374
- error: execution.error?.message || null,
375
- status: liveSession?.status || 'missing',
376
- task_status: ops.readTask(db, handle.task_id)?.status || null,
377
- worktree_preserved: Boolean(
378
- liveSession?.worktree_path && fs.existsSync(liveSession.worktree_path),
379
- ),
380
- merge: undefined,
381
- close_error: null,
382
- };
383
- }
384
- if (code !== 0) {
385
- recordExecutionFailure(db, handle.task_id, {
386
- sessionId: handle.sid,
387
- reason: failureReason(execution),
388
- });
389
- }
390
-
391
- // Process exit is transport evidence, not Ultra task completion. The worker
392
- // must converge the task through the normal dev/test/review authority before
393
- // an explicit auto-merge may integrate it.
394
- let task = ops.readTask(db, handle.task_id);
395
- const taskConverged = Boolean(task && TERMINAL_TASK_STATUSES.has(task.status));
396
- let merge;
397
- let worktreePreserved = true;
398
- let closeError = null;
399
- try {
400
- const closeResult = runner.closeSession(
401
- { db, repoRoot: ctx.repoRoot, sid: handle.sid },
402
- {
403
- status: sessionStatus,
404
- autoMerge: !!ctx.autoMerge && sessionStatus === 'completed' && taskConverged,
405
- mergeBaseBranch: ctx.mergeBaseBranch || 'main',
406
- },
407
- );
408
- merge = closeResult.merge;
409
- worktreePreserved = closeResult.worktree_preserved;
410
- } catch (err) {
411
- closeError = err.message;
412
- process.stderr.write(`parallel-orchestrator: closeSession(${handle.sid}) failed: ${err.message}\n`);
413
- if (ctx.onError) ctx.onError(err);
414
- }
415
- task = ops.readTask(db, handle.task_id);
416
- return {
417
- sid: handle.sid,
418
- task_id: handle.task_id,
419
- code,
420
- signal: execution.signal || null,
421
- error: execution.error?.message || null,
422
- status: sessionStatus,
423
- task_status: task?.status || null,
424
- worktree_preserved: worktreePreserved,
425
- merge,
426
- close_error: closeError,
427
- };
428
- }
429
-
430
- function prepareTaskDispatch(task, wave, ctx) {
431
- const { runtimes, command, commandArgs, commandArgsFor, onError } = ctx;
432
- const decision = dispatchRules.evaluate({
433
- task,
434
- deps_ready: true, // topo order guarantees all deps are in prior waves
435
- available_runtimes: runtimes,
436
- breaker_state: 'ok', // admissionCheck below enforces the real gate
437
- wave,
438
- });
439
- if (decision.action !== 'spawn_agent') {
440
- if (onError) onError(new Error(`task ${task.id}: deferred/blocked by rule ${decision.rule_id}`));
441
- return {
442
- task,
443
- result: { sid: null, task_id: task.id, code: -2, status: decision.action },
444
- };
445
- }
446
-
447
- const args = commandArgsFor ? commandArgsFor(task) : commandArgs;
448
- if (!Array.isArray(args)) {
449
- throw planError(
450
- 'ORCHESTRATOR_COMMAND_INVALID',
451
- `task ${task.id} command arguments must be an array`,
452
- );
453
- }
454
- return { task, decision, command, args };
455
- }
456
-
457
- function reservePreparedTask(db, repoRoot, prepared, ctx) {
458
- if (prepared.result) return prepared.result;
459
- const {
460
- task, decision, command, args,
461
- } = prepared;
462
- const { onError } = ctx;
463
- let handle;
464
- let packet = null;
465
- let authority = null;
466
- const priorContextIds = new Set(
467
- db.prepare('SELECT id FROM context_envelopes').all().map((row) => row.id),
468
- );
469
- try {
470
- if (!task.change_id) {
471
- throw planError(
472
- 'TASK_CHANGE_REQUIRED',
473
- `orchestrator task ${task.id} must belong to the accepted Change`,
474
- );
475
- }
476
- authority = runtimePaths.ensureRuntimeState(repoRoot, {
477
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
478
- });
479
- ops.tx(db, () => {
480
- packet = workerPackets.createWorkerPacket(db, {
481
- role: 'implement',
482
- task_id: task.id,
483
- runtime: decision.runtime,
484
- output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
485
- output_schema: {
486
- type: 'object',
487
- required: ['packet_digest'],
488
- properties: {
489
- packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
490
- },
491
- },
492
- }, { rootDir: repoRoot });
493
- workerPackets.markWorkerPacketAssigned(db, packet.id);
494
- handle = runner.spawnSession({
495
- db, repoRoot,
496
- task_id: task.id,
497
- runtime: decision.runtime,
498
- authority,
499
- command: null,
500
- args: [],
501
- mark_task_started: true,
502
- kernel_mode: true,
503
- packet_digest: packet.packet_digest,
504
- });
505
- handle.task_id = task.id;
506
- handle.packet = packet;
507
- });
508
- } catch (err) {
509
- rollbackFailedReservation(db, repoRoot, {
510
- handle,
511
- packet,
512
- priorContextIds,
513
- });
514
- const authorityBlocked = authorityBlockedResult(err, task.id, {
515
- reservationFailed: true,
516
- });
517
- if (authorityBlocked) return authorityBlocked;
518
- if (onError) onError(err);
519
- recordExecutionFailure(db, task.id, {
520
- reason: `worker session could not be created: ${err.message}`,
521
- });
522
- return {
523
- sid: null,
524
- task_id: task.id,
525
- code: -3,
526
- status: 'spawn_failed',
527
- error: err.message,
528
- _reservationFailed: true,
529
- };
530
- }
531
-
532
- return {
533
- _pendingStart: true,
534
- handle,
535
- command,
536
- args,
537
- };
538
- }
539
-
540
- function rollbackFailedReservation(db, repoRoot, {
541
- handle,
542
- packet,
543
- priorContextIds,
544
- }) {
545
- if (handle?.worktree_path) {
546
- try {
547
- runner._internal.reconcileRemovedWorktree(
548
- repoRoot,
549
- handle.worktree_path,
550
- { sid: handle.sid },
551
- );
552
- } catch { /* the caller preserves the original reservation error */ }
553
- }
554
- if (handle?.artifact_dir) {
555
- const expected = path.resolve(
556
- repoRoot,
557
- '.ultra',
558
- '.runtime',
559
- 'sessions',
560
- handle.sid,
561
- );
562
- const actual = path.resolve(handle.artifact_dir);
563
- if (actual === expected) {
564
- try { fs.rmSync(actual, { recursive: true, force: true }); }
565
- catch { /* the caller preserves the original reservation error */ }
566
- }
567
- }
568
- if (packet?.created && !db.prepare(
569
- 'SELECT 1 FROM worker_packets WHERE id = ?',
570
- ).get(packet.id)) {
571
- const expected = path.resolve(
572
- repoRoot,
573
- '.ultra',
574
- '.runtime',
575
- 'worker-packets',
576
- `${packet.id}.json`,
577
- );
578
- const actual = path.resolve(repoRoot, packet.packet_path);
579
- if (actual === expected) {
580
- try { fs.rmSync(actual, { force: true }); }
581
- catch { /* the caller preserves the original reservation error */ }
582
- }
583
- }
584
- const context = packet?.context_envelope;
585
- if (context?.id
586
- && !priorContextIds.has(context.id)
587
- && !db.prepare('SELECT 1 FROM context_envelopes WHERE id = ?').get(context.id)) {
588
- const actual = path.resolve(repoRoot, context.path);
589
- const runtimeContextRoot = path.resolve(
590
- repoRoot,
591
- '.ultra',
592
- '.runtime',
593
- 'contexts',
594
- );
595
- if (actual.startsWith(`${runtimeContextRoot}${path.sep}`)
596
- && path.basename(actual) === `${context.id}.json`) {
597
- try { fs.rmSync(actual, { force: true }); }
598
- catch { /* the caller preserves the original reservation error */ }
599
- }
600
- }
601
- }
602
-
603
- async function runTask(db, repoRoot, task, wave, ctx) {
604
- const prepared = ctx.prepared || prepareTaskDispatch(task, wave, ctx);
605
- const reservation = reservePreparedTask(db, repoRoot, prepared, ctx);
606
- if (!reservation?._pendingStart) return reservation;
607
- const { handle } = reservation;
608
- if (!handle.process) {
609
- handle.process = runner.startSessionProcess({
610
- db,
611
- repoRoot,
612
- sid: handle.sid,
613
- command: reservation.command,
614
- args: reservation.args,
615
- });
616
- handle.pid = handle.process.pid;
617
- }
618
- const execution = await awaitExit(handle.process);
619
- if (ctx.deferFinalize) {
620
- return { _pendingFinalize: true, handle, execution };
621
- }
622
- return finalizeSession(db, handle, execution, { ...ctx, repoRoot });
623
- }
624
-
625
- function unwindReservations(db, repoRoot, reservations, ctx) {
626
- const errors = [];
627
- for (const reservation of [...reservations].reverse()) {
628
- if (!reservation?._pendingStart) continue;
629
- const { handle } = reservation;
630
- try {
631
- runner.closeSession(
632
- { db, repoRoot, sid: handle.sid },
633
- { status: 'crashed', remove_worktree: true },
634
- );
635
- } catch (error) {
636
- errors.push(error);
637
- if (ctx.onError) ctx.onError(error);
638
- continue;
639
- }
640
- try {
641
- workerPackets.abandonWorkerPacket(
642
- db,
643
- handle.packet?.id,
644
- 'parallel reservation cancelled before execution',
645
- );
646
- } catch (error) {
647
- errors.push(error);
648
- if (ctx.onError) ctx.onError(error);
649
- }
650
- const task = ops.readTask(db, handle.task_id);
651
- if (task?.status === 'in_progress') {
652
- try {
653
- ops.updateTaskStatus(db, handle.task_id, 'pending');
654
- } catch (error) {
655
- errors.push(error);
656
- if (ctx.onError) ctx.onError(error);
657
- }
658
- }
659
- }
660
- if (errors.length > 0) {
661
- throw planError(
662
- 'WAVE_RESERVATION_ROLLBACK_FAILED',
663
- `parallel wave reservation rollback failed: ${errors.map((error) => error.message).join('; ')}`,
664
- );
665
- }
666
- }
667
-
668
- function taskDependenciesReady(db, task) {
669
- return (task.deps || []).every((dependencyId) => {
670
- const dependency = ops.readTask(db, dependencyId);
671
- return Boolean(dependency && TERMINAL_TASK_STATUSES.has(dependency.status));
672
- });
673
- }
674
-
675
- async function runWave(db, repoRoot, wave, ctx) {
676
- const taskRows = wave.tasks.map((id) => ({ id, task: ops.readTask(db, id) }));
677
- const missingTaskIds = taskRows
678
- .filter((entry) => !entry.task)
679
- .map((entry) => entry.id);
680
- if (ctx.onError) {
681
- for (const id of missingTaskIds) {
682
- ctx.onError(new Error(`task ${id} not found in DB; wave cannot converge`));
683
- }
684
- }
685
- const tasks = taskRows.map((entry) => entry.task).filter(Boolean);
686
- const runnable = tasks.filter((task) => (
687
- task.status === 'pending'
688
- && !task.stale
689
- && taskDependenciesReady(db, task)
690
- ));
691
- // Pure preparation must finish for the whole wave before any reservation
692
- // mutates tasks, sessions, or worktrees.
693
- const prepared = runnable.map((task) => prepareTaskDispatch(task, wave, ctx));
694
-
695
- if (prepared.length > 0) {
696
- ops.appendEvent(db, {
697
- type: 'wave_started',
698
- payload: {
699
- wave_id: wave.id,
700
- tasks: runnable.map((task) => task.id),
701
- parallel: wave.parallel,
702
- },
703
- });
704
- }
705
-
706
- let results;
707
- if (wave.parallel && runnable.length > 1) {
708
- const reservations = [];
709
- let reservationFailure = null;
710
- for (const item of prepared) {
711
- const reservation = reservePreparedTask(db, repoRoot, item, {
712
- ...ctx,
713
- deferStart: true,
714
- deferFinalize: true,
715
- });
716
- reservations.push(reservation);
717
- if (reservation?._reservationFailed) {
718
- reservationFailure = reservation;
719
- break;
720
- }
721
- }
722
- if (reservationFailure) {
723
- unwindReservations(db, repoRoot, reservations, ctx);
724
- results = reservations.map((reservation) => (
725
- reservation?._pendingStart
726
- ? {
727
- sid: reservation.handle.sid,
728
- task_id: reservation.handle.task_id,
729
- code: -5,
730
- status: 'reservation_cancelled',
731
- }
732
- : reservation
733
- ));
734
- } else {
735
- const executions = await Promise.all(reservations.map(async (reservation) => {
736
- if (!reservation?._pendingStart) return reservation;
737
- const { handle } = reservation;
738
- try {
739
- handle.process = runner.startSessionProcess({
740
- db,
741
- repoRoot,
742
- sid: handle.sid,
743
- command: reservation.command,
744
- args: reservation.args,
745
- });
746
- handle.pid = handle.process.pid;
747
- const execution = await awaitExit(handle.process);
748
- return { _pendingFinalize: true, handle, execution };
749
- } catch (error) {
750
- const authorityBlocked = authorityBlockedResult(
751
- error,
752
- handle.task_id,
753
- { sid: handle.sid },
754
- );
755
- if (authorityBlocked) {
756
- unwindReservations(db, repoRoot, [reservation], ctx);
757
- return authorityBlocked;
758
- }
759
- if (ctx.onError) ctx.onError(error);
760
- recordExecutionFailure(db, handle.task_id, {
761
- sessionId: handle.sid,
762
- reason: `worker process could not be started: ${error.message}`,
763
- });
764
- return finalizeSession(
765
- db,
766
- handle,
767
- { code: -1, signal: null, error },
768
- { ...ctx, repoRoot },
769
- );
770
- }
771
- }));
772
- // Parallel execution completes in parallel; settlement is serialized
773
- // only after every worker exit has been observed.
774
- results = executions.map((result) => (
775
- result?._pendingFinalize
776
- ? finalizeSession(db, result.handle, result.execution, { ...ctx, repoRoot })
777
- : result
778
- ));
779
- }
780
- } else {
781
- results = [];
782
- for (const item of prepared) {
783
- results.push(await runTask(db, repoRoot, item.task, wave, {
784
- ...ctx,
785
- prepared: item,
786
- }));
787
- }
788
- }
789
-
790
- const finalTasks = tasks.map((task) => ops.readTask(db, task.id)).filter(Boolean);
791
- const complete = missingTaskIds.length === 0
792
- && finalTasks.length === tasks.length
793
- && finalTasks.every((task) => TERMINAL_TASK_STATUSES.has(task.status));
794
- const awaiting = finalTasks
795
- .filter((task) => !TERMINAL_TASK_STATUSES.has(task.status))
796
- .map((task) => ({ id: task.id, status: task.status, stale: Boolean(task.stale) }))
797
- .concat(missingTaskIds.map((id) => ({ id, status: 'missing', stale: false })));
798
- ops.appendEvent(db, {
799
- type: complete ? 'wave_completed' : 'wave_paused',
800
- payload: {
801
- wave_id: wave.id,
802
- executions_succeeded: results.filter((r) => r.status === 'completed').length,
803
- executions_crashed: results.filter((r) => r.status === 'crashed').length,
804
- deferred: results.filter((r) => r.status === 'defer' || r.status === 'block').length,
805
- awaiting_workflow_gates: awaiting,
806
- },
807
- });
808
- return { results, complete, awaiting };
809
- }
810
-
811
- async function runPlan({
812
- db, repoRoot, plan, planPath, changeId = null,
813
- runtimes,
814
- command = null, commandArgs = [], commandArgsFor = null,
815
- autoMerge = false, mergeBaseBranch = 'main',
816
- onError = null,
817
- }) {
818
- if (!db) throw new Error('runPlan: db required');
819
- if (!repoRoot) throw new Error('runPlan: repoRoot required');
820
- if (!Array.isArray(runtimes) || runtimes.length === 0) {
821
- throw new Error('runPlan: runtimes array required');
822
- }
823
- if (typeof command !== 'string' || command.trim() === '') {
824
- const error = new Error(
825
- 'runPlan: an explicit executable command is required; refusing to reserve empty sessions',
826
- );
827
- error.code = 'ORCHESTRATOR_COMMAND_REQUIRED';
828
- throw error;
829
- }
830
- if (!Array.isArray(commandArgs)) {
831
- const error = new Error('runPlan: commandArgs must be an array');
832
- error.code = 'ORCHESTRATOR_COMMAND_INVALID';
833
- throw error;
834
- }
835
- const resolved = validatePlanShape(resolvePlan({
836
- db, plan, planPath, repoRoot, changeId,
837
- }));
838
- const planAuthority = assertPlanAuthority(db, repoRoot, resolved);
839
-
840
- const ctx = {
841
- runtimes, command, commandArgs, commandArgsFor, onError,
842
- autoMerge, mergeBaseBranch,
843
- };
844
- const allResults = [];
845
- let completedWaves = 0;
846
- let pausedWave = null;
847
- for (const wave of resolved.waves) {
848
- const outcome = await runWave(db, repoRoot, wave, ctx);
849
- allResults.push(...outcome.results);
850
- if (!outcome.complete) {
851
- pausedWave = { id: wave.id, awaiting: outcome.awaiting };
852
- break;
853
- }
854
- completedWaves += 1;
855
- }
856
-
857
- const planTaskIds = [...new Set(resolved.waves.flatMap((wave) => wave.tasks))];
858
- const awaitingWorkflowGates = planTaskIds
859
- .map((id) => ops.readTask(db, id) || { id, status: 'missing', stale: false })
860
- .filter((task) => !TERMINAL_TASK_STATUSES.has(task.status))
861
- .map((task) => ({ id: task.id, status: task.status, stale: Boolean(task.stale) }));
862
- const completed = awaitingWorkflowGates.length === 0;
863
- ops.appendEvent(db, {
864
- type: completed ? 'plan_completed' : 'plan_paused',
865
- payload: {
866
- waves: resolved.waves.length,
867
- waves_completed: completedWaves,
868
- paused_wave_id: pausedWave?.id || null,
869
- executions_succeeded: allResults.filter((r) => r.status === 'completed').length,
870
- crashed: allResults.filter((r) => r.status === 'crashed').length,
871
- awaiting_workflow_gates: awaitingWorkflowGates,
872
- },
873
- });
874
-
875
- return {
876
- status: completed ? 'completed' : 'paused',
877
- waves: resolved.waves.length,
878
- waves_completed: completedWaves,
879
- paused_wave_id: pausedWave?.id || null,
880
- plan_checkpoint_id: planAuthority.checkpoint_id || null,
881
- awaiting_workflow_gates: awaitingWorkflowGates,
882
- results: allResults,
883
- };
884
- }
885
-
886
- module.exports = {
887
- runPlan,
888
- // exposed for targeted tests
889
- _internal: {
890
- runWave,
891
- runTask,
892
- resolvePlan,
893
- validatePlanShape,
894
- assertPlanAuthority,
895
- awaitExit,
896
- finalizeSession,
897
- taskDependenciesReady,
898
- recordExecutionFailure,
899
- prepareTaskDispatch,
900
- reservePreparedTask,
901
- unwindReservations,
902
- },
903
- };