ultra-builder-pro-cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -14,11 +14,12 @@
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
16
 
17
- const { initStateDb } = require('./state-db.cjs');
17
+ const { EXPECTED_VERSION, initStateDb } = require('./state-db.cjs');
18
18
  const { assertStateAuthority, inspectProjection } = require('./state-authority.cjs');
19
19
  const baselines = require('./baseline-workflow.cjs');
20
20
  const runtimeState = require('./runtime-state.cjs');
21
21
  const ops = require('./state-ops.cjs');
22
+ const workflows = require('./workflow-state.cjs');
22
23
 
23
24
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
24
25
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
@@ -137,13 +138,12 @@ const MANIFEST_NAMES = new Set([
137
138
  'pom.xml', 'build.gradle', 'build.gradle.kts', 'Gemfile', 'composer.json',
138
139
  ]);
139
140
  const SOURCE_EXTENSIONS = new Set([
140
- '.c', '.cc', '.cpp', '.cs', '.go', '.java', '.js', '.jsx', '.kt', '.kts', '.mjs',
141
- '.move', '.php', '.py', '.rb', '.rs', '.sol', '.swift', '.ts', '.tsx', '.vue',
141
+ '.astro', '.c', '.cc', '.cpp', '.cs', '.css', '.dart', '.ex', '.exs', '.fs', '.fsx',
142
+ '.go', '.graphql', '.gql', '.hcl', '.html', '.java', '.js', '.jsx', '.kt', '.kts',
143
+ '.less', '.lua', '.mjs', '.move', '.php', '.proto', '.py', '.r', '.R', '.rb', '.rs',
144
+ '.sass', '.scala', '.scss', '.sh', '.sol', '.svelte', '.swift', '.tf', '.ts', '.tsx',
145
+ '.vue',
142
146
  ]);
