ultra-builder-pro-cli 0.16.0 → 0.17.1

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 (66) hide show
  1. package/.ultra-template/reports/templates/delivery-report.json +17 -0
  2. package/.ultra-template/reports/templates/test-report.json +18 -0
  3. package/.ultra-template/specs/research-distillate.md +4 -3
  4. package/CHANGELOG.md +95 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +10 -5
  7. package/adapters/_shared/handbook.cjs +1 -1
  8. package/adapters/_shared/interaction-contract.cjs +61 -0
  9. package/adapters/claude.js +1 -1
  10. package/adapters/opencode.js +1 -1
  11. package/bin/orchestrator.js +175 -19
  12. package/commands/ultra-change.md +1 -1
  13. package/commands/ultra-deliver.md +3 -3
  14. package/commands/ultra-init.md +1 -1
  15. package/commands/ultra-plan.md +1 -1
  16. package/commands/ultra-research.md +2 -2
  17. package/commands/ultra-review.md +10 -0
  18. package/commands/ultra-status.md +1 -1
  19. package/commands/ultra-test.md +1 -1
  20. package/docs/AGENT-CONTEXT.md +11 -8
  21. package/docs/ARCHITECTURE.md +67 -9
  22. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  23. package/docs/PLAN.zh-CN.md +19 -13
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
  25. package/docs/WORKFLOW-LIFECYCLE.md +123 -59
  26. package/hooks/pre_stop_check.py +3 -2
  27. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  28. package/mcp-server/lib/change-workflow.cjs +55 -35
  29. package/mcp-server/lib/context-spine.cjs +171 -105
  30. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  31. package/mcp-server/lib/init-project.cjs +178 -41
  32. package/mcp-server/lib/project-breadcrumb.cjs +14 -14
  33. package/mcp-server/lib/state-db.cjs +257 -10
  34. package/mcp-server/lib/state-ops.cjs +64 -1
  35. package/mcp-server/lib/workflow-state.cjs +300 -205
  36. package/mcp-server/server.cjs +58 -34
  37. package/orchestrator/auto-merge.cjs +19 -0
  38. package/orchestrator/daemon.cjs +104 -0
  39. package/orchestrator/parallel-orchestrator.cjs +371 -49
  40. package/orchestrator/session-runner.cjs +354 -25
  41. package/package.json +2 -1
  42. package/skills/ultra-change/SKILL.md +69 -94
  43. package/skills/ultra-deliver/SKILL.md +37 -65
  44. package/skills/ultra-dev/SKILL.md +50 -52
  45. package/skills/ultra-doctor/SKILL.md +34 -49
  46. package/skills/ultra-init/SKILL.md +78 -105
  47. package/skills/ultra-plan/SKILL.md +78 -99
  48. package/skills/ultra-research/SKILL.md +99 -113
  49. package/skills/ultra-review/SKILL.md +38 -53
  50. package/skills/ultra-review/references/review-modes.md +5 -3
  51. package/skills/ultra-review/references/unified-schema.md +26 -6
  52. package/skills/ultra-review/scripts/review_wait.py +12 -0
  53. package/skills/ultra-status/SKILL.md +34 -44
  54. package/skills/ultra-test/SKILL.md +61 -54
  55. package/skills/ultra-think/SKILL.md +37 -51
  56. package/skills/ultra-think/references/decision-dialogue.md +87 -111
  57. package/spec/cli-protocol.md +35 -0
  58. package/spec/mcp-tools.yaml +145 -28
  59. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  60. package/spec/schemas/state-db.sql +8 -2
  61. package/spec/schemas/test-report.v1.schema.json +45 -14
  62. package/ultra-tools/cli.cjs +1 -1
  63. package/ultra-tools/commands/session.cjs +10 -3
  64. package/ultra-tools/commands/status.cjs +19 -14
  65. package/ultra-tools/commands/system.cjs +2 -2
  66. package/ultra-tools/commands/task.cjs +3 -0
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
- // Phase 4.5.1 — Session 标准单元 runner.
4
- // Session = new process + independent git worktree + state.db sessions-table
5
- // lease/heartbeat (D20 + D32 — no .ultra/sessions/<sid>/lease.json file).
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.
6
7
  //
