ultra-builder-pro-cli 0.20.0 → 0.22.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 (109) hide show
  1. package/.ultra-template/docs/research/README.md +26 -5
  2. package/.ultra-template/specs/discovery.md +1 -1
  3. package/.ultra-template/tasks/tasks.json +10 -3
  4. package/.ultra-template/templates/task-context.md +19 -0
  5. package/CHANGELOG.md +106 -0
  6. package/README.md +215 -41
  7. package/adapters/_shared/codex-assets.cjs +69 -11
  8. package/adapters/_shared/interaction-contract.cjs +36 -3
  9. package/adapters/_shared/runtime-assets.cjs +41 -1
  10. package/adapters/claude.js +7 -5
  11. package/adapters/codex.js +1 -1
  12. package/adapters/kimi.js +2 -8
  13. package/adapters/opencode.js +43 -30
  14. package/bin/orchestrator.js +64 -24
  15. package/docs/AGENT-CONTEXT.md +89 -49
  16. package/docs/ARCHITECTURE.md +112 -45
  17. package/docs/ARTIFACT-AUTHORITY.md +274 -0
  18. package/docs/COMMIT-HASH-BACKFILL.md +33 -90
  19. package/docs/DECISIONS.md +14 -6
  20. package/docs/LEGACY-CLI-CROSSWALK.md +16 -6
  21. package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -4
  22. package/docs/ROADMAP.md +20 -6
  23. package/docs/RUNTIME-COMPAT-MATRIX.md +29 -16
  24. package/docs/STATE-DB-ACCESS-POLICY.md +43 -13
  25. package/docs/WORKFLOW-LIFECYCLE.md +105 -50
  26. package/hooks/README.md +10 -8
  27. package/hooks/active_task_context.py +105 -18
  28. package/hooks/context_spine.py +16 -4
  29. package/hooks/health_check.py +5 -4
  30. package/hooks/pre_stop_check.py +5 -2
  31. package/hooks/runtime_paths.py +396 -0
  32. package/hooks/subagent_tracker.py +5 -2
  33. package/hooks/workflow_checkpoint.py +16 -3
  34. package/hooks/workflow_context.py +11 -3
  35. package/hooks/workflow_resume.py +7 -4
  36. package/mcp-server/breadcrumb.cjs +20 -2
  37. package/mcp-server/lib/archive-journal.cjs +1026 -111
  38. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  39. package/mcp-server/lib/artifact-registry.cjs +1127 -0
  40. package/mcp-server/lib/baseline-workflow.cjs +69 -28
  41. package/mcp-server/lib/change-packet.cjs +818 -0
  42. package/mcp-server/lib/change-workflow.cjs +932 -176
  43. package/mcp-server/lib/context-paths.cjs +142 -0
  44. package/mcp-server/lib/context-spine.cjs +511 -45
  45. package/mcp-server/lib/decision-dialogue.cjs +345 -3
  46. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  47. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  48. package/mcp-server/lib/doctor.cjs +401 -11
  49. package/mcp-server/lib/git-bootstrap.cjs +458 -47
  50. package/mcp-server/lib/init-project.cjs +240 -95
  51. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  52. package/mcp-server/lib/plan-store.cjs +700 -5
  53. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  54. package/mcp-server/lib/projector.cjs +211 -42
  55. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  56. package/mcp-server/lib/safe-project-file.cjs +385 -0
  57. package/mcp-server/lib/spec-learning.cjs +28 -12
  58. package/mcp-server/lib/state-authority.cjs +19 -3
  59. package/mcp-server/lib/state-db.cjs +263 -9
  60. package/mcp-server/lib/state-ops.cjs +254 -50
  61. package/mcp-server/lib/task-ledger.cjs +1342 -0
  62. package/mcp-server/lib/telemetry.cjs +12 -3
  63. package/mcp-server/lib/workflow-state.cjs +610 -58
  64. package/mcp-server/server.cjs +423 -42
  65. package/orchestrator/daemon.cjs +2 -10
  66. package/orchestrator/parallel-orchestrator.cjs +314 -50
  67. package/orchestrator/planner/plan-builder.cjs +1 -1
  68. package/orchestrator/recovery.cjs +125 -7
  69. package/orchestrator/session-close-journal-worker.cjs +5 -0
  70. package/orchestrator/session-close-journal.cjs +1014 -0
  71. package/orchestrator/session-runner.cjs +679 -69
  72. package/orchestrator/worktree-manager.cjs +186 -35
  73. package/package.json +2 -3
  74. package/skills/cc-collab/SKILL.md +4 -1
  75. package/skills/codex-collab/SKILL.md +5 -2
  76. package/skills/ultra-change/SKILL.md +34 -8
  77. package/skills/ultra-deliver/SKILL.md +26 -11
  78. package/skills/ultra-dev/SKILL.md +52 -10
  79. package/skills/ultra-doctor/SKILL.md +16 -3
  80. package/skills/ultra-init/SKILL.md +22 -11
  81. package/skills/ultra-plan/SKILL.md +58 -18
  82. package/skills/ultra-research/SKILL.md +37 -35
  83. package/skills/ultra-review/SKILL.md +14 -8
  84. package/skills/ultra-status/SKILL.md +18 -9
  85. package/skills/ultra-test/SKILL.md +11 -5
  86. package/skills/ultra-think/SKILL.md +6 -2
  87. package/skills/ultra-think/references/decision-dialogue.md +17 -6
  88. package/skills/ultra-verify/SKILL.md +4 -1
  89. package/spec/cli-protocol.md +21 -7
  90. package/spec/mcp-tools.yaml +629 -44
  91. package/spec/schemas/change-delta.v1.schema.json +153 -0
  92. package/spec/schemas/context-file.v4.5.schema.json +1 -1
  93. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  94. package/spec/schemas/mcp-tools.schema.json +1 -1
  95. package/spec/schemas/state-db.sql +51 -14
  96. package/spec/schemas/task-ledger.v1.schema.json +217 -0
  97. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  98. package/ultra-tools/cli.cjs +3 -2
  99. package/ultra-tools/commands/db.cjs +76 -25
  100. package/ultra-tools/commands/migrate.cjs +1000 -82
  101. package/ultra-tools/commands/session.cjs +34 -8
  102. package/ultra-tools/commands/status.cjs +32 -6
  103. package/ultra-tools/commands/system.cjs +129 -22
  104. package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -19
  105. package/commands/learn.md +0 -10
  106. package/orchestrator/skill-miner.cjs +0 -133
  107. package/output-styles/coding-vibes.md +0 -67
  108. package/output-styles/structural-thinking.md +0 -61
  109. package/skills/learn/SKILL.md +0 -58
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
4
- // .ultra/state.db. Uses the low-level Server API from
4
+ // .ultra/.runtime/state.db. Uses the low-level Server API from
5
5
  // @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
