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.
- 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 +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- 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 +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -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 +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -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 +63 -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 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- 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 +140 -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 +228 -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 -149
- 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 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- 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/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- 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 -169
- 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 -1467
- 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/plan-builder.test.cjs +0 -165
- 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
|
@@ -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:
|
|
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: {
|
|
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
|
-
'.
|
|
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}`);
|
|
@@ -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;
|
|
@@ -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
|
|
20
|
-
const
|
|
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
|
-
'>
|
|
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)) {
|