7
- // This module is the execution layer. MCP session.* tools record state,
8
- // orchestrator runs the actual git worktree + child_process work.
8
+ // This module is the single session/worktree execution layer shared by MCP
9
+ // session.* tools and the optional orchestrator child-process path.
9
10
  // Callers share a single open state-db connection (WAL supports multi-writer).
10
11
 
11
12
  const fs = require('node:fs');
@@ -14,8 +15,18 @@ const { spawn, execFileSync } = require('node:child_process');
14
15
  const { randomUUID } = require('node:crypto');
15
16
 
16
17
  const ops = require('../mcp-server/lib/state-ops.cjs');
18
+ const workflows = require('../mcp-server/lib/workflow-state.cjs');
17
19
  const skillMiner = require('./skill-miner.cjs');
18
20
 
21
+ const EXPECTED_EXECUTION_GATE_CODES = new Set([
22
+ 'TASK_EXECUTION_CONTRACT_INCOMPLETE',
23
+ 'WORKFLOW_TASK_NOT_EXECUTABLE',
24
+ 'TASK_DEPENDENCIES_INCOMPLETE',
25
+ 'WORKFLOW_PLAN_NOT_COMPLETED',
26
+ 'WORKFLOW_PLAN_TASK_SET_STALE',
27
+ 'WORKFLOW_PLAN_TASK_CONTRACT_STALE',
28
+ ]);
29
+
19
30
  class SessionRunnerError extends Error {
20
31
  constructor(code, message, { cause } = {}) {
21
32
  super(message);
@@ -25,6 +36,10 @@ class SessionRunnerError extends Error {
25
36
  }
26
37
  }
27
38
 
39
+ function isExpectedExecutionGate(error) {
40
+ return Boolean(error && EXPECTED_EXECUTION_GATE_CODES.has(error.code));
41
+ }
42
+
28
43
  function gitWorktreeAdd(repoRoot, worktreePath, ref = 'HEAD') {
29
44
  fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
30
45
  try {
@@ -49,10 +64,240 @@ function gitWorktreeRemove(repoRoot, worktreePath) {
49
64
  }
50
65
  }
51
66
 
67
+ function authorityLinkIsIgnored(worktreePath) {
68
+ try {
69
+ execFileSync(
70
+ 'git',
71
+ ['check-ignore', '--quiet', '--no-index', '--', '.ultra'],
72
+ { cwd: worktreePath, stdio: 'ignore' },
73
+ );
74
+ return true;
75
+ } catch (error) {
76
+ if (error.status === 1) return false;
77
+ throw new SessionRunnerError(
78
+ 'WORKTREE_AUTHORITY_LINK_FAILED',
79
+ `cannot verify the worktree Ultra ignore rule: ${error.message}`,
80
+ { cause: error },
81
+ );
82
+ }
83
+ }
84
+
85
+ function ensureAuthorityLinkIgnored(worktreePath) {
86
+ if (authorityLinkIsIgnored(worktreePath)) return { changed: false, path: null };
87
+ let output;
88
+ try {
89
+ output = execFileSync('git', ['rev-parse', '--git-path', 'info/exclude'], {
90
+ cwd: worktreePath,
91
+ encoding: 'utf8',
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ }).trim();
94
+ } catch (error) {
95
+ throw new SessionRunnerError(
96
+ 'WORKTREE_AUTHORITY_LINK_FAILED',
97
+ `cannot locate the repository-local exclude file: ${error.message}`,
98
+ { cause: error },
99
+ );
100
+ }
101
+ const excludePath = path.isAbsolute(output)
102
+ ? output
103
+ : path.resolve(worktreePath, output);
104
+ let stat = null;
105
+ try { stat = fs.lstatSync(excludePath); }
106
+ catch (error) {
107
+ if (error.code !== 'ENOENT') {
108
+ throw new SessionRunnerError(
109
+ 'WORKTREE_AUTHORITY_LINK_FAILED',
110
+ `cannot inspect ${excludePath}: ${error.message}`,
111
+ { cause: error },
112
+ );
113
+ }
114
+ }
115
+ if (stat && (stat.isSymbolicLink() || !stat.isFile())) {
116
+ throw new SessionRunnerError(
117
+ 'WORKTREE_AUTHORITY_NOT_IGNORED',
118
+ `refusing to update unsafe Git exclude path: ${excludePath}`,
119
+ );
120
+ }
121
+ const before = stat ? fs.readFileSync(excludePath, 'utf8') : '';
122
+ const prefix = before && !before.endsWith('\n') ? `${before}\n` : before;
123
+ const separator = prefix && !prefix.endsWith('\n\n') ? '\n' : '';
124
+ try {
125
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
126
+ fs.writeFileSync(
127
+ excludePath,
128
+ `${prefix}${separator}# Ultra Builder Pro session authority\n.ultra\n`,
129
+ );
130
+ if (!authorityLinkIsIgnored(worktreePath)) {
131
+ throw new Error('Git still does not ignore the session authority link');
132
+ }
133
+ } catch (error) {
134
+ try {
135
+ if (stat) fs.writeFileSync(excludePath, before);
136
+ else fs.rmSync(excludePath, { force: true });
137
+ } catch { /* best effort */ }
138
+ throw new SessionRunnerError(
139
+ 'WORKTREE_AUTHORITY_NOT_IGNORED',
140
+ `cannot establish a safe local ignore rule for .ultra: ${error.message}`,
141
+ { cause: error },
142
+ );
143
+ }
144
+ return { changed: true, path: excludePath };
145
+ }
146
+
147
+ function linkAuthorityIntoWorktree(repoRoot, worktreePath) {
148
+ const authorityDir = path.resolve(repoRoot, '.ultra');
149
+ const stateDbPath = path.join(authorityDir, 'state.db');
150
+ const linkPath = path.join(worktreePath, '.ultra');
151
+ if (!fs.existsSync(stateDbPath)) {
152
+ throw new SessionRunnerError(
153
+ 'WORKTREE_AUTHORITY_MISSING',
154
+ `central Ultra authority is missing at ${stateDbPath}`,
155
+ );
156
+ }
157
+ ensureAuthorityLinkIgnored(worktreePath);
158
+ try {
159
+ if (fs.existsSync(linkPath) || fs.lstatSync(linkPath, { throwIfNoEntry: false })) {
160
+ throw new SessionRunnerError(
161
+ 'WORKTREE_AUTHORITY_CONFLICT',
162
+ `worktree already contains an .ultra entry at ${linkPath}`,
163
+ );
164
+ }
165
+ fs.symlinkSync(
166
+ process.platform === 'win32' ? authorityDir : path.relative(worktreePath, authorityDir),
167
+ linkPath,
168
+ process.platform === 'win32' ? 'junction' : 'dir',
169
+ );
170
+ if (fs.realpathSync(path.join(linkPath, 'state.db')) !== fs.realpathSync(stateDbPath)) {
171
+ throw new Error('linked state.db does not resolve to the central authority');
172
+ }
173
+ return { authorityDir, stateDbPath, linkPath };
174
+ } catch (error) {
175
+ throw error instanceof SessionRunnerError ? error : new SessionRunnerError(
176
+ 'WORKTREE_AUTHORITY_LINK_FAILED',
177
+ `cannot link the worktree to central Ultra authority: ${error.message}`,
178
+ { cause: error },
179
+ );
180
+ }
181
+ }
182
+
183
+ function worktreeRemovalBlocker(repoRoot, worktreePath) {
184
+ if (!fs.existsSync(worktreePath)) return null;
185
+ try {
186
+ const status = execFileSync('git', ['status', '--porcelain=v1'], {
187
+ cwd: worktreePath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
188
+ }).trim();
189
+ if (status) return 'worktree contains uncommitted changes';
190
+ const sessionHead = execFileSync('git', ['rev-parse', 'HEAD'], {
191
+ cwd: worktreePath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
192
+ }).trim();
193
+ const repositoryHead = execFileSync('git', ['rev-parse', 'HEAD'], {
194
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
195
+ }).trim();
196
+ execFileSync(
197
+ 'git', ['merge-base', '--is-ancestor', sessionHead, repositoryHead],
198
+ { cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'] },
199
+ );
200
+ return null;
201
+ } catch (error) {
202
+ if (error.status === 1) return 'worktree commit has not been integrated into the current checkout';
203
+ return `worktree integration could not be verified: ${error.message}`;
204
+ }
205
+ }
206
+
207
+ function changeTaskIntegrationBlocker(db, task, worktreePath) {
208
+ if (!task?.change_id) return null;
209
+ let worktreeHead;
210
+ try {
211
+ worktreeHead = execFileSync('git', ['rev-parse', 'HEAD'], {
212
+ cwd: worktreePath,
213
+ encoding: 'utf8',
214
+ stdio: ['ignore', 'pipe', 'pipe'],
215
+ }).trim();
216
+ } catch (error) {
217
+ return {
218
+ code: 'WORKTREE_HEAD_UNAVAILABLE',
219
+ message: `cannot verify task worktree HEAD: ${error.message}`,
220
+ };
221
+ }
222
+ if (!task.completion_commit || task.completion_commit !== worktreeHead) {
223
+ return {
224
+ code: 'TASK_COMPLETION_COMMIT_STALE',
225
+ message: `task ${task.id} completion commit does not match worktree HEAD`,
226
+ };
227
+ }
228
+ const dev = workflows.listWorkflows(
229
+ db,
230
+ {
231
+ kind: 'dev',
232
+ status: 'ready',
233
+ change_id: task.change_id,
234
+ task_id: task.id,
235
+ limit: 1,
236
+ },
237
+ { rootDir: worktreePath },
238
+ )[0];
239
+ if (!dev || dev.artifact_health.status !== 'pass') {
240
+ return {
241
+ code: 'DEV_WORKFLOW_NOT_READY',
242
+ message: `task ${task.id} has no current ready dev workflow`,
243
+ };
244
+ }
245
+ try {
246
+ workflows.assertApprovedReview(db, task.change_id, task.id, worktreePath);
247
+ } catch (error) {
248
+ return {
249
+ code: error.code || 'TASK_REVIEW_NOT_CURRENT',
250
+ message: error.message,
251
+ };
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function resolveWorktreeBase(repoRoot, requestedBase) {
257
+ const managedBase = path.resolve(repoRoot, '.ultra', 'worktrees');
258
+ const resolved = requestedBase
259
+ ? path.resolve(repoRoot, requestedBase)
260
+ : managedBase;
261
+ if (resolved !== managedBase && !resolved.startsWith(`${managedBase}${path.sep}`)) {
262
+ throw new SessionRunnerError(
263
+ 'WORKTREE_SCOPE_INVALID',
264
+ `worktree_base must stay inside ${managedBase}`,
265
+ );
266
+ }
267
+ return resolved;
268
+ }
269
+
52
270
  function mintSessionId() {
53
271
  return `sess-${randomUUID().slice(0, 8)}`;
54
272
  }
55
273
 
274
+ function assertSessionTaskReady(db, repoRoot, taskId) {
275
+ const task = ops.readTask(db, taskId);
276
+ if (!task) return null; // admissionCheck reports the canonical TASK_NOT_FOUND error.
277
+ // Unbound rows remain readable for legacy recovery, but every current
278
+ // change-owned task must pass the same plan gate as a dev workflow.
279
+ if (!task.change_id) return task;
280
+ ops.assertTaskExecutionContract(task);
281
+ if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
282
+ throw new workflows.WorkflowStateError(
283
+ 'WORKFLOW_TASK_NOT_EXECUTABLE',
284
+ `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
285
+ );
286
+ }
287
+ const incompleteDependencies = (task.deps || []).filter((id) => {
288
+ const dependency = ops.readTask(db, id);
289
+ return !dependency || !['completed', 'expanded'].includes(dependency.status);
290
+ });
291
+ if (incompleteDependencies.length > 0) {
292
+ throw new workflows.WorkflowStateError(
293
+ 'TASK_DEPENDENCIES_INCOMPLETE',
294
+ `task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
295
+ );
296
+ }
297
+ workflows.assertCurrentPlan(db, task.change_id, repoRoot);
298
+ return task;
299
+ }
300
+
56
301
  function spawnSession({
57
302
  db,
58
303
  repoRoot,
@@ -64,12 +309,14 @@ function spawnSession({
64
309
  env = {},
65
310
  stdio = 'ignore',
66
311
  worktreeRef = 'HEAD',
312
+ worktree_base = null,
67
313
  lease_seconds = 1800,
68
314
  }) {
69
315
  if (!db) throw new SessionRunnerError('VALIDATION_ERROR', 'db handle required');
70
316
  if (!repoRoot) throw new SessionRunnerError('VALIDATION_ERROR', 'repoRoot required');
71
317
  if (!task_id) throw new SessionRunnerError('VALIDATION_ERROR', 'task_id required');
72
318
  if (!runtime) throw new SessionRunnerError('VALIDATION_ERROR', 'runtime required');
319
+ const worktreeBase = resolveWorktreeBase(repoRoot, worktree_base);
73
320
 
74
321
  // 1. Admission check
75
322
  const verdict = ops.admissionCheck(db, task_id);
@@ -82,6 +329,9 @@ function spawnSession({
82
329
  err.verdict = verdict;
83
330
  throw err;
84
331
  }
332
+ // Validate workflow authority before takeover can terminate the prior worker
333
+ // or any filesystem mutation creates a new worktree.
334
+ assertSessionTaskReady(db, repoRoot, task_id);
85
335
  if (!verdict.can_spawn && !takeover) {
86
336
  const err = new SessionRunnerError(
87
337
  'ADMISSION_DENIED',
@@ -91,31 +341,53 @@ function spawnSession({
91
341
  throw err;
92
342
  }
93
343
  if (!verdict.can_spawn && takeover && verdict.conflict) {
344
+ const priorSession = ops.readSession(db, verdict.conflict.sid);
345
+ if (priorSession?.pid) {
346
+ try { process.kill(priorSession.pid, 'SIGTERM'); }
347
+ catch (error) {
348
+ if (error.code !== 'ESRCH') {
349
+ throw new SessionRunnerError(
350
+ 'TAKEOVER_FAILED',
351
+ `cannot terminate prior session ${verdict.conflict.sid}: ${error.message}`,
352
+ );
353
+ }
354
+ }
355
+ }
94
356
  ops.updateSession(db, verdict.conflict.sid, { status: 'crashed' });
95
357
  }
96
358
 
97
359
  // 2. Mint sid + paths
98
360
  const sid = mintSessionId();
99
- const worktree_path = path.join(repoRoot, '.ultra', 'worktrees', sid);
361
+ const worktree_path = path.join(worktreeBase, sid);
100
362
  const artifact_dir = path.join(repoRoot, '.ultra', 'sessions', sid);
101
363
 
102
364
  // 3. Git worktree (throws WORKTREE_FAILED on failure)
103
365
  gitWorktreeAdd(repoRoot, worktree_path, worktreeRef);
104
- fs.mkdirSync(artifact_dir, { recursive: true });
366
+ let authority;
367
+ try {
368
+ authority = linkAuthorityIntoWorktree(repoRoot, worktree_path);
369
+ fs.mkdirSync(artifact_dir, { recursive: true });
370
+ } catch (error) {
371
+ gitWorktreeRemove(repoRoot, worktree_path);
372
+ throw error;
373
+ }
105
374
 
106
- // 4. Spawn child process (optional — caller may want a record-only session)
375
+ // 4. Spawn child process (optional — the active host may consume the worktree)
107
376
  let proc = null;
108
377
  if (command) {
109
378
  proc = spawn(command, args, {
110
379
  cwd: worktree_path,
111
380
  env: {
112
381
  ...process.env,
382
+ ...env,
113
383
  UBP_SESSION_ID: sid,
114
384
  UBP_TASK_ID: task_id,
115
385
  UBP_RUNTIME: runtime,
116
386
  UBP_WORKTREE: worktree_path,
117
387
  UBP_ARTIFACT_DIR: artifact_dir,
118
- ...env,
388
+ UBP_DB_PATH: authority.stateDbPath,
389
+ UBP_ROOT_DIR: worktree_path,
390
+ UBP_AUTHORITY_ROOT: repoRoot,
119
391
  },
120
392
  stdio,
121
393
  detached: false,
@@ -138,6 +410,7 @@ function spawnSession({
138
410
  // state.db insert failed — roll back worktree + child
139
411
  if (proc && !proc.killed) { try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ } }
140
412
  gitWorktreeRemove(repoRoot, worktree_path);
413
+ try { fs.rmSync(artifact_dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
141
414
  throw err;
142
415
  }
143
416
 
@@ -145,6 +418,7 @@ function spawnSession({
145
418
  sid,
146
419
  worktree_path,
147
420
  artifact_dir,
421
+ authority_db_path: authority.stateDbPath,
148
422
  lease_expires_at: session.lease_expires_at,
149
423
  pid: proc ? proc.pid : null,
150
424
  process: proc,
@@ -157,7 +431,7 @@ function closeSession({
157
431
  sid,
158
432
  }, {
159
433
  status = 'completed',
160
- remove_worktree = true,
434
+ remove_worktree = false,
161
435
  kill_signal = 'SIGTERM',
162
436
  mineSkill = false,
163
437
  skillsRoot = null,
@@ -169,13 +443,21 @@ function closeSession({
169
443
  if (!session) {
170
444
  throw new SessionRunnerError('SESSION_NOT_FOUND', `session ${sid} not found`);
171
445
  }
446
+ if (remove_worktree && !autoMerge) {
447
+ const blocker = worktreeRemovalBlocker(repoRoot, session.worktree_path);
448
+ if (blocker) {
449
+ throw new SessionRunnerError(
450
+ 'WORKTREE_NOT_INTEGRATED',
451
+ `refusing to remove ${session.worktree_path}: ${blocker}`,
452
+ );
453
+ }
454
+ }
172
455
  if (session.pid) {
173
456
  try { process.kill(session.pid, kill_signal); }
174
457
  catch (_) { /* already dead */ }
175
458
  }
176
- ops.updateSession(db, sid, { status });
177
- // Phase 7.3 — opt-in skill mining (same lifecycle moment; runs after
178
- // status flip so task_completed events are visible).
459
+ // Phase 7.3 — opt-in skill mining reads durable task/session events. It
460
+ // cannot advance task or workflow state.
179
461
  if (mineSkill && skillsRoot) {
180
462
  try { skillMiner.mineSession(db, { sid, skillsRoot }); }
181
463
  catch (err) { process.stderr.write(`skill-miner error: ${err.message}\n`); }
@@ -185,21 +467,55 @@ function closeSession({
185
467
  let mergeResult;
186
468
  let effectiveRemove = remove_worktree;
187
469
  if (autoMerge && status === 'completed' && repoRoot && session.worktree_path) {
188
- const autoMergeMod = require('./auto-merge.cjs');
189
- mergeResult = autoMergeMod.autoMerge({
190
- repoRoot,
191
- worktreePath: session.worktree_path,
192
- baseBranch: mergeBaseBranch,
193
- sid,
194
- task_id: session.task_id,
195
- db,
196
- });
197
- if (mergeResult.reason === 'conflict') effectiveRemove = false;
470
+ const task = session.task_id ? ops.readTask(db, session.task_id) : null;
471
+ if (!task || task.status !== 'completed') {
472
+ mergeResult = {
473
+ merged: false,
474
+ reason: 'task_not_completed',
475
+ task_status: task?.status || null,
476
+ };
477
+ effectiveRemove = false;
478
+ } else {
479
+ const integrationBlocker = changeTaskIntegrationBlocker(
480
+ db,
481
+ task,
482
+ session.worktree_path,
483
+ );
484
+ if (integrationBlocker) {
485
+ mergeResult = {
486
+ merged: false,
487
+ reason: 'workflow_gates_open',
488
+ blocker: integrationBlocker.code,
489
+ message: integrationBlocker.message,
490
+ };
491
+ effectiveRemove = false;
492
+ } else {
493
+ const autoMergeMod = require('./auto-merge.cjs');
494
+ mergeResult = autoMergeMod.autoMerge({
495
+ repoRoot,
496
+ worktreePath: session.worktree_path,
497
+ baseBranch: mergeBaseBranch,
498
+ sid,
499
+ task_id: session.task_id,
500
+ db,
501
+ });
502
+ effectiveRemove = Boolean(
503
+ mergeResult.merged || mergeResult.reason === 'no_changes',
504
+ );
505
+ }
506
+ }
198
507
  }
508
+ ops.updateSession(db, sid, { status });
199
509
  if (effectiveRemove && repoRoot && session.worktree_path) {
200
510
  gitWorktreeRemove(repoRoot, session.worktree_path);
201
511
  }
202
- const out = { sid, status };
512
+ const out = {
513
+ sid,
514
+ status,
515
+ worktree_preserved: Boolean(
516
+ session.worktree_path && fs.existsSync(session.worktree_path),
517
+ ),
518
+ };
203
519
  if (mergeResult) out.merge = mergeResult;
204
520
  return out;
205
521
  }
@@ -216,9 +532,22 @@ function attachHeartbeat(db, sid, { intervalMs = 30000 } = {}) {
216
532
 
217
533
  module.exports = {
218
534
  SessionRunnerError,
535
+ EXPECTED_EXECUTION_GATE_CODES,
536
+ isExpectedExecutionGate,
219
537
  spawnSession,
220
538
  closeSession,
221
539
  attachHeartbeat,
540
+ assertSessionTaskReady,
222
541
  // exposed for tests
223
- _internal: { gitWorktreeAdd, gitWorktreeRemove, mintSessionId },
542
+ _internal: {
543
+ gitWorktreeAdd,
544
+ gitWorktreeRemove,
545
+ mintSessionId,
546
+ authorityLinkIsIgnored,
547
+ ensureAuthorityLinkIgnored,
548
+ linkAuthorityIntoWorktree,
549
+ worktreeRemovalBlocker,
550
+ changeTaskIntegrationBlocker,
551
+ resolveWorktreeBase,
552
+ },
224
553
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra-builder-pro-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
5
5
  "bin": {
6
6
  "ultra-builder-pro-cli": "bin/install.js",
@@ -50,6 +50,7 @@
50
50
  "docs/ROADMAP.md",
51
51
  "docs/ARCHITECTURE.md",
52
52
  "docs/WORKFLOW-LIFECYCLE.md",
53
+ "docs/LEGACY-CLI-CROSSWALK.md",
53
54
  "docs/AGENT-CONTEXT.md",
54
55
  "docs/SKILL-AUTHORING.md",
55
56
  "docs/COMMIT-HASH-BACKFILL.md",