ultra-builder-pro-cli 0.14.0 → 0.16.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 (163) 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 +77 -1
  8. package/README.md +46 -24
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/handbook.cjs +2 -1
  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 +3 -2
  16. package/agents/review-coordinator.md +9 -6
  17. package/commands/ultra-change.md +1 -1
  18. package/commands/ultra-plan.md +1 -1
  19. package/commands/ultra-research.md +1 -1
  20. package/commands/ultra-think.md +2 -2
  21. package/docs/AGENT-CONTEXT.md +43 -19
  22. package/docs/ARCHITECTURE.md +54 -13
  23. package/docs/PLAN.zh-CN.md +4 -2
  24. package/docs/ROADMAP.md +10 -4
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  26. package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
  27. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  28. package/docs/WORKFLOW-LIFECYCLE.md +342 -0
  29. package/hooks/README.md +6 -1
  30. package/hooks/health_check.py +12 -3
  31. package/hooks/subagent_tracker.py +52 -77
  32. package/mcp-server/hook-event.cjs +34 -0
  33. package/mcp-server/lib/archive-journal.cjs +17 -5
  34. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  35. package/mcp-server/lib/change-workflow.cjs +598 -85
  36. package/mcp-server/lib/context-spine.cjs +305 -25
  37. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  38. package/mcp-server/lib/doctor.cjs +42 -3
  39. package/mcp-server/lib/init-project.cjs +210 -16
  40. package/mcp-server/lib/plan-store.cjs +4 -1
  41. package/mcp-server/lib/pricing.cjs +7 -10
  42. package/mcp-server/lib/project-breadcrumb.cjs +15 -0
  43. package/mcp-server/lib/projector.cjs +57 -3
  44. package/mcp-server/lib/spec-learning.cjs +79 -2
  45. package/mcp-server/lib/state-db.cjs +168 -8
  46. package/mcp-server/lib/state-ops.cjs +160 -9
  47. package/mcp-server/lib/workflow-state.cjs +2305 -0
  48. package/mcp-server/server.cjs +215 -15
  49. package/orchestrator/daemon.cjs +1 -2
  50. package/orchestrator/dispatch-rules.cjs +5 -31
  51. package/orchestrator/planner/plan-builder.cjs +21 -12
  52. package/package.json +13 -3
  53. package/skills/ultra-change/SKILL.md +94 -80
  54. package/skills/ultra-change/references/change-contract.md +36 -0
  55. package/skills/ultra-deliver/SKILL.md +76 -79
  56. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  57. package/skills/ultra-dev/SKILL.md +62 -59
  58. package/skills/ultra-doctor/SKILL.md +61 -82
  59. package/skills/ultra-init/SKILL.md +101 -103
  60. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  61. package/skills/ultra-plan/SKILL.md +99 -85
  62. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  63. package/skills/ultra-research/SKILL.md +102 -60
  64. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  65. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  66. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  67. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  68. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  69. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  70. package/skills/ultra-research/references/10-user-personas.md +29 -0
  71. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  72. package/skills/ultra-research/references/20-user-stories.md +30 -0
  73. package/skills/ultra-research/references/21-features-scope.md +29 -0
  74. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  75. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  76. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  77. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  78. package/skills/ultra-research/references/40-deployment.md +29 -0
  79. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  80. package/skills/ultra-research/references/99-synthesis.md +35 -0
  81. package/skills/ultra-review/SKILL.md +63 -73
  82. package/skills/ultra-review/references/review-modes.md +27 -0
  83. package/skills/ultra-review/references/unified-schema.md +25 -0
  84. package/skills/ultra-review/scripts/review_wait.py +40 -3
  85. package/skills/ultra-status/SKILL.md +41 -41
  86. package/skills/ultra-test/SKILL.md +54 -55
  87. package/skills/ultra-think/SKILL.md +26 -12
  88. package/skills/ultra-think/references/decision-dialogue.md +130 -0
  89. package/skills/ultra-verify/SKILL.md +1 -1
  90. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  91. package/spec/cli-protocol.md +17 -3
  92. package/spec/mcp-tools.yaml +683 -64
  93. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  94. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  95. package/spec/schemas/mcp-tools.schema.json +1 -1
  96. package/spec/schemas/state-db.sql +140 -1
  97. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  98. package/spec/schemas/test-report.v1.schema.json +194 -0
  99. package/ultra-tools/cli.cjs +1 -1
  100. package/ultra-tools/commands/migrate.cjs +3 -3
  101. package/ultra-tools/commands/status.cjs +228 -12
  102. package/.ultra-template/delivery-report.json +0 -9
  103. package/.ultra-template/test-report.json +0 -8
  104. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  105. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  106. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  107. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  108. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  109. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  110. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  111. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  112. package/adapters/_shared/tests/validate.test.cjs +0 -30
  113. package/adapters/tests/claude.test.cjs +0 -138
  114. package/adapters/tests/codex-hook.test.cjs +0 -142
  115. package/adapters/tests/codex.test.cjs +0 -430
  116. package/adapters/tests/kimi-hook.test.cjs +0 -149
  117. package/adapters/tests/kimi.test.cjs +0 -259
  118. package/adapters/tests/opencode.test.cjs +0 -357
  119. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  120. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  121. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  122. package/mcp-server/lib/context-spine.test.cjs +0 -93
  123. package/mcp-server/lib/doctor.test.cjs +0 -148
  124. package/mcp-server/lib/plan-store.test.cjs +0 -132
  125. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  126. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  127. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  128. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  129. package/mcp-server/lib/state-db.test.cjs +0 -314
  130. package/mcp-server/lib/task-expander.test.cjs +0 -169
  131. package/mcp-server/lib/topo.test.cjs +0 -158
  132. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  133. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  134. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  135. package/mcp-server/tests/concurrency.test.cjs +0 -112
  136. package/mcp-server/tests/db-ops.test.cjs +0 -121
  137. package/mcp-server/tests/init-project.test.cjs +0 -393
  138. package/mcp-server/tests/migrate.test.cjs +0 -232
  139. package/mcp-server/tests/projector.test.cjs +0 -162
  140. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  141. package/mcp-server/tests/staleness.test.cjs +0 -190
  142. package/mcp-server/tests/state-db.test.cjs +0 -106
  143. package/mcp-server/tests/state-ops.test.cjs +0 -449
  144. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  145. package/mcp-server/tests/telemetry.test.cjs +0 -219
  146. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  147. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  148. package/orchestrator/tests/daemon.test.cjs +0 -311
  149. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  150. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  151. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  152. package/orchestrator/tests/recovery.test.cjs +0 -190
  153. package/orchestrator/tests/session-runner.test.cjs +0 -314
  154. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  155. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  156. package/skills/ultra-research/references/architecture.md +0 -38
  157. package/skills/ultra-research/references/problem-product.md +0 -39
  158. package/skills/ultra-research/references/quality-delivery.md +0 -34
  159. package/skills/ultra-research/references/synthesis.md +0 -38
  160. package/skills/ultra-research/references/users-scope.md +0 -40
  161. package/ultra-tools/legacy-memory.test.cjs +0 -80
  162. package/ultra-tools/status-command.test.cjs +0 -181
  163. 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,12 +12,13 @@ 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');
