ultra-builder-pro-cli 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) 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 +1 -1
  4. package/CHANGELOG.md +66 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +36 -3
  8. package/adapters/_shared/runtime-assets.cjs +41 -1
  9. package/adapters/claude.js +7 -5
  10. package/adapters/codex.js +1 -1
  11. package/adapters/kimi.js +2 -8
  12. package/adapters/opencode.js +30 -28
  13. package/bin/orchestrator.js +64 -24
  14. package/docs/AGENT-CONTEXT.md +74 -43
  15. package/docs/ARCHITECTURE.md +63 -31
  16. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  17. package/docs/DECISIONS.md +10 -4
  18. package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
  19. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  20. package/docs/ROADMAP.md +19 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
  22. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  23. package/docs/WORKFLOW-LIFECYCLE.md +70 -39
  24. package/hooks/README.md +6 -5
  25. package/hooks/active_task_context.py +97 -12
  26. package/hooks/context_spine.py +16 -4
  27. package/hooks/health_check.py +5 -4
  28. package/hooks/pre_stop_check.py +5 -2
  29. package/hooks/runtime_paths.py +396 -0
  30. package/hooks/subagent_tracker.py +5 -2
  31. package/hooks/workflow_checkpoint.py +16 -3
  32. package/hooks/workflow_context.py +11 -3
  33. package/hooks/workflow_resume.py +7 -4
  34. package/mcp-server/breadcrumb.cjs +20 -2
  35. package/mcp-server/lib/archive-journal.cjs +1026 -111
  36. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  37. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  38. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  39. package/mcp-server/lib/change-packet.cjs +818 -0
  40. package/mcp-server/lib/change-workflow.cjs +932 -176
  41. package/mcp-server/lib/context-paths.cjs +111 -0
  42. package/mcp-server/lib/context-spine.cjs +511 -45
  43. package/mcp-server/lib/decision-dialogue.cjs +345 -3
  44. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  45. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  46. package/mcp-server/lib/doctor.cjs +373 -11
  47. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  48. package/mcp-server/lib/init-project.cjs +190 -95
  49. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  50. package/mcp-server/lib/plan-store.cjs +700 -5
  51. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  52. package/mcp-server/lib/projector.cjs +174 -40
  53. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  54. package/mcp-server/lib/safe-project-file.cjs +385 -0
  55. package/mcp-server/lib/spec-learning.cjs +28 -12
  56. package/mcp-server/lib/state-db.cjs +263 -9
  57. package/mcp-server/lib/state-ops.cjs +248 -49
  58. package/mcp-server/lib/telemetry.cjs +12 -3
  59. package/mcp-server/lib/workflow-state.cjs +610 -58
  60. package/mcp-server/server.cjs +278 -33
  61. package/orchestrator/daemon.cjs +2 -10
  62. package/orchestrator/parallel-orchestrator.cjs +314 -50
  63. package/orchestrator/planner/plan-builder.cjs +1 -1
  64. package/orchestrator/recovery.cjs +125 -7
  65. package/orchestrator/session-close-journal-worker.cjs +5 -0
  66. package/orchestrator/session-close-journal.cjs +1014 -0
  67. package/orchestrator/session-runner.cjs +679 -69
  68. package/orchestrator/worktree-manager.cjs +186 -35
  69. package/package.json +2 -3
  70. package/skills/cc-collab/SKILL.md +4 -1
  71. package/skills/codex-collab/SKILL.md +5 -2
  72. package/skills/ultra-change/SKILL.md +26 -8
  73. package/skills/ultra-deliver/SKILL.md +23 -11
  74. package/skills/ultra-dev/SKILL.md +33 -6
  75. package/skills/ultra-doctor/SKILL.md +6 -2
  76. package/skills/ultra-init/SKILL.md +4 -2
  77. package/skills/ultra-plan/SKILL.md +49 -16
  78. package/skills/ultra-research/SKILL.md +30 -27
  79. package/skills/ultra-review/SKILL.md +9 -5
  80. package/skills/ultra-status/SKILL.md +9 -4
  81. package/skills/ultra-test/SKILL.md +10 -5
  82. package/skills/ultra-think/SKILL.md +6 -2
  83. package/skills/ultra-think/references/decision-dialogue.md +17 -6
  84. package/skills/ultra-verify/SKILL.md +4 -1
  85. package/spec/cli-protocol.md +18 -7
  86. package/spec/mcp-tools.yaml +502 -44
  87. package/spec/schemas/change-delta.v1.schema.json +153 -0
  88. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  89. package/spec/schemas/mcp-tools.schema.json +1 -1
  90. package/spec/schemas/state-db.sql +50 -13
  91. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  92. package/ultra-tools/cli.cjs +3 -2
  93. package/ultra-tools/commands/db.cjs +76 -25
  94. package/ultra-tools/commands/migrate.cjs +995 -80
  95. package/ultra-tools/commands/session.cjs +34 -8
  96. package/ultra-tools/commands/status.cjs +32 -6
  97. package/ultra-tools/commands/system.cjs +129 -22
  98. package/commands/learn.md +0 -10
  99. package/orchestrator/skill-miner.cjs +0 -133
  100. package/output-styles/coding-vibes.md +0 -67
  101. package/output-styles/structural-thinking.md +0 -61
  102. 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
 
