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,324 +0,0 @@
1
- 'use strict';
2
-
3
- // Integration tests for task-expander — real SQLite (Testcontainers-style
4
- // in-process) so parent_id column + status transition + task_expanded event
5
- // are exercised end-to-end. The only Test Double is the LLM client, which
6
- // we inject with deterministic child fixtures.
7
-
8
- const test = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const fs = require('node:fs');
11
- const os = require('node:os');
12
- const path = require('node:path');
13
-
14
- const { initStateDb, closeStateDb } = require('./state-db.cjs');
15
- const ops = require('./state-ops.cjs');
16
- const changes = require('./change-workflow.cjs');
17
- const { expandTask, TaskExpandError } = require('./task-expander.cjs');
18
-
19
- function tmpDb() {
20
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-expand-'));
21
- const { db } = initStateDb(path.join(dir, 'state.db'));
22
- db.prepare(
23
- `INSERT INTO baselines
24
- (id, project_name, mode, status, approved_by, approval_note, converged_at)
25
- VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
26
- ).run(new Date().toISOString());
27
- return { dir, db };
28
- }
29
-
30
- function cleanup({ dir, db }) {
31
- closeStateDb(db);
32
- fs.rmSync(dir, { recursive: true, force: true });
33
- }
34
-
35
- function fakeLlm(childrenJson, { shouldFail = false, usage = {} } = {}) {
36
- return {
37
- provider: 'anthropic',
38
- model: 'claude-sonnet-4-6',
39
- async completeJson() {
40
- if (shouldFail) throw new Error('upstream LLM 500');
41
- return {
42
- json: { children: childrenJson },
43
- raw: JSON.stringify({ children: childrenJson }),
44
- usage,
45
- model: 'claude-sonnet-4-6',
46
- provider: 'anthropic',
47
- };
48
- },
49
- };
50
- }
51
-
52
- function seedParent(db, overrides = {}) {
53
- return ops.createTask(db, {
54
- id: 'parent-1',
55
- title: 'Build authentication module',
56
- type: 'feature',
57
- priority: 'P1',
58
- complexity: 9,
59
- files_modified: ['src/auth/index.ts'],
60
- ...overrides,
61
- });
62
- }
63
-
64
- const HAPPY_CHILDREN = [
65
- { id: 'child-1', title: 'Design auth schema', type: 'architecture', priority: 'P1', complexity: 4, deps: [], files_modified: ['src/auth/schema.ts'] },
66
- { id: 'child-2', title: 'Implement login endpoint', type: 'feature', priority: 'P1', complexity: 5, deps: ['child-1'], files_modified: ['src/auth/login.ts'] },
67
- { id: 'child-3', title: 'Add signup endpoint', type: 'feature', priority: 'P2', complexity: 3, deps: ['child-1'], files_modified: ['src/auth/signup.ts'] },
68
- ];
69
-
70
- test('expandTask happy path: creates children, flips parent status, emits task_expanded', async () => {
71
- const ctx = tmpDb();
72
- try {
73
- seedParent(ctx.db);
74
- const result = await expandTask(ctx.db, {
75
- id: 'parent-1',
76
- strategy: 'llm',
77
- llmClient: fakeLlm(HAPPY_CHILDREN),
78
- });
79
- assert.equal(result.parent_id, 'parent-1');
80
- assert.equal(result.children.length, 3);
81
-
82
- const parentAfter = ops.readTask(ctx.db, 'parent-1');
83
- assert.equal(parentAfter.status, 'expanded');
84
-
85
- for (const child of HAPPY_CHILDREN) {
86
- const row = ops.readTask(ctx.db, child.id);
87
- assert.ok(row, `child ${child.id} missing`);
88
- assert.equal(row.parent_id, 'parent-1');
89
- assert.equal(row.status, 'pending');
90
- }
91
-
92
- const events = ops.subscribeEventsSince(ctx.db, { since_id: 0, limit: 100 });
93
- const expandedEvents = events.events.filter((e) => e.type === 'task_expanded');
94
- assert.equal(expandedEvents.length, 1);
95
- assert.equal(expandedEvents[0].task_id, 'parent-1');
96
- const createdEvents = events.events.filter((e) => e.type === 'task_created');
97
- assert.ok(createdEvents.length >= 4, 'parent + 3 children task_created events');
98
- } finally {
99
- cleanup(ctx);
100
- }
101
- });
102
-
103
- test('expandTask: missing parent → TASK_NOT_FOUND', async () => {
104
- const ctx = tmpDb();
105
- try {
106
- await assert.rejects(
107
- expandTask(ctx.db, {
108
- id: 'does-not-exist',
109
- llmClient: fakeLlm(HAPPY_CHILDREN),
110
- }),
111
- (err) => err instanceof TaskExpandError && err.code === 'TASK_NOT_FOUND',
112
- );
113
- } finally {
114
- cleanup(ctx);
115
- }
116
- });
117
-
118
- test('expandTask: already-expanded parent → ALREADY_EXPANDED', async () => {
119
- const ctx = tmpDb();
120
- try {
121
- seedParent(ctx.db);
122
- // First expansion succeeds
123
- await expandTask(ctx.db, {
124
- id: 'parent-1',
125
- llmClient: fakeLlm(HAPPY_CHILDREN),
126
- });
127
- // Second attempt must refuse
128
- await assert.rejects(
129
- expandTask(ctx.db, {
130
- id: 'parent-1',
131
- llmClient: fakeLlm(HAPPY_CHILDREN.map((c, i) => ({ ...c, id: `other-${i}` }))),
132
- }),
133
- (err) => err instanceof TaskExpandError && err.code === 'ALREADY_EXPANDED',
134
- );
135
- } finally {
136
- cleanup(ctx);
137
- }
138
- });
139
-
140
- test('expandTask: strategy="manual" → NOT_IMPLEMENTED', async () => {
141
- const ctx = tmpDb();
142
- try {
143
- seedParent(ctx.db);
144
- await assert.rejects(
145
- expandTask(ctx.db, {
146
- id: 'parent-1',
147
- strategy: 'manual',
148
- llmClient: fakeLlm(HAPPY_CHILDREN),
149
- }),
150
- (err) => err instanceof TaskExpandError && err.code === 'NOT_IMPLEMENTED',
151
- );
152
- } finally {
153
- cleanup(ctx);
154
- }
155
- });
156
-
157
- test('expandTask: missing llmClient → NO_LLM_CLIENT', async () => {
158
- const ctx = tmpDb();
159
- try {
160
- seedParent(ctx.db);
161
- await assert.rejects(
162
- expandTask(ctx.db, { id: 'parent-1' }),
163
- (err) => err instanceof TaskExpandError && err.code === 'NO_LLM_CLIENT',
164
- );
165
- } finally {
166
- cleanup(ctx);
167
- }
168
- });
169
-
170
- test('expandTask: LLM throws → LLM_CALL_FAILED', async () => {
171
- const ctx = tmpDb();
172
- try {
173
- seedParent(ctx.db);
174
- await assert.rejects(
175
- expandTask(ctx.db, {
176
- id: 'parent-1',
177
- llmClient: fakeLlm([], { shouldFail: true }),
178
- }),
179
- (err) => err instanceof TaskExpandError && err.code === 'LLM_CALL_FAILED',
180
- );
181
- // parent status must be unchanged
182
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
183
- } finally {
184
- cleanup(ctx);
185
- }
186
- });
187
-
188
- test('expandTask: invalid child shape → INVALID_OUTPUT, parent untouched', async () => {
189
- const ctx = tmpDb();
190
- try {
191
- seedParent(ctx.db);
192
- const bad = [{ id: 'child-1', title: 'x', type: 'feature', priority: 'P1' }]; // title too short
193
- await assert.rejects(
194
- expandTask(ctx.db, { id: 'parent-1', llmClient: fakeLlm(bad) }),
195
- (err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
196
- );
197
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
198
- } finally {
199
- cleanup(ctx);
200
- }
201
- });
202
-
203
- test('expandTask: duplicate child id → INVALID_OUTPUT', async () => {
204
- const ctx = tmpDb();
205
- try {
206
- seedParent(ctx.db);
207
- const dup = [
208
- { id: 'same', title: 'First child', type: 'feature', priority: 'P2' },
209
- { id: 'same', title: 'Second child', type: 'feature', priority: 'P2' },
210
- ];
211
- await assert.rejects(
212
- expandTask(ctx.db, { id: 'parent-1', llmClient: fakeLlm(dup) }),
213
- (err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
214
- );
215
- } finally {
216
- cleanup(ctx);
217
- }
218
- });
219
-
220
- test('expandTask: tx rollback — child id collides with existing task → parent status unchanged, no children inserted', async () => {
221
- const ctx = tmpDb();
222
- try {
223
- seedParent(ctx.db);
224
- // Pre-create a task with the same id one of the children will try
225
- ops.createTask(ctx.db, {
226
- id: 'child-1',
227
- title: 'Pre-existing task',
228
- type: 'bugfix',
229
- priority: 'P3',
230
- });
231
- await assert.rejects(
232
- expandTask(ctx.db, { id: 'parent-1', llmClient: fakeLlm(HAPPY_CHILDREN) }),
233
- /* any error is acceptable — the point is no partial state */
234
- );
235
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
236
- // child-2 / child-3 must NOT have been inserted
237
- assert.equal(ops.readTask(ctx.db, 'child-2'), null);
238
- assert.equal(ops.readTask(ctx.db, 'child-3'), null);
239
- } finally {
240
- cleanup(ctx);
241
- }
242
- });
243
-
244
- test('expandTask: children inherit parent tag', async () => {
245
- const ctx = tmpDb();
246
- try {
247
- seedParent(ctx.db, { tag: 'feat-auth' });
248
- await expandTask(ctx.db, {
249
- id: 'parent-1',
250
- llmClient: fakeLlm(HAPPY_CHILDREN),
251
- });
252
- for (const child of HAPPY_CHILDREN) {
253
- const row = ops.readTask(ctx.db, child.id);
254
- assert.equal(row.tag, 'feat-auth');
255
- }
256
- } finally {
257
- cleanup(ctx);
258
- }
259
- });
260
-
261
- test('expandTask blocks new children when baseline authority is missing, draft, or migrated', async () => {
262
- for (const baseline of ['missing', 'draft', 'migrated']) {
263
- const ctx = tmpDb();
264
- try {
265
- seedParent(ctx.db);
266
- if (baseline === 'missing') ctx.db.prepare('DELETE FROM baselines').run();
267
- else if (baseline === 'draft') {
268
- ctx.db.prepare("UPDATE baselines SET status = 'draft' WHERE id = 'test-baseline'").run();
269
- } else {
270
- ctx.db.prepare(
271
- "UPDATE baselines SET mode = 'migrated', status = 'adopting' WHERE id = 'test-baseline'",
272
- ).run();
273
- }
274
- await assert.rejects(
275
- expandTask(ctx.db, {
276
- id: 'parent-1', llmClient: fakeLlm(HAPPY_CHILDREN), rootDir: ctx.dir,
277
- }),
278
- (error) => error.code === 'BASELINE_NOT_READY',
279
- );
280
- assert.equal(ops.readTask(ctx.db, 'child-1'), null);
281
- assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
282
- } finally { cleanup(ctx); }
283
- }
284
- });
285
-
286
- test('expandTask preserves active ordinary and approved incident change ownership while adoption is incomplete', async () => {
287
- for (const kind of ['quick', 'incident']) {
288
- const ctx = tmpDb();
289
- try {
290
- if (kind === 'incident') {
291
- ctx.db.prepare(
292
- "UPDATE baselines SET mode = 'brownfield', status = 'adopting' WHERE id = 'test-baseline'",
293
- ).run();
294
- }
295
- const changeId = `expand-${kind}`;
296
- changes.createChange(ctx.db, {
297
- id: changeId,
298
- title: kind === 'incident' ? 'Recover production expansion' : 'Continue planned expansion',
299
- kind,
300
- intent: 'Keep every expanded child inside the already-authorized continuous change.',
301
- ...(kind === 'incident' ? {
302
- baseline_bypass: {
303
- approved_by: 'incident-commander',
304
- reason: 'Production recovery requires a bounded task expansion before adoption converges.',
305
- },
306
- } : {}),
307
- }, { rootDir: ctx.dir });
308
- seedParent(ctx.db, { change_id: changeId });
309
- if (kind === 'quick') {
310
- ctx.db.prepare(
311
- "UPDATE baselines SET mode = 'brownfield', status = 'adopting' WHERE id = 'test-baseline'",
312
- ).run();
313
- }
314
-
315
- const result = await expandTask(ctx.db, {
316
- id: 'parent-1', llmClient: fakeLlm(HAPPY_CHILDREN), rootDir: ctx.dir,
317
- });
318
- assert.equal(result.children.length, HAPPY_CHILDREN.length);
319
- for (const child of HAPPY_CHILDREN) {
320
- assert.equal(ops.readTask(ctx.db, child.id).change_id, changeId);
321
- }
322
- } finally { cleanup(ctx); }
323
- }
324
- });
@@ -1,158 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
-
6
- const { computeWaves } = require('./topo.cjs');
7
-
8
- function makeTask(id, deps = []) {
9
- return { id, deps };
10
- }
11
-
12
- function idsSet(ids) {
13
- return new Set(ids);
14
- }
15
-
16
- function asWaveSets(waves) {
17
- return waves.map(idsSet);
18
- }
19
-
20
- test('topo: empty array → empty waves and cycles', () => {
21
- const r = computeWaves([]);
22
- assert.deepEqual(r.waves, []);
23
- assert.deepEqual(r.cycles, []);
24
- });
25
-
26
- test('topo: single task with no deps → single wave', () => {
27
- const r = computeWaves([makeTask('A')]);
28
- assert.deepEqual(r.waves, [['A']]);
29
- assert.deepEqual(r.cycles, []);
30
- });
31
-
32
- test('topo fixture 1: linear chain A, B(A), C(B) → three serial waves', () => {
33
- const r = computeWaves([
34
- makeTask('A'),
35
- makeTask('B', ['A']),
36
- makeTask('C', ['B']),
37
- ]);
38
- assert.deepEqual(asWaveSets(r.waves), [idsSet(['A']), idsSet(['B']), idsSet(['C'])]);
39
- assert.deepEqual(r.cycles, []);
40
- });
41
-
42
- test('topo fixture 2: fan-out A→{B,C} + independent D→E → 2 waves', () => {
43
- const r = computeWaves([
44
- makeTask('A'),
45
- makeTask('B', ['A']),
46
- makeTask('C', ['A']),
47
- makeTask('D'),
48
- makeTask('E', ['D']),
49
- ]);
50
- assert.equal(r.waves.length, 2);
51
- assert.deepEqual(asWaveSets(r.waves), [
52
- idsSet(['A', 'D']),
53
- idsSet(['B', 'C', 'E']),
54
- ]);
55
- assert.deepEqual(r.cycles, []);
56
- });
57
-
58
- test('topo fixture 3: double diamond merges waves correctly', () => {
59
- const r = computeWaves([
60
- makeTask('A'),
61
- makeTask('B', ['A']),
62
- makeTask('C', ['A']),
63
- makeTask('D', ['B', 'C']),
64
- makeTask('E'),
65
- makeTask('F', ['E']),
66
- makeTask('G', ['E']),
67
- makeTask('H', ['F', 'G']),
68
- ]);
69
- assert.deepEqual(asWaveSets(r.waves), [
70
- idsSet(['A', 'E']),
71
- idsSet(['B', 'C', 'F', 'G']),
72
- idsSet(['D', 'H']),
73
- ]);
74
- assert.deepEqual(r.cycles, []);
75
- });
76
-
77
- test('topo fixture 4: 2-node cycle A↔B → cycle reported, no waves', () => {
78
- const r = computeWaves([
79
- makeTask('A', ['B']),
80
- makeTask('B', ['A']),
81
- ]);
82
- assert.deepEqual(r.waves, []);
83
- assert.equal(r.cycles.length, 1);
84
- assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B']));
85
- });
86
-
87
- test('topo fixture 5: two independent cycles A↔B and C↔D → both reported', () => {
88
- const r = computeWaves([
89
- makeTask('A', ['B']),
90
- makeTask('B', ['A']),
91
- makeTask('C', ['D']),
92
- makeTask('D', ['C']),
93
- ]);
94
- assert.deepEqual(r.waves, []);
95
- assert.equal(r.cycles.length, 2);
96
- const cycleSets = r.cycles.map((c) => idsSet(c));
97
- const expected = [idsSet(['A', 'B']), idsSet(['C', 'D'])];
98
- for (const exp of expected) {
99
- const found = cycleSets.some(
100
- (s) => s.size === exp.size && [...s].every((x) => exp.has(x)),
101
- );
102
- assert.ok(found, `cycle ${[...exp].join(',')} not reported`);
103
- }
104
- });
105
-
106
- test('topo: self-loop counts as cycle', () => {
107
- const r = computeWaves([makeTask('A', ['A'])]);
108
- assert.deepEqual(r.waves, []);
109
- assert.equal(r.cycles.length, 1);
110
- assert.deepEqual(r.cycles[0], ['A']);
111
- });
112
-
113
- test('topo: cycle plus acyclic tail — tail is waved, cycle isolated', () => {
114
- const r = computeWaves([
115
- makeTask('A', ['B']),
116
- makeTask('B', ['A']),
117
- makeTask('X'),
118
- makeTask('Y', ['X']),
119
- ]);
120
- assert.deepEqual(asWaveSets(r.waves), [idsSet(['X']), idsSet(['Y'])]);
121
- assert.equal(r.cycles.length, 1);
122
- assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B']));
123
- });
124
-
125
- test('topo: external deps (ids not in task_ids) are treated as satisfied', () => {
126
- const r = computeWaves([makeTask('A', ['X'])]);
127
- assert.deepEqual(r.waves, [['A']]);
128
- assert.deepEqual(r.cycles, []);
129
- });
130
-
131
- test('topo: input order does not change wave membership', () => {
132
- const tasks = [
133
- makeTask('A'),
134
- makeTask('B', ['A']),
135
- makeTask('C', ['A']),
136
- makeTask('D', ['B', 'C']),
137
- ];
138
- const r1 = computeWaves(tasks);
139
- const r2 = computeWaves([...tasks].reverse());
140
- assert.deepEqual(asWaveSets(r1.waves), asWaveSets(r2.waves));
141
- assert.deepEqual(r1.cycles, r2.cycles);
142
- });
143
-
144
- test('topo: rejects non-array input', () => {
145
- assert.throws(() => computeWaves(null), TypeError);
146
- assert.throws(() => computeWaves({}), TypeError);
147
- });
148
-
149
- test('topo: 3-node cycle A→B→C→A surfaced intact', () => {
150
- const r = computeWaves([
151
- makeTask('A', ['C']),
152
- makeTask('B', ['A']),
153
- makeTask('C', ['B']),
154
- ]);
155
- assert.deepEqual(r.waves, []);
156
- assert.equal(r.cycles.length, 1);
157
- assert.deepEqual(idsSet(r.cycles[0]), idsSet(['A', 'B', 'C']));
158
- });
@@ -1,125 +0,0 @@
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
- });