ultra-builder-pro-cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +77 -1
  8. package/README.md +46 -24
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/handbook.cjs +2 -1
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +3 -2
  16. package/agents/review-coordinator.md +9 -6
  17. package/commands/ultra-change.md +1 -1
  18. package/commands/ultra-plan.md +1 -1
  19. package/commands/ultra-research.md +1 -1
  20. package/commands/ultra-think.md +2 -2
  21. package/docs/AGENT-CONTEXT.md +43 -19
  22. package/docs/ARCHITECTURE.md +54 -13
  23. package/docs/PLAN.zh-CN.md +4 -2
  24. package/docs/ROADMAP.md +10 -4
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  26. package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
  27. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  28. package/docs/WORKFLOW-LIFECYCLE.md +342 -0
  29. package/hooks/README.md +6 -1
  30. package/hooks/health_check.py +12 -3
  31. package/hooks/subagent_tracker.py +52 -77
  32. package/mcp-server/hook-event.cjs +34 -0
  33. package/mcp-server/lib/archive-journal.cjs +17 -5
  34. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  35. package/mcp-server/lib/change-workflow.cjs +598 -85
  36. package/mcp-server/lib/context-spine.cjs +305 -25
  37. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  38. package/mcp-server/lib/doctor.cjs +42 -3
  39. package/mcp-server/lib/init-project.cjs +210 -16
  40. package/mcp-server/lib/plan-store.cjs +4 -1
  41. package/mcp-server/lib/pricing.cjs +7 -10
  42. package/mcp-server/lib/project-breadcrumb.cjs +15 -0
  43. package/mcp-server/lib/projector.cjs +57 -3
  44. package/mcp-server/lib/spec-learning.cjs +79 -2
  45. package/mcp-server/lib/state-db.cjs +168 -8
  46. package/mcp-server/lib/state-ops.cjs +160 -9
  47. package/mcp-server/lib/workflow-state.cjs +2305 -0
  48. package/mcp-server/server.cjs +215 -15
  49. package/orchestrator/daemon.cjs +1 -2
  50. package/orchestrator/dispatch-rules.cjs +5 -31
  51. package/orchestrator/planner/plan-builder.cjs +21 -12
  52. package/package.json +13 -3
  53. package/skills/ultra-change/SKILL.md +94 -80
  54. package/skills/ultra-change/references/change-contract.md +36 -0
  55. package/skills/ultra-deliver/SKILL.md +76 -79
  56. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  57. package/skills/ultra-dev/SKILL.md +62 -59
  58. package/skills/ultra-doctor/SKILL.md +61 -82
  59. package/skills/ultra-init/SKILL.md +101 -103
  60. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  61. package/skills/ultra-plan/SKILL.md +99 -85
  62. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  63. package/skills/ultra-research/SKILL.md +102 -60
  64. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  65. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  66. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  67. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  68. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  69. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  70. package/skills/ultra-research/references/10-user-personas.md +29 -0
  71. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  72. package/skills/ultra-research/references/20-user-stories.md +30 -0
  73. package/skills/ultra-research/references/21-features-scope.md +29 -0
  74. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  75. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  76. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  77. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  78. package/skills/ultra-research/references/40-deployment.md +29 -0
  79. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  80. package/skills/ultra-research/references/99-synthesis.md +35 -0
  81. package/skills/ultra-review/SKILL.md +63 -73
  82. package/skills/ultra-review/references/review-modes.md +27 -0
  83. package/skills/ultra-review/references/unified-schema.md +25 -0
  84. package/skills/ultra-review/scripts/review_wait.py +40 -3
  85. package/skills/ultra-status/SKILL.md +41 -41
  86. package/skills/ultra-test/SKILL.md +54 -55
  87. package/skills/ultra-think/SKILL.md +26 -12
  88. package/skills/ultra-think/references/decision-dialogue.md +130 -0
  89. package/skills/ultra-verify/SKILL.md +1 -1
  90. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  91. package/spec/cli-protocol.md +17 -3
  92. package/spec/mcp-tools.yaml +683 -64
  93. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  94. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  95. package/spec/schemas/mcp-tools.schema.json +1 -1
  96. package/spec/schemas/state-db.sql +140 -1
  97. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  98. package/spec/schemas/test-report.v1.schema.json +194 -0
  99. package/ultra-tools/cli.cjs +1 -1
  100. package/ultra-tools/commands/migrate.cjs +3 -3
  101. package/ultra-tools/commands/status.cjs +228 -12
  102. package/.ultra-template/delivery-report.json +0 -9
  103. package/.ultra-template/test-report.json +0 -8
  104. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  105. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  106. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  107. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  108. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  109. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  110. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  111. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  112. package/adapters/_shared/tests/validate.test.cjs +0 -30
  113. package/adapters/tests/claude.test.cjs +0 -138
  114. package/adapters/tests/codex-hook.test.cjs +0 -142
  115. package/adapters/tests/codex.test.cjs +0 -430
  116. package/adapters/tests/kimi-hook.test.cjs +0 -149
  117. package/adapters/tests/kimi.test.cjs +0 -259
  118. package/adapters/tests/opencode.test.cjs +0 -357
  119. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  120. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  121. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  122. package/mcp-server/lib/context-spine.test.cjs +0 -93
  123. package/mcp-server/lib/doctor.test.cjs +0 -148
  124. package/mcp-server/lib/plan-store.test.cjs +0 -132
  125. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  126. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  127. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  128. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  129. package/mcp-server/lib/state-db.test.cjs +0 -314
  130. package/mcp-server/lib/task-expander.test.cjs +0 -169
  131. package/mcp-server/lib/topo.test.cjs +0 -158
  132. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  133. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  134. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  135. package/mcp-server/tests/concurrency.test.cjs +0 -112
  136. package/mcp-server/tests/db-ops.test.cjs +0 -121
  137. package/mcp-server/tests/init-project.test.cjs +0 -393
  138. package/mcp-server/tests/migrate.test.cjs +0 -232
  139. package/mcp-server/tests/projector.test.cjs +0 -162
  140. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  141. package/mcp-server/tests/staleness.test.cjs +0 -190
  142. package/mcp-server/tests/state-db.test.cjs +0 -106
  143. package/mcp-server/tests/state-ops.test.cjs +0 -449
  144. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  145. package/mcp-server/tests/telemetry.test.cjs +0 -219
  146. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  147. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  148. package/orchestrator/tests/daemon.test.cjs +0 -311
  149. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  150. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  151. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  152. package/orchestrator/tests/recovery.test.cjs +0 -190
  153. package/orchestrator/tests/session-runner.test.cjs +0 -314
  154. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  155. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  156. package/skills/ultra-research/references/architecture.md +0 -38
  157. package/skills/ultra-research/references/problem-product.md +0 -39
  158. package/skills/ultra-research/references/quality-delivery.md +0 -34
  159. package/skills/ultra-research/references/synthesis.md +0 -38
  160. package/skills/ultra-research/references/users-scope.md +0 -40
  161. package/ultra-tools/legacy-memory.test.cjs +0 -80
  162. package/ultra-tools/status-command.test.cjs +0 -181
  163. package/ultra-tools/system-command.test.cjs +0 -309
