ultra-builder-pro-cli 0.13.0 → 0.15.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 (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -2,7 +2,7 @@
2
2
 
3
3
  // MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
4
4
  // .ultra/state.db. Uses the low-level Server API from
5
- // @modelcontextprotocol/sdk so we can pass raw JSON Schema (Draft 2020-12)
5
+ // @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
6
6
  // straight from spec/mcp-tools.yaml without translating to zod.
7
7
 
8
8
  const fs = require('node:fs');
@@ -12,18 +12,18 @@ const yaml = require('js-yaml');
12
12
  const Ajv = require('ajv/dist/2020');
13
13
  const addFormats = require('ajv-formats');
14
14
 
15
- const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
16
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
17
- const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
15
+ const { Server } = require('@modelcontextprotocol/server');
16
+ const { StdioServerTransport } = require('@modelcontextprotocol/server/stdio');
18
17
  const { version: PACKAGE_VERSION } = require('../package.json');
19
18
 
20
- const { initStateDb, closeStateDb } = require('./lib/state-db.cjs');
19
+ const {
20
+ initStateDb, closeStateDb, openStateDb, ensureSchemaVersion,
21
+ } = require('./lib/state-db.cjs');
21
22
  const { assertStateAuthority } = require('./lib/state-authority.cjs');
22
23
  const ops = require('./lib/state-ops.cjs');
23
24
  const projector = require('./lib/projector.cjs');
24
25
  const telemetry = require('./lib/telemetry.cjs');
25
26
  const topo = require('./lib/topo.cjs');
26
- const llm = require('./lib/llm-client.cjs');
27
27
  const parser = require('./lib/prd-parser.cjs');
28
28
  const expander = require('./lib/task-expander.cjs');
29
29
  const planStore = require('./lib/plan-store.cjs');
@@ -31,7 +31,12 @@ const { initProject } = require('./lib/init-project.cjs');
31
31
  const baselines = require('./lib/baseline-workflow.cjs');
32
32
  const changes = require('./lib/change-workflow.cjs');
33
33
  const runtimeState = require('./lib/runtime-state.cjs');
34
+ const workflows = require('./lib/workflow-state.cjs');
34
35
  const doctor = require('./lib/doctor.cjs');
36
+ const {
37
+ readProjectBreadcrumb,
38
+ renderProjectBreadcrumb,
39
+ } = require('./lib/project-breadcrumb.cjs');
35
40
 
36
41
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
37
42
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
@@ -88,12 +93,21 @@ const CHANGE_TOOLS = Object.freeze([
88
93
  'change.archive',
89
94
  ]);
90
95
 
96
+ const WORKFLOW_TOOLS = Object.freeze([
97
+ 'workflow.start',
98
+ 'workflow.get',
99
+ 'workflow.list',
100
+ 'workflow.step',
101
+ 'workflow.complete',
102
+ ]);
103
+
91
104
  const SYSTEM_TOOLS = Object.freeze([
92
105
  'system.doctor',
93
106
  ]);
94
107
 
95
108
  const REGISTERED_TOOLS = Object.freeze([
96
109
  ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
110
+ ...WORKFLOW_TOOLS,
97
111
  ...SYSTEM_TOOLS,
98
112
  ]);
99
113
 
@@ -109,6 +123,7 @@ const MUTATING_TOOLS = new Set([
109
123
  'baseline.start', 'baseline.record', 'baseline.converge',
110
124
  'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
111
125
  'change.learning_propose', 'change.learning_resolve',
126
+ 'workflow.start', 'workflow.step', 'workflow.complete',
112
127
  ]);
113
128
 
114
129
  function loadRegisteredTools() {
@@ -127,22 +142,6 @@ function mintSessionId() {
127
142
  return `sess-${randomUUID().slice(0, 8)}`;
128
143
  }
129
144
 
130
- function resolvePrdText(input, ctx) {
131
- if (input.prd_text && String(input.prd_text).trim()) return String(input.prd_text);
132
- if (input.prd_path) {
133
- const abs = path.isAbsolute(input.prd_path)
134
- ? input.prd_path
135
- : path.join(ctx.rootDir || process.cwd(), input.prd_path);
136
- return fs.readFileSync(abs, 'utf8');
137
- }
138
- return null;
139
- }
140
-
141
- function buildLlmClient(ctx) {
142
- if (ctx && ctx.llmClient) return ctx.llmClient;
143
- return llm.createLlmClient({});
144
- }
145
-
146
145
  async function dispatchTool(name, input, db, ctx = {}) {
147
146
  switch (name) {
148
147
  case 'task.create': {
@@ -265,10 +264,33 @@ async function dispatchTool(name, input, db, ctx = {}) {
265
264
  return ops.switchTaskTag(db, input.id, input.tag);
266
265
  }
267
266
  case 'task.dependency_topo': {
268
- const requested = Array.isArray(input && input.task_ids) ? input.task_ids : null;
269
- const rows = requested
270
- ? requested.map((id) => ops.readTask(db, id)).filter(Boolean)
271
- : ops.listTasks(db, {});
267
+ const hasTaskIds = Array.isArray(input && input.task_ids);
268
+ const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
269
+ if (hasTaskIds === hasChangeId) {
270
+ const err = new Error('provide exactly one of task_ids or change_id');
271
+ err.code = 'VALIDATION_ERROR';
272
+ throw err;
273
+ }
274
+ let rows;
275
+ if (hasTaskIds) {
276
+ rows = input.task_ids.map((id) => {
277
+ const task = ops.readTask(db, id);
278
+ if (!task) {
279
+ const err = new Error(`task ${id} not found`);
280
+ err.code = 'TASK_NOT_FOUND';
281
+ throw err;
282
+ }
283
+ return task;
284
+ });
285
+ } else {
286
+ const change = changes.readChange(db, input.change_id);
287
+ if (!change) {
288
+ const err = new Error(`change ${input.change_id} not found`);
289
+ err.code = 'CHANGE_NOT_FOUND';
290
+ throw err;
291
+ }
292
+ rows = ops.listTasks(db, { change_id: change.id });
293
+ }
272
294
  const graph = rows.map((t) => ({
273
295
  id: t.id,
274
296
  deps: Array.isArray(t.deps) ? t.deps : [],
@@ -283,23 +305,32 @@ async function dispatchTool(name, input, db, ctx = {}) {
283
305
  return { waves: result.waves };
284
306
  }
285
307
  case 'task.parse_prd': {
286
- const prdText = resolvePrdText(input, ctx);
287
- if (!prdText || !prdText.trim()) {
288
- const err = new Error('one of prd_path or prd_text required');
289
- err.code = 'NO_INPUT';
290
- throw err;
291
- }
292
308
  const dryRun = input.dry_run === true;
293
309
  const rootDir = ctx.rootDir || process.cwd();
310
+ if (!dryRun && !String(input.change_id || '').trim()) {
311
+ const err = new Error('persisting a parsed task graph requires change_id');
312
+ err.code = 'CHANGE_REQUIRED';
313
+ throw err;
314
+ }
294
315
  const creationAuthority = { change_id: input.change_id };
295
316
  if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
296
- const client = buildLlmClient(ctx);
297
- const parsed = await parser.parsePrd(prdText, { llmClient: client, tag: input.tag });
317
+ const parsed = parser.parsePrd(input.tasks, {
318
+ tag: input.tag,
319
+ changeId: input.change_id,
320
+ });
298
321
  const shaped = parsed.tasks.map((t) => ({
299
322
  id: t.id, title: t.title, type: t.type, priority: t.priority,
300
323
  complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
301
- tag: t.tag, change_id: input.change_id,
324
+ tag: t.tag, change_id: t.change_id,
302
325
  }));
326
+ const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
327
+ const topoResult = topo.computeWaves(graph);
328
+ if (topoResult.cycles.length > 0) {
329
+ const err = new Error(`parsed task graph has ${topoResult.cycles.length} cycle(s)`);
330
+ err.code = 'CYCLE_DETECTED';
331
+ err.details = { cycles: topoResult.cycles };
332
+ throw err;
333
+ }
303
334
  if (!dryRun) {
304
335
  try {
305
336
  ops.tx(db, () => {
@@ -314,13 +345,9 @@ async function dispatchTool(name, input, db, ctx = {}) {
314
345
  throw wrap;
315
346
  }
316
347
  }
317
- const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
318
- const topoResult = topo.computeWaves(graph);
319
348
  return { tasks: shaped, topo: topoResult.waves };
320
349
  }
321
350
  case 'task.expand': {
322
- // Cheap guards before paying for LLM client construction (which would
323
- // throw NO_LLM_CREDENTIALS when env keys are missing).
324
351
  const parent = ops.readTask(db, input.id);
325
352
  if (!parent) {
326
353
  const err = new Error(`no task ${input.id}`);
@@ -334,32 +361,43 @@ async function dispatchTool(name, input, db, ctx = {}) {
334
361
  }
335
362
  const rootDir = ctx.rootDir || process.cwd();
336
363
  changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
337
- const client = buildLlmClient(ctx);
338
- const result = await expander.expandTask(db, {
364
+ const result = expander.expandTask(db, {
339
365
  id: input.id,
340
- sub_count: input.sub_count,
341
- strategy: input.strategy,
342
- llmClient: client,
366
+ children: input.children,
343
367
  rootDir,
344
368
  });
345
369
  return { parent_id: result.parent_id, children: result.children };
346
370
  }
347
371
  case 'plan.export': {
348
372
  const rootDir = ctx.rootDir || process.cwd();
373
+ const change = changes.readChange(db, input.change_id);
374
+ if (!change) {
375
+ const err = new Error(`change ${input.change_id} not found`);
376
+ err.code = 'CHANGE_NOT_FOUND';
377
+ throw err;
378
+ }
379
+ if (!['active', 'blocked'].includes(change.status)) {
380
+ const err = new Error(`change ${change.id} is ${change.status}`);
381
+ err.code = 'CHANGE_NOT_MUTABLE';
382
+ throw err;
383
+ }
349
384
  const abs = path.isAbsolute(input.out_path)
350
385
  ? input.out_path
351
386
  : path.join(rootDir, input.out_path);
352
- const tasks = ops.listTasks(db, { tag: input.tag });
387
+ const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
353
388
  if (tasks.length === 0) {
354
389
  const err = new Error('no tasks to plan');
355
390
  err.code = 'NO_TASKS';
356
391
  throw err;
357
392
  }
358
- const plan = planStore.buildPlan(tasks, {});
393
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
359
394
  const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
360
395
  ops.appendEvent(db, {
361
- type: 'plan_approved',
362
- payload: { plan_path, wave_count: plan.waves.length, tag: input.tag || null },
396
+ type: 'plan_exported',
397
+ change_id: change.id,
398
+ payload: {
399
+ change_id: change.id, plan_path, wave_count: plan.waves.length, tag: input.tag || null,
400
+ },
363
401
  });
364
402
  return { plan_path, wave_count: plan.waves.length };
365
403
  }
@@ -371,6 +409,13 @@ async function dispatchTool(name, input, db, ctx = {}) {
371
409
  err.code = 'NO_PLAN';
372
410
  throw err;
373
411
  }
412
+ if (loaded.change_id !== input.change_id) {
413
+ const err = new Error(
414
+ `execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
415
+ );
416
+ err.code = 'PLAN_CHANGE_MISMATCH';
417
+ throw err;
418
+ }
374
419
  return { plan: planStore.selectSection(loaded, input.section) };
375
420
  }
376
421
  case 'baseline.start': {
@@ -472,6 +517,44 @@ async function dispatchTool(name, input, db, ctx = {}) {
472
517
  case 'change.archive': {
473
518
  return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
474
519
  }
520
+ case 'workflow.start': {
521
+ return {
522
+ workflow: workflows.startWorkflow(
523
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
524
+ ),
525
+ };
526
+ }
527
+ case 'workflow.get': {
528
+ const workflow = workflows.readWorkflow(
529
+ db, input.id, { rootDir: ctx.rootDir || process.cwd() },
530
+ );
531
+ if (!workflow) {
532
+ const error = new Error(`workflow ${input.id} not found`);
533
+ error.code = 'WORKFLOW_NOT_FOUND';
534
+ throw error;
535
+ }
536
+ return { workflow };
537
+ }
538
+ case 'workflow.list': {
539
+ const rows = workflows.listWorkflows(
540
+ db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
541
+ );
542
+ return { workflows: rows, count: rows.length };
543
+ }
544
+ case 'workflow.step': {
545
+ return {
546
+ workflow: workflows.recordWorkflowStep(
547
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
548
+ ),
549
+ };
550
+ }
551
+ case 'workflow.complete': {
552
+ return {
553
+ workflow: workflows.completeWorkflow(
554
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
555
+ ),
556
+ };
557
+ }
475
558
  case 'system.doctor': {
476
559
  return doctor.runDoctor(db, {
477
560
  rootDir: ctx.rootDir || process.cwd(),
@@ -517,7 +600,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
517
600
  { capabilities: { tools: {} } },
518
601
  );
519
602
 
520
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
603
+ server.setRequestHandler('tools/list', async () => ({
521
604
  tools: tools.map((t) => ({
522
605
  name: t.name,
523
606
  description: t.description,
@@ -525,7 +608,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
525
608
  })),
526
609
  }));
527
610
 
528
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
611
+ server.setRequestHandler('tools/call', async (request) => {
529
612
  const { name, arguments: args = {} } = request.params;
530
613
  const toolStart = Date.now();
531
614
  let toolError = null;
@@ -639,6 +722,51 @@ async function main() {
639
722
  process.on('SIGINT', cleanup);
640
723
  }
641
724
 
725
+ function hookMetadata(value, fallback = 'unknown', maxLength = 256) {
726
+ const normalized = typeof value === 'string' ? value.trim() : '';
727
+ return (normalized || fallback).slice(0, maxLength);
728
+ }
729
+
730
+ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
731
+ if (!['start', 'stop'].includes(action)) {
732
+ const error = new Error('hook lifecycle action must be start or stop');
733
+ error.code = 'VALIDATION_ERROR';
734
+ throw error;
735
+ }
736
+ const root = path.resolve(rootDir || process.cwd());
737
+ const dbPath = path.join(root, '.ultra', 'state.db');
738
+ if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
739
+
740
+ const db = openStateDb(dbPath);
741
+ try {
742
+ ensureSchemaVersion(db);
743
+ const change = db.prepare(
744
+ `SELECT id FROM changes WHERE status IN ('active', 'blocked', 'ready')
745
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
746
+ ).get();
747
+ if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
748
+ const task = db.prepare(
749
+ `SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
750
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
751
+ updated_at DESC, rowid DESC LIMIT 1`,
752
+ ).get(change.id);
753
+ const payload = {
754
+ agent_id: hookMetadata(hookInput.agent_id),
755
+ agent_type: hookMetadata(hookInput.agent_type),
756
+ host_session_id: hookMetadata(hookInput.session_id, '', 256),
757
+ };
758
+ ops.appendEvent(db, {
759
+ type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
760
+ change_id: change.id,
761
+ task_id: task?.id || null,
762
+ payload,
763
+ });
764
+ return { recorded: true, change_id: change.id, task_id: task?.id || null };
765
+ } finally {
766
+ closeStateDb(db);
767
+ }
768
+ }
769
+
642
770
  if (require.main === module) {
643
771
  main().catch((err) => {
644
772
  process.stderr.write(`mcp-server fatal: ${err.message}\n`);
@@ -655,8 +783,12 @@ module.exports = {
655
783
  PLAN_TOOLS,
656
784
  BASELINE_TOOLS,
657
785
  CHANGE_TOOLS,
786
+ WORKFLOW_TOOLS,
658
787
  SYSTEM_TOOLS,
659
788
  REGISTERED_TOOLS,
660
789
  STATELESS_TOOLS,
661
790
  MUTATING_TOOLS,
791
+ appendHookLifecycleEvent,
792
+ readProjectBreadcrumb,
793
+ renderProjectBreadcrumb,
662
794
  };
@@ -16,7 +16,7 @@ const runtimeState = require('../mcp-server/lib/runtime-state.cjs');
16
16
  const projector = require('../mcp-server/lib/projector.cjs');
17
17
  const runner = require('./session-runner.cjs');
18
18
  const recovery = require('./recovery.cjs');
19
- const { evaluate, DEFAULT_RULES, ROUTE_PREFERENCES } = require('./dispatch-rules.cjs');
19
+ const { evaluate, DEFAULT_RULES } = require('./dispatch-rules.cjs');
20
20
 
21
21
  // Phase 8B.1 — routeTask is now a thin wrapper over evaluate() so Phase 5.4
22
22
  // callers see identical behavior while the parallel orchestrator (8B.2) can
@@ -146,6 +146,5 @@ function runDaemon({
146
146
  module.exports = {
147
147
  runDaemon,
148
148
  routeTask,
149
- ROUTE_PREFERENCES,
150
149
  maintainState,
151
150
  };
@@ -8,28 +8,13 @@
8
8
  // • defer — leave task pending, revisit next tick (deps/wave wait)
9
9
  // • block — terminal: don't spawn (breaker, no runtimes, etc.)
10
10
  //
11
- // Phase 5.4 ROUTE_PREFERENCES lives here now. daemon.cjs's `routeTask` is a
12
- // thin wrapper around evaluate() so Phase 5.4 tests stay green while the
13
- // parallel orchestrator (8B.2) gets a richer decision surface: wave state,
14
- // deps readiness, and custom rule injection.
11
+ // Runtime order is caller-owned. The orchestrator does not infer model quality,
12
+ // price, or capability from a host name because every host can be configured
13
+ // with different models. The parallel orchestrator supplies richer dimensions:
14
+ // wave state, dependency readiness, and explicit custom rules.
15
15
 
16
16
  const { isSupportedRuntime } = require('../adapters/_shared/runtime-assets.cjs');
17
17
 
18
- const ROUTE_PREFERENCES = Object.freeze({
19
- haiku: ['opencode', 'claude', 'codex'],
20
- sonnet: ['claude', 'codex', 'opencode'],
21
- opus: ['claude', 'codex'],
22
- });
23
-
24
- function resolveByPreference(ctx) {
25
- const hint = ctx.task && ctx.task.complexity_hint;
26
- const pref = hint ? ROUTE_PREFERENCES[hint] : null;
27
- if (pref) {
28
- for (const r of pref) if (ctx.available_runtimes.includes(r)) return r;
29
- }
30
- return ctx.available_runtimes[0];
31
- }
32
-
33
18
  const DEFAULT_RULES = Object.freeze([
34
19
  {
35
20
  id: 'breaker-blocked',
@@ -56,17 +41,7 @@ const DEFAULT_RULES = Object.freeze([
56
41
  action: 'defer',
57
42
  },
58
43
  {
59
- id: 'by-preference',
60
- priority: 10,
61
- when: (ctx) => {
62
- const hint = ctx.task && ctx.task.complexity_hint;
63
- return !!(hint && ROUTE_PREFERENCES[hint]);
64
- },
65
- action: 'spawn_agent',
66
- resolve: resolveByPreference,
67
- },
68
- {
69
- id: 'fallback-first-available',
44
+ id: 'first-authorized-runtime',
70
45
  priority: 0,
71
46
  when: () => true,
72
47
  action: 'spawn_agent',
@@ -100,5 +75,4 @@ function evaluate(ctx, rules = DEFAULT_RULES) {
100
75
  module.exports = {
101
76
  evaluate,
102
77
  DEFAULT_RULES,
103
- ROUTE_PREFERENCES,
104
78
  };
@@ -15,18 +15,22 @@ const DEFAULT_COMPLEXITY = 3;
15
15
  const TOKENS_PER_COMPLEXITY = Object.freeze({ input: 5000, output: 2000 });
16
16
  const DURATION_MIN_PER_COMPLEXITY = 5;
17
17
 
18
- function buildPlan(tasks, { runtime = 'claude', model = null } = {}) {
18
+ function buildPlan(tasks, { runtime = null, model = null, changeId = null } = {}) {
19
19
  if (!Array.isArray(tasks)) {
20
20
  throw new TypeError('buildPlan: tasks must be an array');
21
21
  }
22
22
 
23
- const graph = tasks.map((t) => ({
23
+ // Plan artifacts are durable and compared by digest. Keep their ordering
24
+ // independent from SQLite insertion order or caller enumeration order.
25
+ const orderedTasks = [...tasks].sort((left, right) => String(left.id).localeCompare(String(right.id)));
26
+
27
+ const graph = orderedTasks.map((t) => ({
24
28
  id: t.id,
25
29
  deps: Array.isArray(t.deps) ? t.deps : [],
26
30
  }));
27
31
  const { waves, cycles } = computeWaves(graph);
28
32
 
29
- const byId = new Map(tasks.map((t) => [t.id, t]));
33
+ const byId = new Map(orderedTasks.map((t) => [t.id, t]));
30
34
 
31
35
  const conflict_surface = [];
32
36
  const wavePayload = waves.map((waveTasks, idx) => {
@@ -52,18 +56,21 @@ function buildPlan(tasks, { runtime = 'claude', model = null } = {}) {
52
56
  });
53
57
 
54
58
  const ownership_forecast = {};
55
- for (const t of tasks) {
59
+ for (const t of orderedTasks) {
56
60
  ownership_forecast[t.id] = Array.isArray(t.files_modified) ? t.files_modified : [];
57
61
  }
58
62
 
59
- const estimated_cost_usd = round4(
60
- tasks.reduce((sum, t) => {
61
- const complexity = Number(t.complexity || DEFAULT_COMPLEXITY);
62
- const ti = complexity * TOKENS_PER_COMPLEXITY.input;
63
- const to = complexity * TOKENS_PER_COMPLEXITY.output;
64
- return sum + (computeCost(runtime, model, ti, to) || 0);
65
- }, 0),
66
- );
63
+ const taskCosts = orderedTasks.map((t) => {
64
+ const complexity = Number(t.complexity || DEFAULT_COMPLEXITY);
65
+ const ti = complexity * TOKENS_PER_COMPLEXITY.input;
66
+ const to = complexity * TOKENS_PER_COMPLEXITY.output;
67
+ return computeCost(runtime, model, ti, to);
68
+ });
69
+ const estimated_cost_usd = orderedTasks.length === 0
70
+ ? 0
71
+ : (taskCosts.some((cost) => cost === null)
72
+ ? null
73
+ : round4(taskCosts.reduce((sum, cost) => sum + cost, 0)));
67
74
 
68
75
  const estimated_duration_min = wavePayload.reduce((acc, wv) => {
69
76
  const complexities = wv.tasks.map(
@@ -76,6 +83,8 @@ function buildPlan(tasks, { runtime = 'claude', model = null } = {}) {
76
83
  }, 0);
77
84
 
78
85
  return {
86
+ schema_version: '1.0',
87
+ change_id: changeId,
79
88
  waves: wavePayload,
80
89
  ownership_forecast,
81
90
  conflict_surface,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra-builder-pro-cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
5
5
  "bin": {
6
6
  "ultra-builder-pro-cli": "bin/install.js",
@@ -49,13 +49,20 @@
49
49
  ".ultra-template",
50
50
  "docs/ROADMAP.md",
51
51
  "docs/ARCHITECTURE.md",
52
+ "docs/WORKFLOW-LIFECYCLE.md",
52
53
  "docs/AGENT-CONTEXT.md",
53
54
  "docs/SKILL-AUTHORING.md",
54
55
  "docs/COMMIT-HASH-BACKFILL.md",
55
56
  "docs/PLAN.zh-CN.md",
56
57
  "docs/STATE-DB-ACCESS-POLICY.md",
57
58
  "docs/USER-HANDBOOK-CONTRACT.md",
58
- "docs/RUNTIME-COMPAT-MATRIX.md"
59
+ "docs/RUNTIME-COMPAT-MATRIX.md",
60
+ "!**/__pycache__/**",
61
+ "!**/*.pyc",
62
+ "!**/*.pyo",
63
+ "!**/*.test.cjs",
64
+ "!**/tests/**",
65
+ "!mcp-server/test-support/**"
59
66
  ],
60
67
  "keywords": [
61
68
  "claude-code",
@@ -98,13 +105,14 @@
98
105
  "install:all": "node bin/install.js --all --local"
99
106
  },
100
107
  "dependencies": {
101
- "@anthropic-ai/sdk": "^0.111.0",
102
- "@modelcontextprotocol/sdk": "^1.29.0",
108
+ "@modelcontextprotocol/server": "2.0.0-beta.5",
103
109
  "@vercel/ncc": "0.38.4",
104
110
  "ajv": "^8.17.1",
105
111
  "ajv-formats": "^3.0.1",
106
112
  "better-sqlite3": "^11.5.0",
107
- "js-yaml": "^4.1.0",
108
- "openai": "^6.34.0"
113
+ "js-yaml": "^4.1.0"
114
+ },
115
+ "devDependencies": {
116
+ "@modelcontextprotocol/client": "2.0.0-beta.5"
109
117
  }
110
118
  }
@@ -5,8 +5,8 @@ description: Ask Claude Code for an independent read-only analysis of a bounded
5
5
 
6
6
  # Consult Claude Code read-only
7
7
 
8
- Treat Claude Code as an untrusted advisor. The current host owns scope, evidence,
9
- decisions, edits, and final verification.
8
+ Treat Claude Code as an untrusted advisor. Keep the current host responsible for
9
+ scope, evidence, decisions, edits, and final verification.
10
10
 
11
11
  ## Preconditions
12
12
 
@@ -5,8 +5,8 @@ description: Ask Codex CLI for an independent read-only analysis of a bounded ar
5
5
 
6
6
  # Consult Codex read-only
7
7
 
8
- Treat Codex as an untrusted advisor. The current host owns scope, evidence, edits,
9
- verification, and the final answer.
8
+ Treat Codex as an untrusted advisor. Keep the current host responsible for scope,
9
+ evidence, edits, verification, and the final answer.
10
10
 
11
11
  ## Preconditions
12
12
 
@@ -23,7 +23,6 @@ Create a session directory under `.ultra/collab/`, then run in a read-only sandb
23
23
  SESSION_PATH=".ultra/collab/$(date +%Y%m%d-%H%M%S)-codex"
24
24
  mkdir -p "${SESSION_PATH}"
25
25
  codex exec -s read-only \
26
- -a never \
27
26
  --ephemeral \
28
27
  --ignore-user-config \
29
28
  --ignore-rules \