ultra-builder-pro-cli 0.23.0 → 0.24.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 (95) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/.ultra-template/tasks/tasks.json +4 -2
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +55 -39
  5. package/adapters/_shared/codex-assets.cjs +121 -65
  6. package/adapters/_shared/interaction-contract.cjs +7 -1
  7. package/adapters/_shared/provenance.cjs +7 -2
  8. package/adapters/_shared/runtime-assets.cjs +3 -2
  9. package/adapters/_shared/runtime-native.cjs +132 -0
  10. package/adapters/claude.js +60 -30
  11. package/adapters/codex.js +178 -81
  12. package/adapters/grok.js +372 -0
  13. package/adapters/kimi.js +7 -3
  14. package/adapters/opencode.js +210 -32
  15. package/agents/code-reviewer.md +4 -2
  16. package/agents/debugger.md +4 -1
  17. package/agents/review-code.md +7 -3
  18. package/agents/review-comments.md +7 -4
  19. package/agents/review-coordinator.md +11 -7
  20. package/agents/review-design.md +8 -4
  21. package/agents/review-errors.md +8 -4
  22. package/agents/review-spec.md +8 -5
  23. package/agents/review-tests.md +7 -4
  24. package/agents/tdd-runner.md +5 -2
  25. package/bin/install.js +9 -4
  26. package/bin/orchestrator.js +4 -13
  27. package/docs/AGENT-CONTEXT.md +143 -229
  28. package/docs/ARCHITECTURE.md +205 -494
  29. package/docs/ARTIFACT-AUTHORITY.md +35 -28
  30. package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
  31. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  32. package/docs/ROADMAP.md +85 -110
  33. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  34. package/docs/SKILL-AUTHORING.md +3 -3
  35. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  36. package/docs/WORKFLOW-LIFECYCLE.md +189 -448
  37. package/hooks/README.md +11 -8
  38. package/hooks/active_task_context.py +16 -16
  39. package/hooks/adapters/grok.py +134 -0
  40. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  41. package/hooks/health_check.py +29 -34
  42. package/hooks/pre_stop_check.py +22 -12
  43. package/hooks/subagent_tracker.py +1 -1
  44. package/hooks/workflow_checkpoint.py +14 -13
  45. package/hooks/workflow_context.py +11 -12
  46. package/hooks/workflow_resume.py +14 -13
  47. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  48. package/mcp-server/lib/baseline-workflow.cjs +108 -82
  49. package/mcp-server/lib/canonical-json.cjs +19 -0
  50. package/mcp-server/lib/change-authority.cjs +26 -0
  51. package/mcp-server/lib/change-workflow.cjs +630 -45
  52. package/mcp-server/lib/context-envelope.cjs +508 -0
  53. package/mcp-server/lib/decision-records.cjs +337 -0
  54. package/mcp-server/lib/doctor.cjs +91 -33
  55. package/mcp-server/lib/init-project.cjs +78 -39
  56. package/mcp-server/lib/managed-file-write.cjs +116 -0
  57. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  58. package/mcp-server/lib/project-context.cjs +178 -0
  59. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  60. package/mcp-server/lib/state-db.cjs +155 -24
  61. package/mcp-server/lib/state-ops.cjs +21 -3
  62. package/mcp-server/lib/task-ledger.cjs +522 -13
  63. package/mcp-server/lib/ultra-facade.cjs +757 -442
  64. package/mcp-server/lib/worker-packet.cjs +358 -0
  65. package/mcp-server/server.cjs +200 -1103
  66. package/orchestrator/daemon.cjs +27 -9
  67. package/orchestrator/parallel-orchestrator.cjs +111 -76
  68. package/orchestrator/session-runner.cjs +135 -34
  69. package/package.json +10 -3
  70. package/skills/ultra-change/SKILL.md +10 -8
  71. package/skills/ultra-deliver/SKILL.md +17 -19
  72. package/skills/ultra-dev/SKILL.md +20 -11
  73. package/skills/ultra-doctor/SKILL.md +4 -4
  74. package/skills/ultra-init/SKILL.md +6 -5
  75. package/skills/ultra-init/references/brownfield-adoption.md +20 -14
  76. package/skills/ultra-plan/SKILL.md +13 -9
  77. package/skills/ultra-research/SKILL.md +18 -9
  78. package/skills/ultra-review/SKILL.md +14 -9
  79. package/skills/ultra-review/references/unified-schema.md +2 -0
  80. package/skills/ultra-review/scripts/review_wait.py +8 -0
  81. package/skills/ultra-status/SKILL.md +2 -2
  82. package/skills/ultra-test/SKILL.md +10 -6
  83. package/skills/ultra-think/SKILL.md +8 -7
  84. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  85. package/spec/cli-protocol.md +80 -199
  86. package/spec/mcp-tools.yaml +43 -2806
  87. package/spec/schemas/state-db.sql +107 -4
  88. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  89. package/ultra-tools/commands/status.cjs +369 -243
  90. package/mcp-server/lib/context-spine.cjs +0 -1208
  91. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  92. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  93. package/mcp-server/lib/spec-learning.cjs +0 -284
  94. package/mcp-server/lib/workflow-state.cjs +0 -3129
  95. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -1,194 +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
32
  const ultraFacade = require('./lib/ultra-facade.cjs');
43
- const {
44
- readProjectBreadcrumb,
45
- renderProjectBreadcrumb,
46
- } = require('./lib/project-breadcrumb.cjs');
47
- const sessionRunner = require('../orchestrator/session-runner.cjs');
48
33
 
49
34
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
50
35
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
51
36
  : path.resolve(__dirname, '..');
52
37
  const TOOLS_FILE = path.join(REPO_ROOT, 'spec', 'mcp-tools.yaml');
