ultra-builder-pro-cli 0.16.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.
- package/.ultra-template/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +86 -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
package/mcp-server/server.cjs
CHANGED
|
@@ -38,6 +38,7 @@ const {
|
|
|
38
38
|
readProjectBreadcrumb,
|
|
39
39
|
renderProjectBreadcrumb,
|
|
40
40
|
} = require('./lib/project-breadcrumb.cjs');
|
|
41
|
+
const sessionRunner = require('../orchestrator/session-runner.cjs');
|
|
41
42
|
|
|
42
43
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
43
44
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
@@ -153,11 +154,6 @@ function buildAjv() {
|
|
|
153
154
|
return ajv;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
function mintSessionId() {
|
|
157
|
-
const { randomUUID } = require('node:crypto');
|
|
158
|
-
return `sess-${randomUUID().slice(0, 8)}`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
157
|
async function dispatchTool(name, input, db, ctx = {}) {
|
|
162
158
|
switch (name) {
|
|
163
159
|
case 'task.create': {
|
|
@@ -207,40 +203,51 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
207
203
|
return ops.subscribeEventsSince(db, input || {});
|
|
208
204
|
}
|
|
209
205
|
case 'session.spawn': {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
err.code = 'ADMISSION_DENIED';
|
|
218
|
-
throw err;
|
|
219
|
-
}
|
|
220
|
-
if (!verdict.can_spawn && input.takeover && verdict.conflict) {
|
|
221
|
-
ops.updateSession(db, verdict.conflict.sid, { status: 'crashed' });
|
|
206
|
+
if (ctx.sessionId) {
|
|
207
|
+
const error = new Error(
|
|
208
|
+
`worker session ${ctx.sessionId} already owns an isolated worktree; `
|
|
209
|
+
+ 'resume that session instead of recursively spawning another one',
|
|
210
|
+
);
|
|
211
|
+
error.code = 'NESTED_SESSION_FORBIDDEN';
|
|
212
|
+
throw error;
|
|
222
213
|
}
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const session = ops.createSession(db, {
|
|
228
|
-
sid,
|
|
214
|
+
const rootDir = ctx.rootDir || process.cwd();
|
|
215
|
+
const handle = sessionRunner.spawnSession({
|
|
216
|
+
db,
|
|
217
|
+
repoRoot: rootDir,
|
|
229
218
|
task_id: input.task_id,
|
|
230
219
|
runtime: input.runtime,
|
|
231
|
-
|
|
232
|
-
|
|
220
|
+
takeover: Boolean(input.takeover),
|
|
221
|
+
worktree_base: input.worktree_base,
|
|
233
222
|
});
|
|
234
223
|
return {
|
|
235
|
-
sid,
|
|
236
|
-
worktree_path,
|
|
237
|
-
artifact_dir,
|
|
238
|
-
lease_expires_at:
|
|
224
|
+
sid: handle.sid,
|
|
225
|
+
worktree_path: handle.worktree_path,
|
|
226
|
+
artifact_dir: handle.artifact_dir,
|
|
227
|
+
lease_expires_at: handle.lease_expires_at,
|
|
228
|
+
worktree_created: true,
|
|
239
229
|
};
|
|
240
230
|
}
|
|
241
231
|
case 'session.close': {
|
|
242
|
-
|
|
243
|
-
|
|
232
|
+
if (ctx.sessionId) {
|
|
233
|
+
const error = new Error(
|
|
234
|
+
ctx.sessionId === input.sid
|
|
235
|
+
? `worker session ${ctx.sessionId} is supervised by its parent; save evidence and exit so the parent can settle the lease`
|
|
236
|
+
: `worker session ${ctx.sessionId} cannot close unrelated session ${input.sid}`,
|
|
237
|
+
);
|
|
238
|
+
error.code = ctx.sessionId === input.sid
|
|
239
|
+
? 'WORKER_SESSION_PARENT_OWNED'
|
|
240
|
+
: 'SESSION_SCOPE_VIOLATION';
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const closed = sessionRunner.closeSession(
|
|
244
|
+
{ db, repoRoot: ctx.rootDir || process.cwd(), sid: input.sid },
|
|
245
|
+
{ status: input.status, remove_worktree: input.remove_worktree === true },
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
worktree_preserved: closed.worktree_preserved,
|
|
250
|
+
};
|
|
244
251
|
}
|
|
245
252
|
case 'session.get': {
|
|
246
253
|
const session = ops.readSession(db, input.sid);
|
|
@@ -252,6 +259,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
252
259
|
return { session };
|
|
253
260
|
}
|
|
254
261
|
case 'session.admission_check': {
|
|
262
|
+
sessionRunner.assertSessionTaskReady(
|
|
263
|
+
db,
|
|
264
|
+
ctx.rootDir || process.cwd(),
|
|
265
|
+
input.task_id,
|
|
266
|
+
);
|
|
255
267
|
return ops.admissionCheck(db, input.task_id);
|
|
256
268
|
}
|
|
257
269
|
case 'session.list': {
|
|
@@ -633,7 +645,13 @@ function errorResponse(code, message, retriable = false, details = undefined) {
|
|
|
633
645
|
};
|
|
634
646
|
}
|
|
635
647
|
|
|
636
|
-
function startServer({
|
|
648
|
+
function startServer({
|
|
649
|
+
dbPath,
|
|
650
|
+
rootDir,
|
|
651
|
+
sessionId = null,
|
|
652
|
+
projectOnWrite = true,
|
|
653
|
+
project = projector.projectAll,
|
|
654
|
+
}) {
|
|
637
655
|
let db = null;
|
|
638
656
|
const getDb = () => {
|
|
639
657
|
if (!db) db = initStateDb(dbPath).db;
|
|
@@ -703,7 +721,12 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
|
|
|
703
721
|
try {
|
|
704
722
|
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
705
723
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
706
|
-
result = await dispatchTool(
|
|
724
|
+
result = await dispatchTool(
|
|
725
|
+
name,
|
|
726
|
+
args,
|
|
727
|
+
toolDb,
|
|
728
|
+
{ rootDir, sessionId, projector: project },
|
|
729
|
+
);
|
|
707
730
|
} catch (err) {
|
|
708
731
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
709
732
|
toolError = code;
|
|
@@ -768,7 +791,8 @@ async function main() {
|
|
|
768
791
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
769
792
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
770
793
|
: path.resolve('.');
|
|
771
|
-
const
|
|
794
|
+
const sessionId = process.env.UBP_SESSION_ID || null;
|
|
795
|
+
const handle = startServer({ dbPath, rootDir, sessionId });
|
|
772
796
|
const transport = new StdioServerTransport();
|
|
773
797
|
await handle.server.connect(transport);
|
|
774
798
|
const cleanup = () => handle.close().finally(() => process.exit(0));
|
|
@@ -40,6 +40,25 @@ function autoMerge({ repoRoot, worktreePath, baseBranch = 'main', sid, task_id =
|
|
|
40
40
|
if (!repoRoot) throw new Error('autoMerge: repoRoot required');
|
|
41
41
|
if (!worktreePath) throw new Error('autoMerge: worktreePath required');
|
|
42
42
|
|
|
43
|
+
try {
|
|
44
|
+
const status = runGit(['status', '--porcelain=v1'], { cwd: worktreePath });
|
|
45
|
+
if (status) {
|
|
46
|
+
return {
|
|
47
|
+
merged: false,
|
|
48
|
+
reason: 'uncommitted_changes',
|
|
49
|
+
session_sha: null,
|
|
50
|
+
base_sha: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
merged: false,
|
|
56
|
+
reason: 'worktree_status_failed',
|
|
57
|
+
session_sha: null,
|
|
58
|
+
base_sha: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
let sessionSha;
|
|
44
63
|
let baseSha;
|
|
45
64
|
try {
|
package/orchestrator/daemon.cjs
CHANGED
|
@@ -32,6 +32,26 @@ function routeTask(task, availableRuntimes) {
|
|
|
32
32
|
return d.action === 'spawn_agent' ? d.runtime : null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function taskDependenciesReady(db, task) {
|
|
36
|
+
return (task.deps || []).every((dependencyId) => {
|
|
37
|
+
const dependency = ops.readTask(db, dependencyId);
|
|
38
|
+
return Boolean(dependency && ['completed', 'expanded'].includes(dependency.status));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasActiveFileConflict(db, task) {
|
|
43
|
+
const declared = new Set(task.files_modified || []);
|
|
44
|
+
if (declared.size === 0) return false;
|
|
45
|
+
return ops.listActiveSessions(db).some((session) => {
|
|
46
|
+
if (!session.task_id || session.task_id === task.id) return false;
|
|
47
|
+
const activeTask = ops.readTask(db, session.task_id);
|
|
48
|
+
return Boolean(
|
|
49
|
+
activeTask
|
|
50
|
+
&& (activeTask.files_modified || []).some((file) => declared.has(file)),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
function maintainState({ db, repoRoot, project = projector.projectAll } = {}) {
|
|
36
56
|
if (!db) throw new Error('maintainState: db required');
|
|
37
57
|
if (!repoRoot) throw new Error('maintainState: repoRoot required');
|
|
@@ -60,6 +80,18 @@ function runDaemon({
|
|
|
60
80
|
if (!Array.isArray(runtimes) || runtimes.length === 0) {
|
|
61
81
|
throw new Error('runDaemon: runtimes array required');
|
|
62
82
|
}
|
|
83
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
84
|
+
const error = new Error(
|
|
85
|
+
'runDaemon: an explicit executable command is required; refusing to reserve empty sessions',
|
|
86
|
+
);
|
|
87
|
+
error.code = 'ORCHESTRATOR_COMMAND_REQUIRED';
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(commandArgs)) {
|
|
91
|
+
const error = new Error('runDaemon: commandArgs must be an array');
|
|
92
|
+
error.code = 'ORCHESTRATOR_COMMAND_INVALID';
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
63
95
|
|
|
64
96
|
let stopped = false;
|
|
65
97
|
const children = [];
|
|
@@ -75,6 +107,54 @@ function runDaemon({
|
|
|
75
107
|
bootRecovery = { recovered: [], count: 0, error: error.message };
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
function settleExecution(handle, task) {
|
|
111
|
+
let settled = false;
|
|
112
|
+
const stopHeartbeat = runner.attachHeartbeat(db, handle.sid);
|
|
113
|
+
const settle = (rawCode, signal, spawnError = null) => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
stopHeartbeat();
|
|
117
|
+
const code = rawCode === null ? -1 : rawCode;
|
|
118
|
+
const success = code === 0;
|
|
119
|
+
// A caller may intentionally close the DB while leaving child processes
|
|
120
|
+
// alive during shutdown. The next boot recovery owns those stale leases.
|
|
121
|
+
if (!db.open) return;
|
|
122
|
+
const liveSession = ops.readSession(db, handle.sid);
|
|
123
|
+
// Takeover or an explicit close already owns the terminal transition.
|
|
124
|
+
// The late child exit must not block the replacement task/session.
|
|
125
|
+
if (!liveSession || liveSession.status !== 'running') return;
|
|
126
|
+
try {
|
|
127
|
+
if (!success) {
|
|
128
|
+
try { ops.patchTask(db, task.id, { status: 'blocked' }); }
|
|
129
|
+
catch (error) {
|
|
130
|
+
runtimeState.recordIncident(db, {
|
|
131
|
+
code: 'ORCHESTRATOR_TASK_BLOCK_FAILED', severity: 'error', retryable: true,
|
|
132
|
+
message: error.message, source_kind: 'task', source_id: task.id,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
ops.recordTaskFailure(db, task.id, {
|
|
136
|
+
reason: spawnError
|
|
137
|
+
? `worker spawn or runtime error: ${spawnError.message}`
|
|
138
|
+
: `runtime process exited with code ${code}${signal ? ` (${signal})` : ''}`,
|
|
139
|
+
session_id: handle.sid,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
runner.closeSession(
|
|
143
|
+
{ db, repoRoot, sid: handle.sid },
|
|
144
|
+
{ status: success ? 'completed' : 'crashed', remove_worktree: false },
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (onError) onError(error);
|
|
148
|
+
else process.stderr.write(`orchestrator settle error: ${error.message}\n`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
handle.process.once('exit', settle);
|
|
152
|
+
handle.process.once('error', (error) => settle(-1, null, error));
|
|
153
|
+
if (handle.process.exitCode !== null && handle.process.exitCode !== undefined) {
|
|
154
|
+
setImmediate(() => settle(handle.process.exitCode, handle.process.signalCode || null));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
78
158
|
function tick() {
|
|
79
159
|
if (stopped) return;
|
|
80
160
|
try {
|
|
@@ -101,6 +181,17 @@ function runDaemon({
|
|
|
101
181
|
}
|
|
102
182
|
for (const task of pending) {
|
|
103
183
|
if (stopped) return;
|
|
184
|
+
if (task.stale) continue;
|
|
185
|
+
if (!taskDependenciesReady(db, task)) continue;
|
|
186
|
+
if (hasActiveFileConflict(db, task)) continue;
|
|
187
|
+
try {
|
|
188
|
+
runner.assertSessionTaskReady(db, repoRoot, task.id);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// A normal workflow gate is a durable pending state, not a daemon
|
|
191
|
+
// failure and not an error to repeat on every poll.
|
|
192
|
+
if (!runner.isExpectedExecutionGate(error) && onError) onError(error);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
104
195
|
// admissionCheck catches both live-session conflicts and tripped breakers.
|
|
105
196
|
let verdict;
|
|
106
197
|
try { verdict = ops.admissionCheck(db, task.id); }
|
|
@@ -118,6 +209,17 @@ function runDaemon({
|
|
|
118
209
|
command,
|
|
119
210
|
args: commandArgs,
|
|
120
211
|
});
|
|
212
|
+
handle.task_id = task.id;
|
|
213
|
+
try {
|
|
214
|
+
ops.patchTask(db, task.id, { status: 'in_progress' });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
runner.closeSession(
|
|
217
|
+
{ db, repoRoot, sid: handle.sid },
|
|
218
|
+
{ status: 'crashed', remove_worktree: false },
|
|
219
|
+
);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
settleExecution(handle, task);
|
|
121
223
|
children.push(handle);
|
|
122
224
|
} catch (err) {
|
|
123
225
|
if (onError) onError(err);
|
|
@@ -147,4 +249,6 @@ module.exports = {
|
|
|
147
249
|
runDaemon,
|
|
148
250
|
routeTask,
|
|
149
251
|
maintainState,
|
|
252
|
+
taskDependenciesReady,
|
|
253
|
+
hasActiveFileConflict,
|
|
150
254
|
};
|