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,1153 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 4.5.1 — standard session unit runner.
4
- // Session = one task/runtime lease + independent git worktree + state.db
5
- // lease/heartbeat. A caller may attach a worker process or let the active host
6
- // operate directly in the returned worktree.
7
- //
8
- // This module is the single session/worktree execution layer shared by MCP
9
- // session.* tools and the optional orchestrator child-process path.
10
- // Callers share a single open state-db connection (WAL supports multi-writer).
11
-
12
- const fs = require('node:fs');
13
- const path = require('node:path');
14
- const { spawn, execFileSync } = require('node:child_process');
15
- const { randomUUID } = require('node:crypto');
16
-
17
- const ops = require('../mcp-server/lib/state-ops.cjs');
18
- const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
19
- const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
20
- const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
21
- const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
22
- const closeJournal = require('./session-close-journal.cjs');
23
-
24
- const EXPECTED_EXECUTION_GATE_CODES = new Set([
25
- 'TASK_EXECUTION_CONTRACT_INCOMPLETE',
26
- 'WORKFLOW_TASK_NOT_EXECUTABLE',
27
- 'TASK_DEPENDENCIES_INCOMPLETE',
28
- 'WORKFLOW_PLAN_NOT_COMPLETED',
29
- 'WORKFLOW_PLAN_TASK_SET_STALE',
30
- 'WORKFLOW_PLAN_TASK_CONTRACT_STALE',
31
- 'WORKFLOW_PLAN_CONTEXT_STALE',
32
- 'WORKFLOW_IMPLEMENTATION_CONTEXT_REQUIRED',
33
- 'WORKFLOW_IMPLEMENTATION_CONTEXT_STALE',
34
- 'TASK_NOT_EXECUTABLE',
35
- 'PLAN_CHECKPOINT_REQUIRED',
36
- 'PLAN_TASK_CONTRACT_MISSING',
37
- 'PLAN_TASK_CONTRACT_STALE',
38
- 'IMPLEMENTATION_CONTEXT_REQUIRED',
39
- 'IMPLEMENTATION_CONTEXT_STALE',
40
- 'PLAN_CONTEXT_STALE',
41
- 'WORKER_PACKET_REQUIRED',
42
- 'CONTEXT_REQUIRED_REF_STALE',
43
- 'ADMISSION_DENIED',
44
- 'ACTIVE_SESSION_LEASE_CONFLICT',
45
- 'SESSION_CLOSE_PENDING',
46
- 'CIRCUIT_TRIPPED',
47
- 'LEGACY_RUNTIME_CONFLICT',
48
- 'LEGACY_RUNTIME_MIGRATION_FAILED',
49
- 'RUNTIME_ADMISSION_ROLLBACK_FAILED',
50
- 'RUNTIME_STATE_CONFLICT',
51
- 'RUNTIME_STATE_MIGRATION_FAILED',
52
- 'RUNTIME_STATE_NOT_QUIESCENT',
53
- 'SESSION_CLOSE_JOURNAL_INVALID',
54
- 'SESSION_CLOSE_JOURNAL_CONFLICT',
55
- 'SESSION_CLOSE_CONFLICT',
56
- ]);
57
-
58
- class SessionRunnerError extends Error {
59
- constructor(code, message, { cause, details } = {}) {
60
- super(message);
61
- this.name = 'SessionRunnerError';
62
- this.code = code;
63
- if (cause) this.cause = cause;
64
- if (details !== undefined) this.details = details;
65
- }
66
- }
67
-
68
- function findExpectedExecutionGate(error) {
69
- const seen = new Set();
70
- let current = error;
71
- while (current && (typeof current === 'object' || typeof current === 'function')) {
72
- if (seen.has(current)) return null;
73
- seen.add(current);
74
- if (EXPECTED_EXECUTION_GATE_CODES.has(current.code)) return current;
75
- current = current.cause;
76
- }
77
- return null;
78
- }
79
-
80
- function isExpectedExecutionGate(error) {
81
- return Boolean(findExpectedExecutionGate(error));
82
- }
83
-
84
- function assertSessionId(sid) {
85
- if (typeof sid !== 'string'
86
- || sid.length === 0
87
- || sid.length > 128
88
- || sid === '.'
89
- || sid === '..'
90
- || path.isAbsolute(sid)
91
- || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sid)) {
92
- throw new SessionRunnerError(
93
- 'WORKTREE_SCOPE_INVALID',
94
- `session id is not a safe worktree leaf: ${String(sid)}`,
95
- );
96
- }
97
- return sid;
98
- }
99
-
100
- function assertManagedWorktreeRoot(repoRoot, { validateRuntimeTree = true } = {}) {
101
- const managedBase = runtimePaths.validateProjectLayout(
102
- repoRoot, { env: {}, validateRuntimeTree },
103
- ).worktreesDir;
104
- const stat = fs.lstatSync(managedBase, { throwIfNoEntry: false });
105
- if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
106
- throw new SessionRunnerError(
107
- 'WORKTREE_SCOPE_INVALID',
108
- `managed worktree root must be a real directory: ${managedBase}`,
109
- );
110
- }
111
- return managedBase;
112
- }
113
-
114
- function assertManagedWorktreePath(repoRoot, candidate, {
115
- sid = null,
116
- validateRuntimeTree = true,
117
- } = {}) {
118
- if (!repoRoot || typeof candidate !== 'string' || !candidate) {
119
- throw new SessionRunnerError(
120
- 'WORKTREE_SCOPE_INVALID',
121
- 'repoRoot and a managed worktree path are required',
122
- );
123
- }
124
- const expectedSid = assertSessionId(sid === null ? path.basename(candidate) : sid);
125
- const managedBase = assertManagedWorktreeRoot(repoRoot, { validateRuntimeTree });
126
- if (String(candidate).split(/[\\/]+/).includes('..')) {
127
- throw new SessionRunnerError(
128
- 'WORKTREE_SCOPE_INVALID',
129
- `worktree path may not contain traversal segments: ${candidate}`,
130
- );
131
- }
132
- const resolved = path.resolve(repoRoot, candidate);
133
- if (path.basename(resolved) !== expectedSid) {
134
- throw new SessionRunnerError(
135
- 'WORKTREE_SCOPE_INVALID',
136
- `worktree path must be the exact managed child for session ${expectedSid}: ${candidate}`,
137
- );
138
- }
139
- const candidateStat = fs.lstatSync(resolved, { throwIfNoEntry: false });
140
- if (candidateStat && (candidateStat.isSymbolicLink() || !candidateStat.isDirectory())) {
141
- throw new SessionRunnerError(
142
- 'WORKTREE_SCOPE_INVALID',
143
- `managed worktree path must be a real directory: ${candidate}`,
144
- );
145
- }
146
- const physicalManaged = physicalCandidate(managedBase);
147
- const physicalResolved = physicalCandidate(resolved);
148
- const physicalRelative = path.relative(physicalManaged, physicalResolved);
149
- if (physicalRelative !== expectedSid) {
150
- throw new SessionRunnerError(
151
- 'WORKTREE_SCOPE_INVALID',
152
- `worktree path must resolve to the exact managed child ${expectedSid}: ${candidate}`,
153
- );
154
- }
155
- return resolved;
156
- }
157
-
158
- function gitWorktreeAdd(repoRoot, worktreePath, ref = 'HEAD') {
159
- const managedPath = assertManagedWorktreePath(repoRoot, worktreePath, {
160
- sid: path.basename(worktreePath),
161
- });
162
- fs.mkdirSync(path.dirname(managedPath), { recursive: true });
163
- try {
164
- execFileSync('git', ['worktree', 'add', '--detach', managedPath, ref], {
165
- cwd: repoRoot, stdio: 'pipe',
166
- });
167
- } catch (err) {
168
- throw new SessionRunnerError('WORKTREE_FAILED', `git worktree add failed: ${err.stderr ? err.stderr.toString().trim() : err.message}`, { cause: err });
169
- }
170
- }
171
-
172
- function gitWorktreeRemove(repoRoot, worktreePath) {
173
- const managedPath = assertManagedWorktreePath(repoRoot, worktreePath, {
174
- sid: path.basename(worktreePath),
175
- validateRuntimeTree: false,
176
- });
177
- try {
178
- execFileSync('git', ['worktree', 'remove', managedPath], {
179
- cwd: repoRoot, stdio: 'pipe',
180
- });
181
- } catch (error) {
182
- throw new SessionRunnerError(
183
- 'WORKTREE_FAILED',
184
- `git worktree remove failed: ${
185
- error.stderr ? error.stderr.toString().trim() : error.message
186
- }`,
187
- { cause: error },
188
- );
189
- }
190
- }
191
-
192
- function gitWorktreeRegistry(repoRoot) {
193
- try {
194
- const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
195
- cwd: repoRoot,
196
- encoding: 'utf8',
197
- stdio: 'pipe',
198
- });
199
- return new Set(output
200
- .split(/\r?\n/)
201
- .filter((line) => line.startsWith('worktree '))
202
- .map((line) => physicalCandidate(line.slice('worktree '.length))));
203
- } catch (error) {
204
- throw new SessionRunnerError(
205
- 'WORKTREE_FAILED',
206
- `git worktree registry read failed: ${
207
- error.stderr ? error.stderr.toString().trim() : error.message
208
- }`,
209
- { cause: error },
210
- );
211
- }
212
- }
213
-
214
- function removeExactPrunableWorktreeRegistry(repoRoot, managedPath) {
215
- let reported;
216
- try {
217
- reported = execFileSync('git', ['rev-parse', '--git-common-dir'], {
218
- cwd: repoRoot,
219
- encoding: 'utf8',
220
- stdio: 'pipe',
221
- }).trim();
222
- } catch (error) {
223
- throw new SessionRunnerError(
224
- 'WORKTREE_FAILED',
225
- `cannot resolve Git common directory: ${
226
- error.stderr ? error.stderr.toString().trim() : error.message
227
- }`,
228
- { cause: error },
229
- );
230
- }
231
- const commonDir = fs.realpathSync(
232
- path.isAbsolute(reported) ? reported : path.resolve(repoRoot, reported),
233
- );
234
- const registryRoot = path.join(commonDir, 'worktrees');
235
- const rootStat = fs.lstatSync(registryRoot, { throwIfNoEntry: false });
236
- if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
237
- throw new SessionRunnerError(
238
- 'WORKTREE_FAILED',
239
- `Git still registers ${managedPath}, but its exact worktree metadata root is unavailable`,
240
- );
241
- }
242
- const expected = path.resolve(managedPath);
243
- const matches = [];
244
- for (const entry of fs.readdirSync(registryRoot, { withFileTypes: true })) {
245
- if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
246
- const adminDir = path.join(registryRoot, entry.name);
247
- const gitdirFile = path.join(adminDir, 'gitdir');
248
- const gitdirStat = fs.lstatSync(gitdirFile, { throwIfNoEntry: false });
249
- if (!gitdirStat?.isFile() || gitdirStat.isSymbolicLink()) continue;
250
- const gitdir = fs.readFileSync(gitdirFile, 'utf8').trim();
251
- if (!gitdir) continue;
252
- const recorded = path.dirname(
253
- path.isAbsolute(gitdir) ? path.resolve(gitdir) : path.resolve(adminDir, gitdir),
254
- );
255
- if (recorded === expected) matches.push(adminDir);
256
- }
257
- if (matches.length !== 1) {
258
- throw new SessionRunnerError(
259
- 'WORKTREE_FAILED',
260
- `cannot identify one exact Git registry entry for removed worktree ${managedPath}`,
261
- );
262
- }
263
- fs.rmSync(matches[0], { recursive: true, force: false });
264
- }
265
-
266
- function reconcileRemovedWorktree(repoRoot, worktreePath, { sid } = {}) {
267
- const managedPath = assertManagedWorktreePath(repoRoot, worktreePath, {
268
- sid: sid || path.basename(worktreePath),
269
- validateRuntimeTree: false,
270
- });
271
- const registryKey = physicalCandidate(managedPath);
272
- let registry = gitWorktreeRegistry(repoRoot);
273
- if (fs.existsSync(managedPath)) {
274
- gitWorktreeRemove(repoRoot, managedPath);
275
- } else if (registry.has(registryKey)) {
276
- try {
277
- execFileSync('git', ['worktree', 'remove', '--force', managedPath], {
278
- cwd: repoRoot,
279
- stdio: 'pipe',
280
- });
281
- } catch {
282
- removeExactPrunableWorktreeRegistry(repoRoot, managedPath);
283
- }
284
- }
285
- registry = gitWorktreeRegistry(repoRoot);
286
- if (!fs.existsSync(managedPath) && registry.has(registryKey)) {
287
- removeExactPrunableWorktreeRegistry(repoRoot, managedPath);
288
- registry = gitWorktreeRegistry(repoRoot);
289
- }
290
- if (registry.has(registryKey)) {
291
- throw new SessionRunnerError(
292
- 'WORKTREE_FAILED',
293
- `Git still registers the managed worktree after removal: ${managedPath}`,
294
- );
295
- }
296
- return managedPath;
297
- }
298
-
299
- function authorityLinkIsIgnored(worktreePath) {
300
- try {
301
- execFileSync(
302
- 'git',
303
- ['check-ignore', '--quiet', '--no-index', '--', '.ultra/.runtime'],
304
- { cwd: worktreePath, stdio: 'ignore' },
305
- );
306
- return true;
307
- } catch (error) {
308
- if (error.status === 1) return false;
309
- throw new SessionRunnerError(
310
- 'WORKTREE_AUTHORITY_LINK_FAILED',
311
- `cannot verify the worktree Ultra ignore rule: ${error.message}`,
312
- { cause: error },
313
- );
314
- }
315
- }
316
-
317
- function ensureAuthorityLinkIgnored(worktreePath) {
318
- if (authorityLinkIsIgnored(worktreePath)) return { changed: false, path: null };
319
- let output;
320
- try {
321
- output = execFileSync('git', ['rev-parse', '--git-path', 'info/exclude'], {
322
- cwd: worktreePath,
323
- encoding: 'utf8',
324
- stdio: ['ignore', 'pipe', 'pipe'],
325
- }).trim();
326
- } catch (error) {
327
- throw new SessionRunnerError(
328
- 'WORKTREE_AUTHORITY_LINK_FAILED',
329
- `cannot locate the repository-local exclude file: ${error.message}`,
330
- { cause: error },
331
- );
332
- }
333
- const excludePath = path.isAbsolute(output)
334
- ? output
335
- : path.resolve(worktreePath, output);
336
- let stat = null;
337
- try { stat = fs.lstatSync(excludePath); }
338
- catch (error) {
339
- if (error.code !== 'ENOENT') {
340
- throw new SessionRunnerError(
341
- 'WORKTREE_AUTHORITY_LINK_FAILED',
342
- `cannot inspect ${excludePath}: ${error.message}`,
343
- { cause: error },
344
- );
345
- }
346
- }
347
- if (stat && (stat.isSymbolicLink() || !stat.isFile())) {
348
- throw new SessionRunnerError(
349
- 'WORKTREE_AUTHORITY_NOT_IGNORED',
350
- `refusing to update unsafe Git exclude path: ${excludePath}`,
351
- );
352
- }
353
- const before = stat ? fs.readFileSync(excludePath, 'utf8') : '';
354
- const prefix = before && !before.endsWith('\n') ? `${before}\n` : before;
355
- const separator = prefix && !prefix.endsWith('\n\n') ? '\n' : '';
356
- try {
357
- fs.mkdirSync(path.dirname(excludePath), { recursive: true });
358
- fs.writeFileSync(
359
- excludePath,
360
- `${prefix}${separator}# Ultra Builder Pro shared session runtime\n/.ultra/.runtime\n`,
361
- );
362
- if (!authorityLinkIsIgnored(worktreePath)) {
363
- throw new Error('Git still does not ignore the session authority link');
364
- }
365
- } catch (error) {
366
- try {
367
- if (stat) fs.writeFileSync(excludePath, before);
368
- else fs.rmSync(excludePath, { force: true });
369
- } catch { /* best effort */ }
370
- throw new SessionRunnerError(
371
- 'WORKTREE_AUTHORITY_NOT_IGNORED',
372
- `cannot establish a safe local ignore rule for .ultra/.runtime: ${error.message}`,
373
- { cause: error },
374
- );
375
- }
376
- return { changed: true, path: excludePath };
377
- }
378
-
379
- function linkAuthorityIntoWorktree(repoRoot, worktreePath, {
380
- authority: preadmittedAuthority = null,
381
- } = {}) {
382
- let authority = preadmittedAuthority;
383
- if (!authority) {
384
- try {
385
- authority = runtimePaths.ensureRuntimeState(repoRoot, {
386
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
387
- });
388
- } catch (error) {
389
- throw new SessionRunnerError(
390
- 'WORKTREE_AUTHORITY_NOT_IGNORED',
391
- `cannot establish the project Ultra storage boundary: ${error.message}`,
392
- { cause: error },
393
- );
394
- }
395
- }
396
- const authorityDir = authority.runtimeDir;
397
- const stateDbPath = authority.stateDbPath;
398
- const worktreeUltraDir = path.join(worktreePath, '.ultra');
399
- const linkPath = path.join(worktreeUltraDir, '.runtime');
400
- if (!fs.existsSync(stateDbPath)) {
401
- throw new SessionRunnerError(
402
- 'WORKTREE_AUTHORITY_MISSING',
403
- `central Ultra authority is missing at ${stateDbPath}`,
404
- );
405
- }
406
- ensureAuthorityLinkIgnored(worktreePath);
407
- try {
408
- fs.mkdirSync(worktreeUltraDir, { recursive: true });
409
- if (fs.existsSync(linkPath) || fs.lstatSync(linkPath, { throwIfNoEntry: false })) {
410
- throw new SessionRunnerError(
411
- 'WORKTREE_AUTHORITY_CONFLICT',
412
- `worktree already contains an .ultra/.runtime entry at ${linkPath}`,
413
- );
414
- }
415
- fs.symlinkSync(
416
- process.platform === 'win32' ? authorityDir : path.relative(worktreeUltraDir, authorityDir),
417
- linkPath,
418
- process.platform === 'win32' ? 'junction' : 'dir',
419
- );
420
- if (fs.realpathSync(path.join(linkPath, 'state.db')) !== fs.realpathSync(stateDbPath)) {
421
- throw new Error('linked state.db does not resolve to the central authority');
422
- }
423
- try {
424
- gitBootstrap.assertUltraStorageBoundary(worktreePath);
425
- } catch (error) {
426
- fs.rmSync(linkPath, { force: true });
427
- throw new SessionRunnerError(
428
- 'WORKTREE_STORAGE_BOUNDARY_STALE',
429
- `selected worktree revision does not preserve Ultra semantic artifacts: ${error.message}`,
430
- { cause: error },
431
- );
432
- }
433
- return { authorityDir, stateDbPath, linkPath };
434
- } catch (error) {
435
- throw error instanceof SessionRunnerError ? error : new SessionRunnerError(
436
- 'WORKTREE_AUTHORITY_LINK_FAILED',
437
- `cannot link the worktree to central Ultra authority: ${error.message}`,
438
- { cause: error },
439
- );
440
- }
441
- }
442
-
443
- function worktreeRemovalBlocker(repoRoot, worktreePath) {
444
- const managedPath = assertManagedWorktreePath(repoRoot, worktreePath, {
445
- sid: path.basename(worktreePath),
446
- });
447
- if (!fs.existsSync(managedPath)) return null;
448
- try {
449
- const status = execFileSync('git', ['status', '--porcelain=v1'], {
450
- cwd: managedPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
451
- }).trim();
452
- if (status) return 'worktree contains uncommitted changes';
453
- const sessionHead = execFileSync('git', ['rev-parse', 'HEAD'], {
454
- cwd: managedPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
455
- }).trim();
456
- const repositoryHead = execFileSync('git', ['rev-parse', 'HEAD'], {
457
- cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
458
- }).trim();
459
- execFileSync(
460
- 'git', ['merge-base', '--is-ancestor', sessionHead, repositoryHead],
461
- { cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'] },
462
- );
463
- return null;
464
- } catch (error) {
465
- if (error.status === 1) return 'worktree commit has not been integrated into the current checkout';
466
- return `worktree integration could not be verified: ${error.message}`;
467
- }
468
- }
469
-
470
- function changeTaskIntegrationBlocker(db, task, worktreePath) {
471
- if (!task?.change_id) return null;
472
- let worktreeHead;
473
- try {
474
- worktreeHead = execFileSync('git', ['rev-parse', 'HEAD'], {
475
- cwd: worktreePath,
476
- encoding: 'utf8',
477
- stdio: ['ignore', 'pipe', 'pipe'],
478
- }).trim();
479
- } catch (error) {
480
- return {
481
- code: 'WORKTREE_HEAD_UNAVAILABLE',
482
- message: `cannot verify task worktree HEAD: ${error.message}`,
483
- };
484
- }
485
- if (!task.completion_commit || task.completion_commit !== worktreeHead) {
486
- return {
487
- code: 'TASK_COMPLETION_COMMIT_STALE',
488
- message: `task ${task.id} completion commit does not match worktree HEAD`,
489
- };
490
- }
491
- const dev = checkpoints.currentCheckpoint(
492
- db,
493
- 'dev',
494
- { task_id: task.id },
495
- { includeDraft: false },
496
- );
497
- if (!dev || checkpoints.checkpointDigest(dev) !== dev.digest) {
498
- return {
499
- code: 'DEV_CHECKPOINT_NOT_ACCEPTED',
500
- message: `task ${task.id} has no current accepted Dev checkpoint`,
501
- };
502
- }
503
- const review = checkpoints.currentCheckpoint(
504
- db,
505
- 'review',
506
- { task_id: task.id },
507
- { includeDraft: false },
508
- );
509
- if (!review || checkpoints.checkpointDigest(review) !== review.digest) {
510
- return {
511
- code: 'REVIEW_CHECKPOINT_NOT_ACCEPTED',
512
- message: `task ${task.id} has no current accepted Review checkpoint`,
513
- };
514
- }
515
- return null;
516
- }
517
-
518
- function physicalCandidate(candidate) {
519
- let existing = candidate;
520
- while (!fs.lstatSync(existing, { throwIfNoEntry: false })) {
521
- const parent = path.dirname(existing);
522
- if (parent === existing) break;
523
- existing = parent;
524
- }
525
- const physicalExisting = fs.realpathSync(existing);
526
- return path.resolve(physicalExisting, path.relative(existing, candidate));
527
- }
528
-
529
- function resolveWorktreeBase(repoRoot, requestedBase) {
530
- const managedBase = runtimePaths.pathsFor(repoRoot).worktreesDir;
531
- const resolved = requestedBase
532
- ? path.resolve(repoRoot, requestedBase)
533
- : managedBase;
534
- const physicalManaged = physicalCandidate(managedBase);
535
- const physicalResolved = physicalCandidate(resolved);
536
- if (resolved !== path.resolve(managedBase) || physicalResolved !== physicalManaged) {
537
- throw new SessionRunnerError(
538
- 'WORKTREE_SCOPE_INVALID',
539
- `worktree_base must be the canonical managed root ${managedBase}`,
540
- );
541
- }
542
- return resolved;
543
- }
544
-
545
- function mintSessionId() {
546
- return `sess-${randomUUID().slice(0, 8)}`;
547
- }
548
-
549
- function assertSessionTaskReady(db, repoRoot, taskId) {
550
- const task = ops.readTask(db, taskId);
551
- if (!task) return null; // admissionCheck reports the canonical TASK_NOT_FOUND error.
552
- if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
553
- throw new SessionRunnerError(
554
- 'TASK_NOT_EXECUTABLE',
555
- `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
556
- );
557
- }
558
- return task;
559
- }
560
-
561
- function assertKernelSessionTaskReady(db, repoRoot, taskId, packetDigest) {
562
- const task = ops.readTask(db, taskId);
563
- if (!task) {
564
- throw new SessionRunnerError('TASK_NOT_FOUND', `task ${taskId} not found`);
565
- }
566
- if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
567
- throw new SessionRunnerError(
568
- 'TASK_NOT_EXECUTABLE',
569
- `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
570
- );
571
- }
572
- const packet = packetDigest
573
- ? db.prepare(
574
- `SELECT id FROM worker_packets
575
- WHERE packet_digest = ? AND scope_type = 'task' AND scope_id = ?
576
- AND status IN ('pending', 'assigned')`,
577
- ).get(packetDigest, task.id)
578
- : null;
579
- if (!packet) {
580
- throw new SessionRunnerError(
581
- 'WORKER_PACKET_REQUIRED',
582
- `task ${task.id} requires the exact Worker Packet prepared for this session`,
583
- );
584
- }
585
- workerPackets.readWorkerPacket(db, packet.id, { rootDir: repoRoot });
586
- return task;
587
- }
588
-
589
- function admissionCheck(db, repoRoot, taskId, options = {}) {
590
- assertSessionTaskReady(db, repoRoot, taskId);
591
- const pendingClose = closeJournal.findForTask(repoRoot, taskId);
592
- if (pendingClose) {
593
- return {
594
- can_spawn: false,
595
- conflict: {
596
- sid: pendingClose.sid,
597
- status: 'closing',
598
- heartbeat_age_ms: 0,
599
- },
600
- recommended_action: 'recover_close',
601
- };
602
- }
603
- return ops.admissionCheck(db, taskId, options);
604
- }
605
-
606
- function startSessionProcess({
607
- db,
608
- repoRoot,
609
- sid,
610
- command,
611
- args = [],
612
- env = {},
613
- stdio = 'ignore',
614
- authority = null,
615
- }) {
616
- if (!db || !repoRoot || !sid || typeof command !== 'string' || !command.trim()) {
617
- throw new SessionRunnerError(
618
- 'VALIDATION_ERROR',
619
- 'db, repoRoot, sid, and an executable command are required',
620
- );
621
- }
622
- if (!Array.isArray(args)) {
623
- throw new SessionRunnerError('VALIDATION_ERROR', 'session process args must be an array');
624
- }
625
- const session = ops.readSession(db, sid);
626
- if (!session) throw new SessionRunnerError('SESSION_NOT_FOUND', `session ${sid} not found`);
627
- if (session.status !== 'running' || session.pid !== null) {
628
- throw new SessionRunnerError(
629
- 'SESSION_NOT_STARTABLE',
630
- `session ${sid} cannot start from status=${session.status}, pid=${String(session.pid)}`,
631
- );
632
- }
633
- const worktreePath = assertManagedWorktreePath(repoRoot, session.worktree_path, { sid });
634
- const resolvedAuthority = authority || runtimePaths.ensureRuntimeState(repoRoot, {
635
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
636
- });
637
- let proc;
638
- try {
639
- proc = spawn(command, args, {
640
- cwd: worktreePath,
641
- env: {
642
- ...process.env,
643
- ...env,
644
- UBP_SESSION_ID: sid,
645
- UBP_TASK_ID: session.task_id,
646
- UBP_RUNTIME: session.runtime,
647
- UBP_WORKTREE: worktreePath,
648
- UBP_ARTIFACT_DIR: session.artifact_dir,
649
- UBP_DB_PATH: resolvedAuthority.stateDbPath,
650
- UBP_ROOT_DIR: worktreePath,
651
- UBP_AUTHORITY_ROOT: repoRoot,
652
- },
653
- stdio,
654
- detached: false,
655
- });
656
- if (Number.isInteger(proc.pid)) {
657
- ops.updateSession(db, sid, { pid: proc.pid });
658
- }
659
- return proc;
660
- } catch (error) {
661
- try { if (proc && !proc.killed) proc.kill('SIGTERM'); } catch { /* best effort */ }
662
- try { ops.updateSession(db, sid, { status: 'crashed' }); } catch { /* preserve original error */ }
663
- throw error;
664
- }
665
- }
666
-
667
- function spawnSession({
668
- db,
669
- repoRoot,
670
- task_id,
671
- runtime,
672
- authority: preadmittedAuthority = null,
673
- takeover = false,
674
- command = null,
675
- args = [],
676
- env = {},
677
- stdio = 'ignore',
678
- worktreeRef = 'HEAD',
679
- worktree_base = null,
680
- lease_seconds = 1800,
681
- mark_task_started = false,
682
- takeover_timeout_ms = 5000,
683
- takeover_poll_ms = 25,
684
- kernel_mode = false,
685
- packet_digest = null,
686
- }) {
687
- if (!db) throw new SessionRunnerError('VALIDATION_ERROR', 'db handle required');
688
- if (!repoRoot) throw new SessionRunnerError('VALIDATION_ERROR', 'repoRoot required');
689
- if (!task_id) throw new SessionRunnerError('VALIDATION_ERROR', 'task_id required');
690
- if (!runtime) throw new SessionRunnerError('VALIDATION_ERROR', 'runtime required');
691
- const worktreeBase = resolveWorktreeBase(repoRoot, worktree_base);
692
-
693
- // 1. Admission check
694
- const verdict = ops.admissionCheck(db, task_id);
695
- // Circuit breaker trumps takeover — tripped tasks require explicit reset.
696
- if (verdict.recommended_action === 'blocked_by_breaker') {
697
- const err = new SessionRunnerError(
698
- 'CIRCUIT_TRIPPED',
699
- `task ${task_id} is tripped by circuit breaker; call resetCircuitBreaker before spawning`,
700
- );
701
- err.verdict = verdict;
702
- throw err;
703
- }
704
- // Validate workflow authority before takeover can terminate the prior worker
705
- // or any filesystem mutation creates a new worktree.
706
- if (kernel_mode) assertKernelSessionTaskReady(db, repoRoot, task_id, packet_digest);
707
- else assertSessionTaskReady(db, repoRoot, task_id);
708
- let authority = preadmittedAuthority;
709
- if (!authority) {
710
- try {
711
- authority = runtimePaths.ensureRuntimeState(repoRoot, {
712
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
713
- });
714
- } catch (error) {
715
- throw new SessionRunnerError(
716
- 'WORKTREE_AUTHORITY_NOT_IGNORED',
717
- `cannot establish the project Ultra storage boundary: ${error.message}`,
718
- { cause: error },
719
- );
720
- }
721
- }
722
- const pendingClose = closeJournal.findForTask(repoRoot, task_id);
723
- if (pendingClose) {
724
- throw new SessionRunnerError(
725
- 'SESSION_CLOSE_PENDING',
726
- `task ${task_id} has an unfinished close intent for session ${pendingClose.sid}; recover it before spawning or taking over`,
727
- );
728
- }
729
- if (!verdict.can_spawn && !takeover) {
730
- const err = new SessionRunnerError(
731
- 'ADMISSION_DENIED',
732
- `active session exists for task ${task_id} (${verdict.conflict && verdict.conflict.sid}); recommended=${verdict.recommended_action}`,
733
- );
734
- err.verdict = verdict;
735
- throw err;
736
- }
737
- if (!verdict.can_spawn && takeover && verdict.conflict) {
738
- const priorSession = ops.readSession(db, verdict.conflict.sid);
739
- if (priorSession?.pid) {
740
- try { process.kill(priorSession.pid, 'SIGTERM'); }
741
- catch (error) {
742
- if (error.code !== 'ESRCH') {
743
- throw new SessionRunnerError(
744
- 'TAKEOVER_FAILED',
745
- `cannot terminate prior session ${verdict.conflict.sid}: ${error.message}`,
746
- );
747
- }
748
- }
749
- if (!waitForProcessExit(priorSession.pid, {
750
- timeoutMs: takeover_timeout_ms,
751
- pollMs: takeover_poll_ms,
752
- })) {
753
- throw new SessionRunnerError(
754
- 'TAKEOVER_FAILED',
755
- `prior session ${verdict.conflict.sid} process ${priorSession.pid} did not exit; its lease remains authoritative`,
756
- );
757
- }
758
- }
759
- ops.updateSession(db, verdict.conflict.sid, { status: 'crashed' });
760
- }
761
-
762
- // 2. Mint sid + paths
763
- const sid = mintSessionId();
764
- const worktree_path = path.join(worktreeBase, sid);
765
- const artifact_dir = path.join(runtimePaths.pathsFor(repoRoot).sessionsDir, sid);
766
-
767
- // 3. Git worktree (throws WORKTREE_FAILED on failure)
768
- gitWorktreeAdd(repoRoot, worktree_path, worktreeRef);
769
- try {
770
- authority = linkAuthorityIntoWorktree(repoRoot, worktree_path, { authority });
771
- fs.mkdirSync(artifact_dir, { recursive: true });
772
- } catch (error) {
773
- try {
774
- fs.rmSync(path.join(worktree_path, '.ultra', '.runtime'), { force: true });
775
- } catch { /* best effort */ }
776
- gitWorktreeRemove(repoRoot, worktree_path);
777
- throw error;
778
- }
779
-
780
- // 4. Persist the task/worktree binding before any worker can run a hook or
781
- // resolve UBP_DB_PATH. This is the anti-spoof authority hand-off.
782
- let session;
783
- try {
784
- session = ops.createSession(db, {
785
- sid,
786
- task_id,
787
- runtime,
788
- pid: null,
789
- worktree_path,
790
- artifact_dir,
791
- lease_seconds,
792
- });
793
- } catch (err) {
794
- // state.db insert failed — no worker has started; roll back the worktree.
795
- gitWorktreeRemove(repoRoot, worktree_path);
796
- try { fs.rmSync(artifact_dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
797
- throw err;
798
- }
799
-
800
- // Automated dispatch must publish the running task state before the worker
801
- // can observe or complete it. Manual host-owned sessions retain the caller's
802
- // task state unless they explicitly opt into this ordering contract.
803
- try {
804
- if (mark_task_started && ops.readTask(db, task_id)?.status === 'pending') {
805
- ops.updateTaskStatus(db, task_id, 'in_progress');
806
- }
807
- } catch (error) {
808
- try { ops.updateSession(db, sid, { status: 'crashed' }); } catch { /* preserve original error */ }
809
- try { gitWorktreeRemove(repoRoot, worktree_path); } catch { /* recovery keeps evidence */ }
810
- try { fs.rmSync(artifact_dir, { recursive: true, force: true }); } catch { /* ignore */ }
811
- throw error;
812
- }
813
-
814
- // 5. Spawn child process only after the durable binding exists. The active
815
- // host may also consume the returned worktree without a child process.
816
- let proc = null;
817
- try {
818
- if (command) {
819
- proc = startSessionProcess({
820
- db,
821
- repoRoot,
822
- sid,
823
- command,
824
- args,
825
- env,
826
- stdio,
827
- authority,
828
- });
829
- session = ops.readSession(db, sid);
830
- }
831
- } catch (error) {
832
- ops.updateSession(db, sid, { status: 'crashed' });
833
- try { gitWorktreeRemove(repoRoot, worktree_path); } catch { /* recovery keeps evidence */ }
834
- throw error;
835
- }
836
-
837
- return {
838
- sid,
839
- worktree_path,
840
- artifact_dir,
841
- authority_db_path: authority.stateDbPath,
842
- lease_expires_at: session.lease_expires_at,
843
- pid: proc ? proc.pid : null,
844
- process: proc,
845
- };
846
- }
847
-
848
- function processIsExecuting(pid) {
849
- if (!Number.isInteger(pid) || pid <= 0) return false;
850
- try {
851
- process.kill(pid, 0);
852
- } catch (error) {
853
- if (error.code === 'ESRCH') return false;
854
- if (error.code !== 'EPERM') throw error;
855
- }
856
- try {
857
- const state = execFileSync('ps', ['-o', 'stat=', '-p', String(pid)], {
858
- encoding: 'utf8',
859
- stdio: ['ignore', 'pipe', 'ignore'],
860
- }).trim();
861
- return Boolean(state) && !state.startsWith('Z');
862
- } catch {
863
- // A failed ps lookup after signal 0 normally means the process vanished.
864
- try {
865
- process.kill(pid, 0);
866
- return true;
867
- } catch (error) {
868
- return error.code !== 'ESRCH';
869
- }
870
- }
871
- }
872
-
873
- function waitForProcessExit(pid, {
874
- timeoutMs = 5000,
875
- pollMs = 25,
876
- } = {}) {
877
- const deadline = Date.now() + timeoutMs;
878
- while (processIsExecuting(pid)) {
879
- if (Date.now() >= deadline) return false;
880
- Atomics.wait(
881
- new Int32Array(new SharedArrayBuffer(4)),
882
- 0,
883
- 0,
884
- Math.min(pollMs, Math.max(1, deadline - Date.now())),
885
- );
886
- }
887
- return true;
888
- }
889
-
890
- function closeSession({
891
- db,
892
- repoRoot,
893
- sid,
894
- }, {
895
- status = 'completed',
896
- remove_worktree = false,
897
- kill_signal = 'SIGTERM',
898
- autoMerge = false,
899
- mergeBaseBranch = 'main',
900
- kill_timeout_ms = 5000,
901
- kill_poll_ms = 25,
902
- } = {}) {
903
- if (!db || !sid) throw new SessionRunnerError('VALIDATION_ERROR', 'db + sid required');
904
- if (!closeJournal.TERMINAL_STATUSES.has(status)) {
905
- throw new SessionRunnerError(
906
- 'VALIDATION_ERROR',
907
- `session close status must be completed or crashed, got ${String(status)}`,
908
- );
909
- }
910
- const session = ops.readSession(db, sid);
911
- if (!session) {
912
- throw new SessionRunnerError('SESSION_NOT_FOUND', `session ${sid} not found`);
913
- }
914
- if (closeJournal.TERMINAL_STATUSES.has(session.status)) {
915
- if (session.status !== status) {
916
- throw new SessionRunnerError(
917
- 'SESSION_TERMINAL_CONFLICT',
918
- `session ${sid} is already ${session.status} and cannot be closed as ${status}`,
919
- );
920
- }
921
- // A terminal row may contain a legacy PID that has since been reused by an
922
- // unrelated process. Terminal authority is idempotent: never signal that
923
- // PID, and clear it before reconciling any durable close intent.
924
- if (session.pid !== null) ops.updateSession(db, sid, { pid: null });
925
- const intent = repoRoot ? closeJournal.read(repoRoot, sid) : null;
926
- if (intent) {
927
- if (intent.requested_status !== status
928
- || intent.task_id !== session.task_id
929
- || path.resolve(intent.worktree_path) !== path.resolve(session.worktree_path)) {
930
- throw new SessionRunnerError(
931
- 'SESSION_CLOSE_JOURNAL_CONFLICT',
932
- `session ${sid} terminal state conflicts with its durable close intent`,
933
- );
934
- }
935
- }
936
- if ((intent || remove_worktree) && repoRoot && session.worktree_path) {
937
- assertManagedWorktreePath(repoRoot, session.worktree_path, { sid: session.sid });
938
- if (fs.existsSync(session.worktree_path)) {
939
- const blocker = worktreeRemovalBlocker(repoRoot, session.worktree_path);
940
- if (blocker) {
941
- throw new SessionRunnerError(
942
- 'WORKTREE_NOT_INTEGRATED',
943
- `refusing to remove ${session.worktree_path}: ${blocker}`,
944
- );
945
- }
946
- }
947
- if (!intent) {
948
- closeJournal.prepare(repoRoot, {
949
- sid,
950
- task_id: session.task_id,
951
- requested_status: status,
952
- worktree_path: session.worktree_path,
953
- });
954
- }
955
- try {
956
- reconcileRemovedWorktree(repoRoot, session.worktree_path, { sid: session.sid });
957
- closeJournal.update(repoRoot, sid, {
958
- phase: 'worktree_removed',
959
- error: null,
960
- });
961
- closeJournal.discard(repoRoot, sid);
962
- } catch (error) {
963
- closeJournal.update(repoRoot, sid, {
964
- phase: 'recovery_failed',
965
- error: error.message,
966
- });
967
- throw error;
968
- }
969
- }
970
- return {
971
- sid,
972
- status,
973
- worktree_preserved: Boolean(
974
- session.worktree_path && fs.existsSync(session.worktree_path),
975
- ),
976
- };
977
- }
978
- if ((remove_worktree || autoMerge) && session.worktree_path) {
979
- assertManagedWorktreePath(repoRoot, session.worktree_path, { sid: session.sid });
980
- }
981
- let closeIntentPrepared = false;
982
- if (remove_worktree && !autoMerge && repoRoot && session.worktree_path) {
983
- closeJournal.prepare(repoRoot, {
984
- sid,
985
- task_id: session.task_id,
986
- requested_status: status,
987
- worktree_path: session.worktree_path,
988
- });
989
- closeIntentPrepared = true;
990
- }
991
- if (session.pid) {
992
- try {
993
- process.kill(session.pid, kill_signal);
994
- } catch (error) {
995
- if (error.code !== 'ESRCH') {
996
- throw new SessionRunnerError(
997
- 'SESSION_TERMINATION_FAILED',
998
- `cannot signal session ${sid} process ${session.pid}: ${error.message}`,
999
- { cause: error },
1000
- );
1001
- }
1002
- }
1003
- if (!waitForProcessExit(session.pid, {
1004
- timeoutMs: kill_timeout_ms,
1005
- pollMs: kill_poll_ms,
1006
- })) {
1007
- if (closeIntentPrepared) {
1008
- closeJournal.update(repoRoot, sid, {
1009
- phase: 'worker_running',
1010
- error: `process ${session.pid} did not exit after ${kill_timeout_ms}ms`,
1011
- });
1012
- }
1013
- throw new SessionRunnerError(
1014
- 'SESSION_TERMINATION_TIMEOUT',
1015
- `session ${sid} process ${session.pid} did not exit after ${kill_timeout_ms}ms; lease and worktree were preserved`,
1016
- );
1017
- }
1018
- // The owned worker is gone. Clear its reusable OS identifier even if a
1019
- // later merge or worktree gate keeps this close intent recoverable.
1020
- ops.updateSession(db, sid, { pid: null });
1021
- }
1022
- // Phase 8B.4 — opt-in auto-merge. Conflict preserves worktree for the
1023
- // human; clean/no-op merges fall through to normal removal below.
1024
- let mergeResult;
1025
- let effectiveRemove = remove_worktree;
1026
- if (autoMerge && status === 'completed' && repoRoot && session.worktree_path) {
1027
- const task = session.task_id ? ops.readTask(db, session.task_id) : null;
1028
- if (!task || task.status !== 'completed') {
1029
- mergeResult = {
1030
- merged: false,
1031
- reason: 'task_not_completed',
1032
- task_status: task?.status || null,
1033
- };
1034
- effectiveRemove = false;
1035
- } else {
1036
- const integrationBlocker = changeTaskIntegrationBlocker(
1037
- db,
1038
- task,
1039
- session.worktree_path,
1040
- );
1041
- if (integrationBlocker) {
1042
- mergeResult = {
1043
- merged: false,
1044
- reason: 'workflow_gates_open',
1045
- blocker: integrationBlocker.code,
1046
- message: integrationBlocker.message,
1047
- };
1048
- effectiveRemove = false;
1049
- } else {
1050
- const autoMergeMod = require('./auto-merge.cjs');
1051
- mergeResult = autoMergeMod.autoMerge({
1052
- repoRoot,
1053
- worktreePath: session.worktree_path,
1054
- baseBranch: mergeBaseBranch,
1055
- sid,
1056
- task_id: session.task_id,
1057
- db,
1058
- });
1059
- effectiveRemove = Boolean(
1060
- mergeResult.merged || mergeResult.reason === 'no_changes',
1061
- );
1062
- }
1063
- }
1064
- }
1065
- if (effectiveRemove && repoRoot && session.worktree_path) {
1066
- const blocker = worktreeRemovalBlocker(repoRoot, session.worktree_path);
1067
- if (blocker) {
1068
- throw new SessionRunnerError(
1069
- 'WORKTREE_NOT_INTEGRATED',
1070
- `refusing to remove ${session.worktree_path}: ${blocker}`,
1071
- );
1072
- }
1073
- if (!closeIntentPrepared) {
1074
- closeJournal.prepare(repoRoot, {
1075
- sid,
1076
- task_id: session.task_id,
1077
- requested_status: status,
1078
- worktree_path: session.worktree_path,
1079
- });
1080
- closeIntentPrepared = true;
1081
- }
1082
- try {
1083
- gitWorktreeRemove(repoRoot, session.worktree_path);
1084
- closeJournal.update(repoRoot, sid, {
1085
- phase: 'worktree_removed',
1086
- error: null,
1087
- });
1088
- } catch (error) {
1089
- closeJournal.update(repoRoot, sid, {
1090
- phase: 'removal_failed',
1091
- error: error.message,
1092
- });
1093
- throw error;
1094
- }
1095
- }
1096
- ops.updateSession(db, sid, { status });
1097
- if (effectiveRemove && repoRoot && session.worktree_path) {
1098
- closeJournal.discard(repoRoot, sid);
1099
- }
1100
- const out = {
1101
- sid,
1102
- status,
1103
- worktree_preserved: Boolean(
1104
- session.worktree_path && fs.existsSync(session.worktree_path),
1105
- ),
1106
- };
1107
- if (mergeResult) out.merge = mergeResult;
1108
- return out;
1109
- }
1110
-
1111
- function attachHeartbeat(db, sid, { intervalMs = 30000 } = {}) {
1112
- if (!db || !sid) throw new SessionRunnerError('VALIDATION_ERROR', 'db + sid required');
1113
- const timer = setInterval(() => {
1114
- try { ops.heartbeatSession(db, sid); }
1115
- catch (_err) { clearInterval(timer); }
1116
- }, intervalMs);
1117
- if (typeof timer.unref === 'function') timer.unref();
1118
- return () => clearInterval(timer);
1119
- }
1120
-
1121
- module.exports = {
1122
- SessionRunnerError,
1123
- EXPECTED_EXECUTION_GATE_CODES,
1124
- findExpectedExecutionGate,
1125
- isExpectedExecutionGate,
1126
- spawnSession,
1127
- startSessionProcess,
1128
- closeSession,
1129
- attachHeartbeat,
1130
- admissionCheck,
1131
- assertSessionTaskReady,
1132
- assertKernelSessionTaskReady,
1133
- // exposed for tests
1134
- _internal: {
1135
- gitWorktreeAdd,
1136
- gitWorktreeRemove,
1137
- gitWorktreeRegistry,
1138
- reconcileRemovedWorktree,
1139
- mintSessionId,
1140
- authorityLinkIsIgnored,
1141
- ensureAuthorityLinkIgnored,
1142
- linkAuthorityIntoWorktree,
1143
- worktreeRemovalBlocker,
1144
- changeTaskIntegrationBlocker,
1145
- processIsExecuting,
1146
- waitForProcessExit,
1147
- closeJournal,
1148
- resolveWorktreeBase,
1149
- assertSessionId,
1150
- assertManagedWorktreeRoot,
1151
- assertManagedWorktreePath,
1152
- },
1153
- };