ultra-builder-pro-cli 0.21.1 → 0.23.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/tasks/tasks.json +10 -3
- package/.ultra-template/templates/task-context.md +19 -0
- package/CHANGELOG.md +78 -0
- package/README.md +210 -36
- package/adapters/_shared/codex-assets.cjs +8 -3
- package/adapters/_shared/interaction-contract.cjs +13 -6
- package/adapters/_shared/runtime-assets.cjs +1 -1
- package/adapters/claude.js +1 -1
- package/adapters/kimi.js +1 -1
- package/adapters/opencode.js +24 -13
- package/docs/AGENT-CONTEXT.md +58 -51
- package/docs/ARCHITECTURE.md +82 -51
- package/docs/ARTIFACT-AUTHORITY.md +83 -44
- package/docs/COMMIT-HASH-BACKFILL.md +33 -90
- package/docs/DECISIONS.md +9 -7
- package/docs/LEGACY-CLI-CROSSWALK.md +10 -2
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +3 -2
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +13 -10
- package/docs/STATE-DB-ACCESS-POLICY.md +47 -19
- package/docs/WORKFLOW-LIFECYCLE.md +79 -49
- package/hooks/README.md +5 -4
- package/hooks/active_task_context.py +41 -39
- package/mcp-server/lib/artifact-registry.cjs +32 -7
- package/mcp-server/lib/baseline-workflow.cjs +60 -24
- package/mcp-server/lib/change-workflow.cjs +42 -6
- package/mcp-server/lib/context-paths.cjs +34 -3
- package/mcp-server/lib/doctor.cjs +28 -0
- package/mcp-server/lib/git-bootstrap.cjs +0 -1
- package/mcp-server/lib/init-project.cjs +52 -2
- package/mcp-server/lib/projector.cjs +43 -8
- package/mcp-server/lib/state-authority.cjs +19 -3
- package/mcp-server/lib/state-ops.cjs +6 -1
- package/mcp-server/lib/task-ledger.cjs +1342 -0
- package/mcp-server/lib/ultra-facade.cjs +646 -0
- package/mcp-server/lib/workflow-state.cjs +166 -2
- package/mcp-server/server.cjs +214 -26
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +38 -90
- package/skills/ultra-deliver/SKILL.md +42 -62
- package/skills/ultra-dev/SKILL.md +34 -73
- package/skills/ultra-doctor/SKILL.md +26 -39
- package/skills/ultra-init/SKILL.md +37 -69
- package/skills/ultra-plan/SKILL.md +52 -125
- package/skills/ultra-research/SKILL.md +41 -101
- package/skills/ultra-review/SKILL.md +25 -44
- package/skills/ultra-status/SKILL.md +21 -40
- package/skills/ultra-test/SKILL.md +25 -60
- package/skills/ultra-think/SKILL.md +29 -48
- package/skills/ultra-think/references/decision-dialogue.md +6 -4
- package/spec/cli-protocol.md +15 -0
- package/spec/mcp-tools.yaml +286 -0
- package/spec/schemas/context-file.v4.5.schema.json +1 -1
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +1 -1
- package/spec/schemas/task-ledger.v1.schema.json +217 -0
- package/ultra-tools/commands/migrate.cjs +5 -2
- package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -19
|
@@ -657,8 +657,9 @@ function inspectRunHealth(run, steps, rootDir) {
|
|
|
657
657
|
const firstOpen = steps.find(
|
|
658
658
|
(item) => item.required && !['completed', 'skipped'].includes(item.status),
|
|
659
659
|
);
|
|
660
|
-
if (!firstOpen)
|
|
661
|
-
|
|
660
|
+
if (!firstOpen) {
|
|
661
|
+
if (runMetadata.draft_dirty !== true) blockers.push('WORKFLOW_ACTIVE_WITHOUT_OPEN_STEP');
|
|
662
|
+
} else if (run.current_step !== firstOpen.step_id) {
|
|
662
663
|
blockers.push(`WORKFLOW_CURRENT_STEP_INVALID:${run.current_step || 'missing'}`);
|
|
663
664
|
}
|
|
664
665
|
}
|
|
@@ -1196,6 +1197,166 @@ function startWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
1196
1197
|
return readWorkflow(db, id, { rootDir });
|
|
1197
1198
|
}
|
|
1198
1199
|
|
|
1200
|
+
function abandonWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
1201
|
+
const id = nonEmpty(input.id, 'id');
|
|
1202
|
+
const reason = nonEmpty(input.reason, 'reason');
|
|
1203
|
+
return ops.tx(db, () => {
|
|
1204
|
+
const run = readWorkflow(db, id, { rootDir });
|
|
1205
|
+
if (!run) throw new WorkflowStateError('WORKFLOW_NOT_FOUND', `workflow ${id} not found`);
|
|
1206
|
+
if (run.status === 'cancelled') return run;
|
|
1207
|
+
if (run.status === 'completed') {
|
|
1208
|
+
throw new WorkflowStateError(
|
|
1209
|
+
'WORKFLOW_NOT_MUTABLE',
|
|
1210
|
+
`accepted workflow ${id} is immutable; create a new draft revision instead`,
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
const metadata = {
|
|
1214
|
+
...run.metadata,
|
|
1215
|
+
abandoned: {
|
|
1216
|
+
reason,
|
|
1217
|
+
abandoned_at: nowIso(),
|
|
1218
|
+
},
|
|
1219
|
+
};
|
|
1220
|
+
delete metadata.authority_invalidation;
|
|
1221
|
+
const ts = nowIso();
|
|
1222
|
+
db.prepare(
|
|
1223
|
+
`UPDATE workflow_runs
|
|
1224
|
+
SET status = 'cancelled', current_step = NULL, blockers_json = '[]',
|
|
1225
|
+
metadata_json = ?, completed_at = ?, updated_at = ?
|
|
1226
|
+
WHERE id = ?`,
|
|
1227
|
+
).run(JSON.stringify(metadata), ts, ts, id);
|
|
1228
|
+
ops.appendEventInTx(db, {
|
|
1229
|
+
type: 'workflow_abandoned',
|
|
1230
|
+
task_id: run.task_id,
|
|
1231
|
+
change_id: run.change_id,
|
|
1232
|
+
payload: { workflow_id: id, kind: run.kind, reason },
|
|
1233
|
+
});
|
|
1234
|
+
return readWorkflow(db, id, { rootDir });
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function prepareWorkflowCheckpoint(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
1239
|
+
const id = nonEmpty(input.id, 'id');
|
|
1240
|
+
const suppliedSteps = new Map(
|
|
1241
|
+
(Array.isArray(input.steps) ? input.steps : []).map((item) => [
|
|
1242
|
+
nonEmpty(item.step_id, 'steps.step_id'),
|
|
1243
|
+
item,
|
|
1244
|
+
]),
|
|
1245
|
+
);
|
|
1246
|
+
const defaultEvidence = Array.isArray(input.evidence) && input.evidence.length > 0
|
|
1247
|
+
? normalizeEvidence(input.evidence)
|
|
1248
|
+
: [{
|
|
1249
|
+
kind: 'checkpoint',
|
|
1250
|
+
ref: `workflow:${id}`,
|
|
1251
|
+
summary: 'The model recorded this semantic stage in one checkpoint.',
|
|
1252
|
+
}];
|
|
1253
|
+
return ops.tx(db, () => {
|
|
1254
|
+
const run = readWorkflow(db, id, { rootDir });
|
|
1255
|
+
if (!run) throw new WorkflowStateError('WORKFLOW_NOT_FOUND', `workflow ${id} not found`);
|
|
1256
|
+
if (run.status === 'completed') return { workflow: run, blockers: [], ready: true };
|
|
1257
|
+
if (run.status === 'cancelled') {
|
|
1258
|
+
throw new WorkflowStateError('WORKFLOW_NOT_MUTABLE', `workflow ${id} is cancelled`);
|
|
1259
|
+
}
|
|
1260
|
+
const blockers = [];
|
|
1261
|
+
const ts = nowIso();
|
|
1262
|
+
for (const workflowStep of run.steps) {
|
|
1263
|
+
if (!workflowStep.required) continue;
|
|
1264
|
+
const supplied = suppliedSteps.get(workflowStep.step_id) || {};
|
|
1265
|
+
const definition = WORKFLOW_DEFINITIONS[run.kind]
|
|
1266
|
+
.find((item) => item.id === workflowStep.step_id);
|
|
1267
|
+
const outputs = supplied.outputs === undefined
|
|
1268
|
+
? workflowStep.outputs
|
|
1269
|
+
: normalizeOutputs(supplied.outputs, rootDir);
|
|
1270
|
+
const evidence = supplied.evidence === undefined
|
|
1271
|
+
? (workflowStep.evidence.length > 0 ? workflowStep.evidence : defaultEvidence)
|
|
1272
|
+
: normalizeEvidence(supplied.evidence);
|
|
1273
|
+
const semanticRecords = supplied.semantic_records === undefined
|
|
1274
|
+
? workflowStep.semantic_records
|
|
1275
|
+
: (run.kind === 'research'
|
|
1276
|
+
? normalizeSemanticRecords(supplied.semantic_records, workflowStep.step_id, rootDir)
|
|
1277
|
+
: normalizeObjects(supplied.semantic_records, 'semantic_records'));
|
|
1278
|
+
if (definition.output_required && outputs.length === 0) {
|
|
1279
|
+
blockers.push(`WORKFLOW_OUTPUT_REQUIRED:${workflowStep.step_id}`);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (definition.evidence_required && evidence.length === 0) {
|
|
1283
|
+
blockers.push(`WORKFLOW_EVIDENCE_REQUIRED:${workflowStep.step_id}`);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (run.kind === 'research' && semanticRecords.length === 0) {
|
|
1287
|
+
blockers.push(`WORKFLOW_SEMANTIC_RECORDS_REQUIRED:${workflowStep.step_id}`);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
db.prepare(
|
|
1291
|
+
`UPDATE workflow_steps
|
|
1292
|
+
SET status = 'completed', evidence_json = ?, outputs_json = ?,
|
|
1293
|
+
decisions_json = ?, semantic_records_json = ?, blockers_json = '[]',
|
|
1294
|
+
started_at = COALESCE(started_at, ?), completed_at = ?, updated_at = ?
|
|
1295
|
+
WHERE run_id = ? AND step_id = ?`,
|
|
1296
|
+
).run(
|
|
1297
|
+
JSON.stringify(evidence),
|
|
1298
|
+
JSON.stringify(outputs),
|
|
1299
|
+
JSON.stringify(supplied.decisions === undefined
|
|
1300
|
+
? workflowStep.decisions
|
|
1301
|
+
: normalizeObjects(supplied.decisions, 'decisions')),
|
|
1302
|
+
JSON.stringify(semanticRecords),
|
|
1303
|
+
ts,
|
|
1304
|
+
ts,
|
|
1305
|
+
ts,
|
|
1306
|
+
id,
|
|
1307
|
+
workflowStep.step_id,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const metadata = { ...run.metadata };
|
|
1311
|
+
delete metadata.authority_invalidation;
|
|
1312
|
+
metadata.draft_dirty = blockers.length > 0;
|
|
1313
|
+
db.prepare(
|
|
1314
|
+
`UPDATE workflow_runs
|
|
1315
|
+
SET status = ?, current_step = NULL, blockers_json = ?, metadata_json = ?, updated_at = ?
|
|
1316
|
+
WHERE id = ?`,
|
|
1317
|
+
).run(
|
|
1318
|
+
blockers.length > 0 ? 'active' : 'ready',
|
|
1319
|
+
JSON.stringify(blockers),
|
|
1320
|
+
JSON.stringify(metadata),
|
|
1321
|
+
ts,
|
|
1322
|
+
id,
|
|
1323
|
+
);
|
|
1324
|
+
ops.appendEventInTx(db, {
|
|
1325
|
+
type: blockers.length > 0 ? 'workflow_checkpoint_rejected' : 'workflow_checkpoint_prepared',
|
|
1326
|
+
task_id: run.task_id,
|
|
1327
|
+
change_id: run.change_id,
|
|
1328
|
+
payload: { workflow_id: id, kind: run.kind, blockers },
|
|
1329
|
+
});
|
|
1330
|
+
return {
|
|
1331
|
+
workflow: readWorkflow(db, id, { rootDir }),
|
|
1332
|
+
blockers,
|
|
1333
|
+
ready: blockers.length === 0,
|
|
1334
|
+
};
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function reopenWorkflowDraft(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
1339
|
+
const id = nonEmpty(input.id, 'id');
|
|
1340
|
+
const blocker = typeof input.blocker === 'string' && input.blocker.trim()
|
|
1341
|
+
? input.blocker.trim()
|
|
1342
|
+
: 'CHECKPOINT_REJECTED';
|
|
1343
|
+
return ops.tx(db, () => {
|
|
1344
|
+
const run = readWorkflow(db, id, { rootDir });
|
|
1345
|
+
if (!run) throw new WorkflowStateError('WORKFLOW_NOT_FOUND', `workflow ${id} not found`);
|
|
1346
|
+
if (run.status === 'completed' || run.status === 'cancelled') return run;
|
|
1347
|
+
const metadata = { ...run.metadata, draft_dirty: true };
|
|
1348
|
+
delete metadata.authority_invalidation;
|
|
1349
|
+
const blockers = [...new Set([...(run.blockers || []), blocker])];
|
|
1350
|
+
db.prepare(
|
|
1351
|
+
`UPDATE workflow_runs
|
|
1352
|
+
SET status = 'active', blockers_json = ?, metadata_json = ?,
|
|
1353
|
+
updated_at = ?
|
|
1354
|
+
WHERE id = ?`,
|
|
1355
|
+
).run(JSON.stringify(blockers), JSON.stringify(metadata), nowIso(), id);
|
|
1356
|
+
return readWorkflow(db, id, { rootDir });
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1199
1360
|
function reviseWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
1200
1361
|
const id = nonEmpty(input.id, 'id');
|
|
1201
1362
|
const replacementId = nonEmpty(input.replacement_id, 'replacement_id');
|
|
@@ -2948,6 +3109,9 @@ module.exports = {
|
|
|
2948
3109
|
WorkflowStateError,
|
|
2949
3110
|
insertWorkflowInTx,
|
|
2950
3111
|
startWorkflow,
|
|
3112
|
+
abandonWorkflow,
|
|
3113
|
+
prepareWorkflowCheckpoint,
|
|
3114
|
+
reopenWorkflowDraft,
|
|
2951
3115
|
readWorkflow,
|
|
2952
3116
|
listWorkflows,
|
|
2953
3117
|
recordWorkflowStep,
|
package/mcp-server/server.cjs
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const { assertStateAuthority } = require('./lib/state-authority.cjs');
|
|
23
23
|
const ops = require('./lib/state-ops.cjs');
|
|
24
24
|
const projector = require('./lib/projector.cjs');
|
|
25
|
+
const taskLedger = require('./lib/task-ledger.cjs');
|
|
25
26
|
const telemetry = require('./lib/telemetry.cjs');
|
|
26
27
|
const topo = require('./lib/topo.cjs');
|
|
27
28
|
const parser = require('./lib/prd-parser.cjs');
|
|
@@ -38,6 +39,7 @@ const workflows = require('./lib/workflow-state.cjs');
|
|
|
38
39
|
const decisions = require('./lib/decision-dialogue.cjs');
|
|
39
40
|
const doctor = require('./lib/doctor.cjs');
|
|
40
41
|
const artifactRegistry = require('./lib/artifact-registry.cjs');
|
|
42
|
+
const ultraFacade = require('./lib/ultra-facade.cjs');
|
|
41
43
|
const {
|
|
42
44
|
readProjectBreadcrumb,
|
|
43
45
|
renderProjectBreadcrumb,
|
|
@@ -62,6 +64,9 @@ const TASK_TOOLS = Object.freeze([
|
|
|
62
64
|
'task.dependency_topo',
|
|
63
65
|
'task.parse_prd',
|
|
64
66
|
'task.expand',
|
|
67
|
+
'task.ledger_get',
|
|
68
|
+
'task.ledger_publish',
|
|
69
|
+
'task.ledger_import',
|
|
65
70
|
]);
|
|
66
71
|
|
|
67
72
|
const SESSION_TOOLS = Object.freeze([
|
|
@@ -133,21 +138,32 @@ const ARTIFACT_TOOLS = Object.freeze([
|
|
|
133
138
|
'artifact.get',
|
|
134
139
|
]);
|
|
135
140
|
|
|
136
|
-
const
|
|
141
|
+
const LEGACY_TOOLS = Object.freeze([
|
|
137
142
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
138
143
|
...DECISION_TOOLS,
|
|
139
144
|
...WORKFLOW_TOOLS,
|
|
140
145
|
...ARTIFACT_TOOLS,
|
|
141
146
|
...SYSTEM_TOOLS,
|
|
142
147
|
]);
|
|
148
|
+
const PUBLIC_TOOLS = ultraFacade.PUBLIC_TOOLS;
|
|
149
|
+
const REGISTERED_TOOLS = Object.freeze([...PUBLIC_TOOLS, ...LEGACY_TOOLS]);
|
|
143
150
|
|
|
144
151
|
const STATELESS_TOOLS = new Set(['task.init_project']);
|
|
145
152
|
|
|
153
|
+
function isStatelessPublicCall(name, input = {}) {
|
|
154
|
+
return name === 'ultra.record'
|
|
155
|
+
&& Array.isArray(input.entries)
|
|
156
|
+
&& input.entries.length === 1
|
|
157
|
+
&& input.entries[0]?.operation === 'task.init_project';
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
// init_project owns its target project's schema, baseline seed, and first
|
|
147
161
|
// projection internally. Do not enqueue a second projection on the caller DB.
|
|
148
162
|
const MUTATING_TOOLS = new Set([
|
|
163
|
+
'ultra.record', 'ultra.checkpoint', 'ultra.sync', 'ultra.session', 'ultra.archive',
|
|
149
164
|
'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
|
|
150
165
|
'task.parse_prd', 'task.expand',
|
|
166
|
+
'task.ledger_import',
|
|
151
167
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
152
168
|
'plan.export',
|
|
153
169
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
@@ -161,6 +177,15 @@ const MUTATING_TOOLS = new Set([
|
|
|
161
177
|
'artifact.record',
|
|
162
178
|
]);
|
|
163
179
|
|
|
180
|
+
const TEAM_LEDGER_SYNC_TOOLS = new Set([
|
|
181
|
+
'ultra.record', 'ultra.checkpoint', 'ultra.archive',
|
|
182
|
+
'task.create', 'task.update', 'task.delete', 'task.parse_prd', 'task.expand',
|
|
183
|
+
'plan.export',
|
|
184
|
+
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
185
|
+
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
186
|
+
'change.converge', 'change.archive',
|
|
187
|
+
]);
|
|
188
|
+
|
|
164
189
|
function loadRegisteredTools() {
|
|
165
190
|
const manifest = yaml.load(fs.readFileSync(TOOLS_FILE, 'utf8'));
|
|
166
191
|
return manifest.tools.filter((t) => REGISTERED_TOOLS.includes(t.name));
|
|
@@ -184,7 +209,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
184
209
|
return { id: task.id, status: task.status, created_at: task.created_at };
|
|
185
210
|
}
|
|
186
211
|
case 'task.update': {
|
|
212
|
+
const before = ops.readTask(db, input.id);
|
|
187
213
|
const task = ops.patchTask(db, input.id, input.patch || {});
|
|
214
|
+
const durableChanged = before
|
|
215
|
+
&& taskLedger.durableTask(before).digest !== taskLedger.durableTask(task).digest;
|
|
216
|
+
if (durableChanged) {
|
|
217
|
+
taskLedger.publishTaskLedger(db, {
|
|
218
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
219
|
+
reason: before.status !== task.status
|
|
220
|
+
? `task_${task.status}`
|
|
221
|
+
: 'task_contract_updated',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
188
224
|
return { ok: true, task };
|
|
189
225
|
}
|
|
190
226
|
case 'task.list': {
|
|
@@ -201,7 +237,12 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
201
237
|
return { task };
|
|
202
238
|
}
|
|
203
239
|
case 'task.delete': {
|
|
204
|
-
|
|
240
|
+
const result = ops.deleteTask(db, input.id, { force: !!input.force });
|
|
241
|
+
taskLedger.publishTaskLedger(db, {
|
|
242
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
243
|
+
reason: 'task_deleted',
|
|
244
|
+
});
|
|
245
|
+
return result;
|
|
205
246
|
}
|
|
206
247
|
case 'task.init_project': {
|
|
207
248
|
return initProject(input);
|
|
@@ -411,8 +452,58 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
411
452
|
children: input.children,
|
|
412
453
|
rootDir,
|
|
413
454
|
});
|
|
455
|
+
taskLedger.publishTaskLedger(db, {
|
|
456
|
+
rootDir,
|
|
457
|
+
reason: 'task_expanded',
|
|
458
|
+
});
|
|
414
459
|
return { parent_id: result.parent_id, children: result.children };
|
|
415
460
|
}
|
|
461
|
+
case 'task.ledger_get': {
|
|
462
|
+
const rootDir = ctx.rootDir || process.cwd();
|
|
463
|
+
return {
|
|
464
|
+
ledger: taskLedger.readTaskLedger(rootDir),
|
|
465
|
+
health: taskLedger.inspectTaskLedger(db, { rootDir }),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
case 'task.ledger_publish': {
|
|
469
|
+
const result = taskLedger.publishTaskLedger(db, {
|
|
470
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
471
|
+
reason: input.reason || 'manual_checkpoint',
|
|
472
|
+
});
|
|
473
|
+
const output = {
|
|
474
|
+
changed: result.changed,
|
|
475
|
+
path: path.relative(ctx.rootDir || process.cwd(), result.path),
|
|
476
|
+
generation: result.ledger.generation,
|
|
477
|
+
state_digest: result.ledger.state_digest,
|
|
478
|
+
};
|
|
479
|
+
if (result.migrated_legacy_projection) {
|
|
480
|
+
output.migrated_legacy_projection = true;
|
|
481
|
+
output.legacy_backup_path = path.relative(
|
|
482
|
+
ctx.rootDir || process.cwd(),
|
|
483
|
+
result.legacy_backup_path,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return output;
|
|
487
|
+
}
|
|
488
|
+
case 'task.ledger_import': {
|
|
489
|
+
const result = taskLedger.importTaskLedger(db, {
|
|
490
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
imported: result.imported,
|
|
494
|
+
preserved: result.preserved,
|
|
495
|
+
deleted: result.deleted,
|
|
496
|
+
imported_changes: result.imported_changes,
|
|
497
|
+
preserved_changes: result.preserved_changes,
|
|
498
|
+
imported_baseline: result.imported_baseline,
|
|
499
|
+
preserved_baseline: result.preserved_baseline,
|
|
500
|
+
generation: result.generation,
|
|
501
|
+
state_digest: result.state_digest,
|
|
502
|
+
requires_plan_revalidation: result.requires_plan_revalidation,
|
|
503
|
+
requires_baseline_revalidation: result.requires_baseline_revalidation,
|
|
504
|
+
already_current: result.already_current,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
416
507
|
case 'plan.export': {
|
|
417
508
|
const rootDir = ctx.rootDir || process.cwd();
|
|
418
509
|
const change = changes.readChange(db, input.change_id);
|
|
@@ -626,21 +717,42 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
626
717
|
};
|
|
627
718
|
}
|
|
628
719
|
case 'baseline.converge': {
|
|
629
|
-
|
|
720
|
+
const result = baselines.convergeBaseline(
|
|
630
721
|
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
631
722
|
);
|
|
723
|
+
if (result.ready) {
|
|
724
|
+
taskLedger.publishTaskLedger(db, {
|
|
725
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
726
|
+
reason: 'baseline_converged',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return result;
|
|
632
730
|
}
|
|
633
731
|
case 'change.create': {
|
|
634
732
|
const { randomUUID } = require('node:crypto');
|
|
635
733
|
const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
|
|
636
|
-
|
|
734
|
+
const result = changes.createChange(
|
|
735
|
+
db,
|
|
736
|
+
{ ...input, id },
|
|
737
|
+
{ rootDir: ctx.rootDir || process.cwd() },
|
|
738
|
+
);
|
|
739
|
+
taskLedger.publishTaskLedger(db, {
|
|
740
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
741
|
+
reason: 'change_created',
|
|
742
|
+
});
|
|
743
|
+
return result;
|
|
637
744
|
}
|
|
638
745
|
case 'change.update': {
|
|
746
|
+
const change = changes.updateChange(
|
|
747
|
+
db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
748
|
+
);
|
|
749
|
+
taskLedger.publishTaskLedger(db, {
|
|
750
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
751
|
+
reason: 'change_updated',
|
|
752
|
+
});
|
|
639
753
|
return {
|
|
640
754
|
ok: true,
|
|
641
|
-
change
|
|
642
|
-
db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
643
|
-
),
|
|
755
|
+
change,
|
|
644
756
|
};
|
|
645
757
|
}
|
|
646
758
|
case 'change.get': {
|
|
@@ -691,10 +803,24 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
691
803
|
};
|
|
692
804
|
}
|
|
693
805
|
case 'change.converge': {
|
|
694
|
-
|
|
806
|
+
const result = changes.convergeChange(
|
|
807
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
808
|
+
);
|
|
809
|
+
taskLedger.publishTaskLedger(db, {
|
|
810
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
811
|
+
reason: 'change_converged',
|
|
812
|
+
});
|
|
813
|
+
return result;
|
|
695
814
|
}
|
|
696
815
|
case 'change.archive': {
|
|
697
|
-
|
|
816
|
+
const result = changes.archiveChange(
|
|
817
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
818
|
+
);
|
|
819
|
+
taskLedger.publishTaskLedger(db, {
|
|
820
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
821
|
+
reason: 'change_archived',
|
|
822
|
+
});
|
|
823
|
+
return result;
|
|
698
824
|
}
|
|
699
825
|
case 'change.delta': {
|
|
700
826
|
return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
@@ -851,6 +977,19 @@ function startServer({
|
|
|
851
977
|
};
|
|
852
978
|
const getDb = (toolName = null) => {
|
|
853
979
|
if (!db) {
|
|
980
|
+
if (toolName === 'ultra.context') {
|
|
981
|
+
if (!fs.existsSync(authorityDbPath)) {
|
|
982
|
+
const error = new Error(
|
|
983
|
+
'Ultra authority is not initialized; invoke ultra-init before reading project context',
|
|
984
|
+
);
|
|
985
|
+
error.code = 'STATE_DB_MISSING';
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
db = openStateDb(authorityDbPath);
|
|
989
|
+
ensureSchemaVersion(db);
|
|
990
|
+
assertStateAuthority(db, rootDir, { importTeamLedger: false });
|
|
991
|
+
return db;
|
|
992
|
+
}
|
|
854
993
|
const projectPaths = runtimePaths.pathsFor(rootDir);
|
|
855
994
|
if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
|
|
856
995
|
// The packaged launcher has no explicit DB override. Its default path
|
|
@@ -874,9 +1013,20 @@ function startServer({
|
|
|
874
1013
|
});
|
|
875
1014
|
}
|
|
876
1015
|
db = initStateDb(authorityDbPath).db;
|
|
1016
|
+
const teamSync = assertStateAuthority(db, rootDir, { importTeamLedger: true });
|
|
1017
|
+
if (teamSync?.status === 'imported') {
|
|
1018
|
+
runtimeState.enqueueProjection(db, {
|
|
1019
|
+
tool_name: 'task.ledger_import',
|
|
1020
|
+
});
|
|
1021
|
+
runtimeState.processProjectionJobs(db, {
|
|
1022
|
+
rootDir,
|
|
1023
|
+
project,
|
|
1024
|
+
limit: 500,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
877
1027
|
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
878
1028
|
}
|
|
879
|
-
|
|
1029
|
+
if (!['system.doctor', 'ultra.doctor'].includes(toolName)
|
|
880
1030
|
&& (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
|
|
881
1031
|
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
882
1032
|
if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
|
|
@@ -892,6 +1042,7 @@ function startServer({
|
|
|
892
1042
|
};
|
|
893
1043
|
|
|
894
1044
|
const tools = loadRegisteredTools();
|
|
1045
|
+
const publicTools = tools.filter((tool) => PUBLIC_TOOLS.includes(tool.name));
|
|
895
1046
|
const ajv = buildAjv();
|
|
896
1047
|
const inputValidators = new Map();
|
|
897
1048
|
const outputValidators = new Map();
|
|
@@ -906,7 +1057,7 @@ function startServer({
|
|
|
906
1057
|
);
|
|
907
1058
|
|
|
908
1059
|
server.setRequestHandler('tools/list', async () => ({
|
|
909
|
-
tools:
|
|
1060
|
+
tools: publicTools.map((t) => ({
|
|
910
1061
|
name: t.name,
|
|
911
1062
|
description: t.description,
|
|
912
1063
|
inputSchema: t.input_schema,
|
|
@@ -952,20 +1103,51 @@ function startServer({
|
|
|
952
1103
|
let result;
|
|
953
1104
|
let toolDb = null;
|
|
954
1105
|
try {
|
|
955
|
-
toolDb = STATELESS_TOOLS.has(name)
|
|
1106
|
+
toolDb = (STATELESS_TOOLS.has(name) || isStatelessPublicCall(name, args))
|
|
1107
|
+
? null
|
|
1108
|
+
: getDb(name);
|
|
956
1109
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1110
|
+
if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
|
|
1111
|
+
taskLedger.syncTaskLedger(toolDb, { rootDir });
|
|
1112
|
+
}
|
|
1113
|
+
const callContext = {
|
|
1114
|
+
rootDir,
|
|
1115
|
+
sessionId,
|
|
1116
|
+
projector: project,
|
|
1117
|
+
markPlanRecoveryRequired,
|
|
1118
|
+
};
|
|
1119
|
+
const callLegacy = async (legacyName, legacyArgs = {}) => {
|
|
1120
|
+
if (!LEGACY_TOOLS.includes(legacyName)) {
|
|
1121
|
+
const error = new Error(`legacy operation ${legacyName} is not supported`);
|
|
1122
|
+
error.code = 'ULTRA_RECORD_OPERATION_UNSUPPORTED';
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
const validateLegacyInput = inputValidators.get(legacyName);
|
|
1126
|
+
if (!validateLegacyInput(legacyArgs)) {
|
|
1127
|
+
const error = new Error(ajv.errorsText(validateLegacyInput.errors));
|
|
1128
|
+
error.code = 'VALIDATION_ERROR';
|
|
1129
|
+
error.details = validateLegacyInput.errors;
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
const legacyResult = await dispatchTool(
|
|
1133
|
+
legacyName,
|
|
1134
|
+
legacyArgs,
|
|
1135
|
+
toolDb,
|
|
1136
|
+
callContext,
|
|
1137
|
+
);
|
|
1138
|
+
const validateLegacyOutput = outputValidators.get(legacyName);
|
|
1139
|
+
if (!validateLegacyOutput(legacyResult)) {
|
|
1140
|
+
const error = new Error(ajv.errorsText(validateLegacyOutput.errors));
|
|
1141
|
+
error.code = 'OUTPUT_SCHEMA_DRIFT';
|
|
1142
|
+
error.details = validateLegacyOutput.errors;
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
return legacyResult;
|
|
1146
|
+
};
|
|
1147
|
+
result = PUBLIC_TOOLS.includes(name)
|
|
1148
|
+
? await ultraFacade.dispatch(name, args, toolDb, { ...callContext, callLegacy })
|
|
1149
|
+
: await dispatchTool(name, args, toolDb, callContext);
|
|
1150
|
+
if (['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
|
|
969
1151
|
planRecovery = result.repair?.plan_publications || null;
|
|
970
1152
|
}
|
|
971
1153
|
} catch (err) {
|
|
@@ -976,7 +1158,11 @@ function startServer({
|
|
|
976
1158
|
}
|
|
977
1159
|
|
|
978
1160
|
let runtimeMeta = null;
|
|
979
|
-
|
|
1161
|
+
const shouldProject = toolDb
|
|
1162
|
+
&& projectOnWrite
|
|
1163
|
+
&& MUTATING_TOOLS.has(name)
|
|
1164
|
+
&& !(name === 'task.ledger_import' && result.already_current === true);
|
|
1165
|
+
if (shouldProject) {
|
|
980
1166
|
const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
|
|
981
1167
|
const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
|
|
982
1168
|
const own = processed.jobs.find((item) => item.id === job.id);
|
|
@@ -986,7 +1172,7 @@ function startServer({
|
|
|
986
1172
|
projection_job_id: job.id,
|
|
987
1173
|
};
|
|
988
1174
|
if (own && own.incident_id) runtimeMeta.incident_id = own.incident_id;
|
|
989
|
-
} else if (toolDb &&
|
|
1175
|
+
} else if (toolDb && ['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
|
|
990
1176
|
runtimeMeta = {
|
|
991
1177
|
state_commit: 'committed',
|
|
992
1178
|
projection_status: result.checks.projections.status === 'pass' ? 'completed' : 'failed',
|
|
@@ -1124,6 +1310,8 @@ module.exports = {
|
|
|
1124
1310
|
ARTIFACT_TOOLS,
|
|
1125
1311
|
SYSTEM_TOOLS,
|
|
1126
1312
|
REGISTERED_TOOLS,
|
|
1313
|
+
LEGACY_TOOLS,
|
|
1314
|
+
PUBLIC_TOOLS,
|
|
1127
1315
|
STATELESS_TOOLS,
|
|
1128
1316
|
MUTATING_TOOLS,
|
|
1129
1317
|
appendHookLifecycleEvent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Explicitly invoked Ultra Builder Pro workflows for Claude Code, OpenCode, Codex CLI, and Kimi Code, with project-local MCP authority and lifecycle recovery.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|