ultra-builder-pro-cli 0.10.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 (137) 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 +86 -1
  7. package/README.md +21 -9
  8. package/adapters/_shared/codex-assets.cjs +24 -342
  9. package/adapters/_shared/handbook.cjs +4 -3
  10. package/adapters/_shared/runtime-assets.cjs +24 -0
  11. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  12. package/adapters/_shared/tests/runtime-assets.test.cjs +11 -4
  13. package/adapters/claude.js +28 -5
  14. package/adapters/kimi.js +15 -138
  15. package/adapters/opencode.js +22 -49
  16. package/adapters/tests/claude.test.cjs +22 -8
  17. package/adapters/tests/codex.test.cjs +21 -8
  18. package/adapters/tests/kimi-hook.test.cjs +1 -1
  19. package/adapters/tests/kimi.test.cjs +11 -11
  20. package/adapters/tests/opencode.test.cjs +47 -8
  21. package/agents/code-reviewer.md +24 -85
  22. package/agents/debugger.md +30 -132
  23. package/agents/review-code.md +24 -104
  24. package/agents/review-comments.md +20 -83
  25. package/agents/review-coordinator.md +40 -137
  26. package/agents/review-design.md +21 -118
  27. package/agents/review-errors.md +22 -70
  28. package/agents/review-spec.md +36 -0
  29. package/agents/review-tests.md +20 -76
  30. package/agents/tdd-runner.md +26 -84
  31. package/commands/learn.md +5 -36
  32. package/commands/ultra-change.md +5 -43
  33. package/commands/ultra-deliver.md +4 -46
  34. package/commands/ultra-dev.md +4 -48
  35. package/commands/ultra-doctor.md +4 -30
  36. package/commands/ultra-init.md +5 -47
  37. package/commands/ultra-plan.md +5 -48
  38. package/commands/ultra-research.md +5 -54
  39. package/commands/ultra-status.md +5 -44
  40. package/commands/ultra-test.md +5 -45
  41. package/commands/ultra-think.md +5 -40
  42. package/docs/AGENT-CONTEXT.md +16 -8
  43. package/docs/ARCHITECTURE.md +32 -5
  44. package/docs/PLAN.zh-CN.md +8 -6
  45. package/docs/ROADMAP.md +5 -3
  46. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  47. package/docs/SKILL-AUTHORING.md +75 -0
  48. package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
  49. package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
  50. package/hooks/README.md +4 -3
  51. package/hooks/adapters/codex.py +1 -1
  52. package/hooks/adapters/kimi.py +1 -1
  53. package/hooks/context_spine.py +120 -16
  54. package/hooks/health_check.py +11 -0
  55. package/hooks/pre_stop_check.py +6 -5
  56. package/mcp-server/lib/baseline-workflow.cjs +415 -0
  57. package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
  58. package/mcp-server/lib/change-workflow.cjs +24 -27
  59. package/mcp-server/lib/change-workflow.test.cjs +202 -6
  60. package/mcp-server/lib/context-spine.cjs +74 -9
  61. package/mcp-server/lib/context-spine.test.cjs +15 -0
  62. package/mcp-server/lib/doctor.cjs +14 -0
  63. package/mcp-server/lib/doctor.test.cjs +25 -1
  64. package/mcp-server/lib/init-project.cjs +49 -14
  65. package/mcp-server/lib/provider-refs.cjs +42 -0
  66. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  67. package/mcp-server/lib/state-db.cjs +63 -4
  68. package/mcp-server/lib/state-db.test.cjs +8 -3
  69. package/mcp-server/server.cjs +48 -3
  70. package/mcp-server/tests/init-project.test.cjs +54 -8
  71. package/mcp-server/tests/server-contract.test.cjs +99 -2
  72. package/mcp-server/tests/state-db.test.cjs +11 -2
  73. package/package.json +3 -2
  74. package/skills/cc-collab/SKILL.md +16 -18
  75. package/skills/code-review-expert/SKILL.md +39 -114
  76. package/skills/code-review-expert/references/correctness-reliability.md +30 -0
  77. package/skills/code-review-expert/references/design-boundaries.md +26 -0
  78. package/skills/code-review-expert/references/removal-plan.md +17 -45
  79. package/skills/codex-collab/SKILL.md +23 -28
  80. package/skills/codex-collab/evals/evals.json +18 -23
  81. package/skills/integration-rules/SKILL.md +28 -67
  82. package/skills/learn/SKILL.md +46 -106
  83. package/skills/security-rules/SKILL.md +34 -56
  84. package/skills/testing-rules/SKILL.md +28 -52
  85. package/skills/ultra-change/SKILL.md +68 -114
  86. package/skills/ultra-deliver/SKILL.md +58 -86
  87. package/skills/ultra-dev/SKILL.md +46 -105
  88. package/skills/ultra-doctor/SKILL.md +45 -72
  89. package/skills/ultra-init/SKILL.md +62 -147
  90. package/skills/ultra-init/references/brownfield-adoption.md +65 -0
  91. package/skills/ultra-plan/SKILL.md +56 -89
  92. package/skills/ultra-research/SKILL.md +64 -152
  93. package/skills/ultra-research/references/architecture.md +38 -0
  94. package/skills/ultra-research/references/problem-product.md +39 -0
  95. package/skills/ultra-research/references/quality-delivery.md +34 -0
  96. package/skills/ultra-research/references/synthesis.md +38 -0
  97. package/skills/ultra-research/references/users-scope.md +40 -0
  98. package/skills/ultra-review/SKILL.md +54 -98
  99. package/skills/ultra-review/references/unified-schema.md +76 -101
  100. package/skills/ultra-review/scripts/review_wait.py +242 -77
  101. package/skills/ultra-status/SKILL.md +41 -56
  102. package/skills/ultra-test/SKILL.md +37 -72
  103. package/skills/ultra-think/SKILL.md +43 -102
  104. package/skills/ultra-verify/SKILL.md +44 -84
  105. package/skills/ultra-verify/evals/evals.json +24 -27
  106. package/spec/cli-protocol.md +4 -0
  107. package/spec/mcp-tools.yaml +223 -4
  108. package/spec/schemas/command-manifest.schema.json +7 -25
  109. package/spec/schemas/mcp-tools.schema.json +1 -1
  110. package/spec/schemas/skill-manifest.schema.json +9 -79
  111. package/spec/schemas/state-db.sql +33 -2
  112. package/ultra-tools/commands/status.cjs +20 -2
  113. package/ultra-tools/commands/task.cjs +4 -1
  114. package/ultra-tools/status-command.test.cjs +20 -0
  115. package/ultra-tools/system-command.test.cjs +5 -0
  116. package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
  117. package/skills/code-review-expert/references/integration-checklist.md +0 -46
  118. package/skills/code-review-expert/references/security-checklist.md +0 -100
  119. package/skills/code-review-expert/references/solid-checklist.md +0 -65
  120. package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
  121. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
  122. package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
  123. package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
  124. package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
  125. package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
  126. package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
  127. package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
  128. package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
  129. package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
  130. package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
  131. package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
  132. package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
  133. package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
  134. package/skills/ultra-research/steps/step-40-deployment.md +0 -123
  135. package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
  136. package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
  137. package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  // Implementation for MCP tool `task.init_project` and the CLI fallback
