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.
- package/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -324
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1471
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- 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
|
-
'.
|
|
141
|
-
'.
|
|
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) ||
|
|
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
|
|
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':
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
|
|
52
|
-
if (!
|
|
53
|
-
throw new PrdParseError('NO_INPUT', '
|
|
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
|
-
|
|
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
|
-
`
|
|
53
|
+
`task graph failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
|
|
77
54
|
);
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
const ids = new Set();
|
|
81
|
-
const
|
|
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
|
|
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
|
-
//
|
|
14
|
-
//
|
|
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 =
|
|
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
|
+
};
|