claude-mycelium 2.0.0 → 2.1.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/.agent-meta/_inhibitors.ndjson +1287 -0
  2. package/.agent-meta/_quarantine.json +45 -0
  3. package/.agent-meta/config.json +9 -0
  4. package/.claude/memory.db +0 -0
  5. package/.claude/settings.local.json +4 -1
  6. package/README.md +81 -235
  7. package/SECURITY.md +145 -0
  8. package/dist/agent/worker.d.ts +8 -0
  9. package/dist/agent/worker.d.ts.map +1 -0
  10. package/dist/agent/worker.js +97 -0
  11. package/dist/agent/worker.js.map +1 -0
  12. package/dist/bin.d.ts +7 -0
  13. package/dist/bin.d.ts.map +1 -0
  14. package/dist/bin.js +11 -0
  15. package/dist/bin.js.map +1 -0
  16. package/dist/cli/cost.d.ts +10 -0
  17. package/dist/cli/cost.d.ts.map +1 -0
  18. package/dist/cli/cost.js +163 -0
  19. package/dist/cli/cost.js.map +1 -0
  20. package/dist/cli/gc.d.ts +10 -0
  21. package/dist/cli/gc.d.ts.map +1 -0
  22. package/dist/cli/gc.js +108 -0
  23. package/dist/cli/gc.js.map +1 -0
  24. package/dist/cli/gradients.d.ts +10 -0
  25. package/dist/cli/gradients.d.ts.map +1 -0
  26. package/dist/cli/gradients.js +69 -0
  27. package/dist/cli/gradients.js.map +1 -0
  28. package/dist/cli/index.d.ts +17 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +72 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/init.d.ts +11 -0
  33. package/dist/cli/init.d.ts.map +1 -0
  34. package/dist/cli/init.js +97 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/status.d.ts +10 -0
  37. package/dist/cli/status.d.ts.map +1 -0
  38. package/dist/cli/status.js +191 -0
  39. package/dist/cli/status.js.map +1 -0
  40. package/dist/coordination/file-locks.d.ts +42 -0
  41. package/dist/coordination/file-locks.d.ts.map +1 -0
  42. package/dist/coordination/file-locks.js +269 -0
  43. package/dist/coordination/file-locks.js.map +1 -0
  44. package/dist/coordination/index.d.ts +4 -0
  45. package/dist/coordination/index.d.ts.map +1 -1
  46. package/dist/coordination/index.js +4 -0
  47. package/dist/coordination/index.js.map +1 -1
  48. package/dist/coordination/inhibitors.d.ts +84 -0
  49. package/dist/coordination/inhibitors.d.ts.map +1 -0
  50. package/dist/coordination/inhibitors.js +290 -0
  51. package/dist/coordination/inhibitors.js.map +1 -0
  52. package/dist/coordination/process-manager.d.ts +73 -0
  53. package/dist/coordination/process-manager.d.ts.map +1 -0
  54. package/dist/coordination/process-manager.js +144 -0
  55. package/dist/coordination/process-manager.js.map +1 -0
  56. package/dist/core/agent-executor.d.ts.map +1 -1
  57. package/dist/core/agent-executor.js +28 -10
  58. package/dist/core/agent-executor.js.map +1 -1
  59. package/dist/core/change-applier.d.ts +29 -5
  60. package/dist/core/change-applier.d.ts.map +1 -1
  61. package/dist/core/change-applier.js +254 -24
  62. package/dist/core/change-applier.js.map +1 -1
  63. package/dist/core/signals/churn.d.ts.map +1 -1
  64. package/dist/core/signals/churn.js +6 -4
  65. package/dist/core/signals/churn.js.map +1 -1
  66. package/dist/core/signals/debt.d.ts.map +1 -1
  67. package/dist/core/signals/debt.js +4 -3
  68. package/dist/core/signals/debt.js.map +1 -1
  69. package/dist/cost/cost-tracker.d.ts.map +1 -1
  70. package/dist/cost/cost-tracker.js +2 -0
  71. package/dist/cost/cost-tracker.js.map +1 -1
  72. package/dist/gc/index.d.ts +17 -0
  73. package/dist/gc/index.d.ts.map +1 -0
  74. package/dist/gc/index.js +17 -0
  75. package/dist/gc/index.js.map +1 -0
  76. package/dist/gc/runner.d.ts +39 -0
  77. package/dist/gc/runner.d.ts.map +1 -0
  78. package/dist/gc/runner.js +277 -0
  79. package/dist/gc/runner.js.map +1 -0
  80. package/dist/gc/trace-compactor.d.ts +31 -0
  81. package/dist/gc/trace-compactor.d.ts.map +1 -0
  82. package/dist/gc/trace-compactor.js +162 -0
  83. package/dist/gc/trace-compactor.js.map +1 -0
  84. package/dist/index.d.ts +5 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +6 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/prompts/index.d.ts +2 -1
  89. package/dist/prompts/index.d.ts.map +1 -1
  90. package/dist/prompts/index.js.map +1 -1
  91. package/dist/quarantine/explorer.d.ts +65 -0
  92. package/dist/quarantine/explorer.d.ts.map +1 -0
  93. package/dist/quarantine/explorer.js +175 -0
  94. package/dist/quarantine/explorer.js.map +1 -0
  95. package/dist/quarantine/index.d.ts +7 -0
  96. package/dist/quarantine/index.d.ts.map +1 -0
  97. package/dist/quarantine/index.js +7 -0
  98. package/dist/quarantine/index.js.map +1 -0
  99. package/dist/quarantine/manager.d.ts +75 -0
  100. package/dist/quarantine/manager.d.ts.map +1 -0
  101. package/dist/quarantine/manager.js +275 -0
  102. package/dist/quarantine/manager.js.map +1 -0
  103. package/dist/task/acceptance.d.ts +29 -0
  104. package/dist/task/acceptance.d.ts.map +1 -0
  105. package/dist/task/acceptance.js +228 -0
  106. package/dist/task/acceptance.js.map +1 -0
  107. package/dist/task/executor.d.ts +30 -0
  108. package/dist/task/executor.d.ts.map +1 -0
  109. package/dist/task/executor.js +429 -0
  110. package/dist/task/executor.js.map +1 -0
  111. package/dist/task/index.d.ts +12 -0
  112. package/dist/task/index.d.ts.map +1 -0
  113. package/dist/task/index.js +12 -0
  114. package/dist/task/index.js.map +1 -0
  115. package/dist/task/planner.d.ts +21 -0
  116. package/dist/task/planner.d.ts.map +1 -0
  117. package/dist/task/planner.js +253 -0
  118. package/dist/task/planner.js.map +1 -0
  119. package/dist/task/storage.d.ts +46 -0
  120. package/dist/task/storage.d.ts.map +1 -0
  121. package/dist/task/storage.js +266 -0
  122. package/dist/task/storage.js.map +1 -0
  123. package/dist/trace/trace-event.d.ts +2 -18
  124. package/dist/trace/trace-event.d.ts.map +1 -1
  125. package/dist/trace/trace-event.js +6 -6
  126. package/dist/trace/trace-event.js.map +1 -1
  127. package/dist/utils/file-utils.d.ts.map +1 -1
  128. package/dist/utils/file-utils.js +54 -15
  129. package/dist/utils/file-utils.js.map +1 -1
  130. package/docs/PHASE5_IMPLEMENTATION.md +237 -0
  131. package/docs/PHASES-3-7-COMPLETE.md +177 -0
  132. package/docs/PHASE_4_COMPLETE.md +135 -0
  133. package/docs/PHASE_7_DELIVERABLES.md +295 -0
  134. package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
  135. package/docs/PHASE_7_SUMMARY.txt +195 -0
  136. package/docs/RELEASE-NOTES-v2.1.md +213 -0
  137. package/docs/ROADMAP.md +64 -57
  138. package/docs/SECURITY-AUDIT.md +387 -0
  139. package/docs/SNAPSHOT.md +59 -32
  140. package/docs/implementation/phase3-summary.md +220 -0
  141. package/package.json +19 -11
  142. package/src/agent/worker.ts +111 -0
  143. package/src/bin.ts +13 -0
  144. package/src/cli/cost.ts +210 -0
  145. package/src/cli/gc.ts +138 -0
  146. package/src/cli/gradients.ts +95 -0
  147. package/src/cli/index.ts +79 -0
  148. package/src/cli/init.ts +139 -0
  149. package/src/cli/status.ts +218 -0
  150. package/src/coordination/file-locks.ts +300 -0
  151. package/src/coordination/index.ts +4 -0
  152. package/src/coordination/inhibitors.ts +345 -0
  153. package/src/coordination/process-manager.ts +199 -0
  154. package/src/core/agent-executor.ts +20 -4
  155. package/src/core/signals/churn.ts +8 -5
  156. package/src/core/signals/debt.ts +4 -3
  157. package/src/cost/cost-tracker.ts +2 -0
  158. package/src/gc/index.ts +17 -0
  159. package/src/gc/runner.ts +314 -0
  160. package/src/gc/trace-compactor.ts +187 -0
  161. package/src/index.ts +7 -1
  162. package/src/prompts/index.ts +2 -1
  163. package/src/quarantine/explorer.ts +234 -0
  164. package/src/quarantine/index.ts +7 -0
  165. package/src/quarantine/manager.ts +336 -0
  166. package/src/task/acceptance.ts +267 -0
  167. package/src/task/executor.ts +538 -0
  168. package/src/task/index.ts +38 -0
  169. package/src/task/planner.ts +294 -0
  170. package/src/task/storage.ts +332 -0
  171. package/src/trace/trace-event.ts +7 -26
  172. package/src/utils/file-utils.ts +61 -15
  173. package/tests/cli/gc.test.ts +206 -0
  174. package/tests/cli/init.test.ts +181 -0
  175. package/tests/cli/status.test.ts +282 -0
  176. package/tests/coordination/file-locks.test.ts +196 -0
  177. package/tests/coordination/inhibitors.test.ts +459 -0
  178. package/tests/coordination/integration.test.ts +195 -0
  179. package/tests/coordination/process-manager.test.ts +165 -0
  180. package/tests/gc/trace-compactor.test.ts +245 -0
  181. package/tests/integration/phase-7.test.ts +145 -0
  182. package/tests/quarantine/explorer.test.ts +381 -0
  183. package/tests/quarantine/manager.test.ts +399 -0
  184. package/tests/security/command-injection.test.ts +88 -0
  185. package/tests/security/path-traversal.test.ts +103 -0
  186. package/tests/task/acceptance.test.ts +411 -0
  187. package/tests/task/executor.test.ts +421 -0
  188. package/tests/task/planner.test.ts +359 -0
  189. package/tsconfig.json +2 -2
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Inhibitor Signal System for Claude-Mycelium
3
+ * Records pattern-specific warnings from failures with exponential decay
4
+ * Storage: .agent-meta/_inhibitors.ndjson (append-only)
5
+ * Reference: ADR-002, second-spec §1
6
+ */
7
+
8
+ import { v4 as uuidv4 } from 'uuid';
9
+ import * as path from 'path';
10
+ import { appendFile, readFile, fileExists, writeFile } from '../utils/file-utils.js';
11
+ import { logInfo, logError } from '../utils/logger.js';
12
+ import type { Inhibitor, Mode, TraceEvent } from '../types/index.js';
13
+
14
+ // Compute file path at runtime to respect TEST_META_DIR
15
+ function getInhibitorsFile(): string {
16
+ const META_DIR = process.env.TEST_META_DIR || '.agent-meta';
17
+ return path.join(META_DIR, '_inhibitors.ndjson');
18
+ }
19
+
20
+ const INHIBITOR_RELEVANCE_THRESHOLD = 0.2;
21
+ const INHIBITOR_GC_THRESHOLD = 0.05;
22
+ const DEFAULT_HALF_LIFE_DAYS = 30;
23
+
24
+ /**
25
+ * Emit an inhibitor signal after a failure
26
+ * Appends to NDJSON file for durability
27
+ */
28
+ export async function emitInhibitor(params: {
29
+ trigger: {
30
+ file?: string;
31
+ pattern?: string;
32
+ mode?: Mode;
33
+ };
34
+ signal: string;
35
+ origin: {
36
+ agent_id: string;
37
+ trace_id: string;
38
+ energy_wasted: number;
39
+ failure_type: 'ci_failed' | 'regression' | 'reverted' | 'loop_detected';
40
+ };
41
+ strength: number;
42
+ half_life_days: number;
43
+ }): Promise<Inhibitor> {
44
+ const inhibitor: Inhibitor = {
45
+ id: uuidv4(),
46
+ timestamp: new Date().toISOString(),
47
+ trigger: params.trigger,
48
+ signal: params.signal,
49
+ origin: params.origin,
50
+ strength: params.strength,
51
+ half_life_days: params.half_life_days,
52
+ };
53
+
54
+ try {
55
+ // Append to NDJSON file
56
+ const line = JSON.stringify(inhibitor) + '\n';
57
+ appendFile(getInhibitorsFile(), line);
58
+
59
+ logInfo('Inhibitor emitted', {
60
+ id: inhibitor.id,
61
+ file: inhibitor.trigger.file,
62
+ mode: inhibitor.trigger.mode,
63
+ pattern: inhibitor.trigger.pattern,
64
+ failure_type: inhibitor.origin.failure_type,
65
+ energy_wasted: `$${inhibitor.origin.energy_wasted.toFixed(4)}`,
66
+ });
67
+
68
+ return inhibitor;
69
+ } catch (error) {
70
+ logError('Failed to emit inhibitor', error as Error, { inhibitor });
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Query relevant inhibitors for a file + mode combination
77
+ * Returns inhibitors sorted by current strength (strongest first)
78
+ * Filters out inhibitors below relevance threshold
79
+ */
80
+ export async function queryInhibitors(
81
+ file: string,
82
+ mode: Mode
83
+ ): Promise<Array<Inhibitor & { currentStrength: number }>> {
84
+ try {
85
+ const all = await readInhibitors();
86
+
87
+ return all
88
+ .map(i => ({ ...i, currentStrength: calculateCurrentStrength(i) }))
89
+ .filter(i => {
90
+ // Must be strong enough to be relevant
91
+ if (i.currentStrength < INHIBITOR_RELEVANCE_THRESHOLD) return false;
92
+
93
+ // Check trigger match
94
+ const matchesFile = !i.trigger.file || i.trigger.file === file;
95
+ const matchesMode = !i.trigger.mode || i.trigger.mode === mode;
96
+
97
+ return matchesFile && matchesMode;
98
+ })
99
+ .sort((a, b) => b.currentStrength - a.currentStrength);
100
+ } catch (error) {
101
+ logError('Failed to query inhibitors', error as Error, { file, mode });
102
+ return [];
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Calculate current strength with exponential decay
108
+ * Formula: strength * 0.5^(days / half_life)
109
+ *
110
+ * Examples (30-day half-life):
111
+ * - Day 0: 1.0
112
+ * - Day 30: 0.5
113
+ * - Day 60: 0.25
114
+ * - Day 90: 0.125
115
+ */
116
+ export function calculateCurrentStrength(inhibitor: Inhibitor): number {
117
+ const ageMs = Date.now() - Date.parse(inhibitor.timestamp);
118
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
119
+ const halfLives = ageDays / inhibitor.half_life_days;
120
+
121
+ return inhibitor.strength * Math.pow(0.5, halfLives);
122
+ }
123
+
124
+ /**
125
+ * Read all inhibitors from NDJSON file
126
+ */
127
+ export async function readInhibitors(): Promise<Inhibitor[]> {
128
+ try {
129
+ if (!fileExists(getInhibitorsFile())) {
130
+ return [];
131
+ }
132
+
133
+ const content = readFile(getInhibitorsFile());
134
+ const lines = content.trim().split('\n').filter(line => line.length > 0);
135
+
136
+ const inhibitors: Inhibitor[] = [];
137
+ for (const line of lines) {
138
+ try {
139
+ const inhibitor = JSON.parse(line) as Inhibitor;
140
+ inhibitors.push(inhibitor);
141
+ } catch (error) {
142
+ logError('Failed to parse inhibitor line', error as Error, {
143
+ line: line.substring(0, 100),
144
+ });
145
+ // Skip corrupted lines
146
+ }
147
+ }
148
+
149
+ return inhibitors;
150
+ } catch (error) {
151
+ logError('Failed to read inhibitors', error as Error);
152
+ return [];
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Extract failure pattern from CI output
158
+ * Heuristic pattern matching for common failure types
159
+ */
160
+ export function extractFailurePattern(ciOutput: string): string | undefined {
161
+ if (ciOutput.includes('TypeError')) return 'changes causing TypeError';
162
+ if (ciOutput.includes('Cannot find module')) return 'breaking imports';
163
+ if (ciOutput.includes('is not assignable')) return 'type mismatches';
164
+ if (ciOutput.includes('ENOENT')) return 'missing file references';
165
+ if (ciOutput.includes('ReferenceError')) return 'undefined variable references';
166
+ if (ciOutput.includes('SyntaxError')) return 'syntax errors';
167
+ return undefined;
168
+ }
169
+
170
+ /**
171
+ * Summarize CI failure for human-readable signal
172
+ */
173
+ export function summarizeCIFailure(ciOutput: string): string {
174
+ const lines = ciOutput.split('\n').filter(line => line.trim().length > 0);
175
+
176
+ // Find the first error line
177
+ for (const line of lines) {
178
+ if (
179
+ line.includes('Error:') ||
180
+ line.includes('error TS') ||
181
+ line.includes('✖') ||
182
+ line.includes('FAIL')
183
+ ) {
184
+ return line.trim().substring(0, 150); // First 150 chars
185
+ }
186
+ }
187
+
188
+ return lines[0]?.substring(0, 150) || 'CI failure (no details available)';
189
+ }
190
+
191
+ /**
192
+ * Check if inhibitor should be emitted after an agent run
193
+ * Cases: CI failed, regression, or loop detected
194
+ */
195
+ export async function maybeEmitInhibitor(
196
+ trace: TraceEvent,
197
+ ciOutput?: string
198
+ ): Promise<Inhibitor | null> {
199
+ try {
200
+ // Case 1: CI failed
201
+ if (!trace.ci_passed && ciOutput) {
202
+ return await emitInhibitor({
203
+ trigger: {
204
+ file: trace.file_path,
205
+ mode: trace.mode,
206
+ pattern: extractFailurePattern(ciOutput),
207
+ },
208
+ signal: `CI failed: ${summarizeCIFailure(ciOutput)}`,
209
+ origin: {
210
+ agent_id: trace.agent_id || 'unknown',
211
+ trace_id: trace.id,
212
+ energy_wasted: trace.cost?.estimated_usd || 0,
213
+ failure_type: 'ci_failed',
214
+ },
215
+ strength: 1.0,
216
+ half_life_days: DEFAULT_HALF_LIFE_DAYS,
217
+ });
218
+ }
219
+
220
+ // Case 2: Regression (gradient increased instead of decreased)
221
+ if (trace.gradient_delta < -0.02) {
222
+ // Made things worse
223
+ return await emitInhibitor({
224
+ trigger: {
225
+ file: trace.file_path,
226
+ mode: trace.mode,
227
+ },
228
+ signal: `${trace.mode} made ${trace.file_path} worse. Gradient increased by ${Math.abs(trace.gradient_delta).toFixed(2)}.`,
229
+ origin: {
230
+ agent_id: trace.agent_id || 'unknown',
231
+ trace_id: trace.id,
232
+ energy_wasted: trace.cost?.estimated_usd || 0,
233
+ failure_type: 'regression',
234
+ },
235
+ strength: 1.0,
236
+ half_life_days: DEFAULT_HALF_LIFE_DAYS,
237
+ });
238
+ }
239
+
240
+ // Case 3: Loop detection is handled separately via checkForLoop()
241
+
242
+ return null;
243
+ } catch (error) {
244
+ logError('Failed to check for inhibitor emission', error as Error, { trace });
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Check for loop pattern (same file + mode, 3+ attempts, no progress)
251
+ * Must be called after trace is recorded
252
+ */
253
+ export async function checkForLoop(
254
+ file: string,
255
+ mode: Mode,
256
+ getRecentTraces: (file: string, mode: Mode, limit: number) => Promise<TraceEvent[]>
257
+ ): Promise<Inhibitor | null> {
258
+ try {
259
+ const recentTraces = await getRecentTraces(file, mode, 5);
260
+
261
+ // Need at least 3 attempts
262
+ if (recentTraces.length < 3) {
263
+ return null;
264
+ }
265
+
266
+ // Check if all recent attempts had poor efficiency (<0.02)
267
+ const isLoop = recentTraces.every(t => {
268
+ const efficiency = t.efficiency || 0;
269
+ return efficiency < 0.02;
270
+ });
271
+
272
+ if (!isLoop) {
273
+ return null;
274
+ }
275
+
276
+ // Calculate total waste
277
+ const totalWasted = recentTraces.reduce(
278
+ (sum, t) => sum + (t.cost?.estimated_usd || 0),
279
+ 0
280
+ );
281
+
282
+ return await emitInhibitor({
283
+ trigger: {
284
+ file,
285
+ mode,
286
+ },
287
+ signal: `${mode} is stuck on ${file}. ${recentTraces.length} attempts with no improvement. Total wasted: $${totalWasted.toFixed(2)}.`,
288
+ origin: {
289
+ agent_id: recentTraces[0]?.agent_id || 'unknown',
290
+ trace_id: recentTraces[0]?.id || 'unknown',
291
+ energy_wasted: totalWasted,
292
+ failure_type: 'loop_detected',
293
+ },
294
+ strength: 1.0,
295
+ half_life_days: 60, // Longer half-life for loops
296
+ });
297
+ } catch (error) {
298
+ logError('Failed to check for loop', error as Error, { file, mode });
299
+ return null;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Garbage collect decayed inhibitors (strength < 0.05)
305
+ * Rewrites the NDJSON file with only relevant inhibitors
306
+ */
307
+ export async function gcInhibitors(): Promise<{ removed: number; kept: number }> {
308
+ try {
309
+ const all = await readInhibitors();
310
+ const active = all.filter(i => calculateCurrentStrength(i) >= INHIBITOR_GC_THRESHOLD);
311
+
312
+ // Rewrite file with only active inhibitors
313
+ if (active.length < all.length) {
314
+ const lines = active.map(i => JSON.stringify(i) + '\n').join('');
315
+ writeFile(getInhibitorsFile(), lines);
316
+
317
+ logInfo('Inhibitor GC complete', {
318
+ removed: all.length - active.length,
319
+ kept: active.length,
320
+ });
321
+ }
322
+
323
+ return {
324
+ removed: all.length - active.length,
325
+ kept: active.length,
326
+ };
327
+ } catch (error) {
328
+ logError('Failed to GC inhibitors', error as Error);
329
+ return { removed: 0, kept: 0 };
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Format inhibitor for display in agent prompts
335
+ */
336
+ export function formatInhibitorForPrompt(
337
+ inhibitor: Inhibitor & { currentStrength: number }
338
+ ): string {
339
+ const strengthPercent = Math.round(inhibitor.currentStrength * 100);
340
+ const patternText = inhibitor.trigger.pattern || 'this approach';
341
+
342
+ return `⚠️ **[Strength: ${strengthPercent}%]** ${patternText}
343
+ ${inhibitor.signal}
344
+ _Origin: ${inhibitor.origin.failure_type} by ${inhibitor.origin.agent_id}, wasted $${inhibitor.origin.energy_wasted.toFixed(4)}_`;
345
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Process management for spawning and managing agent workers
3
+ * Each agent runs as an independent Node.js process via child_process.fork()
4
+ *
5
+ * Reference: ADR-004 lines 15-28
6
+ */
7
+
8
+ import { fork, ChildProcess } from 'child_process';
9
+ import * as path from 'path';
10
+
11
+ export interface SpawnOptions {
12
+ agentId: string;
13
+ maxIterations?: number;
14
+ timeout?: number;
15
+ }
16
+
17
+ export interface AgentProcess {
18
+ child: ChildProcess;
19
+ agentId: string;
20
+ startedAt: Date;
21
+ timeout: NodeJS.Timeout | null;
22
+ }
23
+
24
+ const activeAgents = new Map<string, AgentProcess>();
25
+
26
+ /**
27
+ * Spawn an agent worker process
28
+ *
29
+ * @param options - Agent spawn options
30
+ * @returns ChildProcess handle
31
+ */
32
+ export function spawnAgent(options: SpawnOptions): ChildProcess {
33
+ const {
34
+ agentId,
35
+ maxIterations = 10,
36
+ timeout = 300_000, // 5 minutes default
37
+ } = options;
38
+
39
+ // Path to the compiled worker file
40
+ const workerPath = path.join(__dirname, '../agent/worker.js');
41
+
42
+ const child = fork(workerPath, [], {
43
+ env: {
44
+ ...process.env,
45
+ AGENT_ID: agentId,
46
+ MAX_ITERATIONS: String(maxIterations),
47
+ },
48
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
49
+ });
50
+
51
+ // Set up timeout
52
+ const timer = setTimeout(() => {
53
+ console.warn(`[process-manager] Agent ${agentId} timed out after ${timeout}ms`);
54
+ child.kill('SIGTERM');
55
+ }, timeout);
56
+
57
+ // Clean up on exit
58
+ child.on('exit', (code, signal) => {
59
+ clearTimeout(timer);
60
+ activeAgents.delete(agentId);
61
+ console.log(`[process-manager] Agent ${agentId} exited with code ${code}, signal ${signal}`);
62
+ });
63
+
64
+ // Handle IPC messages
65
+ child.on('message', (message: any) => {
66
+ console.log(`[process-manager] Message from ${agentId}:`, message);
67
+ });
68
+
69
+ // Handle errors
70
+ child.on('error', (error) => {
71
+ console.error(`[process-manager] Error from agent ${agentId}:`, error);
72
+ });
73
+
74
+ // Track the agent
75
+ activeAgents.set(agentId, {
76
+ child,
77
+ agentId,
78
+ startedAt: new Date(),
79
+ timeout: timer,
80
+ });
81
+
82
+ return child;
83
+ }
84
+
85
+ /**
86
+ * Kill an agent process
87
+ *
88
+ * @param agentId - Agent to kill
89
+ * @returns true if agent was killed, false if not found
90
+ */
91
+ export function killAgent(agentId: string): boolean {
92
+ const agent = activeAgents.get(agentId);
93
+ if (!agent) {
94
+ return false;
95
+ }
96
+
97
+ if (agent.timeout) {
98
+ clearTimeout(agent.timeout);
99
+ }
100
+
101
+ agent.child.kill('SIGTERM');
102
+ activeAgents.delete(agentId);
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * List all active agent processes
108
+ *
109
+ * @returns Array of active agent IDs with metadata
110
+ */
111
+ export function listAgents(): Array<{
112
+ agentId: string;
113
+ pid: number;
114
+ startedAt: Date;
115
+ uptime: number;
116
+ }> {
117
+ const agents: Array<{
118
+ agentId: string;
119
+ pid: number;
120
+ startedAt: Date;
121
+ uptime: number;
122
+ }> = [];
123
+
124
+ for (const [agentId, agent] of activeAgents) {
125
+ if (agent.child.pid) {
126
+ agents.push({
127
+ agentId,
128
+ pid: agent.child.pid,
129
+ startedAt: agent.startedAt,
130
+ uptime: Date.now() - agent.startedAt.getTime(),
131
+ });
132
+ }
133
+ }
134
+
135
+ return agents;
136
+ }
137
+
138
+ /**
139
+ * Check if an agent is running
140
+ *
141
+ * @param agentId - Agent to check
142
+ * @returns true if agent is running
143
+ */
144
+ export function isAgentRunning(agentId: string): boolean {
145
+ return activeAgents.has(agentId);
146
+ }
147
+
148
+ /**
149
+ * Get agent process info
150
+ *
151
+ * @param agentId - Agent to get info for
152
+ * @returns Agent info or null if not found
153
+ */
154
+ export function getAgentInfo(agentId: string): {
155
+ agentId: string;
156
+ pid: number;
157
+ startedAt: Date;
158
+ uptime: number;
159
+ } | null {
160
+ const agent = activeAgents.get(agentId);
161
+ if (!agent || !agent.child.pid) {
162
+ return null;
163
+ }
164
+
165
+ return {
166
+ agentId,
167
+ pid: agent.child.pid,
168
+ startedAt: agent.startedAt,
169
+ uptime: Date.now() - agent.startedAt.getTime(),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Kill all active agents
175
+ */
176
+ export function killAllAgents(): void {
177
+ for (const [agentId, agent] of activeAgents) {
178
+ if (agent.timeout) {
179
+ clearTimeout(agent.timeout);
180
+ }
181
+ agent.child.kill('SIGTERM');
182
+ }
183
+ activeAgents.clear();
184
+ }
185
+
186
+ /**
187
+ * Wait for all agents to complete
188
+ *
189
+ * @returns Promise that resolves when all agents have exited
190
+ */
191
+ export async function waitForAllAgents(): Promise<void> {
192
+ const promises = Array.from(activeAgents.values()).map((agent) =>
193
+ new Promise<void>((resolve) => {
194
+ agent.child.once('exit', () => resolve());
195
+ })
196
+ );
197
+
198
+ await Promise.all(promises);
199
+ }
@@ -17,10 +17,9 @@ import { readFile, writeFile } from 'fs/promises';
17
17
  import { v4 as uuid } from 'uuid';
18
18
  import { calculateGradient } from './gradient.js';
19
19
  import { buildPrompt } from '../prompts/index.js';
20
- import { callLLM, type LLMResponse } from '../llm/anthropic-client.js';
20
+ import { callLLM, calculateCost, type LLMResponse } from '../llm/anthropic-client.js';
21
21
  import { applyChanges } from './change-applier.js';
22
22
  import { recordTrace } from '../trace/trace-event.js';
23
- import { calculateCost } from '../cost/cost-tracker.js';
24
23
  import type { Mode, TraceEvent, ChangeSet, CostRecord } from '../types/index.js';
25
24
  import { logInfo, logDebug, logWarn, logError } from '../utils/logger.js';
26
25
  import { ciProvider } from '../utils/ci-provider.js';
@@ -104,7 +103,23 @@ export async function executeAgent(
104
103
  logInfo('Dry run - changes not applied', { file });
105
104
  } else {
106
105
  // Real run - apply changes and verify
107
- changeSet = await applyChanges(file, originalContent, parsedChanges.newContent);
106
+ const result = await applyChanges([
107
+ {
108
+ file,
109
+ newContent: parsedChanges.newContent,
110
+ reason: `Applied ${mode} changes`,
111
+ },
112
+ ]);
113
+
114
+ if (!result.success) {
115
+ throw new Error(result.error || 'Failed to apply changes');
116
+ }
117
+
118
+ changeSet = {
119
+ additions: countLines(parsedChanges.newContent) - countLines(originalContent),
120
+ deletions: 0,
121
+ files_touched: [file],
122
+ };
108
123
 
109
124
  // Step 7: Run CI checks
110
125
  logDebug('Step 7: Running CI checks');
@@ -185,7 +200,8 @@ export async function executeAgent(
185
200
  };
186
201
  } catch (error) {
187
202
  // Error handling - record failed trace
188
- logError('Agent execution failed', error, { file, mode, agentId });
203
+ const errorObj = error instanceof Error ? error : new Error(String(error));
204
+ logError('Agent execution failed', errorObj, { file, mode, agentId });
189
205
 
190
206
  const errorTrace: TraceEvent = {
191
207
  id: traceId,
@@ -12,12 +12,12 @@
12
12
  * Cache TTL: 5 minutes (300 seconds)
13
13
  */
14
14
 
15
- import { exec } from 'child_process';
15
+ import { execFile } from 'child_process';
16
16
  import { promisify } from 'util';
17
17
  import { resolve, dirname } from 'path';
18
18
  import { logDebug, logWarn } from '../../utils/logger.js';
19
19
 
20
- const execAsync = promisify(exec);
20
+ const execFileAsync = promisify(execFile);
21
21
 
22
22
  export interface ChurnResult {
23
23
  commits: number;
@@ -110,7 +110,8 @@ async function findGitRootForFile(filePath: string): Promise<string> {
110
110
  const fileDir = dirname(resolve(filePath));
111
111
 
112
112
  try {
113
- const { stdout } = await execAsync('git rev-parse --show-toplevel', {
113
+ // SECURITY: Use execFile to prevent command injection
114
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
114
115
  cwd: fileDir,
115
116
  maxBuffer: 1024,
116
117
  });
@@ -128,8 +129,10 @@ async function buildChurnCache(gitRoot: string): Promise<ChurnCache> {
128
129
  try {
129
130
  // Get all commits with changed files
130
131
  // Use "30 days ago" which git understands better than ISO dates
131
- const { stdout } = await execAsync(
132
- `git log --since="${CHURN_LOOKBACK_DAYS} days ago" --name-only --pretty=format:"" --all`,
132
+ // SECURITY: Use execFile with array args to prevent command injection
133
+ const { stdout } = await execFileAsync(
134
+ 'git',
135
+ ['log', `--since=${CHURN_LOOKBACK_DAYS} days ago`, '--name-only', '--pretty=format:', '--all'],
133
136
  {
134
137
  cwd: gitRoot,
135
138
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large repos
@@ -11,12 +11,12 @@
11
11
  * Graceful fallback if ESLint missing or fails (returns 0)
12
12
  */
13
13
 
14
- import { exec } from 'child_process';
14
+ import { execFile } from 'child_process';
15
15
  import { promisify } from 'util';
16
16
  import { getLineCount } from '../../utils/file-utils.js';
17
17
  import { logDebug, logWarn } from '../../utils/logger.js';
18
18
 
19
- const execAsync = promisify(exec);
19
+ const execFileAsync = promisify(execFile);
20
20
 
21
21
  export interface DebtResult {
22
22
  errors: number;
@@ -44,7 +44,8 @@ export async function calculateDebt(filePath: string): Promise<DebtResult> {
44
44
 
45
45
  // Run ESLint with JSON formatter
46
46
  // Note: ESLint exits with code 1 if there are linting errors, so we catch and parse stdout
47
- const { stdout } = await execAsync(`npx eslint "${filePath}" --format json`, {
47
+ // SECURITY: Use execFile with array args to prevent command injection
48
+ const { stdout } = await execFileAsync('npx', ['eslint', filePath, '--format', 'json'], {
48
49
  maxBuffer: 1024 * 1024,
49
50
  }).catch((error) => {
50
51
  // ESLint returns exit code 1 when there are lint errors, but stdout still has JSON
@@ -6,6 +6,8 @@ import {
6
6
  appendFile,
7
7
  ensureDir,
8
8
  } from '../utils/file-utils';
9
+ /* eslint-disable @typescript-eslint/no-unused-vars */
10
+ // @ts-ignore
9
11
  import { logDebug, logWarn, createLogger } from '../utils/logger';
10
12
 
11
13
  const logger = createLogger({ module: 'cost-tracker' });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * GC Module - Garbage Collection System
3
+ *
4
+ * Per Phase 7 specification §8, §14
5
+ * - Trace compaction (keep last 10 samples + 7 days)
6
+ * - Inhibitor cleanup (remove weak signals)
7
+ * - Automatic GC every 100 spawns
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * import { runGC } from './gc/index.js';
12
+ * const report = await runGC();
13
+ * ```
14
+ */
15
+
16
+ export * from './trace-compactor.js';
17
+ export * from './runner.js';