ultra-builder-pro-cli 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.ultra-template/docs/research/README.md +21 -44
  2. package/.ultra-template/specs/discovery.md +10 -0
  3. package/CHANGELOG.md +53 -1
  4. package/README.md +25 -9
  5. package/adapters/_shared/handbook.cjs +2 -2
  6. package/adapters/tests/codex.test.cjs +8 -0
  7. package/commands/ultra-init.md +1 -1
  8. package/docs/AGENT-CONTEXT.md +4 -3
  9. package/docs/ARCHITECTURE.md +32 -16
  10. package/docs/PLAN.zh-CN.md +5 -4
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
  13. package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
  14. package/hooks/context_spine.py +37 -4
  15. package/mcp-server/lib/archive-journal.cjs +192 -0
  16. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  17. package/mcp-server/lib/baseline-workflow.cjs +404 -38
  18. package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
  19. package/mcp-server/lib/change-workflow.cjs +249 -75
  20. package/mcp-server/lib/change-workflow.test.cjs +235 -1
  21. package/mcp-server/lib/context-spine.test.cjs +5 -0
  22. package/mcp-server/lib/doctor.cjs +14 -1
  23. package/mcp-server/lib/doctor.test.cjs +42 -4
  24. package/mcp-server/lib/init-project.cjs +292 -9
  25. package/mcp-server/lib/spec-learning.test.cjs +1 -1
  26. package/mcp-server/lib/state-authority.cjs +9 -3
  27. package/mcp-server/lib/state-db.cjs +154 -32
  28. package/mcp-server/lib/state-db.test.cjs +136 -4
  29. package/mcp-server/lib/state-ops.cjs +68 -6
  30. package/mcp-server/lib/task-expander.cjs +7 -1
  31. package/mcp-server/lib/task-expander.test.cjs +71 -0
  32. package/mcp-server/server.cjs +14 -3
  33. package/mcp-server/tests/init-project.test.cjs +162 -1
  34. package/mcp-server/tests/migrate.test.cjs +33 -0
  35. package/mcp-server/tests/server-contract.test.cjs +384 -6
  36. package/mcp-server/tests/state-db.test.cjs +14 -2
  37. package/mcp-server/tests/state-ops.test.cjs +108 -0
  38. package/package.json +1 -1
  39. package/skills/ultra-change/SKILL.md +18 -10
  40. package/skills/ultra-deliver/SKILL.md +13 -9
  41. package/skills/ultra-dev/SKILL.md +6 -4
  42. package/skills/ultra-doctor/SKILL.md +22 -7
  43. package/skills/ultra-init/SKILL.md +104 -59
  44. package/skills/ultra-init/references/brownfield-adoption.md +65 -37
  45. package/skills/ultra-plan/SKILL.md +8 -5
  46. package/skills/ultra-research/SKILL.md +12 -5
  47. package/skills/ultra-status/SKILL.md +6 -4
  48. package/spec/cli-protocol.md +11 -1
  49. package/spec/mcp-tools.yaml +95 -9
  50. package/spec/schemas/state-db.sql +11 -0
  51. package/ultra-tools/cli.cjs +9 -5
  52. package/ultra-tools/commands/migrate.cjs +62 -36
  53. package/ultra-tools/commands/status.cjs +18 -9
  54. package/ultra-tools/commands/system.cjs +340 -6
  55. package/ultra-tools/commands/task.cjs +7 -1
  56. package/ultra-tools/status-command.test.cjs +29 -0
  57. package/ultra-tools/system-command.test.cjs +252 -1
@@ -3,12 +3,17 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
6
+ const {
7
+ initStateDb, openStateDb, closeStateDb, MIGRATED_GAPS,
8
+ } = require('../../mcp-server/lib/state-db.cjs');
7
9
  const ops = require('../../mcp-server/lib/state-ops.cjs');
8
10
  const projector = require('../../mcp-server/lib/projector.cjs');
9
11
 
10
- const SUPPORTED_FROM = '4.4';
11
- const SUPPORTED_TO = '4.5';
12
+ const DEFAULT_FROM = '4.4';
13
+ const SUPPORTED_TRANSITIONS = Object.freeze({
14
+ '4.4': '4.5',
15
+ '4.5': '12.0',
16
+ });
12
17
 
13
18
  // Frozen SQL — values flow through parameter bindings.
14
19
  const RECORD_MIGRATION_SQL = "INSERT INTO migration_history (from_version, to_version, direction, status, notes) VALUES (@from, @to, @direction, @status, @notes)";
