ultra-builder-pro-cli 0.22.0 → 0.24.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 (93) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +68 -35
  4. package/adapters/_shared/codex-assets.cjs +121 -60
  5. package/adapters/_shared/interaction-contract.cjs +20 -7
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +4 -3
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +61 -31
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +8 -4
  13. package/adapters/opencode.js +211 -33
  14. package/agents/code-reviewer.md +4 -2
  15. package/agents/debugger.md +4 -1
  16. package/agents/review-code.md +7 -3
  17. package/agents/review-comments.md +7 -4
  18. package/agents/review-coordinator.md +11 -7
  19. package/agents/review-design.md +8 -4
  20. package/agents/review-errors.md +8 -4
  21. package/agents/review-spec.md +8 -5
  22. package/agents/review-tests.md +7 -4
  23. package/agents/tdd-runner.md +5 -2
  24. package/bin/install.js +9 -4
  25. package/bin/orchestrator.js +4 -13
  26. package/docs/AGENT-CONTEXT.md +141 -231
  27. package/docs/ARCHITECTURE.md +203 -498
  28. package/docs/ARTIFACT-AUTHORITY.md +38 -35
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -442
  35. package/hooks/README.md +11 -8
  36. package/hooks/active_task_context.py +16 -16
  37. package/hooks/adapters/grok.py +134 -0
  38. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  39. package/hooks/health_check.py +19 -29
  40. package/hooks/pre_stop_check.py +22 -12
  41. package/hooks/subagent_tracker.py +1 -1
  42. package/hooks/workflow_checkpoint.py +14 -13
  43. package/hooks/workflow_context.py +11 -12
  44. package/hooks/workflow_resume.py +14 -13
  45. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  46. package/mcp-server/lib/artifact-registry.cjs +25 -1
  47. package/mcp-server/lib/baseline-workflow.cjs +36 -38
  48. package/mcp-server/lib/canonical-json.cjs +19 -0
  49. package/mcp-server/lib/change-workflow.cjs +637 -49
  50. package/mcp-server/lib/context-envelope.cjs +508 -0
  51. package/mcp-server/lib/decision-records.cjs +337 -0
  52. package/mcp-server/lib/doctor.cjs +91 -33
  53. package/mcp-server/lib/init-project.cjs +78 -39
  54. package/mcp-server/lib/managed-file-write.cjs +116 -0
  55. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  56. package/mcp-server/lib/project-context.cjs +178 -0
  57. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  58. package/mcp-server/lib/state-db.cjs +155 -24
  59. package/mcp-server/lib/task-ledger.cjs +522 -13
  60. package/mcp-server/lib/ultra-facade.cjs +834 -0
  61. package/mcp-server/lib/worker-packet.cjs +358 -0
  62. package/mcp-server/server.cjs +208 -1059
  63. package/orchestrator/daemon.cjs +27 -9
  64. package/orchestrator/parallel-orchestrator.cjs +111 -76
  65. package/orchestrator/session-runner.cjs +135 -34
  66. package/package.json +10 -3
  67. package/skills/ultra-change/SKILL.md +39 -98
  68. package/skills/ultra-deliver/SKILL.md +42 -65
  69. package/skills/ultra-dev/SKILL.md +52 -99
  70. package/skills/ultra-doctor/SKILL.md +35 -57
  71. package/skills/ultra-init/SKILL.md +48 -88
  72. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  73. package/skills/ultra-plan/SKILL.md +54 -132
  74. package/skills/ultra-research/SKILL.md +58 -112
  75. package/skills/ultra-review/SKILL.md +28 -46
  76. package/skills/ultra-review/references/unified-schema.md +2 -0
  77. package/skills/ultra-review/scripts/review_wait.py +8 -0
  78. package/skills/ultra-status/SKILL.md +22 -45
  79. package/skills/ultra-test/SKILL.md +27 -61
  80. package/skills/ultra-think/SKILL.md +29 -48
  81. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  82. package/spec/cli-protocol.md +80 -187
  83. package/spec/mcp-tools.yaml +130 -2738
  84. package/spec/schemas/mcp-tools.schema.json +1 -1
  85. package/spec/schemas/state-db.sql +107 -4
  86. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  87. package/ultra-tools/commands/status.cjs +369 -243
  88. package/mcp-server/lib/context-spine.cjs +0 -1208
  89. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  90. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  91. package/mcp-server/lib/spec-learning.cjs +0 -284
  92. package/mcp-server/lib/workflow-state.cjs +0 -2965
  93. package/skills/ultra-think/references/decision-dialogue.md +0 -153
@@ -1,182 +1,53 @@
1
1
  'use strict';
2
2
 
3
- // MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
4
- // .ultra/.runtime/state.db. Uses the low-level Server API from
5
- // @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
6
- // straight from spec/mcp-tools.yaml without translating to zod.
7
-
8
3
  const fs = require('node:fs');
9
4
  const path = require('node:path');
10
5
 
11
- const yaml = require('js-yaml');
12
6
  const Ajv = require('ajv/dist/2020');
13
7
  const addFormats = require('ajv-formats');
14
-
8
+ const yaml = require('js-yaml');
15
9
  const { Server } = require('@modelcontextprotocol/server');
16
10
  const { StdioServerTransport } = require('@modelcontextprotocol/server/stdio');
17
- const { version: PACKAGE_VERSION } = require('../package.json');
18
11
 
12
+ const { version: PACKAGE_VERSION } = require('../package.json');
19
13
  const {
20
- initStateDb, closeStateDb, openStateDb, ensureSchemaVersion,
14
+ closeStateDb,
15
+ ensureSchemaVersion,
16
+ initStateDb,
17
+ openStateDb,
21
18
  } = require('./lib/state-db.cjs');
22
19
  const { assertStateAuthority } = require('./lib/state-authority.cjs');
20
+ const gitBootstrap = require('./lib/git-bootstrap.cjs');
23
21
  const ops = require('./lib/state-ops.cjs');
22
+ const planStore = require('./lib/plan-store.cjs');
24
23
  const projector = require('./lib/projector.cjs');
