claude-mycelium 2.0.0 → 2.2.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 (208) hide show
  1. package/.agent-meta/_inhibitors.ndjson +1287 -0
  2. package/.agent-meta/_quarantine.json +45 -0
  3. package/.agent-meta/config.json +9 -0
  4. package/.agent-meta/tasks/_active.json +4 -0
  5. package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
  6. package/.claude/memory.db +0 -0
  7. package/.claude/settings.local.json +4 -1
  8. package/README.md +85 -233
  9. package/SECURITY.md +145 -0
  10. package/dist/agent/task-worker.d.ts +11 -0
  11. package/dist/agent/task-worker.d.ts.map +1 -0
  12. package/dist/agent/task-worker.js +173 -0
  13. package/dist/agent/task-worker.js.map +1 -0
  14. package/dist/agent/worker.d.ts +8 -0
  15. package/dist/agent/worker.d.ts.map +1 -0
  16. package/dist/agent/worker.js +97 -0
  17. package/dist/agent/worker.js.map +1 -0
  18. package/dist/bin.d.ts +7 -0
  19. package/dist/bin.d.ts.map +1 -0
  20. package/dist/bin.js +11 -0
  21. package/dist/bin.js.map +1 -0
  22. package/dist/cli/cost.d.ts +10 -0
  23. package/dist/cli/cost.d.ts.map +1 -0
  24. package/dist/cli/cost.js +163 -0
  25. package/dist/cli/cost.js.map +1 -0
  26. package/dist/cli/gc.d.ts +10 -0
  27. package/dist/cli/gc.d.ts.map +1 -0
  28. package/dist/cli/gc.js +108 -0
  29. package/dist/cli/gc.js.map +1 -0
  30. package/dist/cli/gradients.d.ts +10 -0
  31. package/dist/cli/gradients.d.ts.map +1 -0
  32. package/dist/cli/gradients.js +70 -0
  33. package/dist/cli/gradients.js.map +1 -0
  34. package/dist/cli/grow.d.ts +17 -0
  35. package/dist/cli/grow.d.ts.map +1 -0
  36. package/dist/cli/grow.js +373 -0
  37. package/dist/cli/grow.js.map +1 -0
  38. package/dist/cli/index.d.ts +17 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +74 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/init.d.ts +11 -0
  43. package/dist/cli/init.d.ts.map +1 -0
  44. package/dist/cli/init.js +97 -0
  45. package/dist/cli/init.js.map +1 -0
  46. package/dist/cli/status.d.ts +10 -0
  47. package/dist/cli/status.d.ts.map +1 -0
  48. package/dist/cli/status.js +191 -0
  49. package/dist/cli/status.js.map +1 -0
  50. package/dist/coordination/file-locks.d.ts +42 -0
  51. package/dist/coordination/file-locks.d.ts.map +1 -0
  52. package/dist/coordination/file-locks.js +269 -0
  53. package/dist/coordination/file-locks.js.map +1 -0
  54. package/dist/coordination/index.d.ts +4 -0
  55. package/dist/coordination/index.d.ts.map +1 -1
  56. package/dist/coordination/index.js +4 -0
  57. package/dist/coordination/index.js.map +1 -1
  58. package/dist/coordination/inhibitors.d.ts +84 -0
  59. package/dist/coordination/inhibitors.d.ts.map +1 -0
  60. package/dist/coordination/inhibitors.js +290 -0
  61. package/dist/coordination/inhibitors.js.map +1 -0
  62. package/dist/coordination/process-manager.d.ts +73 -0
  63. package/dist/coordination/process-manager.d.ts.map +1 -0
  64. package/dist/coordination/process-manager.js +144 -0
  65. package/dist/coordination/process-manager.js.map +1 -0
  66. package/dist/core/agent-executor.d.ts +4 -1
  67. package/dist/core/agent-executor.d.ts.map +1 -1
  68. package/dist/core/agent-executor.js +38 -12
  69. package/dist/core/agent-executor.js.map +1 -1
  70. package/dist/core/change-applier.d.ts +29 -5
  71. package/dist/core/change-applier.d.ts.map +1 -1
  72. package/dist/core/change-applier.js +254 -24
  73. package/dist/core/change-applier.js.map +1 -1
  74. package/dist/core/signals/churn.d.ts.map +1 -1
  75. package/dist/core/signals/churn.js +6 -4
  76. package/dist/core/signals/churn.js.map +1 -1
  77. package/dist/core/signals/debt.d.ts.map +1 -1
  78. package/dist/core/signals/debt.js +4 -3
  79. package/dist/core/signals/debt.js.map +1 -1
  80. package/dist/cost/cost-tracker.d.ts.map +1 -1
  81. package/dist/cost/cost-tracker.js +2 -0
  82. package/dist/cost/cost-tracker.js.map +1 -1
  83. package/dist/gc/index.d.ts +17 -0
  84. package/dist/gc/index.d.ts.map +1 -0
  85. package/dist/gc/index.js +17 -0
  86. package/dist/gc/index.js.map +1 -0
  87. package/dist/gc/runner.d.ts +39 -0
  88. package/dist/gc/runner.d.ts.map +1 -0
  89. package/dist/gc/runner.js +277 -0
  90. package/dist/gc/runner.js.map +1 -0
  91. package/dist/gc/trace-compactor.d.ts +31 -0
  92. package/dist/gc/trace-compactor.d.ts.map +1 -0
  93. package/dist/gc/trace-compactor.js +162 -0
  94. package/dist/gc/trace-compactor.js.map +1 -0
  95. package/dist/index.d.ts +5 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +6 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/prompts/index.d.ts +2 -1
  100. package/dist/prompts/index.d.ts.map +1 -1
  101. package/dist/prompts/index.js.map +1 -1
  102. package/dist/quarantine/explorer.d.ts +65 -0
  103. package/dist/quarantine/explorer.d.ts.map +1 -0
  104. package/dist/quarantine/explorer.js +175 -0
  105. package/dist/quarantine/explorer.js.map +1 -0
  106. package/dist/quarantine/index.d.ts +7 -0
  107. package/dist/quarantine/index.d.ts.map +1 -0
  108. package/dist/quarantine/index.js +7 -0
  109. package/dist/quarantine/index.js.map +1 -0
  110. package/dist/quarantine/manager.d.ts +75 -0
  111. package/dist/quarantine/manager.d.ts.map +1 -0
  112. package/dist/quarantine/manager.js +275 -0
  113. package/dist/quarantine/manager.js.map +1 -0
  114. package/dist/task/acceptance.d.ts +29 -0
  115. package/dist/task/acceptance.d.ts.map +1 -0
  116. package/dist/task/acceptance.js +228 -0
  117. package/dist/task/acceptance.js.map +1 -0
  118. package/dist/task/agent-coordinator.d.ts +40 -0
  119. package/dist/task/agent-coordinator.d.ts.map +1 -0
  120. package/dist/task/agent-coordinator.js +168 -0
  121. package/dist/task/agent-coordinator.js.map +1 -0
  122. package/dist/task/executor.d.ts +37 -0
  123. package/dist/task/executor.d.ts.map +1 -0
  124. package/dist/task/executor.js +462 -0
  125. package/dist/task/executor.js.map +1 -0
  126. package/dist/task/index.d.ts +12 -0
  127. package/dist/task/index.d.ts.map +1 -0
  128. package/dist/task/index.js +12 -0
  129. package/dist/task/index.js.map +1 -0
  130. package/dist/task/planner.d.ts +21 -0
  131. package/dist/task/planner.d.ts.map +1 -0
  132. package/dist/task/planner.js +253 -0
  133. package/dist/task/planner.js.map +1 -0
  134. package/dist/task/storage.d.ts +46 -0
  135. package/dist/task/storage.d.ts.map +1 -0
  136. package/dist/task/storage.js +266 -0
  137. package/dist/task/storage.js.map +1 -0
  138. package/dist/trace/trace-event.d.ts +2 -18
  139. package/dist/trace/trace-event.d.ts.map +1 -1
  140. package/dist/trace/trace-event.js +6 -6
  141. package/dist/trace/trace-event.js.map +1 -1
  142. package/dist/utils/file-utils.d.ts.map +1 -1
  143. package/dist/utils/file-utils.js +54 -15
  144. package/dist/utils/file-utils.js.map +1 -1
  145. package/docs/PHASE5_IMPLEMENTATION.md +237 -0
  146. package/docs/PHASES-3-7-COMPLETE.md +177 -0
  147. package/docs/PHASE_4_COMPLETE.md +135 -0
  148. package/docs/PHASE_7_DELIVERABLES.md +295 -0
  149. package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
  150. package/docs/PHASE_7_SUMMARY.txt +195 -0
  151. package/docs/RELEASE-NOTES-v2.1.md +213 -0
  152. package/docs/ROADMAP.md +194 -107
  153. package/docs/SECURITY-AUDIT.md +387 -0
  154. package/docs/SNAPSHOT.md +59 -32
  155. package/docs/implementation/phase3-summary.md +220 -0
  156. package/package.json +27 -11
  157. package/src/agent/task-worker.ts +196 -0
  158. package/src/agent/worker.ts +111 -0
  159. package/src/bin.ts +13 -0
  160. package/src/cli/cost.ts +210 -0
  161. package/src/cli/gc.ts +138 -0
  162. package/src/cli/gradients.ts +97 -0
  163. package/src/cli/grow.ts +416 -0
  164. package/src/cli/index.ts +81 -0
  165. package/src/cli/init.ts +139 -0
  166. package/src/cli/status.ts +218 -0
  167. package/src/coordination/file-locks.ts +300 -0
  168. package/src/coordination/index.ts +4 -0
  169. package/src/coordination/inhibitors.ts +345 -0
  170. package/src/coordination/process-manager.ts +199 -0
  171. package/src/core/agent-executor.ts +37 -8
  172. package/src/core/signals/churn.ts +8 -5
  173. package/src/core/signals/debt.ts +4 -3
  174. package/src/cost/cost-tracker.ts +2 -0
  175. package/src/gc/index.ts +17 -0
  176. package/src/gc/runner.ts +314 -0
  177. package/src/gc/trace-compactor.ts +187 -0
  178. package/src/index.ts +7 -1
  179. package/src/prompts/index.ts +2 -1
  180. package/src/quarantine/explorer.ts +234 -0
  181. package/src/quarantine/index.ts +7 -0
  182. package/src/quarantine/manager.ts +336 -0
  183. package/src/task/acceptance.ts +267 -0
  184. package/src/task/agent-coordinator.ts +220 -0
  185. package/src/task/executor.ts +543 -0
  186. package/src/task/index.ts +38 -0
  187. package/src/task/planner.ts +294 -0
  188. package/src/task/storage.ts +332 -0
  189. package/src/trace/trace-event.ts +7 -26
  190. package/src/utils/file-utils.ts +61 -15
  191. package/tests/cli/gc.test.ts +206 -0
  192. package/tests/cli/init.test.ts +181 -0
  193. package/tests/cli/status.test.ts +282 -0
  194. package/tests/coordination/file-locks.test.ts +196 -0
  195. package/tests/coordination/inhibitors.test.ts +459 -0
  196. package/tests/coordination/integration.test.ts +195 -0
  197. package/tests/coordination/process-manager.test.ts +165 -0
  198. package/tests/gc/trace-compactor.test.ts +245 -0
  199. package/tests/integration/phase-7.test.ts +145 -0
  200. package/tests/quarantine/explorer.test.ts +381 -0
  201. package/tests/quarantine/manager.test.ts +399 -0
  202. package/tests/security/command-injection.test.ts +88 -0
  203. package/tests/security/path-traversal.test.ts +103 -0
  204. package/tests/task/acceptance.test.ts +411 -0
  205. package/tests/task/executor.test.ts +421 -0
  206. package/tests/task/planner.test.ts +359 -0
  207. package/tests/trace/trace-event.test.ts +62 -20
  208. package/tsconfig.json +2 -2
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Tests for Inhibitor Signal System
3
+ * Validates emission, decay, and querying
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import {
10
+ emitInhibitor,
11
+ queryInhibitors,
12
+ calculateCurrentStrength,
13
+ readInhibitors,
14
+ extractFailurePattern,
15
+ summarizeCIFailure,
16
+ maybeEmitInhibitor,
17
+ checkForLoop,
18
+ gcInhibitors,
19
+ formatInhibitorForPrompt,
20
+ } from '../../src/coordination/inhibitors.js';
21
+ import type { Inhibitor, Mode } from '../../src/types/index.js';
22
+ import type { TraceEvent } from '../../src/trace/trace-event.js';
23
+
24
+ describe('Inhibitor System', () => {
25
+ let TEST_DIR: string;
26
+ let INHIBITORS_FILE: string;
27
+
28
+ beforeEach(() => {
29
+ // Create unique test directory for each test
30
+ TEST_DIR = `.agent-meta-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
31
+ INHIBITORS_FILE = path.join(TEST_DIR, '_inhibitors.ndjson');
32
+
33
+ if (!fs.existsSync(TEST_DIR)) {
34
+ fs.mkdirSync(TEST_DIR, { recursive: true });
35
+ }
36
+
37
+ // Override file path for testing
38
+ process.env.TEST_META_DIR = TEST_DIR;
39
+ });
40
+
41
+ afterEach(() => {
42
+ // Cleanup test files
43
+ if (fs.existsSync(TEST_DIR)) {
44
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
45
+ }
46
+ // Clean up environment
47
+ delete process.env.TEST_META_DIR;
48
+ });
49
+
50
+ describe('emitInhibitor', () => {
51
+ it('should emit inhibitor with correct structure', async () => {
52
+ const inhibitor = await emitInhibitor({
53
+ trigger: {
54
+ file: 'src/test.ts',
55
+ mode: 'complexity_reducer',
56
+ pattern: 'removing null checks',
57
+ },
58
+ signal: 'CI failed: TypeError: Cannot read property x of undefined',
59
+ origin: {
60
+ agent_id: 'agent-123',
61
+ trace_id: 'trace-456',
62
+ energy_wasted: 0.04,
63
+ failure_type: 'ci_failed',
64
+ },
65
+ strength: 1.0,
66
+ half_life_days: 30,
67
+ });
68
+
69
+ expect(inhibitor.id).toBeDefined();
70
+ expect(inhibitor.timestamp).toBeDefined();
71
+ expect(inhibitor.trigger.file).toBe('src/test.ts');
72
+ expect(inhibitor.trigger.mode).toBe('complexity_reducer');
73
+ expect(inhibitor.trigger.pattern).toBe('removing null checks');
74
+ expect(inhibitor.strength).toBe(1.0);
75
+ expect(inhibitor.half_life_days).toBe(30);
76
+ });
77
+
78
+ it('should append to NDJSON file', async () => {
79
+ await emitInhibitor({
80
+ trigger: { file: 'test1.ts', mode: 'error_reducer' },
81
+ signal: 'Test failure 1',
82
+ origin: {
83
+ agent_id: 'a1',
84
+ trace_id: 't1',
85
+ energy_wasted: 0.01,
86
+ failure_type: 'ci_failed',
87
+ },
88
+ strength: 1.0,
89
+ half_life_days: 30,
90
+ });
91
+
92
+ await emitInhibitor({
93
+ trigger: { file: 'test2.ts', mode: 'debt_payer' },
94
+ signal: 'Test failure 2',
95
+ origin: {
96
+ agent_id: 'a2',
97
+ trace_id: 't2',
98
+ energy_wasted: 0.02,
99
+ failure_type: 'regression',
100
+ },
101
+ strength: 0.8,
102
+ half_life_days: 30,
103
+ });
104
+
105
+ const all = await readInhibitors();
106
+ expect(all.length).toBe(2);
107
+ expect(all[0].trigger.file).toBe('test1.ts');
108
+ expect(all[1].trigger.file).toBe('test2.ts');
109
+ });
110
+ });
111
+
112
+ describe('calculateCurrentStrength', () => {
113
+ it('should calculate correct decay at 0 days', () => {
114
+ const inhibitor: Inhibitor = {
115
+ id: '1',
116
+ timestamp: new Date().toISOString(),
117
+ trigger: {},
118
+ signal: 'test',
119
+ origin: {
120
+ agent_id: 'a1',
121
+ trace_id: 't1',
122
+ energy_wasted: 0.01,
123
+ failure_type: 'ci_failed',
124
+ },
125
+ strength: 1.0,
126
+ half_life_days: 30,
127
+ };
128
+
129
+ const strength = calculateCurrentStrength(inhibitor);
130
+ expect(strength).toBeCloseTo(1.0, 2);
131
+ });
132
+
133
+ it('should decay to 0.5 at one half-life (30 days)', () => {
134
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
135
+ const inhibitor: Inhibitor = {
136
+ id: '1',
137
+ timestamp: thirtyDaysAgo.toISOString(),
138
+ trigger: {},
139
+ signal: 'test',
140
+ origin: {
141
+ agent_id: 'a1',
142
+ trace_id: 't1',
143
+ energy_wasted: 0.01,
144
+ failure_type: 'ci_failed',
145
+ },
146
+ strength: 1.0,
147
+ half_life_days: 30,
148
+ };
149
+
150
+ const strength = calculateCurrentStrength(inhibitor);
151
+ expect(strength).toBeCloseTo(0.5, 2);
152
+ });
153
+
154
+ it('should decay to 0.25 at two half-lives (60 days)', () => {
155
+ const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
156
+ const inhibitor: Inhibitor = {
157
+ id: '1',
158
+ timestamp: sixtyDaysAgo.toISOString(),
159
+ trigger: {},
160
+ signal: 'test',
161
+ origin: {
162
+ agent_id: 'a1',
163
+ trace_id: 't1',
164
+ energy_wasted: 0.01,
165
+ failure_type: 'ci_failed',
166
+ },
167
+ strength: 1.0,
168
+ half_life_days: 30,
169
+ };
170
+
171
+ const strength = calculateCurrentStrength(inhibitor);
172
+ expect(strength).toBeCloseTo(0.25, 2);
173
+ });
174
+
175
+ it('should decay to 0.125 at three half-lives (90 days)', () => {
176
+ const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
177
+ const inhibitor: Inhibitor = {
178
+ id: '1',
179
+ timestamp: ninetyDaysAgo.toISOString(),
180
+ trigger: {},
181
+ signal: 'test',
182
+ origin: {
183
+ agent_id: 'a1',
184
+ trace_id: 't1',
185
+ energy_wasted: 0.01,
186
+ failure_type: 'ci_failed',
187
+ },
188
+ strength: 1.0,
189
+ half_life_days: 30,
190
+ };
191
+
192
+ const strength = calculateCurrentStrength(inhibitor);
193
+ expect(strength).toBeCloseTo(0.125, 2);
194
+ });
195
+
196
+ it('should respect custom half-life', () => {
197
+ const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000);
198
+ const inhibitor: Inhibitor = {
199
+ id: '1',
200
+ timestamp: fifteenDaysAgo.toISOString(),
201
+ trigger: {},
202
+ signal: 'test',
203
+ origin: {
204
+ agent_id: 'a1',
205
+ trace_id: 't1',
206
+ energy_wasted: 0.01,
207
+ failure_type: 'ci_failed',
208
+ },
209
+ strength: 1.0,
210
+ half_life_days: 15, // 15-day half-life instead of 30
211
+ };
212
+
213
+ const strength = calculateCurrentStrength(inhibitor);
214
+ expect(strength).toBeCloseTo(0.5, 2); // One half-life at 15 days
215
+ });
216
+ });
217
+
218
+ describe('queryInhibitors', () => {
219
+ it('should filter by relevance threshold (>0.2)', async () => {
220
+ // Fresh inhibitor (strength 1.0)
221
+ await emitInhibitor({
222
+ trigger: { file: 'test.ts', mode: 'error_reducer' },
223
+ signal: 'Fresh failure',
224
+ origin: {
225
+ agent_id: 'a1',
226
+ trace_id: 't1',
227
+ energy_wasted: 0.01,
228
+ failure_type: 'ci_failed',
229
+ },
230
+ strength: 1.0,
231
+ half_life_days: 30,
232
+ });
233
+
234
+ // Old inhibitor (strength ~0.125 at 90 days)
235
+ const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
236
+ const oldInhibitor: Inhibitor = {
237
+ id: '2',
238
+ timestamp: ninetyDaysAgo.toISOString(),
239
+ trigger: { file: 'test.ts', mode: 'error_reducer' },
240
+ signal: 'Old failure',
241
+ origin: {
242
+ agent_id: 'a2',
243
+ trace_id: 't2',
244
+ energy_wasted: 0.01,
245
+ failure_type: 'regression',
246
+ },
247
+ strength: 1.0,
248
+ half_life_days: 30,
249
+ };
250
+
251
+ // Manually write old inhibitor
252
+ const inhibitorsFile = path.join(TEST_DIR, '_inhibitors.ndjson');
253
+ fs.appendFileSync(inhibitorsFile, JSON.stringify(oldInhibitor) + '\n');
254
+
255
+ const relevant = await queryInhibitors('test.ts', 'error_reducer');
256
+
257
+ // Only fresh one should be included (old one is 0.125 < 0.2)
258
+ expect(relevant.length).toBe(1);
259
+ expect(relevant[0].signal).toBe('Fresh failure');
260
+ });
261
+
262
+ it('should match by file', async () => {
263
+ await emitInhibitor({
264
+ trigger: { file: 'match.ts', mode: 'error_reducer' },
265
+ signal: 'Match',
266
+ origin: {
267
+ agent_id: 'a1',
268
+ trace_id: 't1',
269
+ energy_wasted: 0.01,
270
+ failure_type: 'ci_failed',
271
+ },
272
+ strength: 1.0,
273
+ half_life_days: 30,
274
+ });
275
+
276
+ await emitInhibitor({
277
+ trigger: { file: 'nomatch.ts', mode: 'error_reducer' },
278
+ signal: 'No match',
279
+ origin: {
280
+ agent_id: 'a2',
281
+ trace_id: 't2',
282
+ energy_wasted: 0.01,
283
+ failure_type: 'ci_failed',
284
+ },
285
+ strength: 1.0,
286
+ half_life_days: 30,
287
+ });
288
+
289
+ const relevant = await queryInhibitors('match.ts', 'error_reducer');
290
+ expect(relevant.length).toBe(1);
291
+ expect(relevant[0].signal).toBe('Match');
292
+ });
293
+
294
+ it('should match by mode', async () => {
295
+ await emitInhibitor({
296
+ trigger: { file: 'test.ts', mode: 'error_reducer' },
297
+ signal: 'Error reducer',
298
+ origin: {
299
+ agent_id: 'a1',
300
+ trace_id: 't1',
301
+ energy_wasted: 0.01,
302
+ failure_type: 'ci_failed',
303
+ },
304
+ strength: 1.0,
305
+ half_life_days: 30,
306
+ });
307
+
308
+ await emitInhibitor({
309
+ trigger: { file: 'test.ts', mode: 'complexity_reducer' },
310
+ signal: 'Complexity reducer',
311
+ origin: {
312
+ agent_id: 'a2',
313
+ trace_id: 't2',
314
+ energy_wasted: 0.01,
315
+ failure_type: 'ci_failed',
316
+ },
317
+ strength: 1.0,
318
+ half_life_days: 30,
319
+ });
320
+
321
+ const relevant = await queryInhibitors('test.ts', 'error_reducer');
322
+ expect(relevant.length).toBe(1);
323
+ expect(relevant[0].signal).toBe('Error reducer');
324
+ });
325
+
326
+ it('should sort by strength (strongest first)', async () => {
327
+ await emitInhibitor({
328
+ trigger: { file: 'test.ts' },
329
+ signal: 'Weak',
330
+ origin: {
331
+ agent_id: 'a1',
332
+ trace_id: 't1',
333
+ energy_wasted: 0.01,
334
+ failure_type: 'ci_failed',
335
+ },
336
+ strength: 0.3,
337
+ half_life_days: 30,
338
+ });
339
+
340
+ await emitInhibitor({
341
+ trigger: { file: 'test.ts' },
342
+ signal: 'Strong',
343
+ origin: {
344
+ agent_id: 'a2',
345
+ trace_id: 't2',
346
+ energy_wasted: 0.01,
347
+ failure_type: 'ci_failed',
348
+ },
349
+ strength: 0.9,
350
+ half_life_days: 30,
351
+ });
352
+
353
+ const relevant = await queryInhibitors('test.ts', 'error_reducer');
354
+ expect(relevant[0].signal).toBe('Strong');
355
+ expect(relevant[1].signal).toBe('Weak');
356
+ });
357
+ });
358
+
359
+ describe('extractFailurePattern', () => {
360
+ it('should extract TypeError pattern', () => {
361
+ const output = 'TypeError: Cannot read property x of undefined';
362
+ expect(extractFailurePattern(output)).toBe('changes causing TypeError');
363
+ });
364
+
365
+ it('should extract module import pattern', () => {
366
+ const output = "Error: Cannot find module './missing'";
367
+ expect(extractFailurePattern(output)).toBe('breaking imports');
368
+ });
369
+
370
+ it('should extract type assignment pattern', () => {
371
+ const output = 'error TS2322: Type string is not assignable to type number';
372
+ expect(extractFailurePattern(output)).toBe('type mismatches');
373
+ });
374
+
375
+ it('should return undefined for unknown patterns', () => {
376
+ const output = 'Some unknown error';
377
+ expect(extractFailurePattern(output)).toBeUndefined();
378
+ });
379
+ });
380
+
381
+ describe('gcInhibitors', () => {
382
+ it('should remove decayed inhibitors (strength < 0.05)', async () => {
383
+ // Fresh inhibitor (keep)
384
+ await emitInhibitor({
385
+ trigger: { file: 'keep.ts' },
386
+ signal: 'Keep me',
387
+ origin: {
388
+ agent_id: 'a1',
389
+ trace_id: 't1',
390
+ energy_wasted: 0.01,
391
+ failure_type: 'ci_failed',
392
+ },
393
+ strength: 1.0,
394
+ half_life_days: 30,
395
+ });
396
+
397
+ // Very old inhibitor (remove - strength ~0.03125 at 150 days, 5 half-lives)
398
+ // At 150 days with 30-day half-life: 0.5^5 = 0.03125 < 0.05
399
+ const veryOld = new Date(Date.now() - 150 * 24 * 60 * 60 * 1000);
400
+ const oldInhibitor: Inhibitor = {
401
+ id: '2',
402
+ timestamp: veryOld.toISOString(),
403
+ trigger: { file: 'remove.ts' },
404
+ signal: 'Remove me',
405
+ origin: {
406
+ agent_id: 'a2',
407
+ trace_id: 't2',
408
+ energy_wasted: 0.01,
409
+ failure_type: 'regression',
410
+ },
411
+ strength: 1.0,
412
+ half_life_days: 30,
413
+ };
414
+
415
+ const inhibitorsFile = path.join(TEST_DIR, '_inhibitors.ndjson');
416
+ fs.appendFileSync(inhibitorsFile, JSON.stringify(oldInhibitor) + '\n');
417
+
418
+ const result = await gcInhibitors();
419
+
420
+ expect(result.removed).toBeGreaterThan(0);
421
+ expect(result.kept).toBeGreaterThan(0);
422
+
423
+ const remaining = await readInhibitors();
424
+ expect(remaining.every(i => i.signal !== 'Remove me')).toBe(true);
425
+ });
426
+ });
427
+
428
+ describe('formatInhibitorForPrompt', () => {
429
+ it('should format inhibitor for display', () => {
430
+ const inhibitor = {
431
+ id: '1',
432
+ timestamp: new Date().toISOString(),
433
+ trigger: {
434
+ file: 'test.ts',
435
+ mode: 'complexity_reducer' as Mode,
436
+ pattern: 'removing null checks',
437
+ },
438
+ signal: 'CI failed: TypeError',
439
+ origin: {
440
+ agent_id: 'agent-123',
441
+ trace_id: 'trace-456',
442
+ energy_wasted: 0.04,
443
+ failure_type: 'ci_failed' as const,
444
+ },
445
+ strength: 1.0,
446
+ half_life_days: 30,
447
+ currentStrength: 0.85,
448
+ };
449
+
450
+ const formatted = formatInhibitorForPrompt(inhibitor);
451
+
452
+ expect(formatted).toContain('85%');
453
+ expect(formatted).toContain('removing null checks');
454
+ expect(formatted).toContain('CI failed: TypeError');
455
+ expect(formatted).toContain('agent-123');
456
+ expect(formatted).toContain('$0.0400');
457
+ });
458
+ });
459
+ });
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Integration tests for Phase 3: Concurrency & Coordination
3
+ * Tests the full flow of multi-agent coordination
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import * as fs from 'fs/promises';
8
+ import {
9
+ acquireLock,
10
+ releaseLock,
11
+ listLocks,
12
+ } from '../../src/coordination/file-locks.js';
13
+ import {
14
+ spawnAgent,
15
+ killAllAgents,
16
+ listAgents,
17
+ } from '../../src/coordination/process-manager.js';
18
+
19
+ const LOCK_DIR = '.agent-meta/locks';
20
+
21
+ describe('Phase 3 Integration: Multi-Agent Coordination', () => {
22
+ beforeEach(async () => {
23
+ // Clean up before tests
24
+ killAllAgents();
25
+ await new Promise((resolve) => setTimeout(resolve, 50));
26
+ try {
27
+ await fs.rm(LOCK_DIR, { recursive: true, force: true });
28
+ } catch {
29
+ // Ignore
30
+ }
31
+ await fs.mkdir(LOCK_DIR, { recursive: true });
32
+ });
33
+
34
+ afterEach(async () => {
35
+ killAllAgents();
36
+ await new Promise((resolve) => setTimeout(resolve, 100));
37
+ try {
38
+ await fs.rm(LOCK_DIR, { recursive: true, force: true });
39
+ } catch {
40
+ // Ignore
41
+ }
42
+ });
43
+
44
+ it('should coordinate multiple agents working on different files', async () => {
45
+ const files = ['file1.ts', 'file2.ts', 'file3.ts'];
46
+
47
+ // Simulate three agents each acquiring a lock on different files
48
+ const locks = await Promise.all(
49
+ files.map((file, i) =>
50
+ acquireLock(file, `agent-${i + 1}`, 'error_reducer')
51
+ )
52
+ );
53
+
54
+ // All should succeed (different files)
55
+ expect(locks.every((l) => l === true)).toBe(true);
56
+
57
+ const activeLocks = await listLocks();
58
+ expect(activeLocks).toHaveLength(3);
59
+
60
+ // Clean up
61
+ await Promise.all(files.map((file) => releaseLock(file)));
62
+ });
63
+
64
+ it('should prevent multiple agents from working on the same file', async () => {
65
+ const file = 'contested.ts';
66
+
67
+ // Three agents try to lock the same file
68
+ const results = await Promise.all([
69
+ acquireLock(file, 'agent-1', 'error_reducer'),
70
+ acquireLock(file, 'agent-2', 'complexity_reducer'),
71
+ acquireLock(file, 'agent-3', 'debt_payer'),
72
+ ]);
73
+
74
+ // Only one should succeed
75
+ const successes = results.filter(Boolean);
76
+ expect(successes).toHaveLength(1);
77
+
78
+ const activeLocks = await listLocks();
79
+ expect(activeLocks).toHaveLength(1);
80
+
81
+ await releaseLock(file);
82
+ });
83
+
84
+ it('should spawn multiple agents and track them', async () => {
85
+ // Spawn multiple agents
86
+ spawnAgent({ agentId: 'worker-1', maxIterations: 5, timeout: 5000 });
87
+ spawnAgent({ agentId: 'worker-2', maxIterations: 5, timeout: 5000 });
88
+ spawnAgent({ agentId: 'worker-3', maxIterations: 5, timeout: 5000 });
89
+
90
+ // Wait a moment for them to start
91
+ await new Promise((resolve) => setTimeout(resolve, 50));
92
+
93
+ const agents = listAgents();
94
+ expect(agents).toHaveLength(3);
95
+
96
+ const agentIds = agents.map((a) => a.agentId).sort();
97
+ expect(agentIds).toEqual(['worker-1', 'worker-2', 'worker-3']);
98
+
99
+ // All should have PIDs
100
+ expect(agents.every((a) => a.pid > 0)).toBe(true);
101
+ });
102
+
103
+ it('should handle agent lock lifecycle', async () => {
104
+ const file = 'work-file.ts';
105
+
106
+ // Agent acquires lock
107
+ const locked = await acquireLock(file, 'agent-1', 'error_reducer');
108
+ expect(locked).toBe(true);
109
+
110
+ // Verify lock exists
111
+ let locks = await listLocks();
112
+ expect(locks).toHaveLength(1);
113
+ expect(locks[0].agent_id).toBe('agent-1');
114
+
115
+ // Agent releases lock
116
+ const released = await releaseLock(file);
117
+ expect(released).toBe(true);
118
+
119
+ // Verify lock is gone
120
+ locks = await listLocks();
121
+ expect(locks).toHaveLength(0);
122
+
123
+ // Another agent can now acquire
124
+ const locked2 = await acquireLock(file, 'agent-2', 'complexity_reducer');
125
+ expect(locked2).toBe(true);
126
+
127
+ await releaseLock(file);
128
+ });
129
+
130
+ it('should demonstrate full coordination scenario', async () => {
131
+ /**
132
+ * Scenario: 4 agents, 3 files
133
+ * - Agent 1 gets file1
134
+ * - Agent 2 gets file2
135
+ * - Agent 3 gets file3
136
+ * - Agent 4 fails to get any (all locked)
137
+ * - Agent 1 releases file1
138
+ * - Agent 4 gets file1
139
+ */
140
+
141
+ const files = ['file1.ts', 'file2.ts', 'file3.ts'];
142
+
143
+ // Agents 1-3 acquire locks
144
+ const [lock1, lock2, lock3] = await Promise.all([
145
+ acquireLock(files[0], 'agent-1', 'error_reducer'),
146
+ acquireLock(files[1], 'agent-2', 'complexity_reducer'),
147
+ acquireLock(files[2], 'agent-3', 'debt_payer'),
148
+ ]);
149
+
150
+ expect([lock1, lock2, lock3]).toEqual([true, true, true]);
151
+
152
+ // Agent 4 tries all files, all should fail
153
+ const [lock4a, lock4b, lock4c] = await Promise.all([
154
+ acquireLock(files[0], 'agent-4', 'error_reducer'),
155
+ acquireLock(files[1], 'agent-4', 'error_reducer'),
156
+ acquireLock(files[2], 'agent-4', 'error_reducer'),
157
+ ]);
158
+
159
+ expect([lock4a, lock4b, lock4c]).toEqual([false, false, false]);
160
+
161
+ // Agent 1 releases file1
162
+ await releaseLock(files[0]);
163
+
164
+ // Agent 4 can now acquire file1
165
+ const lock4 = await acquireLock(files[0], 'agent-4', 'stabilizer');
166
+ expect(lock4).toBe(true);
167
+
168
+ // Verify final state
169
+ const locks = await listLocks();
170
+ expect(locks).toHaveLength(3);
171
+
172
+ const agentIds = locks.map((l) => l.agent_id).sort();
173
+ expect(agentIds).toEqual(['agent-2', 'agent-3', 'agent-4']);
174
+
175
+ // Clean up
176
+ await Promise.all(files.map((f) => releaseLock(f)));
177
+ });
178
+
179
+ it('should handle rapid lock acquire/release cycles', async () => {
180
+ const file = 'cycle-test.ts';
181
+ const cycles = 10;
182
+
183
+ for (let i = 0; i < cycles; i++) {
184
+ const locked = await acquireLock(file, `agent-${i}`, 'error_reducer');
185
+ expect(locked).toBe(true);
186
+
187
+ const released = await releaseLock(file);
188
+ expect(released).toBe(true);
189
+ }
190
+
191
+ // Should have no locks at the end
192
+ const locks = await listLocks();
193
+ expect(locks).toHaveLength(0);
194
+ });
195
+ });