@@ -30,10 +30,14 @@ const planStore = require('./lib/plan-store.cjs');
30
30
  const { initProject } = require('./lib/init-project.cjs');
31
31
  const baselines = require('./lib/baseline-workflow.cjs');
32
32
  const changes = require('./lib/change-workflow.cjs');
33
+ const contextSpine = require('./lib/context-spine.cjs');
33
34
  const runtimeState = require('./lib/runtime-state.cjs');
35
+ const runtimePaths = require('./lib/runtime-paths.cjs');
36
+ const gitBootstrap = require('./lib/git-bootstrap.cjs');
34
37
  const workflows = require('./lib/workflow-state.cjs');
35
38
  const decisions = require('./lib/decision-dialogue.cjs');
36
39
  const doctor = require('./lib/doctor.cjs');
40
+ const artifactRegistry = require('./lib/artifact-registry.cjs');
37
41
  const {
38
42
  readProjectBreadcrumb,
39
43
  renderProjectBreadcrumb,
@@ -85,6 +89,8 @@ const BASELINE_TOOLS = Object.freeze([
85
89
  const CHANGE_TOOLS = Object.freeze([
86
90
  'change.create',
87
91
  'change.update',
92
+ 'change.delta',
93
+ 'change.documentation_reconcile',
88
94
  'change.get',
89
95
  'change.list',
90
96
  'change.context',
@@ -100,6 +106,8 @@ const WORKFLOW_TOOLS = Object.freeze([
100
106
  'workflow.get',
101
107
  'workflow.list',
102
108
  'workflow.step',
109
+ 'workflow.revise',
110
+ 'workflow.supersede',
103
111
  'workflow.complete',
104
112
  ]);
105
113
 
@@ -120,10 +128,16 @@ const SYSTEM_TOOLS = Object.freeze([
120
128
  'system.doctor',
121
129
  ]);
122
130
 
131
+ const ARTIFACT_TOOLS = Object.freeze([
132
+ 'artifact.record',
133
+ 'artifact.get',
134
+ ]);
135
+
123
136
  const REGISTERED_TOOLS = Object.freeze([
124
137
  ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
125
138
  ...DECISION_TOOLS,
126
139
  ...WORKFLOW_TOOLS,
140
+ ...ARTIFACT_TOOLS,
127
141
  ...SYSTEM_TOOLS,
128
142
  ]);
129
143
 
@@ -137,11 +151,14 @@ const MUTATING_TOOLS = new Set([
137
151
  'session.spawn', 'session.close', 'session.heartbeat',
138
152
  'plan.export',
139
153
  'baseline.start', 'baseline.record', 'baseline.converge',
140
- 'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
154
+ 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
155
+ 'change.context', 'change.converge', 'change.archive',
141
156
  'change.learning_propose', 'change.learning_resolve',
142
157
  'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
143
158
  'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
144
- 'workflow.start', 'workflow.step', 'workflow.complete',
159
+ 'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
160
+ 'workflow.complete',
161
+ 'artifact.record',
145
162
  ]);
146
163
 
147
164
  function loadRegisteredTools() {
@@ -260,12 +277,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
260
277
  return { session };
261
278
  }
262
279
  case 'session.admission_check': {
263
- sessionRunner.assertSessionTaskReady(
280
+ return sessionRunner.admissionCheck(
264
281
  db,
265
282
  ctx.rootDir || process.cwd(),
266
283
  input.task_id,
267
284
  );
268
- return ops.admissionCheck(db, input.task_id);
269
285
  }
270
286
  case 'session.list': {
271
287
  const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
@@ -410,41 +426,175 @@ async function dispatchTool(name, input, db, ctx = {}) {
410
426
  err.code = 'CHANGE_NOT_MUTABLE';
411
427
  throw err;
412
428
  }
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 });
429
+ const tasks = ops.listTasks(db, { change_id: change.id });
417
430
  if (tasks.length === 0) {
418
431
  const err = new Error('no tasks to plan');
419
432
  err.code = 'NO_TASKS';
420
433
  throw err;
421
434
  }
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',
435
+ const context = contextSpine.validateContextSnapshot(db, {
426
436
  change_id: change.id,
427
- payload: {
428
- change_id: change.id, plan_path, wave_count: plan.waves.length, tag: input.tag || null,
437
+ task_id: null,
438
+ role: 'plan',
439
+ gate: 'planning',
440
+ }, { rootDir });
441
+ if (!context.snapshot || context.blockers.length > 0) {
442
+ const err = new Error('plan export requires a current planning Context Manifest');
443
+ err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
444
+ err.details = { blockers: context.blockers };
445
+ throw err;
446
+ }
447
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
448
+ const planRun = db.prepare(
449
+ `SELECT id FROM workflow_runs
450
+ WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
451
+ AND status IN ('active', 'blocked', 'ready')
452
+ ORDER BY started_at DESC, rowid DESC LIMIT 1`,
453
+ ).get(change.id);
454
+ const owner = { type: 'change', id: change.id };
455
+ const contextArtifact = db.prepare(
456
+ `SELECT id FROM artifacts
457
+ WHERE path = ? AND status <> 'archived'
458
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
459
+ ).get(context.snapshot.manifest_path);
460
+ const publication = planStore.prepareChangePlanPublication(plan, {
461
+ rootDir,
462
+ change,
463
+ tasks,
464
+ context: {
465
+ snapshot_id: context.snapshot.id,
466
+ manifest_path: context.snapshot.manifest_path,
467
+ manifest_digest: context.snapshot.manifest_hash,
429
468
  },
430
469
  });
431
- return { plan_path, wave_count: plan.waves.length };
470
+ const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
471
+ kind,
472
+ file,
473
+ digest,
474
+ input: {
475
+ owner_type: owner.type,
476
+ owner_id: owner.id,
477
+ kind,
478
+ path: path.relative(rootDir, file),
479
+ source_refs: [
480
+ { type: 'change', id: change.id, relation: 'planned_for' },
481
+ ...(contextArtifact
482
+ ? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
483
+ : []),
484
+ ...tasks.map((task) => ({
485
+ type: 'task', id: task.id, relation: 'compiled_from_task_contract',
486
+ })),
487
+ ],
488
+ consumer_refs: planRun
489
+ ? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
490
+ : [],
491
+ provenance: {
492
+ writer: 'plan.export',
493
+ workflow_run_id: planRun?.id || null,
494
+ publication_transaction_id: publication.transaction_id,
495
+ },
496
+ metadata: {
497
+ context_snapshot_id: context.snapshot.id,
498
+ context_digest: context.snapshot.manifest_hash,
499
+ terminal_role: !planRun,
500
+ },
501
+ },
502
+ }));
503
+ try {
504
+ ops.tx(db, () => {
505
+ const preflight = artifactInputs.map((artifact) => (
506
+ artifactRegistry.preflightArtifactPublication(
507
+ db, artifact.input, { rootDir },
508
+ )
509
+ ));
510
+ publication.publish();
511
+ const recorded = artifactInputs.map((artifact, index) => (
512
+ artifactRegistry.recordArtifactInTx(db, {
513
+ ...artifact.input,
514
+ id: preflight[index].artifact_id || undefined,
515
+ expected_before_digest: preflight[index].expected_before_digest,
516
+ content_digest: artifact.digest,
517
+ }, { rootDir })
518
+ ));
519
+ ops.appendEventInTx(db, {
520
+ type: 'plan_exported',
521
+ change_id: change.id,
522
+ payload: {
523
+ change_id: change.id,
524
+ plan_path: publication.plan_path,
525
+ plan_md_path: publication.plan_md_path,
526
+ publication_transaction_id: publication.transaction_id,
527
+ wave_count: plan.waves.length,
528
+ },
529
+ });
530
+ return recorded;
531
+ });
532
+ } catch (error) {
533
+ const rollback = publication.rollback();
534
+ const failure = rollback?.rolled_back === false
535
+ ? planStore.planRecoveryRequiredError(
536
+ error,
537
+ rollback,
538
+ publication.transaction_id,
539
+ )
540
+ : error;
541
+ if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
542
+ const issue = failure.details?.rollback_issue
543
+ || failure.details?.issue
544
+ || {
545
+ code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
546
+ message: failure.cause?.message || failure.message,
547
+ };
548
+ ctx.markPlanRecoveryRequired?.({
549
+ recovered: 0,
550
+ finalized: 0,
551
+ pending: 1,
552
+ issues: [issue],
553
+ });
554
+ }
555
+ throw failure;
556
+ }
557
+ try {
558
+ publication.commit();
559
+ } catch (error) {
560
+ const issue = error.details?.issue || {
561
+ code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
562
+ message: error.cause?.message || error.message,
563
+ };
564
+ ctx.markPlanRecoveryRequired?.({
565
+ recovered: 0,
566
+ finalized: 0,
567
+ pending: 1,
568
+ issues: [issue],
569
+ });
570
+ // The registry and target bytes committed atomically before cleanup.
571
+ // The remaining journal is still authoritative recovery work, so the
572
+ // caller must see a typed failure and later mutations must stop.
573
+ throw error;
574
+ }
575
+ return {
576
+ plan_path: publication.plan_path,
577
+ plan_md_path: publication.plan_md_path,
578
+ wave_count: plan.waves.length,
579
+ };
432
580
  }
433
581
  case 'plan.get': {
434
582
  const rootDir = ctx.rootDir || process.cwd();
435
- const loaded = planStore.loadPlanArtifact(rootDir);
583
+ const change = changes.readChange(db, input.change_id);
584
+ if (!change) {
585
+ const err = new Error(`change ${input.change_id} not found`);
586
+ err.code = 'CHANGE_NOT_FOUND';
587
+ throw err;
588
+ }
589
+ const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
590
+ db,
591
+ strict: true,
592
+ });
436
593
  if (!loaded) {
437
594
  const err = new Error('no plan has been computed yet');
438
595
  err.code = 'NO_PLAN';
439
596
  throw err;
440
597
  }
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
598
  return { plan: planStore.selectSection(loaded, input.section) };
449
599
  }
450
600
  case 'baseline.start': {
@@ -546,6 +696,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
546
696
  case 'change.archive': {
547
697
  return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
548
698
  }
699
+ case 'change.delta': {
700
+ return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
701
+ }
702
+ case 'change.documentation_reconcile': {
703
+ return changes.recordDocumentationReconciliation(
704
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
705
+ );
706
+ }
549
707
  case 'decision.thread_start': {
550
708
  return { thread: decisions.startDecisionThread(db, input) };
551
709
  }
@@ -578,7 +736,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
578
736
  return { thread: decisions.supersedeDecision(db, input) };
579
737
  }
580
738
  case 'decision.complete': {
581
- return { thread: decisions.completeDecisionThread(db, input) };
739
+ return {
740
+ thread: decisions.completeDecisionThread(
741
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
742
+ ),
743
+ };
582
744
  }
583
745
  case 'decision.checkpoint': {
584
746
  return {
@@ -618,6 +780,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
618
780
  ),
619
781
  };
620
782
  }
783
+ case 'workflow.revise': {
784
+ return {
785
+ workflow: workflows.reviseWorkflow(
786
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
787
+ ),
788
+ };
789
+ }
790
+ case 'workflow.supersede': {
791
+ return workflows.supersedeWorkflow(
792
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
793
+ );
794
+ }
621
795
  case 'workflow.complete': {
622
796
  return {
623
797
  workflow: workflows.completeWorkflow(
@@ -625,6 +799,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
625
799
  ),
626
800
  };
627
801
  }
802
+ case 'artifact.record': {
803
+ return artifactRegistry.recordArtifact(
804
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
805
+ );
806
+ }
807
+ case 'artifact.get': {
808
+ return { artifact: artifactRegistry.getArtifact(db, input) };
809
+ }
628
810
  case 'system.doctor': {
629
811
  return doctor.runDoctor(db, {
630
812
  rootDir: ctx.rootDir || process.cwd(),
@@ -657,8 +839,55 @@ function startServer({
657
839
  project = projector.projectAll,
658
840
  }) {
659
841
  let db = null;
660
- const getDb = () => {
661
- if (!db) db = initStateDb(dbPath).db;
842
+ let planRecovery = null;
843
+ let authorityDbPath = path.resolve(dbPath);
844
+ const markPlanRecoveryRequired = (details) => {
845
+ planRecovery = {
846
+ recovered: Number(details?.recovered || 0),
847
+ finalized: Number(details?.finalized || 0),
848
+ pending: Math.max(1, Number(details?.pending || 0)),
849
+ issues: Array.isArray(details?.issues) ? details.issues : [],
850
+ };
851
+ };
852
+ const getDb = (toolName = null) => {
853
+ if (!db) {
854
+ const projectPaths = runtimePaths.pathsFor(rootDir);
855
+ if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
856
+ // The packaged launcher has no explicit DB override. Its default path
857
+ // is the future canonical runtime location, which can legitimately be
858
+ // absent while a legacy project still owns its project-level DB. Admit and
859
+ // migrate that project before applying configured-authority binding.
860
+ authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
861
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
862
+ }).stateDbPath;
863
+ } else {
864
+ // Only a real external/task-linked override reaches binding
865
+ // validation. It may never invent or borrow project authority.
866
+ runtimePaths.locateStateDb(rootDir, {
867
+ env: { UBP_DB_PATH: authorityDbPath },
868
+ });
869
+ runtimePaths.ensureRuntimeState(rootDir, {
870
+ env: { UBP_DB_PATH: authorityDbPath },
871
+ allowConfiguredRuntimeLink: true,
872
+ migrateState: false,
873
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
874
+ });
875
+ }
876
+ db = initStateDb(authorityDbPath).db;
877
+ planRecovery = planStore.recoverPlanPublications(db, { rootDir });
878
+ }
879
+ if (toolName !== 'system.doctor'
880
+ && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
881
+ planRecovery = planStore.recoverPlanPublications(db, { rootDir });
882
+ if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
883
+ const error = new Error(
884
+ 'plan publication recovery is incomplete; run system.doctor with repair=true',
885
+ );
886
+ error.code = 'PLAN_RECOVERY_REQUIRED';
887
+ error.details = planRecovery;
888
+ throw error;
889
+ }
890
+ }
662
891
  return db;
663
892
  };
664
893
 
@@ -723,14 +952,22 @@ function startServer({
723
952
  let result;
724
953
  let toolDb = null;
725
954
  try {
726
- toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
955
+ toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
727
956
  if (toolDb) assertStateAuthority(toolDb, rootDir);
728
957
  result = await dispatchTool(
729
958
  name,
730
959
  args,
731
960
  toolDb,
732
- { rootDir, sessionId, projector: project },
961
+ {
962
+ rootDir,
963
+ sessionId,
964
+ projector: project,
965
+ markPlanRecoveryRequired,
966
+ },
733
967
  );
968
+ if (name === 'system.doctor' && args.repair === true) {
969
+ planRecovery = result.repair?.plan_publications || null;
970
+ }
734
971
  } catch (err) {
735
972
  const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
736
973
  toolError = code;
@@ -789,12 +1026,18 @@ function startServer({
789
1026
  }
790
1027
 
791
1028
  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
1029
  const rootDir = process.env.UBP_ROOT_DIR
796
1030
  ? path.resolve(process.env.UBP_ROOT_DIR)
797
1031
  : path.resolve('.');
1032
+ // Keep tools/list side-effect free. Root/layout validation happens inside the
1033
+ // first state-backed tool call so clients receive a typed MCP error instead
1034
+ // of a transport-level startup failure.
1035
+ const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
1036
+ ? process.env.UBP_DB_PATH.trim()
1037
+ : '';
1038
+ const dbPath = configuredDb
1039
+ ? path.resolve(configuredDb)
1040
+ : runtimePaths.pathsFor(rootDir).stateDbPath;
798
1041
  const sessionId = process.env.UBP_SESSION_ID || null;
799
1042
  const handle = startServer({ dbPath, rootDir, sessionId });
800
1043
  const transport = new StdioServerTransport();
@@ -816,7 +1059,7 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
816
1059
  throw error;
817
1060
  }
818
1061
  const root = path.resolve(rootDir || process.cwd());
819
- const dbPath = path.join(root, '.ultra', 'state.db');
1062
+ const dbPath = runtimePaths.locateStateDb(root);
820
1063
  if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
821
1064
 
822
1065
  const db = openStateDb(dbPath);
@@ -878,11 +1121,13 @@ module.exports = {
878
1121
  BASELINE_TOOLS,
879
1122
  CHANGE_TOOLS,
880
1123
  WORKFLOW_TOOLS,
1124
+ ARTIFACT_TOOLS,
881
1125
  SYSTEM_TOOLS,
882
1126
  REGISTERED_TOOLS,
883
1127
  STATELESS_TOOLS,
884
1128
  MUTATING_TOOLS,
885
1129
  appendHookLifecycleEvent,
1130
+ findProjectRoot: runtimePaths.findProjectRoot,
886
1131
  readProjectBreadcrumb,
887
1132
  renderProjectBreadcrumb,
888
1133
  };
@@ -97,7 +97,7 @@ function runDaemon({
97
97
  const children = [];
98
98
  let bootRecovery;
99
99
  try {
100
- bootRecovery = recovery.recoverOnBoot(db);
100
+ bootRecovery = recovery.recoverOnBoot(db, { repoRoot });
101
101
  } catch (error) {
102
102
  runtimeState.recordIncident(db, {
103
103
  code: 'BOOT_RECOVERY_FAILED', severity: 'critical', retryable: true,
@@ -208,17 +208,9 @@ function runDaemon({
208
208
  runtime,
209
209
  command,
210
210
  args: commandArgs,
211
+ mark_task_started: true,
211
212
  });
212
213
  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
214
  settleExecution(handle, task);
223
215
  children.push(handle);
224
216
  } catch (err) {