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.
- package/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.agent-meta/tasks/_active.json +4 -0
- package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +85 -233
- package/SECURITY.md +145 -0
- package/dist/agent/task-worker.d.ts +11 -0
- package/dist/agent/task-worker.d.ts.map +1 -0
- package/dist/agent/task-worker.js +173 -0
- package/dist/agent/task-worker.js.map +1 -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 +70 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/grow.d.ts +17 -0
- package/dist/cli/grow.d.ts.map +1 -0
- package/dist/cli/grow.js +373 -0
- package/dist/cli/grow.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 +74 -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 +4 -1
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +38 -12
- 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/agent-coordinator.d.ts +40 -0
- package/dist/task/agent-coordinator.d.ts.map +1 -0
- package/dist/task/agent-coordinator.js +168 -0
- package/dist/task/agent-coordinator.js.map +1 -0
- package/dist/task/executor.d.ts +37 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +462 -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 +194 -107
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +27 -11
- package/src/agent/task-worker.ts +196 -0
- 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 +97 -0
- package/src/cli/grow.ts +416 -0
- package/src/cli/index.ts +81 -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 +37 -8
- 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/agent-coordinator.ts +220 -0
- package/src/task/executor.ts +543 -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/tests/trace/trace-event.test.ts +62 -20
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explorer Mode Spawning for Claude-Mycelium
|
|
3
|
+
* Adaptive probability based on system efficiency and centrality
|
|
4
|
+
* Reference: initial-spec §7, second-spec §10
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logInfo, logDebug, logError } from '../utils/logger.js';
|
|
8
|
+
import type { Gradient, TraceEvent } from '../types/index.js';
|
|
9
|
+
import {
|
|
10
|
+
getQuarantinedFiles,
|
|
11
|
+
canExplorerAttempt,
|
|
12
|
+
recordExplorerAttempt,
|
|
13
|
+
isQuarantined,
|
|
14
|
+
} from './manager.js';
|
|
15
|
+
|
|
16
|
+
export interface ExplorerConfig {
|
|
17
|
+
baseProbability: number;
|
|
18
|
+
maxProbability: number;
|
|
19
|
+
efficiencyThreshold: number;
|
|
20
|
+
lookbackRuns: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG: ExplorerConfig = {
|
|
24
|
+
baseProbability: 0.05, // 5% base chance
|
|
25
|
+
maxProbability: 0.25, // 25% max chance
|
|
26
|
+
efficiencyThreshold: 0.3, // Below this, increase probability
|
|
27
|
+
lookbackRuns: 20,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determine if explorer should spawn based on system-wide efficiency
|
|
32
|
+
* Probability increases as efficiency drops
|
|
33
|
+
*
|
|
34
|
+
* Formula from spec:
|
|
35
|
+
* - efficiency >= 0.3 → 5% chance
|
|
36
|
+
* - efficiency = 0.15 → 15% chance
|
|
37
|
+
* - efficiency = 0.0 → 25% chance
|
|
38
|
+
*/
|
|
39
|
+
export async function shouldSpawnExplorer(
|
|
40
|
+
getRecentTraces: (limit: number) => Promise<TraceEvent[]>,
|
|
41
|
+
config: ExplorerConfig = DEFAULT_CONFIG
|
|
42
|
+
): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
const recentTraces = await getRecentTraces(config.lookbackRuns);
|
|
45
|
+
|
|
46
|
+
if (recentTraces.length < 10) {
|
|
47
|
+
// Not enough data
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate system-wide efficiency
|
|
52
|
+
const efficiencies = recentTraces.map(t => t.efficiency || 0);
|
|
53
|
+
const avgEfficiency = efficiencies.reduce((a, b) => a + b, 0) / efficiencies.length;
|
|
54
|
+
|
|
55
|
+
// Probability increases as efficiency drops
|
|
56
|
+
const efficiencyFactor = Math.max(
|
|
57
|
+
0,
|
|
58
|
+
config.efficiencyThreshold - avgEfficiency
|
|
59
|
+
) / config.efficiencyThreshold;
|
|
60
|
+
|
|
61
|
+
const probability =
|
|
62
|
+
config.baseProbability +
|
|
63
|
+
(config.maxProbability - config.baseProbability) * efficiencyFactor;
|
|
64
|
+
|
|
65
|
+
const shouldSpawn = Math.random() < probability;
|
|
66
|
+
|
|
67
|
+
if (shouldSpawn) {
|
|
68
|
+
logInfo('Explorer spawn triggered', {
|
|
69
|
+
avgEfficiency: avgEfficiency.toFixed(3),
|
|
70
|
+
probability: (probability * 100).toFixed(1) + '%',
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
logDebug('Explorer spawn skipped', {
|
|
74
|
+
avgEfficiency: avgEfficiency.toFixed(3),
|
|
75
|
+
probability: (probability * 100).toFixed(1) + '%',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return shouldSpawn;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logError('Failed to check explorer spawn', error as Error);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Select target for explorer mode
|
|
88
|
+
* Priority:
|
|
89
|
+
* 1. Quarantined files with explorer attempts remaining
|
|
90
|
+
* 2. Highest gradient non-quarantined file
|
|
91
|
+
*/
|
|
92
|
+
export async function selectExplorerTarget(
|
|
93
|
+
gradients: Gradient[],
|
|
94
|
+
isFileLocked: (file: string) => Promise<boolean>
|
|
95
|
+
): Promise<string | null> {
|
|
96
|
+
try {
|
|
97
|
+
// First, try quarantined files that still have explorer attempts
|
|
98
|
+
const quarantined = await getQuarantinedFiles();
|
|
99
|
+
|
|
100
|
+
for (const entry of quarantined) {
|
|
101
|
+
if (await canExplorerAttempt(entry.file)) {
|
|
102
|
+
const locked = await isFileLocked(entry.file);
|
|
103
|
+
if (!locked) {
|
|
104
|
+
logInfo('Explorer targeting quarantined file', {
|
|
105
|
+
file: entry.file,
|
|
106
|
+
attempts: entry.explorer_attempts,
|
|
107
|
+
max: entry.max_explorer_attempts,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Record the attempt
|
|
111
|
+
await recordExplorerAttempt(entry.file);
|
|
112
|
+
|
|
113
|
+
return entry.file;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Otherwise, pick highest gradient non-quarantined file
|
|
119
|
+
const sorted = [...gradients].sort((a, b) => b.score - a.score);
|
|
120
|
+
|
|
121
|
+
for (const g of sorted) {
|
|
122
|
+
const quarantinedCheck = await isQuarantined(g.file);
|
|
123
|
+
const lockedCheck = await isFileLocked(g.file);
|
|
124
|
+
|
|
125
|
+
if (!quarantinedCheck && !lockedCheck) {
|
|
126
|
+
logInfo('Explorer targeting high-gradient file', {
|
|
127
|
+
file: g.file,
|
|
128
|
+
gradient: g.score.toFixed(3),
|
|
129
|
+
});
|
|
130
|
+
return g.file;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logError('Failed to select explorer target', error as Error);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculate explorer spawn probability for a specific file
|
|
143
|
+
* Based on centrality: 0.1 × (1 + centrality)
|
|
144
|
+
*
|
|
145
|
+
* Examples from spec:
|
|
146
|
+
* - centrality 0.0 → 10% chance
|
|
147
|
+
* - centrality 0.5 → 15% chance
|
|
148
|
+
* - centrality 1.0 → 20% chance
|
|
149
|
+
*/
|
|
150
|
+
export function calculateExplorerProbability(centrality: number): number {
|
|
151
|
+
return 0.1 * (1 + centrality);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if explorer should be spawned for a specific file
|
|
156
|
+
* Uses centrality-based probability
|
|
157
|
+
*/
|
|
158
|
+
export function shouldSpawnExplorerForFile(centrality: number): boolean {
|
|
159
|
+
const probability = calculateExplorerProbability(centrality);
|
|
160
|
+
return Math.random() < probability;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Execute explorer mode with different system prompt
|
|
165
|
+
* Explorer has:
|
|
166
|
+
* - Higher temperature (more creative)
|
|
167
|
+
* - Different instruction set (exploratory)
|
|
168
|
+
* - Less constrained by patterns
|
|
169
|
+
*/
|
|
170
|
+
export interface ExplorerPromptConfig {
|
|
171
|
+
temperature: number; // Higher than normal (e.g., 0.8 vs 0.3)
|
|
172
|
+
systemPromptAddition: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const EXPLORER_PROMPT_CONFIG: ExplorerPromptConfig = {
|
|
176
|
+
temperature: 0.8, // More creative than normal modes
|
|
177
|
+
systemPromptAddition: `
|
|
178
|
+
## Explorer Mode
|
|
179
|
+
|
|
180
|
+
You are in EXPLORER mode. This file has resisted conventional approaches.
|
|
181
|
+
|
|
182
|
+
Your goal is to try creative, unconventional solutions that other modes might not consider:
|
|
183
|
+
- Radical refactoring
|
|
184
|
+
- Complete rewrites of problematic sections
|
|
185
|
+
- Alternative algorithms or patterns
|
|
186
|
+
- Breaking changes if necessary
|
|
187
|
+
|
|
188
|
+
Don't be constrained by existing patterns. Think outside the box.
|
|
189
|
+
|
|
190
|
+
Success criteria:
|
|
191
|
+
- Meaningful gradient improvement (efficiency > 0.2)
|
|
192
|
+
- CI passes
|
|
193
|
+
- Code quality maintained or improved
|
|
194
|
+
|
|
195
|
+
Be bold but careful.
|
|
196
|
+
`,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get explorer mode configuration
|
|
201
|
+
*/
|
|
202
|
+
export function getExplorerConfig(): ExplorerPromptConfig {
|
|
203
|
+
return { ...EXPLORER_PROMPT_CONFIG };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format explorer instructions for prompt
|
|
208
|
+
*/
|
|
209
|
+
export function formatExplorerInstructions(
|
|
210
|
+
file: string,
|
|
211
|
+
isQuarantined: boolean,
|
|
212
|
+
explorerAttempts: number
|
|
213
|
+
): string {
|
|
214
|
+
if (isQuarantined) {
|
|
215
|
+
return `
|
|
216
|
+
## 🔬 Explorer Mode: Quarantined File
|
|
217
|
+
|
|
218
|
+
File: ${file}
|
|
219
|
+
Status: QUARANTINED (${explorerAttempts} explorer attempts made)
|
|
220
|
+
|
|
221
|
+
This file has failed ${explorerAttempts > 0 ? 'multiple' : 'many'} conventional improvement attempts.
|
|
222
|
+
Try creative, unconventional approaches. Don't be afraid to make bold changes.
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return `
|
|
227
|
+
## 🔬 Explorer Mode: Creative Exploration
|
|
228
|
+
|
|
229
|
+
File: ${file}
|
|
230
|
+
Status: Selected for exploratory improvement
|
|
231
|
+
|
|
232
|
+
Try innovative approaches that might not be obvious. Think creatively.
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quarantine Manager for Claude-Mycelium
|
|
3
|
+
* Full withdrawal mechanism when files resist all approaches
|
|
4
|
+
* Storage: .agent-meta/_quarantine.json
|
|
5
|
+
* Reference: initial-spec §6, second-spec §9
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { readJsonFile, writeJsonFile, fileExists } from '../utils/file-utils.js';
|
|
10
|
+
import { logInfo, logWarn, logError } from '../utils/logger.js';
|
|
11
|
+
import type { TraceEvent } from '../types/index.js';
|
|
12
|
+
|
|
13
|
+
// Compute file path at runtime to respect TEST_META_DIR
|
|
14
|
+
function getQuarantineFile(): string {
|
|
15
|
+
const META_DIR = process.env.TEST_META_DIR || '.agent-meta';
|
|
16
|
+
return path.join(META_DIR, '_quarantine.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QuarantineEntry {
|
|
20
|
+
file: string;
|
|
21
|
+
quarantined_at: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
attempts_before_quarantine: number;
|
|
24
|
+
explorer_attempts: number;
|
|
25
|
+
max_explorer_attempts: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface QuarantineState {
|
|
29
|
+
updated_at: string;
|
|
30
|
+
entries: QuarantineEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface QuarantineConfig {
|
|
34
|
+
minSamplesBeforeQuarantine: number;
|
|
35
|
+
efficiencyThreshold: number;
|
|
36
|
+
maxExplorerAttempts: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONFIG: QuarantineConfig = {
|
|
40
|
+
minSamplesBeforeQuarantine: 10, // Spec requirement
|
|
41
|
+
efficiencyThreshold: 0.02, // All samples must be below this
|
|
42
|
+
maxExplorerAttempts: 3, // Spec requirement
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a file should be quarantined
|
|
47
|
+
* Trigger: 10+ samples, all with efficiency < 0.02
|
|
48
|
+
*/
|
|
49
|
+
export async function shouldQuarantine(
|
|
50
|
+
file: string,
|
|
51
|
+
getTracesForFile: (file: string) => Promise<TraceEvent[]>,
|
|
52
|
+
config: QuarantineConfig = DEFAULT_CONFIG
|
|
53
|
+
): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
const traces = await getTracesForFile(file);
|
|
56
|
+
|
|
57
|
+
// Need minimum samples
|
|
58
|
+
if (traces.length < config.minSamplesBeforeQuarantine) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Take last 10 samples
|
|
63
|
+
const recent = traces.slice(-config.minSamplesBeforeQuarantine);
|
|
64
|
+
|
|
65
|
+
// All must have poor efficiency
|
|
66
|
+
const allPoor = recent.every(t => {
|
|
67
|
+
const efficiency = t.efficiency || 0;
|
|
68
|
+
return efficiency < config.efficiencyThreshold;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return allPoor;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logError('Failed to check quarantine condition', error as Error, { file });
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add a file to quarantine
|
|
80
|
+
*/
|
|
81
|
+
export async function addToQuarantine(entry: QuarantineEntry): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
const state = await readQuarantineState();
|
|
84
|
+
|
|
85
|
+
// Check if already quarantined
|
|
86
|
+
const existing = state.entries.find(e => e.file === entry.file);
|
|
87
|
+
if (existing) {
|
|
88
|
+
logWarn('File already quarantined', { file: entry.file });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add entry
|
|
93
|
+
state.entries.push(entry);
|
|
94
|
+
state.updated_at = new Date().toISOString();
|
|
95
|
+
|
|
96
|
+
await writeQuarantineState(state);
|
|
97
|
+
|
|
98
|
+
logWarn('File quarantined', {
|
|
99
|
+
file: entry.file,
|
|
100
|
+
reason: entry.reason,
|
|
101
|
+
attempts: entry.attempts_before_quarantine,
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logError('Failed to add to quarantine', error as Error, { entry });
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a file is quarantined
|
|
111
|
+
*/
|
|
112
|
+
export async function isQuarantined(file: string): Promise<boolean> {
|
|
113
|
+
try {
|
|
114
|
+
const state = await readQuarantineState();
|
|
115
|
+
return state.entries.some(e => e.file === file);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logError('Failed to check quarantine status', error as Error, { file });
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get quarantine entry for a file
|
|
124
|
+
*/
|
|
125
|
+
export async function getQuarantineEntry(file: string): Promise<QuarantineEntry | null> {
|
|
126
|
+
try {
|
|
127
|
+
const state = await readQuarantineState();
|
|
128
|
+
return state.entries.find(e => e.file === file) ?? null;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logError('Failed to get quarantine entry', error as Error, { file });
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if explorer can attempt quarantined file
|
|
137
|
+
*/
|
|
138
|
+
export async function canExplorerAttempt(file: string): Promise<boolean> {
|
|
139
|
+
try {
|
|
140
|
+
const entry = await getQuarantineEntry(file);
|
|
141
|
+
if (!entry) return true; // Not quarantined
|
|
142
|
+
|
|
143
|
+
return entry.explorer_attempts < entry.max_explorer_attempts;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logError('Failed to check explorer attempts', error as Error, { file });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Record an explorer attempt on quarantined file
|
|
152
|
+
*/
|
|
153
|
+
export async function recordExplorerAttempt(file: string): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
const state = await readQuarantineState();
|
|
156
|
+
const entry = state.entries.find(e => e.file === file);
|
|
157
|
+
|
|
158
|
+
if (entry) {
|
|
159
|
+
entry.explorer_attempts++;
|
|
160
|
+
state.updated_at = new Date().toISOString();
|
|
161
|
+
await writeQuarantineState(state);
|
|
162
|
+
|
|
163
|
+
logInfo('Explorer attempt recorded', {
|
|
164
|
+
file,
|
|
165
|
+
attempts: entry.explorer_attempts,
|
|
166
|
+
remaining: entry.max_explorer_attempts - entry.explorer_attempts,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logError('Failed to record explorer attempt', error as Error, { file });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if quarantine should be released after explorer success
|
|
176
|
+
* Only explorer mode with efficiency >= 0.2 can release
|
|
177
|
+
*/
|
|
178
|
+
export async function checkQuarantineRelease(
|
|
179
|
+
file: string,
|
|
180
|
+
trace: TraceEvent
|
|
181
|
+
): Promise<boolean> {
|
|
182
|
+
try {
|
|
183
|
+
if (!(await isQuarantined(file))) return false;
|
|
184
|
+
|
|
185
|
+
// Only explorer can release
|
|
186
|
+
if (trace.mode !== 'explorer') return false;
|
|
187
|
+
|
|
188
|
+
// Must have good efficiency
|
|
189
|
+
const efficiency = trace.efficiency || 0;
|
|
190
|
+
if (efficiency < 0.2) return false;
|
|
191
|
+
|
|
192
|
+
await removeFromQuarantine(file);
|
|
193
|
+
logInfo('Quarantine released', {
|
|
194
|
+
file,
|
|
195
|
+
explorer_efficiency: efficiency.toFixed(3),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logError('Failed to check quarantine release', error as Error, { file });
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove a file from quarantine
|
|
207
|
+
*/
|
|
208
|
+
export async function removeFromQuarantine(file: string): Promise<void> {
|
|
209
|
+
try {
|
|
210
|
+
const state = await readQuarantineState();
|
|
211
|
+
const before = state.entries.length;
|
|
212
|
+
|
|
213
|
+
state.entries = state.entries.filter(e => e.file !== file);
|
|
214
|
+
state.updated_at = new Date().toISOString();
|
|
215
|
+
|
|
216
|
+
await writeQuarantineState(state);
|
|
217
|
+
|
|
218
|
+
if (state.entries.length < before) {
|
|
219
|
+
logInfo('File removed from quarantine', { file });
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logError('Failed to remove from quarantine', error as Error, { file });
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all quarantined files
|
|
229
|
+
*/
|
|
230
|
+
export async function getQuarantinedFiles(): Promise<QuarantineEntry[]> {
|
|
231
|
+
try {
|
|
232
|
+
const state = await readQuarantineState();
|
|
233
|
+
return state.entries;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logError('Failed to get quarantined files', error as Error);
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check and potentially quarantine a file after agent run
|
|
242
|
+
*/
|
|
243
|
+
export async function checkAndQuarantine(
|
|
244
|
+
file: string,
|
|
245
|
+
getTracesForFile: (file: string) => Promise<TraceEvent[]>,
|
|
246
|
+
config?: QuarantineConfig
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
try {
|
|
249
|
+
// Already quarantined?
|
|
250
|
+
if (await isQuarantined(file)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Should quarantine now?
|
|
255
|
+
if (await shouldQuarantine(file, getTracesForFile, config)) {
|
|
256
|
+
const traces = await getTracesForFile(file);
|
|
257
|
+
const recentEfficiency = traces.slice(-5).map(t => t.efficiency || 0);
|
|
258
|
+
const avgEfficiency =
|
|
259
|
+
recentEfficiency.reduce((a, b) => a + b, 0) / recentEfficiency.length;
|
|
260
|
+
|
|
261
|
+
await addToQuarantine({
|
|
262
|
+
file,
|
|
263
|
+
quarantined_at: new Date().toISOString(),
|
|
264
|
+
reason: `No progress after ${traces.length} attempts. Avg efficiency: ${avgEfficiency.toFixed(3)}`,
|
|
265
|
+
attempts_before_quarantine: traces.length,
|
|
266
|
+
explorer_attempts: 0,
|
|
267
|
+
max_explorer_attempts: config?.maxExplorerAttempts ?? DEFAULT_CONFIG.maxExplorerAttempts,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return false;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logError('Failed to check and quarantine', error as Error, { file });
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Read quarantine state from file
|
|
282
|
+
*/
|
|
283
|
+
async function readQuarantineState(): Promise<QuarantineState> {
|
|
284
|
+
try {
|
|
285
|
+
if (!fileExists(getQuarantineFile())) {
|
|
286
|
+
return {
|
|
287
|
+
updated_at: new Date().toISOString(),
|
|
288
|
+
entries: [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return readJsonFile<QuarantineState>(getQuarantineFile());
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logError('Failed to read quarantine state', error as Error);
|
|
295
|
+
return {
|
|
296
|
+
updated_at: new Date().toISOString(),
|
|
297
|
+
entries: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Write quarantine state to file
|
|
304
|
+
*/
|
|
305
|
+
async function writeQuarantineState(state: QuarantineState): Promise<void> {
|
|
306
|
+
try {
|
|
307
|
+
writeJsonFile(getQuarantineFile(), state, 2);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
logError('Failed to write quarantine state', error as Error);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get quarantine statistics
|
|
316
|
+
*/
|
|
317
|
+
export async function getQuarantineStats(): Promise<{
|
|
318
|
+
total: number;
|
|
319
|
+
withExplorerAttempts: number;
|
|
320
|
+
exhausted: number;
|
|
321
|
+
}> {
|
|
322
|
+
try {
|
|
323
|
+
const state = await readQuarantineState();
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
total: state.entries.length,
|
|
327
|
+
withExplorerAttempts: state.entries.filter(e => e.explorer_attempts > 0).length,
|
|
328
|
+
exhausted: state.entries.filter(
|
|
329
|
+
e => e.explorer_attempts >= e.max_explorer_attempts
|
|
330
|
+
).length,
|
|
331
|
+
};
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logError('Failed to get quarantine stats', error as Error);
|
|
334
|
+
return { total: 0, withExplorerAttempts: 0, exhausted: 0 };
|
|
335
|
+
}
|
|
336
|
+
}
|