ultra-builder-pro-cli 0.15.0 → 0.17.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 (71) hide show
  1. package/.ultra-template/specs/research-distillate.md +4 -3
  2. package/CHANGELOG.md +120 -1
  3. package/README.md +67 -30
  4. package/adapters/_shared/codex-assets.cjs +10 -5
  5. package/adapters/_shared/handbook.cjs +3 -2
  6. package/adapters/_shared/interaction-contract.cjs +61 -0
  7. package/adapters/claude.js +1 -1
  8. package/adapters/opencode.js +1 -1
  9. package/bin/orchestrator.js +175 -19
  10. package/commands/ultra-change.md +1 -1
  11. package/commands/ultra-deliver.md +3 -3
  12. package/commands/ultra-init.md +1 -1
  13. package/commands/ultra-plan.md +1 -1
  14. package/commands/ultra-research.md +2 -2
  15. package/commands/ultra-review.md +10 -0
  16. package/commands/ultra-status.md +1 -1
  17. package/commands/ultra-test.md +1 -1
  18. package/commands/ultra-think.md +2 -2
  19. package/docs/AGENT-CONTEXT.md +38 -18
  20. package/docs/ARCHITECTURE.md +91 -13
  21. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  22. package/docs/PLAN.zh-CN.md +19 -12
  23. package/docs/ROADMAP.md +5 -3
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  25. package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
  26. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  27. package/docs/WORKFLOW-LIFECYCLE.md +179 -39
  28. package/hooks/pre_stop_check.py +3 -2
  29. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  30. package/mcp-server/lib/change-workflow.cjs +107 -39
  31. package/mcp-server/lib/context-spine.cjs +215 -87
  32. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  33. package/mcp-server/lib/doctor.cjs +9 -0
  34. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  35. package/mcp-server/lib/init-project.cjs +178 -41
  36. package/mcp-server/lib/project-breadcrumb.cjs +23 -14
  37. package/mcp-server/lib/state-db.cjs +279 -9
  38. package/mcp-server/lib/state-ops.cjs +64 -1
  39. package/mcp-server/lib/workflow-state.cjs +313 -205
  40. package/mcp-server/server.cjs +112 -34
  41. package/orchestrator/auto-merge.cjs +19 -0
  42. package/orchestrator/daemon.cjs +104 -0
  43. package/orchestrator/parallel-orchestrator.cjs +371 -49
  44. package/orchestrator/session-runner.cjs +354 -25
  45. package/package.json +2 -1
  46. package/skills/ultra-change/SKILL.md +69 -65
  47. package/skills/ultra-deliver/SKILL.md +37 -58
  48. package/skills/ultra-dev/SKILL.md +55 -49
  49. package/skills/ultra-doctor/SKILL.md +44 -49
  50. package/skills/ultra-init/SKILL.md +78 -100
  51. package/skills/ultra-plan/SKILL.md +78 -79
  52. package/skills/ultra-research/SKILL.md +99 -98
  53. package/skills/ultra-review/SKILL.md +38 -47
  54. package/skills/ultra-review/references/review-modes.md +5 -3
  55. package/skills/ultra-review/references/unified-schema.md +26 -6
  56. package/skills/ultra-review/scripts/review_wait.py +12 -0
  57. package/skills/ultra-status/SKILL.md +34 -40
  58. package/skills/ultra-test/SKILL.md +61 -48
  59. package/skills/ultra-think/SKILL.md +47 -47
  60. package/skills/ultra-think/references/decision-dialogue.md +106 -0
  61. package/spec/cli-protocol.md +46 -2
  62. package/spec/mcp-tools.yaml +444 -29
  63. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  64. package/spec/schemas/mcp-tools.schema.json +1 -1
  65. package/spec/schemas/state-db.sql +66 -2
  66. package/spec/schemas/test-report.v1.schema.json +45 -14
  67. package/ultra-tools/cli.cjs +1 -1
  68. package/ultra-tools/commands/session.cjs +10 -3
  69. package/ultra-tools/commands/status.cjs +34 -14
  70. package/ultra-tools/commands/system.cjs +2 -2
  71. 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.15.0",