@@ -11,6 +11,8 @@ const recovery = require('../../orchestrator/recovery.cjs');
11
11
  const baselines = require('./baseline-workflow.cjs');
12
12
  const changes = require('./change-workflow.cjs');
13
13
  const archiveJournal = require('./archive-journal.cjs');
14
+ const workflows = require('./workflow-state.cjs');
15
+ const decisions = require('./decision-dialogue.cjs');
14
16
 
15
17
  const SPEC_CONSUMER = 'spec-staleness';
16
18
 
@@ -56,10 +58,27 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
56
58
  const baseline = missing.length === 0
57
59
  ? baselines.inspectBaseline(db, { rootDir })
58
60
  : { status: 'fail', blockers: ['BASELINE_STATE_UNAVAILABLE'], warnings: [], baseline: null };
61
+ const workflowHealth = missing.length === 0
62
+ ? workflows.inspectWorkflowHealth(db, { rootDir })
63
+ : {
64
+ status: 'fail', active: 0, blocked: 0, ready: 0, stale_outputs: [],
65
+ historical_stale_outputs: [], terminal_authority_runs: [], untracked_active_changes: [],
66
+ };
67
+ const decisionHealth = missing.length === 0
68
+ ? decisions.inspectDecisionHealth(db, { rootDir })
69
+ : {
70
+ status: 'fail', active: 0, awaiting_owner: 0, checkpoint_ready: 0,
71
+ deferred_blocking: 0, stale_artifacts: [], current: null, current_thread_id: null,
72
+ };
73
+ const baselineCheckStatus = baseline.status === 'pass'
74
+ ? 'pass'
75
+ : (baseline.baseline?.status === 'ready' ? 'fail' : 'warning');
59
76
  const degraded = integrity !== 'ok' || missing.length > 0 || incidents.length > 0
60
77
  || pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
61
78
  || archiveIntents.length > 0