6
6
  // straight from spec/mcp-tools.yaml without translating to zod.
7
7
 
@@ -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');
@@ -30,10 +31,14 @@ const planStore = require('./lib/plan-store.cjs');
30
31
  const { initProject } = require('./lib/init-project.cjs');
31
32
  const baselines = require('./lib/baseline-workflow.cjs');
32
33
  const changes = require('./lib/change-workflow.cjs');
34
+ const contextSpine = require('./lib/context-spine.cjs');
33
35
  const runtimeState = require('./lib/runtime-state.cjs');
36
+ const runtimePaths = require('./lib/runtime-paths.cjs');
37
+ const gitBootstrap = require('./lib/git-bootstrap.cjs');
34
38
  const workflows = require('./lib/workflow-state.cjs');
35
39
  const decisions = require('./lib/decision-dialogue.cjs');
36
40
  const doctor = require('./lib/doctor.cjs');
41
+ const artifactRegistry = require('./lib/artifact-registry.cjs');
37
42
  const {
38
43
  readProjectBreadcrumb,
39
44
  renderProjectBreadcrumb,
@@ -58,6 +63,9 @@ const TASK_TOOLS = Object.freeze([
58
63
  'task.dependency_topo',
59
64
  'task.parse_prd',
60
65
  'task.expand',
66
+ 'task.ledger_get',
67
+ 'task.ledger_publish',
68
+ 'task.ledger_import',
61
69
  ]);
62
70
 
63
71
  const SESSION_TOOLS = Object.freeze([
@@ -85,6 +93,8 @@ const BASELINE_TOOLS = Object.freeze([
85
93
  const CHANGE_TOOLS = Object.freeze([
86
94
  'change.create',
87
95
  'change.update',
96
+ 'change.delta',
97
+ 'change.documentation_reconcile',
88
98
  'change.get',
89
99
  'change.list',
90
100
  'change.context',
@@ -100,6 +110,8 @@ const WORKFLOW_TOOLS = Object.freeze([
100
110
  'workflow.get',
101
111
  'workflow.list',
102
112
  'workflow.step',
113
+ 'workflow.revise',
114
+ 'workflow.supersede',
103
115
  'workflow.complete',
104
116
  ]);
105
117
 
@@ -120,10 +132,16 @@ const SYSTEM_TOOLS = Object.freeze([
120
132
  'system.doctor',
121
133
  ]);
122
134
 
135
+ const ARTIFACT_TOOLS = Object.freeze([
136
+ 'artifact.record',
137
+ 'artifact.get',
138
+ ]);
139
+
123
140
  const REGISTERED_TOOLS = Object.freeze([
124
141
  ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
125
142
  ...DECISION_TOOLS,
126
143
  ...WORKFLOW_TOOLS,
144
+ ...ARTIFACT_TOOLS,
127
145
  ...SYSTEM_TOOLS,
128
146
  ]);
129
147
 
@@ -134,14 +152,26 @@ const STATELESS_TOOLS = new Set(['task.init_project']);
134
152
  const MUTATING_TOOLS = new Set([
135
153
  'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
136
154
  'task.parse_prd', 'task.expand',
155
+ 'task.ledger_import',
137
156
  'session.spawn', 'session.close', 'session.heartbeat',
138
157
  'plan.export',
139
158
  'baseline.start', 'baseline.record', 'baseline.converge',
140
- 'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
159
+ 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
160
+ 'change.context', 'change.converge', 'change.archive',
141
161
  'change.learning_propose', 'change.learning_resolve',
142
162
  'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
143
163
  'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
144
- 'workflow.start', 'workflow.step', 'workflow.complete',
164
+ 'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
165
+ 'workflow.complete',
166
+ 'artifact.record',
167
+ ]);
168
+
169
+ const TEAM_LEDGER_SYNC_TOOLS = new Set([
170
+ 'task.create', 'task.update', 'task.delete', 'task.parse_prd', 'task.expand',
171
+ 'plan.export',
172
+ 'baseline.start', 'baseline.record', 'baseline.converge',
173
+ 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
174
+ 'change.converge', 'change.archive',
145
175
  ]);
146
176
 
147
177
  function loadRegisteredTools() {
@@ -167,7 +197,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
167
197
  return { id: task.id, status: task.status, created_at: task.created_at };
168
198
  }
169
199
  case 'task.update': {
200
+ const before = ops.readTask(db, input.id);
170
201
  const task = ops.patchTask(db, input.id, input.patch || {});
202
+ const durableChanged = before
203
+ && taskLedger.durableTask(before).digest !== taskLedger.durableTask(task).digest;
204
+ if (durableChanged) {
205
+ taskLedger.publishTaskLedger(db, {
206
+ rootDir: ctx.rootDir || process.cwd(),
207
+ reason: before.status !== task.status
208
+ ? `task_${task.status}`
209
+ : 'task_contract_updated',
210
+ });
211
+ }
171
212
  return { ok: true, task };
172
213
  }
173
214
  case 'task.list': {
@@ -184,7 +225,12 @@ async function dispatchTool(name, input, db, ctx = {}) {
184
225
  return { task };
185
226
  }
186
227
  case 'task.delete': {
187
- return ops.deleteTask(db, input.id, { force: !!input.force });
228
+ const result = ops.deleteTask(db, input.id, { force: !!input.force });
229
+ taskLedger.publishTaskLedger(db, {
230
+ rootDir: ctx.rootDir || process.cwd(),
231
+ reason: 'task_deleted',
232
+ });
233
+ return result;
188
234
  }
189
235
  case 'task.init_project': {
190
236
  return initProject(input);
@@ -260,12 +306,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
260
306
  return { session };
261
307
  }
262
308
  case 'session.admission_check': {
263
- sessionRunner.assertSessionTaskReady(
309
+ return sessionRunner.admissionCheck(
264
310
  db,
265
311
  ctx.rootDir || process.cwd(),
266
312
  input.task_id,
267
313
  );
268
- return ops.admissionCheck(db, input.task_id);
269
314
  }
270
315
  case 'session.list': {
271
316
  const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
@@ -395,8 +440,58 @@ async function dispatchTool(name, input, db, ctx = {}) {
395
440
  children: input.children,
396
441
  rootDir,
397
442
  });
443
+ taskLedger.publishTaskLedger(db, {
444
+ rootDir,
445
+ reason: 'task_expanded',
446
+ });
398
447
  return { parent_id: result.parent_id, children: result.children };
399
448
  }
449
+ case 'task.ledger_get': {
450
+ const rootDir = ctx.rootDir || process.cwd();
451
+ return {
452
+ ledger: taskLedger.readTaskLedger(rootDir),
453
+ health: taskLedger.inspectTaskLedger(db, { rootDir }),
454
+ };
455
+ }
456
+ case 'task.ledger_publish': {
457
+ const result = taskLedger.publishTaskLedger(db, {
458
+ rootDir: ctx.rootDir || process.cwd(),
459
+ reason: input.reason || 'manual_checkpoint',
460
+ });
461
+ const output = {
462
+ changed: result.changed,
463
+ path: path.relative(ctx.rootDir || process.cwd(), result.path),
464
+ generation: result.ledger.generation,
465
+ state_digest: result.ledger.state_digest,
466
+ };
467
+ if (result.migrated_legacy_projection) {
468
+ output.migrated_legacy_projection = true;
469
+ output.legacy_backup_path = path.relative(
470
+ ctx.rootDir || process.cwd(),
471
+ result.legacy_backup_path,
472
+ );
473
+ }
474
+ return output;
475
+ }
476
+ case 'task.ledger_import': {
477
+ const result = taskLedger.importTaskLedger(db, {
478
+ rootDir: ctx.rootDir || process.cwd(),
479
+ });
480
+ return {
481
+ imported: result.imported,
482
+ preserved: result.preserved,
483
+ deleted: result.deleted,
484
+ imported_changes: result.imported_changes,
485
+ preserved_changes: result.preserved_changes,
486
+ imported_baseline: result.imported_baseline,
487
+ preserved_baseline: result.preserved_baseline,
488
+ generation: result.generation,
489
+ state_digest: result.state_digest,
490
+ requires_plan_revalidation: result.requires_plan_revalidation,
491
+ requires_baseline_revalidation: result.requires_baseline_revalidation,
492
+ already_current: result.already_current,
493
+ };
494
+ }
400
495
  case 'plan.export': {
401
496
  const rootDir = ctx.rootDir || process.cwd();
402
497
  const change = changes.readChange(db, input.change_id);
@@ -410,41 +505,179 @@ async function dispatchTool(name, input, db, ctx = {}) {
410
505
  err.code = 'CHANGE_NOT_MUTABLE';
411
506
  throw err;
412
507
  }
413
- const abs = path.isAbsolute(input.out_path)
414
- ? input.out_path
415
- : path.join(rootDir, input.out_path);
416
- const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
508
+ const tasks = ops.listTasks(db, { change_id: change.id });
417
509
  if (tasks.length === 0) {
418
510
  const err = new Error('no tasks to plan');
419
511
  err.code = 'NO_TASKS';
420
512
  throw err;
421
513
  }
422
- const plan = planStore.buildPlan(tasks, { changeId: change.id });
423
- const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
424
- ops.appendEvent(db, {
425
- type: 'plan_exported',
514
+ const context = contextSpine.validateContextSnapshot(db, {
426
515
  change_id: change.id,
427
- payload: {
428
- change_id: change.id, plan_path, wave_count: plan.waves.length, tag: input.tag || null,
516
+ task_id: null,
517
+ role: 'plan',
518
+ gate: 'planning',
519
+ }, { rootDir });
520
+ if (!context.snapshot || context.blockers.length > 0) {
521
+ const err = new Error('plan export requires a current planning Context Manifest');
522
+ err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
523
+ err.details = { blockers: context.blockers };
524
+ throw err;
525
+ }
526
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
527
+ const planRun = db.prepare(
528
+ `SELECT id FROM workflow_runs
529
+ WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
530
+ AND status IN ('active', 'blocked', 'ready')
531
+ ORDER BY started_at DESC, rowid DESC LIMIT 1`,
532
+ ).get(change.id);
533
+ const owner = { type: 'change', id: change.id };
534
+ const contextArtifact = db.prepare(
535
+ `SELECT id FROM artifacts
536
+ WHERE path = ? AND status <> 'archived'
537
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
538
+ ).get(context.snapshot.manifest_path);
539
+ const publication = planStore.prepareChangePlanPublication(plan, {
540
+ rootDir,
541
+ change,
542
+ tasks,
543
+ context: {
544
+ snapshot_id: context.snapshot.id,
545
+ manifest_path: context.snapshot.manifest_path,
546
+ manifest_digest: context.snapshot.manifest_hash,
547
+ },
548
+ });
549
+ const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
550
+ kind,
551
+ file,
552
+ digest,
553
+ input: {
554
+ owner_type: owner.type,
555
+ owner_id: owner.id,
556
+ kind,
557
+ path: path.relative(rootDir, file),
558
+ source_refs: [
559
+ { type: 'change', id: change.id, relation: 'planned_for' },
560
+ ...(contextArtifact
561
+ ? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
562
+ : []),
563
+ ...tasks.map((task) => ({
564
+ type: 'task', id: task.id, relation: 'compiled_from_task_contract',
565
+ })),
566
+ ],
567
+ consumer_refs: planRun
568
+ ? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
569
+ : [],
570
+ provenance: {
571
+ writer: 'plan.export',
572
+ workflow_run_id: planRun?.id || null,
573
+ publication_transaction_id: publication.transaction_id,
574
+ },
575
+ metadata: {
576
+ context_snapshot_id: context.snapshot.id,
577
+ context_digest: context.snapshot.manifest_hash,
578
+ terminal_role: !planRun,
579
+ },
429
580
  },
581
+ }));
582
+ try {
583
+ ops.tx(db, () => {
584
+ const preflight = artifactInputs.map((artifact) => (
585
+ artifactRegistry.preflightArtifactPublication(
586
+ db, artifact.input, { rootDir },
587
+ )
588
+ ));
589
+ publication.publish();
590
+ const recorded = artifactInputs.map((artifact, index) => (
591
+ artifactRegistry.recordArtifactInTx(db, {
592
+ ...artifact.input,
593
+ id: preflight[index].artifact_id || undefined,
594
+ expected_before_digest: preflight[index].expected_before_digest,
595
+ content_digest: artifact.digest,
596
+ }, { rootDir })
597
+ ));
598
+ ops.appendEventInTx(db, {
599
+ type: 'plan_exported',
600
+ change_id: change.id,
601
+ payload: {
602
+ change_id: change.id,
603
+ plan_path: publication.plan_path,
604
+ plan_md_path: publication.plan_md_path,
605
+ publication_transaction_id: publication.transaction_id,
606
+ wave_count: plan.waves.length,
607
+ },
608
+ });
609
+ return recorded;
610
+ });
611
+ } catch (error) {
612
+ const rollback = publication.rollback();
613
+ const failure = rollback?.rolled_back === false
614
+ ? planStore.planRecoveryRequiredError(
615
+ error,
616
+ rollback,
617
+ publication.transaction_id,
618
+ )
619
+ : error;
620
+ if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
621
+ const issue = failure.details?.rollback_issue
622
+ || failure.details?.issue
623
+ || {
624
+ code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
625
+ message: failure.cause?.message || failure.message,
626
+ };
627
+ ctx.markPlanRecoveryRequired?.({
628
+ recovered: 0,
629
+ finalized: 0,
630
+ pending: 1,
631
+ issues: [issue],
632
+ });
633
+ }
634
+ throw failure;
635
+ }
636
+ try {
637
+ publication.commit();
638
+ } catch (error) {
639
+ const issue = error.details?.issue || {
640
+ code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
641
+ message: error.cause?.message || error.message,
642
+ };
643
+ ctx.markPlanRecoveryRequired?.({
644
+ recovered: 0,
645
+ finalized: 0,
646
+ pending: 1,
647
+ issues: [issue],
648
+ });
649
+ // The registry and target bytes committed atomically before cleanup.
650
+ // The remaining journal is still authoritative recovery work, so the
651
+ // caller must see a typed failure and later mutations must stop.
652
+ throw error;
653
+ }
654
+ taskLedger.publishTaskLedger(db, {
655
+ rootDir,
656
+ reason: 'plan_accepted',
430
657
  });
431
- return { plan_path, wave_count: plan.waves.length };
658
+ return {
659
+ plan_path: publication.plan_path,
660
+ plan_md_path: publication.plan_md_path,
661
+ wave_count: plan.waves.length,
662
+ };
432
663
  }
433
664
  case 'plan.get': {
434
665
  const rootDir = ctx.rootDir || process.cwd();
435
- const loaded = planStore.loadPlanArtifact(rootDir);
666
+ const change = changes.readChange(db, input.change_id);
667
+ if (!change) {
668
+ const err = new Error(`change ${input.change_id} not found`);
669
+ err.code = 'CHANGE_NOT_FOUND';
670
+ throw err;
671
+ }
672
+ const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
673
+ db,
674
+ strict: true,
675
+ });
436
676
  if (!loaded) {
437
677
  const err = new Error('no plan has been computed yet');
438
678
  err.code = 'NO_PLAN';
439
679
  throw err;
440
680
  }
441
- if (loaded.change_id !== input.change_id) {
442
- const err = new Error(
443
- `execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
444
- );
445
- err.code = 'PLAN_CHANGE_MISMATCH';
446
- throw err;
447
- }
448
681
  return { plan: planStore.selectSection(loaded, input.section) };
449
682
  }
450
683
  case 'baseline.start': {
@@ -476,21 +709,42 @@ async function dispatchTool(name, input, db, ctx = {}) {
476
709
  };
477
710
  }
478
711
  case 'baseline.converge': {
479
- return baselines.convergeBaseline(
712
+ const result = baselines.convergeBaseline(
480
713
  db, input, { rootDir: ctx.rootDir || process.cwd() },
481
714
  );
715
+ if (result.ready) {
716
+ taskLedger.publishTaskLedger(db, {
717
+ rootDir: ctx.rootDir || process.cwd(),
718
+ reason: 'baseline_converged',
719
+ });
720
+ }
721
+ return result;
482
722
  }
483
723
  case 'change.create': {
484
724
  const { randomUUID } = require('node:crypto');
485
725
  const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
486
- return changes.createChange(db, { ...input, id }, { rootDir: ctx.rootDir || process.cwd() });
726
+ const result = changes.createChange(
727
+ db,
728
+ { ...input, id },
729
+ { rootDir: ctx.rootDir || process.cwd() },
730
+ );
731
+ taskLedger.publishTaskLedger(db, {
732
+ rootDir: ctx.rootDir || process.cwd(),
733
+ reason: 'change_created',
734
+ });
735
+ return result;
487
736
  }
488
737
  case 'change.update': {
738
+ const change = changes.updateChange(
739
+ db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
740
+ );
741
+ taskLedger.publishTaskLedger(db, {
742
+ rootDir: ctx.rootDir || process.cwd(),
743
+ reason: 'change_updated',
744
+ });
489
745
  return {
490
746
  ok: true,
491
- change: changes.updateChange(
492
- db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
493
- ),
747
+ change,
494
748
  };
495
749
  }
496
750
  case 'change.get': {
@@ -541,10 +795,32 @@ async function dispatchTool(name, input, db, ctx = {}) {
541
795
  };
542
796
  }
543
797
  case 'change.converge': {
544
- return changes.convergeChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
798
+ const result = changes.convergeChange(
799
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
800
+ );
801
+ taskLedger.publishTaskLedger(db, {
802
+ rootDir: ctx.rootDir || process.cwd(),
803
+ reason: 'change_converged',
804
+ });
805
+ return result;
545
806
  }
546
807
  case 'change.archive': {
547
- return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
808
+ const result = changes.archiveChange(
809
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
810
+ );
811
+ taskLedger.publishTaskLedger(db, {
812
+ rootDir: ctx.rootDir || process.cwd(),
813
+ reason: 'change_archived',
814
+ });
815
+ return result;
816
+ }
817
+ case 'change.delta': {
818
+ return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
819
+ }
820
+ case 'change.documentation_reconcile': {
821
+ return changes.recordDocumentationReconciliation(
822
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
823
+ );
548
824
  }
549
825
  case 'decision.thread_start': {
550
826
  return { thread: decisions.startDecisionThread(db, input) };
@@ -578,7 +854,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
578
854
  return { thread: decisions.supersedeDecision(db, input) };
579
855
  }
580
856
  case 'decision.complete': {
581
- return { thread: decisions.completeDecisionThread(db, input) };
857
+ return {
858
+ thread: decisions.completeDecisionThread(
859
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
860
+ ),
861
+ };
582
862
  }
583
863
  case 'decision.checkpoint': {
584
864
  return {
@@ -618,6 +898,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
618
898
  ),
619
899
  };
620
900
  }
901
+ case 'workflow.revise': {
902
+ return {
903
+ workflow: workflows.reviseWorkflow(
904
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
905
+ ),
906
+ };
907
+ }
908
+ case 'workflow.supersede': {
909
+ return workflows.supersedeWorkflow(
910
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
911
+ );
912
+ }
621
913
  case 'workflow.complete': {
622
914
  return {
623
915
  workflow: workflows.completeWorkflow(
@@ -625,6 +917,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
625
917
  ),
626
918
  };
627
919
  }
920
+ case 'artifact.record': {
921
+ return artifactRegistry.recordArtifact(
922
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
923
+ );
924
+ }
925
+ case 'artifact.get': {
926
+ return { artifact: artifactRegistry.getArtifact(db, input) };
927
+ }
628
928
  case 'system.doctor': {
629
929
  return doctor.runDoctor(db, {
630
930
  rootDir: ctx.rootDir || process.cwd(),
@@ -657,8 +957,66 @@ function startServer({
657
957
  project = projector.projectAll,
658
958
  }) {
659
959
  let db = null;
660
- const getDb = () => {
661
- if (!db) db = initStateDb(dbPath).db;
960
+ let planRecovery = null;
961
+ let authorityDbPath = path.resolve(dbPath);
962
+ const markPlanRecoveryRequired = (details) => {
963
+ planRecovery = {
964
+ recovered: Number(details?.recovered || 0),
965
+ finalized: Number(details?.finalized || 0),
966
+ pending: Math.max(1, Number(details?.pending || 0)),
967
+ issues: Array.isArray(details?.issues) ? details.issues : [],
968
+ };
969
+ };
970
+ const getDb = (toolName = null) => {
971
+ if (!db) {
972
+ const projectPaths = runtimePaths.pathsFor(rootDir);
973
+ if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
974
+ // The packaged launcher has no explicit DB override. Its default path
975
+ // is the future canonical runtime location, which can legitimately be
976
+ // absent while a legacy project still owns its project-level DB. Admit and
977
+ // migrate that project before applying configured-authority binding.
978
+ authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
979
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
980
+ }).stateDbPath;
981
+ } else {
982
+ // Only a real external/task-linked override reaches binding
983
+ // validation. It may never invent or borrow project authority.
984
+ runtimePaths.locateStateDb(rootDir, {
985
+ env: { UBP_DB_PATH: authorityDbPath },
986
+ });
987
+ runtimePaths.ensureRuntimeState(rootDir, {
988
+ env: { UBP_DB_PATH: authorityDbPath },
989
+ allowConfiguredRuntimeLink: true,
990
+ migrateState: false,
991
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
992
+ });
993
+ }
994
+ db = initStateDb(authorityDbPath).db;
995
+ const teamSync = assertStateAuthority(db, rootDir, { importTeamLedger: true });
996
+ if (teamSync?.status === 'imported') {
997
+ runtimeState.enqueueProjection(db, {
998
+ tool_name: 'task.ledger_import',
999
+ });
1000
+ runtimeState.processProjectionJobs(db, {
1001
+ rootDir,
1002
+ project,
1003
+ limit: 500,
1004
+ });
1005
+ }
1006
+ planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1007
+ }
1008
+ if (toolName !== 'system.doctor'
1009
+ && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
1010
+ planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1011
+ if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
1012
+ const error = new Error(
1013
+ 'plan publication recovery is incomplete; run system.doctor with repair=true',
1014
+ );
1015
+ error.code = 'PLAN_RECOVERY_REQUIRED';
1016
+ error.details = planRecovery;
1017
+ throw error;
1018
+ }
1019
+ }
662
1020
  return db;
663
1021
  };
664
1022
 
@@ -723,14 +1081,25 @@ function startServer({
723
1081
  let result;
724
1082
  let toolDb = null;
725
1083
  try {
726
- toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
1084
+ toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
727
1085
  if (toolDb) assertStateAuthority(toolDb, rootDir);
1086
+ if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
1087
+ taskLedger.syncTaskLedger(toolDb, { rootDir });
1088
+ }
728
1089
  result = await dispatchTool(
729
1090
  name,
730
1091
  args,
731
1092
  toolDb,
732
- { rootDir, sessionId, projector: project },
1093
+ {
1094
+ rootDir,
1095
+ sessionId,
1096
+ projector: project,
1097
+ markPlanRecoveryRequired,
1098
+ },
733
1099
  );
1100
+ if (name === 'system.doctor' && args.repair === true) {
1101
+ planRecovery = result.repair?.plan_publications || null;
1102
+ }
734
1103
  } catch (err) {
735
1104
  const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
736
1105
  toolError = code;
@@ -739,7 +1108,11 @@ function startServer({
739
1108
  }
740
1109
 
741
1110
  let runtimeMeta = null;
742
- if (toolDb && projectOnWrite && MUTATING_TOOLS.has(name)) {
1111
+ const shouldProject = toolDb
1112
+ && projectOnWrite
1113
+ && MUTATING_TOOLS.has(name)
1114
+ && !(name === 'task.ledger_import' && result.already_current === true);
1115
+ if (shouldProject) {
743
1116
  const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
744
1117
  const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
745
1118
  const own = processed.jobs.find((item) => item.id === job.id);
@@ -789,12 +1162,18 @@ function startServer({
789
1162
  }
790
1163
 
791
1164
  async function main() {
792
- const dbPath = process.env.UBP_DB_PATH
793
- ? path.resolve(process.env.UBP_DB_PATH)
794
- : path.resolve('.ultra', 'state.db');
795
1165
  const rootDir = process.env.UBP_ROOT_DIR
796
1166
  ? path.resolve(process.env.UBP_ROOT_DIR)
797
1167
  : path.resolve('.');
1168
+ // Keep tools/list side-effect free. Root/layout validation happens inside the
1169
+ // first state-backed tool call so clients receive a typed MCP error instead
1170
+ // of a transport-level startup failure.
1171
+ const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
1172
+ ? process.env.UBP_DB_PATH.trim()
1173
+ : '';
1174
+ const dbPath = configuredDb
1175
+ ? path.resolve(configuredDb)
1176
+ : runtimePaths.pathsFor(rootDir).stateDbPath;
798
1177
  const sessionId = process.env.UBP_SESSION_ID || null;
799
1178
  const handle = startServer({ dbPath, rootDir, sessionId });
800
1179
  const transport = new StdioServerTransport();
@@ -816,7 +1195,7 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
816
1195
  throw error;
817
1196
  }
818
1197
  const root = path.resolve(rootDir || process.cwd());
819
- const dbPath = path.join(root, '.ultra', 'state.db');
1198
+ const dbPath = runtimePaths.locateStateDb(root);
820
1199
  if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
821
1200
 
822
1201
  const db = openStateDb(dbPath);
@@ -878,11 +1257,13 @@ module.exports = {
878
1257
  BASELINE_TOOLS,
879
1258
  CHANGE_TOOLS,
880
1259
  WORKFLOW_TOOLS,
1260
+ ARTIFACT_TOOLS,
881
1261
  SYSTEM_TOOLS,
882
1262
  REGISTERED_TOOLS,
883
1263
  STATELESS_TOOLS,
884
1264
  MUTATING_TOOLS,
885
1265
  appendHookLifecycleEvent,
1266
+ findProjectRoot: runtimePaths.findProjectRoot,
886
1267
  readProjectBreadcrumb,
887
1268
  renderProjectBreadcrumb,
888
1269
  };