53
-
54
- const TASK_TOOLS = Object.freeze([
55
- 'task.create',
56
- 'task.update',
57
- 'task.list',
58
- 'task.get',
59
- 'task.delete',
60
- 'task.init_project',
61
- 'task.append_event',
62
- 'task.subscribe_events',
63
- 'task.switch_tag',
64
- 'task.dependency_topo',
65
- 'task.parse_prd',
66
- 'task.expand',
67
- 'task.ledger_get',
68
- 'task.ledger_publish',
69
- 'task.ledger_import',
70
- ]);
71
-
72
- const SESSION_TOOLS = Object.freeze([
73
- 'session.spawn',
74
- 'session.close',
75
- 'session.get',
76
- 'session.list',
77
- 'session.admission_check',
78
- 'session.heartbeat',
79
- 'session.subscribe_events',
80
- ]);
81
-
82
- const PLAN_TOOLS = Object.freeze([
83
- 'plan.export',
84
- 'plan.get',
85
- ]);
86
-
87
- const BASELINE_TOOLS = Object.freeze([
88
- 'baseline.start',
89
- 'baseline.record',
90
- 'baseline.get',
91
- 'baseline.converge',
92
- ]);
93
-
94
- const CHANGE_TOOLS = Object.freeze([
95
- 'change.create',
96
- 'change.update',
97
- 'change.delta',
98
- 'change.documentation_reconcile',
99
- 'change.get',
100
- 'change.list',
101
- 'change.context',
102
- 'change.breadcrumb',
103
- 'change.learning_propose',
104
- 'change.learning_resolve',
105
- 'change.converge',
106
- 'change.archive',
107
- ]);
108
-
109
- const WORKFLOW_TOOLS = Object.freeze([
110
- 'workflow.start',
111
- 'workflow.get',
112
- 'workflow.list',
113
- 'workflow.step',
114
- 'workflow.revise',
115
- 'workflow.supersede',
116
- 'workflow.complete',
117
- ]);
118
-
119
- const DECISION_TOOLS = Object.freeze([
120
- 'decision.thread_start',
121
- 'decision.get',
122
- 'decision.list',
123
- 'decision.open',
124
- 'decision.resolve',
125
- 'decision.delegate',
126
- 'decision.defer',
127
- 'decision.supersede',
128
- 'decision.complete',
129
- 'decision.checkpoint',
130
- ]);
131
-
132
- const SYSTEM_TOOLS = Object.freeze([
133
- 'system.doctor',
134
- ]);
135
-
136
- const ARTIFACT_TOOLS = Object.freeze([
137
- 'artifact.record',
138
- 'artifact.get',
139
- ]);
140
-
141
- const LEGACY_TOOLS = Object.freeze([
142
- ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
143
- ...DECISION_TOOLS,
144
- ...WORKFLOW_TOOLS,
145
- ...ARTIFACT_TOOLS,
146
- ...SYSTEM_TOOLS,
147
- ]);
148
38
  const PUBLIC_TOOLS = ultraFacade.PUBLIC_TOOLS;
149
- const REGISTERED_TOOLS = Object.freeze([...PUBLIC_TOOLS, ...LEGACY_TOOLS]);
150
-
151
- const STATELESS_TOOLS = new Set(['task.init_project']);
152
-
153
- function isStatelessPublicCall(name, input = {}) {
154
- return name === 'ultra.record'
155
- && Array.isArray(input.entries)
156
- && input.entries.length === 1
157
- && input.entries[0]?.operation === 'task.init_project';
158
- }
159
-
160
- // init_project owns its target project's schema, baseline seed, and first
161
- // projection internally. Do not enqueue a second projection on the caller DB.
162
- const MUTATING_TOOLS = new Set([
163
- 'ultra.record', 'ultra.checkpoint', 'ultra.sync', 'ultra.session', 'ultra.archive',
164
- 'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
165
- 'task.parse_prd', 'task.expand',
166
- 'task.ledger_import',
167
- 'session.spawn', 'session.close', 'session.heartbeat',
168
- 'plan.export',
169
- 'baseline.start', 'baseline.record', 'baseline.converge',
170
- 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
171
- 'change.context', 'change.converge', 'change.archive',
172
- 'change.learning_propose', 'change.learning_resolve',
173
- 'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
174
- 'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
175
- 'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
176
- 'workflow.complete',
177
- 'artifact.record',
178
- ]);
39
+ const REGISTERED_TOOLS = PUBLIC_TOOLS;
179
40
 