62
- || eventCursor > projectedCursor;
79
+ || eventCursor > projectedCursor || workflowHealth.status === 'fail'
80
+ || decisionHealth.status === 'fail'
81
+ || baselineCheckStatus === 'fail';
63
82
  return {
64
83
  status: degraded ? 'degraded' : 'healthy',
65
84
  checks: {
@@ -77,7 +96,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
77
96
  pending: archiveIntents.length, corrupt: corruptArchiveIntents,
78
97
  },
79
98
  baseline: {
80
- status: baseline.status === 'pass' ? 'pass' : 'warning',
99
+ status: baselineCheckStatus,
81
100
  readiness: baseline.status,
82
101
  mode: baseline.baseline?.mode || null,
83
102
  baseline_status: baseline.baseline?.status || null,
@@ -89,6 +108,22 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
89
108
  blockers: baseline.blockers,
90
109
  warnings: baseline.warnings,
91
110
  },
111
+ workflows: {
112
+ status: workflowHealth.status === 'fail'
113
+ ? 'fail'
114
+ : (workflowHealth.blocked > 0
115
+ || workflowHealth.historical_stale_outputs.length > 0
116
+ || workflowHealth.terminal_authority_runs.length > 0
117
+ || workflowHealth.untracked_active_changes.length > 0 ? 'warning' : 'pass'),
118
+ active: workflowHealth.active,
119
+ blocked: workflowHealth.blocked,
120
+ ready: workflowHealth.ready,
121
+ stale_outputs: workflowHealth.stale_outputs,
122
+ historical_stale_outputs: workflowHealth.historical_stale_outputs,
123
+ terminal_authority_runs: workflowHealth.terminal_authority_runs,
124
+ untracked_active_changes: workflowHealth.untracked_active_changes,
125
+ },
126
+ decisions: decisionHealth,
92
127
  external_providers: {
93
128
  status: 'pass', ownership: 'external',
94
129
  note: 'Ultra stores provider metadata references only and never owns memory or code-graph content.',
@@ -113,6 +148,7 @@ async function runDoctor(db, {
113
148
  if (!repair) return { ...inspectSystem(db, { rootDir }), repair_performed: false };
114
149
  const backupPath = await backupDatabase(db, rootDir);
115
150
  const archives = changes.recoverInterruptedArchives(db, { rootDir });
151
+ const workflowRecovery = workflows.recoverUntrackedChangeWorkflows(db, { rootDir });
116
152
  const recovered = recovery.recoverOnBoot(db);
117
153
  const cursor = runtime.readConsumerCursor(db, SPEC_CONSUMER);
118
154
  const consumed = ops.consumeSpecChangedEvents(db, { since_id: cursor, limit: 500 });
@@ -127,7 +163,10 @@ async function runDoctor(db, {
127
163
  ...inspectSystem(db, { rootDir }),
128
164
  repair_performed: true,
129
165
  backup_path: backupPath,
130
- repair: { archives, recovered, consumed, interrupted, requeued, ensured, projections },
166
+ repair: {
167
+ archives, workflows: workflowRecovery, recovered, consumed,
168
+ interrupted, requeued, ensured, projections,
169
+ },
131
170
  };
132
171
  }
133
172
 
@@ -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}`);
@@ -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;
@@ -120,6 +120,21 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
120
120
  if (Array.isArray(breadcrumb.warnings) && breadcrumb.warnings.length > 0) {
121
121
  lines.push(`Warnings: ${breadcrumb.warnings.join(', ')}`);
122
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
+ if (breadcrumb.decision) {
130
+ const current = breadcrumb.decision.current;
131
+ lines.push(
132
+ `Decision thread: ${breadcrumb.decision.thread_id} (${breadcrumb.decision.status}/${breadcrumb.decision.mode})`,
133
+ current
134
+ ? `Decision: ${current.id} — ${current.question}`
135
+ : 'Decision: checkpoint confirmation required',
136
+ );
137
+ }
123
138
  lines.push(
124
139
  `Next: ${breadcrumb.next_action || 'Inspect Ultra status.'}`,
125
140
  `Route: ${breadcrumb.recommended_workflow || 'ultra-doctor'}`,
@@ -16,8 +16,11 @@ const SCHEMA_VERSION = '4.5';
16
16
  const SOURCE_TAG = '.ultra/state.db';
17
17
 
18
18
  // Frozen SELECTs — values bind through @placeholders.
19
- const LIST_TASKS_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks ORDER BY created_at ASC";
20
- const READ_TASK_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks WHERE id = @id";
19
+ const TASK_PROJECTION_COLUMNS = 'id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, tag, trace_to, outcome, slice_kind, public_seam, verification_command, acceptance_json, context_refs_json, docs_impact_json, ownership_json, context_file, completion_commit, created_at, updated_at';
20
+ const LIST_TASKS_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks ORDER BY created_at ASC`;
21
+ const READ_TASK_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks WHERE id = @id`;
22
+ const CONTRACT_START = '<!-- ultra:task-contract:start -->';
23
+ const CONTRACT_END = '<!-- ultra:task-contract:end -->';
21
24
 
22
25
  function rowToProjection(row) {
23
26
  if (!row) return null;
@@ -28,6 +31,17 @@ function rowToProjection(row) {
28
31
  }
29
32
  if (out[k] === null || out[k] === undefined) delete out[k];
30
33
  }
34
+ const jsonFields = {
35
+ acceptance_json: ['acceptance', []],
36
+ context_refs_json: ['context_refs', []],
37
+ docs_impact_json: ['docs_impact', { status: 'unknown', files: [], rationale: null }],
38
+ ownership_json: ['ownership', {}],
39
+ };
40
+ for (const [column, [field, fallback]] of Object.entries(jsonFields)) {
41
+ try { out[field] = typeof out[column] === 'string' ? JSON.parse(out[column]) : fallback; }
42
+ catch { out[field] = fallback; }
43
+ delete out[column];
44
+ }
31
45
  if (out.stale !== undefined && out.stale !== null) out.stale = Boolean(out.stale);
32
46
  for (const k of Object.keys(out)) {
33
47
  if (out[k] === null || out[k] === undefined) delete out[k];
@@ -94,17 +108,57 @@ function buildContextDoc(taskRow, existingBody) {
94
108
  body = body.replace(/^>\s*\*\*Status\*\*:.*(?:\r?\n|$)(?:\r?\n)?/mi, '');
95
109
  // Strip any previous STALE banner so re-projection after reset is clean.
96
110
  body = body.replace(/^> ⚠️\s*STALE[^\n]*\n(?:>[^\n]*\n)*\n?/m, '');
111
+ body = body.replace(
112
+ new RegExp(`${CONTRACT_START}[\\s\\S]*?${CONTRACT_END}\\n*`, 'm'),
113
+ '',
114
+ );
97
115
  if (taskRow.stale) {
98
116
  const banner = [
99
117
  `> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
100
- '> spec 改动可能让此 task 上下文失效;重新跑 spec 对齐后再继续。',
118
+ '> Specification evidence may have changed. Recompile the task context before continuing.',
101
119
  '',
102
120
  ].join('\n');
103
121
  body = banner + body;
104
122
  }
123
+ body = buildTaskContract(taskRow) + body;
105
124
  return headerLines.join('\n') + body;
106
125
  }
