autonomous-flow-daemon 1.1.0 → 1.6.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -1,53 +1,46 @@
1
+ /**
2
+ * afd daemon — S.E.A.M engine + module orchestrator.
3
+ *
4
+ * Modules:
5
+ * types.ts — shared types and constants
6
+ * workspace-map.ts — project structure cache
7
+ * mcp-handler.ts — MCP stdio JSON-RPC dispatcher
8
+ * http-routes.ts — HTTP IPC endpoints
9
+ */
10
+
1
11
  import { watch } from "chokidar";
2
- import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
3
- import { resolve } from "path";
4
- import { AFD_DIR, PID_FILE, PORT_FILE, WATCH_TARGETS } from "../constants";
12
+ import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, watch as fsWatch, readdirSync } from "fs";
13
+ import { resolve, join } from "path";
14
+ import { QUARANTINE_DIR, WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
5
15
  import { initDb } from "../core/db";
6
16
  import { generateHologram } from "../core/hologram";
7
- import { diagnose } from "../core/immune";
17
+ import { EventBatcher } from "./event-batcher";
8
18
  import type { PatchOp } from "../core/immune";
9
19
  import { detectEcosystem } from "../adapters/index";
10
- import type { DetectionResult } from "../adapters/index";
11
- import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog, buildShiftSummary } from "../core/boast";
20
+ import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog } from "../core/boast";
12
21
  import { discoverWatchTargets } from "../core/discovery";