180
- const TEAM_LEDGER_SYNC_TOOLS = new Set([
181
- 'ultra.record', 'ultra.checkpoint', 'ultra.archive',
182
- 'task.create', 'task.update', 'task.delete', 'task.parse_prd', 'task.expand',
183
- 'plan.export',
184
- 'baseline.start', 'baseline.record', 'baseline.converge',
185
- 'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
186
- 'change.converge', 'change.archive',
187
- ]);
188
-
189
- function loadRegisteredTools() {
41
+ function loadToolContracts() {
190
42
  const manifest = yaml.load(fs.readFileSync(TOOLS_FILE, 'utf8'));
191
- 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;
192
51
  }
193
52
 
194
53
  function buildAjv() {
@@ -197,754 +56,6 @@ function buildAjv() {
197
56
  return ajv;
198
57
  }
199
58
 
200
- async function dispatchTool(name, input, db, ctx = {}) {
201
- switch (name) {
202
- case 'task.create': {
203
- const { randomUUID } = require('node:crypto');
204
- const id = input.id || `task-${randomUUID()}`;
205
- const changeId = ops.resolveTaskCreationChangeId(db, input);
206
- const authorizedInput = { ...input, change_id: changeId };
207
- changes.assertTaskCreationAllowed(db, authorizedInput, { rootDir: ctx.rootDir || process.cwd() });
208
- const task = ops.createTask(db, { ...authorizedInput, id });
209
- return { id: task.id, status: task.status, created_at: task.created_at };
210
- }
211
- case 'task.update': {
212
- const before = ops.readTask(db, input.id);
213
- const task = ops.patchTask(db, input.id, input.patch || {});
214
- const durableChanged = before
215
- && taskLedger.durableTask(before).digest !== taskLedger.durableTask(task).digest;
216
- if (durableChanged) {
217
- taskLedger.publishTaskLedger(db, {
218
- rootDir: ctx.rootDir || process.cwd(),
219
- reason: before.status !== task.status
220
- ? `task_${task.status}`
221
- : 'task_contract_updated',
222
- });
223
- }
224
- return { ok: true, task };
225
- }
226
- case 'task.list': {
227
- const tasks = ops.listTasks(db, input || {});
228
- return { tasks, count: tasks.length };
229
- }
230
- case 'task.get': {
231
- const task = ops.readTask(db, input.id);
232
- if (!task) {
233
- const err = new Error(`task ${input.id} not found`);
234
- err.code = 'TASK_NOT_FOUND';
235
- throw err;
236
- }
237
- return { task };
238
- }
239
- case 'task.delete': {
240
- const result = ops.deleteTask(db, input.id, { force: !!input.force });
241
- taskLedger.publishTaskLedger(db, {
242
- rootDir: ctx.rootDir || process.cwd(),
243
- reason: 'task_deleted',
244
- });
245
- return result;
246
- }
247
- case 'task.init_project': {
248
- return initProject(input);
249
- }
250
- case 'task.append_event': {
251
- const r = ops.appendEvent(db, {
252
- type: input.type,
253
- task_id: input.task_id,
254
- change_id: input.change_id,
255
- session_id: input.session_id,
256
- runtime: input.runtime,
257
- payload: input.payload,
258
- });
259
- return { event_id: r.event_id, ts: r.ts };
260
- }
261
- case 'task.subscribe_events': {
262
- return ops.subscribeEventsSince(db, input || {});
263
- }
264
- case 'session.spawn': {
265
- if (ctx.sessionId) {
266
- const error = new Error(
267
- `worker session ${ctx.sessionId} already owns an isolated worktree; `
268
- + 'resume that session instead of recursively spawning another one',
269
- );
270
- error.code = 'NESTED_SESSION_FORBIDDEN';
271
- throw error;
272
- }
273
- const rootDir = ctx.rootDir || process.cwd();
274
- const handle = sessionRunner.spawnSession({
275
- db,
276
- repoRoot: rootDir,
277
- task_id: input.task_id,
278
- runtime: input.runtime,
279
- takeover: Boolean(input.takeover),
280
- worktree_base: input.worktree_base,
281
- });
282
- return {
283
- sid: handle.sid,
284
- worktree_path: handle.worktree_path,
285
- artifact_dir: handle.artifact_dir,
286
- lease_expires_at: handle.lease_expires_at,
287
- worktree_created: true,
288
- };
289
- }
290
- case 'session.close': {
291
- if (ctx.sessionId) {
292
- const error = new Error(
293
- ctx.sessionId === input.sid
294
- ? `worker session ${ctx.sessionId} is supervised by its parent; save evidence and exit so the parent can settle the lease`
295
- : `worker session ${ctx.sessionId} cannot close unrelated session ${input.sid}`,
296
- );
297
- error.code = ctx.sessionId === input.sid
298
- ? 'WORKER_SESSION_PARENT_OWNED'
299
- : 'SESSION_SCOPE_VIOLATION';
300
- throw error;
301
- }
302
- const closed = sessionRunner.closeSession(
303
- { db, repoRoot: ctx.rootDir || process.cwd(), sid: input.sid },
304
- { status: input.status, remove_worktree: input.remove_worktree === true },
305
- );
306
- return {
307
- ok: true,
308
- worktree_preserved: closed.worktree_preserved,
309
- };
310
- }
311
- case 'session.get': {
312
- const session = ops.readSession(db, input.sid);
313
- if (!session) {
314
- const err = new Error(`session ${input.sid} not found`);
315
- err.code = 'SESSION_NOT_FOUND';
316
- throw err;
317
- }
318
- return { session };
319
- }
320
- case 'session.admission_check': {
321
- return sessionRunner.admissionCheck(
322
- db,
323
- ctx.rootDir || process.cwd(),
324
- input.task_id,
325
- );
326
- }
327
- case 'session.list': {
328
- const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
329
- const status = input.status || 'running';
330
- const filtered = status === 'running'
331
- ? sessions
332
- : db.prepare(
333
- "SELECT * FROM sessions WHERE status = ? AND (? IS NULL OR task_id = ?) ORDER BY started_at ASC LIMIT ?",
334
- ).all(status, input.task_id || null, input.task_id || null, Math.min(input.limit || 100, 500));
335
- const limit = Math.min(input.limit || 100, 500);
336
- const trimmed = filtered.slice(0, limit);
337
- return { sessions: trimmed, count: trimmed.length };
338
- }
339
- case 'session.heartbeat': {
340
- return ops.heartbeatSession(db, input.sid);
341
- }
342
- case 'session.subscribe_events': {
343
- return ops.subscribeEventsSince(db, {
344
- since_id: input.since_id,
345
- session_id: input.sid,
346
- limit: input.limit,
347
- });
348
- }
349
- case 'task.switch_tag': {
350
- return ops.switchTaskTag(db, input.id, input.tag);
351
- }
352
- case 'task.dependency_topo': {
353
- const hasTaskIds = Array.isArray(input && input.task_ids);
354
- const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
355
- if (hasTaskIds === hasChangeId) {
356
- const err = new Error('provide exactly one of task_ids or change_id');
357
- err.code = 'VALIDATION_ERROR';
358
- throw err;
359
- }
360
- let rows;
361
- if (hasTaskIds) {
362
- rows = input.task_ids.map((id) => {
363
- const task = ops.readTask(db, id);
364
- if (!task) {
365
- const err = new Error(`task ${id} not found`);
366
- err.code = 'TASK_NOT_FOUND';
367
- throw err;
368
- }
369
- return task;
370
- });
371
- } else {
372
- const change = changes.readChange(db, input.change_id);
373
- if (!change) {
374
- const err = new Error(`change ${input.change_id} not found`);
375
- err.code = 'CHANGE_NOT_FOUND';
376
- throw err;
377
- }
378
- rows = ops.listTasks(db, { change_id: change.id });
379
- }
380
- const graph = rows.map((t) => ({
381
- id: t.id,
382
- deps: Array.isArray(t.deps) ? t.deps : [],
383
- }));
384
- const result = topo.computeWaves(graph);
385
- if (result.cycles.length > 0) {
386
- const err = new Error(`dependency graph has ${result.cycles.length} cycle(s)`);
387
- err.code = 'CYCLE_DETECTED';
388
- err.details = { cycles: result.cycles };
389
- throw err;
390
- }
391
- return { waves: result.waves };
392
- }
393
- case 'task.parse_prd': {
394
- const dryRun = input.dry_run === true;
395
- const rootDir = ctx.rootDir || process.cwd();
396
- if (!dryRun && !String(input.change_id || '').trim()) {
397
- const err = new Error('persisting a parsed task graph requires change_id');
398
- err.code = 'CHANGE_REQUIRED';
399
- throw err;
400
- }
401
- const creationAuthority = { change_id: input.change_id };
402
- if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
403
- const parsed = parser.parsePrd(input.tasks, {
404
- tag: input.tag,
405
- changeId: input.change_id,
406
- });
407
- const shaped = parsed.tasks.map((t) => ({
408
- id: t.id, title: t.title, type: t.type, priority: t.priority,
409
- complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
410
- tag: t.tag, change_id: t.change_id,
411
- }));
412
- const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
413
- const topoResult = topo.computeWaves(graph);
414
- if (topoResult.cycles.length > 0) {
415
- const err = new Error(`parsed task graph has ${topoResult.cycles.length} cycle(s)`);
416
- err.code = 'CYCLE_DETECTED';
417
- err.details = { cycles: topoResult.cycles };
418
- throw err;
419
- }
420
- if (!dryRun) {
421
- try {
422
- ops.tx(db, () => {
423
- changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
424
- for (const t of shaped) ops.createTask(db, t);
425
- });
426
- } catch (err) {
427
- if (['BASELINE_NOT_READY', 'CHANGE_NOT_FOUND', 'CHANGE_NOT_MUTABLE'].includes(err.code)) throw err;
428
- const wrap = new Error(`failed to persist parsed tasks: ${err.message}`);
429
- wrap.code = 'PARSE_FAILED';
430
- wrap.cause = err;
431
- throw wrap;
432
- }
433
- }
434
- return { tasks: shaped, topo: topoResult.waves };
435
- }
436
- case 'task.expand': {
437
- const parent = ops.readTask(db, input.id);
438
- if (!parent) {
439
- const err = new Error(`no task ${input.id}`);
440
- err.code = 'TASK_NOT_FOUND';
441
- throw err;
442
- }
443
- if (parent.status === 'expanded') {
444
- const err = new Error(`task ${input.id} is already expanded`);
445
- err.code = 'ALREADY_EXPANDED';
446
- throw err;
447
- }
448
- const rootDir = ctx.rootDir || process.cwd();
449
- changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
450
- const result = expander.expandTask(db, {
451
- id: input.id,
452
- children: input.children,
453
- rootDir,
454
- });
455
- taskLedger.publishTaskLedger(db, {
456
- rootDir,
457
- reason: 'task_expanded',
458
- });
459
- return { parent_id: result.parent_id, children: result.children };
460
- }
461
- case 'task.ledger_get': {
462
- const rootDir = ctx.rootDir || process.cwd();
463
- return {
464
- ledger: taskLedger.readTaskLedger(rootDir),
465
- health: taskLedger.inspectTaskLedger(db, { rootDir }),
466
- };
467
- }
468
- case 'task.ledger_publish': {
469
- const result = taskLedger.publishTaskLedger(db, {
470
- rootDir: ctx.rootDir || process.cwd(),
471
- reason: input.reason || 'manual_checkpoint',
472
- });
473
- const output = {
474
- changed: result.changed,
475
- path: path.relative(ctx.rootDir || process.cwd(), result.path),
476
- generation: result.ledger.generation,
477
- state_digest: result.ledger.state_digest,
478
- };
479
- if (result.migrated_legacy_projection) {
480
- output.migrated_legacy_projection = true;
481
- output.legacy_backup_path = path.relative(
482
- ctx.rootDir || process.cwd(),
483
- result.legacy_backup_path,
484
- );
485
- }
486
- return output;
487
- }
488
- case 'task.ledger_import': {
489
- const result = taskLedger.importTaskLedger(db, {
490
- rootDir: ctx.rootDir || process.cwd(),
491
- });
492
- return {
493
- imported: result.imported,
494
- preserved: result.preserved,
495
- deleted: result.deleted,
496
- imported_changes: result.imported_changes,
497
- preserved_changes: result.preserved_changes,
498
- imported_baseline: result.imported_baseline,
499
- preserved_baseline: result.preserved_baseline,
500
- generation: result.generation,
501
- state_digest: result.state_digest,
502
- requires_plan_revalidation: result.requires_plan_revalidation,
503
- requires_baseline_revalidation: result.requires_baseline_revalidation,
504
- already_current: result.already_current,
505
- };
506
- }
507
- case 'plan.export': {
508
- const rootDir = ctx.rootDir || process.cwd();
509
- const change = changes.readChange(db, input.change_id);
510
- if (!change) {
511
- const err = new Error(`change ${input.change_id} not found`);
512
- err.code = 'CHANGE_NOT_FOUND';
513
- throw err;
514
- }
515
- if (!['active', 'blocked'].includes(change.status)) {
516
- const err = new Error(`change ${change.id} is ${change.status}`);
517
- err.code = 'CHANGE_NOT_MUTABLE';
518
- throw err;
519
- }
520
- const tasks = ops.listTasks(db, { change_id: change.id });
521
- if (tasks.length === 0) {
522
- const err = new Error('no tasks to plan');
523
- err.code = 'NO_TASKS';
524
- throw err;
525
- }
526
- const context = contextSpine.validateContextSnapshot(db, {
527
- change_id: change.id,
528
- task_id: null,
529
- role: 'plan',
530
- gate: 'planning',
531
- }, { rootDir });
532
- if (!context.snapshot || context.blockers.length > 0) {
533
- const err = new Error('plan export requires a current planning Context Manifest');
534
- err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
535
- err.details = { blockers: context.blockers };
536
- throw err;
537
- }
538
- const plan = planStore.buildPlan(tasks, { changeId: change.id });
539
- const planRun = db.prepare(
540
- `SELECT id FROM workflow_runs
541
- WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
542
- AND status IN ('active', 'blocked', 'ready')
543
- ORDER BY started_at DESC, rowid DESC LIMIT 1`,
544
- ).get(change.id);
545
- const owner = { type: 'change', id: change.id };
546
- const contextArtifact = db.prepare(
547
- `SELECT id FROM artifacts
548
- WHERE path = ? AND status <> 'archived'
549
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
550
- ).get(context.snapshot.manifest_path);
551
- const publication = planStore.prepareChangePlanPublication(plan, {
552
- rootDir,
553
- change,
554
- tasks,
555
- context: {
556
- snapshot_id: context.snapshot.id,
557
- manifest_path: context.snapshot.manifest_path,
558
- manifest_digest: context.snapshot.manifest_hash,
559
- },
560
- });
561
- const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
562
- kind,
563
- file,
564
- digest,
565
- input: {
566
- owner_type: owner.type,
567
- owner_id: owner.id,
568
- kind,
569
- path: path.relative(rootDir, file),
570
- source_refs: [
571
- { type: 'change', id: change.id, relation: 'planned_for' },
572
- ...(contextArtifact
573
- ? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
574
- : []),
575
- ...tasks.map((task) => ({
576
- type: 'task', id: task.id, relation: 'compiled_from_task_contract',
577
- })),
578
- ],
579
- consumer_refs: planRun
580
- ? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
581
- : [],
582
- provenance: {
583
- writer: 'plan.export',
584
- workflow_run_id: planRun?.id || null,
585
- publication_transaction_id: publication.transaction_id,
586
- },
587
- metadata: {
588
- context_snapshot_id: context.snapshot.id,
589
- context_digest: context.snapshot.manifest_hash,
590
- terminal_role: !planRun,
591
- },
592
- },
593
- }));
594
- try {
595
- ops.tx(db, () => {
596
- const preflight = artifactInputs.map((artifact) => (
597
- artifactRegistry.preflightArtifactPublication(
598
- db, artifact.input, { rootDir },
599
- )
600
- ));
601
- publication.publish();
602
- const recorded = artifactInputs.map((artifact, index) => (
603
- artifactRegistry.recordArtifactInTx(db, {
604
- ...artifact.input,
605
- id: preflight[index].artifact_id || undefined,
606
- expected_before_digest: preflight[index].expected_before_digest,
607
- content_digest: artifact.digest,
608
- }, { rootDir })
609
- ));
610
- ops.appendEventInTx(db, {
611
- type: 'plan_exported',
612
- change_id: change.id,
613
- payload: {
614
- change_id: change.id,
615
- plan_path: publication.plan_path,
616
- plan_md_path: publication.plan_md_path,
617
- publication_transaction_id: publication.transaction_id,
618
- wave_count: plan.waves.length,
619
- },
620
- });
621
- return recorded;
622
- });
623
- } catch (error) {
624
- const rollback = publication.rollback();
625
- const failure = rollback?.rolled_back === false
626
- ? planStore.planRecoveryRequiredError(
627
- error,
628
- rollback,
629
- publication.transaction_id,
630
- )
631
- : error;
632
- if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
633
- const issue = failure.details?.rollback_issue
634
- || failure.details?.issue
635
- || {
636
- code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
637
- message: failure.cause?.message || failure.message,
638
- };
639
- ctx.markPlanRecoveryRequired?.({
640
- recovered: 0,
641
- finalized: 0,
642
- pending: 1,
643
- issues: [issue],
644
- });
645
- }
646
- throw failure;
647
- }
648
- try {
649
- publication.commit();
650
- } catch (error) {
651
- const issue = error.details?.issue || {
652
- code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
653
- message: error.cause?.message || error.message,
654
- };
655
- ctx.markPlanRecoveryRequired?.({
656
- recovered: 0,
657
- finalized: 0,
658
- pending: 1,
659
- issues: [issue],
660
- });
661
- // The registry and target bytes committed atomically before cleanup.
662
- // The remaining journal is still authoritative recovery work, so the
663
- // caller must see a typed failure and later mutations must stop.
664
- throw error;
665
- }
666
- return {
667
- plan_path: publication.plan_path,
668
- plan_md_path: publication.plan_md_path,
669
- wave_count: plan.waves.length,
670
- };
671
- }
672
- case 'plan.get': {
673
- const rootDir = ctx.rootDir || process.cwd();
674
- const change = changes.readChange(db, input.change_id);
675
- if (!change) {
676
- const err = new Error(`change ${input.change_id} not found`);
677
- err.code = 'CHANGE_NOT_FOUND';
678
- throw err;
679
- }
680
- const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
681
- db,
682
- strict: true,
683
- });
684
- if (!loaded) {
685
- const err = new Error('no plan has been computed yet');
686
- err.code = 'NO_PLAN';
687
- throw err;
688
- }
689
- return { plan: planStore.selectSection(loaded, input.section) };
690
- }
691
- case 'baseline.start': {
692
- return {
693
- baseline: baselines.startBaseline(
694
- db, input, { rootDir: ctx.rootDir || process.cwd() },
695
- ),
696
- };
697
- }
698
- case 'baseline.record': {
699
- return {
700
- baseline: baselines.recordBaseline(
701
- db, input, { rootDir: ctx.rootDir || process.cwd() },
702
- ),
703
- };
704
- }
705
- case 'baseline.get': {
706
- const baseline = baselines.readBaseline(db, input.id);
707
- if (!baseline) {
708
- const err = new Error(input.id ? `baseline ${input.id} not found` : 'current baseline not found');
709
- err.code = 'BASELINE_NOT_FOUND';
710
- throw err;
711
- }
712
- return {
713
- baseline,
714
- health: baselines.inspectBaseline(db, {
715
- rootDir: ctx.rootDir || process.cwd(), id: input.id,
716
- }),
717
- };
718
- }
719
- case 'baseline.converge': {
720
- const result = baselines.convergeBaseline(
721
- db, input, { rootDir: ctx.rootDir || process.cwd() },
722
- );
723
- if (result.ready) {
724
- taskLedger.publishTaskLedger(db, {
725
- rootDir: ctx.rootDir || process.cwd(),
726
- reason: 'baseline_converged',
727
- });
728
- }
729
- return result;
730
- }
731
- case 'change.create': {
732
- const { randomUUID } = require('node:crypto');
733
- const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
734
- const result = changes.createChange(
735
- db,
736
- { ...input, id },
737
- { rootDir: ctx.rootDir || process.cwd() },
738
- );
739
- taskLedger.publishTaskLedger(db, {
740
- rootDir: ctx.rootDir || process.cwd(),
741
- reason: 'change_created',
742
- });
743
- return result;
744
- }
745
- case 'change.update': {
746
- const change = changes.updateChange(
747
- db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
748
- );
749
- taskLedger.publishTaskLedger(db, {
750
- rootDir: ctx.rootDir || process.cwd(),
751
- reason: 'change_updated',
752
- });
753
- return {
754
- ok: true,
755
- change,
756
- };
757
- }
758
- case 'change.get': {
759
- const change = changes.readChange(db, input.id);
760
- if (!change) {
761
- const err = new Error(`change ${input.id} not found`);
762
- err.code = 'CHANGE_NOT_FOUND';
763
- throw err;
764
- }
765
- return {
766
- change: {
767
- ...change,
768
- learning_candidates: changes.listSpecLearning(db, change.id),
769
- },
770
- };
771
- }
772
- case 'change.list': {
773
- const rows = changes.listChanges(db, input || {});
774
- return { changes: rows, count: rows.length };
775
- }
776
- case 'change.context': {
777
- const out = changes.compileContext(db, input, { rootDir: ctx.rootDir || process.cwd() });
778
- return {
779
- manifest_path: out.context_manifest_path,
780
- manifest_hash: out.manifest_hash,
781
- manifest: out.manifest,
782
- };
783
- }
784
- case 'change.breadcrumb': {
785
- return {
786
- breadcrumb: changes.readBreadcrumb(
787
- db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
788
- ),
789
- };
790
- }
791
- case 'change.learning_propose': {
792
- return {
793
- candidate: changes.proposeSpecLearning(
794
- db, input, { rootDir: ctx.rootDir || process.cwd() },
795
- ),
796
- };
797
- }
798
- case 'change.learning_resolve': {
799
- return {
800
- candidate: changes.resolveSpecLearning(
801
- db, input, { rootDir: ctx.rootDir || process.cwd() },
802
- ),
803
- };
804
- }
805
- case 'change.converge': {
806
- const result = changes.convergeChange(
807
- db, input, { rootDir: ctx.rootDir || process.cwd() },
808
- );
809
- taskLedger.publishTaskLedger(db, {
810
- rootDir: ctx.rootDir || process.cwd(),
811
- reason: 'change_converged',
812
- });
813
- return result;
814
- }
815
- case 'change.archive': {
816
- const result = changes.archiveChange(
817
- db, input, { rootDir: ctx.rootDir || process.cwd() },
818
- );
819
- taskLedger.publishTaskLedger(db, {
820
- rootDir: ctx.rootDir || process.cwd(),
821
- reason: 'change_archived',
822
- });
823
- return result;
824
- }
825
- case 'change.delta': {
826
- return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
827
- }
828
- case 'change.documentation_reconcile': {
829
- return changes.recordDocumentationReconciliation(
830
- db, input, { rootDir: ctx.rootDir || process.cwd() },
831
- );
832
- }
833
- case 'decision.thread_start': {
834
- return { thread: decisions.startDecisionThread(db, input) };
835
- }
836
- case 'decision.get': {
837
- const thread = decisions.readDecisionThread(db, input.id);
838
- if (!thread) {
839
- const error = new Error(`decision thread ${input.id} not found`);
840
- error.code = 'DECISION_THREAD_NOT_FOUND';
841
- throw error;
842
- }
843
- return { thread };
844
- }
845
- case 'decision.list': {
846
- const threads = decisions.listDecisionThreads(db, input || {});
847
- return { threads, count: threads.length };
848
- }
849
- case 'decision.open': {
850
- return { thread: decisions.openDecision(db, input) };
851
- }
852
- case 'decision.resolve': {
853
- return { thread: decisions.resolveDecision(db, input) };
854
- }
855
- case 'decision.delegate': {
856
- return { thread: decisions.delegateDecision(db, input) };
857
- }
858
- case 'decision.defer': {
859
- return { thread: decisions.deferDecision(db, input) };
860
- }
861
- case 'decision.supersede': {
862
- return { thread: decisions.supersedeDecision(db, input) };
863
- }
864
- case 'decision.complete': {
865
- return {
866
- thread: decisions.completeDecisionThread(
867
- db, input, { rootDir: ctx.rootDir || process.cwd() },
868
- ),
869
- };
870
- }
871
- case 'decision.checkpoint': {
872
- return {
873
- thread: decisions.checkpointDecisionThread(
874
- db, input, { rootDir: ctx.rootDir || process.cwd() },
875
- ),
876
- };
877
- }
878
- case 'workflow.start': {
879
- return {
880
- workflow: workflows.startWorkflow(
881
- db, input, { rootDir: ctx.rootDir || process.cwd() },
882
- ),
883
- };
884
- }
885
- case 'workflow.get': {
886
- const workflow = workflows.readWorkflow(
887
- db, input.id, { rootDir: ctx.rootDir || process.cwd() },
888
- );
889
- if (!workflow) {
890
- const error = new Error(`workflow ${input.id} not found`);
891
- error.code = 'WORKFLOW_NOT_FOUND';
892
- throw error;
893
- }
894
- return { workflow };
895
- }
896
- case 'workflow.list': {
897
- const rows = workflows.listWorkflows(
898
- db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
899
- );
900
- return { workflows: rows, count: rows.length };
901
- }
902
- case 'workflow.step': {
903
- return {
904
- workflow: workflows.recordWorkflowStep(
905
- db, input, { rootDir: ctx.rootDir || process.cwd() },
906
- ),
907
- };
908
- }
909
- case 'workflow.revise': {
910
- return {
911
- workflow: workflows.reviseWorkflow(
912
- db, input, { rootDir: ctx.rootDir || process.cwd() },
913
- ),
914
- };
915
- }
916
- case 'workflow.supersede': {
917
- return workflows.supersedeWorkflow(
918
- db, input, { rootDir: ctx.rootDir || process.cwd() },
919
- );
920
- }
921
- case 'workflow.complete': {
922
- return {
923
- workflow: workflows.completeWorkflow(
924
- db, input, { rootDir: ctx.rootDir || process.cwd() },
925
- ),
926
- };
927
- }
928
- case 'artifact.record': {
929
- return artifactRegistry.recordArtifact(
930
- db, input, { rootDir: ctx.rootDir || process.cwd() },
931
- );
932
- }
933
- case 'artifact.get': {
934
- return { artifact: artifactRegistry.getArtifact(db, input) };
935
- }
936
- case 'system.doctor': {
937
- return doctor.runDoctor(db, {
938
- rootDir: ctx.rootDir || process.cwd(),
939
- repair: input.repair === true,
940
- project: ctx.projector || projector.projectAll,
941
- });
942
- }
943
- default:
944
- throw new Error(`unhandled tool ${name}`);
945
- }
946
- }
947
-
948
59
  function errorResponse(code, message, retriable = false, details = undefined) {
949
60
  const error = { code, message, retriable };
950
61
  if (details !== undefined) error.details = details;
@@ -957,16 +68,48 @@ function errorResponse(code, message, retriable = false, details = undefined) {
957
68
  };
958
69
  }
959
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
+
960
101
  function startServer({
961
102
  dbPath,
962
103
  rootDir,
963
104
  sessionId = null,
964
105
  projectOnWrite = true,
965
106
  project = projector.projectAll,
107
+ runtime = process.env.UBP_RUNTIME || 'unknown',
966
108
  }) {
967
109
  let db = null;
968
- let planRecovery = null;
969
110
  let authorityDbPath = path.resolve(dbPath);
111
+ let planRecovery = null;
112
+
970
113
  const markPlanRecoveryRequired = (details) => {
971
114
  planRecovery = {
972
115
  recovered: Number(details?.recovered || 0),
@@ -975,33 +118,31 @@ function startServer({
975
118
  issues: Array.isArray(details?.issues) ? details.issues : [],
976
119
  };
977
120
  };
978
- const getDb = (toolName = null) => {
121
+
122
+ const getDb = (toolName, input = {}) => {
979
123
  if (!db) {
980
- if (toolName === 'ultra.context') {
981
- if (!fs.existsSync(authorityDbPath)) {
982
- const error = new Error(
983
- 'Ultra authority is not initialized; invoke ultra-init before reading project context',
984
- );
985
- error.code = 'STATE_DB_MISSING';
986
- throw error;
987
- }
988
- db = openStateDb(authorityDbPath);
989
- ensureSchemaVersion(db);
990
- assertStateAuthority(db, rootDir, { importTeamLedger: false });
991
- return db;
992
- }
993
124
  const projectPaths = runtimePaths.pathsFor(rootDir);
994
- if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
995
- // The packaged launcher has no explicit DB override. Its default path
996
- // is the future canonical runtime location, which can legitimately be
997
- // absent while a legacy project still owns its project-level DB. Admit and
998
- // 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) {
999
140
  authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
1000
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
141
+ admitStorageBoundary: () => (
142
+ gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
143
+ ),
1001
144
  }).stateDbPath;
1002
145
  } else {
1003
- // Only a real external/task-linked override reaches binding
1004
- // validation. It may never invent or borrow project authority.
1005
146
  runtimePaths.locateStateDb(rootDir, {
1006
147
  env: { UBP_DB_PATH: authorityDbPath },
1007
148
  });
@@ -1009,29 +150,51 @@ function startServer({
1009
150
  env: { UBP_DB_PATH: authorityDbPath },
1010
151
  allowConfiguredRuntimeLink: true,
1011
152
  migrateState: false,
1012
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
153
+ admitStorageBoundary: () => (
154
+ gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
155
+ ),
1013
156
  });
1014
157
  }
1015
- db = initStateDb(authorityDbPath).db;
1016
- const teamSync = assertStateAuthority(db, rootDir, { importTeamLedger: true });
1017
- if (teamSync?.status === 'imported') {
1018
- runtimeState.enqueueProjection(db, {
1019
- tool_name: 'task.ledger_import',
1020
- });
1021
- runtimeState.processProjectionJobs(db, {
1022
- rootDir,
1023
- project,
1024
- limit: 500,
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',
1025
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
+ }
1026
188
  }
1027
189
  planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1028
190
  }
1029
- if (!['system.doctor', 'ultra.doctor'].includes(toolName)
1030
- && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
191
+
192
+ if (!['ultra.context', 'ultra.doctor'].includes(toolName)
193
+ && (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
1031
194
  planRecovery = planStore.recoverPlanPublications(db, { rootDir });
1032
195
  if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
1033
196
  const error = new Error(
1034
- 'plan publication recovery is incomplete; run system.doctor with repair=true',
197
+ 'plan publication recovery is incomplete; run ultra.doctor with repair=true',
1035
198
  );
1036
199
  error.code = 'PLAN_RECOVERY_REQUIRED';
1037
200
  error.details = planRecovery;
@@ -1041,14 +204,13 @@ function startServer({
1041
204
  return db;
1042
205
  };
1043
206
 
1044
- const tools = loadRegisteredTools();
1045
- const publicTools = tools.filter((tool) => PUBLIC_TOOLS.includes(tool.name));
207
+ const toolContracts = loadToolContracts();
1046
208
  const ajv = buildAjv();
1047
209
  const inputValidators = new Map();
1048
210
  const outputValidators = new Map();
1049
- for (const t of tools) {
1050
- inputValidators.set(t.name, ajv.compile(t.input_schema));
1051
- 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));
1052
214
  }
1053
215
 
1054
216
  const server = new Server(
@@ -1057,156 +219,113 @@ function startServer({
1057
219
  );
1058
220
 
1059
221
  server.setRequestHandler('tools/list', async () => ({
1060
- tools: publicTools.map((t) => ({
1061
- name: t.name,
1062
- description: t.description,
1063
- inputSchema: t.input_schema,
222
+ tools: toolContracts.map((tool) => ({
223
+ name: tool.name,
224
+ description: tool.description,
225
+ inputSchema: tool.input_schema,
1064
226
  })),
1065
227
  }));
1066
228
 
1067
229
  server.setRequestHandler('tools/call', async (request) => {
1068
- const { name, arguments: args = {} } = request.params;
1069
- const toolStart = Date.now();
230
+ const { name, arguments: input = {} } = request.params;
231
+ const startedAt = Date.now();
1070
232
  let toolError = null;
233
+ let toolDb = null;
1071
234
 
1072
235
  const emitTelemetry = () => {
1073
- if (!db) return;
236
+ if (!toolDb) return;
1074
237
  try {
1075
- telemetry.appendTelemetry(db, {
238
+ telemetry.appendTelemetry(toolDb, {
1076
239
  event_type: 'tool_call',
1077
240
  tool_name: name,
1078
- session_id: (args && args.sid) || null,
241
+ session_id: input.scope?.sid || null,
1079
242
  rootDir,
1080
243
  payload: {
1081
- duration_ms: Date.now() - toolStart,
1082
- task_id: (args && (args.task_id || args.id)) || null,
244
+ duration_ms: Date.now() - startedAt,
245
+ task_id: input.scope?.task_id || null,
1083
246
  error: toolError,
1084
247
  },
1085
248
  });
1086
- } catch (err) {
1087
- process.stderr.write(`telemetry warning: ${err.message}\n`);
249
+ } catch (error) {
250
+ process.stderr.write(`telemetry warning: ${error.message}\n`);
1088
251
  }
1089
252
  };
1090
253
 
1091
254
  if (!REGISTERED_TOOLS.includes(name)) {
1092
- toolError = 'UNKNOWN_TOOL';
1093
- emitTelemetry();
1094
255
  return errorResponse('UNKNOWN_TOOL', `tool ${name} is not registered on this server`);
1095
256
  }
1096
257
  const validateInput = inputValidators.get(name);
1097
- if (!validateInput(args)) {
1098
- toolError = 'VALIDATION_ERROR';
1099
- emitTelemetry();
258
+ if (!validateInput(input)) {
1100
259
  return errorResponse('VALIDATION_ERROR', ajv.errorsText(validateInput.errors));
1101
260
  }
1102
261
 
1103
- let result;
1104
- let toolDb = null;
1105
262
  try {
1106
- toolDb = (STATELESS_TOOLS.has(name) || isStatelessPublicCall(name, args))
1107
- ? null
1108
- : getDb(name);
1109
- if (toolDb) assertStateAuthority(toolDb, rootDir);
1110
- if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
263
+ toolDb = isInitializeCall(name, input) ? null : getDb(name, input);
264
+ if (toolDb && requiresTeamSync(name, input)) {
1111
265
  taskLedger.syncTaskLedger(toolDb, { rootDir });
1112
266
  }
1113
- const callContext = {
267
+ const result = await ultraFacade.dispatch(name, input, toolDb, {
1114
268
  rootDir,
269
+ runtime,
1115
270
  sessionId,
1116
271
  projector: project,
1117
272
  markPlanRecoveryRequired,
1118
- };
1119
- const callLegacy = async (legacyName, legacyArgs = {}) => {
1120
- if (!LEGACY_TOOLS.includes(legacyName)) {
1121
- const error = new Error(`legacy operation ${legacyName} is not supported`);
1122
- error.code = 'ULTRA_RECORD_OPERATION_UNSUPPORTED';
1123
- throw error;
1124
- }
1125
- const validateLegacyInput = inputValidators.get(legacyName);
1126
- if (!validateLegacyInput(legacyArgs)) {
1127
- const error = new Error(ajv.errorsText(validateLegacyInput.errors));
1128
- error.code = 'VALIDATION_ERROR';
1129
- error.details = validateLegacyInput.errors;
1130
- throw error;
1131
- }
1132
- const legacyResult = await dispatchTool(
1133
- legacyName,
1134
- legacyArgs,
273
+ });
274
+ if (name === 'ultra.doctor' && input.repair === true) {
275
+ planRecovery = result.repair?.plan_publications || null;
276
+ }
277
+
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(
1135
282
  toolDb,
1136
- callContext,
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,
1137
303
  );
1138
- const validateLegacyOutput = outputValidators.get(legacyName);
1139
- if (!validateLegacyOutput(legacyResult)) {
1140
- const error = new Error(ajv.errorsText(validateLegacyOutput.errors));
1141
- error.code = 'OUTPUT_SCHEMA_DRIFT';
1142
- error.details = validateLegacyOutput.errors;
1143
- throw error;
1144
- }
1145
- return legacyResult;
1146
- };
1147
- result = PUBLIC_TOOLS.includes(name)
1148
- ? await ultraFacade.dispatch(name, args, toolDb, { ...callContext, callLegacy })
1149
- : await dispatchTool(name, args, toolDb, callContext);
1150
- if (['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
1151
- planRecovery = result.repair?.plan_publications || null;
1152
304
  }
1153
- } catch (err) {
1154
- const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
1155
- toolError = code;
1156
- emitTelemetry();
1157
- return errorResponse(code, err.message, !!err.retriable, err.details);
1158
- }
1159
305
 
1160
- let runtimeMeta = null;
1161
- const shouldProject = toolDb
1162
- && projectOnWrite
1163
- && MUTATING_TOOLS.has(name)
1164
- && !(name === 'task.ledger_import' && result.already_current === true);
1165
- if (shouldProject) {
1166
- const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
1167
- const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
1168
- const own = processed.jobs.find((item) => item.id === job.id);
1169
- runtimeMeta = {
1170
- state_commit: 'committed',
1171
- projection_status: own ? own.status : 'failed',
1172
- projection_job_id: job.id,
1173
- };
1174
- if (own && own.incident_id) runtimeMeta.incident_id = own.incident_id;
1175
- } else if (toolDb && ['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
1176
- runtimeMeta = {
1177
- state_commit: 'committed',
1178
- projection_status: result.checks.projections.status === 'pass' ? 'completed' : 'failed',
1179
- backup_path: result.backup_path,
306
+ emitTelemetry();
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 } } : {}),
1180
312
  };
1181
- }
1182
-
1183
- const validateOutput = outputValidators.get(name);
1184
- if (!validateOutput(result)) {
1185
- toolError = 'OUTPUT_SCHEMA_DRIFT';
313
+ } catch (error) {
314
+ toolError = error.code || 'STATE_DB_ERROR';
1186
315
  emitTelemetry();
1187
- const response = errorResponse(
1188
- 'OUTPUT_SCHEMA_DRIFT',
1189
- ajv.errorsText(validateOutput.errors),
1190
- false,
1191
- runtimeMeta ? { _ultra: runtimeMeta } : undefined,
316
+ return errorResponse(
317
+ toolError,
318
+ error.message,
319
+ Boolean(error.retriable),
320
+ error.details,
1192
321
  );
1193
- if (runtimeMeta) response._meta = { ultra: runtimeMeta };
1194
- return response;
1195
322
  }
1196
-
1197
- emitTelemetry();
1198
- const visible = runtimeMeta ? { ...result, _ultra: runtimeMeta } : result;
1199
- return {
1200
- content: [{ type: 'text', text: JSON.stringify(visible) }],
1201
- structuredContent: result,
1202
- ...(runtimeMeta ? { _meta: { ultra: runtimeMeta } } : {}),
1203
- };
1204
323
  });
1205
324
 
1206
325
  return {
1207
326
  server,
1208
327
  get db() { return db; },
1209
- tools,
328
+ tools: toolContracts,
1210
329
  async close() { closeStateDb(db); },
1211
330
  };
1212
331
  }
@@ -1215,17 +334,15 @@ async function main() {
1215
334
  const rootDir = process.env.UBP_ROOT_DIR
1216
335
  ? path.resolve(process.env.UBP_ROOT_DIR)
1217
336
  : path.resolve('.');
1218
- // Keep tools/list side-effect free. Root/layout validation happens inside the
1219
- // first state-backed tool call so clients receive a typed MCP error instead
1220
- // of a transport-level startup failure.
1221
- const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
1222
- ? process.env.UBP_DB_PATH.trim()
1223
- : '';
337
+ const configuredDb = String(process.env.UBP_DB_PATH || '').trim();
1224
338
  const dbPath = configuredDb
1225
339
  ? path.resolve(configuredDb)
1226
340
  : runtimePaths.pathsFor(rootDir).stateDbPath;
1227
- const sessionId = process.env.UBP_SESSION_ID || null;
1228
- const handle = startServer({ dbPath, rootDir, sessionId });
341
+ const handle = startServer({
342
+ dbPath,
343
+ rootDir,
344
+ sessionId: process.env.UBP_SESSION_ID || null,
345
+ });
1229
346
  const transport = new StdioServerTransport();
1230
347
  await handle.server.connect(transport);
1231
348
  const cleanup = () => handle.close().finally(() => process.exit(0));
@@ -1251,37 +368,29 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
1251
368
  const db = openStateDb(dbPath);
1252
369
  try {
1253
370
  ensureSchemaVersion(db);
1254
- const workflow = db.prepare(
1255
- `SELECT change_id, task_id FROM workflow_runs
1256
- WHERE status IN ('active', 'blocked', 'ready') AND change_id IS NOT NULL
1257
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
1258
- ).get();
1259
- if (!workflow) return { recorded: false, reason: 'NO_ACTIVE_WORKFLOW' };
1260
371
  const change = db.prepare(
1261
372
  `SELECT id FROM changes
1262
- WHERE id = ? AND status IN ('active', 'blocked', 'ready')`,
1263
- ).get(workflow.change_id);
373
+ WHERE status = 'active'
374
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
375
+ ).get();
1264
376
  if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
1265
- const task = workflow.task_id
1266
- ? db.prepare(
1267
- `SELECT id FROM tasks
1268
- WHERE id = ? AND change_id = ?
1269
- AND status IN ('in_progress', 'pending', 'blocked')`,
1270
- ).get(workflow.task_id, change.id)
1271
- : db.prepare(
1272
- `SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
1273
- ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
1274
- updated_at DESC, rowid DESC LIMIT 1`,
1275
- ).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);
1276
383
  const payload = {
1277
384
  agent_id: hookMetadata(hookInput.agent_id),
1278
385
  agent_type: hookMetadata(hookInput.agent_type),
1279
386
  host_session_id: hookMetadata(hookInput.session_id, '', 256),
1280
387
  };
388
+ const hookRuntime = hookMetadata(hookInput.runtime, '', 64) || null;
1281
389
  ops.appendEvent(db, {
1282
390
  type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
1283
391
  change_id: change.id,
1284
392
  task_id: task?.id || null,
393
+ runtime: hookRuntime,
1285
394
  payload,
1286
395
  });
1287
396
  return { recorded: true, change_id: change.id, task_id: task?.id || null };
@@ -1291,8 +400,8 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
1291
400
  }
1292
401
 
1293
402
  if (require.main === module) {
1294
- main().catch((err) => {
1295
- process.stderr.write(`mcp-server fatal: ${err.message}\n`);
403
+ main().catch((error) => {
404
+ process.stderr.write(`mcp-server fatal: ${error.message}\n`);
1296
405
  process.exit(1);
1297
406
  });
1298
407
  }
@@ -1300,22 +409,10 @@ if (require.main === module) {
1300
409
  module.exports = {
1301
410
  main,
1302
411
  startServer,
1303
- dispatchTool,
1304
- TASK_TOOLS,
1305
- SESSION_TOOLS,
1306
- PLAN_TOOLS,
1307
- BASELINE_TOOLS,
1308
- CHANGE_TOOLS,
1309
- WORKFLOW_TOOLS,
1310
- ARTIFACT_TOOLS,
1311
- SYSTEM_TOOLS,
1312
412
  REGISTERED_TOOLS,
1313
- LEGACY_TOOLS,
1314
413
  PUBLIC_TOOLS,
1315
- STATELESS_TOOLS,
1316
- MUTATING_TOOLS,
1317
414
  appendHookLifecycleEvent,
1318
415
  findProjectRoot: runtimePaths.findProjectRoot,
1319
- readProjectBreadcrumb,
1320
- renderProjectBreadcrumb,
416
+ readProjectContextEnvelope,
417
+ renderProjectContextEnvelope,
1321
418
  };