3
+ "version": "0.17.0",
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",
@@ -1,69 +1,73 @@
1
1
  ---
2
2
  name: ultra-change
3
- description: Open or resume one bounded post-baseline change and connect its intent, delta, task plan, fresh context, and convergence evidence. Use when handling daily feature work, fixes, redesigns, or incidents after an Ultra baseline exists.
3
+ description: Capture or revise one bounded feature, fix, redesign, or incident contract against a ready Ultra baseline. Use when post-baseline work needs accepted intent before research, planning, or implementation.
4
4
  ---
5
5
 
6
- # Maintain one continuous change
7
-
8
- Use Ultra MCP for lifecycle writes. Keep external memory and code-graph content in
9
- their providers; Ultra stores references only.
10
-
11
- ## Bind or create
12
-
13
- 1. Read `system.doctor`, `baseline.get`, `change.list`, and `change.breadcrumb`.
14
- 2. Resume the change matching the requested outcome. Require an id when several could
15
- match; do not create a duplicate packet.
16
- 3. New ordinary work requires a ready baseline. An incident on unhealthy authority
17
- requires an explicit `baseline_bypass` reason and approver. Never infer approval
18
- from urgency.
19
- 4. Read `references/change-contract.md`. Persist the complete outcome, executable
20
- acceptance, non-goals, public seams, recovery contract, unresolved decisions,
21
- documentation impact, profile rationale, material risk flags, and research
22
- disposition. Do not derive these fields from a title or free-form intent later.
23
- 5. Select `quick`, `standard`, `major`, or `incident` from current scope and risk.
24
- Escalate a proposed quick change when its contract carries material risk or research.
25
- Then call `change.create` or `change.update` and read the normalized record back.
26
- 6. Resume or start the linked `change` workflow with `workflow.list`/`workflow.start`.
27
-
28
- ## Record the change workflow
29
-
30
- Use `workflow.step` as evidence becomes durable:
31
-
32
- - `bind-baseline`: current baseline id, revision, branch, and worktree evidence;
33
- - `classify-change`: kind and rationale;
34
- - `record-intent`: intent artifact, acceptance, scope, and resolved docs impact;
35
- - `plan-change`: completed plan workflow and authoritative task ids;
36
- - `compile-context`: current task-bound `change.context` manifest and digest;
37
- - `verify-readiness`: ready breadcrumb with one executable next action.
38
-
39
- Standard and major changes keep deltas under
40
- `.ultra/changes/active/<id>/delta/` and an evidence-linked `plan.md`. Deltas describe
41
- differences from the baseline; they are not a second baseline. Incidents maintain the
42
- structured diagnosis artifact and recovery evidence.
43
-
44
- When `research_disposition.status` is `bounded` or `required`, run `ultra-research`
45
- with the exact recorded mode and selected steps before planning. Planning is blocked
46
- until that change-bound research run is complete, current, and selection-equivalent.
47
- Use `none` only when the Change Contract already has sufficient evidence.
48
-
49
- Use `ultra-plan` to create complete DB-backed task contracts. Recompile
50
- `change.context` after any task, HEAD, specification, or decision change. The task's DB
51
- contract is authoritative; Prompt input may not override its seam or verification.
52
- Required missing or stale refs block. Budget warnings guide narrower context but never
53
- justify dropping necessary evidence or refusing legitimate work.
54
-
55
- An ordinary active change remains executable only while its durable `bind-baseline`
56
- evidence still names the current approved-ready baseline. HEAD, worktree, specification,
57
- or source-evidence drift created after that binding is advisory until delivery
58
- reconciliation. Adoption/migration state, missing approval or evidence, blocking gaps,
59
- and any other structural baseline failure block tasks, context, and later stages. Only
60
- the recorded incident break-glass authority can bypass that gate.
61
-
62
- Call `workflow.complete` only after all change workflow steps are current. Return the
63
- change id, kind, task set, context digest, blockers or warnings, workflow state, and the
64
- breadcrumb's one route.
65
-
66
- When implementation reveals a stable missing invariant, use
67
- `change.learning_propose` and the approval/reject/apply transition. Unresolved learning
68
- blocks delivery. An approved application must bind the declared target anchor, its
69
- before and after digests, and current evidence before the proposal becomes `applied`.
6
+ # Capture accepted change intent
7
+
8
+ Change is the durable unit that connects later research, plan, implementation,
9
+ verification, review, and baseline reconciliation. Capturing a change does not start
10
+ those workflows or compile execution context.
11
+
12
+ Read `../ultra-think/references/decision-dialogue.md` before asking a material question.
13
+
14
+ ## Inspect and align
15
+
16
+ 1. Read `system.doctor`, `baseline.get`, active changes, decisions, and
17
+ `change.breadcrumb`.
18
+ 2. Resume an existing change that matches the request. Do not duplicate authority.
19
+ 3. Ordinary work requires a healthy ready baseline. An incident may use only an
20
+ explicit `baseline_bypass` reason and approver; urgency is not approval.
21
+ 4. Inspect the checkout and accepted baseline before asking the user. Resolve code and
22
+ runtime facts yourself.
23
+ 5. Convert the request into a Change Contract:
24
+ - observable outcome and executable acceptance;
25
+ - non-goals and public seams;
26
+ - recovery strategy and verification;
27
+ - unresolved material decisions;
28
+ - documentation impact;
29
+ - change kind and evidence-backed risk flags;
30
+ - research disposition.
31
+
32
+ Select `quick`, `standard`, `major`, or `incident` from actual scope and risk. A quick
33
+ change may have no material risk, research obligation, or more than one task.
34
+
35
+ Ask only when a choice changes accepted product intent, scope, public behavior,
36
+ compatibility, security, material cost, external effects, or recovery. Use the host's
37
+ native question UI when available. Normalize clear choices already in the user's
38
+ request. The model owns reversible implementation detail inside the accepted contract.
39
+
40
+ ## Persist the contract
41
+
42
+ When a durable alignment checkpoint is necessary, bind it to a compact artifact and
43
+ pass its id as `alignment_thread_id`. Do not require a checkpoint for a complete,
44
+ unambiguous request that already provides current authority.
45
+
46
+ Call `change.create`. It records and completes exactly:
47
+
48
+ 1. `bind-baseline`
49
+ 2. `classify-change`
50
+ 3. `record-intent`
51
+
52
+ The returned change workflow must be `completed`; research, plan, context, dev, test,
53
+ review, and deliver remain independent capabilities. Standard and major changes may
54
+ store delta artifacts under `.ultra/changes/active/<id>/delta/`. Incidents maintain
55
+ the structured diagnosis artifact.
56
+
57
+ Use `change.update` when accepted intent or evidence changes. Update the Change
58
+ Contract, classification, research disposition, and docs impact together when their
59
+ meaning is coupled. Downstream task, context, test, or review evidence affected by that
60
+ change must become stale and be regenerated; never keep old evidence green by prompt
61
+ assertion.
62
+
63
+ If research is required, invoke `ultra-research` with the recorded disposition. If the
64
+ contract is sufficiently evidenced, the host may recommend `ultra-plan`. `change.context`
65
+ is compiled only for an actual plan, implementation, test, or review consumer.
66
+
67
+ Stable implementation discoveries use the approval-gated specification-learning
68
+ transitions. External memory and graph providers retain their own content; Ultra stores
69
+ metadata references only.
70
+
71
+ Return the normalized contract, change id and kind, research disposition, blockers,
72
+ and `allowed_transitions`. The host selects a recommendation from those transitions;
73
+ MCP does not encode a semantic next action.