ultra-builder-pro-cli 0.5.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 (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const { computeWaves } = require('./topo.cjs');
7
+
8
+ function makeTask(id, deps = []) {
9
+ return { id, deps };
10
+ }
11
+
12
+ function idsSet(ids) {
13
+ return new Set(ids);
14
+ }
15
+
16
+ function asWaveSets(waves) {
17
+ return waves.map(idsSet);
18
+ }
19
+
20
+ test('topo: empty array → empty waves and cycles', () => {
21
+ const r = computeWaves([]);
22
+ assert.deepEqual(r.waves, []);
23
+ assert.deepEqual(r.cycles, []);
24
+ });
25
+
26
+ test('topo: single task with no deps → single wave', () => {
27
+ const r = computeWaves([makeTask('A')]);
28
+ assert.deepEqual(r.waves, [['A']]);
29
+ assert.deepEqual(r.cycles, []);
30
+ });
31
+
32
+ test('topo fixture 1: linear chain A, B(A), C(B) → three serial waves', () => {
33
+ const r = computeWaves([
34
+ makeTask('A'),
35
+ makeTask('B', ['A']),
36
+ makeTask('C', ['B']),
37
+ ]);
38
+ assert.deepEqual(asWaveSets(r.waves), [idsSet(['A']), idsSet(['B']), idsSet(['C'])]);
39
+ assert.deepEqual(r.cycles, []);
40
+ });
41
+
42
+ test('topo fixture 2: fan-out A→{B,C} + independent D→E → 2 waves', () => {
43
+ const r = computeWaves([
44
+ makeTask('A'),
45
+ makeTask('B', ['A']),
46
+ makeTask('C', ['A']),
47
+ makeTask('D'),
48
+ makeTask('E', ['D']),
49
+ ]);
50
+ assert.equal(r.waves.length, 2);
51
+ assert.deepEqual(asWaveSets(r.waves), [
52
+ idsSet(['A', 'D']),
53
+ idsSet(['B', 'C', 'E']),
54
+ ]);
55
+ assert.deepEqual(r.cycles, []);
56
+ });
57
+
58
+ test('topo fixture 3: double diamond merges waves correctly', () => {
59
+ const r = computeWaves([
60
+ makeTask('A'),
61
+ makeTask('B', ['A']),
62
+ makeTask('C', ['A']),
63
+ makeTask('D', ['B', 'C']),
64
+ makeTask('E'),
65
+ makeTask('F', ['E']),
66
+ makeTask('G', ['E']),
67
+ makeTask('H', ['F', 'G']),
68
+ ]);
69
+ assert.deepEqual(asWaveSets(r.waves), [
70
+ idsSet(['A', 'E']),
71
+ idsSet(['B', 'C', 'F', 'G']),
72
+ idsSet(['D', 'H']),
73
+ ]);
74
+ assert.deepEqual(r.cycles, []);
75
+ });
76
+
77
+ test('topo fixture 4: 2-node cycle A↔B → cycle reported, no waves', () => {
78
+ const r = computeWaves([
79
+ makeTask('A', ['B']),
80
+ makeTask('B', ['A']),
81
+ ]);
82
+ assert.deepEqual(r.waves, []);
83
+ assert.equal(r.cycles.length, 1);
84
+ assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B']));
85
+ });
86
+
87
+ test('topo fixture 5: two independent cycles A↔B and C↔D → both reported', () => {
88
+ const r = computeWaves([
89
+ makeTask('A', ['B']),
90
+ makeTask('B', ['A']),
91
+ makeTask('C', ['D']),
92
+ makeTask('D', ['C']),
93
+ ]);
94
+ assert.deepEqual(r.waves, []);
95
+ assert.equal(r.cycles.length, 2);
96
+ const cycleSets = r.cycles.map((c) => idsSet(c));
97
+ const expected = [idsSet(['A', 'B']), idsSet(['C', 'D'])];
98
+ for (const exp of expected) {
99
+ const found = cycleSets.some(
100
+ (s) => s.size === exp.size && [...s].every((x) => exp.has(x)),
101
+ );
102
+ assert.ok(found, `cycle ${[...exp].join(',')} not reported`);
103
+ }
104
+ });
105
+
106
+ test('topo: self-loop counts as cycle', () => {
107
+ const r = computeWaves([makeTask('A', ['A'])]);
108
+ assert.deepEqual(r.waves, []);
109
+ assert.equal(r.cycles.length, 1);
110
+ assert.deepEqual(r.cycles[0], ['A']);
111
+ });
112
+
113
+ test('topo: cycle plus acyclic tail — tail is waved, cycle isolated', () => {
114
+ const r = computeWaves([
115
+ makeTask('A', ['B']),
116
+ makeTask('B', ['A']),
117
+ makeTask('X'),
118
+ makeTask('Y', ['X']),
119
+ ]);
120
+ assert.deepEqual(asWaveSets(r.waves), [idsSet(['X']), idsSet(['Y'])]);
121
+ assert.equal(r.cycles.length, 1);
122
+ assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B']));
123
+ });
124
+
125
+ test('topo: external deps (ids not in task_ids) are treated as satisfied', () => {
126
+ const r = computeWaves([makeTask('A', ['X'])]);
127
+ assert.deepEqual(r.waves, [['A']]);
128
+ assert.deepEqual(r.cycles, []);
129
+ });
130
+
131
+ test('topo: input order does not change wave membership', () => {
132
+ const tasks = [
133
+ makeTask('A'),
134
+ makeTask('B', ['A']),
135
+ makeTask('C', ['A']),
136
+ makeTask('D', ['B', 'C']),
137
+ ];
138
+ const r1 = computeWaves(tasks);
139
+ const r2 = computeWaves([...tasks].reverse());
140
+ assert.deepEqual(asWaveSets(r1.waves), asWaveSets(r2.waves));
141
+ assert.deepEqual(r1.cycles, r2.cycles);
142
+ });
143
+
144
+ test('topo: rejects non-array input', () => {
145
+ assert.throws(() => computeWaves(null), TypeError);
146
+ assert.throws(() => computeWaves({}), TypeError);
147
+ });
148
+
149
+ test('topo: 3-node cycle A→B→C→A surfaced intact', () => {
150
+ const r = computeWaves([
151
+ makeTask('A', ['C']),
152
+ makeTask('B', ['A']),
153
+ makeTask('C', ['B']),
154
+ ]);
155
+ assert.deepEqual(r.waves, []);
156
+ assert.equal(r.cycles.length, 1);
157
+ assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B', 'C']));
158
+ });
@@ -0,0 +1,482 @@
1
+ 'use strict';
2
+
3
+ // MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
4
+ // .ultra/state.db. Uses the low-level Server API from
5
+ // @modelcontextprotocol/sdk so we can pass raw JSON Schema (Draft 2020-12)
6
+ // straight from spec/mcp-tools.yaml without translating to zod.
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+
11
+ const yaml = require('js-yaml');
12
+ const Ajv = require('ajv/dist/2020');
13
+ const addFormats = require('ajv-formats');
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');
18
+ const { version: PACKAGE_VERSION } = require('../package.json');
19
+
20
+ const { initStateDb, closeStateDb } = require('./lib/state-db.cjs');
21
+ const ops = require('./lib/state-ops.cjs');
22
+ const projector = require('./lib/projector.cjs');
23
+ const telemetry = require('./lib/telemetry.cjs');
24
+ const topo = require('./lib/topo.cjs');
25
+ const llm = require('./lib/llm-client.cjs');
26
+ const parser = require('./lib/prd-parser.cjs');
27
+ const expander = require('./lib/task-expander.cjs');
28
+ const planStore = require('./lib/plan-store.cjs');
29
+ const { initProject } = require('./lib/init-project.cjs');
30
+
31
+ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
32
+ ? path.resolve(process.env.UBP_RUNTIME_ROOT)
33
+ : path.resolve(__dirname, '..');
34
+ const TOOLS_FILE = path.join(REPO_ROOT, 'spec', 'mcp-tools.yaml');
35
+
36
+ const TASK_TOOLS = Object.freeze([
37
+ 'task.create',
38
+ 'task.update',
39
+ 'task.list',
40
+ 'task.get',
41
+ 'task.delete',
42
+ 'task.init_project',
43
+ 'task.append_event',
44
+ 'task.subscribe_events',
45
+ 'task.switch_tag',
46
+ 'task.dependency_topo',
47
+ 'task.parse_prd',
48
+ 'task.expand',
49
+ ]);
50
+
51
+ const SESSION_TOOLS = Object.freeze([
52
+ 'session.spawn',
53
+ 'session.close',
54
+ 'session.get',
55
+ 'session.list',
56
+ 'session.admission_check',
57
+ 'session.heartbeat',
58
+ 'session.subscribe_events',
59
+ ]);
60
+
61
+ const PLAN_TOOLS = Object.freeze([
62
+ 'plan.export',
63
+ 'plan.get',
64
+ ]);
65
+
66
+ const REGISTERED_TOOLS = Object.freeze([
67
+ ...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS,
68
+ ]);
69
+
70
+ const STATELESS_TOOLS = new Set(['task.init_project']);
71
+
72
+ // init_project mutates the filesystem of an unrelated project, not state.db —
73
+ // do NOT run the projector (it would overwrite the freshly-copied template).
74
+ const MUTATING_TOOLS = new Set([
75
+ 'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
76
+ 'task.parse_prd', 'task.expand',
77
+ 'session.spawn', 'session.close', 'session.heartbeat',
78
+ ]);
79
+
80
+ function loadRegisteredTools() {
81
+ const manifest = yaml.load(fs.readFileSync(TOOLS_FILE, 'utf8'));
82
+ return manifest.tools.filter((t) => REGISTERED_TOOLS.includes(t.name));
83
+ }
84
+
85
+ function buildAjv() {
86
+ const ajv = new Ajv({ allErrors: true, strict: false });
87
+ addFormats(ajv);
88
+ return ajv;
89
+ }
90
+
91
+ function mintSessionId() {
92
+ const { randomUUID } = require('node:crypto');
93
+ return `sess-${randomUUID().slice(0, 8)}`;
94
+ }
95
+
96
+ function resolvePrdText(input, ctx) {
97
+ if (input.prd_text && String(input.prd_text).trim()) return String(input.prd_text);
98
+ if (input.prd_path) {
99
+ const abs = path.isAbsolute(input.prd_path)
100
+ ? input.prd_path
101
+ : path.join(ctx.rootDir || process.cwd(), input.prd_path);
102
+ return fs.readFileSync(abs, 'utf8');
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function buildLlmClient(ctx) {
108
+ if (ctx && ctx.llmClient) return ctx.llmClient;
109
+ return llm.createLlmClient({});
110
+ }
111
+
112
+ async function dispatchTool(name, input, db, ctx = {}) {
113
+ switch (name) {
114
+ case 'task.create': {
115
+ const { randomUUID } = require('node:crypto');
116
+ const id = input.id || `task-${randomUUID()}`;
117
+ const task = ops.createTask(db, { ...input, id });
118
+ return { id: task.id, status: task.status, created_at: task.created_at };
119
+ }
120
+ case 'task.update': {
121
+ const task = ops.patchTask(db, input.id, input.patch || {});
122
+ return { ok: true, task };
123
+ }
124
+ case 'task.list': {
125
+ const tasks = ops.listTasks(db, input || {});
126
+ return { tasks, count: tasks.length };
127
+ }
128
+ case 'task.get': {
129
+ const task = ops.readTask(db, input.id);
130
+ if (!task) {
131
+ const err = new Error(`task ${input.id} not found`);
132
+ err.code = 'TASK_NOT_FOUND';
133
+ throw err;
134
+ }
135
+ return { task };
136
+ }
137
+ case 'task.delete': {
138
+ return ops.deleteTask(db, input.id, { force: !!input.force });
139
+ }
140
+ case 'task.init_project': {
141
+ return initProject(input);
142
+ }
143
+ case 'task.append_event': {
144
+ const r = ops.appendEvent(db, {
145
+ type: input.type,
146
+ task_id: input.task_id,
147
+ session_id: input.session_id,
148
+ runtime: input.runtime,
149
+ payload: input.payload,
150
+ });
151
+ return { event_id: r.event_id, ts: r.ts };
152
+ }
153
+ case 'task.subscribe_events': {
154
+ return ops.subscribeEventsSince(db, input || {});
155
+ }
156
+ case 'session.spawn': {
157
+ // admission + takeover logic; actual git worktree + child process is
158
+ // delegated to orchestrator/session-runner (Phase 4.5.1). This tool
159
+ // records intent in state.db and returns the paths the runner should use.
160
+ const rootDir = ctx.rootDir || process.cwd();
161
+ const verdict = ops.admissionCheck(db, input.task_id);
162
+ if (!verdict.can_spawn && !input.takeover) {
163
+ const err = new Error(`active session exists for task ${input.task_id}; pass takeover=true or choose resume/abandon`);
164
+ err.code = 'ADMISSION_DENIED';
165
+ throw err;
166
+ }
167
+ if (!verdict.can_spawn && input.takeover && verdict.conflict) {
168
+ ops.updateSession(db, verdict.conflict.sid, { status: 'crashed' });
169
+ }
170
+ const sid = mintSessionId();
171
+ const worktreeBase = input.worktree_base || path.join(rootDir, '.ultra', 'worktrees');
172
+ const worktree_path = path.join(worktreeBase, sid);
173
+ const artifact_dir = path.join(rootDir, '.ultra', 'sessions', sid);
174
+ const session = ops.createSession(db, {
175
+ sid,
176
+ task_id: input.task_id,
177
+ runtime: input.runtime,
178
+ worktree_path,
179
+ artifact_dir,
180
+ });
181
+ return {
182
+ sid,
183
+ worktree_path,
184
+ artifact_dir,
185
+ lease_expires_at: session.lease_expires_at,
186
+ };
187
+ }
188
+ case 'session.close': {
189
+ ops.updateSession(db, input.sid, { status: input.status });
190
+ return { ok: true };
191
+ }
192
+ case 'session.get': {
193
+ const session = ops.readSession(db, input.sid);
194
+ if (!session) {
195
+ const err = new Error(`session ${input.sid} not found`);
196
+ err.code = 'SESSION_NOT_FOUND';
197
+ throw err;
198
+ }
199
+ return { session };
200
+ }
201
+ case 'session.admission_check': {
202
+ return ops.admissionCheck(db, input.task_id);
203
+ }
204
+ case 'session.list': {
205
+ const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
206
+ const status = input.status || 'running';
207
+ const filtered = status === 'running'
208
+ ? sessions
209
+ : db.prepare(
210
+ "SELECT * FROM sessions WHERE status = ? AND (? IS NULL OR task_id = ?) ORDER BY started_at ASC LIMIT ?",
211
+ ).all(status, input.task_id || null, input.task_id || null, Math.min(input.limit || 100, 500));
212
+ const limit = Math.min(input.limit || 100, 500);
213
+ const trimmed = filtered.slice(0, limit);
214
+ return { sessions: trimmed, count: trimmed.length };
215
+ }
216
+ case 'session.heartbeat': {
217
+ return ops.heartbeatSession(db, input.sid);
218
+ }
219
+ case 'session.subscribe_events': {
220
+ return ops.subscribeEventsSince(db, {
221
+ since_id: input.since_id,
222
+ session_id: input.sid,
223
+ limit: input.limit,
224
+ });
225
+ }
226
+ case 'task.switch_tag': {
227
+ return ops.switchTaskTag(db, input.id, input.tag);
228
+ }
229
+ case 'task.dependency_topo': {
230
+ const requested = Array.isArray(input && input.task_ids) ? input.task_ids : null;
231
+ const rows = requested
232
+ ? requested.map((id) => ops.readTask(db, id)).filter(Boolean)
233
+ : ops.listTasks(db, {});
234
+ const graph = rows.map((t) => ({
235
+ id: t.id,
236
+ deps: Array.isArray(t.deps) ? t.deps : [],
237
+ }));
238
+ const result = topo.computeWaves(graph);
239
+ if (result.cycles.length > 0) {
240
+ const err = new Error(`dependency graph has ${result.cycles.length} cycle(s)`);
241
+ err.code = 'CYCLE_DETECTED';
242
+ err.details = { cycles: result.cycles };
243
+ throw err;
244
+ }
245
+ return { waves: result.waves };
246
+ }
247
+ case 'task.parse_prd': {
248
+ const prdText = resolvePrdText(input, ctx);
249
+ if (!prdText || !prdText.trim()) {
250
+ const err = new Error('one of prd_path or prd_text required');
251
+ err.code = 'NO_INPUT';
252
+ throw err;
253
+ }
254
+ const client = buildLlmClient(ctx);
255
+ const parsed = await parser.parsePrd(prdText, { llmClient: client, tag: input.tag });
256
+ const shaped = parsed.tasks.map((t) => ({
257
+ id: t.id, title: t.title, type: t.type, priority: t.priority,
258
+ complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
259
+ tag: t.tag,
260
+ }));
261
+ const dryRun = input.dry_run === true;
262
+ if (!dryRun) {
263
+ try {
264
+ ops.tx(db, () => {
265
+ for (const t of shaped) ops.createTask(db, t);
266
+ });
267
+ } catch (err) {
268
+ const wrap = new Error(`failed to persist parsed tasks: ${err.message}`);
269
+ wrap.code = 'PARSE_FAILED';
270
+ wrap.cause = err;
271
+ throw wrap;
272
+ }
273
+ }
274
+ const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
275
+ const topoResult = topo.computeWaves(graph);
276
+ return { tasks: shaped, topo: topoResult.waves };
277
+ }
278
+ case 'task.expand': {
279
+ // Cheap guards before paying for LLM client construction (which would
280
+ // throw NO_LLM_CREDENTIALS when env keys are missing).
281
+ const parent = ops.readTask(db, input.id);
282
+ if (!parent) {
283
+ const err = new Error(`no task ${input.id}`);
284
+ err.code = 'TASK_NOT_FOUND';
285
+ throw err;
286
+ }
287
+ if (parent.status === 'expanded') {
288
+ const err = new Error(`task ${input.id} is already expanded`);
289
+ err.code = 'ALREADY_EXPANDED';
290
+ throw err;
291
+ }
292
+ const client = buildLlmClient(ctx);
293
+ const result = await expander.expandTask(db, {
294
+ id: input.id,
295
+ sub_count: input.sub_count,
296
+ strategy: input.strategy,
297
+ llmClient: client,
298
+ });
299
+ return { parent_id: result.parent_id, children: result.children };
300
+ }
301
+ case 'plan.export': {
302
+ const rootDir = ctx.rootDir || process.cwd();
303
+ const abs = path.isAbsolute(input.out_path)
304
+ ? input.out_path
305
+ : path.join(rootDir, input.out_path);
306
+ const tasks = ops.listTasks(db, { tag: input.tag });
307
+ if (tasks.length === 0) {
308
+ const err = new Error('no tasks to plan');
309
+ err.code = 'NO_TASKS';
310
+ throw err;
311
+ }
312
+ const plan = planStore.buildPlan(tasks, {});
313
+ const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
314
+ ops.appendEvent(db, {
315
+ type: 'plan_approved',
316
+ payload: { plan_path, wave_count: plan.waves.length, tag: input.tag || null },
317
+ });
318
+ return { plan_path, wave_count: plan.waves.length };
319
+ }
320
+ case 'plan.get': {
321
+ const rootDir = ctx.rootDir || process.cwd();
322
+ const loaded = planStore.loadPlanArtifact(rootDir);
323
+ if (!loaded) {
324
+ const err = new Error('no plan has been computed yet');
325
+ err.code = 'NO_PLAN';
326
+ throw err;
327
+ }
328
+ return { plan: planStore.selectSection(loaded, input.section) };
329
+ }
330
+ default:
331
+ throw new Error(`unhandled tool ${name}`);
332
+ }
333
+ }
334
+
335
+ function errorResponse(code, message, retriable = false, details = undefined) {
336
+ const error = { code, message, retriable };
337
+ if (details !== undefined) error.details = details;
338
+ return {
339
+ isError: true,
340
+ content: [{
341
+ type: 'text',
342
+ text: JSON.stringify({ ok: false, error }),
343
+ }],
344
+ };
345
+ }
346
+
347
+ function startServer({ dbPath, rootDir, projectOnWrite = true }) {
348
+ let db = null;
349
+ const getDb = () => {
350
+ if (!db) db = initStateDb(dbPath).db;
351
+ return db;
352
+ };
353
+
354
+ const tools = loadRegisteredTools();
355
+ const ajv = buildAjv();
356
+ const inputValidators = new Map();
357
+ const outputValidators = new Map();
358
+ for (const t of tools) {
359
+ inputValidators.set(t.name, ajv.compile(t.input_schema));
360
+ outputValidators.set(t.name, ajv.compile(t.output_schema));
361
+ }
362
+
363
+ const server = new Server(
364
+ { name: 'ultra-builder-pro-mcp', version: PACKAGE_VERSION },
365
+ { capabilities: { tools: {} } },
366
+ );
367
+
368
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
369
+ tools: tools.map((t) => ({
370
+ name: t.name,
371
+ description: t.description,
372
+ inputSchema: t.input_schema,
373
+ })),
374
+ }));
375
+
376
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
377
+ const { name, arguments: args = {} } = request.params;
378
+ const toolStart = Date.now();
379
+ let toolError = null;
380
+
381
+ const emitTelemetry = () => {
382
+ if (!db) return;
383
+ try {
384
+ telemetry.appendTelemetry(db, {
385
+ event_type: 'tool_call',
386
+ tool_name: name,
387
+ session_id: (args && args.sid) || null,
388
+ rootDir,
389
+ payload: {
390
+ duration_ms: Date.now() - toolStart,
391
+ task_id: (args && (args.task_id || args.id)) || null,
392
+ error: toolError,
393
+ },
394
+ });
395
+ } catch (err) {
396
+ process.stderr.write(`telemetry warning: ${err.message}\n`);
397
+ }
398
+ };
399
+
400
+ if (!REGISTERED_TOOLS.includes(name)) {
401
+ toolError = 'UNKNOWN_TOOL';
402
+ emitTelemetry();
403
+ return errorResponse('UNKNOWN_TOOL', `tool ${name} is not registered on this server`);
404
+ }
405
+ const validateInput = inputValidators.get(name);
406
+ if (!validateInput(args)) {
407
+ toolError = 'VALIDATION_ERROR';
408
+ emitTelemetry();
409
+ return errorResponse('VALIDATION_ERROR', ajv.errorsText(validateInput.errors));
410
+ }
411
+
412
+ let result;
413
+ let toolDb = null;
414
+ try {
415
+ toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
416
+ result = await dispatchTool(name, args, toolDb, { rootDir });
417
+ } catch (err) {
418
+ const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
419
+ toolError = code;
420
+ emitTelemetry();
421
+ return errorResponse(code, err.message, !!err.retriable, err.details);
422
+ }
423
+
424
+ const validateOutput = outputValidators.get(name);
425
+ if (!validateOutput(result)) {
426
+ toolError = 'OUTPUT_SCHEMA_DRIFT';
427
+ emitTelemetry();
428
+ return errorResponse('OUTPUT_SCHEMA_DRIFT', ajv.errorsText(validateOutput.errors));
429
+ }
430
+
431
+ if (toolDb && projectOnWrite && MUTATING_TOOLS.has(name)) {
432
+ try { projector.projectAll(toolDb, { rootDir }); }
433
+ catch (err) { process.stderr.write(`projector warning: ${err.message}\n`); }
434
+ }
435
+
436
+ emitTelemetry();
437
+ return {
438
+ content: [{ type: 'text', text: JSON.stringify(result) }],
439
+ structuredContent: result,
440
+ };
441
+ });
442
+
443
+ return {
444
+ server,
445
+ get db() { return db; },
446
+ tools,
447
+ async close() { closeStateDb(db); },
448
+ };
449
+ }
450
+
451
+ async function main() {
452
+ const dbPath = process.env.UBP_DB_PATH
453
+ ? path.resolve(process.env.UBP_DB_PATH)
454
+ : path.resolve('.ultra', 'state.db');
455
+ const rootDir = process.env.UBP_ROOT_DIR
456
+ ? path.resolve(process.env.UBP_ROOT_DIR)
457
+ : path.resolve('.');
458
+ const handle = startServer({ dbPath, rootDir });
459
+ const transport = new StdioServerTransport();
460
+ await handle.server.connect(transport);
461
+ const cleanup = () => handle.close().finally(() => process.exit(0));
462
+ process.on('SIGTERM', cleanup);
463
+ process.on('SIGINT', cleanup);
464
+ }
465
+
466
+ if (require.main === module) {
467
+ main().catch((err) => {
468
+ process.stderr.write(`mcp-server fatal: ${err.message}\n`);
469
+ process.exit(1);
470
+ });
471
+ }
472
+
473
+ module.exports = {
474
+ main,
475
+ startServer,
476
+ dispatchTool,
477
+ TASK_TOOLS,
478
+ SESSION_TOOLS,
479
+ REGISTERED_TOOLS,
480
+ STATELESS_TOOLS,
481
+ MUTATING_TOOLS,
482
+ };