13
-
14
- // ── Suppression Safety Constants ──
15
- const DOUBLE_TAP_WINDOW_MS = 30_000; // 30 seconds — balances demo speed and production safety
16
- const MASS_EVENT_THRESHOLD = 3; // >3 unlinks in 1 second
17
- const MASS_EVENT_WINDOW_MS = 1_000; // 1 second window
18
-
19
- interface HologramStats {
20
- totalRequests: number;
21
- totalOriginalChars: number;
22
- totalHologramChars: number;
23
- }
24
-
25
- interface DaemonState {
26
- startedAt: number;
27
- filesDetected: number;
28
- lastEvent: string | null;
29
- lastEventAt: number | null;
30
- watchedFiles: string[];
31
- hologramStats: HologramStats;
32
- ecosystems: DetectionResult[];
33
- autoHealCount: number;
34
- autoHealLog: { id: string; at: number }[];
35
- // Suppression safety: recent unlink timestamps for mass-event detection
36
- recentUnlinks: number[];
37
- // Suppression safety: per-file first-tap timestamps for double-tap detection
38
- firstTapTimestamps: Map<string, number>;
39
- suppressionSkippedCount: number;
40
- dormantTransitions: { antibodyId: string; at: number }[];
41
- totalFileBytesSaved: number;
42
- }
43
-
22
+ import { formatTimestamp, lineDiff } from "../core/log-utils";
23
+ import { semanticDiff, isAstSupported } from "../core/semantic-diff";
24
+ import { LruStringMap } from "../core/lru-map";
25
+
26
+ import {
27
+ DOUBLE_TAP_WINDOW_MS, MASS_EVENT_THRESHOLD, MASS_EVENT_WINDOW_MS,
28
+ TAP_CLEANUP_INTERVAL_MS, SELF_WRITE_DEBOUNCE_MS, VALIDATOR_TIMEOUT_MS, VALIDATORS_DIR,
29
+ } from "./types";
30
+ import type { DaemonState, DaemonContext, DaemonOptions, ValidatorFn } from "./types";
31
+ import { createWorkspaceMap } from "./workspace-map";
32
+ import { startMcpStdio } from "./mcp-handler";
33
+ import { createHttpHandler } from "./http-routes";
34
+ import { assertInsideWorkspace } from "./guards";
35
+
36
+ // ── State ──
44
37
  const state: DaemonState = {
45
38
  startedAt: Date.now(),
46
39
  filesDetected: 0,
47
40
  lastEvent: null,
48
41
  lastEventAt: null,
49
- watchedFiles: [],
50
- hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
42
+ watchedFiles: new Set(),
43
+ hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
51
44
  ecosystems: [],
52
45
  autoHealCount: 0,
53
46
  autoHealLog: [],
@@ -56,22 +49,68 @@ const state: DaemonState = {
56
49
  suppressionSkippedCount: 0,
57
50
  dormantTransitions: [],
58
51
  totalFileBytesSaved: 0,
52
+ totalSavedTokens: 0,
53
+ fileSnapshots: new LruStringMap(10 * 1024 * 1024),
54
+ sseClients: new Set(),
55
+ customValidators: new Map(),
56
+ mistakeCache: new Map(),
59
57
  };
60
58
 
59
+ const _ws = resolveWorkspacePaths();
60
+
61
+ let _cleanupResources: {
62
+ watcher?: ReturnType<typeof watch>;
63
+ interval?: ReturnType<typeof setInterval>;
64
+ wsMapGetTimer?: () => ReturnType<typeof setTimeout> | null;
65
+ validatorWatcher?: ReturnType<typeof fsWatch>;
66
+ db?: { close(): void };
67
+ eventBatcher?: EventBatcher;
68
+ } = {};
69
+
61
70
  function cleanup() {
62
- try { unlinkSync(PID_FILE); } catch {}
63
- try { unlinkSync(PORT_FILE); } catch {}
71
+ try { _cleanupResources.eventBatcher?.destroy(); } catch {}
72
+ try { _cleanupResources.interval && clearInterval(_cleanupResources.interval); } catch {}
73
+ try { const mt = _cleanupResources.wsMapGetTimer?.(); mt && clearTimeout(mt); } catch {}
74
+ try { _cleanupResources.watcher?.close(); } catch {}
75
+ try { _cleanupResources.validatorWatcher?.close(); } catch {}
76
+ try { _cleanupResources.db?.close(); } catch {}
77
+ try { unlinkSync(_ws.pidFile); } catch {}
78
+ try { unlinkSync(_ws.portFile); } catch {}
64
79
  }
65
80
 
66
- interface DaemonOptions {
67
- mcp?: boolean;
81
+ // ── S.E.A.M Logger ──
82
+ const GUARD_LINE = "========== GUARDED ==========";
83
+ const GUARD_PHASES = new Set(["Mutate", "Quarantine"]);
84
+
85
+ function createSeamLogger(mcp: boolean) {
86
+ const log = mcp ? console.error.bind(console) : console.log.bind(console);
87
+ return function seam(phase: string, msg: string) {
88
+ if (GUARD_PHASES.has(phase)) {
89
+ log(`\n${GUARD_LINE}`);
90
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
91
+ log(`${GUARD_LINE}\n`);
92
+ } else {
93
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
94
+ }
95
+ const encoder = new TextEncoder();
96
+ const payload = JSON.stringify({ phase, msg, ts: Date.now() });
97
+ const sseData = encoder.encode(`data: ${payload}\n\n`);
98
+ const dead: ReadableStreamDefaultController<Uint8Array>[] = [];
99
+ for (const controller of state.sseClients) {
100
+ try { controller.enqueue(sseData); } catch { dead.push(controller); }
101
+ }
102
+ for (const c of dead) state.sseClients.delete(c);
103
+ };
68
104
  }
69
105
 
106
+ // ══════════════════════════════════════════════════════════
70
107
  export function main(options: DaemonOptions = {}) {
71
- // Detect ecosystem at startup
72
108
  state.ecosystems = detectEcosystem(process.cwd());
73
109
 
74
110
  const db = initDb();
111
+ _cleanupResources.db = db;
112
+
113
+ // ── Prepared statements ──
75
114
  const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
76
115
  const insertAntibody = db.prepare(
77
116
  "INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
@@ -81,49 +120,219 @@ export function main(options: DaemonOptions = {}) {
81
120
  const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
82
121
  const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
83
122
  const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
123
+ const findAntibodyById = db.prepare("SELECT * FROM antibodies WHERE id = ?");
84
124
  const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
85
125
 
86
- // ── S.E.A.M Cycle Logger ──
87
- function seam(phase: string, msg: string) {
88
- console.log(`[afd] [${phase}] ${msg}`);
126
+ // Hologram stats
127
+ const getLifetime = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_lifetime WHERE id = 1");
128
+ const updateLifetime = db.prepare(
129
+ "UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
130
+ );
131
+ const upsertDaily = db.prepare(`
132
+ INSERT INTO hologram_daily (date, requests, original_chars, hologram_chars) VALUES (?, ?, ?, ?)
133
+ ON CONFLICT(date) DO UPDATE SET requests = requests + excluded.requests,
134
+ original_chars = original_chars + excluded.original_chars,
135
+ hologram_chars = hologram_chars + excluded.hologram_chars
136
+ `);
137
+ const getDailyAll = db.prepare("SELECT date, requests, original_chars, hologram_chars FROM hologram_daily ORDER BY date DESC LIMIT 7");
138
+ const purgeOldDaily = db.prepare("DELETE FROM hologram_daily WHERE date < date('now', '-7 days')");
139
+
140
+ // ── Telemetry ──
141
+ const insertTelemetry = db.prepare(
142
+ "INSERT INTO telemetry (category, action, detail, duration_ms, timestamp) VALUES (?, ?, ?, ?, ?)"
143
+ );
144
+ function trackEvent(category: string, action: string, detail?: string, durationMs?: number) {
145
+ try { insertTelemetry.run(category, action, detail ?? null, durationMs ?? null, Date.now()); } catch { /* crash-only */ }
89
146
  }
90
147
 
91
- // ── Auto-Seed Antibodies on Startup ──
92
- // Read existing immune-critical files and learn antibodies so they can be restored on delete
148
+ // ── Mistake History (Passive Defense) ──
149
+ const insertMistakeHistory = db.prepare(
150
+ "INSERT INTO mistake_history (file_path, mistake_type, description, antibody_id, timestamp) VALUES (?, ?, ?, ?, ?)"
151
+ );
152
+ const queryMistakesByFile = db.prepare(
153
+ "SELECT mistake_type, description, timestamp FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5"
154
+ );
155
+ const deleteMistakeOverflow = db.prepare(
156
+ "DELETE FROM mistake_history WHERE file_path = ? AND id NOT IN (SELECT id FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5)"
157
+ );
158
+
159
+ function recordMistake(filePath: string, mistakeType: string, description: string, antibodyId?: string) {
160
+ try {
161
+ const normalizedPath = filePath.replace(/\\/g, "/");
162
+ const truncatedDesc = description.slice(0, 200);
163
+ insertMistakeHistory.run(normalizedPath, mistakeType, truncatedDesc, antibodyId ?? null, Date.now());
164
+ deleteMistakeOverflow.run(normalizedPath, normalizedPath);
165
+ const cached = state.mistakeCache.get(normalizedPath) ?? [];
166
+ cached.unshift({ mistake_type: mistakeType, description: truncatedDesc, timestamp: Date.now() });
167
+ if (cached.length > 5) cached.length = 5;
168
+ state.mistakeCache.set(normalizedPath, cached);
169
+ } catch { /* crash-only */ }
170
+ }
171
+
172
+ try {
173
+ const allMistakes = db.prepare("SELECT file_path, mistake_type, description, timestamp FROM mistake_history ORDER BY timestamp DESC").all() as { file_path: string; mistake_type: string; description: string; timestamp: number }[];
174
+ for (const row of allMistakes) {
175
+ const cached = state.mistakeCache.get(row.file_path) ?? [];
176
+ if (cached.length < 5) {
177
+ cached.push({ mistake_type: row.mistake_type, description: row.description, timestamp: row.timestamp });
178
+ state.mistakeCache.set(row.file_path, cached);
179
+ }
180
+ }
181
+ } catch { /* crash-only — empty cache is fine */ }
182
+
183
+ function today(): string { return new Date().toISOString().slice(0, 10); }
184
+
185
+ // Load persisted hologram stats
186
+ const persisted = getLifetime.get() as { total_requests: number; total_original_chars: number; total_hologram_chars: number } | null;
187
+ if (persisted) {
188
+ state.hologramStats.totalRequests = persisted.total_requests;
189
+ state.hologramStats.totalOriginalChars = persisted.total_original_chars;
190
+ state.hologramStats.totalHologramChars = persisted.total_hologram_chars;
191
+ }
192
+
193
+ const seam = createSeamLogger(!!options.mcp);
194
+
195
+ function persistHologramStats(originalChars: number, hologramChars: number) {
196
+ state.hologramStats.totalRequests++;
197
+ state.hologramStats.totalOriginalChars += originalChars;
198
+ state.totalSavedTokens += Math.max(0, Math.floor((originalChars - hologramChars) / 4));
199
+ state.hologramStats.totalHologramChars += hologramChars;
200
+ state.hologramStats.sessionOriginalChars += originalChars;
201
+ state.hologramStats.sessionHologramChars += hologramChars;
202
+ try {
203
+ const hs = state.hologramStats;
204
+ updateLifetime.run(hs.totalRequests, hs.totalOriginalChars, hs.totalHologramChars);
205
+ upsertDaily.run(today(), 1, originalChars, hologramChars);
206
+ purgeOldDaily.run();
207
+ } catch (err) {
208
+ console.error("[afd] Failed to persist hologram stats:", err instanceof Error ? err.message : String(err));
209
+ }
210
+ }
211
+
212
+ function snapshotFile(filePath: string) {
213
+ try {
214
+ if (existsSync(filePath)) state.fileSnapshots.set(filePath, readFileSync(filePath, "utf-8"));
215
+ } catch { /* ignore */ }
216
+ }
217
+
218
+ async function safeHologram(filePath: string, source: string): Promise<string> {
219
+ try {
220
+ const result = await generateHologram(filePath, source);
221
+ persistHologramStats(result.originalLength, result.hologramLength);
222
+ return result.hologram;
223
+ } catch {
224
+ const lines = source.split("\n");
225
+ return lines.slice(0, 50).join("\n") + (lines.length > 50 ? "\n// … (truncated, AST parse failed)" : "");
226
+ }
227
+ }
228
+
229
+ function quarantineFile(originalPath: string, corruptedContent: string | null): void {
230
+ try {
231
+ mkdirSync(QUARANTINE_DIR, { recursive: true });
232
+ const now = new Date();
233
+ const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
234
+ const baseName = originalPath.replace(/[\\/]/g, "_").replace(/^_+/, "");
235
+ const quarantinePath = resolve(QUARANTINE_DIR, `${ts}_${baseName}`);
236
+ writeFileSync(quarantinePath, corruptedContent ?? "DELETED", "utf-8");
237
+ seam("Quarantine", `Saved corrupted state → .afd/quarantine/${ts}_${baseName}`);
238
+ } catch (err) {
239
+ seam("Quarantine", `FAILED to quarantine ${originalPath}: ${err instanceof Error ? err.message : String(err)}`);
240
+ }
241
+ }
242
+
243
+ // ── Auto-Seed Antibodies ──
93
244
  function seedAntibodies() {
94
245
  const immuneFiles = [
95
246
  { id: "IMM-001", path: ".claudeignore" },
96
247
  { id: "IMM-002", path: ".claude/hooks.json" },
97
248
  { id: "IMM-003", path: "CLAUDE.md" },
98
249
  ];
99
-
100
250
  for (const { id, path: filePath } of immuneFiles) {
101
251
  if (existsSync(filePath)) {
102
252
  const content = readFileSync(filePath, "utf-8");
103
253
  const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
104
254
  insertAntibody.run(id, "auto-seed", filePath, JSON.stringify(patches));
255
+ state.fileSnapshots.set(filePath, content);
105
256
  seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
106
257
  }
107
258
  }
108
259
  }
109
260
 
261
+ const selfWrites = new Set<string>();
110
262
  seedAntibodies();
111
263
 
112
- // ── Suppression Safety: Helper Functions ──
264
+ // ── Dynamic Immune Synthesis ──
265
+ const validatorsDir = join(process.cwd(), VALIDATORS_DIR);
266
+
267
+ async function loadValidators() {
268
+ state.customValidators.clear();
269
+ if (!existsSync(validatorsDir)) return;
270
+ let files: string[];
271
+ try { files = readdirSync(validatorsDir).filter(f => f.endsWith(".js")); } catch { return; }
272
+ for (const file of files) {
273
+ const absPath = resolve(validatorsDir, file);
274
+ try {
275
+ const mod = await import(absPath);
276
+ const fn = mod.default ?? mod;
277
+ if (typeof fn === "function") {
278
+ state.customValidators.set(file, fn as ValidatorFn);
279
+ seam("Adapt", `🧬 Validator loaded: ${file}`);
280
+ } else {
281
+ seam("Adapt", `⚠️ Validator ${file} does not export a function — skipped`);
282
+ }
283
+ } catch (err) {
284
+ seam("Adapt", `⚠️ Failed to load validator ${file}: ${err instanceof Error ? err.message : String(err)}`);
285
+ }
286
+ }
287
+ if (state.customValidators.size > 0) {
288
+ seam("Adapt", `Dynamic Immune Synthesis: ${state.customValidators.size} active validator(s)`);
289
+ }
290
+ }
291
+
292
+ loadValidators();
293
+
294
+ try {
295
+ mkdirSync(validatorsDir, { recursive: true });
296
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
297
+ const validatorWatcher = fsWatch(validatorsDir, (_eventType, filename) => {
298
+ if (!filename?.endsWith(".js")) return;
299
+ if (reloadTimer) clearTimeout(reloadTimer);
300
+ reloadTimer = setTimeout(() => { seam("Sense", `Validator change detected: ${filename} — reloading...`); loadValidators(); reloadTimer = null; }, 200);
301
+ });
302
+ _cleanupResources.validatorWatcher = validatorWatcher;
303
+ } catch (err) {
304
+ seam("Adapt", `⚠️ Cannot watch ${VALIDATORS_DIR}: ${err instanceof Error ? err.message : String(err)}`);
305
+ }
113
306
 
114
- /** Check if we're in a mass-event burst (>3 unlinks within 1 second) */
307
+ function runCustomValidators(newContent: string, filePath: string): boolean {
308
+ for (const [name, fn] of state.customValidators) {
309
+ try {
310
+ const t0 = performance.now();
311
+ const result = fn(newContent, filePath);
312
+ const elapsed = performance.now() - t0;
313
+ if (elapsed > VALIDATOR_TIMEOUT_MS) seam("Adapt", `⚠️ Validator ${name} took ${Math.round(elapsed)}ms (>${VALIDATOR_TIMEOUT_MS}ms)`);
314
+ if (result === true) { trackEvent("validator", name, filePath); seam("Adapt", `🛡️ Custom validator ${name} flagged corruption in ${filePath}`); return true; }
315
+ } catch (err) {
316
+ seam("Adapt", `⚠️ Validator ${name} threw error — ignored: ${err instanceof Error ? err.message : String(err)}`);
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ // ── Periodic cleanup ──
323
+ const tapCleanupInterval = setInterval(() => {
324
+ const now = Date.now();
325
+ for (const [file, ts] of state.firstTapTimestamps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) state.firstTapTimestamps.delete(file); }
326
+ for (const [file, ts] of corruptionTaps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) corruptionTaps.delete(file); }
327
+ }, TAP_CLEANUP_INTERVAL_MS);
328
+ _cleanupResources.interval = tapCleanupInterval;
329
+
330
+ // ── Suppression Safety ──
115
331
  function isMassEvent(now: number): boolean {
116
- // Prune old entries beyond the window
117
332
  state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
118
333
  return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
119
334
  }
120
335
 
121
- /** Clear first-tap state when mass event is detected (bulk ops are not intentional user deletes) */
122
- function clearTapsOnMassEvent() {
123
- state.firstTapTimestamps.clear();
124
- }
125
-
126
- /** Auto-heal: re-apply patches for a given antibody, with metrics & boast */
127
336
  function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
128
337
  const t0 = performance.now();
129
338
  try {
@@ -133,6 +342,7 @@ export function main(options: DaemonOptions = {}) {
133
342
  for (const patch of patches) {
134
343
  if (patch.op === "add" && patch.value) {
135
344
  const targetPath = resolve(patch.path.replace(/^\//, ""));
345
+ assertInsideWorkspace(targetPath, _ws.root);
136
346
  writeFileSync(targetPath, patch.value, "utf-8");
137
347
  bytesWritten += patch.value.length;
138
348
  }
@@ -141,364 +351,240 @@ export function main(options: DaemonOptions = {}) {
141
351
  state.autoHealCount++;
142
352
  state.totalFileBytesSaved += bytesWritten;
143
353
  state.autoHealLog.push({ id: antibodyId, at: Date.now() });
354
+ trackEvent("immune", "heal_hit", JSON.stringify({ antibodyId, fileTarget, bytesWritten, healMs }));
355
+ recordMistake(fileTarget, "file-deleted", `File deleted and restored via antibody ${antibodyId}`, antibodyId);
144
356
  if (state.autoHealLog.length > 100) state.autoHealLog.shift();
145
-
146
- // Delightful logging: metrics + occasional boast
147
357
  const metrics = calcHealMetrics(bytesWritten, healMs);
148
- const boast = maybeHealBoast(5); // 1-in-5 chance
358
+ const boast = maybeHealBoast(5);
149
359
  const fileName = fileTarget.split("/").pop() ?? fileTarget;
150
- console.log(formatHealLog(fileName, metrics, boast));
360
+ (options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
151
361
  } catch (err) {
152
362
  seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
153
363
  }
154
364
  }
155
365
 
156
- /**
157
- * Handle unlink event with Double-Tap and Mass-Event heuristics.
158
- * Returns true if the event was handled (healed or made dormant).
159
- */
160
- /**
161
- * Handle unlink event with Double-Tap and Mass-Event heuristics.
162
- * Returns: "healed" | "dormant" | null
163
- */
164
366
  function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
165
- // Record for mass-event detection
166
367
  state.recentUnlinks.push(now);
167
368
  insertUnlinkLog.run(filePath, now);
168
-
169
- // Mass-event check: if >3 unlinks in 1s, skip ALL suppression logic
170
- if (isMassEvent(now)) {
171
- state.suppressionSkippedCount++;
172
- clearTapsOnMassEvent();
173
- return null; // Do nothing — likely git checkout or bulk operation
174
- }
175
-
176
- // Find active (non-dormant) antibody protecting this file
369
+ if (isMassEvent(now)) { state.suppressionSkippedCount++; trackEvent("immune", "suppression", JSON.stringify({ filePath })); state.firstTapTimestamps.clear(); return null; }
177
370
  const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
178
- if (!antibody) return null; // No antibody for this file
179
-
180
- // Fetch full antibody data for healing
181
- const fullAntibody = db.prepare("SELECT * FROM antibodies WHERE id = ?").get(antibody.id) as {
182
- id: string; patch_op: string; file_target: string;
183
- } | null;
371
+ if (!antibody) return null;
372
+ const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
184
373
  if (!fullAntibody) return null;
185
-
186
- // Double-Tap detection
187
374
  const previousTap = state.firstTapTimestamps.get(filePath);
188
-
189
375
  if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
190
- // SECOND TAP within window → user is intentional → make dormant
191
376
  setAntibodyDormant.run(antibody.id);
192
377
  state.firstTapTimestamps.delete(filePath);
193
378
  state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
194
- console.log(formatDormantLog(antibody.id));
379
+ if (state.dormantTransitions.length > 100) state.dormantTransitions.shift();
380
+ trackEvent("immune", "dormant", JSON.stringify({ antibodyId: antibody.id }));
381
+ (options.mcp ? console.error : console.log)(formatDormantLog(antibody.id));
195
382
  return "dormant";
196
383
  }
197
-
198
- // FIRST TAP: record timestamp and auto-heal
199
384
  state.firstTapTimestamps.set(filePath, now);
385
+ quarantineFile(filePath, null);
200
386
  autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
201
387
  return "healed";
202
388
  }
203
389
 
204
- // ── Smart Discovery: scan for AI-context files ──
390
+ // ── Smart Discovery + Watcher ──
205
391
  const discovery = discoverWatchTargets(WATCH_TARGETS);
206
392
  seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
207
393
  if (discovery.discoveredCount > 0) {
208
394
  seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
209
395
  }
210
396
 
211
- // File watcher atomic: 100 to handle rapid delete-recreate cycles
212
- // watcher.add() after restore ensures re-detection (see handleUnlink caller)
213
- const watcher = watch(discovery.targets, {
214
- ignoreInitial: false,
215
- persistent: true,
216
- atomic: 100,
397
+ const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
398
+ _cleanupResources.watcher = watcher;
399
+
400
+ const immuneMap: Record<string, string> = { ".claudeignore": "IMM-001", ".claude/hooks.json": "IMM-002", "CLAUDE.md": "IMM-003" };
401
+ const corruptionTaps = new Map<string, number>();
402
+
403
+ function isInternalPath(p: string): boolean {
404
+ const normalized = p.replace(/\\/g, "/");
405
+ return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
406
+ }
407
+
408
+ function isCorrupted(oldContent: string, newContent: string, filePath: string): boolean {
409
+ if (runCustomValidators(newContent, filePath)) return true;
410
+ const trimmed = newContent.trim();
411
+ if (trimmed.length === 0) return true;
412
+ if (filePath.endsWith(".json")) {
413
+ if (trimmed === "{}" || trimmed === "[]") return true;
414
+ try { JSON.parse(newContent); } catch { return true; }
415
+ }
416
+ if (oldContent.length > 50 && newContent.length < oldContent.length * 0.1) return true;
417
+ return false;
418
+ }
419
+
420
+ // ── Event Batcher (Adaptive Debounce) ──
421
+ const eventBatcher = new EventBatcher({
422
+ debounceMs: 300,
423
+ isImmunePath: (p: string) => {
424
+ const normalized = p.replace(/\\/g, "/");
425
+ return normalized in immuneMap;
426
+ },
427
+ onImmediate: (event: string, path: string) => handleFileEvent(event, path),
428
+ onBatch: (events) => {
429
+ if (events.length > 1) {
430
+ seam("Sense", `[Batch] Processing ${events.length} events as single batch`);
431
+ }
432
+ for (const e of events) handleFileEvent(e.event, e.path);
433
+ },
217
434
  });
435
+ _cleanupResources.eventBatcher = eventBatcher;
218
436
 
437
+ // ── Watcher Event Handler (S.E.A.M) ──
219
438
  watcher.on("all", (event, path) => {
439
+ if (isInternalPath(path)) return;
440
+ if (selfWrites.has(path)) return;
441
+ eventBatcher.push(event, path);
442
+ });
443
+
444
+ function handleFileEvent(event: string, path: string) {
445
+ const _seamStart = performance.now();
446
+
220
447
  state.filesDetected++;
221
448
  state.lastEvent = `${event}:${path}`;
222
449
  state.lastEventAt = Date.now();
223
- if (!state.watchedFiles.includes(path)) {
224
- state.watchedFiles.push(path);
225
- }
450
+ state.watchedFiles.add(path);
226
451
  insertEvent.run(event, path, Date.now());
227
452
 
228
- // S.E.A.M cycle logging
229
- seam("Sense", `${event} → ${path}`);
453
+ if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
230
454
 
231
455
  if (event === "unlink") {
232
- seam("Extract", `File deleted: ${path} — checking antibodies`);
456
+ seam("Sense", `unlink ${path}`);
457
+ state.fileSnapshots.delete(path);
458
+ state.watchedFiles.delete(path);
233
459
  const result = handleUnlink(path, Date.now());
234
460
  if (result === "healed") {
235
- seam("Mutate", `Restore complete for ${path}`);
236
- // Re-add to watcher so future deletes are detected
461
+ selfWrites.add(path);
462
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
463
+ seam("Mutate", `Restored ${path} from antibody snapshot`);
464
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
465
+ snapshotFile(path);
237
466
  watcher.add(path);
238
467
  } else if (result === "dormant") {
239
- seam("Adapt", `Double-tap confirmed — ${path} deletion honored`);
240
- } else {
241
- seam("Extract", `No antibody found for ${path} — skipped`);
242
- }
243
- } else if (event === "change") {
244
- // Re-seed antibody on file change so restore always has latest content
245
- const immuneMap: Record<string, string> = {
246
- ".claudeignore": "IMM-001",
247
- ".claude/hooks.json": "IMM-002",
248
- "CLAUDE.md": "IMM-003",
249
- };
250
- const abId = immuneMap[path];
251
- if (abId && existsSync(path)) {
252
- const content = readFileSync(path, "utf-8");
253
- const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: content }];
254
- insertAntibody.run(abId, "auto-seed", path, JSON.stringify(patches));
255
- seam("Adapt", `Antibody ${abId} refreshed for ${path}`);
468
+ seam("Adapt", `Double-tap confirmed — user intentionally deleted ${path}, antibody deactivated`);
256
469
  }
470
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
471
+ return;
257
472
  }
258
- });
259
473
 
260
- // ── MCP stdio mode: JSON-RPC over stdin/stdout ──
261
- if (options.mcp) {
262
- console.error("[afd] MCP stdio mode — awaiting JSON-RPC on stdin");
263
- (async () => {
264
- const decoder = new TextDecoder();
265
- for await (const chunk of Bun.stdin.stream()) {
266
- const line = decoder.decode(chunk).trim();
267
- if (!line) continue;
268
- try {
269
- const req = JSON.parse(line);
270
- const response = {
271
- jsonrpc: "2.0",
272
- id: req.id,
273
- result: {
274
- tools: [
275
- { name: "afd_diagnose", description: "Run afd health diagnosis" },
276
- { name: "afd_score", description: "Get daemon score/stats" },
277
- { name: "afd_hologram", description: "Generate hologram for a file" },
278
- ],
279
- },
280
- };
281
- process.stdout.write(JSON.stringify(response) + "\n");
282
- } catch {
283
- // Crash-only: malformed input is ignored
284
- }
285
- }
286
- })();
287
- return; // file watcher + stdin loop keep process alive
288
- }
289
-
290
- // HTTP server for IPC
291
- const server = Bun.serve({
292
- port: 0,
293
- async fetch(req) {
294
- const url = new URL(req.url);
295
-
296
- if (url.pathname === "/health") {
297
- return Response.json({ status: "alive", pid: process.pid });
298
- }
299
-
300
- if (url.pathname === "/mini-status") {
301
- const last = state.autoHealLog.length > 0
302
- ? state.autoHealLog[state.autoHealLog.length - 1].id
303
- : null;
304
- return Response.json({
305
- status: "ON",
306
- healed_count: state.autoHealCount,
307
- last_healed: last,
308
- });
309
- }
310
-
311
- if (url.pathname === "/hologram") {
312
- const file = url.searchParams.get("file");
313
- if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
314
- try {
315
- const absPath = resolve(file);
316
- const source = readFileSync(absPath, "utf-8");
317
- const result = generateHologram(file, source);
318
- state.hologramStats.totalRequests++;
319
- state.hologramStats.totalOriginalChars += result.originalLength;
320
- state.hologramStats.totalHologramChars += result.hologramLength;
321
- return Response.json(result);
322
- } catch (err: unknown) {
323
- const msg = err instanceof Error ? err.message : String(err);
324
- return Response.json({ error: msg }, { status: 404 });
325
- }
326
- }
327
-
328
- if (url.pathname === "/diagnose") {
329
- const raw = url.searchParams.get("raw") === "true";
330
- const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
331
- const result = diagnose(known, { raw });
332
- return Response.json(result);
333
- }
334
-
335
- if (url.pathname === "/antibodies") {
336
- const rows = listAntibodies.all();
337
- return Response.json({ antibodies: rows });
338
- }
339
-
340
- if (url.pathname === "/antibodies/learn" && req.method === "POST") {
341
- try {
342
- const body = await req.json() as {
343
- id: string;
344
- patternType: string;
345
- fileTarget: string;
346
- patches: PatchOp[];
347
- };
348
- insertAntibody.run(
349
- body.id,
350
- body.patternType,
351
- body.fileTarget,
352
- JSON.stringify(body.patches)
353
- );
354
- return Response.json({ status: "learned", id: body.id });
355
- } catch (err: unknown) {
356
- const msg = err instanceof Error ? err.message : String(err);
357
- return Response.json({ error: msg }, { status: 400 });
474
+ if (event === "change") {
475
+ if (!existsSync(path)) return;
476
+ let newContent: string;
477
+ try { newContent = readFileSync(path, "utf-8"); } catch { return; }
478
+ const oldContent = state.fileSnapshots.get(path);
479
+ const newSize = newContent.length;
480
+
481
+ if (oldContent !== undefined && oldContent !== newContent) {
482
+ if (isAstSupported(path)) {
483
+ try {
484
+ const sdiff = semanticDiff(path, oldContent, newContent);
485
+ const breakingTag = sdiff.hasBreakingChanges ? " ⚠️ BREAKING" : "";
486
+ seam("Sense", `change → ${path} (${newSize} bytes)${breakingTag}\n [semantic] ${sdiff.summary}`);
487
+ } catch {
488
+ const diffs = lineDiff(oldContent, newContent);
489
+ seam("Sense", `change ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
490
+ }
491
+ } else {
492
+ const diffs = lineDiff(oldContent, newContent);
493
+ if (diffs.length > 0) seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
494
+ else seam("Sense", `change → ${path} (${newSize} bytes, whitespace-only diff)`);
358
495
  }
496
+ } else if (oldContent === undefined) {
497
+ seam("Sense", `change → ${path} (${newSize} bytes, no previous snapshot)`);
498
+ } else {
499
+ seam("Sense", `change → ${path} (${newSize} bytes, content identical — touch or metadata)`);
359
500
  }
360
501
 
361
- if (url.pathname === "/auto-heal/record" && req.method === "POST") {
362
- try {
363
- const body = await req.json() as { id: string };
364
- state.autoHealCount++;
365
- state.autoHealLog.push({ id: body.id, at: Date.now() });
366
- // Keep log bounded
367
- if (state.autoHealLog.length > 100) state.autoHealLog.shift();
368
- return Response.json({ status: "recorded", total: state.autoHealCount });
369
- } catch (err: unknown) {
370
- const msg = err instanceof Error ? err.message : String(err);
371
- return Response.json({ error: msg }, { status: 400 });
502
+ const normalizedPath = path.replace(/\\/g, "/");
503
+ const abId = immuneMap[normalizedPath];
504
+ if (abId && oldContent !== undefined) {
505
+ if (isCorrupted(oldContent, newContent, path)) {
506
+ const prevCorruption = corruptionTaps.get(path);
507
+ const now = Date.now();
508
+ if (prevCorruption && (now - prevCorruption) < DOUBLE_TAP_WINDOW_MS) {
509
+ corruptionTaps.delete(path);
510
+ state.fileSnapshots.set(path, newContent);
511
+ insertAntibody.run(abId, "auto-seed", path, JSON.stringify([{ op: "add", path: `/${path}`, value: newContent }]));
512
+ trackEvent("immune", "heal_false_positive", JSON.stringify({ filePath: path, abId }));
513
+ seam("Adapt", `Corruption double-tap on ${path} — standing down, accepting new content`);
514
+ } else {
515
+ corruptionTaps.set(path, now);
516
+ quarantineFile(path, newContent);
517
+ selfWrites.add(path);
518
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
519
+ writeFileSync(resolve(path), oldContent, "utf-8");
520
+ trackEvent("immune", "heal_hit", JSON.stringify({ filePath: path, abId }));
521
+ recordMistake(path, "corruption", `Silent corruption detected (${oldContent.length} → ${newSize} bytes) — restored`, abId);
522
+ seam("Mutate", `Silent corruption detected in ${path} (${oldContent.length} → ${newSize} bytes) — restored from snapshot`);
523
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
524
+ }
525
+ } else {
526
+ state.fileSnapshots.set(path, newContent);
527
+ const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: newContent }];
528
+ insertAntibody.run(abId, "auto-seed", path, JSON.stringify(patches));
529
+ trackEvent("immune", "heal_pass", JSON.stringify({ filePath: path, abId }));
530
+ seam("Adapt", `Antibody ${abId} updated: stored latest ${path} (${newSize} bytes) for auto-restore`);
372
531
  }
532
+ } else {
533
+ state.fileSnapshots.set(path, newContent);
373
534
  }
535
+ trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
536
+ return;
537
+ }
374
538
 
375
- if (url.pathname === "/score") {
376
- const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
377
- const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
378
- const abCount = countAntibodies.get() as { cnt: number };
379
- const hs = state.hologramStats;
380
- const globalSavings = hs.totalOriginalChars > 0
381
- ? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
382
- : 0;
383
- return Response.json({
384
- uptime,
385
- filesDetected: state.filesDetected,
386
- totalEvents: eventCount.cnt,
387
- lastEvent: state.lastEvent,
388
- lastEventAt: state.lastEventAt,
389
- watchedFiles: state.watchedFiles,
390
- watchTargets: discovery.targets,
391
- hologram: {
392
- requests: hs.totalRequests,
393
- originalChars: hs.totalOriginalChars,
394
- hologramChars: hs.totalHologramChars,
395
- savings: globalSavings,
396
- },
397
- immune: {
398
- antibodies: abCount.cnt,
399
- autoHealed: state.autoHealCount,
400
- lastAutoHeal: state.autoHealLog.length > 0
401
- ? state.autoHealLog[state.autoHealLog.length - 1]
402
- : null,
403
- },
404
- ecosystem: {
405
- detected: state.ecosystems.map(e => ({
406
- name: e.adapter.name,
407
- confidence: e.confidence,
408
- schema: e.adapter.getHarnessSchema(),
409
- })),
410
- primary: state.ecosystems[0]?.adapter.name ?? "Unknown",
411
- },
412
- suppression: {
413
- massEventsSkipped: state.suppressionSkippedCount,
414
- dormantTransitions: state.dormantTransitions.length,
415
- activeTaps: state.firstTapTimestamps.size,
416
- },
417
- });
418
- }
419
-
420
- if (url.pathname === "/sync") {
421
- const rows = listAntibodies.all() as {
422
- id: string;
423
- pattern_type: string;
424
- file_target: string;
425
- patch_op: string;
426
- created_at: string;
427
- }[];
428
- // Sanitize: strip absolute paths, keep only relative patterns
429
- const sanitized = rows.map(r => {
430
- const patches = JSON.parse(r.patch_op) as PatchOp[];
431
- const cleanPatches = patches.map(p => ({
432
- ...p,
433
- // Ensure paths are relative (strip any leading drive/abs prefix)
434
- path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
435
- // Strip absolute paths from values
436
- value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
437
- }));
438
- return {
439
- id: r.id,
440
- patternType: r.pattern_type,
441
- fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
442
- patches: cleanPatches,
443
- learnedAt: r.created_at,
444
- };
445
- });
446
- const payload = {
447
- version: "0.1.0",
448
- generatedAt: new Date().toISOString(),
449
- ecosystem: state.ecosystems[0]?.adapter.name ?? "Unknown",
450
- antibodyCount: sanitized.length,
451
- antibodies: sanitized,
452
- };
453
- // Write payload to disk
454
- const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
455
- writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
456
- return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
457
- }
458
-
459
- if (url.pathname === "/shift-summary") {
460
- const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
461
- const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
462
- const summary = buildShiftSummary({
463
- uptimeSeconds: uptime,
464
- totalEvents: eventCount.cnt,
465
- healsPerformed: state.autoHealCount,
466
- totalFileBytesSaved: state.totalFileBytesSaved,
467
- suppressionsSkipped: state.suppressionSkippedCount,
468
- dormantTransitions: state.dormantTransitions.length,
469
- });
470
- return Response.json(summary);
471
- }
539
+ seam("Sense", `${event} → ${path}`);
540
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
541
+ }
472
542
 
473
- if (url.pathname === "/stop") {
474
- cleanup();
475
- setTimeout(() => process.exit(0), 100);
476
- return Response.json({ status: "stopping" });
477
- }
543
+ // ── Workspace Map ──
544
+ const wsMap = createWorkspaceMap();
545
+ _cleanupResources.wsMapGetTimer = wsMap.getTimer;
546
+ watcher.on("add", () => wsMap.markDirty());
547
+ watcher.on("unlink", () => wsMap.markDirty());
548
+ wsMap.get(); // initial build
549
+
550
+ // ── Build DaemonContext ──
551
+ const ctx: DaemonContext = {
552
+ state, db: db as unknown as DaemonContext["db"], ws: _ws, options,
553
+ insertEvent, insertAntibody, listAntibodies, antibodyIds: antibodyIds as unknown as DaemonContext["antibodyIds"],
554
+ countAntibodies: countAntibodies as unknown as DaemonContext["countAntibodies"],
555
+ getDailyAll: getDailyAll as unknown as DaemonContext["getDailyAll"],
556
+ insertTelemetry: insertTelemetry as unknown as DaemonContext["insertTelemetry"],
557
+ insertMistakeHistory: insertMistakeHistory as unknown as DaemonContext["insertMistakeHistory"],
558
+ queryMistakesByFile: queryMistakesByFile as unknown as DaemonContext["queryMistakesByFile"],
559
+ deleteMistakeOverflow: deleteMistakeOverflow as unknown as DaemonContext["deleteMistakeOverflow"],
560
+ seam, persistHologramStats, safeHologram,
561
+ getWorkspaceMap: wsMap.get, today, discoveryTargets: discovery.targets,
562
+ port: 0,
563
+ };
478
564
 
479
- return Response.json({ error: "not found" }, { status: 404 });
480
- },
481
- });
565
+ // ── MCP Mode ──
566
+ if (options.mcp) {
567
+ startMcpStdio(ctx);
568
+ return;
569
+ }
482
570
 
571
+ // ── HTTP Server ──
572
+ const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
483
573
  const port = server.port;
574
+ ctx.port = port;
484
575
 
485
- mkdirSync(AFD_DIR, { recursive: true });
486
- writeFileSync(PID_FILE, String(process.pid));
487
- writeFileSync(PORT_FILE, String(port));
488
-
489
- console.log(`[afd daemon] pid=${process.pid} port=${port}`);
576
+ mkdirSync(_ws.afdDir, { recursive: true });
577
+ writeFileSync(_ws.pidFile, String(process.pid));
578
+ writeFileSync(_ws.portFile, String(port));
490
579
 
491
- process.on("uncaughtException", (err) => {
492
- console.error("[afd daemon] FATAL:", err.message);
493
- cleanup();
494
- process.exit(1);
495
- });
580
+ console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
496
581
 
582
+ process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
497
583
  process.on("SIGTERM", () => { cleanup(); process.exit(0); });
498
584
  process.on("SIGINT", () => { cleanup(); process.exit(0); });
499
585
  }
500
586
 
501
- // Auto-execute when run directly (not imported)
502
587
  if (import.meta.main) {
503
- main();
588
+ const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
589
+ main({ mcp });
504
590
  }