opencastle 0.31.7 → 0.32.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 (189) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.mjs +15 -0
  3. package/dist/cli/agents.d.ts.map +1 -1
  4. package/dist/cli/agents.js +19 -5
  5. package/dist/cli/agents.js.map +1 -1
  6. package/dist/cli/artifacts-cli.d.ts +3 -0
  7. package/dist/cli/artifacts-cli.d.ts.map +1 -0
  8. package/dist/cli/artifacts-cli.js +36 -0
  9. package/dist/cli/artifacts-cli.js.map +1 -0
  10. package/dist/cli/baselines.d.ts.map +1 -1
  11. package/dist/cli/baselines.js +11 -0
  12. package/dist/cli/baselines.js.map +1 -1
  13. package/dist/cli/convoy/artifacts.d.ts +25 -0
  14. package/dist/cli/convoy/artifacts.d.ts.map +1 -0
  15. package/dist/cli/convoy/artifacts.js +129 -0
  16. package/dist/cli/convoy/artifacts.js.map +1 -0
  17. package/dist/cli/convoy/artifacts.test.d.ts +2 -0
  18. package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
  19. package/dist/cli/convoy/artifacts.test.js +169 -0
  20. package/dist/cli/convoy/artifacts.test.js.map +1 -0
  21. package/dist/cli/convoy/compaction.d.ts +23 -0
  22. package/dist/cli/convoy/compaction.d.ts.map +1 -0
  23. package/dist/cli/convoy/compaction.js +117 -0
  24. package/dist/cli/convoy/compaction.js.map +1 -0
  25. package/dist/cli/convoy/compaction.test.d.ts +2 -0
  26. package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
  27. package/dist/cli/convoy/compaction.test.js +205 -0
  28. package/dist/cli/convoy/compaction.test.js.map +1 -0
  29. package/dist/cli/convoy/contracts.d.ts +22 -0
  30. package/dist/cli/convoy/contracts.d.ts.map +1 -0
  31. package/dist/cli/convoy/contracts.js +254 -0
  32. package/dist/cli/convoy/contracts.js.map +1 -0
  33. package/dist/cli/convoy/contracts.test.d.ts +2 -0
  34. package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
  35. package/dist/cli/convoy/contracts.test.js +239 -0
  36. package/dist/cli/convoy/contracts.test.js.map +1 -0
  37. package/dist/cli/convoy/dag-analysis.d.ts +40 -0
  38. package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
  39. package/dist/cli/convoy/dag-analysis.js +282 -0
  40. package/dist/cli/convoy/dag-analysis.js.map +1 -0
  41. package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
  42. package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
  43. package/dist/cli/convoy/dag-analysis.test.js +289 -0
  44. package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
  45. package/dist/cli/convoy/effort-scaling.d.ts +20 -0
  46. package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
  47. package/dist/cli/convoy/effort-scaling.js +82 -0
  48. package/dist/cli/convoy/effort-scaling.js.map +1 -0
  49. package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
  50. package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
  51. package/dist/cli/convoy/effort-scaling.test.js +120 -0
  52. package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
  53. package/dist/cli/convoy/engine.d.ts.map +1 -1
  54. package/dist/cli/convoy/engine.js +280 -6
  55. package/dist/cli/convoy/engine.js.map +1 -1
  56. package/dist/cli/convoy/engine.test.js +155 -18
  57. package/dist/cli/convoy/engine.test.js.map +1 -1
  58. package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
  59. package/dist/cli/convoy/event-schemas.js +55 -0
  60. package/dist/cli/convoy/event-schemas.js.map +1 -1
  61. package/dist/cli/convoy/isolation.d.ts +27 -0
  62. package/dist/cli/convoy/isolation.d.ts.map +1 -0
  63. package/dist/cli/convoy/isolation.js +120 -0
  64. package/dist/cli/convoy/isolation.js.map +1 -0
  65. package/dist/cli/convoy/isolation.test.d.ts +2 -0
  66. package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
  67. package/dist/cli/convoy/isolation.test.js +105 -0
  68. package/dist/cli/convoy/isolation.test.js.map +1 -0
  69. package/dist/cli/convoy/review-stages.d.ts +9 -0
  70. package/dist/cli/convoy/review-stages.d.ts.map +1 -0
  71. package/dist/cli/convoy/review-stages.js +134 -0
  72. package/dist/cli/convoy/review-stages.js.map +1 -0
  73. package/dist/cli/convoy/review-stages.test.d.ts +2 -0
  74. package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
  75. package/dist/cli/convoy/review-stages.test.js +197 -0
  76. package/dist/cli/convoy/review-stages.test.js.map +1 -0
  77. package/dist/cli/convoy/skill-refinement.d.ts +39 -0
  78. package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
  79. package/dist/cli/convoy/skill-refinement.js +239 -0
  80. package/dist/cli/convoy/skill-refinement.js.map +1 -0
  81. package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
  82. package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
  83. package/dist/cli/convoy/skill-refinement.test.js +230 -0
  84. package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
  85. package/dist/cli/convoy/spec-builder.d.ts +1 -0
  86. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  87. package/dist/cli/convoy/spec-builder.js +11 -0
  88. package/dist/cli/convoy/spec-builder.js.map +1 -1
  89. package/dist/cli/convoy/spec-builder.test.js +54 -0
  90. package/dist/cli/convoy/spec-builder.test.js.map +1 -1
  91. package/dist/cli/convoy/store.d.ts +3 -2
  92. package/dist/cli/convoy/store.d.ts.map +1 -1
  93. package/dist/cli/convoy/store.js +20 -2
  94. package/dist/cli/convoy/store.js.map +1 -1
  95. package/dist/cli/convoy/store.test.js +15 -15
  96. package/dist/cli/convoy/store.test.js.map +1 -1
  97. package/dist/cli/convoy/tdd-gate.d.ts +15 -0
  98. package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
  99. package/dist/cli/convoy/tdd-gate.js +119 -0
  100. package/dist/cli/convoy/tdd-gate.js.map +1 -0
  101. package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
  102. package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
  103. package/dist/cli/convoy/tdd-gate.test.js +227 -0
  104. package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
  105. package/dist/cli/convoy/types.d.ts +91 -0
  106. package/dist/cli/convoy/types.d.ts.map +1 -1
  107. package/dist/cli/convoy/types.js +8 -0
  108. package/dist/cli/convoy/types.js.map +1 -1
  109. package/dist/cli/insights.d.ts +3 -0
  110. package/dist/cli/insights.d.ts.map +1 -0
  111. package/dist/cli/insights.js +94 -0
  112. package/dist/cli/insights.js.map +1 -0
  113. package/dist/cli/lesson.d.ts.map +1 -1
  114. package/dist/cli/lesson.js +7 -0
  115. package/dist/cli/lesson.js.map +1 -1
  116. package/dist/cli/log.d.ts.map +1 -1
  117. package/dist/cli/log.js +7 -0
  118. package/dist/cli/log.js.map +1 -1
  119. package/dist/cli/package-config.d.ts +12 -0
  120. package/dist/cli/package-config.d.ts.map +1 -0
  121. package/dist/cli/package-config.js +37 -0
  122. package/dist/cli/package-config.js.map +1 -0
  123. package/dist/cli/package.d.ts +23 -0
  124. package/dist/cli/package.d.ts.map +1 -0
  125. package/dist/cli/package.js +285 -0
  126. package/dist/cli/package.js.map +1 -0
  127. package/dist/cli/package.test.d.ts +2 -0
  128. package/dist/cli/package.test.d.ts.map +1 -0
  129. package/dist/cli/package.test.js +236 -0
  130. package/dist/cli/package.test.js.map +1 -0
  131. package/dist/cli/pipeline.d.ts +6 -0
  132. package/dist/cli/pipeline.d.ts.map +1 -1
  133. package/dist/cli/pipeline.js +15 -2
  134. package/dist/cli/pipeline.js.map +1 -1
  135. package/dist/cli/run/schema.d.ts.map +1 -1
  136. package/dist/cli/run/schema.js +32 -0
  137. package/dist/cli/run/schema.js.map +1 -1
  138. package/dist/cli/run/schema.test.js +51 -0
  139. package/dist/cli/run/schema.test.js.map +1 -1
  140. package/dist/cli/skills.d.ts +3 -0
  141. package/dist/cli/skills.d.ts.map +1 -0
  142. package/dist/cli/skills.js +107 -0
  143. package/dist/cli/skills.js.map +1 -0
  144. package/dist/cli/types.d.ts +4 -1
  145. package/dist/cli/types.d.ts.map +1 -1
  146. package/package.json +2 -1
  147. package/src/cli/agents.ts +20 -5
  148. package/src/cli/artifacts-cli.ts +41 -0
  149. package/src/cli/baselines.ts +12 -0
  150. package/src/cli/convoy/artifacts.test.ts +201 -0
  151. package/src/cli/convoy/artifacts.ts +186 -0
  152. package/src/cli/convoy/compaction.test.ts +245 -0
  153. package/src/cli/convoy/compaction.ts +164 -0
  154. package/src/cli/convoy/contracts.test.ts +279 -0
  155. package/src/cli/convoy/contracts.ts +280 -0
  156. package/src/cli/convoy/dag-analysis.test.ts +349 -0
  157. package/src/cli/convoy/dag-analysis.ts +371 -0
  158. package/src/cli/convoy/effort-scaling.test.ts +140 -0
  159. package/src/cli/convoy/effort-scaling.ts +90 -0
  160. package/src/cli/convoy/engine.test.ts +175 -18
  161. package/src/cli/convoy/engine.ts +301 -7
  162. package/src/cli/convoy/event-schemas.ts +55 -0
  163. package/src/cli/convoy/isolation.test.ts +137 -0
  164. package/src/cli/convoy/isolation.ts +165 -0
  165. package/src/cli/convoy/review-stages.test.ts +235 -0
  166. package/src/cli/convoy/review-stages.ts +166 -0
  167. package/src/cli/convoy/skill-refinement.test.ts +277 -0
  168. package/src/cli/convoy/skill-refinement.ts +306 -0
  169. package/src/cli/convoy/spec-builder.test.ts +61 -0
  170. package/src/cli/convoy/spec-builder.ts +9 -0
  171. package/src/cli/convoy/store.test.ts +15 -15
  172. package/src/cli/convoy/store.ts +26 -4
  173. package/src/cli/convoy/tdd-gate.test.ts +281 -0
  174. package/src/cli/convoy/tdd-gate.ts +154 -0
  175. package/src/cli/convoy/types.ts +51 -0
  176. package/src/cli/insights.ts +99 -0
  177. package/src/cli/lesson.ts +8 -0
  178. package/src/cli/log.ts +8 -0
  179. package/src/cli/package-config.ts +48 -0
  180. package/src/cli/package.test.ts +276 -0
  181. package/src/cli/package.ts +329 -0
  182. package/src/cli/pipeline.ts +21 -2
  183. package/src/cli/run/schema.test.ts +58 -0
  184. package/src/cli/run/schema.ts +33 -0
  185. package/src/cli/skills.ts +121 -0
  186. package/src/cli/types.ts +4 -1
  187. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  188. package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
  189. package/src/orchestrator/prompts/generate-convoy.prompt.md +19 -0