143
- const SOURCE_DIR_SEGMENTS = new Set([
144
- 'src', 'app', 'apps', 'lib', 'server', 'backend', 'frontend', 'api', 'cmd', 'internal',
145
- ]);
146
-
147
147
  function scanRepositoryFiles(rootDir, { maxFiles = 5000, maxDepth = 8 } = {}) {
148
148
  const files = [];
149
149
  function walk(dir, depth) {
@@ -198,6 +198,122 @@ function packageProfile(rootDir) {
198
198
  }
199
199
  }
200
200
 
201
+ const JAVASCRIPT_TECHNOLOGIES = Object.freeze([
202
+ { packages: ['next'], name: 'Next.js', capability: 'frontend' },
203
+ { packages: ['react'], name: 'React', capability: 'frontend' },
204
+ { packages: ['vue', 'nuxt'], name: 'Vue', capability: 'frontend' },
205
+ { packages: ['svelte', '@sveltejs/kit'], name: 'Svelte', capability: 'frontend' },
206
+ { packages: ['@angular/core'], name: 'Angular', capability: 'frontend' },
207
+ { packages: ['express'], name: 'Express', capability: 'backend' },
208
+ { packages: ['fastify'], name: 'Fastify', capability: 'backend' },
209
+ { packages: ['@nestjs/core'], name: 'NestJS', capability: 'backend' },
210
+ { packages: ['hono'], name: 'Hono', capability: 'backend' },
211
+ { packages: ['koa'], name: 'Koa', capability: 'backend' },
212
+ { packages: ['commander', 'yargs', 'oclif'], name: 'Node CLI', capability: 'cli' },
213
+ { packages: ['@prisma/client', 'prisma'], name: 'Prisma' },
214
+ { packages: ['typescript'], name: 'TypeScript' },
215
+ { packages: ['vitest'], name: 'Vitest' },
216
+ { packages: ['jest'], name: 'Jest' },
217
+ { packages: ['playwright', '@playwright/test'], name: 'Playwright' },
218
+ ]);
219
+
220
+ function readTextBounded(file, maxBytes = 256 * 1024) {
221
+ try {
222
+ const stat = fs.statSync(file);
223
+ if (!stat.isFile() || stat.size > maxBytes) return '';
224
+ return fs.readFileSync(file, 'utf8');
225
+ } catch {
226
+ return '';
227
+ }
228
+ }
229
+
230
+ function detectTechnologyProfile(rootDir, files) {
231
+ const technologies = new Set();
232
+ const capabilities = new Set();
233
+ const packageFiles = files.filter((file) => path.basename(file) === 'package.json').slice(0, 100);
234
+ for (const relative of packageFiles) {
235
+ let pkg;
236
+ try { pkg = JSON.parse(readTextBounded(path.join(rootDir, relative))); }
237
+ catch { continue; }
238
+ technologies.add('Node.js');
239
+ const dependencies = new Set([
240
+ ...Object.keys(pkg.dependencies || {}),
241
+ ...Object.keys(pkg.devDependencies || {}),
242
+ ...Object.keys(pkg.peerDependencies || {}),
243
+ ]);
244
+ if (pkg.bin && (typeof pkg.bin === 'string' || typeof pkg.bin === 'object')) {
245
+ capabilities.add('cli');
246
+ }
247
+ for (const technology of JAVASCRIPT_TECHNOLOGIES) {
248
+ if (!technology.packages.some((name) => dependencies.has(name))) continue;
249
+ technologies.add(technology.name);
250
+ if (technology.capability) capabilities.add(technology.capability);
251
+ }
252
+ }
253
+
254
+ if (files.some((file) => file === 'tsconfig.json' || /\.(ts|tsx)$/.test(file))) {
255
+ technologies.add('TypeScript');
256
+ }
257
+ if (files.some((file) => /(^|\/)index\.html$/.test(file))) {
258
+ technologies.add('HTML');
259
+ capabilities.add('frontend');
260
+ }
261
+ if (files.some((file) => /\.(css|scss|sass|less)$/.test(file))) technologies.add('CSS');
262
+
263
+ const pythonFiles = files.filter((file) => (
264
+ ['pyproject.toml', 'requirements.txt'].includes(path.basename(file)) || file.endsWith('.py')
265
+ ));
266
+ if (pythonFiles.length > 0) {
267
+ technologies.add('Python');
268
+ const declarations = pythonFiles
269
+ .filter((file) => ['pyproject.toml', 'requirements.txt'].includes(path.basename(file)))
270
+ .map((file) => readTextBounded(path.join(rootDir, file)).toLowerCase())
271
+ .join('\n');
272
+ for (const [needle, label, capability] of [
273
+ ['fastapi', 'FastAPI', 'backend'], ['django', 'Django', 'backend'],
274
+ ['flask', 'Flask', 'backend'], ['typer', 'Typer', 'cli'], ['click', 'Click', 'cli'],
275
+ ['pytest', 'Pytest', null],
276
+ ]) {
277
+ if (!declarations.includes(needle)) continue;
278
+ technologies.add(label);
279
+ if (capability) capabilities.add(capability);
280
+ }
281
+ }
282
+ if (files.some((file) => ['go.mod', 'go.work'].includes(path.basename(file)) || file.endsWith('.go'))) {
283
+ technologies.add('Go');
284
+ }
285
+ if (files.some((file) => path.basename(file) === 'Cargo.toml' || file.endsWith('.rs'))) {
286
+ technologies.add('Rust');
287
+ const cargo = files.filter((file) => path.basename(file) === 'Cargo.toml')
288
+ .map((file) => readTextBounded(path.join(rootDir, file)).toLowerCase()).join('\n');
289
+ if (cargo.includes('clap')) {
290
+ technologies.add('Clap');
291
+ capabilities.add('cli');
292
+ }
293
+ }
294
+ if (files.some((file) => ['pom.xml', 'build.gradle', 'build.gradle.kts'].includes(path.basename(file)))) {
295
+ technologies.add('JVM');
296
+ }
297
+ if (files.some((file) => path.basename(file) === 'Gemfile' || file.endsWith('.rb'))) {
298
+ technologies.add('Ruby');
299
+ }
300
+ if (files.some((file) => path.basename(file) === 'composer.json' || file.endsWith('.php'))) {
301
+ technologies.add('PHP');
302
+ }
303
+
304
+ let projectType = 'other';
305
+ if (capabilities.has('frontend') && capabilities.has('backend')) projectType = 'fullstack';
306
+ else if (capabilities.has('frontend')) projectType = 'web';
307
+ else if (capabilities.has('backend')) projectType = 'api';
308
+ else if (capabilities.has('cli')) projectType = 'cli';
309
+ return {
310
+ detected_project_type: projectType,
311
+ detected_stack: technologies.size > 0 ? [...technologies].join(', ') : null,
312
+ technology_signals: [...technologies],
313
+ capability_signals: [...capabilities].sort(),
314
+ };
315
+ }
316
+
201
317
  function classifyRepository(absTarget) {
202
318
  const { files, truncated } = scanRepositoryFiles(absTarget);
203
319
  const packageData = packageProfile(absTarget);
@@ -220,12 +336,10 @@ function classifyRepository(absTarget) {
220
336
  }).slice(0, 100);
221
337
  const sourceSignals = files.filter((file) => {
222
338
  const lower = file.toLowerCase();
223
- if (testSignals.includes(file) || lower.endsWith('.config.js') || lower.endsWith('.config.ts')) return false;
224
- const segments = file.split('/');
339
+ if (testSignals.includes(file) || /(^|\/)[^/]+\.config\.(c|m)?(j|t)s$/.test(lower)) return false;
225
340
  const ext = path.extname(file);
226
341
  if (!SOURCE_EXTENSIONS.has(ext)) return false;
227
- return segments.some((segment) => SOURCE_DIR_SEGMENTS.has(segment))
228
- || /(^|\/)(main|index|server|app)\.[^.]+$/.test(lower);
342
+ return true;
229
343
  }).slice(0, 200);
230
344
  const workspaceRoots = [...new Set(packageData.workspacePatterns
231
345
  .flatMap((pattern) => expandWorkspacePattern(absTarget, pattern)))]
@@ -247,12 +361,15 @@ function classifyRepository(absTarget) {
247
361
  if (deploymentSignals.length > 0) reasons.push('DEPLOYMENT_PRESENT');
248
362
  if (databaseSignals.length > 0) reasons.push('PERSISTED_STATE_PRESENT');
249
363
  if (workspaceMarkers.length > 0 || workspaceRoots.length > 0) reasons.push('MONOREPO_PRESENT');
364
+ if (truncated) reasons.push('SCAN_TRUNCATED');
250
365
  if (reasons.length === 0) reasons.push('SKELETON_ONLY');
251
366
  const mode = sourceSignals.length > 0 || testSignals.length > 0
252
- || deploymentSignals.length > 0 || databaseSignals.length > 0
367
+ || deploymentSignals.length > 0 || databaseSignals.length > 0 || truncated
253
368
  ? 'brownfield' : 'greenfield';
369
+ const technologyProfile = detectTechnologyProfile(absTarget, files);
254
370
  return {
255
371
  mode,
372
+ ...technologyProfile,
256
373
  reasons,
257
374
  repository_kind: workspaceMarkers.length > 0 || workspaceRoots.length > 0 ? 'monorepo' : 'single',
258
375
  workspace_markers: workspaceMarkers,
@@ -290,7 +407,7 @@ function rollbackInitialization(ultraDir, backupPath) {
290
407
  }
291
408
 
292
409
  function projectionMigrationError(projection) {
293
- const targets = { '4.4': '4.5', '4.5': '12.0' };
410
+ const targets = { '4.4': '4.5', '4.5': EXPECTED_VERSION };
294
411
  const target = targets[projection.version];
295
412
  if (target) {
296
413
  return new InitProjectError(
@@ -307,6 +424,67 @@ function projectionMigrationError(projection) {
307
424
  );
308
425
  }
309
426
 
427
+ function ensureInitializationWorkflows(db, baseline, { rootDir }) {
428
+ let initRun = workflows.listWorkflows(
429
+ db, { kind: 'init', baseline_id: baseline.id, limit: 1 }, { rootDir },
430
+ )[0];
431
+ let researchRun = workflows.listWorkflows(
432
+ db, { kind: 'research', baseline_id: baseline.id, limit: 1 }, { rootDir },
433
+ )[0];
434
+ if (baseline.status === 'ready') {
435
+ const compatibilityOnly = baseline.mode === 'migrated';
436
+ const provenanceComplete = !compatibilityOnly
437
+ && initRun?.status === 'completed'
438
+ && researchRun?.status === 'completed'
439
+ && baseline.research_run_id === researchRun.id;
440
+ return {
441
+ init_id: initRun?.id || null,
442
+ init_status: initRun?.status || 'missing',
443
+ research_id: researchRun?.id || null,
444
+ research_mode: researchRun?.mode || null,
445
+ research_status: compatibilityOnly
446
+ ? 'not_required' : (researchRun?.status || 'missing'),
447
+ provenance_status: compatibilityOnly
448
+ ? 'compatibility_only' : (provenanceComplete ? 'complete' : 'incomplete'),
449
+ next_action: compatibilityOnly
450
+ ? 'ultra-init' : (provenanceComplete ? 'ultra-change' : 'ultra-doctor'),
451
+ };
452
+ }
453
+ if (!initRun) {
454
+ initRun = workflows.startWorkflow(db, {
455
+ kind: 'init', baseline_id: baseline.id,
456
+ subject: `Establish the ${baseline.mode} baseline for ${baseline.project_name}.`,
457
+ metadata: { repository_revision: baseline.repository_revision, scope: baseline.scope },
458
+ }, { rootDir });
459
+ for (const stepId of ['inspect-authority', 'classify-repository', 'scaffold-authority']) {
460
+ initRun = workflows.recordWorkflowStep(db, {
461
+ id: initRun.id, step_id: stepId, status: 'completed',
462
+ }, { rootDir });
463
+ }
464
+ }
465
+
466
+ if (baseline.mode !== 'migrated') {
467
+ if (!researchRun) {
468
+ const researchMode = baseline.mode === 'brownfield' ? 'adoption' : 'full';
469
+ researchRun = workflows.startWorkflow(db, {
470
+ kind: 'research', mode: researchMode, baseline_id: baseline.id,
471
+ subject: baseline.mode === 'brownfield'
472
+ ? 'Adopt the existing system as an evidence-backed product and architecture baseline.'
473
+ : 'Validate the complete product and architecture baseline before planning.',
474
+ }, { rootDir });
475
+ }
476
+ }
477
+ return {
478
+ init_id: initRun.id,
479
+ init_status: initRun.status,
480
+ research_id: researchRun?.id || null,
481
+ research_mode: researchRun?.mode || null,
482
+ research_status: researchRun?.status || 'not_started',
483
+ provenance_status: 'in_progress',
484
+ next_action: baseline.mode === 'migrated' ? 'ultra-init' : 'ultra-research',
485
+ };
486
+ }
487
+
310
488
  function resumeProject({
311
489
  absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
312
490
  scope, repositoryProfile, projection,
@@ -316,6 +494,7 @@ function resumeProject({
316
494
  let initialized;
317
495
  let copiedFiles = [];
318
496
  let baselineResult;
497
+ let workflowResult;
319
498
  try {
320
499
  initialized = initStateDb(stateDbPath);
321
500
  const { db } = initialized;
@@ -331,6 +510,7 @@ function resumeProject({
331
510
  classification: repositoryProfile,
332
511
  }, { rootDir: absTarget });
333
512
  }
513
+ workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
334
514
  ops.appendEvent(db, {
335
515
  type: 'project_resumed',
336
516
  payload: {
@@ -367,16 +547,19 @@ function resumeProject({
367
547
  created_path: ultraDir,
368
548
  state_db_path: stateDbPath,
369
549
  status: 'resumed',
370
- mode: resolvedMode,
550
+ mode: baselineResult.mode,
371
551
  baseline: {
372
552
  id: baselineResult.id,
373
553
  mode: baselineResult.mode,
374
554
  status: baselineResult.status,
555
+ project_type: baselineResult.project_type,
556
+ stack: baselineResult.stack,
375
557
  repository_revision: baselineResult.repository_revision,
376
558
  scope: baselineResult.scope,
377
559
  },
378
560
  copied_files: copiedFiles,
379
561
  repository_profile: repositoryProfile,
562
+ workflow: workflowResult,
380
563
  };
381
564
  if (initialized.backup_path) result.migration_backup_path = initialized.backup_path;
382
565
  return result;
@@ -404,8 +587,12 @@ function initProject({
404
587
  ensureTargetDir(absTarget);
405
588
  const repositoryProfile = classifyRepository(absTarget);
406
589
  const resolvedMode = mode === 'auto' ? repositoryProfile.mode : detectProjectMode(absTarget, mode);
590
+ const resolvedProjectType = project_type || repositoryProfile.detected_project_type;
591
+ const resolvedStack = stack || repositoryProfile.detected_stack || undefined;
407
592
  repositoryProfile.detected_mode = repositoryProfile.mode;
408
593
  repositoryProfile.mode = resolvedMode;
594
+ repositoryProfile.selected_project_type = resolvedProjectType;
595
+ repositoryProfile.selected_stack = resolvedStack || null;
409
596
  const source = ensureTemplate(source_template);
410
597
  const ultraDir = path.join(absTarget, '.ultra');
411
598
 
@@ -422,7 +609,8 @@ function initProject({
422
609
  throw projectionMigrationError(projection);
423
610
  }
424
611
  return resumeProject({
425
- absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
612
+ absTarget, ultraDir, source, project_name, project_type: resolvedProjectType,
613
+ stack: resolvedStack, resolvedMode,
426
614
  scope, repositoryProfile, projection,
427
615
  });
428
616
  }
@@ -441,6 +629,7 @@ function initProject({
441
629
 
442
630
  let copiedFiles;
443
631
  let baselineResult;
632
+ let workflowResult;
444
633
  try {
445
634
  fs.mkdirSync(ultraDir, { recursive: true });
446
635
  copiedFiles = copyTemplate(source, ultraDir);
@@ -449,11 +638,13 @@ function initProject({
449
638
  const { db } = initStateDb(stateDbPath);
450
639
  try {
451
640
  baselineResult = baselines.startBaseline(db, {
452
- id: 'project-baseline', project_name, project_type, stack,
641
+ id: 'project-baseline', project_name, project_type: resolvedProjectType,
642
+ stack: resolvedStack,
453
643
  mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
454
644
  scope: scope === undefined ? ['.'] : scope,
455
645
  classification: repositoryProfile,
456
646
  }, { rootDir: absTarget });
647
+ workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
457
648
  ops.appendEvent(db, {
458
649
  type: 'project_initialized',
459
650
  payload: {
@@ -491,11 +682,14 @@ function initProject({
491
682
  id: baselineResult.id,
492
683
  mode: baselineResult.mode,
493
684
  status: baselineResult.status,
685
+ project_type: baselineResult.project_type,
686
+ stack: baselineResult.stack,
494
687
  repository_revision: baselineResult.repository_revision,
495
688
  scope: baselineResult.scope,
496
689
  },
497
690
  copied_files: copiedFiles,
498
691
  repository_profile: repositoryProfile,
692
+ workflow: workflowResult,
499
693
  };
500
694
  if (backupPath) result.backup_path = backupPath;
501
695
  return result;
@@ -18,8 +18,11 @@ function renderPlanMd(plan) {
18
18
  const lines = [];
19
19
  lines.push('# Execution Plan');
20
20
  lines.push('');
21
+ if (plan.change_id) lines.push(`- Change: ${plan.change_id}`);
21
22
  lines.push(`- Waves: ${plan.waves.length}`);
22
- lines.push(`- Estimated cost: $${plan.estimated_cost_usd}`);
23
+ lines.push(plan.estimated_cost_usd === null
24
+ ? '- Estimated cost: unavailable (exact runtime model was not recorded)'
25
+ : `- Estimated cost: $${plan.estimated_cost_usd}`);
23
26
  lines.push(`- Estimated duration: ${plan.estimated_duration_min} min`);
24
27
  if (plan.conflict_surface.length > 0) {
25
28
  lines.push(`- Conflicts: ${plan.conflict_surface.length}`);
@@ -1,17 +1,11 @@
1
1
  'use strict';
2
2
 
3
- // Phase 8A.1 — PRD → task[] parser. Delegates LLM call to an injected
4
- // llmClient (see lib/llm-client.cjs). The parser itself is pure logic:
5
- // prompt assembly → schema validation → id normalization. Keeping the
6
- // llmClient as a parameter makes unit tests a Test Double exercise without
7
- // mocking network I/O.
3
+ // Validate and normalize a task graph derived from a PRD by the current host.
4
+ // The MCP runtime is deliberately deterministic: model judgment stays with
5
+ // the host that already owns the user interaction, while this module owns the
6
+ // machine contract before state.db is mutated.
8
7
 
9
8
  const Ajv = require('ajv/dist/2020');
10
- const {
11
- buildSystemPrompt,
12
- buildUserPrompt,
13
- } = require('../../orchestrator/planner/prd-prompt.cjs');
14
-
15
9
  const TASK_LIST_SCHEMA = Object.freeze({
16
10
  type: 'object',
17
11
  required: ['tasks'],
@@ -48,39 +42,22 @@ class PrdParseError extends Error {
48
42
  }
49
43
  }
50
44
 
51
- async function parsePrd(prdText, { llmClient, tag } = {}) {
52
- if (!prdText || typeof prdText !== 'string' || !prdText.trim()) {
53
- throw new PrdParseError('NO_INPUT', 'prd text is empty');
54
- }
55
- if (!llmClient || typeof llmClient.completeJson !== 'function') {
56
- throw new PrdParseError('NO_LLM_CLIENT', 'llmClient.completeJson is required');
45
+ function parsePrd(tasks, { tag, changeId } = {}) {
46
+ if (!Array.isArray(tasks) || tasks.length === 0) {
47
+ throw new PrdParseError('NO_INPUT', 'at least one host-derived task is required');
57
48
  }
58
-
59
- const system = buildSystemPrompt();
60
- const user = buildUserPrompt(prdText);
61
-
62
- let reply;
63
- try {
64
- reply = await llmClient.completeJson({ system, user });
65
- } catch (err) {
66
- throw new PrdParseError(
67
- 'PARSE_FAILED',
68
- `LLM call failed: ${err.message}`,
69
- err,
70
- );
71
- }
72
-
73
- if (!validateTaskList(reply.json)) {
49
+ const input = { tasks };
50
+ if (!validateTaskList(input)) {
74
51
  throw new PrdParseError(
75
52
  'PARSE_FAILED',
76
- `LLM output failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
53
+ `task graph failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
77
54
  );
78
55
  }
79
56
 
80
57
  const ids = new Set();
81
- const tasks = reply.json.tasks.map((t) => {
58
+ const normalized = tasks.map((t) => {
82
59
  if (ids.has(t.id)) {
83
- throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in LLM output`);
60
+ throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in task graph`);
84
61
  }
85
62
  ids.add(t.id);
86
63
  return {
@@ -92,15 +69,11 @@ async function parsePrd(prdText, { llmClient, tag } = {}) {
92
69
  deps: Array.isArray(t.deps) ? t.deps : [],
93
70
  files_modified: Array.isArray(t.files_modified) ? t.files_modified : [],
94
71
  tag: tag || null,
72
+ change_id: changeId || null,
95
73
  };
96
74
  });
97
75
 
98
- return {
99
- tasks,
100
- usage: reply.usage || {},
101
- model: reply.model,
102
- provider: reply.provider,
103
- };
76
+ return { tasks: normalized };
104
77
  }
105
78
 
106
79
  module.exports = {
@@ -10,31 +10,28 @@
10
10
  // Claude: https://www.anthropic.com/pricing
11
11
  // OpenAI: https://platform.openai.com/docs/pricing
12
12
  //
13
- // `default` inside each runtime is the fallback when a session did not
14
- // record which specific model it used.
13
+ // Pricing is exact-model only. A runtime label is not a billable model and
14
+ // host applications can route the same runtime through different providers.
15
+ // Missing or unknown models therefore return null instead of a fabricated
16
+ // estimate.
15
17
 
16
18
  const PRICING = Object.freeze({
17
19
  claude: Object.freeze({
18
20
  'claude-opus-4-7': { input: 15e-6, output: 75e-6 },
19
21
  'claude-sonnet-4-6': { input: 3e-6, output: 15e-6 },
20
22
  'claude-haiku-4-5': { input: 0.8e-6, output: 4e-6 },
21
- default: { input: 3e-6, output: 15e-6 }, // assume sonnet
22
23
  }),
23
24
  codex: Object.freeze({
24
25
  'gpt-5.4': { input: 2e-6, output: 10e-6 },
25
- default: { input: 2e-6, output: 10e-6 },
26
- }),
27
- opencode: Object.freeze({
28
- // OpenCode is a frontend over whichever model the user configured;
29
- // treat as sonnet-equivalent unless a model name is passed in.
30
- default: { input: 3e-6, output: 15e-6 },
31
26
  }),
27
+ opencode: Object.freeze({}),
28
+ kimi: Object.freeze({}),
32
29
  });
33
30
 
34
31
  function computeCost(runtime, model, tokens_input, tokens_output) {
35
32
  const r = PRICING[runtime];
36
33
  if (!r) return null;
37
- const p = (model && r[model]) || r.default;
34
+ const p = model ? r[model] : null;
38
35
  if (!p) return null;
39
36
  const ti = Number(tokens_input) || 0;
40
37
  const to = Number(tokens_output) || 0;
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const Database = require('better-sqlite3');
7
+
8
+ const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
9
+ const { readBreadcrumb } = require('./context-spine.cjs');
10
+
11
+ function blockedBreadcrumb(code, {
12
+ nextAction,
13
+ workflow,
14
+ } = {}) {
15
+ return {
16
+ change_id: null,
17
+ task_id: null,
18
+ role: 'plan',
19
+ gate: 'alignment',
20
+ readiness: 'blocked',
21
+ blockers: [code],
22
+ warnings: [],
23
+ next_action: nextAction,
24
+ recommended_workflow: workflow,
25
+ context_manifest_path: null,
26
+ context_manifest_hash: null,
27
+ git_head: null,
28
+ baseline: null,
29
+ };
30
+ }
31
+
32
+ function migrationBreadcrumb(code) {
33
+ return blockedBreadcrumb(code, {
34
+ nextAction: 'Resume project initialization to migrate and re-establish the Ultra baseline.',
35
+ workflow: 'ultra-init',
36
+ });
37
+ }
38
+
39
+ function doctorBreadcrumb(code) {
40
+ return blockedBreadcrumb(code, {
41
+ nextAction: 'Run ultra-doctor to inspect and recover the authoritative Ultra state database.',
42
+ workflow: 'ultra-doctor',
43
+ });
44
+ }
45
+
46
+ function latestSchemaVersion(db) {
47
+ return db.prepare(
48
+ 'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
49
+ ).get()?.version || null;
50
+ }
51
+
52
+ function tableNames(db) {
53
+ return new Set(db.prepare(
54
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
55
+ ).all().map((row) => row.name));
56
+ }
57
+
58
+ function readProjectBreadcrumb(rootDir) {
59
+ const root = path.resolve(rootDir || process.cwd());
60
+ const ultraDir = path.join(root, '.ultra');
61
+ if (!fs.existsSync(ultraDir)) return null;
62
+ const dbPath = path.join(ultraDir, 'state.db');
63
+ if (!fs.existsSync(dbPath)) {
64
+ return blockedBreadcrumb('STATE_DB_MISSING', {
65
+ nextAction: 'Initialize or resume this Ultra project through ultra-init.',
66
+ workflow: 'ultra-init',
67
+ });
68
+ }
69
+
70
+ let db;
71
+ try {
72
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
73
+ db.pragma('query_only = ON');
74
+ db.pragma('busy_timeout = 1000');
75
+ const tables = tableNames(db);
76
+ if (!tables.has('schema_version')) {
77
+ return migrationBreadcrumb('STATE_SCHEMA_MIGRATION_REQUIRED:unknown');
78
+ }
79
+ const version = latestSchemaVersion(db);
80
+ if (version !== EXPECTED_VERSION) {
81
+ return migrationBreadcrumb(`STATE_SCHEMA_MIGRATION_REQUIRED:${version || 'unknown'}`);
82
+ }
83
+ const missing = REQUIRED_TABLES.filter((name) => !tables.has(name));
84
+ if (missing.length > 0) {
85
+ return doctorBreadcrumb(`STATE_SCHEMA_INCOMPLETE:${missing.join(',')}`);
86
+ }
87
+ return readBreadcrumb(db, {}, { rootDir: root });
88
+ } catch (error) {
89
+ const code = error && typeof error.code === 'string' ? error.code : 'unknown';
90
+ return doctorBreadcrumb(`STATE_DB_UNREADABLE:${code}`);
91
+ } finally {
92
+ if (db) db.close();
93
+ }
94
+ }
95
+
96
+ function renderProjectBreadcrumb(rootDir, breadcrumb) {
97
+ if (!breadcrumb) return '';
98
+ const root = path.resolve(rootDir || process.cwd());
99
+ const baseline = breadcrumb.baseline || {};
100
+ const task = breadcrumb.task_id || 'none';
101
+ const taskStatus = breadcrumb.task_status || 'none';
102
+ const lines = breadcrumb.change_id ? [
103
+ '[Ultra context spine]',
104
+ `Project: ${root}`,
105
+ `Baseline: ${baseline.id || 'unknown'} (${baseline.mode || 'unknown'}/${baseline.status || 'unknown'})`,
106
+ `Change: ${breadcrumb.change_id} (${breadcrumb.change_status || 'active'})`,
107
+ `Task: ${task} (${taskStatus})`,
108
+ `Role: ${breadcrumb.role || 'plan'}`,
109
+ `Gate: ${breadcrumb.gate || 'alignment'}`,
110
+ `Readiness: ${breadcrumb.readiness || 'blocked'}`,
111
+ ] : [
112
+ '[Ultra baseline]',
113
+ `Project: ${root}`,
114
+ `Baseline: ${baseline.id || 'missing'} (${baseline.mode || 'unknown'}/${baseline.status || 'missing'})`,
115
+ `Readiness: ${breadcrumb.readiness || 'blocked'}`,
116
+ ];
117
+ if (Array.isArray(breadcrumb.blockers) && breadcrumb.blockers.length > 0) {
118
+ lines.push(`Blockers: ${breadcrumb.blockers.join(', ')}`);
119
+ }
120
+ if (Array.isArray(breadcrumb.warnings) && breadcrumb.warnings.length > 0) {
121
+ lines.push(`Warnings: ${breadcrumb.warnings.join(', ')}`);
122
+ }
123
+ if (breadcrumb.workflow) {
124
+ lines.push(
125
+ `Workflow: ${breadcrumb.workflow.id} (${breadcrumb.workflow.kind}/${breadcrumb.workflow.status})`,
126
+ `Step: ${breadcrumb.workflow.current_step || 'finalize'}`,
127
+ );
128
+ }
129
+ lines.push(
130
+ `Next: ${breadcrumb.next_action || 'Inspect Ultra status.'}`,
131
+ `Route: ${breadcrumb.recommended_workflow || 'ultra-doctor'}`,
132
+ );
133
+ if (breadcrumb.context_manifest_path) {
134
+ lines.push(
135
+ `Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,
136
+ );
137
+ }
138
+ lines.push('Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.');
139
+ return lines.join('\n');
140
+ }
141
+
142
+ module.exports = {
143
+ readProjectBreadcrumb,
144
+ renderProjectBreadcrumb,
145
+ };