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.
Files changed (58) hide show
  1. package/.ultra-template/tasks/tasks.json +10 -3
  2. package/.ultra-template/templates/task-context.md +19 -0
  3. package/CHANGELOG.md +78 -0
  4. package/README.md +210 -36
  5. package/adapters/_shared/codex-assets.cjs +8 -3
  6. package/adapters/_shared/interaction-contract.cjs +13 -6
  7. package/adapters/_shared/runtime-assets.cjs +1 -1
  8. package/adapters/claude.js +1 -1
  9. package/adapters/kimi.js +1 -1
  10. package/adapters/opencode.js +24 -13
  11. package/docs/AGENT-CONTEXT.md +58 -51
  12. package/docs/ARCHITECTURE.md +82 -51
  13. package/docs/ARTIFACT-AUTHORITY.md +83 -44
  14. package/docs/COMMIT-HASH-BACKFILL.md +33 -90
  15. package/docs/DECISIONS.md +9 -7
  16. package/docs/LEGACY-CLI-CROSSWALK.md +10 -2
  17. package/docs/PLUGIN-ISOLATION-CONTRACT.md +3 -2
  18. package/docs/ROADMAP.md +3 -3
  19. package/docs/RUNTIME-COMPAT-MATRIX.md +13 -10
  20. package/docs/STATE-DB-ACCESS-POLICY.md +47 -19
  21. package/docs/WORKFLOW-LIFECYCLE.md +79 -49
  22. package/hooks/README.md +5 -4
  23. package/hooks/active_task_context.py +41 -39
  24. package/mcp-server/lib/artifact-registry.cjs +32 -7
  25. package/mcp-server/lib/baseline-workflow.cjs +60 -24
  26. package/mcp-server/lib/change-workflow.cjs +42 -6
  27. package/mcp-server/lib/context-paths.cjs +34 -3
  28. package/mcp-server/lib/doctor.cjs +28 -0
  29. package/mcp-server/lib/git-bootstrap.cjs +0 -1
  30. package/mcp-server/lib/init-project.cjs +52 -2
  31. package/mcp-server/lib/projector.cjs +43 -8
  32. package/mcp-server/lib/state-authority.cjs +19 -3
  33. package/mcp-server/lib/state-ops.cjs +6 -1
  34. package/mcp-server/lib/task-ledger.cjs +1342 -0
  35. package/mcp-server/lib/ultra-facade.cjs +646 -0
  36. package/mcp-server/lib/workflow-state.cjs +166 -2
  37. package/mcp-server/server.cjs +214 -26
  38. package/package.json +1 -1
  39. package/skills/ultra-change/SKILL.md +38 -90
  40. package/skills/ultra-deliver/SKILL.md +42 -62
  41. package/skills/ultra-dev/SKILL.md +34 -73
  42. package/skills/ultra-doctor/SKILL.md +26 -39
  43. package/skills/ultra-init/SKILL.md +37 -69
  44. package/skills/ultra-plan/SKILL.md +52 -125
  45. package/skills/ultra-research/SKILL.md +41 -101
  46. package/skills/ultra-review/SKILL.md +25 -44
  47. package/skills/ultra-status/SKILL.md +21 -40
  48. package/skills/ultra-test/SKILL.md +25 -60
  49. package/skills/ultra-think/SKILL.md +29 -48
  50. package/skills/ultra-think/references/decision-dialogue.md +6 -4
  51. package/spec/cli-protocol.md +15 -0
  52. package/spec/mcp-tools.yaml +286 -0
  53. package/spec/schemas/context-file.v4.5.schema.json +1 -1
  54. package/spec/schemas/mcp-tools.schema.json +1 -1
  55. package/spec/schemas/state-db.sql +1 -1
  56. package/spec/schemas/task-ledger.v1.schema.json +217 -0
  57. package/ultra-tools/commands/migrate.cjs +5 -2
  58. 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) blockers.push('WORKFLOW_ACTIVE_WITHOUT_OPEN_STEP');
661
- else if (run.current_step !== firstOpen.step_id) {
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,
@@ -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 REGISTERED_TOOLS = Object.freeze([
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
- return ops.deleteTask(db, input.id, { force: !!input.force });
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
- return baselines.convergeBaseline(
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
- return changes.createChange(db, { ...input, id }, { rootDir: ctx.rootDir || process.cwd() });
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: changes.updateChange(
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
- return changes.convergeChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
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
- return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
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
- if (toolName !== 'system.doctor'
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: tools.map((t) => ({
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) ? null : getDb(name);
1106
+ toolDb = (STATELESS_TOOLS.has(name) || isStatelessPublicCall(name, args))
1107
+ ? null
1108
+ : getDb(name);
956
1109
  if (toolDb) assertStateAuthority(toolDb, rootDir);
957
- result = await dispatchTool(
958
- name,
959
- args,
960
- toolDb,
961
- {
962
- rootDir,
963
- sessionId,
964
- projector: project,
965
- markPlanRecoveryRequired,
966
- },
967
- );
968
- if (name === 'system.doctor' && args.repair === true) {
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
- if (toolDb && projectOnWrite && MUTATING_TOOLS.has(name)) {
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 && name === 'system.doctor' && args.repair === true) {
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.21.1",
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",