claude-mycelium 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.agent-meta/_inhibitors.ndjson +1287 -0
  2. package/.agent-meta/_quarantine.json +45 -0
  3. package/.agent-meta/config.json +9 -0
  4. package/.agent-meta/tasks/_active.json +4 -0
  5. package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
  6. package/.claude/memory.db +0 -0
  7. package/.claude/settings.local.json +4 -1
  8. package/README.md +85 -233
  9. package/SECURITY.md +145 -0
  10. package/dist/agent/task-worker.d.ts +11 -0
  11. package/dist/agent/task-worker.d.ts.map +1 -0
  12. package/dist/agent/task-worker.js +173 -0
  13. package/dist/agent/task-worker.js.map +1 -0
  14. package/dist/agent/worker.d.ts +8 -0
  15. package/dist/agent/worker.d.ts.map +1 -0
  16. package/dist/agent/worker.js +97 -0
  17. package/dist/agent/worker.js.map +1 -0
  18. package/dist/bin.d.ts +7 -0
  19. package/dist/bin.d.ts.map +1 -0
  20. package/dist/bin.js +11 -0
  21. package/dist/bin.js.map +1 -0
  22. package/dist/cli/cost.d.ts +10 -0
  23. package/dist/cli/cost.d.ts.map +1 -0
  24. package/dist/cli/cost.js +163 -0
  25. package/dist/cli/cost.js.map +1 -0
  26. package/dist/cli/gc.d.ts +10 -0
  27. package/dist/cli/gc.d.ts.map +1 -0
  28. package/dist/cli/gc.js +108 -0
  29. package/dist/cli/gc.js.map +1 -0
  30. package/dist/cli/gradients.d.ts +10 -0
  31. package/dist/cli/gradients.d.ts.map +1 -0
  32. package/dist/cli/gradients.js +70 -0
  33. package/dist/cli/gradients.js.map +1 -0
  34. package/dist/cli/grow.d.ts +17 -0
  35. package/dist/cli/grow.d.ts.map +1 -0
  36. package/dist/cli/grow.js +373 -0
  37. package/dist/cli/grow.js.map +1 -0
  38. package/dist/cli/index.d.ts +17 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +74 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/init.d.ts +11 -0
  43. package/dist/cli/init.d.ts.map +1 -0
  44. package/dist/cli/init.js +97 -0
  45. package/dist/cli/init.js.map +1 -0
  46. package/dist/cli/status.d.ts +10 -0
  47. package/dist/cli/status.d.ts.map +1 -0
  48. package/dist/cli/status.js +191 -0
  49. package/dist/cli/status.js.map +1 -0
  50. package/dist/coordination/file-locks.d.ts +42 -0
  51. package/dist/coordination/file-locks.d.ts.map +1 -0
  52. package/dist/coordination/file-locks.js +269 -0
  53. package/dist/coordination/file-locks.js.map +1 -0
  54. package/dist/coordination/index.d.ts +4 -0
  55. package/dist/coordination/index.d.ts.map +1 -1
  56. package/dist/coordination/index.js +4 -0
  57. package/dist/coordination/index.js.map +1 -1
  58. package/dist/coordination/inhibitors.d.ts +84 -0
  59. package/dist/coordination/inhibitors.d.ts.map +1 -0
  60. package/dist/coordination/inhibitors.js +290 -0
  61. package/dist/coordination/inhibitors.js.map +1 -0
  62. package/dist/coordination/process-manager.d.ts +73 -0
  63. package/dist/coordination/process-manager.d.ts.map +1 -0
  64. package/dist/coordination/process-manager.js +144 -0
  65. package/dist/coordination/process-manager.js.map +1 -0
  66. package/dist/core/agent-executor.d.ts +4 -1
  67. package/dist/core/agent-executor.d.ts.map +1 -1
  68. package/dist/core/agent-executor.js +38 -12
  69. package/dist/core/agent-executor.js.map +1 -1
  70. package/dist/core/change-applier.d.ts +29 -5
  71. package/dist/core/change-applier.d.ts.map +1 -1
  72. package/dist/core/change-applier.js +254 -24
  73. package/dist/core/change-applier.js.map +1 -1
  74. package/dist/core/signals/churn.d.ts.map +1 -1
  75. package/dist/core/signals/churn.js +6 -4
  76. package/dist/core/signals/churn.js.map +1 -1
  77. package/dist/core/signals/debt.d.ts.map +1 -1
  78. package/dist/core/signals/debt.js +4 -3
  79. package/dist/core/signals/debt.js.map +1 -1
  80. package/dist/cost/cost-tracker.d.ts.map +1 -1
  81. package/dist/cost/cost-tracker.js +2 -0
  82. package/dist/cost/cost-tracker.js.map +1 -1
  83. package/dist/gc/index.d.ts +17 -0
  84. package/dist/gc/index.d.ts.map +1 -0
  85. package/dist/gc/index.js +17 -0
  86. package/dist/gc/index.js.map +1 -0
  87. package/dist/gc/runner.d.ts +39 -0
  88. package/dist/gc/runner.d.ts.map +1 -0
  89. package/dist/gc/runner.js +277 -0
  90. package/dist/gc/runner.js.map +1 -0
  91. package/dist/gc/trace-compactor.d.ts +31 -0
  92. package/dist/gc/trace-compactor.d.ts.map +1 -0
  93. package/dist/gc/trace-compactor.js +162 -0
  94. package/dist/gc/trace-compactor.js.map +1 -0
  95. package/dist/index.d.ts +5 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +6 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/prompts/index.d.ts +2 -1
  100. package/dist/prompts/index.d.ts.map +1 -1
  101. package/dist/prompts/index.js.map +1 -1
  102. package/dist/quarantine/explorer.d.ts +65 -0
  103. package/dist/quarantine/explorer.d.ts.map +1 -0
  104. package/dist/quarantine/explorer.js +175 -0
  105. package/dist/quarantine/explorer.js.map +1 -0
  106. package/dist/quarantine/index.d.ts +7 -0
  107. package/dist/quarantine/index.d.ts.map +1 -0
  108. package/dist/quarantine/index.js +7 -0
  109. package/dist/quarantine/index.js.map +1 -0
  110. package/dist/quarantine/manager.d.ts +75 -0
  111. package/dist/quarantine/manager.d.ts.map +1 -0
  112. package/dist/quarantine/manager.js +275 -0
  113. package/dist/quarantine/manager.js.map +1 -0
  114. package/dist/task/acceptance.d.ts +29 -0
  115. package/dist/task/acceptance.d.ts.map +1 -0
  116. package/dist/task/acceptance.js +228 -0
  117. package/dist/task/acceptance.js.map +1 -0
  118. package/dist/task/agent-coordinator.d.ts +40 -0
  119. package/dist/task/agent-coordinator.d.ts.map +1 -0
  120. package/dist/task/agent-coordinator.js +168 -0
  121. package/dist/task/agent-coordinator.js.map +1 -0
  122. package/dist/task/executor.d.ts +37 -0
  123. package/dist/task/executor.d.ts.map +1 -0
  124. package/dist/task/executor.js +462 -0
  125. package/dist/task/executor.js.map +1 -0
  126. package/dist/task/index.d.ts +12 -0
  127. package/dist/task/index.d.ts.map +1 -0
  128. package/dist/task/index.js +12 -0
  129. package/dist/task/index.js.map +1 -0
  130. package/dist/task/planner.d.ts +21 -0
  131. package/dist/task/planner.d.ts.map +1 -0
  132. package/dist/task/planner.js +253 -0
  133. package/dist/task/planner.js.map +1 -0
  134. package/dist/task/storage.d.ts +46 -0
  135. package/dist/task/storage.d.ts.map +1 -0
  136. package/dist/task/storage.js +266 -0
  137. package/dist/task/storage.js.map +1 -0
  138. package/dist/trace/trace-event.d.ts +2 -18
  139. package/dist/trace/trace-event.d.ts.map +1 -1
  140. package/dist/trace/trace-event.js +6 -6
  141. package/dist/trace/trace-event.js.map +1 -1
  142. package/dist/utils/file-utils.d.ts.map +1 -1
  143. package/dist/utils/file-utils.js +54 -15
  144. package/dist/utils/file-utils.js.map +1 -1
  145. package/docs/PHASE5_IMPLEMENTATION.md +237 -0
  146. package/docs/PHASES-3-7-COMPLETE.md +177 -0
  147. package/docs/PHASE_4_COMPLETE.md +135 -0
  148. package/docs/PHASE_7_DELIVERABLES.md +295 -0
  149. package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
  150. package/docs/PHASE_7_SUMMARY.txt +195 -0
  151. package/docs/RELEASE-NOTES-v2.1.md +213 -0
  152. package/docs/ROADMAP.md +194 -107
  153. package/docs/SECURITY-AUDIT.md +387 -0
  154. package/docs/SNAPSHOT.md +59 -32
  155. package/docs/implementation/phase3-summary.md +220 -0
  156. package/package.json +27 -11
  157. package/src/agent/task-worker.ts +196 -0
  158. package/src/agent/worker.ts +111 -0
  159. package/src/bin.ts +13 -0
  160. package/src/cli/cost.ts +210 -0
  161. package/src/cli/gc.ts +138 -0
  162. package/src/cli/gradients.ts +97 -0
  163. package/src/cli/grow.ts +416 -0
  164. package/src/cli/index.ts +81 -0
  165. package/src/cli/init.ts +139 -0
  166. package/src/cli/status.ts +218 -0
  167. package/src/coordination/file-locks.ts +300 -0
  168. package/src/coordination/index.ts +4 -0
  169. package/src/coordination/inhibitors.ts +345 -0
  170. package/src/coordination/process-manager.ts +199 -0
  171. package/src/core/agent-executor.ts +37 -8
  172. package/src/core/signals/churn.ts +8 -5
  173. package/src/core/signals/debt.ts +4 -3
  174. package/src/cost/cost-tracker.ts +2 -0
  175. package/src/gc/index.ts +17 -0
  176. package/src/gc/runner.ts +314 -0
  177. package/src/gc/trace-compactor.ts +187 -0
  178. package/src/index.ts +7 -1
  179. package/src/prompts/index.ts +2 -1
  180. package/src/quarantine/explorer.ts +234 -0
  181. package/src/quarantine/index.ts +7 -0
  182. package/src/quarantine/manager.ts +336 -0
  183. package/src/task/acceptance.ts +267 -0
  184. package/src/task/agent-coordinator.ts +220 -0
  185. package/src/task/executor.ts +543 -0
  186. package/src/task/index.ts +38 -0
  187. package/src/task/planner.ts +294 -0
  188. package/src/task/storage.ts +332 -0
  189. package/src/trace/trace-event.ts +7 -26
  190. package/src/utils/file-utils.ts +61 -15
  191. package/tests/cli/gc.test.ts +206 -0
  192. package/tests/cli/init.test.ts +181 -0
  193. package/tests/cli/status.test.ts +282 -0
  194. package/tests/coordination/file-locks.test.ts +196 -0
  195. package/tests/coordination/inhibitors.test.ts +459 -0
  196. package/tests/coordination/integration.test.ts +195 -0
  197. package/tests/coordination/process-manager.test.ts +165 -0
  198. package/tests/gc/trace-compactor.test.ts +245 -0
  199. package/tests/integration/phase-7.test.ts +145 -0
  200. package/tests/quarantine/explorer.test.ts +381 -0
  201. package/tests/quarantine/manager.test.ts +399 -0
  202. package/tests/security/command-injection.test.ts +88 -0
  203. package/tests/security/path-traversal.test.ts +103 -0
  204. package/tests/task/acceptance.test.ts +411 -0
  205. package/tests/task/executor.test.ts +421 -0
  206. package/tests/task/planner.test.ts +359 -0
  207. package/tests/trace/trace-event.test.ts +62 -20
  208. package/tsconfig.json +2 -2
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Status CLI Command
3
+ *
4
+ * Display system overview
5
+ * Show agent count, recent activity
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import chalk from 'chalk';
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ import { fileExists } from '../utils/file-utils.js';
13
+ import { loadConfig } from '../utils/config.js';
14
+ import type { TraceEvent } from '../types/index.js';
15
+
16
+ const command = new Command()
17
+ .name('status')
18
+ .description('Show system status and recent activity')
19
+ .option('--json', 'Output as JSON')
20
+ .action(async (options: any) => {
21
+ try {
22
+ const status = await getSystemStatus();
23
+
24
+ if (options.json) {
25
+ console.log(JSON.stringify(status, null, 2));
26
+ return;
27
+ }
28
+
29
+ // Colorized output
30
+ console.log('');
31
+ console.log(chalk.bold('Claude Mycelium Status'));
32
+ console.log(chalk.dim('─'.repeat(60)));
33
+
34
+ console.log(chalk.bold('Configuration:'));
35
+ console.log(
36
+ ` Config version: ${status.config.version}`
37
+ );
38
+ console.log(
39
+ ` Created: ${new Date(status.config.created_at).toLocaleDateString()}`
40
+ );
41
+ console.log(
42
+ ` Error provider: ${status.config.error_provider}`
43
+ );
44
+
45
+ console.log(chalk.bold('Statistics:'));
46
+ console.log(` Total spawns: ${status.spawn_count}`);
47
+ console.log(` Last GC at spawn: ${status.last_gc_at_spawn}`);
48
+ console.log(
49
+ ` Spawns since GC: ${status.spawn_count - status.last_gc_at_spawn}`
50
+ );
51
+
52
+ console.log(chalk.bold('Storage:'));
53
+ console.log(` Trace events: ${status.trace_event_count}`);
54
+ console.log(` Quarantined files: ${status.quarantine_count}`);
55
+ console.log(` Inhibitors: ${status.inhibitor_count}`);
56
+ console.log(
57
+ ` Errors logged: ${status.error_count}`
58
+ );
59
+
60
+ if (status.recent_activity.length > 0) {
61
+ console.log(chalk.bold('Recent Activity (last 5):'));
62
+ for (const activity of status.recent_activity.slice(0, 5)) {
63
+ const timeAgo = getTimeAgo(new Date(activity.timestamp));
64
+ const modeColor = getModeColor(activity.mode);
65
+ console.log(
66
+ ` ${chalk[modeColor](activity.mode.padEnd(18))} ${activity.file.substring(0, 35).padEnd(35)} ${timeAgo.padStart(10)}`
67
+ );
68
+ }
69
+ }
70
+
71
+ console.log(chalk.dim('─'.repeat(60)));
72
+ console.log('');
73
+ } catch (error) {
74
+ const message = error instanceof Error ? error.message : 'Unknown error';
75
+ console.error(chalk.red(`Error: ${message}`));
76
+ process.exit(1);
77
+ }
78
+ });
79
+
80
+ /**
81
+ * Get system status
82
+ */
83
+ async function getSystemStatus() {
84
+ const config = loadConfig();
85
+ let traceEventCount = 0;
86
+ let quarantineCount = 0;
87
+ let inhibitorCount = 0;
88
+ let errorCount = 0;
89
+ const recentActivity: Array<{ timestamp: string; mode: string; file: string }> = [];
90
+
91
+ // Count trace events
92
+ const tracesDir = '.agent-meta/traces';
93
+ if (fileExists(tracesDir)) {
94
+ try {
95
+ const entries = await fs.readdir(tracesDir);
96
+ const traceFiles = entries.filter(f => f.endsWith('.ndjson'));
97
+
98
+ for (const file of traceFiles) {
99
+ const filePath = path.join(tracesDir, file);
100
+
101
+ try {
102
+ const content = await fs.readFile(filePath, 'utf-8');
103
+ const lines = content.split('\n').filter(Boolean);
104
+ traceEventCount += lines.length;
105
+
106
+ // Collect recent activity
107
+ for (const line of lines) {
108
+ try {
109
+ const event = JSON.parse(line) as TraceEvent;
110
+ recentActivity.push({
111
+ timestamp: event.timestamp,
112
+ mode: event.mode,
113
+ file: event.file_path,
114
+ });
115
+ } catch {
116
+ // Skip malformed lines
117
+ }
118
+ }
119
+ } catch {
120
+ // File read error
121
+ }
122
+ }
123
+ } catch {
124
+ // Directory read error
125
+ }
126
+ }
127
+
128
+ // Count quarantine entries
129
+ const quarantinePath = '.agent-meta/_quarantine.json';
130
+ if (fileExists(quarantinePath)) {
131
+ try {
132
+ const content = await fs.readFile(quarantinePath, 'utf-8');
133
+ const quarantine = JSON.parse(content);
134
+ quarantineCount = (quarantine.entries || []).length;
135
+ } catch {
136
+ // File read error
137
+ }
138
+ }
139
+
140
+ // Count inhibitors
141
+ const inhibitorsPath = '.agent-meta/_inhibitors.ndjson';
142
+ if (fileExists(inhibitorsPath)) {
143
+ try {
144
+ const content = await fs.readFile(inhibitorsPath, 'utf-8');
145
+ const lines = content.split('\n').filter(Boolean);
146
+ inhibitorCount = lines.length;
147
+ } catch {
148
+ // File read error
149
+ }
150
+ }
151
+
152
+ // Count errors
153
+ const errorsPath = '.agent-meta/_errors.json';
154
+ if (fileExists(errorsPath)) {
155
+ try {
156
+ const content = await fs.readFile(errorsPath, 'utf-8');
157
+ const errors = JSON.parse(content);
158
+ errorCount = (errors.errors || []).length;
159
+ } catch {
160
+ // File read error
161
+ }
162
+ }
163
+
164
+ // Sort recent activity by timestamp descending
165
+ recentActivity.sort(
166
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
167
+ );
168
+
169
+ return {
170
+ config,
171
+ spawn_count: config.spawn_count,
172
+ last_gc_at_spawn: config.last_gc_at_spawn,
173
+ trace_event_count: traceEventCount,
174
+ quarantine_count: quarantineCount,
175
+ inhibitor_count: inhibitorCount,
176
+ error_count: errorCount,
177
+ recent_activity: recentActivity.slice(0, 10),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Get human-readable time ago
183
+ */
184
+ function getTimeAgo(date: Date): string {
185
+ const now = new Date();
186
+ const diff = now.getTime() - date.getTime();
187
+ const seconds = Math.floor(diff / 1000);
188
+ const minutes = Math.floor(seconds / 60);
189
+ const hours = Math.floor(minutes / 60);
190
+ const days = Math.floor(hours / 24);
191
+
192
+ if (days > 0) return `${days}d ago`;
193
+ if (hours > 0) return `${hours}h ago`;
194
+ if (minutes > 0) return `${minutes}m ago`;
195
+ return `${seconds}s ago`;
196
+ }
197
+
198
+ /**
199
+ * Get color for mode
200
+ */
201
+ function getModeColor(mode: string): 'red' | 'yellow' | 'green' | 'blue' | 'magenta' | 'cyan' {
202
+ switch (mode) {
203
+ case 'error_reducer':
204
+ return 'red';
205
+ case 'complexity_reducer':
206
+ return 'yellow';
207
+ case 'debt_payer':
208
+ return 'magenta';
209
+ case 'stabilizer':
210
+ return 'green';
211
+ case 'explorer':
212
+ return 'cyan';
213
+ default:
214
+ return 'blue';
215
+ }
216
+ }
217
+
218
+ export default command;
@@ -0,0 +1,300 @@
1
+ /**
2
+ * File-based locking for multi-agent coordination
3
+ * Uses atomic O_CREAT|O_EXCL flags via fs.open() for race-free lock acquisition
4
+ *
5
+ * Reference: ADR-004, second-spec §4.1
6
+ */
7
+
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+ import type { LockFile, Mode } from '../types/index.js';
11
+
12
+ const LOCK_DIR = '.agent-meta/locks';
13
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
14
+
15
+ /**
16
+ * Acquire an exclusive lock on a file
17
+ * Uses atomic O_CREAT|O_EXCL to prevent races
18
+ *
19
+ * @param filePath - File to lock (relative path)
20
+ * @param agentId - Agent requesting the lock
21
+ * @param mode - Mode of operation
22
+ * @param taskId - Optional task ID
23
+ * @returns true if lock acquired, false if already locked
24
+ */
25
+ export async function acquireLock(
26
+ filePath: string,
27
+ agentId: string,
28
+ mode: Mode,
29
+ taskId?: string
30
+ ): Promise<boolean> {
31
+ await ensureLockDir();
32
+
33
+ const lockPath = getLockPath(filePath);
34
+ const now = new Date();
35
+ const expiresAt = new Date(now.getTime() + DEFAULT_TIMEOUT_MS);
36
+
37
+ const lockData: LockFile = {
38
+ agent_id: agentId,
39
+ file: filePath,
40
+ mode,
41
+ acquired_at: now.toISOString(),
42
+ expires_at: expiresAt.toISOString(),
43
+ pid: process.pid,
44
+ task_id: taskId,
45
+ };
46
+
47
+ try {
48
+ // Check for expired lock first and attempt takeover
49
+ const takenOver = await tryTakeoverExpiredLock(lockPath, lockData);
50
+ if (takenOver) {
51
+ return true;
52
+ }
53
+
54
+ // Atomic creation: wx = O_CREAT|O_EXCL
55
+ // Will fail if file already exists
56
+ const fd = await fs.open(lockPath, 'wx');
57
+ await fd.writeFile(JSON.stringify(lockData, null, 2));
58
+ await fd.close();
59
+
60
+ return true;
61
+ } catch (error: any) {
62
+ if (error.code === 'EEXIST') {
63
+ // Lock already exists, check if we can take it over
64
+ return false;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Release a lock
72
+ *
73
+ * @param filePath - File to unlock
74
+ * @returns true if lock released, false if not locked by this process
75
+ */
76
+ export async function releaseLock(filePath: string): Promise<boolean> {
77
+ const lockPath = getLockPath(filePath);
78
+
79
+ try {
80
+ // Verify we own the lock before releasing
81
+ const content = await fs.readFile(lockPath, 'utf-8');
82
+ const lock: LockFile = JSON.parse(content);
83
+
84
+ if (lock.pid !== process.pid) {
85
+ // Not our lock
86
+ return false;
87
+ }
88
+
89
+ await fs.unlink(lockPath);
90
+ return true;
91
+ } catch (error: any) {
92
+ if (error.code === 'ENOENT') {
93
+ // Lock doesn't exist
94
+ return false;
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if a file is locked
102
+ *
103
+ * @param filePath - File to check
104
+ * @returns LockFile if locked and valid, null otherwise
105
+ */
106
+ export async function checkLock(filePath: string): Promise<LockFile | null> {
107
+ const lockPath = getLockPath(filePath);
108
+
109
+ try {
110
+ const content = await fs.readFile(lockPath, 'utf-8');
111
+ const lock: LockFile = JSON.parse(content);
112
+
113
+ // Check if lock is expired
114
+ if (isExpired(lock)) {
115
+ return null;
116
+ }
117
+
118
+ // Check if process is alive
119
+ if (!isProcessAlive(lock.pid)) {
120
+ return null;
121
+ }
122
+
123
+ return lock;
124
+ } catch (error: any) {
125
+ if (error.code === 'ENOENT') {
126
+ return null;
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Try to take over an expired or dead process lock
134
+ *
135
+ * @param lockPath - Path to lock file
136
+ * @param newLock - New lock data
137
+ * @returns true if takeover successful
138
+ */
139
+ async function tryTakeoverExpiredLock(
140
+ lockPath: string,
141
+ newLock: LockFile
142
+ ): Promise<boolean> {
143
+ try {
144
+ const content = await fs.readFile(lockPath, 'utf-8');
145
+ const oldLock: LockFile = JSON.parse(content);
146
+
147
+ // Check if lock is expired
148
+ if (isExpired(oldLock)) {
149
+ await fs.unlink(lockPath);
150
+ const fd = await fs.open(lockPath, 'wx');
151
+ await fd.writeFile(JSON.stringify(newLock, null, 2));
152
+ await fd.close();
153
+ return true;
154
+ }
155
+
156
+ // Check if process is dead
157
+ if (!isProcessAlive(oldLock.pid)) {
158
+ await fs.unlink(lockPath);
159
+ const fd = await fs.open(lockPath, 'wx');
160
+ await fd.writeFile(JSON.stringify(newLock, null, 2));
161
+ await fd.close();
162
+ return true;
163
+ }
164
+
165
+ return false;
166
+ } catch (error: any) {
167
+ if (error.code === 'ENOENT') {
168
+ // Lock doesn't exist, normal acquisition can proceed
169
+ return false;
170
+ }
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Check if a lock has expired (5-minute timeout)
177
+ */
178
+ function isExpired(lock: LockFile): boolean {
179
+ const expiresAt = new Date(lock.expires_at);
180
+ return Date.now() > expiresAt.getTime();
181
+ }
182
+
183
+ /**
184
+ * Check if a process is alive
185
+ * Uses process.kill(pid, 0) which doesn't actually kill but checks existence
186
+ */
187
+ function isProcessAlive(pid: number): boolean {
188
+ try {
189
+ // Signal 0 checks if process exists without killing it
190
+ process.kill(pid, 0);
191
+ return true;
192
+ } catch (error: any) {
193
+ if (error.code === 'ESRCH') {
194
+ // Process not found
195
+ return false;
196
+ }
197
+ if (error.code === 'EPERM') {
198
+ // Process exists but we don't have permission to signal it
199
+ // This means it's alive
200
+ return true;
201
+ }
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Get the lock file path for a given file
208
+ */
209
+ function getLockPath(filePath: string): string {
210
+ // Sanitize file path to create a unique lock filename
211
+ const sanitized = filePath.replace(/[^a-zA-Z0-9]/g, '_');
212
+ return path.join(LOCK_DIR, `${sanitized}.lock`);
213
+ }
214
+
215
+ /**
216
+ * Ensure lock directory exists
217
+ */
218
+ async function ensureLockDir(): Promise<void> {
219
+ try {
220
+ await fs.mkdir(LOCK_DIR, { recursive: true });
221
+ } catch (error: any) {
222
+ if (error.code !== 'EEXIST') {
223
+ throw error;
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * List all active locks
230
+ */
231
+ export async function listLocks(): Promise<LockFile[]> {
232
+ try {
233
+ await ensureLockDir();
234
+ const files = await fs.readdir(LOCK_DIR);
235
+ const locks: LockFile[] = [];
236
+
237
+ for (const file of files) {
238
+ if (!file.endsWith('.lock')) continue;
239
+
240
+ try {
241
+ const lockPath = path.join(LOCK_DIR, file);
242
+ const content = await fs.readFile(lockPath, 'utf-8');
243
+ const lock: LockFile = JSON.parse(content);
244
+
245
+ // Only include valid locks
246
+ if (!isExpired(lock) && isProcessAlive(lock.pid)) {
247
+ locks.push(lock);
248
+ }
249
+ } catch {
250
+ // Skip invalid lock files
251
+ continue;
252
+ }
253
+ }
254
+
255
+ return locks;
256
+ } catch (error: any) {
257
+ if (error.code === 'ENOENT') {
258
+ return [];
259
+ }
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Clean up expired and dead process locks
266
+ * Returns the number of locks cleaned up
267
+ */
268
+ export async function cleanupStaleLocks(): Promise<number> {
269
+ try {
270
+ await ensureLockDir();
271
+ const files = await fs.readdir(LOCK_DIR);
272
+ let cleaned = 0;
273
+
274
+ for (const file of files) {
275
+ if (!file.endsWith('.lock')) continue;
276
+
277
+ try {
278
+ const lockPath = path.join(LOCK_DIR, file);
279
+ const content = await fs.readFile(lockPath, 'utf-8');
280
+ const lock: LockFile = JSON.parse(content);
281
+
282
+ // Remove if expired or process is dead
283
+ if (isExpired(lock) || !isProcessAlive(lock.pid)) {
284
+ await fs.unlink(lockPath);
285
+ cleaned++;
286
+ }
287
+ } catch {
288
+ // Skip files we can't process
289
+ continue;
290
+ }
291
+ }
292
+
293
+ return cleaned;
294
+ } catch (error: any) {
295
+ if (error.code === 'ENOENT') {
296
+ return 0;
297
+ }
298
+ throw error;
299
+ }
300
+ }
@@ -5,6 +5,10 @@
5
5
  * - Gradient caching
6
6
  * - File locks (Phase 3)
7
7
  * - Process management (Phase 3)
8
+ * - Inhibitor signals (Phase 4)
8
9
  */
9
10
 
10
11
  export * from './gradient-cache.js';
12
+ export * from './file-locks.js';
13
+ export * from './process-manager.js';
14
+ export * from './inhibitors.js';