@@ -30,6 +31,8 @@ const { initProject } = require('./lib/init-project.cjs');
30
31
  const baselines = require('./lib/baseline-workflow.cjs');
31
32
  const changes = require('./lib/change-workflow.cjs');
32
33
  const runtimeState = require('./lib/runtime-state.cjs');
34
+ const workflows = require('./lib/workflow-state.cjs');
35
+ const decisions = require('./lib/decision-dialogue.cjs');
33
36
  const doctor = require('./lib/doctor.cjs');
34
37
  const {
35
38
  readProjectBreadcrumb,
@@ -91,12 +94,34 @@ const CHANGE_TOOLS = Object.freeze([
91
94
  'change.archive',
92
95
  ]);
93
96
 
97
+ const WORKFLOW_TOOLS = Object.freeze([
98
+ 'workflow.start',
99
+ 'workflow.get',
100
+ 'workflow.list',
101
+ 'workflow.step',
102
+ 'workflow.complete',
103
+ ]);
104
+
105
+ const DECISION_TOOLS = Object.freeze([
106
+ 'decision.thread_start',
107
+ 'decision.get',
108
+ 'decision.list',
109
+ 'decision.open',
110
+ 'decision.resolve',
111
+ 'decision.delegate',
112
+ 'decision.defer',
113
+ 'decision.supersede',
114
+ 'decision.checkpoint',
115
+ ]);
116
+
94
117
  const SYSTEM_TOOLS = Object.freeze([
95
118
  'system.doctor',
96
119
  ]);
97
120
 
98
121
  const REGISTERED_TOOLS = Object.freeze([
99
122
  ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
123
+ ...DECISION_TOOLS,
124
+ ...WORKFLOW_TOOLS,
100
125
  ...SYSTEM_TOOLS,
101
126
  ]);
102
127
 
@@ -112,6 +137,9 @@ const MUTATING_TOOLS = new Set([
112
137
  'baseline.start', 'baseline.record', 'baseline.converge',
113
138
  'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
114
139
  'change.learning_propose', 'change.learning_resolve',
140
+ 'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
141
+ 'decision.defer', 'decision.supersede', 'decision.checkpoint',
142
+ 'workflow.start', 'workflow.step', 'workflow.complete',
115
143
  ]);
116
144
 
117
145
  function loadRegisteredTools() {
@@ -252,10 +280,33 @@ async function dispatchTool(name, input, db, ctx = {}) {
252
280
  return ops.switchTaskTag(db, input.id, input.tag);
253
281
  }
254
282
  case 'task.dependency_topo': {
255
- const requested = Array.isArray(input && input.task_ids) ? input.task_ids : null;
256
- const rows = requested
257
- ? requested.map((id) => ops.readTask(db, id)).filter(Boolean)
258
- : ops.listTasks(db, {});
283
+ const hasTaskIds = Array.isArray(input && input.task_ids);
284
+ const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
285
+ if (hasTaskIds === hasChangeId) {
286
+ const err = new Error('provide exactly one of task_ids or change_id');
287
+ err.code = 'VALIDATION_ERROR';
288
+ throw err;
289
+ }
290
+ let rows;
291
+ if (hasTaskIds) {
292
+ rows = input.task_ids.map((id) => {
293
+ const task = ops.readTask(db, id);
294
+ if (!task) {
295
+ const err = new Error(`task ${id} not found`);
296
+ err.code = 'TASK_NOT_FOUND';
297
+ throw err;
298
+ }
299
+ return task;
300
+ });
301
+ } else {
302
+ const change = changes.readChange(db, input.change_id);
303
+ if (!change) {
304
+ const err = new Error(`change ${input.change_id} not found`);
305
+ err.code = 'CHANGE_NOT_FOUND';
306
+ throw err;
307
+ }
308
+ rows = ops.listTasks(db, { change_id: change.id });
309
+ }
259
310
  const graph = rows.map((t) => ({
260
311
  id: t.id,
261
312
  deps: Array.isArray(t.deps) ? t.deps : [],
@@ -272,6 +323,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
272
323
  case 'task.parse_prd': {
273
324
  const dryRun = input.dry_run === true;
274
325
  const rootDir = ctx.rootDir || process.cwd();
326
+ if (!dryRun && !String(input.change_id || '').trim()) {
327
+ const err = new Error('persisting a parsed task graph requires change_id');
328
+ err.code = 'CHANGE_REQUIRED';
329
+ throw err;
330
+ }
275
331
  const creationAuthority = { change_id: input.change_id };
276
332
  if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
277
333
  const parsed = parser.parsePrd(input.tasks, {
@@ -330,20 +386,34 @@ async function dispatchTool(name, input, db, ctx = {}) {
330
386
  }
331
387
  case 'plan.export': {
332
388
  const rootDir = ctx.rootDir || process.cwd();
389
+ const change = changes.readChange(db, input.change_id);
390
+ if (!change) {
391
+ const err = new Error(`change ${input.change_id} not found`);
392
+ err.code = 'CHANGE_NOT_FOUND';
393
+ throw err;
394
+ }
395
+ if (!['active', 'blocked'].includes(change.status)) {
396
+ const err = new Error(`change ${change.id} is ${change.status}`);
397
+ err.code = 'CHANGE_NOT_MUTABLE';
398
+ throw err;
399
+ }
333
400
  const abs = path.isAbsolute(input.out_path)
334
401
  ? input.out_path
335
402
  : path.join(rootDir, input.out_path);
336
- const tasks = ops.listTasks(db, { tag: input.tag });
403
+ const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
337
404
  if (tasks.length === 0) {
338
405
  const err = new Error('no tasks to plan');
339
406
  err.code = 'NO_TASKS';
340
407
  throw err;
341
408
  }
342
- const plan = planStore.buildPlan(tasks, {});
409
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
343
410
  const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
344
411
  ops.appendEvent(db, {
345
- type: 'plan_approved',
346
- payload: { plan_path, wave_count: plan.waves.length, tag: input.tag || null },
412
+ type: 'plan_exported',
413
+ change_id: change.id,
414
+ payload: {
415
+ change_id: change.id, plan_path, wave_count: plan.waves.length, tag: input.tag || null,
416
+ },
347
417
  });
348
418
  return { plan_path, wave_count: plan.waves.length };
349
419
  }
@@ -355,6 +425,13 @@ async function dispatchTool(name, input, db, ctx = {}) {
355
425
  err.code = 'NO_PLAN';
356
426
  throw err;
357
427
  }
428
+ if (loaded.change_id !== input.change_id) {
429
+ const err = new Error(
430
+ `execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
431
+ );
432
+ err.code = 'PLAN_CHANGE_MISMATCH';
433
+ throw err;
434
+ }
358
435
  return { plan: planStore.selectSection(loaded, input.section) };
359
436
  }
360
437
  case 'baseline.start': {
@@ -456,6 +533,82 @@ async function dispatchTool(name, input, db, ctx = {}) {
456
533
  case 'change.archive': {
457
534
  return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
458
535
  }
536
+ case 'decision.thread_start': {
537
+ return { thread: decisions.startDecisionThread(db, input) };
538
+ }
539
+ case 'decision.get': {
540
+ const thread = decisions.readDecisionThread(db, input.id);
541
+ if (!thread) {
542
+ const error = new Error(`decision thread ${input.id} not found`);
543
+ error.code = 'DECISION_THREAD_NOT_FOUND';
544
+ throw error;
545
+ }
546
+ return { thread };
547
+ }
548
+ case 'decision.list': {
549
+ const threads = decisions.listDecisionThreads(db, input || {});
550
+ return { threads, count: threads.length };
551
+ }
552
+ case 'decision.open': {
553
+ return { thread: decisions.openDecision(db, input) };
554
+ }
555
+ case 'decision.resolve': {
556
+ return { thread: decisions.resolveDecision(db, input) };
557
+ }
558
+ case 'decision.delegate': {
559
+ return { thread: decisions.delegateDecision(db, input) };
560
+ }
561
+ case 'decision.defer': {
562
+ return { thread: decisions.deferDecision(db, input) };
563
+ }
564
+ case 'decision.supersede': {
565
+ return { thread: decisions.supersedeDecision(db, input) };
566
+ }
567
+ case 'decision.checkpoint': {
568
+ return {
569
+ thread: decisions.checkpointDecisionThread(
570
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
571
+ ),
572
+ };
573
+ }
574
+ case 'workflow.start': {
575
+ return {
576
+ workflow: workflows.startWorkflow(
577
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
578
+ ),
579
+ };
580
+ }
581
+ case 'workflow.get': {
582
+ const workflow = workflows.readWorkflow(
583
+ db, input.id, { rootDir: ctx.rootDir || process.cwd() },
584
+ );
585
+ if (!workflow) {
586
+ const error = new Error(`workflow ${input.id} not found`);
587
+ error.code = 'WORKFLOW_NOT_FOUND';
588
+ throw error;
589
+ }
590
+ return { workflow };
591
+ }
592
+ case 'workflow.list': {
593
+ const rows = workflows.listWorkflows(
594
+ db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
595
+ );
596
+ return { workflows: rows, count: rows.length };
597
+ }
598
+ case 'workflow.step': {
599
+ return {
600
+ workflow: workflows.recordWorkflowStep(
601
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
602
+ ),
603
+ };
604
+ }
605
+ case 'workflow.complete': {
606
+ return {
607
+ workflow: workflows.completeWorkflow(
608
+ db, input, { rootDir: ctx.rootDir || process.cwd() },
609
+ ),
610
+ };
611
+ }
459
612
  case 'system.doctor': {
460
613
  return doctor.runDoctor(db, {
461
614
  rootDir: ctx.rootDir || process.cwd(),
@@ -501,7 +654,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
501
654
  { capabilities: { tools: {} } },
502
655
  );
503
656
 
504
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
657
+ server.setRequestHandler('tools/list', async () => ({
505
658
  tools: tools.map((t) => ({
506
659
  name: t.name,
507
660
  description: t.description,
@@ -509,7 +662,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
509
662
  })),
510
663
  }));
511
664
 
512
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
665
+ server.setRequestHandler('tools/call', async (request) => {
513
666
  const { name, arguments: args = {} } = request.params;
514
667
  const toolStart = Date.now();
515
668
  let toolError = null;
@@ -623,6 +776,51 @@ async function main() {
623
776
  process.on('SIGINT', cleanup);
624
777
  }
625
778
 
779
+ function hookMetadata(value, fallback = 'unknown', maxLength = 256) {
780
+ const normalized = typeof value === 'string' ? value.trim() : '';
781
+ return (normalized || fallback).slice(0, maxLength);
782
+ }
783
+
784
+ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
785
+ if (!['start', 'stop'].includes(action)) {
786
+ const error = new Error('hook lifecycle action must be start or stop');
787
+ error.code = 'VALIDATION_ERROR';
788
+ throw error;
789
+ }
790
+ const root = path.resolve(rootDir || process.cwd());
791
+ const dbPath = path.join(root, '.ultra', 'state.db');
792
+ if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
793
+
794
+ const db = openStateDb(dbPath);
795
+ try {
796
+ ensureSchemaVersion(db);
797
+ const change = db.prepare(
798
+ `SELECT id FROM changes WHERE status IN ('active', 'blocked', 'ready')
799
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
800
+ ).get();
801
+ if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
802
+ const task = db.prepare(
803
+ `SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
804
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
805
+ updated_at DESC, rowid DESC LIMIT 1`,
806
+ ).get(change.id);
807
+ const payload = {
808
+ agent_id: hookMetadata(hookInput.agent_id),
809
+ agent_type: hookMetadata(hookInput.agent_type),
810
+ host_session_id: hookMetadata(hookInput.session_id, '', 256),
811
+ };
812
+ ops.appendEvent(db, {
813
+ type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
814
+ change_id: change.id,
815
+ task_id: task?.id || null,
816
+ payload,
817
+ });
818
+ return { recorded: true, change_id: change.id, task_id: task?.id || null };
819
+ } finally {
820
+ closeStateDb(db);
821
+ }
822
+ }
823
+
626
824
  if (require.main === module) {
627
825
  main().catch((err) => {
628
826
  process.stderr.write(`mcp-server fatal: ${err.message}\n`);
@@ -639,10 +837,12 @@ module.exports = {
639
837
  PLAN_TOOLS,
640
838
  BASELINE_TOOLS,
641
839
  CHANGE_TOOLS,
840
+ WORKFLOW_TOOLS,
642
841
  SYSTEM_TOOLS,
643
842
  REGISTERED_TOOLS,
644
843
  STATELESS_TOOLS,
645
844
  MUTATING_TOOLS,
845
+ appendHookLifecycleEvent,
646
846
  readProjectBreadcrumb,
647
847
  renderProjectBreadcrumb,
648
848
  };
@@ -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.14.0",
3
+ "version": "0.16.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,11 +105,14 @@
98
105
  "install:all": "node bin/install.js --all --local"
99
106
  },
100
107
  "dependencies": {
101
- "@modelcontextprotocol/sdk": "^1.29.0",
108
+ "@modelcontextprotocol/server": "2.0.0-beta.5",
102
109
  "@vercel/ncc": "0.38.4",
103
110
  "ajv": "^8.17.1",
104
111
  "ajv-formats": "^3.0.1",
105
112
  "better-sqlite3": "^11.5.0",
106
113
  "js-yaml": "^4.1.0"
114
+ },
115
+ "devDependencies": {
116
+ "@modelcontextprotocol/client": "2.0.0-beta.5"
107
117
  }
108
118
  }