24
+ const {
25
+ readProjectContextEnvelope,
26
+ renderProjectContextEnvelope,
27
+ } = require('./lib/project-context.cjs');
28
+ const runtimePaths = require('./lib/runtime-paths.cjs');
29
+ const runtimeState = require('./lib/runtime-state.cjs');
25
30
  const taskLedger = require('./lib/task-ledger.cjs');
26
31
  const telemetry = require('./lib/telemetry.cjs');
27
- const topo = require('./lib/topo.cjs');
28
- const parser = require('./lib/prd-parser.cjs');
29
- const expander = require('./lib/task-expander.cjs');
30
- const planStore = require('./lib/plan-store.cjs');
31
- const { initProject } = require('./lib/init-project.cjs');
32
- const baselines = require('./lib/baseline-workflow.cjs');
33
- const changes = require('./lib/change-workflow.cjs');
34
- const contextSpine = require('./lib/context-spine.cjs');
35
- const runtimeState = require('./lib/runtime-state.cjs');
36
- const runtimePaths = require('./lib/runtime-paths.cjs');
37
- const gitBootstrap = require('./lib/git-bootstrap.cjs');
38
- const workflows = require('./lib/workflow-state.cjs');
39
- const decisions = require('./lib/decision-dialogue.cjs');
40
- const doctor = require('./lib/doctor.cjs');
41
- const artifactRegistry = require('./lib/artifact-registry.cjs');
42
- const {
43
- readProjectBreadcrumb,
44
- renderProjectBreadcrumb,
45
- } = require('./lib/project-breadcrumb.cjs');
46
- const sessionRunner = require('../orchestrator/session-runner.cjs');
32
+ const ultraFacade = require('./lib/ultra-facade.cjs');
47
33
 
48
34
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
49
35
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
50
36
  : path.resolve(__dirname, '..');
51
37
  const TOOLS_FILE = path.join(REPO_ROOT, 'spec', 'mcp-tools.yaml');
38
+ const PUBLIC_TOOLS = ultraFacade.PUBLIC_TOOLS;
39
+ const REGISTERED_TOOLS = PUBLIC_TOOLS;
52
40
 
