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.
Files changed (63) hide show
  1. package/.ultra-template/specs/architecture.md +54 -223
  2. package/.ultra-template/specs/discovery.md +34 -231
  3. package/.ultra-template/specs/product.md +44 -200
  4. package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
  5. package/.ultra-template/tasks/tasks.json +6 -0
  6. package/CHANGELOG.md +42 -1
  7. package/README.md +21 -9
  8. package/adapters/_shared/codex-assets.cjs +1 -1
  9. package/adapters/_shared/handbook.cjs +4 -3
  10. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  11. package/adapters/opencode.js +8 -2
  12. package/adapters/tests/codex.test.cjs +2 -2
  13. package/adapters/tests/kimi-hook.test.cjs +1 -1
  14. package/adapters/tests/opencode.test.cjs +36 -1
  15. package/commands/ultra-init.md +3 -3
  16. package/docs/AGENT-CONTEXT.md +16 -8
  17. package/docs/ARCHITECTURE.md +32 -5
  18. package/docs/PLAN.zh-CN.md +7 -5
  19. package/docs/ROADMAP.md +4 -2
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  21. package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
  22. package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
  23. package/hooks/README.md +4 -3
  24. package/hooks/adapters/codex.py +1 -1
  25. package/hooks/adapters/kimi.py +1 -1
  26. package/hooks/context_spine.py +120 -16
  27. package/hooks/health_check.py +11 -0
  28. package/hooks/pre_stop_check.py +6 -5
  29. package/mcp-server/lib/baseline-workflow.cjs +415 -0
  30. package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
  31. package/mcp-server/lib/change-workflow.cjs +24 -27
  32. package/mcp-server/lib/change-workflow.test.cjs +202 -6
  33. package/mcp-server/lib/context-spine.cjs +74 -9
  34. package/mcp-server/lib/context-spine.test.cjs +15 -0
  35. package/mcp-server/lib/doctor.cjs +14 -0
  36. package/mcp-server/lib/doctor.test.cjs +25 -1
  37. package/mcp-server/lib/init-project.cjs +49 -14
  38. package/mcp-server/lib/provider-refs.cjs +42 -0
  39. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  40. package/mcp-server/lib/state-db.cjs +63 -4
  41. package/mcp-server/lib/state-db.test.cjs +8 -3
  42. package/mcp-server/server.cjs +48 -3
  43. package/mcp-server/tests/init-project.test.cjs +54 -8
  44. package/mcp-server/tests/server-contract.test.cjs +99 -2
  45. package/mcp-server/tests/state-db.test.cjs +11 -2
  46. package/package.json +1 -1
  47. package/skills/ultra-change/SKILL.md +24 -13
  48. package/skills/ultra-deliver/SKILL.md +15 -5
  49. package/skills/ultra-dev/SKILL.md +4 -0
  50. package/skills/ultra-doctor/SKILL.md +8 -2
  51. package/skills/ultra-init/SKILL.md +46 -50
  52. package/skills/ultra-init/references/brownfield-adoption.md +65 -0
  53. package/skills/ultra-plan/SKILL.md +9 -2
  54. package/skills/ultra-research/SKILL.md +10 -5
  55. package/skills/ultra-status/SKILL.md +13 -6
  56. package/spec/cli-protocol.md +4 -0
  57. package/spec/mcp-tools.yaml +223 -4
  58. package/spec/schemas/mcp-tools.schema.json +1 -1
  59. package/spec/schemas/state-db.sql +33 -2
  60. package/ultra-tools/commands/status.cjs +20 -2
  61. package/ultra-tools/commands/task.cjs +4 -1
  62. package/ultra-tools/status-command.test.cjs +20 -0
  63. package/ultra-tools/system-command.test.cjs +5 -0
@@ -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, ...CHANGE_TOOLS, ...SYSTEM_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 mutates the filesystem of an unrelated project, not state.db —
94
- // do NOT run the projector (it would overwrite the freshly-copied template).
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("SELECT version FROM schema_version WHERE version = '9.1'").get();
40
- assert.equal(version.version, '9.1');
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 injects project metadata into tasks.json', () => {
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.project.name, 'meta-demo');
60
- assert.equal(tasksJson.project.type, 'web');
61
- assert.equal(tasksJson.project.stack, 'next');
62
- assert.ok(tasksJson.created.length > 0);
63
- assert.ok(tasksJson.updated.length > 0);
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.project.name, 'mcp-init');
522
- assert.equal(tasksJson.project.type, 'cli');
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 in place with Context Spine state', () => {
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, '10.0');
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.11.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 `change.list` and `change.breadcrumb`.
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
- 2. Capture the observable outcome, acceptance, non-goals, affected public seam,
28
+ 3. Capture the observable outcome, acceptance, non-goals, affected public seam,
24
29
  documentation impact, and unresolved decisions.
25
- 3. Classify the change as `quick`, `standard`, `major`, or `incident`. Ask only for a
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
- 4. Call `change.create` with a stable kebab-case id, or `change.update` when resuming.
28
- 5. Write the minimum artifact set required by the classification:
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
- 6. Create the smallest executable vertical slice with `task.create`. Each task needs
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
- 7. Compile `change.context` for the next role and gate. Include only required
48
- references and the task execution contract. Treat missing references, digest or
49
- HEAD drift, an exceeded context budget, and unknown documentation impact as
50
- blockers.
51
- 8. Call `change.breadcrumb` and return its single next action.
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 from a bounded fresh
65
- context.
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` and bind exactly one relevant change, or a null change id for an
14
- initial baseline.
15
- 2. Call `change.breadcrumb`. Block on stale context, incomplete linked tasks, or
16
- unresolved readiness.
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, incident identifiers, cursor
62
- lag, whether repair ran, the backup path, and each unresolved action with its owner.
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.