ultra-builder-pro-cli 0.13.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 (172) 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 +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  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 +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,393 +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 { execFileSync } = require('node:child_process');
9
- const Database = require('better-sqlite3');
10
-
11
- const {
12
- initProject, InitProjectError, DEFAULT_TEMPLATE, classifyRepository,
13
- } = require('../lib/init-project.cjs');
14
- const { EXPECTED_VERSION } = require('../lib/state-db.cjs');
15
-
16
- function mkTempDir(prefix = 'ubp-init-test-') {
17
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
18
- }
19
-
20
- function cleanup(dir) {
21
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* best effort */ }
22
- }
23
-
24
- test('initProject copies bundled template into .ultra/', () => {
25
- const target = mkTempDir();
26
- try {
27
- const r = initProject({
28
- target_dir: target,
29
- project_name: 'demo',
30
- project_type: 'cli',
31
- stack: 'node',
32
- });
33
- assert.equal(r.status, 'created');
34
- assert.equal(r.mode, 'greenfield');
35
- assert.equal(r.created_path, path.join(target, '.ultra'));
36
- assert.ok(r.copied_files.includes('tasks/tasks.json'));
37
- assert.ok(r.copied_files.includes('specs/product.md'));
38
- assert.ok(r.copied_files.includes('changes/active/.gitkeep'));
39
- assert.ok(r.copied_files.includes('changes/archive/.gitkeep'));
40
- assert.equal(r.state_db_path, path.join(target, '.ultra', 'state.db'));
41
- assert.ok(fs.existsSync(r.state_db_path));
42
- const db = new Database(r.state_db_path, { readonly: true });
43
- try {
44
- const version = db.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
45
- assert.equal(version.version, EXPECTED_VERSION);
46
- assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
47
- const baseline = db.prepare('SELECT mode, status, project_name FROM baselines').get();
48
- assert.deepEqual(baseline, { mode: 'greenfield', status: 'draft', project_name: 'demo' });
49
- } finally {
50
- db.close();
51
- }
52
- assert.ok(!r.copied_files.some((f) => f.endsWith('.DS_Store')));
53
- } finally { cleanup(target); }
54
- });
55
-
56
- test('initProject auto-detects existing source as brownfield and starts adoption', () => {
57
- const target = mkTempDir();
58
- try {
59
- fs.mkdirSync(path.join(target, 'src'));
60
- fs.writeFileSync(path.join(target, 'package.json'), '{"name":"legacy-app"}\n');
61
- fs.writeFileSync(path.join(target, 'src', 'index.js'), 'module.exports = true;\n');
62
- const result = initProject({ target_dir: target, project_name: 'legacy-app' });
63
- assert.equal(result.mode, 'brownfield');
64
- const db = new Database(result.state_db_path, { readonly: true });
65
- try {
66
- const baseline = db.prepare('SELECT mode, status, scope_json FROM baselines').get();
67
- assert.equal(baseline.mode, 'brownfield');
68
- assert.equal(baseline.status, 'adopting');
69
- assert.deepEqual(JSON.parse(baseline.scope_json), ['.']);
70
- } finally {
71
- db.close();
72
- }
73
- } finally { cleanup(target); }
74
- });
75
-
76
- test('auto classification keeps a documentation-and-manifest-only skeleton greenfield', () => {
77
- const target = mkTempDir();
78
- try {
79
- fs.writeFileSync(path.join(target, 'README.md'), '# New service skeleton\n');
80
- fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
81
- name: 'new-service', private: true, scripts: { test: 'node --test' },
82
- }));
83
- const profile = classifyRepository(target);
84
- assert.equal(profile.mode, 'greenfield');
85
- assert.equal(profile.repository_kind, 'single');
86
- assert.deepEqual(profile.source_signals, []);
87
-
88
- const result = initProject({ target_dir: target, project_name: 'new-service' });
89
- assert.equal(result.mode, 'greenfield');
90
- assert.deepEqual(result.repository_profile.verification_commands, ['npm test']);
91
- } finally { cleanup(target); }
92
- });
93
-
94
- test('auto classification records bounded monorepo evidence for brownfield adoption', () => {
95
- const target = mkTempDir();
96
- try {
97
- fs.mkdirSync(path.join(target, 'packages', 'api', 'src'), { recursive: true });
98
- fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
99
- name: 'legacy-suite', private: true, workspaces: ['packages/*'],
100
- scripts: { build: 'turbo build', test: 'turbo test', lint: 'turbo lint' },
101
- }));
102
- fs.writeFileSync(path.join(target, 'packages', 'api', 'package.json'), '{"name":"api"}\n');
103
- fs.writeFileSync(path.join(target, 'packages', 'api', 'src', 'index.js'), 'module.exports = true;\n');
104
- fs.writeFileSync(path.join(target, 'Dockerfile'), 'FROM node:22\n');
105
-
106
- const result = initProject({ target_dir: target, project_name: 'legacy-suite' });
107
- assert.equal(result.mode, 'brownfield');
108
- assert.equal(result.repository_profile.repository_kind, 'monorepo');
109
- assert.deepEqual(result.repository_profile.workspace_roots, ['packages/api']);
110
- assert.ok(result.repository_profile.source_signals.includes('packages/api/src/index.js'));
111
- assert.ok(result.repository_profile.deployment_signals.includes('Dockerfile'));
112
- assert.deepEqual(
113
- result.repository_profile.verification_commands,
114
- ['npm run build', 'npm test', 'npm run lint'],
115
- );
116
-
117
- const db = new Database(result.state_db_path, { readonly: true });
118
- try {
119
- const row = db.prepare(
120
- 'SELECT repository_branch, worktree_state, classification_json FROM baselines',
121
- ).get();
122
- assert.equal(row.repository_branch, null);
123
- assert.equal(row.worktree_state, 'unavailable');
124
- assert.equal(JSON.parse(row.classification_json).repository_kind, 'monorepo');
125
- } finally { db.close(); }
126
- } finally { cleanup(target); }
127
- });
128
-
129
- test('brownfield initialization persists an explicitly selected monorepo scope', () => {
130
- const target = mkTempDir();
131
- try {
132
- fs.mkdirSync(path.join(target, 'packages', 'api', 'src'), { recursive: true });
133
- fs.mkdirSync(path.join(target, 'packages', 'web', 'src'), { recursive: true });
134
- fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
135
- name: 'legacy-suite', private: true, workspaces: ['packages/*'],
136
- }));
137
- fs.writeFileSync(path.join(target, 'packages', 'api', 'src', 'index.js'), 'module.exports = true;\n');
138
- fs.writeFileSync(path.join(target, 'packages', 'web', 'src', 'index.js'), 'module.exports = true;\n');
139
-
140
- const result = initProject({
141
- target_dir: target,
142
- project_name: 'legacy-suite',
143
- scope: ['packages/api'],
144
- });
145
-
146
- const db = new Database(result.state_db_path, { readonly: true });
147
- try {
148
- const baseline = db.prepare('SELECT scope_json FROM baselines').get();
149
- assert.deepEqual(JSON.parse(baseline.scope_json), ['packages/api']);
150
- assert.deepEqual(result.baseline.scope, ['packages/api']);
151
- } finally { db.close(); }
152
- } finally { cleanup(target); }
153
- });
154
-
155
- test('maintenance CLI forwards an explicit initialization mode', () => {
156
- const target = mkTempDir();
157
- try {
158
- fs.writeFileSync(path.join(target, 'README.md'), '# Intentional greenfield notes\n');
159
- const output = execFileSync(process.execPath, [
160
- path.resolve(__dirname, '..', '..', 'ultra-tools', 'cli.cjs'),
161
- 'task', 'init-project', '--target-dir', target, '--project-name', 'explicit-mode',
162
- '--mode', 'greenfield',
163
- ], { encoding: 'utf8' });
164
- const envelope = JSON.parse(output);
165
- assert.equal(envelope.ok, true);
166
- assert.equal(envelope.data.mode, 'greenfield');
167
- } finally { cleanup(target); }
168
- });
169
-
170
- test('initProject keeps project metadata in authoritative baseline state and tasks.json as a projection', () => {
171
- const target = mkTempDir();
172
- try {
173
- initProject({
174
- target_dir: target,
175
- project_name: 'meta-demo',
176
- project_type: 'web',
177
- stack: 'next',
178
- });
179
- const tasksJson = JSON.parse(fs.readFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
180
- assert.equal(tasksJson.schema_version, '4.5');
181
- assert.equal(tasksJson.source, '.ultra/state.db');
182
- assert.deepEqual(tasksJson.tasks, []);
183
- assert.equal('project' in tasksJson, false);
184
- const db = new Database(path.join(target, '.ultra', 'state.db'), { readonly: true });
185
- try {
186
- const baseline = db.prepare('SELECT project_name, project_type, stack FROM baselines').get();
187
- assert.deepEqual(baseline, { project_name: 'meta-demo', project_type: 'web', stack: 'next' });
188
- } finally {
189
- db.close();
190
- }
191
- } finally { cleanup(target); }
192
- });
193
-
194
- test('initProject records the declared project_initialized lifecycle event', () => {
195
- const rootDir = mkTempDir();
196
- try {
197
- const result = initProject({ target_dir: rootDir, project_name: 'events-fixture' });
198
- const db = new Database(result.state_db_path, { readonly: true });
199
- try {
200
- const events = db.prepare('SELECT type, payload_json FROM events ORDER BY id').all();
201
- assert.deepEqual(events.map((row) => row.type), ['baseline_started', 'project_initialized']);
202
- assert.deepEqual(JSON.parse(events[1].payload_json), {
203
- project_name: 'events-fixture', mode: 'greenfield', baseline_id: 'project-baseline',
204
- });
205
- } finally {
206
- db.close();
207
- }
208
- } finally {
209
- fs.rmSync(rootDir, { recursive: true, force: true });
210
- }
211
- });
212
-
213
- test('initProject refuses when .ultra/ already exists and overwrite=false', () => {
214
- const target = mkTempDir();
215
- try {
216
- initProject({ target_dir: target, project_name: 'first' });
217
- assert.throws(
218
- () => initProject({ target_dir: target, project_name: 'again' }),
219
- (err) => err instanceof InitProjectError && err.code === 'ULTRA_DIR_EXISTS',
220
- );
221
- } finally { cleanup(target); }
222
- });
223
-
224
- test('initProject resume preserves existing artifacts and installs only missing current scaffold files', () => {
225
- const target = mkTempDir();
226
- try {
227
- fs.mkdirSync(path.join(target, '.ultra', 'specs'), { recursive: true });
228
- fs.writeFileSync(path.join(target, '.ultra', 'specs', 'product.md'), '# Existing product contract\n');
229
-
230
- const result = initProject({
231
- target_dir: target, project_name: 'existing-project', mode: 'brownfield', resume: true,
232
- });
233
- assert.equal(result.status, 'resumed');
234
- assert.equal(
235
- fs.readFileSync(path.join(target, '.ultra', 'specs', 'product.md'), 'utf8'),
236
- '# Existing product contract\n',
237
- );
238
- assert.ok(fs.existsSync(path.join(target, '.ultra', 'specs', 'architecture.md')));
239
- assert.ok(result.copied_files.includes('specs/architecture.md'));
240
- assert.equal(result.copied_files.includes('specs/product.md'), false);
241
-
242
- const second = initProject({
243
- target_dir: target, project_name: 'existing-project', resume: true,
244
- });
245
- assert.equal(second.status, 'resumed');
246
- assert.deepEqual(second.copied_files, []);
247
- const db = new Database(result.state_db_path, { readonly: true });
248
- try {
249
- assert.equal(db.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
250
- } finally { db.close(); }
251
- } finally { cleanup(target); }
252
- });
253
-
254
- test('initProject resume refuses projection-only tasks until the supported import runs', () => {
255
- const target = mkTempDir();
256
- try {
257
- fs.mkdirSync(path.join(target, '.ultra', 'tasks'), { recursive: true });
258
- fs.writeFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), JSON.stringify({
259
- schema_version: '4.5', source: '.ultra/state.db',
260
- tasks: [{ id: 'legacy', title: 'Legacy task', type: 'feature', priority: 'P1', status: 'pending' }],
261
- }));
262
- assert.throws(
263
- () => initProject({ target_dir: target, project_name: 'legacy', resume: true }),
264
- (error) => error.code === 'LEGACY_STATE_MIGRATION_REQUIRED'
265
- && /--from=4\.5 --to=12\.0/.test(error.message),
266
- );
267
- assert.equal(fs.existsSync(path.join(target, '.ultra', 'state.db')), false);
268
- assert.equal(fs.existsSync(path.join(target, '.ultra', 'specs', 'product.md')), false);
269
- } finally { cleanup(target); }
270
- });
271
-
272
- test('initProject rejects conflicting resume and overwrite modes', () => {
273
- const target = mkTempDir();
274
- try {
275
- fs.mkdirSync(path.join(target, '.ultra'));
276
- assert.throws(
277
- () => initProject({
278
- target_dir: target, project_name: 'conflict', resume: true, overwrite: true,
279
- }),
280
- (error) => error.code === 'VALIDATION_ERROR' && /resume.*overwrite/i.test(error.message),
281
- );
282
- } finally { cleanup(target); }
283
- });
284
-
285
- test('initProject with overwrite=true backs up existing .ultra/', () => {
286
- const target = mkTempDir();
287
- try {
288
- initProject({ target_dir: target, project_name: 'first' });
289
- const sentinel = path.join(target, '.ultra', 'sentinel.txt');
290
- fs.writeFileSync(sentinel, 'before-backup');
291
-
292
- const r = initProject({ target_dir: target, project_name: 'second', overwrite: true });
293
- assert.equal(r.status, 'overwritten');
294
- assert.ok(r.backup_path);
295
- assert.ok(fs.existsSync(path.join(r.backup_path, 'sentinel.txt')));
296
- assert.equal(
297
- fs.readFileSync(path.join(r.backup_path, 'sentinel.txt'), 'utf8'),
298
- 'before-backup',
299
- );
300
- assert.ok(!fs.existsSync(sentinel));
301
- } finally { cleanup(target); }
302
- });
303
-
304
- test('initProject restores the prior .ultra when overwrite initialization fails', () => {
305
- const target = mkTempDir();
306
- const badTemplate = mkTempDir('ubp-bad-template-');
307
- try {
308
- initProject({ target_dir: target, project_name: 'first' });
309
- fs.writeFileSync(path.join(target, '.ultra', 'sentinel.txt'), 'restore-me');
310
- fs.cpSync(DEFAULT_TEMPLATE, badTemplate, { recursive: true });
311
- fs.writeFileSync(path.join(badTemplate, 'tasks', 'tasks.json'), '{not-json\n');
312
-
313
- assert.throws(
314
- () => initProject({
315
- target_dir: target,
316
- project_name: 'broken-overwrite',
317
- overwrite: true,
318
- source_template: badTemplate,
319
- }),
320
- (error) => error instanceof InitProjectError && error.code === 'IO_ERROR',
321
- );
322
- assert.equal(
323
- fs.readFileSync(path.join(target, '.ultra', 'sentinel.txt'), 'utf8'),
324
- 'restore-me',
325
- );
326
- assert.equal(
327
- fs.readdirSync(target).some((name) => name.startsWith('.ultra.backup.')),
328
- false,
329
- );
330
- } finally {
331
- cleanup(target);
332
- cleanup(badTemplate);
333
- }
334
- });
335
-
336
- test('initProject rejects empty project_name', () => {
337
- const target = mkTempDir();
338
- try {
339
- assert.throws(
340
- () => initProject({ target_dir: target, project_name: '' }),
341
- (err) => err instanceof InitProjectError && err.code === 'VALIDATION_ERROR',
342
- );
343
- } finally { cleanup(target); }
344
- });
345
-
346
- test('initProject rejects missing source_template', () => {
347
- const target = mkTempDir();
348
- try {
349
- assert.throws(
350
- () => initProject({
351
- target_dir: target,
352
- project_name: 'demo',
353
- source_template: '/tmp/does-not-exist-ubp-init',
354
- }),
355
- (err) => err instanceof InitProjectError && err.code === 'TEMPLATE_MISSING',
356
- );
357
- } finally { cleanup(target); }
358
- });
359
-
360
- test('initProject surface TARGET_NOT_DIR when target_dir is a file', () => {
361
- const parent = mkTempDir();
362
- const filePath = path.join(parent, 'not-a-dir.txt');
363
- fs.writeFileSync(filePath, 'x');
364
- try {
365
- assert.throws(
366
- () => initProject({ target_dir: filePath, project_name: 'demo' }),
367
- (err) => err instanceof InitProjectError && err.code === 'TARGET_NOT_DIR',
368
- );
369
- } finally { cleanup(parent); }
370
- });
371
-
372
- test('copied tree matches bundled templates/.ultra/ (diff-equal excluding .DS_Store)', () => {
373
- const target = mkTempDir();
374
- try {
375
- const r = initProject({ target_dir: target, project_name: 'diff-equal' });
376
- const templateFiles = [];
377
- (function walk(dir, prefix = '') {
378
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
379
- if (e.name === '.DS_Store') continue;
380
- const rel = prefix ? path.join(prefix, e.name) : e.name;
381
- if (e.isDirectory()) walk(path.join(dir, e.name), rel);
382
- else if (e.isFile()) templateFiles.push(rel);
383
- }
384
- })(DEFAULT_TEMPLATE);
385
- assert.deepEqual(r.copied_files.sort(), templateFiles.sort());
386
- for (const rel of templateFiles) {
387
- if (rel === 'tasks/tasks.json') continue; // metadata injected by design
388
- const src = fs.readFileSync(path.join(DEFAULT_TEMPLATE, rel));
389
- const dst = fs.readFileSync(path.join(r.created_path, rel));
390
- assert.deepEqual(dst, src, `content diff in ${rel}`);
391
- }
392
- } finally { cleanup(target); }
393
- });
@@ -1,232 +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 REPO_ROOT = path.resolve(__dirname, '..', '..');
11
- const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
12
- const FIXTURE = path.join(REPO_ROOT, 'spec', 'fixtures', 'v4.4-project');
13
-
14
- const { openStateDb, closeStateDb } = require('../lib/state-db.cjs');
15
-
16
- function tmpProject() {
17
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mig-'));
18
- copyRecursive(FIXTURE, dir);
19
- return dir;
20
- }
21
-
22
- function copyRecursive(src, dst) {
23
- fs.mkdirSync(dst, { recursive: true });
24
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
25
- const s = path.join(src, entry.name);
26
- const d = path.join(dst, entry.name);
27
- if (entry.isDirectory()) copyRecursive(s, d);
28
- else fs.copyFileSync(s, d);
29
- }
30
- }
31
-
32
- function runCli(args, opts = {}) {
33
- const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', ...opts });
34
- const lines = r.stdout.trim().split('\n').filter(Boolean);
35
- const last = lines[lines.length - 1] || '{}';
36
- return { code: r.status, envelope: JSON.parse(last), stderr: r.stderr };
37
- }
38
-
39
- test('migrate --dry prints the plan without writing state.db or backups', () => {
40
- const dir = tmpProject();
41
- try {
42
- const dbPath = path.join(dir, '.ultra', 'state.db');
43
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--dry', '--source-dir', dir]);
44
- assert.equal(r.code, 0);
45
- assert.equal(r.envelope.ok, true);
46
- assert.equal(r.envelope.data.mode, 'dry');
47
- assert.equal(r.envelope.data.tasks_to_insert, 3);
48
- assert.equal(r.envelope.data.events_to_insert, 6);
49
- assert.equal(r.envelope.data.warnings.length, 1, 'task-3 status mismatch must be flagged');
50
- assert.equal(r.envelope.data.warnings[0].task_id, 'task-3');
51
- assert.equal(fs.existsSync(dbPath), false, 'dry must not create state.db');
52
- const ultraEntries = fs.readdirSync(path.join(dir, '.ultra'));
53
- assert.equal(ultraEntries.some((n) => n.startsWith('backup-v4.4-')), false, 'dry must not create a backup');
54
- } finally {
55
- fs.rmSync(dir, { recursive: true, force: true });
56
- }
57
- });
58
-
59
- test('migrate forward inserts tasks + events, records migration_history, creates backup', () => {
60
- const dir = tmpProject();
61
- try {
62
- const dbPath = path.join(dir, '.ultra', 'state.db');
63
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
64
- assert.equal(r.code, 0);
65
- assert.equal(r.envelope.ok, true);
66
- assert.equal(r.envelope.data.mode, 'apply');
67
- assert.equal(r.envelope.data.tasks_inserted, 3);
68
- assert.equal(r.envelope.data.events_inserted, 6);
69
- assert.ok(fs.existsSync(r.envelope.data.backup_dir), 'backup dir must exist');
70
- assert.ok(fs.existsSync(dbPath), 'state.db must be written');
71
-
72
- const db = openStateDb(dbPath);
73
- const taskIds = db.prepare('SELECT id FROM tasks ORDER BY id').all().map((r) => r.id);
74
- assert.deepEqual(taskIds, ['task-1', 'task-2', 'task-3']);
75
-
76
- const migrated = db.prepare(
77
- 'SELECT id, deps, estimated_days, context_file, created_at, updated_at FROM tasks ORDER BY id',
78
- ).all();
79
- assert.deepEqual(JSON.parse(migrated[1].deps), ['task-1']);
80
- assert.equal(migrated[1].estimated_days, 1.5);
81
- assert.equal(migrated[1].context_file, '.ultra/tasks/contexts/task-2.md');
82
- for (const task of migrated) {
83
- assert.match(task.created_at, /^2026-04-15T00:00:00\.000Z$/);
84
- assert.match(task.updated_at, /^2026-04-16T00:00:00\.000Z$/);
85
- }
86
-
87
- // Status comes from tasks.json (task-3 = pending), not the context md (blocked)
88
- const t3 = db.prepare("SELECT status FROM tasks WHERE id = 'task-3'").get();
89
- assert.equal(t3.status, 'pending');
90
-
91
- const eventCount = db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
92
- assert.equal(eventCount, 6);
93
-
94
- const mig = db.prepare("SELECT direction, status FROM migration_history ORDER BY id").all();
95
- assert.equal(mig.length, 1);
96
- assert.equal(mig[0].direction, 'forward');
97
- assert.equal(mig[0].status, 'success');
98
- closeStateDb(db);
99
-
100
- const projected = JSON.parse(fs.readFileSync(
101
- path.join(dir, '.ultra', 'tasks', 'tasks.json'),
102
- 'utf8',
103
- ));
104
- assert.equal(projected.schema_version, '4.5');
105
- assert.equal(projected.source, '.ultra/state.db');
106
- assert.equal(projected.tasks.length, 3);
107
- assert.equal(projected.tasks[1].estimated_days, 1.5);
108
- assert.deepEqual(projected.tasks[1].deps, ['task-1']);
109
- assert.match(
110
- fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'contexts', 'task-2.md'), 'utf8'),
111
- /Wire JWT validation into the request pipeline/,
112
- );
113
- const contextTemplate = fs.readFileSync(
114
- path.join(dir, '.ultra', 'tasks', 'contexts', 'TEMPLATE.md'),
115
- 'utf8',
116
- );
117
- assert.doesNotMatch(contextTemplate, /> \*\*Status\*\*:/);
118
- assert.doesNotMatch(contextTemplate, /mid_workflow_recall|session_context/);
119
- assert.match(contextTemplate, /Preserve this acceptance item/);
120
- } finally {
121
- fs.rmSync(dir, { recursive: true, force: true });
122
- }
123
- });
124
-
125
- test('migrate imports a v4.5 projection into schema 12 authority and requires evidence-backed re-adoption', () => {
126
- const dir = tmpProject();
127
- try {
128
- const tasksPath = path.join(dir, '.ultra', 'tasks', 'tasks.json');
129
- const projection = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
130
- delete projection.version;
131
- projection.schema_version = '4.5';
132
- projection.source = '.ultra/state.db';
133
- fs.writeFileSync(tasksPath, `${JSON.stringify(projection, null, 2)}\n`);
134
-
135
- const r = runCli(['migrate', '--from=4.5', '--to=12.0', '--source-dir', dir]);
136
- assert.equal(r.code, 0);
137
- assert.equal(r.envelope.data.from, '4.5');
138
- assert.equal(r.envelope.data.to, '12.0');
139
- assert.match(path.basename(r.envelope.data.backup_dir), /^backup-v4\.5-/);
140
-
141
- const db = openStateDb(path.join(dir, '.ultra', 'state.db'));
142
- try {
143
- assert.equal(db.prepare('SELECT COUNT(*) AS count FROM tasks').get().count, 3);
144
- const baseline = db.prepare(
145
- "SELECT mode, status, gaps_json FROM baselines WHERE id = 'migrated-baseline'",
146
- ).get();
147
- assert.deepEqual(
148
- { mode: baseline.mode, status: baseline.status },
149
- { mode: 'migrated', status: 'adopting' },
150
- );
151
- assert.equal(JSON.parse(baseline.gaps_json)[0].id, 'legacy-rebaseline-required');
152
- } finally { closeStateDb(db); }
153
- } finally {
154
- fs.rmSync(dir, { recursive: true, force: true });
155
- }
156
- });
157
-
158
- test('migrate --rollback restores .ultra contents and writes a rollback row', () => {
159
- const dir = tmpProject();
160
- try {
161
- const dbPath = path.join(dir, '.ultra', 'state.db');
162
- const fwd = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
163
- assert.equal(fwd.code, 0);
164
-
165
- // Capture rollback audit BEFORE the db file is removed by reading from the
166
- // backup snapshot we'll restore from.
167
- const backupDir = fwd.envelope.data.backup_dir;
168
-
169
- const back = runCli(['migrate', '--from=4.4', '--to=4.5', '--rollback', '--source-dir', dir]);
170
- assert.equal(back.code, 0);
171
- assert.equal(back.envelope.ok, true);
172
- assert.equal(back.envelope.data.mode, 'rollback');
173
- assert.equal(back.envelope.data.backup_dir, backupDir);
174
-
175
- // state.db is gone
176
- assert.equal(fs.existsSync(dbPath), false);
177
-
178
- // .ultra/tasks/tasks.json restored to original v4.4 content
179
- const tasksJson = JSON.parse(fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
180
- assert.equal(tasksJson.version, '4.4');
181
- assert.equal(tasksJson.tasks.length, 3);
182
- closeStateDb(undefined);
183
- } finally {
184
- fs.rmSync(dir, { recursive: true, force: true });
185
- }
186
- });
187
-
188
- test('migrate refuses to merge a v4.4 projection into a non-empty state.db', () => {
189
- const dir = tmpProject();
190
- try {
191
- const dbPath = path.join(dir, '.ultra', 'state.db');
192
- const init = require('../lib/state-db.cjs').initStateDb(dbPath);
193
- require('../lib/state-ops.cjs').createTask(init.db, {
194
- id: 'existing', title: 'existing task', type: 'feature', priority: 'P1',
195
- });
196
- closeStateDb(init.db);
197
-
198
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
199
- assert.equal(r.code, 2);
200
- assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
201
- assert.match(r.envelope.error.message, /non-empty state\.db/);
202
-
203
- const db = openStateDb(dbPath);
204
- assert.deepEqual(db.prepare('SELECT id FROM tasks ORDER BY id').all(), [{ id: 'existing' }]);
205
- closeStateDb(db);
206
- } finally {
207
- fs.rmSync(dir, { recursive: true, force: true });
208
- }
209
- });
210
-
211
- test('migrate rejects unsupported --from / --to versions', () => {
212
- const r1 = runCli(['migrate', '--from=3.0', '--to=4.5', '--dry', '--source-dir', FIXTURE]);
213
- assert.equal(r1.code, 1);
214
- assert.equal(r1.envelope.error.code, 'UNSUPPORTED_VERSION');
215
-
216
- const r2 = runCli(['migrate', '--from=4.4', '--to=5.0', '--dry', '--source-dir', FIXTURE]);
217
- assert.equal(r2.code, 1);
218
- assert.equal(r2.envelope.error.code, 'UNSUPPORTED_VERSION');
219
- });
220
-
221
- test('migrate forward fails cleanly when tasks.json is missing', () => {
222
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mig-empty-'));
223
- fs.mkdirSync(path.join(dir, '.ultra'), { recursive: true });
224
- try {
225
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
226
- assert.equal(r.code, 2);
227
- assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
228
- assert.match(r.envelope.error.message, /tasks\.json missing/);
229
- } finally {
230
- fs.rmSync(dir, { recursive: true, force: true });
231
- }
232
- });