ultra-builder-pro-cli 0.14.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 (154) 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 +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. 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,7 @@ 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');
33
35
  const doctor = require('./lib/doctor.cjs');
34
36
  const {
35
37
  readProjectBreadcrumb,
@@ -91,12 +93,21 @@ const CHANGE_TOOLS = Object.freeze([
91
93
  'change.archive',
92
94
  ]);
93
95
 
96
+ const WORKFLOW_TOOLS = Object.freeze([
97
+ 'workflow.start',
98
+ 'workflow.get',
99
+ 'workflow.list',
100
+ 'workflow.step',
101
+ 'workflow.complete',
102
+ ]);
103
+
94
104
  const SYSTEM_TOOLS = Object.freeze([
95
105
  'system.doctor',
96
106
  ]);
97
107
 
98
108
  const REGISTERED_TOOLS = Object.freeze([
99
109
  ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
110
+ ...WORKFLOW_TOOLS,
100
111
  ...SYSTEM_TOOLS,
101
112
  ]);
102
113
 
@@ -112,6 +123,7 @@ const MUTATING_TOOLS = new Set([
112
123
  'baseline.start', 'baseline.record', 'baseline.converge',
113
124
  'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
114
125
  'change.learning_propose', 'change.learning_resolve',
126
+ 'workflow.start', 'workflow.step', 'workflow.complete',
115
127
  ]);
116
128
 
117
129
  function loadRegisteredTools() {
@@ -252,10 +264,33 @@ async function dispatchTool(name, input, db, ctx = {}) {
252
264
  return ops.switchTaskTag(db, input.id, input.tag);
253
265
  }
254
266
  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, {});
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
+ }
259
294
  const graph = rows.map((t) => ({
260
295
  id: t.id,
261
296
  deps: Array.isArray(t.deps) ? t.deps : [],
@@ -272,6 +307,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
272
307
  case 'task.parse_prd': {
273
308
  const dryRun = input.dry_run === true;
274
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
+ }
275
315
  const creationAuthority = { change_id: input.change_id };
276
316
  if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
277
317
  const parsed = parser.parsePrd(input.tasks, {
@@ -330,20 +370,34 @@ async function dispatchTool(name, input, db, ctx = {}) {
330
370
  }
331
371
  case 'plan.export': {
332
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
+ }
333
384
  const abs = path.isAbsolute(input.out_path)
334
385
  ? input.out_path
335
386
  : path.join(rootDir, input.out_path);
336
- const tasks = ops.listTasks(db, { tag: input.tag });
387
+ const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
337
388
  if (tasks.length === 0) {
338
389
  const err = new Error('no tasks to plan');
339
390
  err.code = 'NO_TASKS';
340
391
  throw err;
341
392
  }
342
- const plan = planStore.buildPlan(tasks, {});
393
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
343
394
  const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
344
395
  ops.appendEvent(db, {
345
- type: 'plan_approved',
346
- 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
+ },
347
401
  });
348
402
  return { plan_path, wave_count: plan.waves.length };
349
403
  }
@@ -355,6 +409,13 @@ async function dispatchTool(name, input, db, ctx = {}) {
355
409
  err.code = 'NO_PLAN';
356
410
  throw err;
357
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
+ }
358
419
  return { plan: planStore.selectSection(loaded, input.section) };
359
420
  }
360
421
  case 'baseline.start': {
@@ -456,6 +517,44 @@ async function dispatchTool(name, input, db, ctx = {}) {
456
517
  case 'change.archive': {
457
518
  return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
458
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
+ }
459
558
  case 'system.doctor': {
460
559
  return doctor.runDoctor(db, {
461
560
  rootDir: ctx.rootDir || process.cwd(),
@@ -501,7 +600,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
501
600
  { capabilities: { tools: {} } },
502
601
  );
503
602
 
504
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
603
+ server.setRequestHandler('tools/list', async () => ({
505
604
  tools: tools.map((t) => ({
506
605
  name: t.name,
507
606
  description: t.description,
@@ -509,7 +608,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
509
608
  })),
510
609
  }));
511
610
 
512
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
611
+ server.setRequestHandler('tools/call', async (request) => {
513
612
  const { name, arguments: args = {} } = request.params;
514
613
  const toolStart = Date.now();
515
614
  let toolError = null;
@@ -623,6 +722,51 @@ async function main() {
623
722
  process.on('SIGINT', cleanup);
624
723
  }
625
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
+
626
770
  if (require.main === module) {
627
771
  main().catch((err) => {
628
772
  process.stderr.write(`mcp-server fatal: ${err.message}\n`);
@@ -639,10 +783,12 @@ module.exports = {
639
783
  PLAN_TOOLS,
640
784
  BASELINE_TOOLS,
641
785
  CHANGE_TOOLS,
786
+ WORKFLOW_TOOLS,
642
787
  SYSTEM_TOOLS,
643
788
  REGISTERED_TOOLS,
644
789
  STATELESS_TOOLS,
645
790
  MUTATING_TOOLS,
791
+ appendHookLifecycleEvent,
646
792
  readProjectBreadcrumb,
647
793
  renderProjectBreadcrumb,
648
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.14.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,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
  }
@@ -1,84 +1,69 @@
1
1
  ---
2
2
  name: ultra-change
3
- description: Open or resume a bounded continuous change and connect its intent, delta, decisions, context, and executable tasks. Use when maintaining or extending an Ultra project after its baseline exists.
3
+ description: Open or resume one bounded post-baseline change and connect its intent, delta, task plan, fresh context, and convergence evidence. Use when handling daily feature work, fixes, redesigns, or incidents after an Ultra baseline exists.
4
4
  ---
5
5
 
6
- # Open or resume a continuous change
6
+ # Maintain one continuous change
7
7
 
8
- Keep post-delivery work aligned through a delta, implementation, learning, and
9
- convergence loop.
8
+ Use Ultra MCP for lifecycle writes. Keep external memory and code-graph content in
9
+ their providers; Ultra stores references only.
10
10
 
11
- ## Authority boundary
11
+ ## Bind or create
12
12
 
13
- Use Ultra MCP tools for change and task lifecycle writes. Do not write raw SQLite or
14
- mutate generated task and context projections. External memory or code-graph systems
15
- own their payloads; Ultra stores only provider references supplied by the user or host.
13
+ 1. Read `system.doctor`, `baseline.get`, `change.list`, and `change.breadcrumb`.
14
+ 2. Resume the change matching the requested outcome. Require an id when several could
15
+ match; do not create a duplicate packet.
16
+ 3. New ordinary work requires a ready baseline. An incident on unhealthy authority
17
+ requires an explicit `baseline_bypass` reason and approver. Never infer approval
18
+ from urgency.
19
+ 4. Read `references/change-contract.md`. Persist the complete outcome, executable
20
+ acceptance, non-goals, public seams, recovery contract, unresolved decisions,
21
+ documentation impact, profile rationale, material risk flags, and research
22
+ disposition. Do not derive these fields from a title or free-form intent later.
23
+ 5. Select `quick`, `standard`, `major`, or `incident` from current scope and risk.
24
+ Escalate a proposed quick change when its contract carries material risk or research.
25
+ Then call `change.create` or `change.update` and read the normalized record back.
26
+ 6. Resume or start the linked `change` workflow with `workflow.list`/`workflow.start`.
16
27
 
17
- ## Workflow
28
+ ## Record the change workflow
18
29
 
19
- 1. Call `baseline.get`. State integrity failure routes to `ultra-doctor`.
20
- - Create ordinary work only when the baseline is healthy and `ready`.
21
- - Resume a change that was already active when baseline drift appears; record the
22
- condition as a warning and restore baseline readiness before ordinary convergence.
23
- - Create an `incident` on an unhealthy baseline only with an explicit
24
- `baseline_bypass` containing the reason and approver.
25
- - Route every other missing, migrated, incomplete, or stale baseline to `ultra-init`.
26
- 2. Call `change.list` and `change.breadcrumb`.
27
- - Resume the single change that matches the requested outcome.
28
- - Require an explicit id when several changes could match.
29
- - Create a new change only when no existing packet represents the same outcome.
30
- 3. Capture the observable outcome, acceptance, non-goals, affected public seam,
31
- documentation impact, and unresolved decisions.
32
- 4. Classify the change as `quick`, `standard`, `major`, or `incident`. Ask only for a
33
- material product, risk, or recovery decision that repository evidence cannot answer.
34
- 5. Call `change.create` with a stable kebab-case id, or `change.update` when resuming.
35
- For incident break-glass, persist the approved reason in `baseline_bypass`; never
36
- infer approval from urgency.
37
- 6. Write the minimum artifact set required by the classification:
30
+ Use `workflow.step` as evidence becomes durable:
38
31
 
39
- ```text
40
- .ultra/changes/active/<change-id>/
41
- intent.md
42
- delta/ # standard and major
43
- plan.md # standard and major
44
- diagnosis.md # incident
45
- context-manifest.json # generated projection
46
- spec-learning.json # generated projection
47
- verification.md # generated projection
48
- ```
32
+ - `bind-baseline`: current baseline id, revision, branch, and worktree evidence;
33
+ - `classify-change`: kind and rationale;
34
+ - `record-intent`: intent artifact, acceptance, scope, and resolved docs impact;
35
+ - `plan-change`: completed plan workflow and authoritative task ids;
36
+ - `compile-context`: current task-bound `change.context` manifest and digest;
37
+ - `verify-readiness`: ready breadcrumb with one executable next action.
49
38
 
50
- Delta files describe differences from the baseline, not a second copy of it.
51
- Incident diagnosis records the symptom, earliest bad state, falsifiable
52
- hypotheses, evidence, root cause, and recovery boundary.
53
- 7. Create the smallest executable vertical slice with `task.create`. Each task needs
54
- one outcome, bounded ownership, dependencies, a live public seam, an exact
55
- verification command, and documentation impact.
56
- 8. Compile `change.context` for the next role and gate. Include only required
57
- references and the task execution contract. Missing required references, digest or
58
- HEAD drift, an absent public seam or verification command, and unknown
59
- documentation impact are blockers. File count, token estimate, and context-share
60
- thresholds are advisory warnings: narrow reads or split the slice when useful, but
61
- never raise a threshold merely to make the warning disappear or refuse necessary
62
- incident context.
63
- 9. Call `change.breadcrumb` and return its single next action.
39
+ Standard and major changes keep deltas under
40
+ `.ultra/changes/active/<id>/delta/` and an evidence-linked `plan.md`. Deltas describe
41
+ differences from the baseline; they are not a second baseline. Incidents maintain the
42
+ structured diagnosis artifact and recovery evidence.
64
43
 
65
- ## Specification learning
44
+ When `research_disposition.status` is `bounded` or `required`, run `ultra-research`
45
+ with the exact recorded mode and selected steps before planning. Planning is blocked
46
+ until that change-bound research run is complete, current, and selection-equivalent.
47
+ Use `none` only when the Change Contract already has sufficient evidence.
66
48
 
67
- When implementation uncovers a stable requirement, invariant, or public behavior
68
- missing from the baseline, call `change.learning_propose` with evidence and a target
69
- document. The proposal does not edit the baseline. It must be approved or rejected,
70
- and an approved item is marked applied only after the target document is updated and
71
- verified.
49
+ Use `ultra-plan` to create complete DB-backed task contracts. Recompile
50
+ `change.context` after any task, HEAD, specification, or decision change. The task's DB
51
+ contract is authoritative; Prompt input may not override its seam or verification.
52
+ Required missing or stale refs block. Budget warnings guide narrower context but never
53
+ justify dropping necessary evidence or refusing legitimate work.
72
54
 
73
- ## Completion gate
55
+ An ordinary active change remains executable only while its durable `bind-baseline`
56
+ evidence still names the current approved-ready baseline. HEAD, worktree, specification,
57
+ or source-evidence drift created after that binding is advisory until delivery
58
+ reconciliation. Adoption/migration state, missing approval or evidence, blocking gaps,
59
+ and any other structural baseline failure block tasks, context, and later stages. Only
60
+ the recorded incident break-glass authority can bypass that gate.
74
61
 
75
- Do not claim alignment while a material decision is unresolved, documentation impact
76
- is unknown, context is stale, or the next task is not executable. Ordinary change
77
- convergence requires a non-migrated `ready` baseline. Normal HEAD or tracked-spec drift
78
- caused by the active change is reconciled at archive.
62
+ Call `workflow.complete` only after all change workflow steps are current. Return the
63
+ change id, kind, task set, context digest, blockers or warnings, workflow state, and the
64
+ breadcrumb's one route.
79
65
 
80
- An approved break-glass incident may converge and archive while baseline adoption is
81
- incomplete. Its archive must add an open `baseline_blocker` reconciliation gap owned by
82
- the recorded approver. Report the incident as closed and the project baseline as not
83
- ready until that gap is resolved. Every archive declares baseline updates or an exact
84
- no-change reason; failed ordinary reconciliation rolls back state and artifacts.
66
+ When implementation reveals a stable missing invariant, use
67
+ `change.learning_propose` and the approval/reject/apply transition. Unresolved learning
68
+ blocks delivery. An approved application must bind the declared target anchor, its
69
+ before and after digests, and current evidence before the proposal becomes `applied`.
@@ -0,0 +1,36 @@
1
+ # Change Contract and profile
2
+
3
+ Create one contract before planning. Keep every field specific enough to verify and
4
+ stable enough to survive a fresh session.
5
+
6
+ ## Contract
7
+
8
+ - `outcome`: one externally observable result.
9
+ - `acceptance`: stable ids with a criterion and exact verification for each accepted
10
+ behavior.
11
+ - `non_goals`: explicit boundaries that prevent accidental scope growth.
12
+ - `public_seams`: entry points or outputs through which acceptance is observed.
13
+ - `recovery`: a bounded reversal or recovery strategy and its verification.
14
+ - `unresolved_decisions`: stable ids, summary, blocking state, and owner when known.
15
+
16
+ Resolve every blocking decision before planning. Keep non-blocking uncertainty visible
17
+ in the task and verification contracts.
18
+
19
+ ## Profile
20
+
21
+ - `quick`: one low-risk task, no material risk flags, and no research dependency.
22
+ - `standard`: a bounded change with multiple tasks or ordinary integration risk.
23
+ - `major`: a broad or consequential change with material contract, migration,
24
+ authorization, security, multi-module, external-integration, or release impact.
25
+ - `incident`: urgent diagnosis and recovery with a reproducible symptom. Baseline
26
+ break-glass still requires a recorded approver and recovery contract.
27
+
28
+ Record a profile rationale and every applicable risk flag. Select the smallest profile
29
+ whose guarantees cover the real blast radius; never use profile labels as estimates.
30
+
31
+ ## Research disposition
32
+
33
+ Use `none` with a rationale when accepted evidence is sufficient. Use `bounded` or
34
+ `required` with a supported mode and the exact selected semantic steps when product,
35
+ behavior, architecture, deployment, or risk facts remain load-bearing. The recorded
36
+ selection is the planning gate.