4
- // `ultra-tools init`. Copies the bundled templates/.ultra/ skeleton to the
5
- // target project root, injecting project metadata into tasks.json.
4
+ // `ultra-tools task init-project`. Copies the bundled templates/.ultra/ skeleton
5
+ // to the target project root and records project metadata in authoritative state.
6
6
  //
7
7
  // Error contract matches spec/mcp-tools.yaml#task.init_project:
8
8
  // ULTRA_DIR_EXISTS | TEMPLATE_MISSING | TARGET_NOT_DIR | IO_ERROR
@@ -15,6 +15,8 @@ const fs = require('node:fs');
15
15
  const path = require('node:path');
16
16
 
17
17
  const { initStateDb } = require('./state-db.cjs');
18
+ const baselines = require('./baseline-workflow.cjs');
19
+ const runtimeState = require('./runtime-state.cjs');
18
20
 
19
21
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
20
22
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
@@ -95,20 +97,30 @@ function copyTemplate(source, dest) {
95
97
  return files;
96
98
  }
97
99
 
98
- function injectTasksJson(ultraDir, { project_name, project_type, stack }) {
100
+ function validateTasksTemplate(ultraDir) {
99
101
  const tasksFile = path.join(ultraDir, 'tasks', 'tasks.json');
100
102
  if (!fs.existsSync(tasksFile)) return;
101
103
  let data;
102
104
  try { data = JSON.parse(fs.readFileSync(tasksFile, 'utf8')); }
103
105
  catch (err) { throw new InitProjectError('IO_ERROR', `tasks.json malformed: ${err.message}`, { cause: err }); }
104
- const nowIso = new Date().toISOString();
105
- data.created = nowIso;
106
- data.updated = nowIso;
107
- data.project = data.project || {};
108
- data.project.name = project_name;
109
- if (project_type) data.project.type = project_type;
110
- if (stack) data.project.stack = stack;
111
- fs.writeFileSync(tasksFile, JSON.stringify(data, null, 2) + '\n');
106
+ if (!data || typeof data !== 'object' || !Array.isArray(data.tasks)) {
107
+ throw new InitProjectError('IO_ERROR', 'tasks.json template must contain a tasks array');
108
+ }
109
+ }
110
+
111
+ const IGNORED_ADOPTION_ENTRIES = new Set([
112
+ '.DS_Store', '.git', '.gitignore', '.ultra', 'node_modules', 'vendor', '.venv', 'venv',
113
+ ]);
114
+
115
+ function detectProjectMode(absTarget, requested = 'auto') {
116
+ if (!['auto', 'greenfield', 'brownfield'].includes(requested)) {
117
+ throw new InitProjectError('VALIDATION_ERROR', `unsupported initialization mode: ${requested}`);
118
+ }
119
+ if (requested !== 'auto') return requested;
120
+ const meaningful = fs.readdirSync(absTarget, { withFileTypes: true })
121
+ .some((entry) => !IGNORED_ADOPTION_ENTRIES.has(entry.name)
122
+ && !entry.name.startsWith('.ultra.backup.'));
123
+ return meaningful ? 'brownfield' : 'greenfield';
112
124
  }
113
125
 
114
126
  function rollbackInitialization(ultraDir, backupPath) {
@@ -130,6 +142,7 @@ function initProject({
130
142
  project_name,
131
143
  project_type,
132
144
  stack,
145
+ mode = 'auto',
133
146
  overwrite = false,
134
147
  source_template,
135
148
  } = {}) {
@@ -139,6 +152,7 @@ function initProject({
139
152
 
140
153
  const absTarget = resolveTargetDir(target_dir);
141
154
  ensureTargetDir(absTarget);
155
+ const resolvedMode = detectProjectMode(absTarget, mode);
142
156
  const source = ensureTemplate(source_template);
143
157
  const ultraDir = path.join(absTarget, '.ultra');
144
158
 
@@ -155,13 +169,27 @@ function initProject({
155
169
  }
156
170
 
157
171
  let copiedFiles;
172
+ let baselineResult;
158
173
  try {
159
174
  fs.mkdirSync(ultraDir, { recursive: true });
160
175
  copiedFiles = copyTemplate(source, ultraDir);
161
- injectTasksJson(ultraDir, { project_name, project_type, stack });
176
+ validateTasksTemplate(ultraDir);
162
177
  const stateDbPath = path.join(ultraDir, 'state.db');
163
178
  const { db } = initStateDb(stateDbPath);
164
- db.close();
179
+ try {
180
+ baselineResult = baselines.startBaseline(db, {
181
+ id: 'project-baseline', project_name, project_type, stack,
182
+ mode: resolvedMode, repository_revision: baselines.gitHead(absTarget), scope: ['.'],
183
+ }, { rootDir: absTarget });
184
+ const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
185
+ const projection = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
186
+ const projected = projection.jobs.find((item) => item.id === job.id);
187
+ if (!projected || projected.status !== 'completed') {
188
+ throw new Error(projected?.error || 'initial projection did not complete');
189
+ }
190
+ } finally {
191
+ db.close();
192
+ }
165
193
  } catch (err) {
166
194
  const rollbackErrors = rollbackInitialization(ultraDir, backupPath);
167
195
  const rollbackNote = rollbackErrors.length > 0
@@ -179,10 +207,17 @@ function initProject({
179
207
  created_path: ultraDir,
180
208
  state_db_path: stateDbPath,
181
209
  status,
210
+ mode: resolvedMode,
211
+ baseline: {
212
+ id: baselineResult.id,
213
+ mode: baselineResult.mode,
214
+ status: baselineResult.status,
215
+ repository_revision: baselineResult.repository_revision,
216
+ },
182
217
  copied_files: copiedFiles,
183
218
  };
184
219
  if (backupPath) result.backup_path = backupPath;
185
220
  return result;
186
221
  }
187
222
 
188
- module.exports = { initProject, InitProjectError, DEFAULT_TEMPLATE };
223
+ module.exports = { initProject, InitProjectError, DEFAULT_TEMPLATE, detectProjectMode };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const PROVIDER_KINDS = new Set(['memory', 'code_graph']);
4
+ const PROVIDER_FIELDS = new Set([
5
+ 'provider', 'project', 'reference', 'revision', 'indexed_head', 'status',
6
+ ]);
7
+
8
+ class ProviderRefsError extends Error {
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = 'ProviderRefsError';
12
+ this.code = code;
13
+ }
14
+ }
15
+
16
+ function normalizeProviderRefs(value, ErrorClass = ProviderRefsError) {
17
+ if (value == null) return {};
18
+ if (typeof value !== 'object' || Array.isArray(value)) {
19
+ throw new ErrorClass('VALIDATION_ERROR', 'provider_refs must be an object');
20
+ }
21
+ const out = {};
22
+ for (const [kind, ref] of Object.entries(value)) {
23
+ if (!PROVIDER_KINDS.has(kind) || !ref || typeof ref !== 'object' || Array.isArray(ref)) {
24
+ throw new ErrorClass('VALIDATION_ERROR', `invalid external provider reference: ${kind}`);
25
+ }
26
+ for (const key of Object.keys(ref)) {
27
+ if (!PROVIDER_FIELDS.has(key)) {
28
+ throw new ErrorClass(
29
+ 'PROVIDER_CONTENT_FORBIDDEN',
30
+ `provider_refs.${kind}.${key} is not metadata; provider content remains external`,
31
+ );
32
+ }
33
+ }
34
+ if (typeof ref.provider !== 'string' || !ref.provider.trim()) {
35
+ throw new ErrorClass('VALIDATION_ERROR', `provider_refs.${kind}.provider required`);
36
+ }
37
+ out[kind] = { ...ref, provider: ref.provider.trim() };
38
+ }
39
+ return out;
40
+ }
41
+
42
+ module.exports = { ProviderRefsError, normalizeProviderRefs };
@@ -20,6 +20,11 @@ function fixture() {
20
20
  execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
21
21
  execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
22
22
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
23
+ db.prepare(
24
+ `INSERT INTO baselines
25
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
26
+ VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
27
+ ).run(new Date().toISOString());
23
28
  changes.createChange(db, {
24
29
  id: 'learning-change', title: 'Learning change', kind: 'quick',
25
30
  intent: 'Exercise guarded specification-learning transitions.',
@@ -9,10 +9,11 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
9
9
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
10
10
  : path.resolve(__dirname, '..', '..');
11
11
  const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
12
- const EXPECTED_VERSION = '10.0';
12
+ const EXPECTED_VERSION = '11.0';
13
13
  const KIMI_SCHEMA_VERSION = '9.1';
14
14
 
15
15
  const REQUIRED_TABLES = Object.freeze([
16
+ 'baselines',
16
17
  'tasks',
17
18
  'events',
18
19
  'sessions',
@@ -31,6 +32,43 @@ const REQUIRED_TABLES = Object.freeze([
31
32
  'event_consumers',
32
33
  ]);
33
34
 
35
+ function applyBaselineUpgrade(db, fromVersion) {
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS baselines (
38
+ id TEXT PRIMARY KEY,
39
+ project_name TEXT NOT NULL,
40
+ project_type TEXT,
41
+ stack TEXT,
42
+ mode TEXT NOT NULL CHECK (mode IN ('greenfield', 'brownfield', 'migrated')),
43
+ status TEXT NOT NULL DEFAULT 'draft'
44
+ CHECK (status IN ('draft', 'adopting', 'blocked', 'ready', 'superseded')),
45
+ repository_root TEXT NOT NULL DEFAULT '.',
46
+ scope_json TEXT NOT NULL DEFAULT '["."]',
47
+ repository_revision TEXT,
48
+ spec_refs_json TEXT NOT NULL DEFAULT '[]',
49
+ evidence_json TEXT NOT NULL DEFAULT '[]',
50
+ verification_json TEXT NOT NULL DEFAULT '[]',
51
+ unknowns_json TEXT NOT NULL DEFAULT '[]',
52
+ provider_refs_json TEXT NOT NULL DEFAULT '{}',
53
+ approved_by TEXT,
54
+ approval_note TEXT,
55
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
56
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
57
+ converged_at TEXT
58
+ );
59
+ CREATE INDEX IF NOT EXISTS baselines_status ON baselines(status, updated_at);
60
+ `);
61
+ if (fromVersion && fromVersion !== EXPECTED_VERSION) {
62
+ db.prepare(
63
+ `INSERT OR IGNORE INTO baselines
64
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
65
+ VALUES ('migrated-baseline', 'Migrated Ultra project', 'migrated', 'ready',
66
+ 'schema-migration', 'Legacy project preserved during schema 11.0 migration',
67
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
68
+ ).run();
69
+ }
70
+ }
71
+
34
72
  function readSchemaSql() {
35
73
  if (!fs.existsSync(SCHEMA_FILE)) {
36
74
  throw new Error(`state-db schema missing at ${SCHEMA_FILE}`);
@@ -223,22 +261,43 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db)) {
223
261
  db.transaction(() => {
224
262
  applyCompatibleColumns(db);
225
263
  const contextChanged = applyContextSpineUpgrade(db);
226
- if (contextChanged || (fromVersion && fromVersion !== EXPECTED_VERSION)) {
264
+ const contextMigration = db.prepare(
265
+ "SELECT 1 FROM migration_history WHERE to_version = '10.0' AND status = 'success' LIMIT 1",
266
+ ).get();
267
+ const needsContextMigration = Boolean(
268
+ fromVersion && !['10.0', EXPECTED_VERSION].includes(fromVersion) && !contextMigration,
269
+ );
270
+ if (contextChanged || needsContextMigration) {
227
271
  db.prepare(
228
272
  `INSERT INTO migration_history
229
273
  (from_version, to_version, direction, status, notes)
230
274
  VALUES (?, ?, 'forward', 'success', ?)`,
231
275
  ).run(
232
276
  runtimeChanged ? KIMI_SCHEMA_VERSION : (fromVersion || 'unknown'),
233
- EXPECTED_VERSION,
277
+ '10.0',
234
278
  'Add Context Spine role/gate readiness, execution contracts, breadcrumbs, and specification learning',
235
279
  );
236
280
  }
281
+ applyBaselineUpgrade(db, fromVersion);
282
+ const baselineMigration = db.prepare(
283
+ "SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
284
+ ).get();
285
+ if (fromVersion && fromVersion !== EXPECTED_VERSION && !baselineMigration) {
286
+ db.prepare(
287
+ `INSERT INTO migration_history
288
+ (from_version, to_version, direction, status, notes)
289
+ VALUES (?, ?, 'forward', 'success', ?)`,
290
+ ).run(
291
+ fromVersion === '10.0' ? fromVersion : '10.0',
292
+ EXPECTED_VERSION,
293
+ 'Add authoritative greenfield and brownfield baseline adoption, evidence, convergence, and drift state',
294
+ );
295
+ }
237
296
  db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
238
297
  db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
239
298
  db.prepare(
240
299
  'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
241
- ).run(EXPECTED_VERSION, 'Role-scoped context snapshots, deterministic breadcrumbs, and approval-gated specification learning');
300
+ ).run(EXPECTED_VERSION, 'Greenfield and brownfield baseline adoption, convergence, and drift authority');
242
301
  })();
243
302
  }
244
303
 
@@ -123,7 +123,7 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
123
123
  const legacy = openStateDb(file);
124
124
  const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
125
125
  legacy.exec(legacySchema);
126
- legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0')").run();
126
+ legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0', '11.0')").run();
127
127
  legacy.prepare(
128
128
  "INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
129
129
  ).run();
@@ -146,16 +146,21 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
146
146
  closeStateDb(legacy);
147
147
 
148
148
  const upgraded = initStateDb(file);
149
- assert.equal(upgraded.schema_version, '10.0');
149
+ assert.equal(upgraded.schema_version, '11.0');
150
150
  assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
151
151
  assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
152
152
  assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
153
153
  assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
154
154
  const migrations = upgraded.db.prepare(
155
- "SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0') ORDER BY id",
155
+ "SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0', '11.0') ORDER BY id",
156
156
  ).all();
157
157
  assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
158
158
  assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
159
+ assert.ok(migrations.some((row) => row.to_version === '11.0' && /baseline adoption/i.test(row.notes)));
160
+ assert.deepEqual(
161
+ upgraded.db.prepare("SELECT mode, status FROM baselines WHERE id = 'migrated-baseline'").get(),
162
+ { mode: 'migrated', status: 'ready' },
163
+ );
159
164
 
160
165
  upgraded.db.prepare(
161
166
  "INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
@@ -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 });