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,67 +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
-
10
- const { initStateDb, closeStateDb } = require('./state-db.cjs');
11
- const changes = require('./change-workflow.cjs');
12
- const learning = require('./spec-learning.cjs');
13
-
14
- function fixture() {
15
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-learning-'));
16
- execFileSync('git', ['init', '-q'], { cwd: rootDir });
17
- execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: rootDir });
18
- execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: rootDir });
19
- fs.writeFileSync(path.join(rootDir, 'README.md'), '# Fixture\n');
20
- execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
21
- execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
22
- const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
23
- db.prepare(
24
- `INSERT INTO baselines
25
- (id, project_name, mode, status, approved_by, approval_note, converged_at)
26
- VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
27
- ).run(new Date().toISOString());
28
- changes.createChange(db, {
29
- id: 'learning-change', title: 'Learning change', kind: 'quick',
30
- intent: 'Exercise guarded specification-learning transitions.',
31
- }, { rootDir });
32
- return { rootDir, db };
33
- }
34
-
35
- test('spec learning cannot skip approval or mutate after apply', () => {
36
- const fx = fixture();
37
- try {
38
- learning.proposeSpecLearning(fx.db, {
39
- id: 'candidate-1', change_id: 'learning-change', target_ref: 'README.md',
40
- summary: 'A stable public contract was discovered.',
41
- }, { rootDir: fx.rootDir });
42
- assert.throws(
43
- () => learning.resolveSpecLearning(fx.db, {
44
- change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
45
- resolution: 'Applied.',
46
- }, { rootDir: fx.rootDir }),
47
- (error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
48
- );
49
- learning.resolveSpecLearning(fx.db, {
50
- change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'approve',
51
- }, { rootDir: fx.rootDir });
52
- learning.resolveSpecLearning(fx.db, {
53
- change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
54
- resolution: 'Applied to README.md.',
55
- }, { rootDir: fx.rootDir });
56
- assert.throws(
57
- () => learning.resolveSpecLearning(fx.db, {
58
- change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'reject',
59
- resolution: 'Too late.',
60
- }, { rootDir: fx.rootDir }),
61
- (error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
62
- );
63
- } finally {
64
- closeStateDb(fx.db);
65
- fs.rmSync(fx.rootDir, { recursive: true, force: true });
66
- }
67
- });
@@ -1,314 +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
-
9
- const {
10
- EXPECTED_VERSION,
11
- REQUIRED_TABLES,
12
- SCHEMA_FILE,
13
- initStateDb,
14
- closeStateDb,
15
- openStateDb,
16
- tableNames,
17
- } = require('./state-db.cjs');
18
- const stateOps = require('./state-ops.cjs');
19
-
20
- function tmpDbPath(prefix = 'ubp-state') {
21
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
22
- return { dir, file: path.join(dir, 'state.db') };
23
- }
24
-
25
- test('initStateDb creates workflow tables without an Ultra memory store', () => {
26
- const { dir, file } = tmpDbPath();
27
- try {
28
- const init = initStateDb(file);
29
- assert.equal(init.created, true);
30
- assert.equal(init.schema_version, EXPECTED_VERSION);
31
- for (const t of REQUIRED_TABLES) {
32
- assert.ok(init.tables.includes(t), `missing table ${t}`);
33
- }
34
- assert.ok(!init.tables.includes('memory_entries'));
35
- assert.ok(!init.tables.includes('memory_fts'));
36
- closeStateDb(init.db);
37
- } finally {
38
- fs.rmSync(dir, { recursive: true, force: true });
39
- }
40
- });
41
-
42
- test('initStateDb applies WAL + busy_timeout + foreign_keys pragmas', () => {
43
- const { dir, file } = tmpDbPath();
44
- try {
45
- const { db } = initStateDb(file);
46
- assert.equal(db.pragma('journal_mode', { simple: true }), 'wal');
47
- assert.equal(db.pragma('busy_timeout', { simple: true }), 5000);
48
- assert.equal(db.pragma('foreign_keys', { simple: true }), 1);
49
- closeStateDb(db);
50
- } finally {
51
- fs.rmSync(dir, { recursive: true, force: true });
52
- }
53
- });
54
-
55
- test('initStateDb is idempotent — second call does not duplicate seed rows', () => {
56
- const { dir, file } = tmpDbPath();
57
- try {
58
- const first = initStateDb(file);
59
- const firstRows = first.db.prepare('SELECT COUNT(*) AS n FROM schema_version').get().n;
60
- closeStateDb(first.db);
61
-
62
- const second = initStateDb(file);
63
- assert.equal(second.created, false, 'second init should not recreate schema');
64
- assert.equal(second.schema_version, EXPECTED_VERSION);
65
-
66
- const secondRows = second.db.prepare('SELECT COUNT(*) AS n FROM schema_version').get().n;
67
- assert.equal(secondRows, firstRows, 'schema_version row count must not grow on re-init');
68
- closeStateDb(second.db);
69
- } finally {
70
- fs.rmSync(dir, { recursive: true, force: true });
71
- }
72
- });
73
-
74
- test('openStateDb on an empty file produces no tables until schema is applied', () => {
75
- const { dir, file } = tmpDbPath();
76
- try {
77
- const db = openStateDb(file);
78
- assert.deepEqual(tableNames(db), []);
79
- closeStateDb(db);
80
- } finally {
81
- fs.rmSync(dir, { recursive: true, force: true });
82
- }
83
- });
84
-
85
- test('initStateDb upgrades an exact schema 11 project without replacing its approved baseline', () => {
86
- const { dir, file } = tmpDbPath('ubp-schema-11-upgrade');
87
- try {
88
- const legacy = openStateDb(file);
89
- legacy.exec(`
90
- CREATE TABLE schema_version (
91
- version TEXT PRIMARY KEY,
92
- applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
93
- description TEXT
94
- );
95
- CREATE TABLE migration_history (
96
- id INTEGER PRIMARY KEY AUTOINCREMENT,
97
- from_version TEXT NOT NULL,
98
- to_version TEXT NOT NULL,
99
- direction TEXT NOT NULL CHECK (direction IN ('forward', 'rollback')),
100
- ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
101
- status TEXT NOT NULL CHECK (status IN ('success', 'failed', 'dry_run')),
102
- notes TEXT
103
- );
104
- CREATE TABLE baselines (
105
- id TEXT PRIMARY KEY,
106
- project_name TEXT NOT NULL,
107
- project_type TEXT,
108
- stack TEXT,
109
- mode TEXT NOT NULL CHECK (mode IN ('greenfield', 'brownfield', 'migrated')),
110
- status TEXT NOT NULL CHECK (status IN ('draft', 'adopting', 'blocked', 'ready', 'superseded')),
111
- repository_root TEXT NOT NULL DEFAULT '.',
112
- scope_json TEXT NOT NULL DEFAULT '["."]',
113
- repository_revision TEXT,
114
- spec_refs_json TEXT NOT NULL DEFAULT '[]',
115
- evidence_json TEXT NOT NULL DEFAULT '[]',
116
- verification_json TEXT NOT NULL DEFAULT '[]',
117
- unknowns_json TEXT NOT NULL DEFAULT '[]',
118
- provider_refs_json TEXT NOT NULL DEFAULT '{}',
119
- approved_by TEXT,
120
- approval_note TEXT,
121
- started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
122
- converged_at TEXT,
123
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
124
- );
125
- CREATE TABLE changes (
126
- id TEXT PRIMARY KEY,
127
- title TEXT NOT NULL,
128
- kind TEXT NOT NULL CHECK (kind IN ('quick', 'standard', 'major', 'incident')),
129
- status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'blocked', 'ready', 'archived', 'cancelled')),
130
- intent TEXT NOT NULL,
131
- docs_impact_json TEXT NOT NULL DEFAULT '{}',
132
- provider_refs_json TEXT NOT NULL DEFAULT '{}',
133
- base_commit TEXT,
134
- artifact_root TEXT,
135
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
136
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
137
- closed_at TEXT
138
- );
139
- INSERT INTO schema_version (version, description) VALUES ('11.0', 'baseline authority');
140
- INSERT INTO baselines
141
- (id, project_name, mode, status, approved_by, approval_note, converged_at)
142
- VALUES
143
- ('approved-baseline', 'legacy-project', 'brownfield', 'ready', 'owner', 'approved',
144
- '2026-07-01T00:00:00.000Z');
145
- `);
146
- closeStateDb(legacy);
147
-
148
- let upgraded;
149
- try {
150
- upgraded = initStateDb(file);
151
- } catch (error) {
152
- assert.fail(`schema 11 upgrade failed: ${error.message}\n${error.stack || ''}`);
153
- }
154
- try {
155
- assert.equal(upgraded.schema_version, '12.0');
156
- assert.ok(fs.existsSync(upgraded.backup_path));
157
- const baseline = upgraded.db.prepare(
158
- 'SELECT id, mode, status, worktree_state, gaps_json FROM baselines',
159
- ).get();
160
- assert.deepEqual(baseline, {
161
- id: 'approved-baseline', mode: 'brownfield', status: 'ready',
162
- worktree_state: 'unavailable', gaps_json: '[]',
163
- });
164
- assert.equal(
165
- upgraded.db.prepare("SELECT COUNT(*) AS count FROM baselines WHERE mode = 'migrated'").get().count,
166
- 0,
167
- );
168
- const changeColumns = upgraded.db.prepare('PRAGMA table_info(changes)').all().map((row) => row.name);
169
- assert.ok(changeColumns.includes('baseline_bypass_json'));
170
- } finally { closeStateDb(upgraded.db); }
171
- } finally {
172
- fs.rmSync(dir, { recursive: true, force: true });
173
- }
174
- });
175
-
176
- test('initStateDb exposes its pre-migration backup when an incompatible legacy schema fails', () => {
177
- const { dir, file } = tmpDbPath('ubp-schema-upgrade-failure');
178
- try {
179
- const legacy = openStateDb(file);
180
- legacy.exec(`
181
- CREATE TABLE schema_version (
182
- version TEXT PRIMARY KEY,
183
- applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
184
- description TEXT
185
- );
186
- CREATE TABLE migration_history (
187
- id INTEGER PRIMARY KEY AUTOINCREMENT,
188
- from_version TEXT NOT NULL,
189
- to_version TEXT NOT NULL,
190
- direction TEXT NOT NULL,
191
- applied_at TEXT NOT NULL,
192
- status TEXT NOT NULL,
193
- notes TEXT
194
- );
195
- INSERT INTO schema_version (version, description) VALUES ('11.0', 'incompatible fixture');
196
- `);
197
- closeStateDb(legacy);
198
-
199
- let failure;
200
- try {
201
- initStateDb(file);
202
- } catch (error) {
203
- failure = error;
204
- }
205
- assert.ok(failure, 'the incompatible migration_history table must fail schema application');
206
- assert.match(failure.message, /no such column: ts/);
207
- assert.ok(failure.migration_backup_path, 'failure must retain the backup location for recovery');
208
- assert.ok(fs.existsSync(failure.migration_backup_path));
209
- } finally {
210
- fs.rmSync(dir, { recursive: true, force: true });
211
- }
212
- });
213
-
214
- test('Phase 8A.1 schema: tasks.parent_id column + tasks_parent partial index + seed row', () => {
215
- const { dir, file } = tmpDbPath();
216
- try {
217
- const { db } = initStateDb(file);
218
-
219
- const cols = db.prepare("PRAGMA table_info(tasks)").all();
220
- const parentCol = cols.find((c) => c.name === 'parent_id');
221
- assert.ok(parentCol, 'tasks.parent_id column must exist');
222
- assert.equal(parentCol.type, 'TEXT');
223
- assert.equal(parentCol.notnull, 0, 'parent_id must be nullable (top-level tasks)');
224
-
225
- const indexRow = db
226
- .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'tasks_parent'")
227
- .get();
228
- assert.ok(indexRow, 'tasks_parent index must exist');
229
-
230
- const seedRow = db
231
- .prepare("SELECT version, description FROM schema_version WHERE version = '8A.1'")
232
- .get();
233
- assert.ok(seedRow, 'schema_version row for 8A.1 must be seeded');
234
- assert.match(seedRow.description, /parent_id/);
235
-
236
- const fkInfo = db.prepare("PRAGMA foreign_key_list(tasks)").all();
237
- const parentFk = fkInfo.find((fk) => fk.from === 'parent_id');
238
- assert.ok(parentFk, 'parent_id must declare a foreign key to tasks(id)');
239
- assert.equal(parentFk.table, 'tasks');
240
- assert.equal(parentFk.to, 'id');
241
- assert.equal(parentFk.on_delete, 'SET NULL');
242
-
243
- closeStateDb(db);
244
- } finally {
245
- fs.rmSync(dir, { recursive: true, force: true });
246
- }
247
- });
248
-
249
- test('initStateDb migrates existing runtime constraints to Kimi without losing references', () => {
250
- const { dir, file } = tmpDbPath('ubp-kimi-runtime-upgrade');
251
- try {
252
- const legacy = openStateDb(file);
253
- const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
254
- legacy.exec(legacySchema);
255
- legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0', '11.0', '12.0')").run();
256
- legacy.prepare(
257
- "INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
258
- ).run();
259
- legacy.prepare(
260
- `INSERT INTO sessions
261
- (sid, task_id, runtime, worktree_path, artifact_dir, lease_expires_at)
262
- VALUES ('session-old', 'task-old', 'codex', '/tmp/worktree', '/tmp/artifacts', '2099-01-01T00:00:00.000Z')`,
263
- ).run();
264
- legacy.prepare(
265
- "INSERT INTO telemetry (session_id, event_type, tool_name) VALUES ('session-old', 'tool_call', 'task.list')",
266
- ).run();
267
- legacy.prepare(
268
- `INSERT INTO incidents
269
- (id, code, severity, message, session_id)
270
- VALUES ('incident-old', 'OLD', 'warning', 'preserve me', 'session-old')`,
271
- ).run();
272
- legacy.prepare(
273
- "INSERT INTO events (type, session_id, runtime) VALUES ('session_spawned', 'session-old', 'codex')",
274
- ).run();
275
- closeStateDb(legacy);
276
-
277
- const upgraded = initStateDb(file);
278
- assert.equal(upgraded.schema_version, '12.0');
279
- assert.ok(upgraded.backup_path);
280
- assert.ok(fs.existsSync(upgraded.backup_path));
281
- assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
282
- assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
283
- assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
284
- assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
285
- const migrations = upgraded.db.prepare(
286
- "SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0', '11.0', '12.0') ORDER BY id",
287
- ).all();
288
- assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
289
- assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
290
- assert.ok(migrations.some((row) => row.to_version === '11.0' && /baseline adoption/i.test(row.notes)));
291
- assert.ok(migrations.some((row) => row.to_version === '12.0' && /gap ledger|re-adoption/i.test(row.notes)));
292
- assert.deepEqual(
293
- upgraded.db.prepare("SELECT mode, status FROM baselines WHERE id = 'migrated-baseline'").get(),
294
- { mode: 'migrated', status: 'adopting' },
295
- );
296
-
297
- upgraded.db.prepare(
298
- "INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
299
- ).run();
300
- const session = stateOps.createSession(upgraded.db, {
301
- sid: 'session-kimi',
302
- task_id: 'task-kimi',
303
- runtime: 'kimi',
304
- worktree_path: '/tmp/kimi-worktree',
305
- artifact_dir: '/tmp/kimi-artifacts',
306
- });
307
- assert.equal(session.runtime, 'kimi');
308
- const event = stateOps.appendEvent(upgraded.db, { type: 'kimi-ready', runtime: 'kimi' });
309
- assert.ok(event.event_id > 0);
310
- closeStateDb(upgraded.db);
311
- } finally {
312
- fs.rmSync(dir, { recursive: true, force: true });
313
- }
314
- });
@@ -1,169 +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
-
9
- const { initStateDb, closeStateDb } = require('./state-db.cjs');
10
- const ops = require('./state-ops.cjs');
11
- const changes = require('./change-workflow.cjs');
12
- const { expandTask, TaskExpandError } = require('./task-expander.cjs');
13
-
14
- function tmpDb() {
15
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-expand-'));
16
- const { db } = initStateDb(path.join(dir, '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', 'fixture', ?)`,
21
- ).run(new Date().toISOString());
22
- return { dir, db };
23
- }
24
-
25
- function cleanup({ dir, db }) {
26
- closeStateDb(db);
27
- fs.rmSync(dir, { recursive: true, force: true });
28
- }
29
-
30
- function seedParent(db, overrides = {}) {
31
- return ops.createTask(db, {
32
- id: 'parent-1',
33
- title: 'Build authentication module',
34
- type: 'feature',
35
- priority: 'P1',
36
- complexity: 9,
37
- files_modified: ['src/auth/index.ts'],
38
- ...overrides,
39
- });
40
- }
41
-
42
- const HAPPY_CHILDREN = [
43
- { id: 'child-1', title: 'Design auth schema', type: 'architecture', priority: 'P1', complexity: 4, deps: [], files_modified: ['src/auth/schema.ts'] },
44
- { id: 'child-2', title: 'Implement login endpoint', type: 'feature', priority: 'P1', complexity: 5, deps: ['child-1'], files_modified: ['src/auth/login.ts'] },
45
- { id: 'child-3', title: 'Add signup endpoint', type: 'feature', priority: 'P2', complexity: 3, deps: ['child-1'], files_modified: ['src/auth/signup.ts'] },
46
- ];
47
-
48
- test('expandTask atomically persists host-derived children without a provider client', () => {
49
- const ctx = tmpDb();
50
- try {
51
- seedParent(ctx.db);
52
- const result = expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN });
53
- assert.equal(result.parent_id, 'parent-1');
54
- assert.equal(result.children.length, 3);
55
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'expanded');
56
- for (const child of HAPPY_CHILDREN) {
57
- const row = ops.readTask(ctx.db, child.id);
58
- assert.equal(row.parent_id, 'parent-1');
59
- assert.equal(row.status, 'pending');
60
- }
61
- const events = ops.subscribeEventsSince(ctx.db, { since_id: 0, limit: 100 }).events;
62
- assert.equal(events.filter((event) => event.type === 'task_expanded').length, 1);
63
- } finally { cleanup(ctx); }
64
- });
65
-
66
- test('expandTask rejects missing and already expanded parents', () => {
67
- const ctx = tmpDb();
68
- try {
69
- assert.throws(
70
- () => expandTask(ctx.db, { id: 'missing', children: HAPPY_CHILDREN }),
71
- (err) => err instanceof TaskExpandError && err.code === 'TASK_NOT_FOUND',
72
- );
73
- seedParent(ctx.db);
74
- expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN });
75
- assert.throws(
76
- () => expandTask(ctx.db, { id: 'parent-1', children: [{ ...HAPPY_CHILDREN[0], id: 'other' }] }),
77
- (err) => err instanceof TaskExpandError && err.code === 'ALREADY_EXPANDED',
78
- );
79
- } finally { cleanup(ctx); }
80
- });
81
-
82
- test('expandTask requires valid host-derived children and leaves the parent untouched on failure', () => {
83
- const ctx = tmpDb();
84
- try {
85
- seedParent(ctx.db);
86
- for (const children of [undefined, [], [{
87
- id: 'child-1', title: 'x', type: 'feature', priority: 'P1',
88
- }]]) {
89
- assert.throws(
90
- () => expandTask(ctx.db, { id: 'parent-1', children }),
91
- (err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
92
- );
93
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
94
- }
95
- } finally { cleanup(ctx); }
96
- });
97
-
98
- test('expandTask rejects duplicate child ids', () => {
99
- const ctx = tmpDb();
100
- try {
101
- seedParent(ctx.db);
102
- assert.throws(
103
- () => expandTask(ctx.db, { id: 'parent-1', children: [
104
- { id: 'same', title: 'First child', type: 'feature', priority: 'P2' },
105
- { id: 'same', title: 'Second child', type: 'feature', priority: 'P2' },
106
- ] }),
107
- (err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
108
- );
109
- } finally { cleanup(ctx); }
110
- });
111
-
112
- test('expandTask rolls back every child when one id collides', () => {
113
- const ctx = tmpDb();
114
- try {
115
- seedParent(ctx.db);
116
- ops.createTask(ctx.db, {
117
- id: 'child-1', title: 'Pre-existing task', type: 'bugfix', priority: 'P3',
118
- });
119
- assert.throws(() => expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN }));
120
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
121
- assert.equal(ops.readTask(ctx.db, 'child-2'), null);
122
- assert.equal(ops.readTask(ctx.db, 'child-3'), null);
123
- } finally { cleanup(ctx); }
124
- });
125
-
126
- test('expandTask children inherit parent tag and change ownership', () => {
127
- const ctx = tmpDb();
128
- try {
129
- const { change } = changes.createChange(ctx.db, {
130
- id: 'expand-change', title: 'Expand one task', kind: 'quick',
131
- intent: 'Keep child ownership inside the active change.',
132
- docs_impact: { status: 'none', rationale: 'test fixture' },
133
- }, { rootDir: ctx.dir });
134
- seedParent(ctx.db, { tag: 'feat-auth', change_id: change.id });
135
- const result = expandTask(ctx.db, {
136
- id: 'parent-1', children: HAPPY_CHILDREN, rootDir: ctx.dir,
137
- });
138
- for (const child of result.children) {
139
- const row = ops.readTask(ctx.db, child.id);
140
- assert.equal(row.tag, 'feat-auth');
141
- assert.equal(row.change_id, change.id);
142
- }
143
- } finally { cleanup(ctx); }
144
- });
145
-
146
- test('expandTask enforces baseline authority before writing children', () => {
147
- for (const baseline of ['missing', 'draft', 'migrated']) {
148
- const ctx = tmpDb();
149
- try {
150
- seedParent(ctx.db);
151
- if (baseline === 'missing') ctx.db.prepare('DELETE FROM baselines').run();
152
- else if (baseline === 'draft') {
153
- ctx.db.prepare("UPDATE baselines SET status = 'draft' WHERE id = 'test-baseline'").run();
154
- } else {
155
- ctx.db.prepare(
156
- "UPDATE baselines SET mode = 'migrated', status = 'adopting' WHERE id = 'test-baseline'",
157
- ).run();
158
- }
159
- assert.throws(
160
- () => expandTask(ctx.db, {
161
- id: 'parent-1', children: HAPPY_CHILDREN, rootDir: ctx.dir,
162
- }),
163
- (error) => error.code === 'BASELINE_NOT_READY',
164
- );
165
- assert.equal(ops.readTask(ctx.db, 'child-1'), null);
166
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
167
- } finally { cleanup(ctx); }
168
- }
169
- });