ultra-builder-pro-cli 0.11.0 → 0.12.1
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/specs/architecture.md +54 -223
- package/.ultra-template/specs/discovery.md +34 -231
- package/.ultra-template/specs/product.md +44 -200
- package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
- package/.ultra-template/tasks/tasks.json +6 -0
- package/CHANGELOG.md +42 -1
- package/README.md +21 -9
- package/adapters/_shared/codex-assets.cjs +1 -1
- package/adapters/_shared/handbook.cjs +4 -3
- package/adapters/_shared/tests/handbook.test.cjs +4 -0
- package/adapters/opencode.js +8 -2
- package/adapters/tests/codex.test.cjs +2 -2
- package/adapters/tests/kimi-hook.test.cjs +1 -1
- package/adapters/tests/opencode.test.cjs +36 -1
- package/commands/ultra-init.md +3 -3
- package/docs/AGENT-CONTEXT.md +16 -8
- package/docs/ARCHITECTURE.md +32 -5
- package/docs/PLAN.zh-CN.md +7 -5
- package/docs/ROADMAP.md +4 -2
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
- package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
- package/hooks/README.md +4 -3
- package/hooks/adapters/codex.py +1 -1
- package/hooks/adapters/kimi.py +1 -1
- package/hooks/context_spine.py +120 -16
- package/hooks/health_check.py +11 -0
- package/hooks/pre_stop_check.py +6 -5
- package/mcp-server/lib/baseline-workflow.cjs +415 -0
- package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
- package/mcp-server/lib/change-workflow.cjs +24 -27
- package/mcp-server/lib/change-workflow.test.cjs +202 -6
- package/mcp-server/lib/context-spine.cjs +74 -9
- package/mcp-server/lib/context-spine.test.cjs +15 -0
- package/mcp-server/lib/doctor.cjs +14 -0
- package/mcp-server/lib/doctor.test.cjs +25 -1
- package/mcp-server/lib/init-project.cjs +49 -14
- package/mcp-server/lib/provider-refs.cjs +42 -0
- package/mcp-server/lib/spec-learning.test.cjs +5 -0
- package/mcp-server/lib/state-db.cjs +63 -4
- package/mcp-server/lib/state-db.test.cjs +8 -3
- package/mcp-server/server.cjs +48 -3
- package/mcp-server/tests/init-project.test.cjs +54 -8
- package/mcp-server/tests/server-contract.test.cjs +99 -2
- package/mcp-server/tests/state-db.test.cjs +11 -2
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +24 -13
- package/skills/ultra-deliver/SKILL.md +15 -5
- package/skills/ultra-dev/SKILL.md +4 -0
- package/skills/ultra-doctor/SKILL.md +8 -2
- package/skills/ultra-init/SKILL.md +46 -50
- package/skills/ultra-init/references/brownfield-adoption.md +65 -0
- package/skills/ultra-plan/SKILL.md +9 -2
- package/skills/ultra-research/SKILL.md +10 -5
- package/skills/ultra-status/SKILL.md +13 -6
- package/spec/cli-protocol.md +4 -0
- package/spec/mcp-tools.yaml +223 -4
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +33 -2
- package/ultra-tools/commands/status.cjs +20 -2
- package/ultra-tools/commands/task.cjs +4 -1
- package/ultra-tools/status-command.test.cjs +20 -0
- package/ultra-tools/system-command.test.cjs +5 -0
package/mcp-server/server.cjs
CHANGED
|
@@ -28,6 +28,7 @@ const parser = require('./lib/prd-parser.cjs');
|
|
|
28
28
|
const expander = require('./lib/task-expander.cjs');
|
|
29
29
|
const planStore = require('./lib/plan-store.cjs');
|
|
30
30
|
const { initProject } = require('./lib/init-project.cjs');
|
|
31
|
+
const baselines = require('./lib/baseline-workflow.cjs');
|
|
31
32
|
const changes = require('./lib/change-workflow.cjs');
|
|
32
33
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
33
34
|
const doctor = require('./lib/doctor.cjs');
|
|
@@ -67,6 +68,13 @@ const PLAN_TOOLS = Object.freeze([
|
|
|
67
68
|
'plan.get',
|
|
68
69
|
]);
|
|
69
70
|
|
|
71
|
+
const BASELINE_TOOLS = Object.freeze([
|
|
72
|
+
'baseline.start',
|
|
73
|
+
'baseline.record',
|
|
74
|
+
'baseline.get',
|
|
75
|
+
'baseline.converge',
|
|
76
|
+
]);
|
|
77
|
+
|
|
70
78
|
const CHANGE_TOOLS = Object.freeze([
|
|
71
79
|
'change.create',
|
|
72
80
|
'change.update',
|
|
@@ -85,18 +93,20 @@ const SYSTEM_TOOLS = Object.freeze([
|
|
|
85
93
|
]);
|
|
86
94
|
|
|
87
95
|
const REGISTERED_TOOLS = Object.freeze([
|
|
88
|
-
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...
|
|
96
|
+
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
97
|
+
...SYSTEM_TOOLS,
|
|
89
98
|
]);
|
|
90
99
|
|
|
91
100
|
const STATELESS_TOOLS = new Set(['task.init_project']);
|
|
92
101
|
|
|
93
|
-
// init_project
|
|
94
|
-
//
|
|
102
|
+
// init_project owns its target project's schema, baseline seed, and first
|
|
103
|
+
// projection internally. Do not enqueue a second projection on the caller DB.
|
|
95
104
|
const MUTATING_TOOLS = new Set([
|
|
96
105
|
'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
|
|
97
106
|
'task.parse_prd', 'task.expand',
|
|
98
107
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
99
108
|
'plan.export',
|
|
109
|
+
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
100
110
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
101
111
|
'change.learning_propose', 'change.learning_resolve',
|
|
102
112
|
]);
|
|
@@ -352,6 +362,39 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
352
362
|
}
|
|
353
363
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
354
364
|
}
|
|
365
|
+
case 'baseline.start': {
|
|
366
|
+
return {
|
|
367
|
+
baseline: baselines.startBaseline(
|
|
368
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case 'baseline.record': {
|
|
373
|
+
return {
|
|
374
|
+
baseline: baselines.recordBaseline(
|
|
375
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case 'baseline.get': {
|
|
380
|
+
const baseline = baselines.readBaseline(db, input.id);
|
|
381
|
+
if (!baseline) {
|
|
382
|
+
const err = new Error(input.id ? `baseline ${input.id} not found` : 'current baseline not found');
|
|
383
|
+
err.code = 'BASELINE_NOT_FOUND';
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
baseline,
|
|
388
|
+
health: baselines.inspectBaseline(db, {
|
|
389
|
+
rootDir: ctx.rootDir || process.cwd(), id: input.id,
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
case 'baseline.converge': {
|
|
394
|
+
return baselines.convergeBaseline(
|
|
395
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
355
398
|
case 'change.create': {
|
|
356
399
|
const { randomUUID } = require('node:crypto');
|
|
357
400
|
const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
|
|
@@ -598,6 +641,8 @@ module.exports = {
|
|
|
598
641
|
dispatchTool,
|
|
599
642
|
TASK_TOOLS,
|
|
600
643
|
SESSION_TOOLS,
|
|
644
|
+
PLAN_TOOLS,
|
|
645
|
+
BASELINE_TOOLS,
|
|
601
646
|
CHANGE_TOOLS,
|
|
602
647
|
SYSTEM_TOOLS,
|
|
603
648
|
REGISTERED_TOOLS,
|
|
@@ -5,9 +5,11 @@ const assert = require('node:assert/strict');
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
8
9
|
const Database = require('better-sqlite3');
|
|
9
10
|
|
|
10
11
|
const { initProject, InitProjectError, DEFAULT_TEMPLATE } = require('../lib/init-project.cjs');
|
|
12
|
+
const { EXPECTED_VERSION } = require('../lib/state-db.cjs');
|
|
11
13
|
|
|
12
14
|
function mkTempDir(prefix = 'ubp-init-test-') {
|
|
13
15
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -27,6 +29,7 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
27
29
|
stack: 'node',
|
|
28
30
|
});
|
|
29
31
|
assert.equal(r.status, 'created');
|
|
32
|
+
assert.equal(r.mode, 'greenfield');
|
|
30
33
|
assert.equal(r.created_path, path.join(target, '.ultra'));
|
|
31
34
|
assert.ok(r.copied_files.includes('tasks/tasks.json'));
|
|
32
35
|
assert.ok(r.copied_files.includes('specs/product.md'));
|
|
@@ -36,9 +39,11 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
36
39
|
assert.ok(fs.existsSync(r.state_db_path));
|
|
37
40
|
const db = new Database(r.state_db_path, { readonly: true });
|
|
38
41
|
try {
|
|
39
|
-
const version = db.prepare(
|
|
40
|
-
assert.equal(version.version,
|
|
42
|
+
const version = db.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
|
|
43
|
+
assert.equal(version.version, EXPECTED_VERSION);
|
|
41
44
|
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
|
|
45
|
+
const baseline = db.prepare('SELECT mode, status, project_name FROM baselines').get();
|
|
46
|
+
assert.deepEqual(baseline, { mode: 'greenfield', status: 'draft', project_name: 'demo' });
|
|
42
47
|
} finally {
|
|
43
48
|
db.close();
|
|
44
49
|
}
|
|
@@ -46,7 +51,42 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
46
51
|
} finally { cleanup(target); }
|
|
47
52
|
});
|
|
48
53
|
|
|
49
|
-
test('initProject
|
|
54
|
+
test('initProject auto-detects existing source as brownfield and starts adoption', () => {
|
|
55
|
+
const target = mkTempDir();
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(path.join(target, 'src'));
|
|
58
|
+
fs.writeFileSync(path.join(target, 'package.json'), '{"name":"legacy-app"}\n');
|
|
59
|
+
fs.writeFileSync(path.join(target, 'src', 'index.js'), 'module.exports = true;\n');
|
|
60
|
+
const result = initProject({ target_dir: target, project_name: 'legacy-app' });
|
|
61
|
+
assert.equal(result.mode, 'brownfield');
|
|
62
|
+
const db = new Database(result.state_db_path, { readonly: true });
|
|
63
|
+
try {
|
|
64
|
+
const baseline = db.prepare('SELECT mode, status, scope_json FROM baselines').get();
|
|
65
|
+
assert.equal(baseline.mode, 'brownfield');
|
|
66
|
+
assert.equal(baseline.status, 'adopting');
|
|
67
|
+
assert.deepEqual(JSON.parse(baseline.scope_json), ['.']);
|
|
68
|
+
} finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
} finally { cleanup(target); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('maintenance CLI forwards an explicit initialization mode', () => {
|
|
75
|
+
const target = mkTempDir();
|
|
76
|
+
try {
|
|
77
|
+
fs.writeFileSync(path.join(target, 'README.md'), '# Intentional greenfield notes\n');
|
|
78
|
+
const output = execFileSync(process.execPath, [
|
|
79
|
+
path.resolve(__dirname, '..', '..', 'ultra-tools', 'cli.cjs'),
|
|
80
|
+
'task', 'init-project', '--target-dir', target, '--project-name', 'explicit-mode',
|
|
81
|
+
'--mode', 'greenfield',
|
|
82
|
+
], { encoding: 'utf8' });
|
|
83
|
+
const envelope = JSON.parse(output);
|
|
84
|
+
assert.equal(envelope.ok, true);
|
|
85
|
+
assert.equal(envelope.data.mode, 'greenfield');
|
|
86
|
+
} finally { cleanup(target); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('initProject keeps project metadata in authoritative baseline state and tasks.json as a projection', () => {
|
|
50
90
|
const target = mkTempDir();
|
|
51
91
|
try {
|
|
52
92
|
initProject({
|
|
@@ -56,11 +96,17 @@ test('initProject injects project metadata into tasks.json', () => {
|
|
|
56
96
|
stack: 'next',
|
|
57
97
|
});
|
|
58
98
|
const tasksJson = JSON.parse(fs.readFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
59
|
-
assert.equal(tasksJson.
|
|
60
|
-
assert.equal(tasksJson.
|
|
61
|
-
assert.
|
|
62
|
-
assert.
|
|
63
|
-
|
|
99
|
+
assert.equal(tasksJson.schema_version, '4.5');
|
|
100
|
+
assert.equal(tasksJson.source, '.ultra/state.db');
|
|
101
|
+
assert.deepEqual(tasksJson.tasks, []);
|
|
102
|
+
assert.equal('project' in tasksJson, false);
|
|
103
|
+
const db = new Database(path.join(target, '.ultra', 'state.db'), { readonly: true });
|
|
104
|
+
try {
|
|
105
|
+
const baseline = db.prepare('SELECT project_name, project_type, stack FROM baselines').get();
|
|
106
|
+
assert.deepEqual(baseline, { project_name: 'meta-demo', project_type: 'web', stack: 'next' });
|
|
107
|
+
} finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
64
110
|
} finally { cleanup(target); }
|
|
65
111
|
});
|
|
66
112
|
|
|
@@ -14,12 +14,23 @@ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio
|
|
|
14
14
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
15
15
|
const SERVER = path.join(REPO_ROOT, 'mcp-server', 'server.cjs');
|
|
16
16
|
const PACKAGE_VERSION = require(path.join(REPO_ROOT, 'package.json')).version;
|
|
17
|
+
const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
17
18
|
|
|
18
19
|
function tmpProject() {
|
|
19
20
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mcp-'));
|
|
20
21
|
return { dir, dbPath: path.join(dir, '.ultra', 'state.db') };
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function seedMigratedBaseline(project) {
|
|
25
|
+
const { db } = initStateDb(project.dbPath);
|
|
26
|
+
db.prepare(
|
|
27
|
+
`INSERT OR IGNORE INTO baselines
|
|
28
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
29
|
+
VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
|
|
30
|
+
).run(new Date().toISOString());
|
|
31
|
+
closeStateDb(db);
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
async function withClient({ dir, dbPath }, fn, extraEnv = {}) {
|
|
24
35
|
const transport = new StdioClientTransport({
|
|
25
36
|
command: process.execPath,
|
|
@@ -74,6 +85,10 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
74
85
|
const list = await client.listTools();
|
|
75
86
|
const names = list.tools.map((t) => t.name).sort();
|
|
76
87
|
assert.deepEqual(names, [
|
|
88
|
+
'baseline.converge',
|
|
89
|
+
'baseline.get',
|
|
90
|
+
'baseline.record',
|
|
91
|
+
'baseline.start',
|
|
77
92
|
'change.archive',
|
|
78
93
|
'change.breadcrumb',
|
|
79
94
|
'change.context',
|
|
@@ -118,9 +133,78 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
118
133
|
}
|
|
119
134
|
});
|
|
120
135
|
|
|
136
|
+
test('baseline MCP tools adopt and converge an existing checkout without storing provider payloads', async () => {
|
|
137
|
+
const proj = tmpProject();
|
|
138
|
+
try {
|
|
139
|
+
fs.mkdirSync(path.join(proj.dir, '.ultra', 'specs'), { recursive: true });
|
|
140
|
+
fs.mkdirSync(path.join(proj.dir, 'src'), { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(proj.dir, '.ultra', 'specs', 'product.md'), '# Product\n\nObserved behavior.\n');
|
|
142
|
+
fs.writeFileSync(path.join(proj.dir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nCurrent boundary.\n');
|
|
143
|
+
fs.writeFileSync(path.join(proj.dir, 'src', 'index.js'), 'module.exports = true;\n');
|
|
144
|
+
require('node:child_process').execFileSync('git', ['init', '-q'], { cwd: proj.dir });
|
|
145
|
+
require('node:child_process').execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: proj.dir });
|
|
146
|
+
require('node:child_process').execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: proj.dir });
|
|
147
|
+
require('node:child_process').execFileSync('git', ['add', '.'], { cwd: proj.dir });
|
|
148
|
+
require('node:child_process').execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: proj.dir });
|
|
149
|
+
const revision = require('node:child_process').execFileSync(
|
|
150
|
+
'git', ['rev-parse', 'HEAD'], { cwd: proj.dir, encoding: 'utf8' },
|
|
151
|
+
).trim();
|
|
152
|
+
|
|
153
|
+
await withClient(proj, async (client) => {
|
|
154
|
+
const started = readToolPayload(await client.callTool({
|
|
155
|
+
name: 'baseline.start',
|
|
156
|
+
arguments: {
|
|
157
|
+
id: 'mcp-baseline', project_name: 'fixture', mode: 'brownfield',
|
|
158
|
+
repository_revision: revision, scope: ['.'],
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
assert.equal(started.baseline.status, 'adopting');
|
|
162
|
+
|
|
163
|
+
const recorded = readToolPayload(await client.callTool({
|
|
164
|
+
name: 'baseline.record',
|
|
165
|
+
arguments: {
|
|
166
|
+
id: 'mcp-baseline', repository_revision: revision,
|
|
167
|
+
spec_refs: [
|
|
168
|
+
{ kind: 'product', path: '.ultra/specs/product.md' },
|
|
169
|
+
{ kind: 'architecture', path: '.ultra/specs/architecture.md' },
|
|
170
|
+
],
|
|
171
|
+
evidence: [{ kind: 'source', ref: 'src/index.js', summary: 'Current entry point.' }],
|
|
172
|
+
verification: [{
|
|
173
|
+
name: 'smoke', command: 'node -e "require(\'./src\')"', status: 'pass', evidence: 'exit 0',
|
|
174
|
+
}],
|
|
175
|
+
unknowns: [],
|
|
176
|
+
provider_refs: {
|
|
177
|
+
code_graph: { provider: 'codebase-memory-mcp', project: 'fixture', status: 'fresh' },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
assert.match(recorded.baseline.spec_refs[0].digest, /^[0-9a-f]{64}$/);
|
|
182
|
+
|
|
183
|
+
const converged = readToolPayload(await client.callTool({
|
|
184
|
+
name: 'baseline.converge',
|
|
185
|
+
arguments: {
|
|
186
|
+
id: 'mcp-baseline', expected_revision: revision, approved_by: 'owner',
|
|
187
|
+
approval_note: 'The baseline matches the current checkout.', accept_known_red: false,
|
|
188
|
+
},
|
|
189
|
+
}));
|
|
190
|
+
assert.equal(converged.ready, true);
|
|
191
|
+
assert.equal(converged.baseline.status, 'ready');
|
|
192
|
+
|
|
193
|
+
const got = readToolPayload(await client.callTool({
|
|
194
|
+
name: 'baseline.get', arguments: {},
|
|
195
|
+
}));
|
|
196
|
+
assert.equal(got.baseline.id, 'mcp-baseline');
|
|
197
|
+
assert.equal(JSON.stringify(got.baseline).includes('captured prompt'), false);
|
|
198
|
+
});
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
121
204
|
test('change.create + change.context expose a continuous change unit with external provider references', async () => {
|
|
122
205
|
const proj = tmpProject();
|
|
123
206
|
try {
|
|
207
|
+
seedMigratedBaseline(proj);
|
|
124
208
|
fs.writeFileSync(path.join(proj.dir, 'README.md'), '# project\n');
|
|
125
209
|
await withClient(proj, async (client) => {
|
|
126
210
|
const created = await client.callTool({
|
|
@@ -172,6 +256,7 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
172
256
|
test('spec-learning MCP tools persist an approval-gated candidate and projection', async () => {
|
|
173
257
|
const proj = tmpProject();
|
|
174
258
|
try {
|
|
259
|
+
seedMigratedBaseline(proj);
|
|
175
260
|
await withClient(proj, async (client) => {
|
|
176
261
|
await client.callTool({
|
|
177
262
|
name: 'change.create',
|
|
@@ -233,6 +318,7 @@ test('spec-learning MCP tools persist an approval-gated candidate and projection
|
|
|
233
318
|
test('system.doctor returns structured provider ownership and projection health', async () => {
|
|
234
319
|
const proj = tmpProject();
|
|
235
320
|
try {
|
|
321
|
+
seedMigratedBaseline(proj);
|
|
236
322
|
await withClient(proj, async (client) => {
|
|
237
323
|
await client.callTool({
|
|
238
324
|
name: 'task.create',
|
|
@@ -515,11 +601,22 @@ test('task.init_project creates .ultra/ skeleton in a fresh target directory', a
|
|
|
515
601
|
assert.equal(payload.status, 'created');
|
|
516
602
|
assert.equal(payload.created_path, path.join(freshTarget, '.ultra'));
|
|
517
603
|
assert.equal(payload.state_db_path, path.join(freshTarget, '.ultra', 'state.db'));
|
|
604
|
+
assert.equal(payload.mode, 'greenfield');
|
|
605
|
+
assert.equal(payload.baseline.status, 'draft');
|
|
518
606
|
assert.ok(fs.existsSync(payload.state_db_path));
|
|
519
607
|
assert.ok(payload.copied_files.includes('tasks/tasks.json'));
|
|
520
608
|
const tasksJson = JSON.parse(fs.readFileSync(path.join(payload.created_path, 'tasks', 'tasks.json'), 'utf8'));
|
|
521
|
-
assert.equal(tasksJson.
|
|
522
|
-
assert.
|
|
609
|
+
assert.equal(tasksJson.source, '.ultra/state.db');
|
|
610
|
+
assert.deepEqual(tasksJson.tasks, []);
|
|
611
|
+
const state = new Database(payload.state_db_path, { readonly: true });
|
|
612
|
+
try {
|
|
613
|
+
assert.deepEqual(
|
|
614
|
+
state.prepare('SELECT project_name, project_type, mode FROM baselines').get(),
|
|
615
|
+
{ project_name: 'mcp-init', project_type: 'cli', mode: 'greenfield' },
|
|
616
|
+
);
|
|
617
|
+
} finally {
|
|
618
|
+
state.close();
|
|
619
|
+
}
|
|
523
620
|
});
|
|
524
621
|
} finally {
|
|
525
622
|
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
@@ -8,7 +8,7 @@ const path = require('node:path');
|
|
|
8
8
|
|
|
9
9
|
const stateDb = require('../lib/state-db.cjs');
|
|
10
10
|
|
|
11
|
-
test('initStateDb upgrades a pre-10.0 database
|
|
11
|
+
test('initStateDb upgrades a pre-10.0 database through Context Spine into baseline authority', () => {
|
|
12
12
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-upgrade-'));
|
|
13
13
|
const file = path.join(dir, 'state.db');
|
|
14
14
|
try {
|
|
@@ -59,7 +59,7 @@ test('initStateDb upgrades a pre-10.0 database in place with Context Spine state
|
|
|
59
59
|
assert.ok(columns.includes('change_id'));
|
|
60
60
|
const eventColumns = upgraded.db.prepare('PRAGMA table_info(events)').all().map((row) => row.name);
|
|
61
61
|
assert.ok(eventColumns.includes('change_id'));
|
|
62
|
-
assert.equal(upgraded.schema_version, '
|
|
62
|
+
assert.equal(upgraded.schema_version, '11.0');
|
|
63
63
|
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '10.0'").get());
|
|
64
64
|
const contextColumns = upgraded.db.prepare('PRAGMA table_info(context_snapshots)').all()
|
|
65
65
|
.map((row) => row.name);
|
|
@@ -69,12 +69,21 @@ test('initStateDb upgrades a pre-10.0 database in place with Context Spine state
|
|
|
69
69
|
assert.ok(upgraded.db.prepare(
|
|
70
70
|
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'spec_learning_candidates'",
|
|
71
71
|
).get());
|
|
72
|
+
const baseline = upgraded.db.prepare(
|
|
73
|
+
"SELECT mode, status FROM baselines WHERE id = 'migrated-baseline'",
|
|
74
|
+
).get();
|
|
75
|
+
assert.deepEqual(baseline, { mode: 'migrated', status: 'ready' });
|
|
72
76
|
const contextMigration = upgraded.db.prepare(
|
|
73
77
|
"SELECT from_version, to_version, notes FROM migration_history WHERE to_version = '10.0' ORDER BY id DESC LIMIT 1",
|
|
74
78
|
).get();
|
|
75
79
|
assert.equal(contextMigration.from_version, '9.1');
|
|
76
80
|
assert.equal(contextMigration.to_version, '10.0');
|
|
77
81
|
assert.match(contextMigration.notes, /Context Spine/);
|
|
82
|
+
const baselineMigration = upgraded.db.prepare(
|
|
83
|
+
"SELECT from_version, to_version, notes FROM migration_history WHERE to_version = '11.0' ORDER BY id DESC LIMIT 1",
|
|
84
|
+
).get();
|
|
85
|
+
assert.equal(baselineMigration.from_version, '10.0');
|
|
86
|
+
assert.match(baselineMigration.notes, /baseline adoption/i);
|
|
78
87
|
assert.ok(upgraded.db.prepare(
|
|
79
88
|
"SELECT 1 FROM migration_history WHERE from_version = '8A.1' AND to_version = '9.1' AND notes LIKE '%Kimi%'",
|
|
80
89
|
).get());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
@@ -16,16 +16,21 @@ own their payloads; Ultra stores only provider references supplied by the user o
|
|
|
16
16
|
|
|
17
17
|
## Workflow
|
|
18
18
|
|
|
19
|
-
1. Call `
|
|
19
|
+
1. Call `baseline.get`. State integrity failure routes to `ultra-doctor`. When no
|
|
20
|
+
change is active, a missing, incomplete, or stale baseline routes to `ultra-init`.
|
|
21
|
+
An urgent or already active bounded change may proceed with that condition recorded
|
|
22
|
+
as a warning. `change.converge` requires an approved `ready` baseline; repository
|
|
23
|
+
revision and tracked-spec drift created by the change are reconciled at archive.
|
|
24
|
+
2. Call `change.list` and `change.breadcrumb`.
|
|
20
25
|
- Resume the single change that matches the requested outcome.
|
|
21
26
|
- Require an explicit id when several changes could match.
|
|
22
27
|
- Create a new change only when no existing packet represents the same outcome.
|
|
23
|
-
|
|
28
|
+
3. Capture the observable outcome, acceptance, non-goals, affected public seam,
|
|
24
29
|
documentation impact, and unresolved decisions.
|
|
25
|
-
|
|
30
|
+
4. Classify the change as `quick`, `standard`, `major`, or `incident`. Ask only for a
|
|
26
31
|
material product, risk, or recovery decision that repository evidence cannot answer.
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
5. Call `change.create` with a stable kebab-case id, or `change.update` when resuming.
|
|
33
|
+
6. Write the minimum artifact set required by the classification:
|
|
29
34
|
|
|
30
35
|
```text
|
|
31
36
|
.ultra/changes/active/<change-id>/
|
|
@@ -41,14 +46,17 @@ own their payloads; Ultra stores only provider references supplied by the user o
|
|
|
41
46
|
Delta files describe differences from the baseline, not a second copy of it.
|
|
42
47
|
Incident diagnosis records the symptom, earliest bad state, falsifiable
|
|
43
48
|
hypotheses, evidence, root cause, and recovery boundary.
|
|
44
|
-
|
|
49
|
+
7. Create the smallest executable vertical slice with `task.create`. Each task needs
|
|
45
50
|
one outcome, bounded ownership, dependencies, a live public seam, an exact
|
|
46
51
|
verification command, and documentation impact.
|
|
47
|
-
|
|
48
|
-
references and the task execution contract.
|
|
49
|
-
HEAD drift, an
|
|
50
|
-
blockers.
|
|
51
|
-
|
|
52
|
+
8. Compile `change.context` for the next role and gate. Include only required
|
|
53
|
+
references and the task execution contract. Missing required references, digest or
|
|
54
|
+
HEAD drift, an absent public seam or verification command, and unknown
|
|
55
|
+
documentation impact are blockers. File count, token estimate, and context-share
|
|
56
|
+
thresholds are advisory warnings: narrow reads or split the slice when useful, but
|
|
57
|
+
never raise a threshold merely to make the warning disappear or refuse necessary
|
|
58
|
+
incident context.
|
|
59
|
+
9. Call `change.breadcrumb` and return its single next action.
|
|
52
60
|
|
|
53
61
|
## Specification learning
|
|
54
62
|
|
|
@@ -61,5 +69,8 @@ verified.
|
|
|
61
69
|
## Completion gate
|
|
62
70
|
|
|
63
71
|
Do not claim alignment while a material decision is unresolved, documentation impact
|
|
64
|
-
is unknown, context is stale, or the next task is not executable
|
|
65
|
-
|
|
72
|
+
is unknown, context is stale, or the next task is not executable. `change.converge`
|
|
73
|
+
also requires a `ready` baseline, so incomplete adoption must be resolved before
|
|
74
|
+
convergence. Normal HEAD or tracked-spec drift caused by the active change is not a
|
|
75
|
+
convergence blocker. Declare every baseline update at archive; archive fails and rolls
|
|
76
|
+
back when revision or digest health remains stale after reconciliation.
|
|
@@ -10,10 +10,12 @@ review, and delivery reports are evidence artifacts.
|
|
|
10
10
|
|
|
11
11
|
## Bind current evidence
|
|
12
12
|
|
|
13
|
-
1. Call `change.list
|
|
14
|
-
initial
|
|
15
|
-
2. Call `change.breadcrumb`.
|
|
16
|
-
|
|
13
|
+
1. Call `baseline.get`, then `change.list`. Bind exactly one relevant change, or the
|
|
14
|
+
current draft/adopting baseline when this is the initial delivery.
|
|
15
|
+
2. Call `change.breadcrumb`. Context-size warnings are advisory, but delivery blocks
|
|
16
|
+
on incomplete baseline adoption, stale execution context, incomplete linked tasks,
|
|
17
|
+
or unresolved readiness. HEAD and tracked-spec drift caused by the current change
|
|
18
|
+
remain visible warnings until archive reconciliation.
|
|
17
19
|
3. Require a passing test report for the current full HEAD and at least one declared
|
|
18
20
|
public seam.
|
|
19
21
|
4. Require current independent `spec_fidelity` and `engineering_standards` review
|
|
@@ -33,6 +35,10 @@ Reconcile accepted delta content and documentation impact into baseline document
|
|
|
33
35
|
Record updated paths or a specific no-change reason. Unknown documentation impact,
|
|
34
36
|
unresolved placeholders, and behavior that exists only in the delta block convergence.
|
|
35
37
|
|
|
38
|
+
For an initial baseline without a change, call `baseline.record`, obtain explicit owner
|
|
39
|
+
approval, and call `baseline.converge`. Do not simulate initial delivery through a null
|
|
40
|
+
change record.
|
|
41
|
+
|
|
36
42
|
## Verify the release candidate
|
|
37
43
|
|
|
38
44
|
Use a clean, reviewed release commit. Show any remaining diff and obtain explicit
|
|
@@ -53,7 +59,11 @@ delta, documentation, both independent review axes, and incident diagnosis when
|
|
|
53
59
|
applicable. Do not collapse review axes or replace exact signal fields with prose.
|
|
54
60
|
|
|
55
61
|
Resolve named blockers, rerun invalidated gates, then call `change.archive` with the
|
|
56
|
-
summary and baseline updates.
|
|
62
|
+
summary and baseline updates. Archive reconciliation refreshes the authoritative
|
|
63
|
+
baseline revision and digests. The archive transaction rechecks baseline health and
|
|
64
|
+
rolls back the state row, artifacts, and reconciliation events if any tracked digest
|
|
65
|
+
or repository revision remains stale. Verify the result with `baseline.get` before
|
|
66
|
+
release.
|
|
57
67
|
|
|
58
68
|
## Optional release
|
|
59
69
|
|
|
@@ -15,6 +15,10 @@ projected status fields. If task MCP access fails, stop and route state health t
|
|
|
15
15
|
|
|
16
16
|
Read `change.breadcrumb` when a change is active. Stale context, blocked readiness,
|
|
17
17
|
unknown documentation impact, or a HEAD mismatch blocks implementation.
|
|
18
|
+
Baseline-adoption and context-size warnings do not block an active slice. Address
|
|
19
|
+
them when practical without dropping required evidence or arbitrarily changing a
|
|
20
|
+
threshold. Approved baseline availability is enforced at convergence; revision and
|
|
21
|
+
tracked-spec health are reconciled and verified at archive.
|
|
18
22
|
|
|
19
23
|
## Workflow
|
|
20
24
|
|
|
@@ -17,6 +17,7 @@ Call `system.doctor` with `{ "repair": false }` and inspect:
|
|
|
17
17
|
- failed or interrupted projection jobs;
|
|
18
18
|
- orphan sessions;
|
|
19
19
|
- missing active-change artifact roots;
|
|
20
|
+
- missing, incomplete, or stale project baseline readiness and specification digests;
|
|
20
21
|
- the boundary to separately installed memory and code-graph providers.
|
|
21
22
|
|
|
22
23
|
Do not infer health from generated task JSON. Do not mutate external provider state,
|
|
@@ -31,6 +32,10 @@ delete artifacts, or collect memory during diagnosis.
|
|
|
31
32
|
- An orphan session may be reconciled; never kill unrelated processes blindly.
|
|
32
33
|
- A missing change artifact requires version-control, backup, or deliberate recreation
|
|
33
34
|
evidence rather than silent regeneration.
|
|
35
|
+
- Missing or incomplete baseline evidence routes to `ultra-init`. Doctor repair does
|
|
36
|
+
not approve a baseline, accept known-red verification, or invent adoption evidence.
|
|
37
|
+
Baseline readiness is an advisory workflow check, not state-database corruption and
|
|
38
|
+
not a reason for doctor repair.
|
|
34
39
|
|
|
35
40
|
## Explicit repair
|
|
36
41
|
|
|
@@ -58,8 +63,9 @@ host wiring. Neither substitutes for the other.
|
|
|
58
63
|
|
|
59
64
|
## Output
|
|
60
65
|
|
|
61
|
-
Lead with `healthy` or `degraded`. List failing checks
|
|
62
|
-
lag, whether repair ran, the backup
|
|
66
|
+
Lead with `healthy` or `degraded`. List failing authority checks separately from
|
|
67
|
+
baseline warnings, incident identifiers, cursor lag, whether repair ran, the backup
|
|
68
|
+
path, and each unresolved action with its owner.
|
|
63
69
|
|
|
64
70
|
Do not fix application code, delete state, reset Git, close healthy sessions, or
|
|
65
71
|
install and repair external providers through this workflow.
|