@@ -1649,6 +1649,7 @@ function makeTaskRecord(overrides = {}) {
1649
1649
  dispute_id: null,
1650
1650
  drift_score: null,
1651
1651
  drift_retried: 0,
1652
+ compaction_count: 0,
1652
1653
  ...overrides,
1653
1654
  };
1654
1655
  }
@@ -1756,7 +1757,8 @@ describe('review pipeline', () => {
1756
1757
  });
1757
1758
  const result = await engine.run();
1758
1759
  expect(result.status).toBe('done');
1759
- expect(mockReviewRunner).toHaveBeenCalledOnce();
1760
+ // Two-stage review: stage 1 (spec compliance) + stage 2 (code quality) = 2 calls
1761
+ expect(mockReviewRunner).toHaveBeenCalledTimes(2);
1760
1762
  expect(mockReviewRunner).toHaveBeenCalledWith(expect.objectContaining({ agent: 'developer' }), 'fast', 'default');
1761
1763
  });
1762
1764
  it('fast review BLOCK + retries remaining — task retried with feedback prepended', async () => {
@@ -1766,8 +1768,9 @@ describe('review pipeline', () => {
1766
1768
  return Promise.resolve({ success: true, output: 'ok', exitCode: 0 });
1767
1769
  });
1768
1770
  const mockReviewRunner = vi.fn()
1769
- .mockResolvedValueOnce({ verdict: 'block', feedback: 'Missing tests', tokens: 50, model: 'reviewer' })
1770
- .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 50, model: 'reviewer' });
1771
+ .mockResolvedValueOnce({ verdict: 'block', feedback: 'Missing tests', tokens: 50, model: 'reviewer' }) // round 1 stage 1 → block (short-circuits)
1772
+ .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 50, model: 'reviewer' }) // round 2 stage 1 → pass
1773
+ .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 50, model: 'reviewer' }); // round 2 stage 2 → pass
1771
1774
  const engine = makeEngine({
1772
1775
  spec: makeSpec({ defaults: { review: 'fast' } }, [{ max_retries: 1 }]),
1773
1776
  specYaml: 'name: test',
@@ -1780,7 +1783,8 @@ describe('review pipeline', () => {
1780
1783
  const result = await engine.run();
1781
1784
  expect(result.status).toBe('done');
1782
1785
  expect(adapter.execute).toHaveBeenCalledTimes(2);
1783
- expect(mockReviewRunner).toHaveBeenCalledTimes(2);
1786
+ // Round 1: stage 1 blocks (1 call). Round 2: stage 1 pass + stage 2 pass (2 calls). Total: 3
1787
+ expect(mockReviewRunner).toHaveBeenCalledTimes(3);
1784
1788
  // Prompt on second attempt should contain feedback
1785
1789
  const secondPrompt = adapter.execute.mock.calls[1][0].prompt;
1786
1790
  expect(secondPrompt).toContain('Missing tests');
@@ -1809,10 +1813,10 @@ describe('review pipeline', () => {
1809
1813
  let callCount = 0;
1810
1814
  const mockReviewRunner = vi.fn().mockImplementation(() => {
1811
1815
  callCount++;
1812
- // 2 pass, 1 block
1813
- return Promise.resolve(callCount <= 2
1814
- ? { verdict: 'pass', feedback: '', tokens: 30, model: 'reviewer' }
1815
- : { verdict: 'block', feedback: 'Minor issue', tokens: 30, model: 'reviewer' });
1816
+ // Reviewer C blocks at stage 1 (call 3); reviewers A and B pass both stages (calls 1,2,4,5)
1817
+ return Promise.resolve(callCount === 3
1818
+ ? { verdict: 'block', feedback: 'Minor issue', tokens: 30, model: 'reviewer' }
1819
+ : { verdict: 'pass', feedback: '', tokens: 30, model: 'reviewer' });
1816
1820
  });
1817
1821
  const engine = makeEngine({
1818
1822
  spec: makeSpec({ defaults: { review: 'panel' } }),
@@ -1825,7 +1829,8 @@ describe('review pipeline', () => {
1825
1829
  });
1826
1830
  const result = await engine.run();
1827
1831
  expect(result.status).toBe('done');
1828
- expect(mockReviewRunner).toHaveBeenCalledTimes(3);
1832
+ // Two-stage panel: 2 pass reviewers × 2 stages + 1 block reviewer × 1 stage = 5 calls
1833
+ expect(mockReviewRunner).toHaveBeenCalledTimes(5);
1829
1834
  });
1830
1835
  it('panel review 2/3 BLOCK — task retried with MUST-FIX', async () => {
1831
1836
  let reviewCallCount = 0;
@@ -1875,9 +1880,9 @@ describe('review pipeline', () => {
1875
1880
  });
1876
1881
  const result = await engine.run();
1877
1882
  expect(result.status).toBe('done');
1878
- // first task: budget not exceeded (0 < 100), review runs
1879
- // second task: budget exceeded (200 >= 100), review skipped
1880
- expect(mockReviewRunner).toHaveBeenCalledTimes(1);
1883
+ // first task: budget not exceeded (0 < 100), two-stage review runs (2 calls, total 400 tokens)
1884
+ // second task: budget exceeded (400 >= 100), review skipped
1885
+ expect(mockReviewRunner).toHaveBeenCalledTimes(2);
1881
1886
  });
1882
1887
  it('auto route: developer agent with empty diff → auto-pass (no reviewer call)', async () => {
1883
1888
  // Given: 'auto' review setting, developer agent, empty diff (git will fail on mock path)
@@ -1910,7 +1915,7 @@ describe('review pipeline', () => {
1910
1915
  const store = createConvoyStore(dbPath);
1911
1916
  const tasks = store.getTasksByConvoy(result.convoyId);
1912
1917
  store.close();
1913
- expect(tasks[0].review_tokens).toBe(77);
1918
+ expect(tasks[0].review_tokens).toBe(154); // two-stage: 77 (stage 1) + 77 (stage 2)
1914
1919
  expect(tasks[0].review_level).toBe('fast');
1915
1920
  expect(tasks[0].review_verdict).toBe('pass');
1916
1921
  });
@@ -1952,8 +1957,9 @@ describe('review pipeline', () => {
1952
1957
  });
1953
1958
  it('full fast-review flow: BLOCK on first attempt → retry → PASS → done with complete events', async () => {
1954
1959
  const mockReviewRunner = vi.fn()
1955
- .mockResolvedValueOnce({ verdict: 'block', feedback: 'Add more tests', tokens: 40, model: 'reviewer' })
1956
- .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 35, model: 'reviewer' });
1960
+ .mockResolvedValueOnce({ verdict: 'block', feedback: 'Add more tests', tokens: 40, model: 'reviewer' }) // round 1 stage 1 → block
1961
+ .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 35, model: 'reviewer' }) // round 2 stage 1 → pass
1962
+ .mockResolvedValueOnce({ verdict: 'pass', feedback: '', tokens: 35, model: 'reviewer' }); // round 2 stage 2 → pass
1957
1963
  const engine = makeEngine({
1958
1964
  spec: makeSpec({ defaults: { review: 'fast' } }, [{ id: 'task-1', max_retries: 1 }]),
1959
1965
  specYaml: 'name: test',
@@ -1966,7 +1972,8 @@ describe('review pipeline', () => {
1966
1972
  const result = await engine.run();
1967
1973
  expect(result.status).toBe('done');
1968
1974
  expect(adapter.execute).toHaveBeenCalledTimes(2);
1969
- expect(mockReviewRunner).toHaveBeenCalledTimes(2);
1975
+ // Round 1: 1 call (block short-circuits). Round 2: 2 calls (stage 1 + stage 2). Total: 3
1976
+ expect(mockReviewRunner).toHaveBeenCalledTimes(3);
1970
1977
  const store = createConvoyStore(dbPath);
1971
1978
  const tasks = store.getTasksByConvoy(result.convoyId);
1972
1979
  const events = store.getEvents(result.convoyId);
@@ -2009,7 +2016,9 @@ describe('review pipeline', () => {
2009
2016
  const result = await engine.run();
2010
2017
  expect(result.status).toBe('done');
2011
2018
  expect(adapter.execute).toHaveBeenCalledTimes(2);
2012
- expect(mockReviewRunner).toHaveBeenCalledTimes(6);
2019
+ // Two-stage panel round 1: 2 blocks ×1 call + 1 pass ×2 calls = 4 calls
2020
+ // Two-stage panel round 2: 3 pass ×2 calls = 6 calls. Total: 10
2021
+ expect(mockReviewRunner).toHaveBeenCalledTimes(10);
2013
2022
  const store = createConvoyStore(dbPath);
2014
2023
  const tasks = store.getTasksByConvoy(result.convoyId);
2015
2024
  store.close();
@@ -2099,10 +2108,16 @@ describe('drift detection', () => {
2099
2108
  });
2100
2109
  it('detect_drift=true triggers drift check and retries on low confidence', async () => {
2101
2110
  // Call sequence: main task → drift check (low score) → main task retry
2111
+ const driftRetryOutput = [
2112
+ 'done retry',
2113
+ '<!-- OUTPUT_CONTRACT',
2114
+ '{ "files_changed": ["src/foo.ts"], "tests_added": ["src/foo.test.ts"], "summary": "done" }',
2115
+ '-->',
2116
+ ].join('\n');
2102
2117
  adapter.execute
2103
2118
  .mockResolvedValueOnce({ success: true, output: 'done', exitCode: 0 })
2104
2119
  .mockResolvedValueOnce({ success: true, output: '{"score": 0.3, "explanation": "uncertain"}', exitCode: 0 })
2105
- .mockResolvedValueOnce({ success: true, output: 'done retry', exitCode: 0 });
2120
+ .mockResolvedValueOnce({ success: true, output: driftRetryOutput, exitCode: 0 });
2106
2121
  const engine = makeEngine({
2107
2122
  spec: makeSpec({ defaults: { detect_drift: true } }, [{ id: 'task-1', max_retries: 1 }]),
2108
2123
  specYaml: 'name: test',
@@ -3183,4 +3198,126 @@ describe('createEventEmitter callsite safety', () => {
3183
3198
  testStore.close();
3184
3199
  });
3185
3200
  });
3201
+ // ── Contract retry ────────────────────────────────────────────────────────────
3202
+ describe('contract retry', () => {
3203
+ it('retries when output is missing OUTPUT_CONTRACT and retries remain', async () => {
3204
+ const validContractOutput = [
3205
+ 'Work done.',
3206
+ '<!-- OUTPUT_CONTRACT',
3207
+ '{ "files_changed": ["src/foo.ts"], "tests_added": ["src/foo.test.ts"], "summary": "implemented" }',
3208
+ '-->',
3209
+ ].join('\n');
3210
+ const adapter = makeAdapter();
3211
+ adapter.execute
3212
+ .mockResolvedValueOnce({ success: true, output: 'no contract here', exitCode: 0 })
3213
+ .mockResolvedValueOnce({ success: true, output: validContractOutput, exitCode: 0 });
3214
+ const engine = makeEngine({
3215
+ spec: makeSpec({}, [{ agent: 'developer', max_retries: 1 }]),
3216
+ specYaml: 'name: test',
3217
+ adapter,
3218
+ dbPath,
3219
+ _worktreeManager: makeWorktreeManager(),
3220
+ _mergeQueue: makeMergeQueue(),
3221
+ });
3222
+ const result = await engine.run();
3223
+ expect(result.status).toBe('done');
3224
+ expect(adapter.execute).toHaveBeenCalledTimes(2);
3225
+ // Second prompt should contain the contract retry message
3226
+ const secondPrompt = adapter.execute.mock.calls[1][0].prompt;
3227
+ expect(secondPrompt).toContain('OUTPUT_CONTRACT');
3228
+ expect(secondPrompt).toContain('Missing fields');
3229
+ const store = createConvoyStore(dbPath);
3230
+ const tasks = store.getTasksByConvoy(result.convoyId);
3231
+ store.close();
3232
+ expect(tasks[0].status).toBe('done');
3233
+ });
3234
+ it('emits contract_violation and marks done when retries exhausted', async () => {
3235
+ const adapter = makeAdapter();
3236
+ adapter.execute.mockResolvedValue({ success: true, output: 'no contract here', exitCode: 0 });
3237
+ const engine = makeEngine({
3238
+ spec: makeSpec({}, [{ agent: 'developer', max_retries: 0 }]),
3239
+ specYaml: 'name: test',
3240
+ adapter,
3241
+ dbPath,
3242
+ _worktreeManager: makeWorktreeManager(),
3243
+ _mergeQueue: makeMergeQueue(),
3244
+ });
3245
+ const result = await engine.run();
3246
+ expect(result.status).toBe('done');
3247
+ expect(adapter.execute).toHaveBeenCalledTimes(1);
3248
+ const store = createConvoyStore(dbPath);
3249
+ const events = store.getEvents(result.convoyId);
3250
+ const tasks = store.getTasksByConvoy(result.convoyId);
3251
+ store.close();
3252
+ const violationEvent = events.find(e => e.type === 'contract_violation');
3253
+ expect(violationEvent).toBeDefined();
3254
+ expect(tasks[0].status).toBe('done');
3255
+ });
3256
+ });
3257
+ // ── Compaction continuation ───────────────────────────────────────────────────
3258
+ describe('compaction continuation', () => {
3259
+ it('re-enqueues without incrementing retries when threshold exceeded', async () => {
3260
+ const adapter = makeAdapter();
3261
+ adapter.execute
3262
+ .mockResolvedValueOnce({ success: true, output: 'phase 1 done', exitCode: 0, usage: { total_tokens: 170_000 } })
3263
+ .mockResolvedValueOnce({ success: true, output: 'all done', exitCode: 0, usage: { total_tokens: 1_000 } });
3264
+ const engine = makeEngine({
3265
+ spec: makeSpec({ defaults: { compaction: { enabled: true, token_threshold_pct: 80, summary_max_tokens: 2000 } } }, [{ model: 'claude-sonnet-4-6', max_retries: 0 }]),
3266
+ specYaml: 'name: test',
3267
+ adapter,
3268
+ dbPath,
3269
+ _worktreeManager: makeWorktreeManager(),
3270
+ _mergeQueue: makeMergeQueue(),
3271
+ });
3272
+ const result = await engine.run();
3273
+ expect(result.status).toBe('done');
3274
+ expect(adapter.execute).toHaveBeenCalledTimes(2);
3275
+ const store = createConvoyStore(dbPath);
3276
+ const events = store.getEvents(result.convoyId);
3277
+ const tasks = store.getTasksByConvoy(result.convoyId);
3278
+ store.close();
3279
+ // Compaction event emitted
3280
+ const compactedEvent = events.find(e => e.type === 'context_compacted');
3281
+ expect(compactedEvent).toBeDefined();
3282
+ // Task completed successfully and retries were NOT incremented by compaction
3283
+ expect(tasks[0].status).toBe('done');
3284
+ expect(tasks[0].retries).toBe(0);
3285
+ });
3286
+ it('fails with context_exhausted when max compactions reached', async () => {
3287
+ const adapter = makeAdapter();
3288
+ // All calls return high token count — will exhaust compaction budget after 3+1 calls
3289
+ adapter.execute.mockResolvedValue({
3290
+ success: true,
3291
+ output: 'partial work',
3292
+ exitCode: 0,
3293
+ usage: { total_tokens: 170_000 },
3294
+ });
3295
+ const engine = makeEngine({
3296
+ spec: makeSpec({ defaults: { compaction: { enabled: true, token_threshold_pct: 80, summary_max_tokens: 2000 } } }, [{ model: 'claude-sonnet-4-6', max_retries: 0 }]),
3297
+ specYaml: 'name: test',
3298
+ adapter,
3299
+ dbPath,
3300
+ _worktreeManager: makeWorktreeManager(),
3301
+ _mergeQueue: makeMergeQueue(),
3302
+ });
3303
+ const result = await engine.run();
3304
+ expect(result.status).toBe('failed');
3305
+ const store = createConvoyStore(dbPath);
3306
+ const events = store.getEvents(result.convoyId);
3307
+ const tasks = store.getTasksByConvoy(result.convoyId);
3308
+ store.close();
3309
+ const exhaustedEvent = events.find(e => {
3310
+ if (e.type !== 'task_failed')
3311
+ return false;
3312
+ try {
3313
+ return JSON.parse(e.data).reason === 'context_exhausted';
3314
+ }
3315
+ catch {
3316
+ return false;
3317
+ }
3318
+ });
3319
+ expect(exhaustedEvent).toBeDefined();
3320
+ expect(tasks[0].status).toBe('failed');
3321
+ });
3322
+ });
3186
3323
  //# sourceMappingURL=engine.test.js.map