ultra-builder-pro-cli 0.5.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 (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ // Phase 5.2 — Circuit breaker:
4
+ // per-task consecutive failures ≥3 → trip; tripped tasks refused spawn.
5
+
6
+ const test = require('node:test');
7
+ const assert = require('node:assert/strict');
8
+ const fs = require('node:fs');
9
+ const os = require('node:os');
10
+ const path = require('node:path');
11
+
12
+ const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
13
+ const ops = require('../lib/state-ops.cjs');
14
+
15
+ function freshDb() {
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-cb-'));
17
+ const file = path.join(dir, 'state.db');
18
+ const init = initStateDb(file);
19
+ return { dir, db: init.db };
20
+ }
21
+
22
+ function teardown(dir, db) {
23
+ try { closeStateDb(db); } catch (_) { /* ignore */ }
24
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
25
+ }
26
+
27
+ function seedTask(db, id) {
28
+ ops.createTask(db, { id, title: 'cb target', type: 'feature', priority: 'P1' });
29
+ return id;
30
+ }
31
+
32
+ test('isCircuitTripped: unrecorded task → false', () => {
33
+ const { dir, db } = freshDb();
34
+ try {
35
+ seedTask(db, 'cb-1');
36
+ assert.equal(ops.isCircuitTripped(db, 'cb-1'), false);
37
+ } finally { teardown(dir, db); }
38
+ });
39
+
40
+ test('recordTaskFailure: 3 consecutive → trips + task_circuit_broken event', () => {
41
+ const { dir, db } = freshDb();
42
+ try {
43
+ seedTask(db, 'cb-2');
44
+ ops.recordTaskFailure(db, 'cb-2', { reason: 'test1' });
45
+ assert.equal(ops.isCircuitTripped(db, 'cb-2'), false);
46
+ ops.recordTaskFailure(db, 'cb-2', { reason: 'test2' });
47
+ assert.equal(ops.isCircuitTripped(db, 'cb-2'), false);
48
+ ops.recordTaskFailure(db, 'cb-2', { reason: 'test3' });
49
+ assert.equal(ops.isCircuitTripped(db, 'cb-2'), true);
50
+
51
+ const { events } = ops.subscribeEventsSince(db, { since_id: 0, limit: 500 });
52
+ const brokenEvents = events.filter((e) => e.type === 'task_circuit_broken' && e.task_id === 'cb-2');
53
+ assert.equal(brokenEvents.length, 1, 'circuit broken event should fire exactly once');
54
+ const failures = events.filter((e) => e.type === 'task_failure' && e.task_id === 'cb-2');
55
+ assert.equal(failures.length, 3);
56
+ } finally { teardown(dir, db); }
57
+ });
58
+
59
+ test('recordTaskFailure: 4th + 5th failure does not re-emit task_circuit_broken', () => {
60
+ const { dir, db } = freshDb();
61
+ try {
62
+ seedTask(db, 'cb-3');
63
+ for (let i = 0; i < 5; i += 1) {
64
+ ops.recordTaskFailure(db, 'cb-3', { reason: `test${i}` });
65
+ }
66
+ assert.equal(ops.isCircuitTripped(db, 'cb-3'), true);
67
+ const { events } = ops.subscribeEventsSince(db, { since_id: 0, limit: 500 });
68
+ const brokenEvents = events.filter((e) => e.type === 'task_circuit_broken' && e.task_id === 'cb-3');
69
+ assert.equal(brokenEvents.length, 1, 'should only emit on the trip transition');
70
+ } finally { teardown(dir, db); }
71
+ });
72
+
73
+ test('resetCircuitBreaker: clears count and tripped_at', () => {
74
+ const { dir, db } = freshDb();
75
+ try {
76
+ seedTask(db, 'cb-4');
77
+ ops.recordTaskFailure(db, 'cb-4', { reason: 'x' });
78
+ ops.recordTaskFailure(db, 'cb-4', { reason: 'x' });
79
+ ops.recordTaskFailure(db, 'cb-4', { reason: 'x' });
80
+ assert.equal(ops.isCircuitTripped(db, 'cb-4'), true);
81
+
82
+ ops.resetCircuitBreaker(db, 'cb-4');
83
+ assert.equal(ops.isCircuitTripped(db, 'cb-4'), false);
84
+
85
+ // Next failure starts count at 1, not accumulating pre-reset.
86
+ ops.recordTaskFailure(db, 'cb-4', { reason: 'x' });
87
+ assert.equal(ops.isCircuitTripped(db, 'cb-4'), false);
88
+ ops.recordTaskFailure(db, 'cb-4', { reason: 'x' });
89
+ assert.equal(ops.isCircuitTripped(db, 'cb-4'), false);
90
+ } finally { teardown(dir, db); }
91
+ });
92
+
93
+ test('admissionCheck: tripped task → can_spawn=false, recommended=blocked_by_breaker', () => {
94
+ const { dir, db } = freshDb();
95
+ try {
96
+ seedTask(db, 'cb-5');
97
+ for (let i = 0; i < 3; i += 1) ops.recordTaskFailure(db, 'cb-5', { reason: 'x' });
98
+
99
+ const verdict = ops.admissionCheck(db, 'cb-5');
100
+ assert.equal(verdict.can_spawn, false);
101
+ assert.equal(verdict.recommended_action, 'blocked_by_breaker');
102
+ } finally { teardown(dir, db); }
103
+ });
104
+
105
+ test('admissionCheck: reset after trip → can_spawn=true', () => {
106
+ const { dir, db } = freshDb();
107
+ try {
108
+ seedTask(db, 'cb-6');
109
+ for (let i = 0; i < 3; i += 1) ops.recordTaskFailure(db, 'cb-6', { reason: 'x' });
110
+ ops.resetCircuitBreaker(db, 'cb-6');
111
+ const verdict = ops.admissionCheck(db, 'cb-6');
112
+ assert.equal(verdict.can_spawn, true);
113
+ } finally { teardown(dir, db); }
114
+ });
115
+
116
+ test('custom threshold: fail_threshold=2 trips on 2nd failure', () => {
117
+ const { dir, db } = freshDb();
118
+ try {
119
+ seedTask(db, 'cb-7');
120
+ ops.recordTaskFailure(db, 'cb-7', { reason: 'x', fail_threshold: 2 });
121
+ assert.equal(ops.isCircuitTripped(db, 'cb-7'), false);
122
+ ops.recordTaskFailure(db, 'cb-7', { reason: 'x', fail_threshold: 2 });
123
+ assert.equal(ops.isCircuitTripped(db, 'cb-7'), true);
124
+ } finally { teardown(dir, db); }
125
+ });
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
+ const ops = require('../lib/state-ops.cjs');
12
+ const projector = require('../lib/projector.cjs');
13
+
14
+ function gitRun(cwd, args, opts = {}) {
15
+ const env = {
16
+ ...process.env,
17
+ GIT_AUTHOR_NAME: 'ubp-test',
18
+ GIT_AUTHOR_EMAIL: 'ubp@example.com',
19
+ GIT_COMMITTER_NAME: 'ubp-test',
20
+ GIT_COMMITTER_EMAIL: 'ubp@example.com',
21
+ };
22
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8', env, ...opts });
23
+ if (r.status !== 0 && !opts.allowFail) {
24
+ throw new Error(`git ${args.join(' ')} failed: ${r.stderr || r.stdout}`);
25
+ }
26
+ return { code: r.status, stdout: r.stdout.trim(), stderr: r.stderr };
27
+ }
28
+
29
+ function tmpRepo() {
30
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-hash-flow-'));
31
+ gitRun(dir, ['init', '-q', '-b', 'main']);
32
+ gitRun(dir, ['config', 'commit.gpgsign', 'false']);
33
+ // Ignore the SQLite WAL/SHM sidecars so they don't appear as modifications.
34
+ fs.writeFileSync(path.join(dir, '.gitignore'), '.ultra/state.db-shm\n.ultra/state.db-wal\n');
35
+ return dir;
36
+ }
37
+
38
+ test('two-commit flow lands feat then chore with the right SHA in context md', () => {
39
+ const dir = tmpRepo();
40
+ try {
41
+ const dbPath = path.join(dir, '.ultra', 'state.db');
42
+ const init = initStateDb(dbPath);
43
+ const db = init.db;
44
+
45
+ // Task starts pending, no completion_commit yet.
46
+ ops.createTask(db, { id: 'task-h1', title: 'commit hash flow', type: 'feature', priority: 'P1' });
47
+ projector.projectAll(db, { rootDir: dir });
48
+
49
+ // Stage initial state and land the feat commit.
50
+ fs.writeFileSync(path.join(dir, 'src.js'), '// implementation\n');
51
+ gitRun(dir, ['add', '-A']);
52
+ gitRun(dir, ['commit', '-q', '-m', 'feat: task-h1 — commit hash flow']);
53
+
54
+ const featSha = gitRun(dir, ['rev-parse', 'HEAD']).stdout;
55
+
56
+ // Step 3 — update state.db with the new SHA.
57
+ ops.patchTask(db, 'task-h1', { completion_commit: featSha });
58
+
59
+ // Step 4 — re-project the context so its YAML carries the hash.
60
+ projector.projectContext(db, 'task-h1', {}, { rootDir: dir });
61
+
62
+ // Step 5 — chore commit captures the projection update.
63
+ gitRun(dir, ['add', '-A']);
64
+ gitRun(dir, ['commit', '-q', '-m', 'chore: record task-h1 completion hash']);
65
+
66
+ const log = gitRun(dir, ['log', '--oneline', '-2']).stdout.split('\n');
67
+ assert.match(log[0], /chore: record task-h1 completion hash/);
68
+ assert.match(log[1], /feat: task-h1 — commit hash flow/);
69
+
70
+ const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-task-h1.md');
71
+ const ctxText = fs.readFileSync(ctxFile, 'utf8');
72
+ // The chore commit's working tree must reflect the recorded SHA.
73
+ const dbRow = db.prepare("SELECT completion_commit FROM tasks WHERE id = 'task-h1'").get();
74
+ assert.equal(dbRow.completion_commit, featSha);
75
+ // Header is regenerated; ensure projector wrote v4.5 + the right task_id.
76
+ assert.match(ctxText, /^---/);
77
+ assert.match(ctxText, /schema_version: 4\.5/);
78
+ assert.match(ctxText, /task_id: task-h1/);
79
+ closeStateDb(db);
80
+ } finally {
81
+ fs.rmSync(dir, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test('rerunning steps 4–5 against the same SHA produces an empty chore commit attempt', () => {
86
+ const dir = tmpRepo();
87
+ try {
88
+ const dbPath = path.join(dir, '.ultra', 'state.db');
89
+ const init = initStateDb(dbPath);
90
+ const db = init.db;
91
+ ops.createTask(db, { id: 'task-h2', title: 'idempotent', type: 'feature', priority: 'P1' });
92
+ projector.projectAll(db, { rootDir: dir });
93
+
94
+ fs.writeFileSync(path.join(dir, 'a.js'), '// a\n');
95
+ gitRun(dir, ['add', '-A']);
96
+ gitRun(dir, ['commit', '-q', '-m', 'feat: task-h2 — idempotent']);
97
+ const sha = gitRun(dir, ['rev-parse', 'HEAD']).stdout;
98
+ ops.patchTask(db, 'task-h2', { completion_commit: sha });
99
+ projector.projectContext(db, 'task-h2', {}, { rootDir: dir });
100
+ gitRun(dir, ['add', '-A']);
101
+ gitRun(dir, ['commit', '-q', '-m', 'chore: record task-h2 completion hash']);
102
+
103
+ // Idempotency: re-projecting the same db state must produce byte-identical
104
+ // context md (generated_at is derived from tasks.updated_at, not now()).
105
+ projector.projectContext(db, 'task-h2', {}, { rootDir: dir });
106
+ gitRun(dir, ['add', '-A']);
107
+ const status = gitRun(dir, ['status', '--porcelain']).stdout;
108
+ assert.equal(status, '', 're-projection of unchanged state must leave nothing to commit');
109
+
110
+ // A retry chore commit on an empty diff must be refused by git.
111
+ const r = gitRun(dir, ['commit', '-q', '-m', 'chore: idempotent retry'], { allowFail: true });
112
+ assert.notEqual(r.code, 0, 'a follow-up empty chore commit must fail');
113
+ closeStateDb(db);
114
+ } finally {
115
+ fs.rmSync(dir, { recursive: true, force: true });
116
+ }
117
+ });
@@ -0,0 +1,96 @@
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 { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
9
+
10
+ const { initStateDb, openStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
+ const ops = require('../lib/state-ops.cjs');
12
+
13
+ const WORKER_COUNT = 20;
14
+ const TASKS_PER_WORKER = 50;
15
+
16
+ function tmpDb() {
17
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-conc-up-'));
18
+ return { dir, file: path.join(dir, 'state.db') };
19
+ }
20
+
21
+ if (!isMainThread) {
22
+ const { dbPath, workerIdx, count } = workerData;
23
+ const db = openStateDb(dbPath);
24
+ const completed = [];
25
+ for (let i = 0; i < count; i++) {
26
+ const id = `w${workerIdx}-t${i}`;
27
+ ops.updateTaskStatus(db, id, 'in_progress');
28
+ ops.updateTaskStatus(db, id, 'completed');
29
+ completed.push(id);
30
+ }
31
+ closeStateDb(db);
32
+ parentPort.postMessage({ workerIdx, completed });
33
+ } else {
34
+ test(`${WORKER_COUNT} workers × ${TASKS_PER_WORKER} updateTaskStatus — no loss, monotonic events`, async () => {
35
+ const { dir, file } = tmpDb();
36
+ try {
37
+ const init = initStateDb(file);
38
+ const db = init.db;
39
+
40
+ // Pre-seed disjoint tasks so workers never contend on the same row.
41
+ for (let w = 0; w < WORKER_COUNT; w++) {
42
+ for (let t = 0; t < TASKS_PER_WORKER; t++) {
43
+ ops.createTask(db, {
44
+ id: `w${w}-t${t}`,
45
+ title: `seed w${w}t${t}`,
46
+ type: 'feature',
47
+ priority: 'P3',
48
+ });
49
+ }
50
+ }
51
+ closeStateDb(db);
52
+
53
+ const expectedTasks = WORKER_COUNT * TASKS_PER_WORKER;
54
+ const results = await Promise.all(
55
+ Array.from({ length: WORKER_COUNT }, (_, idx) =>
56
+ new Promise((resolve, reject) => {
57
+ const w = new Worker(__filename, {
58
+ workerData: { dbPath: file, workerIdx: idx, count: TASKS_PER_WORKER },
59
+ });
60
+ w.on('message', resolve);
61
+ w.on('error', reject);
62
+ w.on('exit', (code) => {
63
+ if (code !== 0) reject(new Error(`worker ${idx} exit ${code}`));
64
+ });
65
+ }),
66
+ ),
67
+ );
68
+
69
+ const completedClaims = results.flatMap((r) => r.completed);
70
+ assert.equal(completedClaims.length, expectedTasks);
71
+
72
+ const verify = openStateDb(file);
73
+ const completedRows = verify.prepare("SELECT id, status FROM tasks WHERE status = @status").all({ status: 'completed' });
74
+ assert.equal(completedRows.length, expectedTasks, 'every task must end in completed');
75
+
76
+ // Each task contributes 1 task_created + 1 task_started + 1 task_completed = 3 events.
77
+ const totalEvents = verify.prepare('SELECT COUNT(*) AS n FROM events').get().n;
78
+ assert.equal(totalEvents, expectedTasks * 3, 'every transition must record an event');
79
+
80
+ const ids = verify.prepare('SELECT id FROM events ORDER BY id ASC').all().map((r) => r.id);
81
+ assert.equal(new Set(ids).size, ids.length, 'event ids must be unique');
82
+ for (let i = 1; i < ids.length; i++) {
83
+ assert.ok(ids[i] > ids[i - 1], 'event ids must be strictly monotonic');
84
+ }
85
+
86
+ const startedCount = verify.prepare("SELECT COUNT(*) AS n FROM events WHERE type = @type").get({ type: 'task_started' }).n;
87
+ const completedCount = verify.prepare("SELECT COUNT(*) AS n FROM events WHERE type = @type").get({ type: 'task_completed' }).n;
88
+ assert.equal(startedCount, expectedTasks, 'every task got a task_started event');
89
+ assert.equal(completedCount, expectedTasks, 'every task got a task_completed event');
90
+
91
+ closeStateDb(verify);
92
+ } finally {
93
+ fs.rmSync(dir, { recursive: true, force: true });
94
+ }
95
+ });
96
+ }
@@ -0,0 +1,112 @@
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 { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
9
+
10
+ const { initStateDb, openStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
+
12
+ function tmpDb() {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-conc-'));
14
+ return { dir, file: path.join(dir, 'state.db') };
15
+ }
16
+
17
+ function appendEventRaw(db, type, payload) {
18
+ const stmt = db.prepare(
19
+ `INSERT INTO events (type, runtime, payload_json) VALUES (?, ?, ?)`,
20
+ );
21
+ return stmt.run(type, 'claude', JSON.stringify(payload)).lastInsertRowid;
22
+ }
23
+
24
+ function withRetry(fn, attempts = 3, baseMs = 25) {
25
+ let lastErr;
26
+ for (let i = 0; i < attempts; i++) {
27
+ try {
28
+ return fn();
29
+ } catch (err) {
30
+ if (!String(err.code || err.message).includes('SQLITE_BUSY')) throw err;
31
+ lastErr = err;
32
+ const wait = baseMs * Math.pow(2, i);
33
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, wait);
34
+ }
35
+ }
36
+ throw lastErr;
37
+ }
38
+
39
+ if (!isMainThread) {
40
+ const { dbPath, role, count, baseId } = workerData;
41
+ const db = openStateDb(dbPath);
42
+ const written = [];
43
+ for (let i = 0; i < count; i++) {
44
+ const id = withRetry(() =>
45
+ appendEventRaw(db, 'task_created', { role, baseId, i }),
46
+ );
47
+ written.push(Number(id));
48
+ }
49
+ closeStateDb(db);
50
+ parentPort.postMessage({ role, written });
51
+ } else {
52
+ test('three concurrent writers append to events with no loss and monotonic id', async () => {
53
+ const { dir, file } = tmpDb();
54
+ try {
55
+ const init = initStateDb(file);
56
+ closeStateDb(init.db);
57
+
58
+ const roles = ['mcp', 'cli', 'daemon'];
59
+ const COUNT_PER_WORKER = 100;
60
+ const expected = roles.length * COUNT_PER_WORKER;
61
+
62
+ const results = await Promise.all(
63
+ roles.map(
64
+ (role, idx) =>
65
+ new Promise((resolve, reject) => {
66
+ const w = new Worker(__filename, {
67
+ workerData: {
68
+ dbPath: file,
69
+ role,
70
+ count: COUNT_PER_WORKER,
71
+ baseId: idx * COUNT_PER_WORKER,
72
+ },
73
+ });
74
+ w.on('message', (m) => resolve(m));
75
+ w.on('error', reject);
76
+ w.on('exit', (code) => {
77
+ if (code !== 0) reject(new Error(`worker ${role} exit ${code}`));
78
+ });
79
+ }),
80
+ ),
81
+ );
82
+
83
+ const verify = openStateDb(file);
84
+ const total = verify.prepare('SELECT COUNT(*) AS n FROM events').get().n;
85
+ assert.equal(total, expected, 'total event count must match');
86
+
87
+ const ids = verify.prepare('SELECT id FROM events ORDER BY id ASC').all().map((r) => r.id);
88
+ assert.equal(new Set(ids).size, ids.length, 'event ids must be unique');
89
+ for (let i = 1; i < ids.length; i++) {
90
+ assert.ok(ids[i] > ids[i - 1], 'event ids must be strictly monotonic');
91
+ }
92
+ const minId = ids[0];
93
+ const maxId = ids[ids.length - 1];
94
+ assert.ok(maxId - minId + 1 >= ids.length, 'sequence must not have collisions');
95
+
96
+ const perRole = verify
97
+ .prepare("SELECT json_extract(payload_json, '$.role') AS role, COUNT(*) AS n FROM events GROUP BY role")
98
+ .all();
99
+ for (const row of perRole) {
100
+ assert.equal(row.n, COUNT_PER_WORKER, `role ${row.role} count`);
101
+ }
102
+ closeStateDb(verify);
103
+
104
+ // Workers should each have observed their own row ids returned cleanly.
105
+ const collected = results.flatMap((r) => r.written);
106
+ assert.equal(collected.length, expected);
107
+ assert.equal(new Set(collected).size, expected, 'no duplicate ids reported by workers');
108
+ } finally {
109
+ fs.rmSync(dir, { recursive: true, force: true });
110
+ }
111
+ });
112
+ }
@@ -0,0 +1,117 @@
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 Database = require('better-sqlite3');
11
+
12
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
13
+ const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
14
+ const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
15
+ const ops = require('../lib/state-ops.cjs');
16
+
17
+ function tmpDir() {
18
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-ops-'));
19
+ }
20
+
21
+ function runCli(args) {
22
+ const r = spawnSync(process.execPath, [CLI, 'db', ...args], { encoding: 'utf8' });
23
+ const lines = r.stdout.trim().split('\n').filter(Boolean);
24
+ const last = lines[lines.length - 1] || '{}';
25
+ return { code: r.status, envelope: JSON.parse(last), stderr: r.stderr };
26
+ }
27
+
28
+ test('db checkpoint runs PRAGMA wal_checkpoint(TRUNCATE) and reports counters', () => {
29
+ const dir = tmpDir();
30
+ const dbPath = path.join(dir, 'state.db');
31
+ try {
32
+ closeStateDb(initStateDb(dbPath).db);
33
+ const { code, envelope } = runCli(['checkpoint', '--path', dbPath]);
34
+ assert.equal(code, 0);
35
+ assert.equal(envelope.ok, true);
36
+ assert.equal(typeof envelope.data.busy, 'number');
37
+ assert.equal(typeof envelope.data.checkpointed, 'number');
38
+ } finally {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ test('db vacuum reports reclaimed bytes after deleting rows', () => {
44
+ const dir = tmpDir();
45
+ const dbPath = path.join(dir, 'state.db');
46
+ try {
47
+ const init = initStateDb(dbPath);
48
+ for (let i = 0; i < 50; i++) {
49
+ ops.appendEvent(init.db, { type: 'task_created', task_id: `t-${i}` });
50
+ }
51
+ init.db.prepare('DELETE FROM events').run();
52
+ closeStateDb(init.db);
53
+
54
+ const { code, envelope } = runCli(['vacuum', '--path', dbPath]);
55
+ assert.equal(code, 0);
56
+ assert.equal(envelope.ok, true);
57
+ assert.ok(envelope.data.bytes_after <= envelope.data.bytes_before);
58
+ assert.equal(typeof envelope.data.reclaimed_bytes, 'number');
59
+ } finally {
60
+ fs.rmSync(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ test('db integrity returns ok on a healthy database', () => {
65
+ const dir = tmpDir();
66
+ const dbPath = path.join(dir, 'state.db');
67
+ try {
68
+ closeStateDb(initStateDb(dbPath).db);
69
+ const { code, envelope } = runCli(['integrity', '--path', dbPath]);
70
+ assert.equal(code, 0);
71
+ assert.equal(envelope.ok, true);
72
+ assert.equal(envelope.data.integrity_ok, true);
73
+ assert.deepEqual(envelope.data.messages, ['ok']);
74
+ } finally {
75
+ fs.rmSync(dir, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test('db backup writes a file that opens independently with the same schema', () => {
80
+ const dir = tmpDir();
81
+ const dbPath = path.join(dir, 'state.db');
82
+ const backupPath = path.join(dir, 'snap.db');
83
+ try {
84
+ const init = initStateDb(dbPath);
85
+ ops.createTask(init.db, { id: 'b-1', title: 'backup', type: 'feature', priority: 'P1' });
86
+ closeStateDb(init.db);
87
+
88
+ const { code, envelope } = runCli(['backup', '--path', dbPath, '--to', backupPath]);
89
+ assert.equal(code, 0);
90
+ assert.equal(envelope.ok, true);
91
+ assert.equal(envelope.data.target, path.resolve(backupPath));
92
+ assert.ok(envelope.data.size_bytes > 0);
93
+
94
+ // Independently open the backup with a fresh connection
95
+ const snap = new Database(backupPath);
96
+ const v = snap.prepare("SELECT version FROM schema_version").get();
97
+ assert.equal(v.version, '4.5');
98
+ const t = snap.prepare("SELECT id FROM tasks WHERE id = 'b-1'").get();
99
+ assert.equal(t.id, 'b-1');
100
+ snap.close();
101
+ } finally {
102
+ fs.rmSync(dir, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test('db <verb> on a missing database returns DB_NOT_FOUND with exit 2', () => {
107
+ const dir = tmpDir();
108
+ try {
109
+ const missing = path.join(dir, 'missing.db');
110
+ const { code, envelope } = runCli(['integrity', '--path', missing]);
111
+ assert.equal(code, 2);
112
+ assert.equal(envelope.ok, false);
113
+ assert.equal(envelope.error.code, 'DB_NOT_FOUND');
114
+ } finally {
115
+ fs.rmSync(dir, { recursive: true, force: true });
116
+ }
117
+ });