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.
- package/.ultra-template/reports/templates/delivery-report.json +17 -0
- package/.ultra-template/reports/templates/test-report.json +18 -0
- package/.ultra-template/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +95 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +1 -1
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/docs/AGENT-CONTEXT.md +11 -8
- package/docs/ARCHITECTURE.md +67 -9
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -13
- package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
- package/docs/WORKFLOW-LIFECYCLE.md +123 -59
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +55 -35
- package/mcp-server/lib/context-spine.cjs +171 -105
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +14 -14
- package/mcp-server/lib/state-db.cjs +257 -10
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +300 -205
- package/mcp-server/server.cjs +58 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -94
- package/skills/ultra-deliver/SKILL.md +37 -65
- package/skills/ultra-dev/SKILL.md +50 -52
- package/skills/ultra-doctor/SKILL.md +34 -49
- package/skills/ultra-init/SKILL.md +78 -105
- package/skills/ultra-plan/SKILL.md +78 -99
- package/skills/ultra-research/SKILL.md +99 -113
- package/skills/ultra-review/SKILL.md +38 -53
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -44
- package/skills/ultra-test/SKILL.md +61 -54
- package/skills/ultra-think/SKILL.md +37 -51
- package/skills/ultra-think/references/decision-dialogue.md +87 -111
- package/spec/cli-protocol.md +35 -0
- package/spec/mcp-tools.yaml +145 -28
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/state-db.sql +8 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +19 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Phase 4.5.1 —
|
|
4
|
-
// Session =
|
|
5
|
-
// lease/heartbeat
|
|
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
|
|
8
|
-
//
|
|
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(
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
177
|
-
//
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
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 = {
|
|
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: {
|
|
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.
|
|
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",
|