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,171 +0,0 @@
1
- 'use strict';
2
-
3
- // Test Double rationale: external LLM API — deterministic fixtures prove
4
- // parsing logic (prompt assembly, schema validation, id normalization)
5
- // without egress cost or key dependence. Integration coverage against a
6
- // real LLM lives behind an env gate and is exercised separately.
7
-
8
- const test = require('node:test');
9
- const assert = require('node:assert/strict');
10
-
11
- const { parsePrd, PrdParseError, TASK_LIST_SCHEMA } = require('./prd-parser.cjs');
12
-
13
- function fakeClient(jsonFn, { usage = { input_tokens: 100, output_tokens: 50 }, model = 'claude-sonnet-4-6', provider = 'anthropic' } = {}) {
14
- return {
15
- provider,
16
- model,
17
- async completeJson({ system, user }) {
18
- const json = typeof jsonFn === 'function' ? await jsonFn({ system, user }) : jsonFn;
19
- return { json, raw: JSON.stringify(json), usage, model, provider };
20
- },
21
- };
22
- }
23
-
24
- const HAPPY_TASKS = {
25
- tasks: [
26
- {
27
- id: 'task-1',
28
- title: 'Design auth schema',
29
- type: 'architecture',
30
- priority: 'P1',
31
- complexity: 5,
32
- deps: [],
33
- files_modified: ['src/auth/schema.ts'],
34
- },
35
- {
36
- id: 'task-2',
37
- title: 'Implement login endpoint',
38
- type: 'feature',
39
- priority: 'P1',
40
- complexity: 6,
41
- deps: ['task-1'],
42
- files_modified: ['src/auth/login.ts'],
43
- },
44
- ],
45
- };
46
-
47
- test('parsePrd: happy path normalizes tasks and returns usage metadata', async () => {
48
- const client = fakeClient(HAPPY_TASKS);
49
- const result = await parsePrd('Build login', { llmClient: client, tag: 'feat-auth' });
50
- assert.equal(result.tasks.length, 2);
51
- assert.equal(result.tasks[0].id, 'task-1');
52
- assert.equal(result.tasks[0].tag, 'feat-auth');
53
- assert.deepEqual(result.tasks[1].deps, ['task-1']);
54
- assert.equal(result.model, 'claude-sonnet-4-6');
55
- assert.equal(result.provider, 'anthropic');
56
- assert.equal(result.usage.input_tokens, 100);
57
- });
58
-
59
- test('parsePrd: empty text → NO_INPUT', async () => {
60
- await assert.rejects(
61
- parsePrd('', { llmClient: fakeClient(HAPPY_TASKS) }),
62
- (err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
63
- );
64
- await assert.rejects(
65
- parsePrd(' ', { llmClient: fakeClient(HAPPY_TASKS) }),
66
- (err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
67
- );
68
- });
69
-
70
- test('parsePrd: missing llmClient → NO_LLM_CLIENT', async () => {
71
- await assert.rejects(
72
- parsePrd('Build something', {}),
73
- (err) => err instanceof PrdParseError && err.code === 'NO_LLM_CLIENT',
74
- );
75
- });
76
-
77
- test('parsePrd: llmClient error bubbles up as PARSE_FAILED', async () => {
78
- const brokenClient = {
79
- provider: 'anthropic',
80
- model: 'claude-sonnet-4-6',
81
- async completeJson() { throw new Error('upstream LLM 500'); },
82
- };
83
- await assert.rejects(
84
- parsePrd('Build something', { llmClient: brokenClient }),
85
- (err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /upstream LLM 500/.test(err.message),
86
- );
87
- });
88
-
89
- test('parsePrd: invalid JSON shape rejected — missing required "priority"', async () => {
90
- const bad = {
91
- tasks: [{
92
- id: 'task-1', title: 'Foo', type: 'feature', // priority missing
93
- complexity: 3, deps: [], files_modified: [],
94
- }],
95
- };
96
- await assert.rejects(
97
- parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
98
- (err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /priority/.test(err.message),
99
- );
100
- });
101
-
102
- test('parsePrd: invalid enum value rejected — bad type', async () => {
103
- const bad = {
104
- tasks: [{
105
- id: 'task-1', title: 'Foo', type: 'chore', priority: 'P1',
106
- }],
107
- };
108
- await assert.rejects(
109
- parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
110
- (err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
111
- );
112
- });
113
-
114
- test('parsePrd: duplicate task id rejected', async () => {
115
- const dup = {
116
- tasks: [
117
- { id: 'task-1', title: 'First copy', type: 'feature', priority: 'P2' },
118
- { id: 'task-1', title: 'Second copy', type: 'feature', priority: 'P2' },
119
- ],
120
- };
121
- await assert.rejects(
122
- parsePrd('Build dupes', { llmClient: fakeClient(dup) }),
123
- (err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /duplicate/i.test(err.message),
124
- );
125
- });
126
-
127
- test('parsePrd: missing optional fields are normalized to defaults', async () => {
128
- const minimal = {
129
- tasks: [{
130
- id: 'task-1', title: 'Minimal task', type: 'feature', priority: 'P3',
131
- }],
132
- };
133
- const result = await parsePrd('anything', { llmClient: fakeClient(minimal) });
134
- assert.equal(result.tasks[0].complexity, null);
135
- assert.deepEqual(result.tasks[0].deps, []);
136
- assert.deepEqual(result.tasks[0].files_modified, []);
137
- assert.equal(result.tasks[0].tag, null);
138
- });
139
-
140
- test('parsePrd: prompt contains PRD text passed to llmClient', async () => {
141
- let captured;
142
- const client = {
143
- provider: 'anthropic',
144
- model: 'claude-sonnet-4-6',
145
- async completeJson({ system, user }) {
146
- captured = { system, user };
147
- return {
148
- json: HAPPY_TASKS,
149
- raw: '',
150
- usage: {},
151
- model: 'claude-sonnet-4-6',
152
- provider: 'anthropic',
153
- };
154
- },
155
- };
156
- await parsePrd('Build the thing with acceptance criteria X', { llmClient: client });
157
- assert.match(captured.system, /engineering planner/i);
158
- assert.match(captured.user, /Build the thing with acceptance criteria X/);
159
- });
160
-
161
- test('parsePrd: empty tasks array rejected (schema requires minItems:1)', async () => {
162
- await assert.rejects(
163
- parsePrd('Build nothing', { llmClient: fakeClient({ tasks: [] }) }),
164
- (err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
165
- );
166
- });
167
-
168
- test('TASK_LIST_SCHEMA: exported for downstream consumers', () => {
169
- assert.equal(TASK_LIST_SCHEMA.required[0], 'tasks');
170
- assert.equal(TASK_LIST_SCHEMA.properties.tasks.minItems, 1);
171
- });
@@ -1,92 +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 runtime = require('./runtime-state.cjs');
11
-
12
- function fixture() {
13
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-runtime-state-'));
14
- const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
15
- return { rootDir, db };
16
- }
17
-
18
- function cleanup({ rootDir, db }) {
19
- closeStateDb(db);
20
- fs.rmSync(rootDir, { recursive: true, force: true });
21
- }
22
-
23
- test('projection jobs complete and retain their event cursor', () => {
24
- const fx = fixture();
25
- try {
26
- const job = runtime.enqueueProjection(fx.db, { tool_name: 'task.create', event_cursor: 7 });
27
- const result = runtime.processProjectionJobs(fx.db, {
28
- rootDir: fx.rootDir,
29
- project: () => ({ tasks_json: { count: 0 }, contexts: [] }),
30
- });
31
- assert.equal(result.processed, 1);
32
- assert.equal(result.jobs[0].status, 'completed');
33
- const stored = runtime.readProjectionJob(fx.db, job.id);
34
- assert.equal(stored.status, 'completed');
35
- assert.equal(stored.event_cursor, 7);
36
- } finally {
37
- cleanup(fx);
38
- }
39
- });
40
-
41
- test('projection failures are explicit, retryable incidents rather than swallowed warnings', () => {
42
- const fx = fixture();
43
- try {
44
- runtime.enqueueProjection(fx.db, { tool_name: 'task.update', event_cursor: 4, max_attempts: 1 });
45
- // Test Double rationale: deterministic projector failure is required; filesystem permission failures vary by host.
46
- const result = runtime.processProjectionJobs(fx.db, {
47
- rootDir: fx.rootDir,
48
- project: () => { throw new Error('projection disk unavailable'); },
49
- });
50
- assert.equal(result.jobs[0].status, 'failed');
51
- assert.match(result.jobs[0].error, /projection disk unavailable/);
52
- const incidents = runtime.listIncidents(fx.db, { status: 'open' });
53
- assert.equal(incidents.length, 1);
54
- assert.equal(incidents[0].code, 'PROJECTION_FAILED');
55
- assert.equal(incidents[0].retryable, true);
56
- } finally {
57
- cleanup(fx);
58
- }
59
- });
60
-
61
- test('event consumer cursors are durable and monotonic', () => {
62
- const fx = fixture();
63
- try {
64
- assert.equal(runtime.readConsumerCursor(fx.db, 'spec-staleness'), 0);
65
- runtime.writeConsumerCursor(fx.db, 'spec-staleness', 12);
66
- runtime.writeConsumerCursor(fx.db, 'spec-staleness', 9);
67
- assert.equal(runtime.readConsumerCursor(fx.db, 'spec-staleness'), 12);
68
- } finally {
69
- cleanup(fx);
70
- }
71
- });
72
-
73
- test('interrupted running projections are requeued only after the stale cutoff', () => {
74
- const fx = fixture();
75
- try {
76
- const stale = runtime.enqueueProjection(fx.db, { tool_name: 'task.update', event_cursor: 3 });
77
- const fresh = runtime.enqueueProjection(fx.db, { tool_name: 'task.create', event_cursor: 4 });
78
- fx.db.prepare("UPDATE projection_jobs SET status = 'running', updated_at = ? WHERE id = ?")
79
- .run('2000-01-01T00:00:00.000Z', stale.id);
80
- fx.db.prepare("UPDATE projection_jobs SET status = 'running', updated_at = ? WHERE id = ?")
81
- .run('2999-01-01T00:00:00.000Z', fresh.id);
82
-
83
- const result = runtime.requeueInterruptedProjections(fx.db, {
84
- staleBefore: '2026-01-01T00:00:00.000Z',
85
- });
86
- assert.equal(result.requeued, 1);
87
- assert.equal(runtime.readProjectionJob(fx.db, stale.id).status, 'pending');
88
- assert.equal(runtime.readProjectionJob(fx.db, fresh.id).status, 'running');
89
- } finally {
90
- cleanup(fx);
91
- }
92
- });
@@ -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
- });