107
126
 
127
+ function buildTaskContract(task) {
128
+ const acceptance = Array.isArray(task.acceptance) ? task.acceptance : [];
129
+ const refs = Array.isArray(task.context_refs) ? task.context_refs : [];
130
+ const docs = task.docs_impact || { status: 'unknown', files: [], rationale: null };
131
+ const ownership = task.ownership || {};
132
+ const lines = [
133
+ CONTRACT_START,
134
+ '## Execution Contract (generated from state.db)',
135
+ '',
136
+ `- Outcome: ${task.outcome || 'unresolved'}`,
137
+ `- Slice: ${task.slice_kind || 'unresolved'}`,
138
+ `- Public seam: ${task.public_seam || 'unresolved'}`,
139
+ `- Verification: ${task.verification_command || 'unresolved'}`,
140
+ `- Trace: ${task.trace_to || 'unresolved'}`,
141
+ `- Owner: ${ownership.owner || 'unresolved'}`,
142
+ `- Documentation: ${docs.status || 'unknown'}${docs.rationale ? ` — ${docs.rationale}` : ''}`,
143
+ '',
144
+ '### Acceptance',
145
+ '',
146
+ ...(acceptance.length > 0
147
+ ? acceptance.map((item) => `- ${item.id}: ${item.criterion} — verify with \`${item.verification}\``)
148
+ : ['- unresolved']),
149
+ '',
150
+ '### Context References',
151
+ '',
152
+ ...(refs.length > 0
153
+ ? refs.map((item) => `- \`${item.ref}\` — ${item.reason}`)
154
+ : ['- unresolved']),
155
+ '',
156
+ CONTRACT_END,
157
+ '',
158
+ ];
159
+ return lines.join('\n');
160
+ }
161
+
108
162
  function escapeYaml(str) {
109
163
  if (str == null) return '';
110
164
  if (/[:#\[\]\{\}&\*!|>'"%@`]/.test(str) || /^\s|\s$/.test(str)) {