53
- const TASK_TOOLS = Object.freeze([
54
- 'task.create',
55
- 'task.update',
56
- 'task.list',
57
- 'task.get',
58
- 'task.delete',
59
- 'task.init_project',
60
- 'task.append_event',
61
- 'task.subscribe_events',
62
- 'task.switch_tag',
63
- 'task.dependency_topo',
64
- 'task.parse_prd',
65
- 'task.expand',
66
- 'task.ledger_get',
67
- 'task.ledger_publish',
68
- 'task.ledger_import',
69
- ]);
70
-
71
- const SESSION_TOOLS = Object.freeze([
72
- 'session.spawn',
73
- 'session.close',
74
- 'session.get',
75
- 'session.list',
76
- 'session.admission_check',
77
- 'session.heartbeat',
78
- 'session.subscribe_events',
79
- ]);
80
-
81
- const PLAN_TOOLS = Object.freeze([
82
- 'plan.export',
83
- 'plan.get',
84
- ]);
85
-
86
- const BASELINE_TOOLS = Object.freeze([
87
- 'baseline.start',
88
- 'baseline.record',
89
- 'baseline.get',
90
- 'baseline.converge',
91
- ]);
92
-
93
- const CHANGE_TOOLS = Object.freeze([
94
- 'change.create',
95
- 'change.update',
96
- 'change.delta',
97
- 'change.documentation_reconcile',
98
- 'change.get',
99
- 'change.list',
100
- 'change.context',
101
- 'change.breadcrumb',
102
- 'change.learning_propose',
103
- 'change.learning_resolve',
104
- 'change.converge',
105
- 'change.archive',
106
- ]);
107
-
108
- const WORKFLOW_TOOLS = Object.freeze([
109
- 'workflow.start',
110
- 'workflow.get',
111
- 'workflow.list',
112
- 'workflow.step',
113
- 'workflow.revise',
114
- 'workflow.supersede',
115
- 'workflow.complete',
116
- ]);
117
-
118
- const DECISION_TOOLS = Object.freeze([
119
- 'decision.thread_start',
120
- 'decision.get',
121
- 'decision.list',
122
- 'decision.open',
123
- 'decision.resolve',
124
- 'decision.delegate',
125
- 'decision.defer',
126
- 'decision.supersede',
127
- 'decision.complete',
128
- 'decision.checkpoint',
129
- ]);
130
-
131
- const SYSTEM_TOOLS = Object.freeze([
132
- 'system.doctor',
133
- ]);
134
-
135
- const ARTIFACT_TOOLS = Object.freeze([
136
- 'artifact.record',
137
- 'artifact.get',
138
- ]);
139
-
140
- const REGISTERED_TOOLS = Object.freeze([
141
- ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
142
- ...DECISION_TOOLS,
143
- ...WORKFLOW_TOOLS,
144
- ...ARTIFACT_TOOLS,
145
- ...SYSTEM_TOOLS,
146
- ]);
147
-
148
- const STATELESS_TOOLS = new Set(['task.init_project']);
149
-
150
- // init_project owns its target project's schema, baseline seed, and first
151
- // projection internally. Do not enqueue a second projection on the caller DB.
152
- const MUTATING_TOOLS = new Set([
153
- 'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
154
- 'task.parse_prd', 'task.expand',
155
- 'task.ledger_import',
156
- 'session.spawn', 'session.close', 'session.heartbeat',
157
- 'plan.export',
158
- 'baseline.start', 'baseline.record', 'baseline.converge',
159
- 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
160
- 'change.context', 'change.converge', 'change.archive',
161
- 'change.learning_propose', 'change.learning_resolve',
162
- 'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
163
- 'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
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',
175
- ]);
176
-
177
- function loadRegisteredTools() {
41
+ function loadToolContracts() {
178
42
  const manifest = yaml.load(fs.readFileSync(TOOLS_FILE, 'utf8'));
179
- return manifest.tools.filter((t) => REGISTERED_TOOLS.includes(t.name));
43
+ const names = manifest.tools.map((tool) => tool.name).sort();
44
+ const expected = [...PUBLIC_TOOLS].sort();
45
+ if (JSON.stringify(names) !== JSON.stringify(expected)) {
46
+ const error = new Error('published MCP manifest does not match the seven-tool kernel');
47
+ error.code = 'PUBLIC_CONTRACT_DRIFT';
48
+ throw error;
49
+ }
50
+ return manifest.tools;
180
51
  }
181
52
 
182
53
  function buildAjv() {
@@ -185,758 +56,6 @@ function buildAjv() {
185
56
  return ajv;
186
57
  }
187
58
 
188
- async function dispatchTool(name, input, db, ctx = {}) {
189
- switch (name) {
190
- case 'task.create': {
191
- const { randomUUID } = require('node:crypto');
192
- const id = input.id || `task-${randomUUID()}`;
193
- const changeId = ops.resolveTaskCreationChangeId(db, input);
194
- const authorizedInput = { ...input, change_id: changeId };
195
- changes.assertTaskCreationAllowed(db, authorizedInput, { rootDir: ctx.rootDir || process.cwd() });
196
- const task = ops.createTask(db, { ...authorizedInput, id });
197
- return { id: task.id, status: task.status, created_at: task.created_at };
198
- }
199
- case 'task.update': {
200
- const before = ops.readTask(db, input.id);
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
- }
212
- return { ok: true, task };
213
- }
214
- case 'task.list': {
215
- const tasks = ops.listTasks(db, input || {});
216
- return { tasks, count: tasks.length };
217
- }
218
- case 'task.get': {
219
- const task = ops.readTask(db, input.id);
220
- if (!task) {
221
- const err = new Error(`task ${input.id} not found`);
222
- err.code = 'TASK_NOT_FOUND';
223
- throw err;
224
- }
225
- return { task };
226
- }
227
- case 'task.delete': {
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;
234
- }
235
- case 'task.init_project': {
236
- return initProject(input);
237
- }
238
- case 'task.append_event': {
239
- const r = ops.appendEvent(db, {
240
- type: input.type,
241
- task_id: input.task_id,
242
- change_id: input.change_id,
243
- session_id: input.session_id,
244
- runtime: input.runtime,
245
- payload: input.payload,
246
- });
247
- return { event_id: r.event_id, ts: r.ts };
248
- }
249
- case 'task.subscribe_events': {
250
- return ops.subscribeEventsSince(db, input || {});
251
- }
252
- case 'session.spawn': {
253
- if (ctx.sessionId) {
254
- const error = new Error(
255
- `worker session ${ctx.sessionId} already owns an isolated worktree; `
256
- + 'resume that session instead of recursively spawning another one',
257
- );
258
- error.code = 'NESTED_SESSION_FORBIDDEN';
259
- throw error;
260
- }
261
- const rootDir = ctx.rootDir || process.cwd();
262
- const handle = sessionRunner.spawnSession({
263
- db,
264
- repoRoot: rootDir,
265
- task_id: input.task_id,
266
- runtime: input.runtime,
267
- takeover: Boolean(input.takeover),
268
- worktree_base: input.worktree_base,
269
- });
270
- return {
271
- sid: handle.sid,
272
- worktree_path: handle.worktree_path,
273
- artifact_dir: handle.artifact_dir,
274
- lease_expires_at: handle.lease_expires_at,
275
- worktree_created: true,
276
- };
277
- }
278
- case 'session.close': {
279
- if (ctx.sessionId) {
280
- const error = new Error(
281
- ctx.sessionId === input.sid
282
- ? `worker session ${ctx.sessionId} is supervised by its parent; save evidence and exit so the parent can settle the lease`
283
- : `worker session ${ctx.sessionId} cannot close unrelated session ${input.sid}`,
284
- );
285
- error.code = ctx.sessionId === input.sid
286
- ? 'WORKER_SESSION_PARENT_OWNED'
287
- : 'SESSION_SCOPE_VIOLATION';
288
- throw error;
289
- }
290
- const closed = sessionRunner.closeSession(
291
- { db, repoRoot: ctx.rootDir || process.cwd(), sid: input.sid },
292
- { status: input.status, remove_worktree: input.remove_worktree === true },
293
- );
294
- return {
295
- ok: true,
296
- worktree_preserved: closed.worktree_preserved,
297
- };
298
- }
299
- case 'session.get': {
300
- const session = ops.readSession(db, input.sid);
301
- if (!session) {
302
- const err = new Error(`session ${input.sid} not found`);
303
- err.code = 'SESSION_NOT_FOUND';
304
- throw err;
305
- }
306
- return { session };
307
- }
308
- case 'session.admission_check': {
309
- return sessionRunner.admissionCheck(
310
- db,
311
- ctx.rootDir || process.cwd(),
312
- input.task_id,
313
- );
314
- }
315
- case 'session.list': {
316
- const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
317
- const status = input.status || 'running';
318
- const filtered = status === 'running'
319
- ? sessions
320
- : db.prepare(
321
- "SELECT * FROM sessions WHERE status = ? AND (? IS NULL OR task_id = ?) ORDER BY started_at ASC LIMIT ?",
322
- ).all(status, input.task_id || null, input.task_id || null, Math.min(input.limit || 100, 500));
323
- const limit = Math.min(input.limit || 100, 500);
324
- const trimmed = filtered.slice(0, limit);
325
- return { sessions: trimmed, count: trimmed.length };
326
- }
327
- case 'session.heartbeat': {
328
- return ops.heartbeatSession(db, input.sid);
329
- }
330
- case 'session.subscribe_events': {
331
- return ops.subscribeEventsSince(db, {
332
- since_id: input.since_id,
333
- session_id: input.sid,
334
- limit: input.limit,
335
- });
336
- }
337
- case 'task.switch_tag': {
338
- return ops.switchTaskTag(db, input.id, input.tag);
339
- }
340
- case 'task.dependency_topo': {
341
- const hasTaskIds = Array.isArray(input && input.task_ids);
342
- const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
343
- if (hasTaskIds === hasChangeId) {
344
- const err = new Error('provide exactly one of task_ids or change_id');
345
- err.code = 'VALIDATION_ERROR';
346
- throw err;
347
- }
348
- let rows;
349
- if (hasTaskIds) {
350
- rows = input.task_ids.map((id) => {
351
- const task = ops.readTask(db, id);
352
- if (!task) {
353
- const err = new Error(`task ${id} not found`);
354
- err.code = 'TASK_NOT_FOUND';
355
- throw err;
356
- }
357
- return task;
358
- });
359
- } else {
360
- const change = changes.readChange(db, input.change_id);
361
- if (!change) {
362
- const err = new Error(`change ${input.change_id} not found`);
363
- err.code = 'CHANGE_NOT_FOUND';
364
- throw err;
365
- }
366
- rows = ops.listTasks(db, { change_id: change.id });
367
- }
368
- const graph = rows.map((t) => ({
369
- id: t.id,
370
- deps: Array.isArray(t.deps) ? t.deps : [],
371
- }));
372
- const result = topo.computeWaves(graph);
373
- if (result.cycles.length > 0) {
374
- const err = new Error(`dependency graph has ${result.cycles.length} cycle(s)`);
375
- err.code = 'CYCLE_DETECTED';
376
- err.details = { cycles: result.cycles };
377
- throw err;
378
- }
379
- return { waves: result.waves };
380
- }
381
- case 'task.parse_prd': {
382
- const dryRun = input.dry_run === true;
383
- const rootDir = ctx.rootDir || process.cwd();
384
- if (!dryRun && !String(input.change_id || '').trim()) {
385
- const err = new Error('persisting a parsed task graph requires change_id');
386
- err.code = 'CHANGE_REQUIRED';
387
- throw err;
388
- }
389
- const creationAuthority = { change_id: input.change_id };
390
- if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
391
- const parsed = parser.parsePrd(input.tasks, {
392
- tag: input.tag,
393
- changeId: input.change_id,
394
- });
395
- const shaped = parsed.tasks.map((t) => ({
396
- id: t.id, title: t.title, type: t.type, priority: t.priority,
397
- complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
398
- tag: t.tag, change_id: t.change_id,
399
- }));
400
- const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
401
- const topoResult = topo.computeWaves(graph);
402
- if (topoResult.cycles.length > 0) {
403
- const err = new Error(`parsed task graph has ${topoResult.cycles.length} cycle(s)`);
404
- err.code = 'CYCLE_DETECTED';
405
- err.details = { cycles: topoResult.cycles };
406
- throw err;
407
- }
408
- if (!dryRun) {
409
- try {
410
- ops.tx(db, () => {
411
- changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
412
- for (const t of shaped) ops.createTask(db, t);
413
- });
414
- } catch (err) {
415
- if (['BASELINE_NOT_READY', 'CHANGE_NOT_FOUND', 'CHANGE_NOT_MUTABLE'].includes(err.code)) throw err;
416
- const wrap = new Error(`failed to persist parsed tasks: ${err.message}`);
417
- wrap.code = 'PARSE_FAILED';
418
- wrap.cause = err;
419
- throw wrap;
420
- }
421
- }
422
- return { tasks: shaped, topo: topoResult.waves };
423
- }
424
- case 'task.expand': {
425
- const parent = ops.readTask(db, input.id);
426
- if (!parent) {
427
- const err = new Error(`no task ${input.id}`);
428
- err.code = 'TASK_NOT_FOUND';
429
- throw err;
430
- }
431
- if (parent.status === 'expanded') {
432
- const err = new Error(`task ${input.id} is already expanded`);
433
- err.code = 'ALREADY_EXPANDED';
434
- throw err;
435
- }
436
- const rootDir = ctx.rootDir || process.cwd();
437
- changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
438
- const result = expander.expandTask(db, {
439
- id: input.id,
440
- children: input.children,
441
- rootDir,
442
- });
443
- taskLedger.publishTaskLedger(db, {
444
- rootDir,
445
- reason: 'task_expanded',
446
- });
447
- return { parent_id: result.parent_id, children: result.children };
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
- }
495
- case 'plan.export': {
496
- const rootDir = ctx.rootDir || process.cwd();
497
- const change = changes.readChange(db, input.change_id);
498
- if (!change) {
499
- const err = new Error(`change ${input.change_id} not found`);
500
- err.code = 'CHANGE_NOT_FOUND';
501
- throw err;
502
- }
503
- if (!['active', 'blocked'].includes(change.status)) {
504
- const err = new Error(`change ${change.id} is ${change.status}`);
505
- err.code = 'CHANGE_NOT_MUTABLE';
506
- throw err;
507
- }
508
- const tasks = ops.listTasks(db, { change_id: change.id });
509
- if (tasks.length === 0) {
510
- const err = new Error('no tasks to plan');
511
- err.code = 'NO_TASKS';
512
- throw err;
513
- }
514
- const context = contextSpine.validateContextSnapshot(db, {
515
- change_id: change.id,
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
- },
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',
657
- });
658
- return {
659
- plan_path: publication.plan_path,
660
- plan_md_path: publication.plan_md_path,
661
- wave_count: plan.waves.length,
662
- };
663
- }
664
- case 'plan.get': {
665
- const rootDir = ctx.rootDir || process.cwd();
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
- });
676
- if (!loaded) {
677
- const err = new Error('no plan has been computed yet');
678
- err.code = 'NO_PLAN';
679
- throw err;
680
- }
681
- return { plan: planStore.selectSection(loaded, input.section) };
682
- }
683
- case 'baseline.start': {
684
- return {
685
- baseline: baselines.startBaseline(
686
- db, input, { rootDir: ctx.rootDir || process.cwd() },
687
- ),
688
- };
689
- }
690
- case 'baseline.record': {
691
- return {
692
- baseline: baselines.recordBaseline(
693
- db, input, { rootDir: ctx.rootDir || process.cwd() },
694
- ),
695
- };
696
- }
697
- case 'baseline.get': {
698
- const baseline = baselines.readBaseline(db, input.id);
699
- if (!baseline) {
700
- const err = new Error(input.id ? `baseline ${input.id} not found` : 'current baseline not found');
701
- err.code = 'BASELINE_NOT_FOUND';
702
- throw err;
703
- }
704
- return {
705
- baseline,
706
- health: baselines.inspectBaseline(db, {
707
- rootDir: ctx.rootDir || process.cwd(), id: input.id,
708
- }),
709
- };
710
- }
711
- case 'baseline.converge': {
712
- const result = baselines.convergeBaseline(
713
- db, input, { rootDir: ctx.rootDir || process.cwd() },
714
- );
715
- if (result.ready) {
716
- taskLedger.publishTaskLedger(db, {
717
- rootDir: ctx.rootDir || process.cwd(),
718
- reason: 'baseline_converged',
719
- });
720
- }
721
- return result;
722
- }
723
- case 'change.create': {
724
- const { randomUUID } = require('node:crypto');
725
- const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
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;
736
- }
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
- });
745
- return {
746
- ok: true,
747
- change,
748
- };
749
- }
750
- case 'change.get': {
751
- const change = changes.readChange(db, input.id);
752
- if (!change) {
753
- const err = new Error(`change ${input.id} not found`);
754
- err.code = 'CHANGE_NOT_FOUND';
755
- throw err;
756
- }
757
- return {
758
- change: {
759
- ...change,
760
- learning_candidates: changes.listSpecLearning(db, change.id),
761
- },
762
- };
763
- }
764
- case 'change.list': {
765
- const rows = changes.listChanges(db, input || {});
766
- return { changes: rows, count: rows.length };
767
- }
768
- case 'change.context': {
769
- const out = changes.compileContext(db, input, { rootDir: ctx.rootDir || process.cwd() });
770
- return {
771
- manifest_path: out.context_manifest_path,
772
- manifest_hash: out.manifest_hash,
773
- manifest: out.manifest,
774
- };
775
- }
776
- case 'change.breadcrumb': {
777
- return {
778
- breadcrumb: changes.readBreadcrumb(
779
- db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
780
- ),
781
- };
782
- }
783
- case 'change.learning_propose': {
784
- return {
785
- candidate: changes.proposeSpecLearning(
786
- db, input, { rootDir: ctx.rootDir || process.cwd() },
787
- ),
788
- };
789
- }
790
- case 'change.learning_resolve': {
791
- return {
792
- candidate: changes.resolveSpecLearning(
793
- db, input, { rootDir: ctx.rootDir || process.cwd() },
794
- ),
795
- };
796
- }
797
- case 'change.converge': {
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;
806
- }
807
- case 'change.archive': {
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
- );
824
- }
825
- case 'decision.thread_start': {
826
- return { thread: decisions.startDecisionThread(db, input) };
827
- }
828
- case 'decision.get': {
829
- const thread = decisions.readDecisionThread(db, input.id);
830
- if (!thread) {
831
- const error = new Error(`decision thread ${input.id} not found`);
832
- error.code = 'DECISION_THREAD_NOT_FOUND';
833
- throw error;
834
- }
835
- return { thread };
836
- }
837
- case 'decision.list': {
838
- const threads = decisions.listDecisionThreads(db, input || {});
839
- return { threads, count: threads.length };
840
- }
841
- case 'decision.open': {
842
- return { thread: decisions.openDecision(db, input) };
843
- }
844
- case 'decision.resolve': {
845
- return { thread: decisions.resolveDecision(db, input) };
846
- }
847
- case 'decision.delegate': {
848
- return { thread: decisions.delegateDecision(db, input) };
849
- }
850
- case 'decision.defer': {
851
- return { thread: decisions.deferDecision(db, input) };
852
- }
853
- case 'decision.supersede': {
854
- return { thread: decisions.supersedeDecision(db, input) };
855
- }
856
- case 'decision.complete': {
857
- return {
858
- thread: decisions.completeDecisionThread(
859
- db, input, { rootDir: ctx.rootDir || process.cwd() },
860
- ),
861
- };
862
- }
863
- case 'decision.checkpoint': {
864
- return {
865
- thread: decisions.checkpointDecisionThread(
866
- db, input, { rootDir: ctx.rootDir || process.cwd() },
867
- ),
868
- };
869
- }
870
- case 'workflow.start': {
871
- return {
872
- workflow: workflows.startWorkflow(
873
- db, input, { rootDir: ctx.rootDir || process.cwd() },
874
- ),
875
- };
876
- }
877
- case 'workflow.get': {
878
- const workflow = workflows.readWorkflow(
879
- db, input.id, { rootDir: ctx.rootDir || process.cwd() },
880
- );
881
- if (!workflow) {
882
- const error = new Error(`workflow ${input.id} not found`);
883
- error.code = 'WORKFLOW_NOT_FOUND';
884
- throw error;
885
- }
886
- return { workflow };
887
- }
888
- case 'workflow.list': {
889
- const rows = workflows.listWorkflows(
890
- db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
891
- );
892
- return { workflows: rows, count: rows.length };
893
- }
894
- case 'workflow.step': {
895
- return {
896
- workflow: workflows.recordWorkflowStep(
897
- db, input, { rootDir: ctx.rootDir || process.cwd() },
898
- ),
899
- };
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
- }
913
- case 'workflow.complete': {
914
- return {
915
- workflow: workflows.completeWorkflow(
916
- db, input, { rootDir: ctx.rootDir || process.cwd() },
917
- ),
918
- };
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
- }
928
- case 'system.doctor': {
929
- return doctor.runDoctor(db, {
930
- rootDir: ctx.rootDir || process.cwd(),
931
- repair: input.repair === true,
932
- project: ctx.projector || projector.projectAll,
933
- });
934
- }
935
- default:
936
- throw new Error(`unhandled tool ${name}`);
937
- }
938
- }
939
-
940
59
  function errorResponse(code, message, retriable = false, details = undefined) {
941
60
  const error = { code, message, retriable };
942
61
  if (details !== undefined) error.details = details;
@@ -949,16 +68,48 @@ function errorResponse(code, message, retriable = false, details = undefined) {
949
68
  };
950
69
  }
