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,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,7 @@
1
+ /**
2
+ * Quarantine System for Claude-Mycelium
3
+ * Exports quarantine manager and explorer mode
4
+ */
5
+
6
+ export * from './manager.js';
7
+ export * from './explorer.js';
@@ -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
+ }