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.
- package/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +81 -235
- package/SECURITY.md +145 -0
- package/dist/agent/worker.d.ts +8 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +97 -0
- package/dist/agent/worker.js.map +1 -0
- package/dist/bin.d.ts +7 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/cost.d.ts +10 -0
- package/dist/cli/cost.d.ts.map +1 -0
- package/dist/cli/cost.js +163 -0
- package/dist/cli/cost.js.map +1 -0
- package/dist/cli/gc.d.ts +10 -0
- package/dist/cli/gc.d.ts.map +1 -0
- package/dist/cli/gc.js +108 -0
- package/dist/cli/gc.js.map +1 -0
- package/dist/cli/gradients.d.ts +10 -0
- package/dist/cli/gradients.d.ts.map +1 -0
- package/dist/cli/gradients.js +69 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/status.d.ts +10 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +191 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/coordination/file-locks.d.ts +42 -0
- package/dist/coordination/file-locks.d.ts.map +1 -0
- package/dist/coordination/file-locks.js +269 -0
- package/dist/coordination/file-locks.js.map +1 -0
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +4 -0
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/inhibitors.d.ts +84 -0
- package/dist/coordination/inhibitors.d.ts.map +1 -0
- package/dist/coordination/inhibitors.js +290 -0
- package/dist/coordination/inhibitors.js.map +1 -0
- package/dist/coordination/process-manager.d.ts +73 -0
- package/dist/coordination/process-manager.d.ts.map +1 -0
- package/dist/coordination/process-manager.js +144 -0
- package/dist/coordination/process-manager.js.map +1 -0
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +28 -10
- package/dist/core/agent-executor.js.map +1 -1
- package/dist/core/change-applier.d.ts +29 -5
- package/dist/core/change-applier.d.ts.map +1 -1
- package/dist/core/change-applier.js +254 -24
- package/dist/core/change-applier.js.map +1 -1
- package/dist/core/signals/churn.d.ts.map +1 -1
- package/dist/core/signals/churn.js +6 -4
- package/dist/core/signals/churn.js.map +1 -1
- package/dist/core/signals/debt.d.ts.map +1 -1
- package/dist/core/signals/debt.js +4 -3
- package/dist/core/signals/debt.js.map +1 -1
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +2 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/gc/index.d.ts +17 -0
- package/dist/gc/index.d.ts.map +1 -0
- package/dist/gc/index.js +17 -0
- package/dist/gc/index.js.map +1 -0
- package/dist/gc/runner.d.ts +39 -0
- package/dist/gc/runner.d.ts.map +1 -0
- package/dist/gc/runner.js +277 -0
- package/dist/gc/runner.js.map +1 -0
- package/dist/gc/trace-compactor.d.ts +31 -0
- package/dist/gc/trace-compactor.d.ts.map +1 -0
- package/dist/gc/trace-compactor.js +162 -0
- package/dist/gc/trace-compactor.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/quarantine/explorer.d.ts +65 -0
- package/dist/quarantine/explorer.d.ts.map +1 -0
- package/dist/quarantine/explorer.js +175 -0
- package/dist/quarantine/explorer.js.map +1 -0
- package/dist/quarantine/index.d.ts +7 -0
- package/dist/quarantine/index.d.ts.map +1 -0
- package/dist/quarantine/index.js +7 -0
- package/dist/quarantine/index.js.map +1 -0
- package/dist/quarantine/manager.d.ts +75 -0
- package/dist/quarantine/manager.d.ts.map +1 -0
- package/dist/quarantine/manager.js +275 -0
- package/dist/quarantine/manager.js.map +1 -0
- package/dist/task/acceptance.d.ts +29 -0
- package/dist/task/acceptance.d.ts.map +1 -0
- package/dist/task/acceptance.js +228 -0
- package/dist/task/acceptance.js.map +1 -0
- package/dist/task/executor.d.ts +30 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +429 -0
- package/dist/task/executor.js.map +1 -0
- package/dist/task/index.d.ts +12 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +12 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/planner.d.ts +21 -0
- package/dist/task/planner.d.ts.map +1 -0
- package/dist/task/planner.js +253 -0
- package/dist/task/planner.js.map +1 -0
- package/dist/task/storage.d.ts +46 -0
- package/dist/task/storage.d.ts.map +1 -0
- package/dist/task/storage.js +266 -0
- package/dist/task/storage.js.map +1 -0
- package/dist/trace/trace-event.d.ts +2 -18
- package/dist/trace/trace-event.d.ts.map +1 -1
- package/dist/trace/trace-event.js +6 -6
- package/dist/trace/trace-event.js.map +1 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +54 -15
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/PHASE5_IMPLEMENTATION.md +237 -0
- package/docs/PHASES-3-7-COMPLETE.md +177 -0
- package/docs/PHASE_4_COMPLETE.md +135 -0
- package/docs/PHASE_7_DELIVERABLES.md +295 -0
- package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
- package/docs/PHASE_7_SUMMARY.txt +195 -0
- package/docs/RELEASE-NOTES-v2.1.md +213 -0
- package/docs/ROADMAP.md +64 -57
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +19 -11
- package/src/agent/worker.ts +111 -0
- package/src/bin.ts +13 -0
- package/src/cli/cost.ts +210 -0
- package/src/cli/gc.ts +138 -0
- package/src/cli/gradients.ts +95 -0
- package/src/cli/index.ts +79 -0
- package/src/cli/init.ts +139 -0
- package/src/cli/status.ts +218 -0
- package/src/coordination/file-locks.ts +300 -0
- package/src/coordination/index.ts +4 -0
- package/src/coordination/inhibitors.ts +345 -0
- package/src/coordination/process-manager.ts +199 -0
- package/src/core/agent-executor.ts +20 -4
- package/src/core/signals/churn.ts +8 -5
- package/src/core/signals/debt.ts +4 -3
- package/src/cost/cost-tracker.ts +2 -0
- package/src/gc/index.ts +17 -0
- package/src/gc/runner.ts +314 -0
- package/src/gc/trace-compactor.ts +187 -0
- package/src/index.ts +7 -1
- package/src/prompts/index.ts +2 -1
- package/src/quarantine/explorer.ts +234 -0
- package/src/quarantine/index.ts +7 -0
- package/src/quarantine/manager.ts +336 -0
- package/src/task/acceptance.ts +267 -0
- package/src/task/executor.ts +538 -0
- package/src/task/index.ts +38 -0
- package/src/task/planner.ts +294 -0
- package/src/task/storage.ts +332 -0
- package/src/trace/trace-event.ts +7 -26
- package/src/utils/file-utils.ts +61 -15
- package/tests/cli/gc.test.ts +206 -0
- package/tests/cli/init.test.ts +181 -0
- package/tests/cli/status.test.ts +282 -0
- package/tests/coordination/file-locks.test.ts +196 -0
- package/tests/coordination/inhibitors.test.ts +459 -0
- package/tests/coordination/integration.test.ts +195 -0
- package/tests/coordination/process-manager.test.ts +165 -0
- package/tests/gc/trace-compactor.test.ts +245 -0
- package/tests/integration/phase-7.test.ts +145 -0
- package/tests/quarantine/explorer.test.ts +381 -0
- package/tests/quarantine/manager.test.ts +399 -0
- package/tests/security/command-injection.test.ts +88 -0
- package/tests/security/path-traversal.test.ts +103 -0
- package/tests/task/acceptance.test.ts +411 -0
- package/tests/task/executor.test.ts +421 -0
- package/tests/task/planner.test.ts +359 -0
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
package/src/core/signals/debt.ts
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* Graceful fallback if ESLint missing or fails (returns 0)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
package/src/cost/cost-tracker.ts
CHANGED
|
@@ -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' });
|
package/src/gc/index.ts
ADDED
|
@@ -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';
|