ultra-builder-pro-cli 0.14.0 → 0.15.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 (154) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,181 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 6.3 — ultra-tools status cost panel:
4
- // • `ultra-tools status --cost --json` prints {period, by_runtime,
5
- // top_tasks, total_cost_usd}.
6
- // • Default human-readable mode renders a summary table.
7
- // • --since 7d filters telemetry to the last 7 days.
8
-
9
- const { test } = require('node:test');
10
- const assert = require('node:assert/strict');
11
- const fs = require('node:fs');
12
- const os = require('node:os');
13
- const path = require('node:path');
14
-
15
- const { initStateDb, closeStateDb } = require('../mcp-server/lib/state-db.cjs');
16
- const ops = require('../mcp-server/lib/state-ops.cjs');
17
- const telemetry = require('../mcp-server/lib/telemetry.cjs');
18
- const statusCmd = require('./commands/status.cjs');
19
-
20
- function freshFixture() {
21
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-status-'));
22
- const file = path.join(dir, '.ultra', 'state.db');
23
- const init = initStateDb(file);
24
- return { dir, db: init.db };
25
- }
26
-
27
- function teardown(dir, db) {
28
- try { closeStateDb(db); } catch (_) { /* ignore */ }
29
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
30
- }
31
-
32
- function seedCalls(db, dir) {
33
- ops.createTask(db, { id: 't-cheap', title: 'cheap', type: 'feature', priority: 'P2' });
34
- ops.createTask(db, { id: 't-pricey', title: 'pricey', type: 'feature', priority: 'P1' });
35
- ops.createSession(db, { sid: 's-x', task_id: 't-cheap', runtime: 'claude', worktree_path: `${dir}/w1`, artifact_dir: `${dir}/a1` });
36
- ops.createSession(db, { sid: 's-y', task_id: 't-pricey', runtime: 'codex', worktree_path: `${dir}/w2`, artifact_dir: `${dir}/a2` });
37
- telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 't', session_id: 's-x', runtime: 'claude', tokens_input: 500, tokens_output: 100, rootDir: dir, payload: { model: 'claude-haiku-4-5' } });
38
- telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 't', session_id: 's-y', runtime: 'codex', tokens_input: 10000, tokens_output: 2000, rootDir: dir, payload: { model: 'gpt-5.4' } });
39
- telemetry.appendTelemetry(db, { event_type: 'tool_call', tool_name: 'task.list', session_id: null, rootDir: dir });
40
- }
41
-
42
- test('status --json --cost returns by_runtime + top_tasks + total_cost', () => {
43
- const { dir, db } = freshFixture();
44
- try {
45
- seedCalls(db, dir);
46
- const out = statusCmd.buildCostPanel(db, { since: null, limit: 10 });
47
- assert.ok(Array.isArray(out.by_runtime));
48
- assert.ok(Array.isArray(out.top_tasks));
49
- assert.ok(typeof out.total_cost_usd === 'number');
50
- const byR = Object.fromEntries(out.by_runtime.map((r) => [r.runtime, r]));
51
- assert.equal(byR.claude.calls, 1);
52
- assert.equal(byR.codex.calls, 1);
53
- assert.equal(byR.unknown.calls, 1); // CLI call with null session
54
- assert.equal(out.top_tasks[0].task_id, 't-pricey');
55
- assert.ok(out.total_cost_usd > 0);
56
- } finally { teardown(dir, db); }
57
- });
58
-
59
- test('buildCostPanel: empty db → zero totals, empty arrays', () => {
60
- const { dir, db } = freshFixture();
61
- try {
62
- const out = statusCmd.buildCostPanel(db, {});
63
- assert.equal(out.total_cost_usd, 0);
64
- assert.equal(out.by_runtime.length, 0);
65
- assert.equal(out.top_tasks.length, 0);
66
- assert.deepEqual(out.baseline, {
67
- id: null, mode: null, status: 'missing', health: 'fail',
68
- repository_revision: null, blockers: ['BASELINE_MISSING'], warnings: [],
69
- });
70
- } finally { teardown(dir, db); }
71
- });
72
-
73
- test('status exposes an in-progress brownfield baseline from authoritative state', () => {
74
- const { dir, db } = freshFixture();
75
- try {
76
- db.prepare(
77
- `INSERT INTO baselines (id, project_name, mode, status)
78
- VALUES ('adoption', 'legacy', 'brownfield', 'adopting')`,
79
- ).run();
80
- const out = statusCmd.buildCostPanel(db, { rootDir: dir });
81
- assert.equal(out.baseline.id, 'adoption');
82
- assert.equal(out.baseline.mode, 'brownfield');
83
- assert.equal(out.baseline.status, 'adopting');
84
- assert.deepEqual(out.baseline.blockers, ['BASELINE_NOT_READY:adopting']);
85
- assert.match(statusCmd.renderHuman(out), /Baseline: brownfield\/adopting.*blocked/i);
86
- } finally { teardown(dir, db); }
87
- });
88
-
89
- test('status reports a legacy schema as migration-required instead of throwing SQLite errors', () => {
90
- const { dir, db } = freshFixture();
91
- try {
92
- db.exec('DROP TABLE baselines');
93
- const out = statusCmd.buildCostPanel(db, { rootDir: dir });
94
- assert.equal(out.baseline.status, 'migration_required');
95
- assert.deepEqual(out.baseline.blockers, ['BASELINE_SCHEMA_MIGRATION_REQUIRED']);
96
- assert.match(statusCmd.renderHuman(out), /migration_required/i);
97
- } finally { teardown(dir, db); }
98
- });
99
-
100
- test('status summarizes the authoritative baseline gap ledger', () => {
101
- const { dir, db } = freshFixture();
102
- try {
103
- db.prepare(
104
- `INSERT INTO baselines (id, project_name, mode, status, gaps_json)
105
- VALUES ('adoption', 'legacy', 'brownfield', 'adopting', ?)`,
106
- ).run(JSON.stringify([
107
- { id: 'blocker', category: 'baseline_blocker', status: 'open', blocking: true, summary: 'Missing proof', evidence_refs: [] },
108
- { id: 'debt', category: 'technical_debt', status: 'accepted', blocking: false, summary: 'Slow test', evidence_refs: [] },
109
- ]));
110
- const out = statusCmd.buildCostPanel(db, { rootDir: dir });
111
- assert.deepEqual(out.baseline.gaps, {
112
- total: 2, open: 1, blocking: 1,
113
- by_category: { baseline_blocker: 1, technical_debt: 1 },
114
- });
115
- } finally { teardown(dir, db); }
116
- });
117
-
118
- test('buildCostPanel: since cutoff in future → all filtered out', () => {
119
- const { dir, db } = freshFixture();
120
- try {
121
- seedCalls(db, dir);
122
- const future = new Date(Date.now() + 60 * 1000).toISOString();
123
- const out = statusCmd.buildCostPanel(db, { since: future });
124
- assert.equal(out.by_runtime.length, 0);
125
- assert.equal(out.top_tasks.length, 0);
126
- } finally { teardown(dir, db); }
127
- });
128
-
129
- test('parseSince: 7d / 24h / iso passthrough', () => {
130
- const now = Date.parse('2026-04-17T12:00:00Z');
131
- const sevenDaysAgo = statusCmd.parseSince('7d', now);
132
- const expectedDayCutoff = new Date(now - 7 * 24 * 3600 * 1000).toISOString();
133
- assert.equal(sevenDaysAgo, expectedDayCutoff);
134
- assert.equal(statusCmd.parseSince('24h', now), new Date(now - 24 * 3600 * 1000).toISOString());
135
- assert.equal(statusCmd.parseSince('2026-04-10T00:00:00Z'), '2026-04-10T00:00:00Z');
136
- assert.equal(statusCmd.parseSince(null), null);
137
- });
138
-
139
- test('parseSince: invalid → throws', () => {
140
- assert.throws(() => statusCmd.parseSince('bogus'), /since/i);
141
- });
142
-
143
- test('renderHuman: includes key section headers', () => {
144
- const { dir, db } = freshFixture();
145
- try {
146
- seedCalls(db, dir);
147
- const panel = statusCmd.buildCostPanel(db, {});
148
- const text = statusCmd.renderHuman(panel);
149
- assert.match(text, /Cost by runtime/i);
150
- assert.match(text, /Top tasks/i);
151
- assert.match(text, /claude/);
152
- assert.match(text, /codex/);
153
- } finally { teardown(dir, db); }
154
- });
155
-
156
- test('dispatch: --json echoes buildCostPanel output shape', () => {
157
- const { dir, db } = freshFixture();
158
- try {
159
- seedCalls(db, dir);
160
- closeStateDb(db);
161
- const captured = [];
162
- const origWrite = process.stdout.write.bind(process.stdout);
163
- process.stdout.write = (chunk) => { captured.push(String(chunk)); return true; };
164
- const envBackup = process.env.UBP_DB_PATH;
165
- process.env.UBP_DB_PATH = path.join(dir, '.ultra', 'state.db');
166
- try {
167
- const code = statusCmd.dispatch(['--cost', '--json']);
168
- assert.equal(code, 0);
169
- } finally {
170
- process.stdout.write = origWrite;
171
- if (envBackup === undefined) delete process.env.UBP_DB_PATH; else process.env.UBP_DB_PATH = envBackup;
172
- }
173
- const joined = captured.join('');
174
- const parsed = JSON.parse(joined);
175
- assert.ok(parsed.ok);
176
- assert.ok(parsed.data.by_runtime);
177
- assert.ok(parsed.data.top_tasks);
178
- } finally {
179
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
180
- }
181
- });
@@ -1,309 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { spawnSync } = require('node:child_process');
9
-
10
- const { initStateDb, closeStateDb } = require('../mcp-server/lib/state-db.cjs');
11
-
12
- const CLI = path.join(__dirname, 'cli.cjs');
13
-
14
- function fixture() {
15
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-cli-'));
16
- const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
17
- db.prepare(
18
- `INSERT INTO baselines
19
- (id, project_name, mode, status, approved_by, approval_note, converged_at)
20
- VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
21
- ).run(new Date().toISOString());
22
- closeStateDb(db);
23
- return rootDir;
24
- }
25
-
26
- function invoke(rootDir, args) {
27
- return spawnSync(process.execPath, [CLI, 'system', ...args], {
28
- cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
29
- });
30
- }
31
-
32
- test('system doctor provides a read-only JSON health envelope', () => {
33
- const rootDir = fixture();
34
- try {
35
- const result = invoke(rootDir, ['doctor']);
36
- assert.equal(result.status, 0, result.stderr);
37
- const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
38
- assert.equal(envelope.ok, true);
39
- assert.equal(envelope.data.status, 'healthy');
40
- assert.equal(envelope.data.repair_performed, false);
41
- } finally {
42
- fs.rmSync(rootDir, { recursive: true, force: true });
43
- }
44
- });
45
-
46
- test('system doctor --repair creates a backup before recovery', () => {
47
- const rootDir = fixture();
48
- try {
49
- const result = invoke(rootDir, ['doctor', '--repair']);
50
- assert.equal(result.status, 0, result.stderr);
51
- const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
52
- assert.equal(envelope.ok, true);
53
- assert.equal(envelope.data.repair_performed, true);
54
- assert.ok(fs.existsSync(envelope.data.backup_path));
55
- } finally {
56
- fs.rmSync(rootDir, { recursive: true, force: true });
57
- }
58
- });
59
-
60
- test('system doctor --repair migrates an older schema only after preserving a pre-migration backup', () => {
61
- const rootDir = fixture();
62
- try {
63
- const dbPath = path.join(rootDir, '.ultra', 'state.db');
64
- const Database = require('better-sqlite3');
65
- const db = new Database(dbPath);
66
- db.prepare("DELETE FROM schema_version WHERE version = '12.0'").run();
67
- db.close();
68
-
69
- const result = invoke(rootDir, ['doctor', '--repair']);
70
- assert.equal(result.status, 0, result.stderr);
71
- const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
72
- assert.equal(envelope.ok, true);
73
- assert.equal(envelope.data.schema_version, '12.0');
74
- assert.ok(fs.existsSync(envelope.data.schema_migration_backup_path));
75
- assert.ok(fs.existsSync(envelope.data.backup_path));
76
- } finally {
77
- fs.rmSync(rootDir, { recursive: true, force: true });
78
- }
79
- });
80
-
81
- test('system doctor reports a corrupt database as structured recovery guidance', () => {
82
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-corrupt-'));
83
- try {
84
- fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
85
- fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'not a sqlite database');
86
- const result = invoke(rootDir, ['doctor']);
87
- assert.equal(result.status, 2);
88
- const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
89
- assert.equal(envelope.ok, false);
90
- assert.equal(envelope.error.code, 'STATE_DB_CORRUPT');
91
- assert.match(envelope.error.message, /restore.*backup|rebaseline/i);
92
- } finally {
93
- fs.rmSync(rootDir, { recursive: true, force: true });
94
- }
95
- });
96
-
97
- test('system restore replaces only a corrupt state database from a verified managed backup', () => {
98
- const rootDir = fixture();
99
- try {
100
- const dbPath = path.join(rootDir, '.ultra', 'state.db');
101
- const backupsDir = path.join(rootDir, '.ultra', 'backups');
102
- fs.mkdirSync(backupsDir, { recursive: true });
103
- const sourceBackup = path.join(backupsDir, 'verified.db');
104
- fs.copyFileSync(dbPath, sourceBackup);
105
- fs.writeFileSync(dbPath, 'not a sqlite database');
106
-
107
- const refused = invoke(rootDir, ['restore', '--backup', sourceBackup]);
108
- assert.equal(refused.status, 1);
109
- assert.equal(JSON.parse(refused.stdout.trim()).error.code, 'CONFIRMATION_REQUIRED');
110
- assert.equal(fs.readFileSync(dbPath, 'utf8'), 'not a sqlite database');
111
-
112
- const restored = invoke(rootDir, [
113
- 'restore', '--backup', sourceBackup,
114
- '--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
115
- ]);
116
- assert.equal(restored.status, 0, restored.stderr);
117
- const envelope = JSON.parse(restored.stdout.trim());
118
- assert.equal(envelope.ok, true);
119
- assert.equal(envelope.data.schema_version, '12.0');
120
- assert.equal(envelope.data.source_backup, fs.realpathSync(sourceBackup));
121
- assert.ok(fs.existsSync(envelope.data.quarantined_state_path));
122
- assert.equal(
123
- fs.readFileSync(envelope.data.quarantined_state_path, 'utf8'),
124
- 'not a sqlite database',
125
- );
126
- const restoredDb = require('better-sqlite3')(dbPath, { readonly: true });
127
- try {
128
- assert.equal(restoredDb.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
129
- } finally { restoredDb.close(); }
130
- } finally {
131
- fs.rmSync(rootDir, { recursive: true, force: true });
132
- }
133
- });
134
-
135
- test('system rebaseline preserves corrupt authority and legacy projection before starting brownfield adoption', () => {
136
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-rebaseline-'));
137
- try {
138
- fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
139
- fs.mkdirSync(path.join(rootDir, '.ultra', 'tasks', 'contexts'), { recursive: true });
140
- fs.writeFileSync(path.join(rootDir, 'src', 'index.js'), 'module.exports = true;\n');
141
- fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'broken authority');
142
- const tasksPath = path.join(rootDir, '.ultra', 'tasks', 'tasks.json');
143
- fs.writeFileSync(tasksPath, `${JSON.stringify({
144
- schema_version: '4.5', source: '.ultra/state.db',
145
- tasks: [{ id: 'legacy-task', title: 'Preserved task', type: 'feature', priority: 'P1', status: 'pending' }],
146
- }, null, 2)}\n`);
147
- fs.writeFileSync(
148
- path.join(rootDir, '.ultra', 'tasks', 'contexts', 'legacy-task.md'),
149
- '# Preserved task context\n',
150
- );
151
-
152
- const result = invoke(rootDir, [
153
- 'rebaseline', '--project-name', 'legacy-app',
154
- '--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
155
- ]);
156
- assert.equal(result.status, 0, result.stderr);
157
- const envelope = JSON.parse(result.stdout.trim());
158
- assert.equal(envelope.ok, true);
159
- assert.equal(envelope.data.baseline.mode, 'brownfield');
160
- assert.equal(envelope.data.baseline.status, 'adopting');
161
- assert.ok(fs.existsSync(envelope.data.quarantined_state_path));
162
- assert.ok(fs.existsSync(envelope.data.legacy_projection_path));
163
- assert.equal(
164
- JSON.parse(fs.readFileSync(envelope.data.legacy_projection_path, 'utf8')).tasks[0].id,
165
- 'legacy-task',
166
- );
167
- assert.ok(fs.existsSync(path.join(
168
- envelope.data.recovery_backup_dir, 'tasks', 'contexts', 'legacy-task.md',
169
- )));
170
- assert.equal(
171
- fs.existsSync(path.join(rootDir, '.ultra', 'tasks', 'contexts', 'legacy-task.md')),
172
- false,
173
- );
174
- assert.deepEqual(JSON.parse(fs.readFileSync(tasksPath, 'utf8')).tasks, []);
175
- } finally {
176
- fs.rmSync(rootDir, { recursive: true, force: true });
177
- }
178
- });
179
-
180
- test('system restore rolls back the corrupt database and sidecars when replacement initialization fails', () => {
181
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-restore-rollback-'));
182
- try {
183
- const ultraDir = path.join(rootDir, '.ultra');
184
- const backupsDir = path.join(ultraDir, 'backups');
185
- fs.mkdirSync(backupsDir, { recursive: true });
186
- const statePath = path.join(ultraDir, 'state.db');
187
- const originals = {
188
- '': Buffer.from('broken authority'),
189
- '-wal': Buffer.from('broken wal'),
190
- '-shm': Buffer.from('broken shm'),
191
- };
192
- for (const [suffix, content] of Object.entries(originals)) {
193
- fs.writeFileSync(`${statePath}${suffix}`, content);
194
- }
195
-
196
- const incompatibleBackup = path.join(backupsDir, 'structurally-incompatible.db');
197
- const backupDb = require('better-sqlite3')(incompatibleBackup);
198
- backupDb.exec("CREATE TABLE schema_version (version TEXT NOT NULL); INSERT INTO schema_version VALUES ('11.0');");
199
- backupDb.close();
200
-
201
- const result = invoke(rootDir, [
202
- 'restore', '--backup', incompatibleBackup,
203
- '--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
204
- ]);
205
- assert.notEqual(result.status, 0, result.stderr);
206
- const envelope = JSON.parse(result.stdout.trim());
207
- assert.equal(envelope.ok, false);
208
- assert.equal(envelope.error.code, 'STATE_RESTORE_FAILED');
209
- for (const [suffix, content] of Object.entries(originals)) {
210
- assert.deepEqual(fs.readFileSync(`${statePath}${suffix}`), content);
211
- }
212
- } finally {
213
- fs.rmSync(rootDir, { recursive: true, force: true });
214
- }
215
- });
216
-
217
- test('system rebaseline rolls back authority, sidecars, and the full task projection when adoption fails', () => {
218
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-rebaseline-rollback-'));
219
- try {
220
- const ultraDir = path.join(rootDir, '.ultra');
221
- const tasksDir = path.join(ultraDir, 'tasks');
222
- fs.mkdirSync(path.join(tasksDir, 'contexts'), { recursive: true });
223
- fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
224
- fs.writeFileSync(path.join(rootDir, 'src', 'index.js'), 'module.exports = true;\n');
225
- const statePath = path.join(ultraDir, 'state.db');
226
- const originals = {
227
- '': Buffer.from('broken authority'),
228
- '-wal': Buffer.from('broken wal'),
229
- '-shm': Buffer.from('broken shm'),
230
- };
231
- for (const [suffix, content] of Object.entries(originals)) {
232
- fs.writeFileSync(`${statePath}${suffix}`, content);
233
- }
234
- const tasksPath = path.join(tasksDir, 'tasks.json');
235
- const contextPath = path.join(tasksDir, 'contexts', 'legacy.md');
236
- const tasksContent = Buffer.from('{"schema_version":"4.5","source":".ultra/state.db","tasks":[]}\n');
237
- const contextContent = Buffer.from('# Legacy context\n');
238
- fs.writeFileSync(tasksPath, tasksContent);
239
- fs.writeFileSync(contextPath, contextContent);
240
-
241
- const result = invoke(rootDir, [
242
- 'rebaseline', '--project-name', 'legacy-app', '--scope', 'missing-workspace',
243
- '--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
244
- ]);
245
- assert.notEqual(result.status, 0, result.stderr);
246
- const envelope = JSON.parse(result.stdout.trim());
247
- assert.equal(envelope.ok, false);
248
- assert.equal(envelope.error.code, 'IO_ERROR');
249
- for (const [suffix, content] of Object.entries(originals)) {
250
- assert.deepEqual(fs.readFileSync(`${statePath}${suffix}`), content);
251
- }
252
- assert.deepEqual(fs.readFileSync(tasksPath), tasksContent);
253
- assert.deepEqual(fs.readFileSync(contextPath), contextContent);
254
- } finally {
255
- fs.rmSync(rootDir, { recursive: true, force: true });
256
- }
257
- });
258
-
259
- test('system restore rejects backups outside the managed project backup directory', () => {
260
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-external-backup-'));
261
- const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-external-backup-'));
262
- try {
263
- fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
264
- fs.mkdirSync(path.join(rootDir, '.ultra', 'backups'), { recursive: true });
265
- fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'broken authority');
266
- const external = path.join(externalDir, 'state.db');
267
- fs.writeFileSync(external, 'not trusted');
268
- const result = invoke(rootDir, [
269
- 'restore', '--backup', external,
270
- '--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
271
- ]);
272
- assert.equal(result.status, 1);
273
- assert.equal(JSON.parse(result.stdout.trim()).error.code, 'BACKUP_OUTSIDE_MANAGED_ROOT');
274
- } finally {
275
- fs.rmSync(rootDir, { recursive: true, force: true });
276
- fs.rmSync(externalDir, { recursive: true, force: true });
277
- }
278
- });
279
-
280
- test('system corrupt-state recovery commands refuse to replace readable authority', () => {
281
- const rootDir = fixture();
282
- try {
283
- const backupsDir = path.join(rootDir, '.ultra', 'backups');
284
- fs.mkdirSync(backupsDir, { recursive: true });
285
- const backup = path.join(backupsDir, 'healthy.db');
286
- fs.copyFileSync(path.join(rootDir, '.ultra', 'state.db'), backup);
287
-
288
- const restore = invoke(rootDir, [
289
- 'restore', '--backup', backup,
290
- '--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
291
- ]);
292
- assert.equal(restore.status, 2);
293
- assert.equal(JSON.parse(restore.stdout.trim()).error.code, 'STATE_DB_NOT_CORRUPT');
294
-
295
- const rebaseline = invoke(rootDir, [
296
- 'rebaseline', '--project-name', 'fixture',
297
- '--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
298
- ]);
299
- assert.equal(rebaseline.status, 2);
300
- assert.equal(JSON.parse(rebaseline.stdout.trim()).error.code, 'STATE_DB_NOT_CORRUPT');
301
-
302
- const db = require('better-sqlite3')(path.join(rootDir, '.ultra', 'state.db'), { readonly: true });
303
- try {
304
- assert.equal(db.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
305
- } finally { db.close(); }
306
- } finally {
307
- fs.rmSync(rootDir, { recursive: true, force: true });
308
- }
309
- });