@@ -73,7 +78,7 @@ function copyDirSync(src, dst) {
73
78
  const s = path.join(src, entry.name);
74
79
  const d = path.join(dst, entry.name);
75
80
  if (entry.name === 'state.db' || entry.name === 'state.db-wal' || entry.name === 'state.db-shm') continue;
76
- if (entry.name.startsWith('backup-v4.4-')) continue;
81
+ if (/^backup-v[^/]+-/.test(entry.name)) continue;
77
82
  if (entry.isDirectory()) copyDirSync(s, d);
78
83
  else fs.copyFileSync(s, d);
79
84
  }
@@ -148,15 +153,15 @@ function normalizeLegacyTask(task, tasksJson, contextHeaders, sourceDir) {
148
153
  };
149
154
  }
150
155
 
151
- function planForward(sourceDir) {
156
+ function planForward(sourceDir, expectedFrom = DEFAULT_FROM) {
152
157
  const tasksPath = path.join(sourceDir, '.ultra', 'tasks', 'tasks.json');
153
158
  const tasksJson = readJsonOptional(tasksPath);
154
159
  if (!tasksJson || !Array.isArray(tasksJson.tasks)) {
155
160
  throw new Error(`migrate: tasks.json missing or malformed at ${tasksPath}`);
156
161
  }
157
162
  const version = String(tasksJson.version || tasksJson.schema_version || '');
158
- if (version !== SUPPORTED_FROM) {
159
- throw new Error(`migrate: expected v${SUPPORTED_FROM} tasks.json, found ${version || '(missing version)'}`);
163
+ if (version !== expectedFrom) {
164
+ throw new Error(`migrate: expected v${expectedFrom} tasks.json, found ${version || '(missing version)'}`);
160
165
  }
161
166
 
162
167
  const contextHeaders = {};
@@ -173,7 +178,7 @@ function planForward(sourceDir) {
173
178
  task_id: task.id,
174
179
  json_status: task.status,
175
180
  context_status: ctx.status,
176
- resolution: 'tasks.json wins (v4.4 → v4.5 D21 rule)',
181
+ resolution: `tasks.json wins (v${expectedFrom} projection migration rule)`,
177
182
  });
178
183
  }
179
184
  }
@@ -190,6 +195,7 @@ function planForward(sourceDir) {
190
195
  events: eventList,
191
196
  contextHeaders,
192
197
  warnings,
198
+ projectName: tasksJson.project?.name || path.basename(sourceDir),
193
199
  };
194
200
  }
195
201
 
@@ -241,16 +247,17 @@ function recordMigration(db, { from, to, direction, status, notes }) {
241
247
  db.prepare(RECORD_MIGRATION_SQL).run({ from, to, direction, status, notes });
242
248
  }
243
249
 
