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,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
- const { execFileSync } = require('node:child_process');
9
-
10
- const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
11
- const ops = require('../../mcp-server/lib/state-ops.cjs');
12
- const runner = require('../session-runner.cjs');
13
-
14
- // Short-lived child: keep the process alive until killed.
15
- const LONG_SLEEP_CMD = process.execPath;
16
- const LONG_SLEEP_ARGS = ['-e', 'setInterval(() => {}, 60000);'];
17
-
18
- function mkRepo() {
19
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-runner-repo-'));
20
- execFileSync('git', ['init', '-q'], { cwd: dir });
21
- execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: dir });
22
- execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: dir });
23
- fs.writeFileSync(path.join(dir, 'README.md'), '# test\n');
24
- execFileSync('git', ['add', '-A'], { cwd: dir });
25
- execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: dir });
26
- return dir;
27
- }
28
-
29
- function mkDb(repoRoot) {
30
- const dbPath = path.join(repoRoot, '.ultra', 'state.db');
31
- const { db } = initStateDb(dbPath);
32
- return { db, dbPath };
33
- }
34
-
35
- function seedTask(db, id = 't-1') {
36
- ops.createTask(db, { id, title: 'runner target', type: 'feature', priority: 'P1' });
37
- return id;
38
- }
39
-
40
- function cleanup(repoRoot, db) {
41
- try { closeStateDb(db); } catch (_) { /* ignore */ }
42
- try { fs.rmSync(repoRoot, { recursive: true, force: true }); } catch (_) { /* ignore */ }
43
- }
44
-
45
- function isProcessAlive(pid) {
46
- try { process.kill(pid, 0); return true; }
47
- catch (_) { return false; }
48
- }
49
-
50
- test('spawnSession creates worktree, child process, and sessions row', () => {
51
- const repoRoot = mkRepo();
52
- const { db } = mkDb(repoRoot);
53
- let handle;
54
- try {
55
- seedTask(db, 'r-1');
56
- handle = runner.spawnSession({
57
- db, repoRoot,
58
- task_id: 'r-1', runtime: 'claude',
59
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
60
- });
61
- assert.match(handle.sid, /^sess-/);
62
- assert.ok(fs.existsSync(handle.worktree_path));
63
- assert.ok(fs.existsSync(handle.artifact_dir));
64
- assert.ok(handle.pid);
65
- assert.ok(isProcessAlive(handle.pid));
66
-
67
- const row = ops.readSession(db, handle.sid);
68
- assert.equal(row.task_id, 'r-1');
69
- assert.equal(row.status, 'running');
70
- assert.equal(row.pid, handle.pid);
71
- } finally {
72
- if (handle && handle.pid) { try { process.kill(handle.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
73
- cleanup(repoRoot, db);
74
- }
75
- });
76
-
77
- test('closeSession kills child, marks completed, removes worktree', async () => {
78
- const repoRoot = mkRepo();
79
- const { db } = mkDb(repoRoot);
80
- let handle;
81
- try {
82
- seedTask(db, 'r-close');
83
- handle = runner.spawnSession({
84
- db, repoRoot,
85
- task_id: 'r-close', runtime: 'claude',
86
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
87
- });
88
- const wt = handle.worktree_path;
89
- runner.closeSession(
90
- { db, repoRoot, sid: handle.sid },
91
- { status: 'completed', kill_signal: 'SIGKILL' },
92
- );
93
-
94
- const row = ops.readSession(db, handle.sid);
95
- assert.equal(row.status, 'completed');
96
- // give the kernel a tick to reap the SIGKILL
97
- await new Promise((r) => setTimeout(r, 50));
98
- assert.ok(!isProcessAlive(handle.pid), `pid ${handle.pid} should be dead`);
99
- assert.ok(!fs.existsSync(wt));
100
- } finally {
101
- cleanup(repoRoot, db);
102
- }
103
- });
104
-
105
- test('spawnSession refuses second session without takeover (ADMISSION_DENIED)', () => {
106
- const repoRoot = mkRepo();
107
- const { db } = mkDb(repoRoot);
108
- let first;
109
- try {
110
- seedTask(db, 'r-adm');
111
- first = runner.spawnSession({
112
- db, repoRoot,
113
- task_id: 'r-adm', runtime: 'claude',
114
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
115
- });
116
- assert.throws(
117
- () => runner.spawnSession({
118
- db, repoRoot,
119
- task_id: 'r-adm', runtime: 'codex',
120
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
121
- }),
122
- (err) => err instanceof runner.SessionRunnerError && err.code === 'ADMISSION_DENIED',
123
- );
124
- } finally {
125
- if (first && first.pid) { try { process.kill(first.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
126
- cleanup(repoRoot, db);
127
- }
128
- });
129
-
130
- test('takeover=true crashes old session and spawns new one', () => {
131
- const repoRoot = mkRepo();
132
- const { db } = mkDb(repoRoot);
133
- let first, second;
134
- try {
135
- seedTask(db, 'r-take');
136
- first = runner.spawnSession({
137
- db, repoRoot,
138
- task_id: 'r-take', runtime: 'claude',
139
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
140
- });
141
- second = runner.spawnSession({
142
- db, repoRoot,
143
- task_id: 'r-take', runtime: 'codex',
144
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
145
- takeover: true,
146
- });
147
- assert.notEqual(second.sid, first.sid);
148
-
149
- const firstRow = ops.readSession(db, first.sid);
150
- assert.equal(firstRow.status, 'crashed');
151
- const secondRow = ops.readSession(db, second.sid);
152
- assert.equal(secondRow.status, 'running');
153
- assert.equal(secondRow.runtime, 'codex');
154
- } finally {
155
- for (const h of [first, second]) {
156
- if (h && h.pid) { try { process.kill(h.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
157
- }
158
- cleanup(repoRoot, db);
159
- }
160
- });
161
-
162
- test('attachHeartbeat updates heartbeat_at while running', async () => {
163
- const repoRoot = mkRepo();
164
- const { db } = mkDb(repoRoot);
165
- let handle;
166
- try {
167
- seedTask(db, 'r-hb');
168
- handle = runner.spawnSession({
169
- db, repoRoot,
170
- task_id: 'r-hb', runtime: 'claude',
171
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
172
- });
173
- const stop = runner.attachHeartbeat(db, handle.sid, { intervalMs: 20 });
174
- await new Promise((r) => setTimeout(r, 80));
175
- stop();
176
-
177
- const row = ops.readSession(db, handle.sid);
178
- assert.ok(row.heartbeat_at, 'heartbeat_at should be populated');
179
- // lease extended beyond initial default of started_at + 1800s? compare against start
180
- assert.ok(Date.parse(row.heartbeat_at) > 0);
181
- } finally {
182
- if (handle && handle.pid) { try { process.kill(handle.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
183
- cleanup(repoRoot, db);
184
- }
185
- });
186
-
187
- test('kill -9 child: reapOrphanSessions marks orphan after heartbeat grace expires', async () => {
188
- const repoRoot = mkRepo();
189
- const { db } = mkDb(repoRoot);
190
- let handle;
191
- try {
192
- seedTask(db, 'r-orphan');
193
- handle = runner.spawnSession({
194
- db, repoRoot,
195
- task_id: 'r-orphan', runtime: 'claude',
196
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
197
- });
198
- // force lease + heartbeat into the past so reaper has something to grab
199
- ops.updateSession(db, handle.sid, {
200
- lease_expires_at: new Date(Date.now() - 60 * 1000).toISOString(),
201
- heartbeat_at: new Date(Date.now() - 60 * 1000).toISOString(),
202
- });
203
- process.kill(handle.pid, 'SIGKILL');
204
-
205
- const reaped = ops.reapOrphanSessions(db, { graceSeconds: 30 });
206
- assert.ok(reaped.reaped.includes(handle.sid));
207
- const row = ops.readSession(db, handle.sid);
208
- assert.equal(row.status, 'orphan');
209
- } finally {
210
- cleanup(repoRoot, db);
211
- }
212
- });
213
-
214
- test('concurrent spawns on same task: only one succeeds (admission serialization)', async () => {
215
- const repoRoot = mkRepo();
216
- const { db } = mkDb(repoRoot);
217
- let winner;
218
- try {
219
- seedTask(db, 'r-race');
220
-
221
- const attempts = [0, 1, 2].map((i) =>
222
- Promise.resolve().then(() => {
223
- try {
224
- return runner.spawnSession({
225
- db, repoRoot,
226
- task_id: 'r-race', runtime: ['claude', 'opencode', 'codex'][i],
227
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
228
- });
229
- } catch (err) {
230
- return { error: err.code };
231
- }
232
- }),
233
- );
234
- const results = await Promise.all(attempts);
235
- const winners = results.filter((r) => r && r.sid);
236
- const denied = results.filter((r) => r && r.error === 'ADMISSION_DENIED');
237
- assert.equal(winners.length, 1, `expected 1 winner, got ${winners.length}`);
238
- assert.equal(denied.length, 2, `expected 2 denied, got ${denied.length}`);
239
- winner = winners[0];
240
- } finally {
241
- if (winner && winner.pid) { try { process.kill(winner.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
242
- cleanup(repoRoot, db);
243
- }
244
- });
245
-
246
- test('spawnSession refuses tripped task (CIRCUIT_TRIPPED, takeover cannot override)', () => {
247
- const repoRoot = mkRepo();
248
- const { db } = mkDb(repoRoot);
249
- try {
250
- seedTask(db, 'r-cb');
251
- for (let i = 0; i < 3; i += 1) ops.recordTaskFailure(db, 'r-cb', { reason: 'test' });
252
- assert.equal(ops.isCircuitTripped(db, 'r-cb'), true);
253
-
254
- const attempt = (takeover) => runner.spawnSession({
255
- db, repoRoot,
256
- task_id: 'r-cb', runtime: 'claude',
257
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
258
- takeover,
259
- });
260
- assert.throws(() => attempt(false), (err) => err.code === 'CIRCUIT_TRIPPED');
261
- assert.throws(() => attempt(true), (err) => err.code === 'CIRCUIT_TRIPPED');
262
- } finally {
263
- cleanup(repoRoot, db);
264
- }
265
- });
266
-
267
- test('spawnSession works again after resetCircuitBreaker', () => {
268
- const repoRoot = mkRepo();
269
- const { db } = mkDb(repoRoot);
270
- let handle;
271
- try {
272
- seedTask(db, 'r-cb-reset');
273
- for (let i = 0; i < 3; i += 1) ops.recordTaskFailure(db, 'r-cb-reset', { reason: 'test' });
274
- ops.resetCircuitBreaker(db, 'r-cb-reset');
275
- handle = runner.spawnSession({
276
- db, repoRoot,
277
- task_id: 'r-cb-reset', runtime: 'claude',
278
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
279
- });
280
- assert.ok(handle.sid);
281
- } finally {
282
- if (handle && handle.pid) { try { process.kill(handle.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
283
- cleanup(repoRoot, db);
284
- }
285
- });
286
-
287
- test('two sessions on different tasks get independent worktrees', () => {
288
- const repoRoot = mkRepo();
289
- const { db } = mkDb(repoRoot);
290
- let a, b;
291
- try {
292
- seedTask(db, 'r-multi-a');
293
- seedTask(db, 'r-multi-b');
294
- a = runner.spawnSession({
295
- db, repoRoot,
296
- task_id: 'r-multi-a', runtime: 'claude',
297
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
298
- });
299
- b = runner.spawnSession({
300
- db, repoRoot,
301
- task_id: 'r-multi-b', runtime: 'codex',
302
- command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
303
- });
304
- assert.notEqual(a.sid, b.sid);
305
- assert.notEqual(a.worktree_path, b.worktree_path);
306
- assert.notEqual(a.pid, b.pid);
307
- assert.ok(isProcessAlive(a.pid) && isProcessAlive(b.pid));
308
- } finally {
309
- for (const h of [a, b]) {
310
- if (h && h.pid) { try { process.kill(h.pid, 'SIGKILL'); } catch (_) { /* ignore */ } }
311
- }
312
- cleanup(repoRoot, db);
313
- }
314
- });
@@ -1,147 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 7.3 — Skill miner (heuristic, no LLM):
4
- // • mineSession scans a session's events + task state for "solved
5
- // something non-trivial" signals and writes skills/learned/
6
- // <ts>_<sid>_unverified.md drafts.
7
- // • Idempotent: same sid twice → only one draft per signal.
8
-
9
- const { test } = require('node:test');
10
- const assert = require('node:assert/strict');
11
- const fs = require('node:fs');
12
- const os = require('node:os');
13
- const path = require('node:path');
14
-
15
- const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
16
- const ops = require('../../mcp-server/lib/state-ops.cjs');
17
- const skillMiner = require('../skill-miner.cjs');
18
-
19
- function mkFixture() {
20
- const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-skill-'));
21
- const dbPath = path.join(repoRoot, '.ultra', 'state.db');
22
- const { db } = initStateDb(dbPath);
23
- const skillsRoot = path.join(repoRoot, 'skills');
24
- fs.mkdirSync(skillsRoot, { recursive: true });
25
- return { repoRoot, db, skillsRoot };
26
- }
27
-
28
- function teardown(repoRoot, db) {
29
- try { closeStateDb(db); } catch (_) { /* ignore */ }
30
- try { fs.rmSync(repoRoot, { recursive: true, force: true }); } catch (_) { /* ignore */ }
31
- }
32
-
33
- function seed(db, { sid, task_id, title = 'demo', tag = null }) {
34
- ops.createTask(db, { id: task_id, title, type: 'feature', priority: 'P1', tag });
35
- ops.createSession(db, {
36
- sid, task_id, runtime: 'claude',
37
- worktree_path: `/tmp/${sid}/wt`, artifact_dir: `/tmp/${sid}/art`,
38
- });
39
- }
40
-
41
- test('mineSession: task_completed signal → generates draft', () => {
42
- const { repoRoot, db, skillsRoot } = mkFixture();
43
- try {
44
- seed(db, { sid: 's-done', task_id: 't-done', title: 'ship auth flow' });
45
- ops.patchTask(db, 't-done', { status: 'in_progress' });
46
- ops.appendEvent(db, { type: 'task_completed', task_id: 't-done', session_id: 's-done', payload: { to: 'completed' } });
47
- ops.patchTask(db, 't-done', { status: 'completed' });
48
-
49
- const out = skillMiner.mineSession(db, { sid: 's-done', skillsRoot });
50
- assert.equal(out.drafts.length, 1);
51
- assert.match(out.drafts[0], /_unverified\.md$/);
52
- const draft = fs.readFileSync(out.drafts[0], 'utf8');
53
- assert.match(draft, /kind: task-completion/);
54
- assert.match(draft, /unverified: true/);
55
- assert.match(draft, /ship auth flow/);
56
- } finally { teardown(repoRoot, db); }
57
- });
58
-
59
- test('mineSession: task_circuit_broken signal → debug-pattern draft', () => {
60
- const { repoRoot, db, skillsRoot } = mkFixture();
61
- try {
62
- seed(db, { sid: 's-cb', task_id: 't-cb', title: 'flaky integration' });
63
- ops.appendEvent(db, { type: 'task_circuit_broken', task_id: 't-cb', session_id: 's-cb', payload: { threshold: 3 } });
64
- const out = skillMiner.mineSession(db, { sid: 's-cb', skillsRoot });
65
- assert.equal(out.drafts.length, 1);
66
- const draft = fs.readFileSync(out.drafts[0], 'utf8');
67
- assert.match(draft, /kind: debug-pattern/);
68
- } finally { teardown(repoRoot, db); }
69
- });
70
-
71
- test('mineSession: session_crashed signal → recovery-pattern draft', () => {
72
- const { repoRoot, db, skillsRoot } = mkFixture();
73
- try {
74
- seed(db, { sid: 's-crash', task_id: 't-crash', title: 'crashing task' });
75
- ops.appendEvent(db, { type: 'session_crashed', task_id: 't-crash', session_id: 's-crash', payload: { to: 'crashed' } });
76
- const out = skillMiner.mineSession(db, { sid: 's-crash', skillsRoot });
77
- assert.equal(out.drafts.length, 1);
78
- const draft = fs.readFileSync(out.drafts[0], 'utf8');
79
- assert.match(draft, /kind: recovery-pattern/);
80
- } finally { teardown(repoRoot, db); }
81
- });
82
-
83
- test('mineSession: no trigger signals → no draft', () => {
84
- const { repoRoot, db, skillsRoot } = mkFixture();
85
- try {
86
- seed(db, { sid: 's-quiet', task_id: 't-quiet', title: 'nothing happened' });
87
- const out = skillMiner.mineSession(db, { sid: 's-quiet', skillsRoot });
88
- assert.equal(out.drafts.length, 0);
89
- } finally { teardown(repoRoot, db); }
90
- });
91
-
92
- test('mineSession: idempotent — running twice on same sid yields no new drafts', () => {
93
- const { repoRoot, db, skillsRoot } = mkFixture();
94
- try {
95
- seed(db, { sid: 's-idem', task_id: 't-idem', title: 'shipped' });
96
- ops.patchTask(db, 't-idem', { status: 'in_progress' });
97
- ops.appendEvent(db, { type: 'task_completed', task_id: 't-idem', session_id: 's-idem', payload: { to: 'completed' } });
98
- ops.patchTask(db, 't-idem', { status: 'completed' });
99
-
100
- const first = skillMiner.mineSession(db, { sid: 's-idem', skillsRoot });
101
- const second = skillMiner.mineSession(db, { sid: 's-idem', skillsRoot });
102
- assert.equal(first.drafts.length, 1);
103
- assert.equal(second.drafts.length, 0, 'second mining should see existing draft and skip');
104
- const learned = fs.readdirSync(path.join(skillsRoot, 'learned'));
105
- assert.equal(learned.length, 1);
106
- } finally { teardown(repoRoot, db); }
107
- });
108
-
109
- test('mineSession: missing session → empty drafts array', () => {
110
- const { repoRoot, db, skillsRoot } = mkFixture();
111
- try {
112
- const out = skillMiner.mineSession(db, { sid: 's-nope', skillsRoot });
113
- assert.deepEqual(out.drafts, []);
114
- } finally { teardown(repoRoot, db); }
115
- });
116
-
117
- test('AC flow: 5 mock sessions (3 with signal, 2 empty) → ≥3 drafts', () => {
118
- const { repoRoot, db, skillsRoot } = mkFixture();
119
- try {
120
- // 3 with a trigger signal
121
- for (const [i, kind] of [['ac1', 'completed'], ['ac2', 'crashed'], ['ac3', 'broken']]) {
122
- seed(db, { sid: `s-${i}`, task_id: `t-${i}`, title: `task ${i}` });
123
- if (kind === 'completed') {
124
- ops.patchTask(db, `t-${i}`, { status: 'in_progress' });
125
- ops.appendEvent(db, { type: 'task_completed', task_id: `t-${i}`, session_id: `s-${i}`, payload: {} });
126
- ops.patchTask(db, `t-${i}`, { status: 'completed' });
127
- } else if (kind === 'crashed') {
128
- ops.appendEvent(db, { type: 'session_crashed', task_id: `t-${i}`, session_id: `s-${i}`, payload: {} });
129
- } else if (kind === 'broken') {
130
- ops.appendEvent(db, { type: 'task_circuit_broken', task_id: `t-${i}`, session_id: `s-${i}`, payload: { threshold: 3 } });
131
- }
132
- }
133
- // 2 empty
134
- for (const i of ['ac4', 'ac5']) {
135
- seed(db, { sid: `s-${i}`, task_id: `t-${i}`, title: `task ${i}` });
136
- }
137
-
138
- let totalDrafts = 0;
139
- for (const i of ['ac1', 'ac2', 'ac3', 'ac4', 'ac5']) {
140
- const out = skillMiner.mineSession(db, { sid: `s-${i}`, skillsRoot });
141
- totalDrafts += out.drafts.length;
142
- }
143
- assert.ok(totalDrafts >= 3, `expected ≥3 drafts across 5 sessions, got ${totalDrafts}`);
144
- const learned = fs.readdirSync(path.join(skillsRoot, 'learned'));
145
- assert.ok(learned.length >= 3);
146
- } finally { teardown(repoRoot, db); }
147
- });
@@ -1,148 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 8B.3 — N-concurrent git worktree management.
4
- //
5
- // Session-runner (Phase 4.5) already encapsulates `git worktree add/remove`
6
- // for a single session. worktree-manager is the registry / batch layer:
7
- // allocate N, track them via `git worktree list --porcelain`, release one
8
- // or all (for crash recovery), detect filesystem orphans.
9
- //
10
- // AC (from PLAN §6 Phase 8B.3):
11
- // - 3 concurrent slices → 3 independent worktrees, git worktree list
12
- // shows them; main branch checkout untouched
13
- // - 5 rapid allocations in one tick → no .git/config.lock contention
14
- // (Node single-thread execFileSync is naturally serialized)
15
- // - releaseAll cleans filesystem leftovers even when git forgot them
16
-
17
- const { test } = require('node:test');
18
- const assert = require('node:assert/strict');
19
- const fs = require('node:fs');
20
- const os = require('node:os');
21
- const path = require('node:path');
22
- const { execFileSync } = require('node:child_process');
23
-
24
- const wm = require('../worktree-manager.cjs');
25
-
26
- function mkRepo() {
27
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-wtmgr-'));
28
- execFileSync('git', ['init', '-q'], { cwd: dir });
29
- execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: dir });
30
- execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: dir });
31
- fs.writeFileSync(path.join(dir, 'README.md'), '# seed\n');
32
- execFileSync('git', ['add', '-A'], { cwd: dir });
33
- execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: dir });
34
- return dir;
35
- }
36
-
37
- function cleanup(dir) {
38
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
39
- }
40
-
41
- test('allocate: creates worktree under .ultra/worktrees/<sid> and returns path', () => {
42
- const repo = mkRepo();
43
- try {
44
- const { worktree_path } = wm.allocate({ repoRoot: repo, sid: 'sess-abc' });
45
- assert.ok(worktree_path.endsWith('.ultra/worktrees/sess-abc'));
46
- assert.ok(fs.existsSync(worktree_path), 'worktree dir should exist');
47
- assert.ok(fs.existsSync(path.join(worktree_path, 'README.md')), 'seed file visible');
48
- } finally { cleanup(repo); }
49
- });
50
-
51
- test('release: removes worktree dir and unregisters from git', () => {
52
- const repo = mkRepo();
53
- try {
54
- const { worktree_path } = wm.allocate({ repoRoot: repo, sid: 'sess-rel' });
55
- wm.release({ repoRoot: repo, worktree_path });
56
- assert.equal(fs.existsSync(worktree_path), false, 'dir should be removed');
57
- const active = wm.listActive(repo);
58
- assert.equal(active.length, 0, 'no slices should remain');
59
- } finally { cleanup(repo); }
60
- });
61
-
62
- test('3 concurrent allocates → 3 independent worktrees visible via listActive', () => {
63
- const repo = mkRepo();
64
- try {
65
- const sids = ['sess-a', 'sess-b', 'sess-c'];
66
- const paths = sids.map((sid) => wm.allocate({ repoRoot: repo, sid }).worktree_path);
67
- for (const p of paths) assert.ok(fs.existsSync(p), `${p} missing`);
68
- const active = wm.listActive(repo);
69
- const activeSids = active.map((a) => a.sid).sort();
70
- assert.deepEqual(activeSids, sids.slice().sort());
71
- // Every entry has a distinct worktree_path
72
- const uniquePaths = new Set(active.map((a) => a.worktree_path));
73
- assert.equal(uniquePaths.size, 3);
74
- } finally { cleanup(repo); }
75
- });
76
-
77
- test('5 rapid allocates in one tick → no .git/config.lock contention', () => {
78
- const repo = mkRepo();
79
- try {
80
- const sids = ['s1', 's2', 's3', 's4', 's5'];
81
- // Execute synchronously back-to-back; Node single-thread guarantees
82
- // serialized git calls, so no file-lock race.
83
- for (const sid of sids) {
84
- wm.allocate({ repoRoot: repo, sid });
85
- }
86
- assert.equal(wm.listActive(repo).length, 5);
87
- } finally { cleanup(repo); }
88
- });
89
-
90
- test('listActive: only returns worktrees under .ultra/worktrees/ (main excluded)', () => {
91
- const repo = mkRepo();
92
- try {
93
- // Manually add a worktree OUTSIDE .ultra/worktrees — should be ignored.
94
- const outsideDir = path.join(repo, 'other-wt');
95
- execFileSync('git', ['worktree', 'add', '--detach', outsideDir, 'HEAD'], {
96
- cwd: repo, stdio: 'pipe',
97
- });
98
- wm.allocate({ repoRoot: repo, sid: 'sess-domain' });
99
- const active = wm.listActive(repo);
100
- assert.equal(active.length, 1);
101
- assert.equal(active[0].sid, 'sess-domain');
102
- } finally { cleanup(repo); }
103
- });
104
-
105
- test('listActive: empty repo → []', () => {
106
- const repo = mkRepo();
107
- try {
108
- assert.deepEqual(wm.listActive(repo), []);
109
- } finally { cleanup(repo); }
110
- });
111
-
112
- test('releaseAll: clears all allocated worktrees + filesystem orphans', () => {
113
- const repo = mkRepo();
114
- try {
115
- wm.allocate({ repoRoot: repo, sid: 'x1' });
116
- wm.allocate({ repoRoot: repo, sid: 'x2' });
117
- // Create an orphan dir: git doesn't know about it, but fs exists.
118
- const orphanDir = path.join(repo, '.ultra', 'worktrees', 'orphan-dir');
119
- fs.mkdirSync(orphanDir, { recursive: true });
120
- fs.writeFileSync(path.join(orphanDir, 'junk.txt'), 'x');
121
-
122
- const result = wm.releaseAll(repo);
123
- assert.equal(result.cleaned, 2, 'should report 2 git-tracked cleanups');
124
- assert.equal(wm.listActive(repo).length, 0);
125
- assert.equal(fs.existsSync(orphanDir), false, 'orphan must be swept');
126
- } finally { cleanup(repo); }
127
- });
128
-
129
- test('allocate: baseRef honored — worktree checked out at given ref', () => {
130
- const repo = mkRepo();
131
- try {
132
- // Make a second commit so we have two distinct SHAs.
133
- fs.writeFileSync(path.join(repo, 'second.md'), '# second\n');
134
- execFileSync('git', ['add', '-A'], { cwd: repo });
135
- execFileSync('git', ['commit', '-q', '-m', 'second'], { cwd: repo });
136
- const firstSha = execFileSync('git', ['rev-parse', 'HEAD~1'], {
137
- cwd: repo, stdio: ['ignore', 'pipe', 'pipe'],
138
- }).toString().trim();
139
-
140
- const { worktree_path } = wm.allocate({
141
- repoRoot: repo, sid: 'sess-ref', baseRef: firstSha,
142
- });
143
- const wtSha = execFileSync('git', ['rev-parse', 'HEAD'], {
144
- cwd: worktree_path, stdio: ['ignore', 'pipe', 'pipe'],
145
- }).toString().trim();
146
- assert.equal(wtSha, firstSha);
147
- } finally { cleanup(repo); }
148
- });
@@ -1,38 +0,0 @@
1
- # Architecture research
2
-
3
- Read this reference when the system boundary, quality attributes, integration seams,
4
- technology choice, or implementation constraints are unresolved.
5
-
6
- ## Current system evidence
7
-
8
- Start from the current checkout:
9
-
10
- - entry points and external interfaces;
11
- - domain boundaries and authoritative state;
12
- - data flows and side effects;
13
- - existing dependencies and deployment topology;
14
- - failure, recovery, authorization, and observability paths.
15
-
16
- Do not design a replacement architecture before identifying the live path and the
17
- constraint that requires change.
18
-
19
- ## Decision method
20
-
21
- For each material architecture decision:
22
-
23
- 1. state the required capability and quality constraints;
24
- 2. identify credible options already compatible with the system when possible;
25
- 3. use official primary documentation for library or platform behavior;
26
- 4. evaluate integration cost, operability, failure modes, migration, and rollback;
27
- 5. record the selected option, rejected alternatives that may recur, and the evidence
28
- that would reopen the decision.
29
-
30
- Do not compare technologies for ceremony. A single established repository pattern is
31
- the default when it satisfies the contract.
32
-
33
- ## Record the result
34
-
35
- Update the architecture specification with system context, boundaries, building
36
- blocks, runtime flow, authoritative state, external contracts, quality scenarios,
37
- deployment constraints, and accepted decisions. Connect each required behavior to a
38
- reachable public seam and a verification approach.
@@ -1,39 +0,0 @@
1
- # Problem and product research
2
-
3
- Read this reference when the problem, value, market context, or product direction is
4
- uncertain.
5
-
6
- ## Establish the problem
7
-
8
- - Identify the affected actor, current behavior, observable cost, and desired outcome.
9
- - Distinguish user evidence from stakeholder belief and solution preference.
10
- - Find current alternatives, including manual work or doing nothing.
11
- - State what evidence would disprove the problem framing.
12
-
13
- Use repository artifacts, support data, interviews, analytics, or primary external
14
- sources that actually exist. Do not invent quotes, demand, or market size.
15
-
16
- ## Decide how much market work is needed
17
-
18
- Market sizing and competitive research are optional. Use them only when they affect a
19
- funding, positioning, pricing, or build-versus-buy decision.
20
-
21
- When needed:
22
-
23
- 1. define the market boundary and time period;
24
- 2. show the calculation inputs and sources;
25
- 3. separate direct, adjacent, and substitute alternatives;
26
- 4. explain how the evidence changes the product decision.
27
-
28
- Do not present unsourced top-down totals or call a category competitor-free.
29
-
30
- ## Record the result
31
-
32
- Update the discovery specification with:
33
-
34
- - problem and affected actors;
35
- - observed evidence and source quality;
36
- - desired outcome and non-goals;
37
- - material alternatives and tradeoffs;
38
- - assumptions, validation method, and blocking questions;
39
- - the accepted product decision and why it follows from the evidence.