ultra-builder-pro-cli 0.14.0 → 0.16.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 (163) 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 +77 -1
  8. package/README.md +46 -24
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/handbook.cjs +2 -1
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +3 -2
  16. package/agents/review-coordinator.md +9 -6
  17. package/commands/ultra-change.md +1 -1
  18. package/commands/ultra-plan.md +1 -1
  19. package/commands/ultra-research.md +1 -1
  20. package/commands/ultra-think.md +2 -2
  21. package/docs/AGENT-CONTEXT.md +43 -19
  22. package/docs/ARCHITECTURE.md +54 -13
  23. package/docs/PLAN.zh-CN.md +4 -2
  24. package/docs/ROADMAP.md +10 -4
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  26. package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
  27. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  28. package/docs/WORKFLOW-LIFECYCLE.md +342 -0
  29. package/hooks/README.md +6 -1
  30. package/hooks/health_check.py +12 -3
  31. package/hooks/subagent_tracker.py +52 -77
  32. package/mcp-server/hook-event.cjs +34 -0
  33. package/mcp-server/lib/archive-journal.cjs +17 -5
  34. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  35. package/mcp-server/lib/change-workflow.cjs +598 -85
  36. package/mcp-server/lib/context-spine.cjs +305 -25
  37. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  38. package/mcp-server/lib/doctor.cjs +42 -3
  39. package/mcp-server/lib/init-project.cjs +210 -16
  40. package/mcp-server/lib/plan-store.cjs +4 -1
  41. package/mcp-server/lib/pricing.cjs +7 -10
  42. package/mcp-server/lib/project-breadcrumb.cjs +15 -0
  43. package/mcp-server/lib/projector.cjs +57 -3
  44. package/mcp-server/lib/spec-learning.cjs +79 -2
  45. package/mcp-server/lib/state-db.cjs +168 -8
  46. package/mcp-server/lib/state-ops.cjs +160 -9
  47. package/mcp-server/lib/workflow-state.cjs +2305 -0
  48. package/mcp-server/server.cjs +215 -15
  49. package/orchestrator/daemon.cjs +1 -2
  50. package/orchestrator/dispatch-rules.cjs +5 -31
  51. package/orchestrator/planner/plan-builder.cjs +21 -12
  52. package/package.json +13 -3
  53. package/skills/ultra-change/SKILL.md +94 -80
  54. package/skills/ultra-change/references/change-contract.md +36 -0
  55. package/skills/ultra-deliver/SKILL.md +76 -79
  56. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  57. package/skills/ultra-dev/SKILL.md +62 -59
  58. package/skills/ultra-doctor/SKILL.md +61 -82
  59. package/skills/ultra-init/SKILL.md +101 -103
  60. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  61. package/skills/ultra-plan/SKILL.md +99 -85
  62. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  63. package/skills/ultra-research/SKILL.md +102 -60
  64. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  65. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  66. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  67. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  68. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  69. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  70. package/skills/ultra-research/references/10-user-personas.md +29 -0
  71. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  72. package/skills/ultra-research/references/20-user-stories.md +30 -0
  73. package/skills/ultra-research/references/21-features-scope.md +29 -0
  74. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  75. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  76. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  77. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  78. package/skills/ultra-research/references/40-deployment.md +29 -0
  79. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  80. package/skills/ultra-research/references/99-synthesis.md +35 -0
  81. package/skills/ultra-review/SKILL.md +63 -73
  82. package/skills/ultra-review/references/review-modes.md +27 -0
  83. package/skills/ultra-review/references/unified-schema.md +25 -0
  84. package/skills/ultra-review/scripts/review_wait.py +40 -3
  85. package/skills/ultra-status/SKILL.md +41 -41
  86. package/skills/ultra-test/SKILL.md +54 -55
  87. package/skills/ultra-think/SKILL.md +26 -12
  88. package/skills/ultra-think/references/decision-dialogue.md +130 -0
  89. package/skills/ultra-verify/SKILL.md +1 -1
  90. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  91. package/spec/cli-protocol.md +17 -3
  92. package/spec/mcp-tools.yaml +683 -64
  93. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  94. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  95. package/spec/schemas/mcp-tools.schema.json +1 -1
  96. package/spec/schemas/state-db.sql +140 -1
  97. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  98. package/spec/schemas/test-report.v1.schema.json +194 -0
  99. package/ultra-tools/cli.cjs +1 -1
  100. package/ultra-tools/commands/migrate.cjs +3 -3
  101. package/ultra-tools/commands/status.cjs +228 -12
  102. package/.ultra-template/delivery-report.json +0 -9
  103. package/.ultra-template/test-report.json +0 -8
  104. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  105. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  106. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  107. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  108. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  109. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  110. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  111. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  112. package/adapters/_shared/tests/validate.test.cjs +0 -30
  113. package/adapters/tests/claude.test.cjs +0 -138
  114. package/adapters/tests/codex-hook.test.cjs +0 -142
  115. package/adapters/tests/codex.test.cjs +0 -430
  116. package/adapters/tests/kimi-hook.test.cjs +0 -149
  117. package/adapters/tests/kimi.test.cjs +0 -259
  118. package/adapters/tests/opencode.test.cjs +0 -357
  119. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  120. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  121. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  122. package/mcp-server/lib/context-spine.test.cjs +0 -93
  123. package/mcp-server/lib/doctor.test.cjs +0 -148
  124. package/mcp-server/lib/plan-store.test.cjs +0 -132
  125. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  126. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  127. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  128. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  129. package/mcp-server/lib/state-db.test.cjs +0 -314
  130. package/mcp-server/lib/task-expander.test.cjs +0 -169
  131. package/mcp-server/lib/topo.test.cjs +0 -158
  132. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  133. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  134. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  135. package/mcp-server/tests/concurrency.test.cjs +0 -112
  136. package/mcp-server/tests/db-ops.test.cjs +0 -121
  137. package/mcp-server/tests/init-project.test.cjs +0 -393
  138. package/mcp-server/tests/migrate.test.cjs +0 -232
  139. package/mcp-server/tests/projector.test.cjs +0 -162
  140. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  141. package/mcp-server/tests/staleness.test.cjs +0 -190
  142. package/mcp-server/tests/state-db.test.cjs +0 -106
  143. package/mcp-server/tests/state-ops.test.cjs +0 -449
  144. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  145. package/mcp-server/tests/telemetry.test.cjs +0 -219
  146. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  147. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  148. package/orchestrator/tests/daemon.test.cjs +0 -311
  149. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  150. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  151. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  152. package/orchestrator/tests/recovery.test.cjs +0 -190
  153. package/orchestrator/tests/session-runner.test.cjs +0 -314
  154. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  155. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  156. package/skills/ultra-research/references/architecture.md +0 -38
  157. package/skills/ultra-research/references/problem-product.md +0 -39
  158. package/skills/ultra-research/references/quality-delivery.md +0 -34
  159. package/skills/ultra-research/references/synthesis.md +0 -38
  160. package/skills/ultra-research/references/users-scope.md +0 -40
  161. package/ultra-tools/legacy-memory.test.cjs +0 -80
  162. package/ultra-tools/status-command.test.cjs +0 -181
  163. 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
- });