951
70
 
71
+ function isInitializeCall(name, input = {}) {
72
+ return name === 'ultra.record'
73
+ && Array.isArray(input.entries)
74
+ && input.entries.length === 1
75
+ && input.entries[0]?.kind === 'baseline'
76
+ && input.entries[0]?.action === 'initialize';
77
+ }
78
+
79
+ function isMutation(name, input = {}) {
80
+ if (name === 'ultra.record' || name === 'ultra.checkpoint' || name === 'ultra.archive') {
81
+ return true;
82
+ }
83
+ if (name === 'ultra.sync') return input.action !== 'inspect';
84
+ if (name === 'ultra.session') {
85
+ return ['acquire', 'heartbeat', 'release'].includes(input.action);
86
+ }
87
+ return name === 'ultra.doctor' && input.repair === true;
88
+ }
89
+
90
+ function requiresTeamSync(name, input = {}) {
91
+ return name === 'ultra.record'
92
+ || name === 'ultra.checkpoint'
93
+ || name === 'ultra.archive'
94
+ || (name === 'ultra.session' && input.action === 'acquire');
95
+ }
96
+
97
+ function ownsTeamAuthorityInspection(name) {
98
+ return ['ultra.context', 'ultra.sync', 'ultra.doctor'].includes(name);
99
+ }
100
+
952
101
  function startServer({
953
102
  dbPath,
954
103
  rootDir,
955
104
  sessionId = null,
956
105
  projectOnWrite = true,
957
106
  project = projector.projectAll,
107
+ runtime = process.env.UBP_RUNTIME || 'unknown',
958
108
  }) {
959
109
  let db = null;
960
- let planRecovery = null;
961
110
  let authorityDbPath = path.resolve(dbPath);
111
+ let planRecovery = null;
112
+
962
113
  const markPlanRecoveryRequired = (details) => {
963
114
  planRecovery = {
964
115
  recovered: Number(details?.recovered || 0),
@@ -967,20 +118,31 @@ function startServer({
967
118
  issues: Array.isArray(details?.issues) ? details.issues : [],
968
119
  };
969
120
  };
970
- const getDb = (toolName = null) => {
121
+
122
+ const getDb = (toolName, input = {}) => {
971
123
  if (!db) {
972
124
  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.
125
+ const usesProjectAuthority = [
126
+ projectPaths.legacyStateDbPath,
127
+ projectPaths.stateDbPath,
128
+ ].includes(authorityDbPath);
129
+ const projectExists = fs.existsSync(path.join(rootDir, '.ultra'));
130
+
131
+ if (!projectExists) {
132
+ const error = new Error(
133
+ 'Ultra authority is not initialized; invoke ultra-init before using project tools',
134
+ );
135
+ error.code = 'STATE_DB_MISSING';
136
+ throw error;
137
+ }
138
+
139
+ if (usesProjectAuthority) {
978
140
  authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
979
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
141
+ admitStorageBoundary: () => (
142
+ gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
143
+ ),
980
144
  }).stateDbPath;
981
145
  } else {
982
- // Only a real external/task-linked override reaches binding
983
- // validation. It may never invent or borrow project authority.
984
146
  runtimePaths.locateStateDb(rootDir, {
985
147
  env: { UBP_DB_PATH: authorityDbPath },
986
148
  });
@@ -988,29 +150,51 @@ function startServer({
988
150
  env: { UBP_DB_PATH: authorityDbPath },
989
151
  allowConfiguredRuntimeLink: true,
990
152
  migrateState: false,
991
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
153
+ admitStorageBoundary: () => (
154
+ gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
155
+ ),
992
156
  });
993
157
  }
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,
158
+
159
+ if (['ultra.context', 'ultra.doctor'].includes(toolName)
160
+ && !fs.existsSync(authorityDbPath)) {
161
+ const error = new Error('Ultra runtime state is missing');
162
+ error.code = 'STATE_DB_MISSING';
163
+ throw error;
164
+ }
165
+
166
+ db = fs.existsSync(authorityDbPath)
167
+ ? openStateDb(authorityDbPath)
168
+ : initStateDb(authorityDbPath).db;
169
+ ensureSchemaVersion(db);
170
+
171
+ if (!ownsTeamAuthorityInspection(toolName)) {
172
+ const teamSync = assertStateAuthority(db, rootDir, {
173
+ importTeamLedger: toolName !== 'ultra.context',
1004
174
  });
175
+ if (teamSync?.status === 'imported') {
176
+ const job = runtimeState.enqueueProjection(db, { tool_name: 'ultra.sync' });
177
+ runtimeState.processProjectionJobs(db, {
178
+ rootDir,
179
+ project,
180
+ limit: 500,
181
+ });
182
+ if (!job?.id) {
183
+ const error = new Error('team authority import projection was not scheduled');
184
+ error.code = 'STATE_CORRUPT';
185
+ throw error;
186
+ }
187
+ }
1005
188
  }
1006
189
  planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1007
190
  }
1008
- if (toolName !== 'system.doctor'
1009
- && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
191
+
192
+ if (!['ultra.context', 'ultra.doctor'].includes(toolName)
193
+ && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
1010
194
  planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1011
195
  if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
1012
196
  const error = new Error(
1013
- 'plan publication recovery is incomplete; run system.doctor with repair=true',
197
+ 'plan publication recovery is incomplete; run ultra.doctor with repair=true',
1014
198
  );
1015
199
  error.code = 'PLAN_RECOVERY_REQUIRED';
1016
200
  error.details = planRecovery;
@@ -1020,13 +204,13 @@ function startServer({
1020
204
  return db;
1021
205
  };
1022
206
 
1023
- const tools = loadRegisteredTools();
207
+ const toolContracts = loadToolContracts();
1024
208
  const ajv = buildAjv();
1025
209
  const inputValidators = new Map();
1026
210
  const outputValidators = new Map();
1027
- for (const t of tools) {
1028
- inputValidators.set(t.name, ajv.compile(t.input_schema));
1029
- outputValidators.set(t.name, ajv.compile(t.output_schema));
211
+ for (const tool of toolContracts) {
212
+ inputValidators.set(tool.name, ajv.compile(tool.input_schema));
213
+ outputValidators.set(tool.name, ajv.compile(tool.output_schema));
1030
214
  }
1031
215
 
1032
216
  const server = new Server(
@@ -1035,128 +219,113 @@ function startServer({
1035
219
  );
1036
220
 
1037
221
  server.setRequestHandler('tools/list', async () => ({
1038
- tools: tools.map((t) => ({
1039
- name: t.name,
1040
- description: t.description,
1041
- inputSchema: t.input_schema,
222
+ tools: toolContracts.map((tool) => ({
223
+ name: tool.name,
224
+ description: tool.description,
225
+ inputSchema: tool.input_schema,
1042
226
  })),
1043
227
  }));
1044
228
 
1045
229
  server.setRequestHandler('tools/call', async (request) => {
1046
- const { name, arguments: args = {} } = request.params;
1047
- const toolStart = Date.now();
230
+ const { name, arguments: input = {} } = request.params;
231
+ const startedAt = Date.now();
1048
232
  let toolError = null;
233
+ let toolDb = null;
1049
234
 
1050
235
  const emitTelemetry = () => {
1051
- if (!db) return;
236
+ if (!toolDb) return;
1052
237
  try {
1053
- telemetry.appendTelemetry(db, {
238
+ telemetry.appendTelemetry(toolDb, {
1054
239
  event_type: 'tool_call',
1055
240
  tool_name: name,
1056
- session_id: (args && args.sid) || null,
241
+ session_id: input.scope?.sid || null,
1057
242
  rootDir,
1058
243
  payload: {
1059
- duration_ms: Date.now() - toolStart,
1060
- task_id: (args && (args.task_id || args.id)) || null,
244
+ duration_ms: Date.now() - startedAt,
245
+ task_id: input.scope?.task_id || null,
1061
246
  error: toolError,
1062
247
  },
1063
248
  });
1064
- } catch (err) {
1065
- process.stderr.write(`telemetry warning: ${err.message}\n`);
249
+ } catch (error) {
250
+ process.stderr.write(`telemetry warning: ${error.message}\n`);
1066
251
  }
1067
252
  };
1068
253
 
1069
254
  if (!REGISTERED_TOOLS.includes(name)) {
1070
- toolError = 'UNKNOWN_TOOL';
1071
- emitTelemetry();
1072
255
  return errorResponse('UNKNOWN_TOOL', `tool ${name} is not registered on this server`);
1073
256
  }
1074
257
  const validateInput = inputValidators.get(name);
1075
- if (!validateInput(args)) {
1076
- toolError = 'VALIDATION_ERROR';
1077
- emitTelemetry();
258
+ if (!validateInput(input)) {
1078
259
  return errorResponse('VALIDATION_ERROR', ajv.errorsText(validateInput.errors));
1079
260
  }
1080
261
 
1081
- let result;
1082
- let toolDb = null;
1083
262
  try {
1084
- toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
1085
- if (toolDb) assertStateAuthority(toolDb, rootDir);
1086
- if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
263
+ toolDb = isInitializeCall(name, input) ? null : getDb(name, input);
264
+ if (toolDb && requiresTeamSync(name, input)) {
1087
265
  taskLedger.syncTaskLedger(toolDb, { rootDir });
1088
266
  }
1089
- result = await dispatchTool(
1090
- name,
1091
- args,
1092
- toolDb,
1093
- {
1094
- rootDir,
1095
- sessionId,
1096
- projector: project,
1097
- markPlanRecoveryRequired,
1098
- },
1099
- );
1100
- if (name === 'system.doctor' && args.repair === true) {
267
+ const result = await ultraFacade.dispatch(name, input, toolDb, {
268
+ rootDir,
269
+ runtime,
270
+ sessionId,
271
+ projector: project,
272
+ markPlanRecoveryRequired,
273
+ });
274
+ if (name === 'ultra.doctor' && input.repair === true) {
1101
275
  planRecovery = result.repair?.plan_publications || null;
1102
276
  }
1103
- } catch (err) {
1104
- const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
1105
- toolError = code;
1106
- emitTelemetry();
1107
- return errorResponse(code, err.message, !!err.retriable, err.details);
1108
- }
1109
277
 
1110
- let runtimeMeta = null;
1111
- const shouldProject = toolDb
1112
- && projectOnWrite
1113
- && MUTATING_TOOLS.has(name)
1114
- && !(name === 'task.ledger_import' && result.already_current === true);
1115
- if (shouldProject) {
1116
- const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
1117
- const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
1118
- const own = processed.jobs.find((item) => item.id === job.id);
1119
- runtimeMeta = {
1120
- state_commit: 'committed',
1121
- projection_status: own ? own.status : 'failed',
1122
- projection_job_id: job.id,
1123
- };
1124
- if (own && own.incident_id) runtimeMeta.incident_id = own.incident_id;
1125
- } else if (toolDb && name === 'system.doctor' && args.repair === true) {
1126
- runtimeMeta = {
1127
- state_commit: 'committed',
1128
- projection_status: result.checks.projections.status === 'pass' ? 'completed' : 'failed',
1129
- backup_path: result.backup_path,
1130
- };
1131
- }
278
+ let runtimeMeta = null;
279
+ if (toolDb && projectOnWrite && isMutation(name, input)) {
280
+ const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
281
+ const processed = runtimeState.processProjectionJobs(
282
+ toolDb,
283
+ { rootDir, project, limit: 500 },
284
+ );
285
+ const own = processed.jobs.find((item) => item.id === job.id);
286
+ runtimeMeta = {
287
+ state_commit: 'committed',
288
+ projection_status: own?.status || 'failed',
289
+ projection_job_id: job.id,
290
+ };
291
+ if (own?.incident_id) runtimeMeta.incident_id = own.incident_id;
292
+ }
293
+
294
+ const validateOutput = outputValidators.get(name);
295
+ if (!validateOutput(result)) {
296
+ toolError = 'OUTPUT_SCHEMA_DRIFT';
297
+ emitTelemetry();
298
+ return errorResponse(
299
+ 'OUTPUT_SCHEMA_DRIFT',
300
+ ajv.errorsText(validateOutput.errors),
301
+ false,
302
+ runtimeMeta ? { _ultra: runtimeMeta } : undefined,
303
+ );
304
+ }
1132
305
 
1133
- const validateOutput = outputValidators.get(name);
1134
- if (!validateOutput(result)) {
1135
- toolError = 'OUTPUT_SCHEMA_DRIFT';
1136
306
  emitTelemetry();
1137
- const response = errorResponse(
1138
- 'OUTPUT_SCHEMA_DRIFT',
1139
- ajv.errorsText(validateOutput.errors),
1140
- false,
1141
- runtimeMeta ? { _ultra: runtimeMeta } : undefined,
307
+ const visible = runtimeMeta ? { ...result, _ultra: runtimeMeta } : result;
308
+ return {
309
+ content: [{ type: 'text', text: JSON.stringify(visible) }],
310
+ structuredContent: result,
311
+ ...(runtimeMeta ? { _meta: { ultra: runtimeMeta } } : {}),
312
+ };
313
+ } catch (error) {
314
+ toolError = error.code || 'STATE_DB_ERROR';
315
+ emitTelemetry();
316
+ return errorResponse(
317
+ toolError,
318
+ error.message,
319
+ Boolean(error.retriable),
320
+ error.details,
1142
321
  );
1143
- if (runtimeMeta) response._meta = { ultra: runtimeMeta };
1144
- return response;
1145
322
  }
1146
-
1147
- emitTelemetry();
1148
- const visible = runtimeMeta ? { ...result, _ultra: runtimeMeta } : result;
1149
- return {
1150
- content: [{ type: 'text', text: JSON.stringify(visible) }],
1151
- structuredContent: result,
1152
- ...(runtimeMeta ? { _meta: { ultra: runtimeMeta } } : {}),
1153
- };
1154
323
  });
1155
324
 
1156
325
  return {
1157
326
  server,
1158
327
  get db() { return db; },
1159
- tools,
328
+ tools: toolContracts,
1160
329
  async close() { closeStateDb(db); },
1161
330
  };
1162
331
  }
@@ -1165,17 +334,15 @@ async function main() {
1165
334
  const rootDir = process.env.UBP_ROOT_DIR
1166
335
  ? path.resolve(process.env.UBP_ROOT_DIR)
1167
336
  : 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
- : '';
337
+ const configuredDb = String(process.env.UBP_DB_PATH || '').trim();
1174
338
  const dbPath = configuredDb
1175
339
  ? path.resolve(configuredDb)
1176
340
  : runtimePaths.pathsFor(rootDir).stateDbPath;
1177
- const sessionId = process.env.UBP_SESSION_ID || null;
1178
- const handle = startServer({ dbPath, rootDir, sessionId });
341
+ const handle = startServer({
342
+ dbPath,
343
+ rootDir,
344
+ sessionId: process.env.UBP_SESSION_ID || null,
345
+ });
1179
346
  const transport = new StdioServerTransport();
1180
347
  await handle.server.connect(transport);
1181
348
  const cleanup = () => handle.close().finally(() => process.exit(0));
@@ -1201,37 +368,29 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
1201
368
  const db = openStateDb(dbPath);
1202
369
  try {
1203
370
  ensureSchemaVersion(db);
1204
- const workflow = db.prepare(
1205
- `SELECT change_id, task_id FROM workflow_runs
1206
- WHERE status IN ('active', 'blocked', 'ready') AND change_id IS NOT NULL
1207
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
1208
- ).get();
1209
- if (!workflow) return { recorded: false, reason: 'NO_ACTIVE_WORKFLOW' };
1210
371
  const change = db.prepare(
1211
372
  `SELECT id FROM changes
1212
- WHERE id = ? AND status IN ('active', 'blocked', 'ready')`,
1213
- ).get(workflow.change_id);
373
+ WHERE status = 'active'
374
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
375
+ ).get();
1214
376
  if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
1215
- const task = workflow.task_id
1216
- ? db.prepare(
1217
- `SELECT id FROM tasks
1218
- WHERE id = ? AND change_id = ?
1219
- AND status IN ('in_progress', 'pending', 'blocked')`,
1220
- ).get(workflow.task_id, change.id)
1221
- : db.prepare(
1222
- `SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
1223
- ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
1224
- updated_at DESC, rowid DESC LIMIT 1`,
1225
- ).get(change.id);
377
+ const task = db.prepare(
378
+ `SELECT id FROM tasks
379
+ WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
380
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
381
+ updated_at DESC, rowid DESC LIMIT 1`,
382
+ ).get(change.id);
1226
383
  const payload = {
1227
384
  agent_id: hookMetadata(hookInput.agent_id),
1228
385
  agent_type: hookMetadata(hookInput.agent_type),
1229
386
  host_session_id: hookMetadata(hookInput.session_id, '', 256),
1230
387
  };
388
+ const hookRuntime = hookMetadata(hookInput.runtime, '', 64) || null;
1231
389
  ops.appendEvent(db, {
1232
390
  type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
1233
391
  change_id: change.id,
1234
392
  task_id: task?.id || null,
393
+ runtime: hookRuntime,
1235
394
  payload,
1236
395
  });
1237
396
  return { recorded: true, change_id: change.id, task_id: task?.id || null };
@@ -1241,8 +400,8 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
1241
400
  }
1242
401
 
1243
402
  if (require.main === module) {
1244
- main().catch((err) => {
1245
- process.stderr.write(`mcp-server fatal: ${err.message}\n`);
403
+ main().catch((error) => {
404
+ process.stderr.write(`mcp-server fatal: ${error.message}\n`);
1246
405
  process.exit(1);
1247
406
  });
1248
407
  }
@@ -1250,20 +409,10 @@ if (require.main === module) {
1250
409
  module.exports = {
1251
410
  main,
1252
411
  startServer,
1253
- dispatchTool,
1254
- TASK_TOOLS,
1255
- SESSION_TOOLS,
1256
- PLAN_TOOLS,
1257
- BASELINE_TOOLS,
1258
- CHANGE_TOOLS,
1259
- WORKFLOW_TOOLS,
1260
- ARTIFACT_TOOLS,
1261
- SYSTEM_TOOLS,
1262
412
  REGISTERED_TOOLS,
1263
- STATELESS_TOOLS,
1264
- MUTATING_TOOLS,
413
+ PUBLIC_TOOLS,
1265
414
  appendHookLifecycleEvent,
1266
415
  findProjectRoot: runtimePaths.findProjectRoot,
1267
- readProjectBreadcrumb,
1268
- renderProjectBreadcrumb,
416
+ readProjectContextEnvelope,
417
+ renderProjectContextEnvelope,
1269
418
  };