ultra-builder-pro-cli 0.12.1 → 0.14.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 +21 -44
- package/.ultra-template/specs/discovery.md +10 -0
- package/CHANGELOG.md +95 -1
- package/README.md +31 -16
- package/adapters/_shared/codex-assets.cjs +23 -163
- package/adapters/_shared/handbook.cjs +2 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
- package/adapters/opencode.js +30 -120
- package/adapters/tests/claude.test.cjs +3 -2
- package/adapters/tests/codex-hook.test.cjs +34 -7
- package/adapters/tests/codex.test.cjs +21 -6
- package/adapters/tests/kimi-hook.test.cjs +34 -7
- package/adapters/tests/opencode.test.cjs +70 -84
- package/commands/ultra-init.md +1 -1
- package/docs/AGENT-CONTEXT.md +10 -6
- package/docs/ARCHITECTURE.md +40 -17
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +2 -2
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
- package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -299
- package/hooks/health_check.py +19 -17
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +13 -19
- 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/lib/archive-journal.cjs +192 -0
- package/mcp-server/lib/archive-journal.test.cjs +35 -0
- package/mcp-server/lib/baseline-workflow.cjs +404 -38
- package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
- package/mcp-server/lib/change-workflow.cjs +249 -75
- package/mcp-server/lib/change-workflow.test.cjs +235 -1
- package/mcp-server/lib/context-spine.test.cjs +5 -0
- package/mcp-server/lib/doctor.cjs +14 -1
- package/mcp-server/lib/doctor.test.cjs +42 -4
- package/mcp-server/lib/init-project.cjs +292 -9
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/prd-parser.test.cjs +49 -138
- package/mcp-server/lib/project-breadcrumb.cjs +139 -0
- package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
- package/mcp-server/lib/spec-learning.test.cjs +1 -1
- package/mcp-server/lib/state-authority.cjs +9 -3
- package/mcp-server/lib/state-db.cjs +154 -32
- package/mcp-server/lib/state-db.test.cjs +136 -4
- package/mcp-server/lib/state-ops.cjs +68 -6
- package/mcp-server/lib/task-expander.cjs +14 -37
- package/mcp-server/lib/task-expander.test.cjs +80 -164
- package/mcp-server/server.cjs +34 -37
- package/mcp-server/tests/init-project.test.cjs +162 -1
- package/mcp-server/tests/migrate.test.cjs +33 -0
- package/mcp-server/tests/server-contract.test.cjs +391 -17
- package/mcp-server/tests/state-db.test.cjs +14 -2
- package/mcp-server/tests/state-ops.test.cjs +108 -0
- package/package.json +2 -4
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +18 -10
- package/skills/ultra-deliver/SKILL.md +13 -9
- package/skills/ultra-dev/SKILL.md +6 -4
- package/skills/ultra-doctor/SKILL.md +22 -7
- package/skills/ultra-init/SKILL.md +104 -59
- package/skills/ultra-init/references/brownfield-adoption.md +65 -37
- package/skills/ultra-plan/SKILL.md +20 -7
- package/skills/ultra-research/SKILL.md +12 -5
- package/skills/ultra-status/SKILL.md +6 -4
- package/skills/ultra-verify/SKILL.md +21 -30
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +11 -1
- package/spec/mcp-tools.yaml +133 -19
- package/spec/schemas/state-db.sql +11 -0
- package/ultra-tools/cli.cjs +9 -5
- package/ultra-tools/commands/migrate.cjs +62 -36
- package/ultra-tools/commands/status.cjs +18 -9
- package/ultra-tools/commands/system.cjs +340 -6
- package/ultra-tools/commands/task.cjs +7 -1
- package/ultra-tools/status-command.test.cjs +29 -0
- package/ultra-tools/system-command.test.cjs +252 -1
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
|
@@ -1,171 +1,82 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Test Double rationale: external LLM API — deterministic fixtures prove
|
|
4
|
-
// parsing logic (prompt assembly, schema validation, id normalization)
|
|
5
|
-
// without egress cost or key dependence. Integration coverage against a
|
|
6
|
-
// real LLM lives behind an env gate and is exercised separately.
|
|
7
|
-
|
|
8
3
|
const test = require('node:test');
|
|
9
4
|
const assert = require('node:assert/strict');
|
|
10
5
|
|
|
11
6
|
const { parsePrd, PrdParseError, TASK_LIST_SCHEMA } = require('./prd-parser.cjs');
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
id: 'task-2',
|
|
37
|
-
title: 'Implement login endpoint',
|
|
38
|
-
type: 'feature',
|
|
39
|
-
priority: 'P1',
|
|
40
|
-
complexity: 6,
|
|
41
|
-
deps: ['task-1'],
|
|
42
|
-
files_modified: ['src/auth/login.ts'],
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
test('parsePrd: happy path normalizes tasks and returns usage metadata', async () => {
|
|
48
|
-
const client = fakeClient(HAPPY_TASKS);
|
|
49
|
-
const result = await parsePrd('Build login', { llmClient: client, tag: 'feat-auth' });
|
|
8
|
+
const HAPPY_TASKS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'task-1',
|
|
11
|
+
title: 'Design auth schema',
|
|
12
|
+
type: 'architecture',
|
|
13
|
+
priority: 'P1',
|
|
14
|
+
complexity: 5,
|
|
15
|
+
deps: [],
|
|
16
|
+
files_modified: ['src/auth/schema.ts'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'task-2',
|
|
20
|
+
title: 'Implement login endpoint',
|
|
21
|
+
type: 'feature',
|
|
22
|
+
priority: 'P1',
|
|
23
|
+
complexity: 6,
|
|
24
|
+
deps: ['task-1'],
|
|
25
|
+
files_modified: ['src/auth/login.ts'],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
test('parsePrd validates and normalizes host-derived tasks without a provider client', () => {
|
|
30
|
+
const result = parsePrd(HAPPY_TASKS, { tag: 'feat-auth', changeId: 'auth-change' });
|
|
50
31
|
assert.equal(result.tasks.length, 2);
|
|
51
|
-
assert.equal(result.tasks[0].id, 'task-1');
|
|
52
32
|
assert.equal(result.tasks[0].tag, 'feat-auth');
|
|
33
|
+
assert.equal(result.tasks[0].change_id, 'auth-change');
|
|
53
34
|
assert.deepEqual(result.tasks[1].deps, ['task-1']);
|
|
54
|
-
assert.equal(result
|
|
55
|
-
assert.equal(result
|
|
56
|
-
assert.equal(result
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('parsePrd: empty text → NO_INPUT', async () => {
|
|
60
|
-
await assert.rejects(
|
|
61
|
-
parsePrd('', { llmClient: fakeClient(HAPPY_TASKS) }),
|
|
62
|
-
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
63
|
-
);
|
|
64
|
-
await assert.rejects(
|
|
65
|
-
parsePrd(' ', { llmClient: fakeClient(HAPPY_TASKS) }),
|
|
66
|
-
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
67
|
-
);
|
|
35
|
+
assert.equal(Object.hasOwn(result, 'model'), false);
|
|
36
|
+
assert.equal(Object.hasOwn(result, 'provider'), false);
|
|
37
|
+
assert.equal(Object.hasOwn(result, 'usage'), false);
|
|
68
38
|
});
|
|
69
39
|
|
|
70
|
-
test('parsePrd
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
40
|
+
test('parsePrd requires at least one host-derived task', () => {
|
|
41
|
+
for (const value of [undefined, null, [], 'not-an-array']) {
|
|
42
|
+
assert.throws(
|
|
43
|
+
() => parsePrd(value),
|
|
44
|
+
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
75
47
|
});
|
|
76
48
|
|
|
77
|
-
test('parsePrd
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
await assert.rejects(
|
|
84
|
-
parsePrd('Build something', { llmClient: brokenClient }),
|
|
85
|
-
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /upstream LLM 500/.test(err.message),
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('parsePrd: invalid JSON shape rejected — missing required "priority"', async () => {
|
|
90
|
-
const bad = {
|
|
91
|
-
tasks: [{
|
|
92
|
-
id: 'task-1', title: 'Foo', type: 'feature', // priority missing
|
|
93
|
-
complexity: 3, deps: [], files_modified: [],
|
|
94
|
-
}],
|
|
95
|
-
};
|
|
96
|
-
await assert.rejects(
|
|
97
|
-
parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
|
|
49
|
+
test('parsePrd rejects an invalid task shape', () => {
|
|
50
|
+
assert.throws(
|
|
51
|
+
() => parsePrd([{
|
|
52
|
+
id: 'task-1', title: 'Missing priority', type: 'feature',
|
|
53
|
+
}]),
|
|
98
54
|
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /priority/.test(err.message),
|
|
99
55
|
);
|
|
100
56
|
});
|
|
101
57
|
|
|
102
|
-
test('parsePrd
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
id: 'task-1', title: 'Foo', type: 'chore', priority: 'P1',
|
|
106
|
-
}],
|
|
107
|
-
};
|
|
108
|
-
await assert.rejects(
|
|
109
|
-
parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
|
|
110
|
-
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('parsePrd: duplicate task id rejected', async () => {
|
|
115
|
-
const dup = {
|
|
116
|
-
tasks: [
|
|
58
|
+
test('parsePrd rejects duplicate task ids', () => {
|
|
59
|
+
assert.throws(
|
|
60
|
+
() => parsePrd([
|
|
117
61
|
{ id: 'task-1', title: 'First copy', type: 'feature', priority: 'P2' },
|
|
118
62
|
{ id: 'task-1', title: 'Second copy', type: 'feature', priority: 'P2' },
|
|
119
|
-
],
|
|
120
|
-
};
|
|
121
|
-
await assert.rejects(
|
|
122
|
-
parsePrd('Build dupes', { llmClient: fakeClient(dup) }),
|
|
63
|
+
]),
|
|
123
64
|
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /duplicate/i.test(err.message),
|
|
124
65
|
);
|
|
125
66
|
});
|
|
126
67
|
|
|
127
|
-
test('parsePrd
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}],
|
|
132
|
-
};
|
|
133
|
-
const result = await parsePrd('anything', { llmClient: fakeClient(minimal) });
|
|
68
|
+
test('parsePrd normalizes optional fields', () => {
|
|
69
|
+
const result = parsePrd([{
|
|
70
|
+
id: 'task-1', title: 'Minimal task', type: 'feature', priority: 'P3',
|
|
71
|
+
}]);
|
|
134
72
|
assert.equal(result.tasks[0].complexity, null);
|
|
135
73
|
assert.deepEqual(result.tasks[0].deps, []);
|
|
136
74
|
assert.deepEqual(result.tasks[0].files_modified, []);
|
|
137
75
|
assert.equal(result.tasks[0].tag, null);
|
|
76
|
+
assert.equal(result.tasks[0].change_id, null);
|
|
138
77
|
});
|
|
139
78
|
|
|
140
|
-
test('
|
|
141
|
-
let captured;
|
|
142
|
-
const client = {
|
|
143
|
-
provider: 'anthropic',
|
|
144
|
-
model: 'claude-sonnet-4-6',
|
|
145
|
-
async completeJson({ system, user }) {
|
|
146
|
-
captured = { system, user };
|
|
147
|
-
return {
|
|
148
|
-
json: HAPPY_TASKS,
|
|
149
|
-
raw: '',
|
|
150
|
-
usage: {},
|
|
151
|
-
model: 'claude-sonnet-4-6',
|
|
152
|
-
provider: 'anthropic',
|
|
153
|
-
};
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
await parsePrd('Build the thing with acceptance criteria X', { llmClient: client });
|
|
157
|
-
assert.match(captured.system, /engineering planner/i);
|
|
158
|
-
assert.match(captured.user, /Build the thing with acceptance criteria X/);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('parsePrd: empty tasks array rejected (schema requires minItems:1)', async () => {
|
|
162
|
-
await assert.rejects(
|
|
163
|
-
parsePrd('Build nothing', { llmClient: fakeClient({ tasks: [] }) }),
|
|
164
|
-
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('TASK_LIST_SCHEMA: exported for downstream consumers', () => {
|
|
79
|
+
test('TASK_LIST_SCHEMA remains available to MCP contract consumers', () => {
|
|
169
80
|
assert.equal(TASK_LIST_SCHEMA.required[0], 'tasks');
|
|
170
81
|
assert.equal(TASK_LIST_SCHEMA.properties.tasks.minItems, 1);
|
|
171
82
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
lines.push(
|
|
124
|
+
`Next: ${breadcrumb.next_action || 'Inspect Ultra status.'}`,
|
|
125
|
+
`Route: ${breadcrumb.recommended_workflow || 'ultra-doctor'}`,
|
|
126
|
+
);
|
|
127
|
+
if (breadcrumb.context_manifest_path) {
|
|
128
|
+
lines.push(
|
|
129
|
+
`Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
lines.push('Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.');
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
readProjectBreadcrumb,
|
|
138
|
+
renderProjectBreadcrumb,
|
|
139
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const Database = require('better-sqlite3');
|
|
9
|
+
|
|
10
|
+
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
11
|
+
const { createChange } = require('./change-workflow.cjs');
|
|
12
|
+
const { createTask } = require('./state-ops.cjs');
|
|
13
|
+
const { compileRoleContext, readBreadcrumb } = require('./context-spine.cjs');
|
|
14
|
+
const {
|
|
15
|
+
readProjectBreadcrumb,
|
|
16
|
+
renderProjectBreadcrumb,
|
|
17
|
+
} = require('./project-breadcrumb.cjs');
|
|
18
|
+
|
|
19
|
+
function tmpProject(prefix) {
|
|
20
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
21
|
+
fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
|
|
22
|
+
return rootDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('project reader returns the exact canonical breadcrumb from state.db', () => {
|
|
26
|
+
const rootDir = tmpProject('ubp-project-breadcrumb-');
|
|
27
|
+
const state = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
28
|
+
try {
|
|
29
|
+
state.db.prepare(
|
|
30
|
+
`INSERT INTO baselines
|
|
31
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
32
|
+
VALUES ('baseline-1', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
|
|
33
|
+
).run(new Date().toISOString());
|
|
34
|
+
const { change } = createChange(state.db, {
|
|
35
|
+
id: 'db-change', title: 'Database change', kind: 'quick',
|
|
36
|
+
intent: 'Prove every host consumes one breadcrumb.',
|
|
37
|
+
docs_impact: { status: 'none', rationale: 'test fixture' },
|
|
38
|
+
}, { rootDir });
|
|
39
|
+
const task = createTask(state.db, {
|
|
40
|
+
id: 'db-task', title: 'Use one breadcrumb', type: 'bugfix', priority: 'P0',
|
|
41
|
+
change_id: change.id,
|
|
42
|
+
});
|
|
43
|
+
compileRoleContext(state.db, {
|
|
44
|
+
input: {
|
|
45
|
+
task_id: task.id,
|
|
46
|
+
role: 'implement',
|
|
47
|
+
gate: 'implementation',
|
|
48
|
+
execution_contract: {
|
|
49
|
+
slice_kind: 'tracer_bullet',
|
|
50
|
+
public_seam: 'all host injections',
|
|
51
|
+
verification_command: 'npm test',
|
|
52
|
+
},
|
|
53
|
+
next_action: 'Run the cross-host breadcrumb regression test.',
|
|
54
|
+
},
|
|
55
|
+
change,
|
|
56
|
+
tasks: [task],
|
|
57
|
+
rootDir,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const expected = readBreadcrumb(state.db, {}, { rootDir });
|
|
61
|
+
const actual = readProjectBreadcrumb(rootDir);
|
|
62
|
+
assert.deepEqual(actual, expected);
|
|
63
|
+
const rendered = renderProjectBreadcrumb(rootDir, actual);
|
|
64
|
+
assert.match(rendered, /Change: db-change/);
|
|
65
|
+
assert.match(rendered, /Task: db-task/);
|
|
66
|
+
assert.ok(rendered.includes(actual.next_action));
|
|
67
|
+
assert.match(rendered, /Authority: \.ultra\/state\.db/);
|
|
68
|
+
} finally {
|
|
69
|
+
closeStateDb(state.db);
|
|
70
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('project reader routes a missing authority to ultra-init', () => {
|
|
75
|
+
const rootDir = tmpProject('ubp-project-missing-db-');
|
|
76
|
+
try {
|
|
77
|
+
const breadcrumb = readProjectBreadcrumb(rootDir);
|
|
78
|
+
assert.equal(breadcrumb.readiness, 'blocked');
|
|
79
|
+
assert.deepEqual(breadcrumb.blockers, ['STATE_DB_MISSING']);
|
|
80
|
+
assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
|
|
81
|
+
} finally {
|
|
82
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('project reader routes an old schema to ultra-init without consulting projections', () => {
|
|
87
|
+
const rootDir = tmpProject('ubp-project-old-schema-');
|
|
88
|
+
const db = new Database(path.join(rootDir, '.ultra', 'state.db'));
|
|
89
|
+
try {
|
|
90
|
+
db.exec(`
|
|
91
|
+
CREATE TABLE schema_version (
|
|
92
|
+
version TEXT PRIMARY KEY,
|
|
93
|
+
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
94
|
+
);
|
|
95
|
+
INSERT INTO schema_version(version) VALUES ('10.0');
|
|
96
|
+
`);
|
|
97
|
+
} finally {
|
|
98
|
+
db.close();
|
|
99
|
+
}
|
|
100
|
+
fs.writeFileSync(path.join(rootDir, '.ultra', 'workflow-state.json'), JSON.stringify({
|
|
101
|
+
command: 'ultra-dev', task_id: 'projection-task', status: 'active',
|
|
102
|
+
}));
|
|
103
|
+
try {
|
|
104
|
+
const breadcrumb = readProjectBreadcrumb(rootDir);
|
|
105
|
+
assert.equal(breadcrumb.readiness, 'blocked');
|
|
106
|
+
assert.ok(breadcrumb.blockers.includes('STATE_SCHEMA_MIGRATION_REQUIRED:10.0'));
|
|
107
|
+
assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
|
|
108
|
+
assert.doesNotMatch(JSON.stringify(breadcrumb), /projection-task|ultra-dev/);
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
@@ -23,7 +23,7 @@ function fixture() {
|
|
|
23
23
|
db.prepare(
|
|
24
24
|
`INSERT INTO baselines
|
|
25
25
|
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
26
|
-
VALUES ('test-baseline', 'fixture', '
|
|
26
|
+
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
27
27
|
).run(new Date().toISOString());
|
|
28
28
|
changes.createChange(db, {
|
|
29
29
|
id: 'learning-change', title: 'Learning change', kind: 'quick',
|
|
@@ -51,11 +51,17 @@ function assertStateAuthority(db, rootDir) {
|
|
|
51
51
|
projection_path: projection.path,
|
|
52
52
|
projection_version: projection.version,
|
|
53
53
|
};
|
|
54
|
-
|
|
54
|
+
const migrationTargets = {
|
|
55
|
+
'4.4': '4.5',
|
|
56
|
+
'4.5': '12.0',
|
|
57
|
+
};
|
|
58
|
+
const migrationTarget = migrationTargets[projection.version];
|
|
59
|
+
if (migrationTarget) {
|
|
55
60
|
throw new StateAuthorityError(
|
|
56
61
|
'LEGACY_STATE_MIGRATION_REQUIRED',
|
|
57
|
-
`state.db has no tasks but ${projection.path} contains ${projection.taskCount}
|
|
58
|
-
+
|
|
62
|
+
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} v${projection.version} tasks; `
|
|
63
|
+
+ `run ultra-tools migrate --from=${projection.version} --to=${migrationTarget} `
|
|
64
|
+
+ '--source-dir <project-root> before using Ultra MCP tools',
|
|
59
65
|
details,
|
|
60
66
|
);
|
|
61
67
|
}
|