244
- function ensureBackupName(sourceDir) {
250
+ function ensureBackupName(sourceDir, fromVersion = DEFAULT_FROM) {
245
251
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
246
- return path.join(sourceDir, '.ultra', `backup-v4.4-${ts}`);
252
+ return path.join(sourceDir, '.ultra', `backup-v${fromVersion}-${ts}`);
247
253
  }
248
254
 
249
- function findLatestBackup(sourceDir) {
255
+ function findLatestBackup(sourceDir, fromVersion = DEFAULT_FROM) {
250
256
  const ultra = path.join(sourceDir, '.ultra');
251
257
  if (!fs.existsSync(ultra)) return null;
258
+ const prefix = `backup-v${fromVersion}-`;
252
259
  const candidates = fs.readdirSync(ultra)
253
- .filter((n) => n.startsWith('backup-v4.4-'))
260
+ .filter((n) => n.startsWith(prefix))
254
261
  .sort();
255
262
  return candidates.length === 0 ? null : path.join(ultra, candidates[candidates.length - 1]);
256
263
  }
@@ -258,9 +265,11 @@ function findLatestBackup(sourceDir) {
258
265
  function cmdForward(flags) {
259
266
  const sourceDir = path.resolve(flags.sourceDir || '.');
260
267
  const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
268
+ const fromVersion = flags.from || DEFAULT_FROM;
269
+ const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
261
270
  let plan;
262
271
  try {
263
- plan = planForward(sourceDir);
272
+ plan = planForward(sourceDir, fromVersion);
264
273
  } catch (err) {
265
274
  emit({ ok: false, error: { code: 'MIGRATE_FAILED', message: err.message, retriable: false } });
266
275
  return 2;
@@ -271,8 +280,8 @@ function cmdForward(flags) {
271
280
  ok: true,
272
281
  data: {
273
282
  mode: 'dry',
274
- from: SUPPORTED_FROM,
275
- to: SUPPORTED_TO,
283
+ from: fromVersion,
284
+ to: toVersion,
276
285
  source_dir: sourceDir,
277
286
  db_path: dbPath,
278
287
  tasks_to_insert: plan.tasks.length,
@@ -284,7 +293,7 @@ function cmdForward(flags) {
284
293
  }
285
294
 
286
295
  // Real run: backup the entire .ultra subtree first.
287
- const backupDir = ensureBackupName(sourceDir);
296
+ const backupDir = ensureBackupName(sourceDir, fromVersion);
288
297
  const ultraSource = path.join(sourceDir, '.ultra');
289
298
  copyDirSync(ultraSource, backupDir);
290
299
 
@@ -297,9 +306,18 @@ function cmdForward(flags) {
297
306
  throw new Error(`migrate: refusing to merge into non-empty state.db (tasks=${existing})`);
298
307
  }
299
308
  const inserted = applyForward(db, plan);
309
+ db.prepare(
310
+ `INSERT INTO baselines
311
+ (id, project_name, mode, status, gaps_json, approval_note)
312
+ VALUES ('migrated-baseline', ?, 'migrated', 'adopting', ?, ?)`,
313
+ ).run(
314
+ plan.projectName,
315
+ JSON.stringify(MIGRATED_GAPS),
316
+ `Legacy v${fromVersion} projection imported; evidence-backed owner re-adoption is required`,
317
+ );
300
318
  recordMigration(db, {
301
- from: SUPPORTED_FROM,
302
- to: SUPPORTED_TO,
319
+ from: fromVersion,
320
+ to: toVersion,
303
321
  direction: 'forward',
304
322
  status: 'success',
305
323
  notes: `tasks=${inserted.taskInserted} events=${inserted.eventsInserted} warnings=${plan.warnings.length}`,
@@ -312,8 +330,8 @@ function cmdForward(flags) {
312
330
  ok: true,
313
331
  data: {
314
332
  mode: 'apply',
315
- from: SUPPORTED_FROM,
316
- to: SUPPORTED_TO,
333
+ from: fromVersion,
334
+ to: toVersion,
317
335
  source_dir: sourceDir,
318
336
  db_path: dbPath,
319
337
  backup_dir: backupDir,
@@ -328,8 +346,8 @@ function cmdForward(flags) {
328
346
  if (db) {
329
347
  try {
330
348
  recordMigration(db, {
331
- from: SUPPORTED_FROM,
332
- to: SUPPORTED_TO,
349
+ from: fromVersion,
350
+ to: toVersion,
333
351
  direction: 'forward',
334
352
  status: 'failed',
335
353
  notes: err.message,
@@ -346,9 +364,11 @@ function cmdForward(flags) {
346
364
  function cmdRollback(flags) {
347
365
  const sourceDir = path.resolve(flags.sourceDir || '.');
348
366
  const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
349
- const backupDir = findLatestBackup(sourceDir);
367
+ const fromVersion = flags.from || DEFAULT_FROM;
368
+ const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
369
+ const backupDir = findLatestBackup(sourceDir, fromVersion);
350
370
  if (!backupDir) {
351
- emit({ ok: false, error: { code: 'NO_BACKUP', message: 'no backup-v4.4-* directory found' } });
371
+ emit({ ok: false, error: { code: 'NO_BACKUP', message: `no backup-v${fromVersion}-* directory found` } });
352
372
  return 2;
353
373
  }
354
374
 
@@ -358,8 +378,8 @@ function cmdRollback(flags) {
358
378
  if (fs.existsSync(dbPath)) {
359
379
  db = openStateDb(dbPath);
360
380
  recordMigration(db, {
361
- from: SUPPORTED_TO,
362
- to: SUPPORTED_FROM,
381
+ from: toVersion,
382
+ to: fromVersion,
363
383
  direction: 'rollback',
364
384
  status: 'success',
365
385
  notes: `restored from ${backupDir}`,
@@ -376,7 +396,7 @@ function cmdRollback(flags) {
376
396
  // Restore .ultra contents from backup, leaving the backup itself in place.
377
397
  const ultraDir = path.join(sourceDir, '.ultra');
378
398
  for (const entry of fs.readdirSync(ultraDir, { withFileTypes: true })) {
379
- if (entry.name.startsWith('backup-v4.4-')) continue;
399
+ if (/^backup-v[^/]+-/.test(entry.name)) continue;
380
400
  const target = path.join(ultraDir, entry.name);
381
401
  fs.rmSync(target, { recursive: true, force: true });
382
402
  }
@@ -386,8 +406,8 @@ function cmdRollback(flags) {
386
406
  ok: true,
387
407
  data: {
388
408
  mode: 'rollback',
389
- from: SUPPORTED_TO,
390
- to: SUPPORTED_FROM,
409
+ from: toVersion,
410
+ to: fromVersion,
391
411
  backup_dir: backupDir,
392
412
  source_dir: sourceDir,
393
413
  },
@@ -406,29 +426,35 @@ function dispatch(args) {
406
426
  process.stdout.write(USAGE);
407
427
  return 0;
408
428
  }
409
- if (flags.from && flags.from !== SUPPORTED_FROM) {
410
- emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${flags.from} unsupported (only ${SUPPORTED_FROM})` } });
429
+ const fromVersion = flags.from || DEFAULT_FROM;
430
+ const expectedTo = SUPPORTED_TRANSITIONS[fromVersion];
431
+ if (!expectedTo) {
432
+ emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${fromVersion} unsupported (supported: ${Object.keys(SUPPORTED_TRANSITIONS).join(', ')})` } });
411
433
  return 1;
412
434
  }
413
- if (flags.to && flags.to !== SUPPORTED_TO) {
414
- emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported (only ${SUPPORTED_TO})` } });
435
+ if (flags.to && flags.to !== expectedTo) {
436
+ emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported for --from ${fromVersion} (expected ${expectedTo})` } });
415
437
  return 1;
416
438
  }
417
439
  return flags.rollback ? cmdRollback(flags) : cmdForward(flags);
418
440
  }
419
441
 
420
- const USAGE = `ultra-tools migrate --from=4.4 --to=4.5 [flags]
442
+ const USAGE = `ultra-tools migrate --from=<version> --to=<version> [flags]
443
+
444
+ Supported transitions:
445
+ 4.4 -> 4.5 import the legacy task projection into authoritative state
446
+ 4.5 -> 12.0 import a projection-only project into current authoritative state
421
447
 
422
448
  Flags:
423
449
  --source-dir <dir> project root containing .ultra/ (default: .)
424
450
  --db-path <path> state.db destination (default: <source-dir>/.ultra/state.db)
425
451
  --dry print the migration plan without writing
426
- --rollback restore the most recent backup-v4.4-* and drop state.db
452
+ --rollback restore the most recent matching backup-v<from>-* and drop state.db
427
453
 
428
454
  The forward flow: backup .ultra/ → init state.db → insert tasks from
429
455
  tasks.json → merge context md status (tasks.json wins on conflict, warnings
430
456
  recorded) → insert activity-log events → record migration_history.
431
- Rollback restores from the latest backup-v4.4-* directory and writes a
457
+ Rollback restores from the latest matching backup-v<from>-* directory and writes a
432
458
  matching migration_history rollback row before dropping state.db.
433
459
  `;
434
460
 
@@ -71,17 +71,26 @@ function buildCostPanel(db, { since = null, limit = 3, rootDir = process.cwd() }
71
71
  const total_cost_usd = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
72
72
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
73
73
  const current = baselineHealth.baseline;
74
+ const baselineStatus = current?.status
75
+ || (baselineHealth.blockers.includes('BASELINE_SCHEMA_MIGRATION_REQUIRED')
76
+ ? 'migration_required' : 'missing');
77
+ const baseline = {
78
+ id: current?.id || null,
79
+ mode: current?.mode || null,
80
+ status: baselineStatus,
81
+ health: baselineHealth.status,
82
+ repository_revision: current?.repository_revision || null,
83
+ blockers: baselineHealth.blockers,
84
+ warnings: baselineHealth.warnings,
85
+ };
86
+ if (current) {
87
+ baseline.repository_branch = current.repository_branch || null;
88
+ baseline.worktree_state = current.worktree_state || 'unavailable';
89
+ baseline.gaps = baselines.summarizeGaps(current.gaps);
90
+ }
74
91
  return {
75
92
  period: { since: since || 'all-time' },
76
- baseline: {
77
- id: current?.id || null,
78
- mode: current?.mode || null,
79
- status: current?.status || 'missing',
80
- health: baselineHealth.status,
81
- repository_revision: current?.repository_revision || null,
82
- blockers: baselineHealth.blockers,
83
- warnings: baselineHealth.warnings,
84
- },
93
+ baseline,
85
94
  by_runtime,
86
95
  top_tasks,
87
96
  total_cost_usd,
@@ -2,16 +2,323 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const Database = require('better-sqlite3');
5
6
 
6
- const { openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
7
+ const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
8
+ const { initProject } = require('../../mcp-server/lib/init-project.cjs');
7
9
  const doctor = require('../../mcp-server/lib/doctor.cjs');
8
10
 
9
- const USAGE = `ultra-tools system doctor [--repair]\n`;
11
+ const RESTORE_CONFIRMATION = 'REPLACE_CORRUPT_ULTRA_STATE';
12
+ const REBASELINE_CONFIRMATION = 'REBASELINE_CORRUPT_ULTRA_STATE';
13
+ const USAGE = `ultra-tools system <verb> [options]
14
+
15
+ VERBS:
16
+ doctor [--repair]
17
+ restore --backup <.ultra/backups/...db> --confirm ${RESTORE_CONFIRMATION}
18
+ rebaseline --project-name <name> [--scope <path>] --confirm ${REBASELINE_CONFIRMATION}
19
+ `;
20
+
21
+ class SystemCommandError extends Error {
22
+ constructor(code, message, exitCode = 1) {
23
+ super(message);
24
+ this.name = 'SystemCommandError';
25
+ this.code = code;
26
+ this.exitCode = exitCode;
27
+ }
28
+ }
10
29
 
11
30
  function emit(envelope) {
12
31
  process.stdout.write(`${JSON.stringify(envelope)}\n`);
13
32
  }
14
33
 
34
+ function stamp() {
35
+ return new Date().toISOString().replace(/[:.]/g, '-');
36
+ }
37
+
38
+ function parseOptions(args, allowed) {
39
+ const options = { scope: [] };
40
+ for (let index = 0; index < args.length; index += 1) {
41
+ const arg = args[index];
42
+ if (arg === '-h' || arg === '--help') {
43
+ options.help = true;
44
+ continue;
45
+ }
46
+ const [flag, inline] = arg.startsWith('--') && arg.includes('=')
47
+ ? arg.split(/=(.*)/s, 2)
48
+ : [arg, null];
49
+ const name = flag.startsWith('--') ? flag.slice(2) : null;
50
+ if (!name || !allowed.has(name)) {
51
+ throw new SystemCommandError('VALIDATION_ERROR', `unknown system option: ${arg}`);
52
+ }
53
+ const value = inline === null ? args[index += 1] : inline;
54
+ if (value === undefined || String(value).length === 0 || String(value).startsWith('--')) {
55
+ throw new SystemCommandError('VALIDATION_ERROR', `${flag} requires a value`);
56
+ }
57
+ if (name === 'scope') options.scope.push(value);
58
+ else options[name.replaceAll('-', '_')] = value;
59
+ }
60
+ return options;
61
+ }
62
+
63
+ function inspectSqlite(file) {
64
+ if (!fs.existsSync(file)) return { exists: false, valid: false, corrupt: false };
65
+ let db;
66
+ try {
67
+ db = new Database(file, { readonly: true, fileMustExist: true });
68
+ const integrity = db.pragma('integrity_check', { simple: true });
69
+ const hasSchema = Boolean(db.prepare(
70
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_version'",
71
+ ).get());
72
+ return { exists: true, valid: integrity === 'ok' && hasSchema, corrupt: false, integrity, hasSchema };
73
+ } catch (error) {
74
+ const corrupt = /not a database|database disk image is malformed|file is encrypted|malformed/i
75
+ .test(String(error.message || ''));
76
+ if (!corrupt) throw error;
77
+ return { exists: true, valid: false, corrupt: true, error: error.message };
78
+ } finally {
79
+ closeStateDb(db);
80
+ }
81
+ }
82
+
83
+ function inspectSqliteSnapshot(file) {
84
+ if (!fs.existsSync(file)) return { exists: false, valid: false, corrupt: false };
85
+ const snapshotDir = fs.mkdtempSync(path.join(path.dirname(file), '.state-inspect-'));
86
+ const snapshotPath = path.join(snapshotDir, 'state.db');
87
+ try {
88
+ for (const suffix of ['', '-wal', '-shm']) {
89
+ const source = `${file}${suffix}`;
90
+ if (fs.existsSync(source)) fs.copyFileSync(source, `${snapshotPath}${suffix}`);
91
+ }
92
+ return inspectSqlite(snapshotPath);
93
+ } finally {
94
+ fs.rmSync(snapshotDir, { recursive: true, force: true });
95
+ }
96
+ }
97
+
98
+ function ensureCorruptState(rootDir) {
99
+ const statePath = path.join(rootDir, '.ultra', 'state.db');
100
+ const state = inspectSqliteSnapshot(statePath);
101
+ if (!state.exists) throw new SystemCommandError('STATE_DB_MISSING', `.ultra/state.db not found under ${rootDir}`, 2);
102
+ if (!state.corrupt) {
103
+ throw new SystemCommandError(
104
+ 'STATE_DB_NOT_CORRUPT',
105
+ 'state.db is readable; use system doctor --repair for supported recovery instead of replacing authority',
106
+ 2,
107
+ );
108
+ }
109
+ return statePath;
110
+ }
111
+
112
+ function managedBackup(rootDir, candidate) {
113
+ if (!candidate) throw new SystemCommandError('VALIDATION_ERROR', '--backup is required');
114
+ const backupRoot = path.join(rootDir, '.ultra', 'backups');
115
+ if (!fs.existsSync(backupRoot)) {
116
+ throw new SystemCommandError('BACKUP_NOT_FOUND', `managed backup directory does not exist: ${backupRoot}`);
117
+ }
118
+ const resolvedRoot = fs.realpathSync(backupRoot);
119
+ const requested = path.isAbsolute(candidate) ? candidate : path.resolve(rootDir, candidate);
120
+ let resolved;
121
+ try { resolved = fs.realpathSync(requested); }
122
+ catch {
123
+ throw new SystemCommandError('BACKUP_NOT_FOUND', `backup does not exist: ${requested}`);
124
+ }
125
+ if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
126
+ throw new SystemCommandError(
127
+ 'BACKUP_OUTSIDE_MANAGED_ROOT',
128
+ `backup must be inside ${backupRoot}`,
129
+ );
130
+ }
131
+ if (!fs.statSync(resolved).isFile()) {
132
+ throw new SystemCommandError('BACKUP_NOT_FOUND', `backup is not a file: ${resolved}`);
133
+ }
134
+ const inspection = inspectSqlite(resolved);
135
+ if (!inspection.valid) {
136
+ throw new SystemCommandError('BACKUP_INVALID', `backup is not a verified Ultra SQLite database: ${resolved}`);
137
+ }
138
+ return resolved;
139
+ }
140
+
141
+ function createRecoveryDir(rootDir, prefix) {
142
+ const dir = path.join(rootDir, '.ultra', 'backups', `${prefix}-${stamp()}`);
143
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
144
+ fs.mkdirSync(dir, { recursive: false });
145
+ return dir;
146
+ }
147
+
148
+ function moveStateFiles(statePath, recoveryDir, moved = []) {
149
+ for (const suffix of ['', '-wal', '-shm']) {
150
+ const source = `${statePath}${suffix}`;
151
+ if (!fs.existsSync(source)) continue;
152
+ const target = path.join(recoveryDir, `state.db${suffix}`);
153
+ fs.renameSync(source, target);
154
+ moved.push({ source, target });
155
+ }
156
+ return moved;
157
+ }
158
+
159
+ function restoreMovedFiles(moved) {
160
+ const errors = [];
161
+ for (const item of moved) {
162
+ try {
163
+ if (fs.existsSync(item.source)) fs.rmSync(item.source, { force: true });
164
+ } catch (error) {
165
+ errors.push(`replacement cleanup failed for ${item.source}: ${error.message}`);
166
+ }
167
+ }
168
+ for (const item of moved) {
169
+ try {
170
+ if (fs.existsSync(item.target)) fs.renameSync(item.target, item.source);
171
+ } catch (error) {
172
+ errors.push(`evidence restore failed for ${item.source}: ${error.message}`);
173
+ }
174
+ }
175
+ return errors;
176
+ }
177
+
178
+ async function dispatchRestore(args) {
179
+ let options;
180
+ try { options = parseOptions(args, new Set(['backup', 'confirm'])); }
181
+ catch (error) { emit({ ok: false, error: { code: error.code, message: error.message } }); return error.exitCode || 1; }
182
+ if (options.help) { process.stdout.write(USAGE); return 0; }
183
+ if (options.confirm !== RESTORE_CONFIRMATION) {
184
+ emit({
185
+ ok: false,
186
+ error: {
187
+ code: 'CONFIRMATION_REQUIRED',
188
+ message: `restore requires --confirm ${RESTORE_CONFIRMATION}`,
189
+ },
190
+ });
191
+ return 1;
192
+ }
193
+ const rootDir = process.cwd();
194
+ let moved = [];
195
+ let activeState;
196
+ let staging;
197
+ try {
198
+ activeState = ensureCorruptState(rootDir);
199
+ const sourceBackup = managedBackup(rootDir, options.backup);
200
+ const recoveryDir = createRecoveryDir(rootDir, 'restore');
201
+ staging = path.join(rootDir, '.ultra', `.state.restore-${stamp()}.db`);
202
+ fs.copyFileSync(sourceBackup, staging, fs.constants.COPYFILE_EXCL);
203
+ moveStateFiles(activeState, recoveryDir, moved);
204
+ fs.renameSync(staging, activeState);
205
+ const initialized = initStateDb(activeState);
206
+ closeStateDb(initialized.db);
207
+ emit({
208
+ ok: true,
209
+ data: {
210
+ status: 'restored',
211
+ source_backup: sourceBackup,
212
+ recovery_backup_dir: recoveryDir,
213
+ quarantined_state_path: path.join(recoveryDir, 'state.db'),
214
+ schema_version: initialized.schema_version,
215
+ schema_migration_backup_path: initialized.backup_path || null,
216
+ next_action: 'Run ultra-tools system doctor, then ultra-init to inspect baseline readiness.',
217
+ },
218
+ });
219
+ return 0;
220
+ } catch (error) {
221
+ if (staging) {
222
+ try { fs.rmSync(staging, { force: true }); } catch { /* rollback continues */ }
223
+ }
224
+ const rollbackErrors = activeState ? restoreMovedFiles(moved) : [];
225
+ emit({
226
+ ok: false,
227
+ error: {
228
+ code: error instanceof SystemCommandError ? error.code : 'STATE_RESTORE_FAILED',
229
+ message: `${error.message}${rollbackErrors.length ? `; rollback errors: ${rollbackErrors.join('; ')}` : ''}`,
230
+ },
231
+ });
232
+ return error.exitCode || 2;
233
+ }
234
+ }
235
+
236
+ async function dispatchRebaseline(args) {
237
+ let options;
238
+ try { options = parseOptions(args, new Set(['project-name', 'scope', 'confirm'])); }
239
+ catch (error) { emit({ ok: false, error: { code: error.code, message: error.message } }); return error.exitCode || 1; }
240
+ if (options.help) { process.stdout.write(USAGE); return 0; }
241
+ if (options.confirm !== REBASELINE_CONFIRMATION) {
242
+ emit({
243
+ ok: false,
244
+ error: {
245
+ code: 'CONFIRMATION_REQUIRED',
246
+ message: `rebaseline requires --confirm ${REBASELINE_CONFIRMATION}`,
247
+ },
248
+ });
249
+ return 1;
250
+ }
251
+ if (!options.project_name) {
252
+ emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: '--project-name is required' } });
253
+ return 1;
254
+ }
255
+ const rootDir = process.cwd();
256
+ let moved = [];
257
+ let projectionMove = null;
258
+ let activeState;
259
+ try {
260
+ activeState = ensureCorruptState(rootDir);
261
+ const recoveryDir = createRecoveryDir(rootDir, 'rebaseline');
262
+ moveStateFiles(activeState, recoveryDir, moved);
263
+ const tasksDir = path.join(rootDir, '.ultra', 'tasks');
264
+ if (fs.existsSync(tasksDir)) {
265
+ const legacyTasksDir = path.join(recoveryDir, 'tasks');
266
+ projectionMove = { source: tasksDir, target: legacyTasksDir };
267
+ fs.renameSync(tasksDir, legacyTasksDir);
268
+ }
269
+ const initialized = initProject({
270
+ target_dir: rootDir,
271
+ project_name: options.project_name,
272
+ mode: 'brownfield',
273
+ scope: options.scope.length > 0 ? options.scope : undefined,
274
+ resume: true,
275
+ });
276
+ emit({
277
+ ok: true,
278
+ data: {
279
+ status: 'rebaseline_started',
280
+ recovery_backup_dir: recoveryDir,
281
+ quarantined_state_path: path.join(recoveryDir, 'state.db'),
282
+ legacy_projection_path: projectionMove
283
+ ? path.join(projectionMove.target, 'tasks.json')
284
+ : null,
285
+ baseline: initialized.baseline,
286
+ copied_files: initialized.copied_files,
287
+ next_action: 'Inspect the preserved evidence, record the brownfield baseline, and obtain owner approval before convergence.',
288
+ },
289
+ });
290
+ return 0;
291
+ } catch (error) {
292
+ const rollbackErrors = [];
293
+ if (moved.length > 0) {
294
+ const generatedState = activeState || path.join(rootDir, '.ultra', 'state.db');
295
+ for (const suffix of ['', '-wal', '-shm']) {
296
+ try { fs.rmSync(`${generatedState}${suffix}`, { force: true }); }
297
+ catch (rollbackError) { rollbackErrors.push(`generated state cleanup failed: ${rollbackError.message}`); }
298
+ }
299
+ }
300
+ if (projectionMove) {
301
+ try {
302
+ if (fs.existsSync(projectionMove.source)) {
303
+ fs.rmSync(projectionMove.source, { recursive: true, force: true });
304
+ }
305
+ if (fs.existsSync(projectionMove.target)) fs.renameSync(projectionMove.target, projectionMove.source);
306
+ } catch (rollbackError) {
307
+ rollbackErrors.push(`task projection restore failed: ${rollbackError.message}`);
308
+ }
309
+ }
310
+ rollbackErrors.push(...restoreMovedFiles(moved));
311
+ emit({
312
+ ok: false,
313
+ error: {
314
+ code: error.code || 'STATE_REBASELINE_FAILED',
315
+ message: `${error.message}${rollbackErrors.length ? `; rollback errors: ${rollbackErrors.join('; ')}` : ''}`,
316
+ },
317
+ });
318
+ return error.exitCode || 2;
319
+ }
320
+ }
321
+
15
322
  async function dispatchDoctor(args) {
16
323
  const unknown = args.filter((arg) => !['--repair', '-h', '--help'].includes(arg));
17
324
  if (args.includes('-h') || args.includes('--help')) {
@@ -28,16 +335,33 @@ async function dispatchDoctor(args) {
28
335
  emit({ ok: false, error: { code: 'STATE_DB_MISSING', message: `.ultra/state.db not found under ${rootDir}` } });
29
336
  return 2;
30
337
  }
31
- const db = openStateDb(dbPath);
338
+ let db;
32
339
  try {
340
+ const repair = args.includes('--repair');
341
+ const initialized = repair ? initStateDb(dbPath) : null;
342
+ db = initialized ? initialized.db : openStateDb(dbPath);
33
343
  const data = await doctor.runDoctor(db, { rootDir, repair: args.includes('--repair') });
344
+ if (initialized) {
345
+ data.schema_version = initialized.schema_version;
346
+ data.schema_migration_backup_path = initialized.backup_path || null;
347
+ }
34
348
  emit({ ok: true, data });
35
349
  return data.status === 'healthy' ? 0 : 2;
36
350
  } catch (error) {
37
- emit({ ok: false, error: { code: error.code || 'STATE_DB_ERROR', message: error.message } });
351
+ const corrupt = /not a database|database disk image is malformed|file is encrypted or is not a database|malformed/i
352
+ .test(String(error.message || ''));
353
+ emit({
354
+ ok: false,
355
+ error: {
356
+ code: corrupt ? 'STATE_DB_CORRUPT' : (error.code || 'STATE_DB_ERROR'),
357
+ message: corrupt
358
+ ? `state.db is corrupt; after owner approval use system restore with a verified managed backup or system rebaseline to quarantine and rebuild authority: ${error.message}`
359
+ : error.message,
360
+ },
361
+ });
38
362
  return 2;
39
363
  } finally {
40
- closeStateDb(db);
364
+ if (db) closeStateDb(db);
41
365
  }
42
366
  }
43
367
 
@@ -47,6 +371,8 @@ async function dispatch(args) {
47
371
  process.stdout.write(USAGE);
48
372
  return 0;
49
373
  }
374
+ if (verb === 'restore') return dispatchRestore(rest);
375
+ if (verb === 'rebaseline') return dispatchRebaseline(rest);
50
376
  if (verb !== 'doctor') {
51
377
  emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown system verb: ${verb}` } });
52
378
  return 1;
@@ -54,4 +380,12 @@ async function dispatch(args) {
54
380
  return dispatchDoctor(rest);
55
381
  }
56
382
 
57
- module.exports = { USAGE, dispatch, dispatchDoctor };
383
+ module.exports = {
384
+ USAGE,
385
+ RESTORE_CONFIRMATION,
386
+ REBASELINE_CONFIRMATION,
387
+ dispatch,
388
+ dispatchDoctor,
389
+ dispatchRestore,
390
+ dispatchRebaseline,
391
+ };