autonomous-flow-daemon 1.1.0 → 1.9.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 (75) hide show
  1. package/CHANGELOG.md +85 -46
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -337
  5. package/mcp-config.json +10 -10
  6. package/package.json +14 -6
  7. package/src/adapters/index.ts +370 -159
  8. package/src/cli.ts +162 -57
  9. package/src/commands/benchmark.ts +187 -0
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/diagnose.ts +56 -14
  13. package/src/commands/doctor.ts +243 -0
  14. package/src/commands/evolution.ts +190 -0
  15. package/src/commands/fix.ts +158 -138
  16. package/src/commands/hooks.ts +136 -0
  17. package/src/commands/lang.ts +41 -41
  18. package/src/commands/mcp.ts +129 -0
  19. package/src/commands/plugin.ts +110 -0
  20. package/src/commands/restart.ts +14 -0
  21. package/src/commands/score.ts +276 -208
  22. package/src/commands/start.ts +155 -96
  23. package/src/commands/stats.ts +103 -0
  24. package/src/commands/status.ts +157 -0
  25. package/src/commands/stop.ts +68 -49
  26. package/src/commands/suggest.ts +211 -0
  27. package/src/commands/sync.ts +567 -21
  28. package/src/commands/vaccine.ts +177 -0
  29. package/src/constants.ts +32 -8
  30. package/src/core/boast.ts +280 -265
  31. package/src/core/config.ts +49 -49
  32. package/src/core/correlation-engine.ts +265 -0
  33. package/src/core/db.ts +145 -46
  34. package/src/core/discovery.ts +65 -65
  35. package/src/core/evolution.ts +215 -0
  36. package/src/core/federation.ts +129 -0
  37. package/src/core/hologram/engine.ts +71 -0
  38. package/src/core/hologram/fallback.ts +11 -0
  39. package/src/core/hologram/go-extractor.ts +203 -0
  40. package/src/core/hologram/incremental.ts +227 -0
  41. package/src/core/hologram/py-extractor.ts +132 -0
  42. package/src/core/hologram/rust-extractor.ts +244 -0
  43. package/src/core/hologram/ts-extractor.ts +406 -0
  44. package/src/core/hologram/types.ts +27 -0
  45. package/src/core/hologram.ts +73 -243
  46. package/src/core/hook-manager.ts +259 -0
  47. package/src/core/i18n/messages.ts +309 -266
  48. package/src/core/immune.ts +8 -123
  49. package/src/core/locale.ts +88 -88
  50. package/src/core/log-rotate.ts +33 -0
  51. package/src/core/log-utils.ts +38 -0
  52. package/src/core/lru-map.ts +61 -0
  53. package/src/core/notify.ts +74 -66
  54. package/src/core/plugin-manager.ts +225 -0
  55. package/src/core/rule-engine.ts +287 -0
  56. package/src/core/rule-suggestion.ts +127 -0
  57. package/src/core/semantic-diff.ts +432 -0
  58. package/src/core/telemetry.ts +94 -0
  59. package/src/core/vaccine-registry.ts +212 -0
  60. package/src/core/validator-generator.ts +224 -0
  61. package/src/core/workspace.ts +28 -0
  62. package/src/core/yaml-minimal.ts +176 -0
  63. package/src/daemon/client.ts +78 -37
  64. package/src/daemon/event-batcher.ts +108 -0
  65. package/src/daemon/guards.ts +13 -0
  66. package/src/daemon/http-routes.ts +376 -0
  67. package/src/daemon/mcp-handler.ts +575 -0
  68. package/src/daemon/mcp-subscriptions.ts +81 -0
  69. package/src/daemon/mesh.ts +51 -0
  70. package/src/daemon/server.ts +655 -504
  71. package/src/daemon/types.ts +121 -0
  72. package/src/daemon/workspace-map.ts +104 -0
  73. package/src/platform.ts +60 -39
  74. package/src/version.ts +15 -0
  75. package/README.ko.md +0 -306
@@ -1,504 +1,655 @@
1
- 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";
5
- import { initDb } from "../core/db";
6
- import { generateHologram } from "../core/hologram";
7
- import { diagnose } from "../core/immune";
8
- import type { PatchOp } from "../core/immune";
9
- import { detectEcosystem } from "../adapters/index";
10
- import type { DetectionResult } from "../adapters/index";
11
- import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog, buildShiftSummary } from "../core/boast";
12
- 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
-
44
- const state: DaemonState = {
45
- startedAt: Date.now(),
46
- filesDetected: 0,
47
- lastEvent: null,
48
- lastEventAt: null,
49
- watchedFiles: [],
50
- hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
51
- ecosystems: [],
52
- autoHealCount: 0,
53
- autoHealLog: [],
54
- recentUnlinks: [],
55
- firstTapTimestamps: new Map(),
56
- suppressionSkippedCount: 0,
57
- dormantTransitions: [],
58
- totalFileBytesSaved: 0,
59
- };
60
-
61
- function cleanup() {
62
- try { unlinkSync(PID_FILE); } catch {}
63
- try { unlinkSync(PORT_FILE); } catch {}
64
- }
65
-
66
- interface DaemonOptions {
67
- mcp?: boolean;
68
- }
69
-
70
- export function main(options: DaemonOptions = {}) {
71
- // Detect ecosystem at startup
72
- state.ecosystems = detectEcosystem(process.cwd());
73
-
74
- const db = initDb();
75
- const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
76
- const insertAntibody = db.prepare(
77
- "INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
78
- );
79
- const listAntibodies = db.prepare("SELECT * FROM antibodies ORDER BY created_at DESC");
80
- const antibodyIds = db.prepare("SELECT id FROM antibodies WHERE dormant = 0");
81
- const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
82
- const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
83
- const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
84
- const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
85
-
86
- // ── S.E.A.M Cycle Logger ──
87
- function seam(phase: string, msg: string) {
88
- console.log(`[afd] [${phase}] ${msg}`);
89
- }
90
-
91
- // ── Auto-Seed Antibodies on Startup ──
92
- // Read existing immune-critical files and learn antibodies so they can be restored on delete
93
- function seedAntibodies() {
94
- const immuneFiles = [
95
- { id: "IMM-001", path: ".claudeignore" },
96
- { id: "IMM-002", path: ".claude/hooks.json" },
97
- { id: "IMM-003", path: "CLAUDE.md" },
98
- ];
99
-
100
- for (const { id, path: filePath } of immuneFiles) {
101
- if (existsSync(filePath)) {
102
- const content = readFileSync(filePath, "utf-8");
103
- const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
104
- insertAntibody.run(id, "auto-seed", filePath, JSON.stringify(patches));
105
- seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
106
- }
107
- }
108
- }
109
-
110
- seedAntibodies();
111
-
112
- // ── Suppression Safety: Helper Functions ──
113
-
114
- /** Check if we're in a mass-event burst (>3 unlinks within 1 second) */
115
- function isMassEvent(now: number): boolean {
116
- // Prune old entries beyond the window
117
- state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
118
- return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
119
- }
120
-
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
- function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
128
- const t0 = performance.now();
129
- try {
130
- seam("Mutate", `Restoring ${fileTarget} via antibody ${antibodyId}...`);
131
- const patches = JSON.parse(patchOp) as PatchOp[];
132
- let bytesWritten = 0;
133
- for (const patch of patches) {
134
- if (patch.op === "add" && patch.value) {
135
- const targetPath = resolve(patch.path.replace(/^\//, ""));
136
- writeFileSync(targetPath, patch.value, "utf-8");
137
- bytesWritten += patch.value.length;
138
- }
139
- }
140
- const healMs = Math.round(performance.now() - t0);
141
- state.autoHealCount++;
142
- state.totalFileBytesSaved += bytesWritten;
143
- state.autoHealLog.push({ id: antibodyId, at: Date.now() });
144
- if (state.autoHealLog.length > 100) state.autoHealLog.shift();
145
-
146
- // Delightful logging: metrics + occasional boast
147
- const metrics = calcHealMetrics(bytesWritten, healMs);
148
- const boast = maybeHealBoast(5); // 1-in-5 chance
149
- const fileName = fileTarget.split("/").pop() ?? fileTarget;
150
- console.log(formatHealLog(fileName, metrics, boast));
151
- } catch (err) {
152
- seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
153
- }
154
- }
155
-
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
- function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
165
- // Record for mass-event detection
166
- state.recentUnlinks.push(now);
167
- 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
177
- 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;
184
- if (!fullAntibody) return null;
185
-
186
- // Double-Tap detection
187
- const previousTap = state.firstTapTimestamps.get(filePath);
188
-
189
- if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
190
- // SECOND TAP within window → user is intentional → make dormant
191
- setAntibodyDormant.run(antibody.id);
192
- state.firstTapTimestamps.delete(filePath);
193
- state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
194
- console.log(formatDormantLog(antibody.id));
195
- return "dormant";
196
- }
197
-
198
- // FIRST TAP: record timestamp and auto-heal
199
- state.firstTapTimestamps.set(filePath, now);
200
- autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
201
- return "healed";
202
- }
203
-
204
- // ── Smart Discovery: scan for AI-context files ──
205
- const discovery = discoverWatchTargets(WATCH_TARGETS);
206
- seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
207
- if (discovery.discoveredCount > 0) {
208
- seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
209
- }
210
-
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,
217
- });
218
-
219
- watcher.on("all", (event, path) => {
220
- state.filesDetected++;
221
- state.lastEvent = `${event}:${path}`;
222
- state.lastEventAt = Date.now();
223
- if (!state.watchedFiles.includes(path)) {
224
- state.watchedFiles.push(path);
225
- }
226
- insertEvent.run(event, path, Date.now());
227
-
228
- // S.E.A.M cycle logging
229
- seam("Sense", `${event} → ${path}`);
230
-
231
- if (event === "unlink") {
232
- seam("Extract", `File deleted: ${path} — checking antibodies`);
233
- const result = handleUnlink(path, Date.now());
234
- if (result === "healed") {
235
- seam("Mutate", `Restore complete for ${path}`);
236
- // Re-add to watcher so future deletes are detected
237
- watcher.add(path);
238
- } 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}`);
256
- }
257
- }
258
- });
259
-
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 });
358
- }
359
- }
360
-
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 });
372
- }
373
- }
374
-
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
- }
472
-
473
- if (url.pathname === "/stop") {
474
- cleanup();
475
- setTimeout(() => process.exit(0), 100);
476
- return Response.json({ status: "stopping" });
477
- }
478
-
479
- return Response.json({ error: "not found" }, { status: 404 });
480
- },
481
- });
482
-
483
- const port = server.port;
484
-
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}`);
490
-
491
- process.on("uncaughtException", (err) => {
492
- console.error("[afd daemon] FATAL:", err.message);
493
- cleanup();
494
- process.exit(1);
495
- });
496
-
497
- process.on("SIGTERM", () => { cleanup(); process.exit(0); });
498
- process.on("SIGINT", () => { cleanup(); process.exit(0); });
499
- }
500
-
501
- // Auto-execute when run directly (not imported)
502
- if (import.meta.main) {
503
- main();
504
- }
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
+
11
+ import { watch } from "chokidar";
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";
15
+ import { initDb } from "../core/db";
16
+ import { generateHologram } from "../core/hologram";
17
+ import { EventBatcher } from "./event-batcher";
18
+ import type { PatchOp } from "../core/immune";
19
+ import { detectEcosystem } from "../adapters/index";
20
+ import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog } from "../core/boast";
21
+ import { discoverWatchTargets } from "../core/discovery";
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, SeamEventEntry, QuarantineLogEntry } 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
+ import { registerMesh, deregisterMesh } from "./mesh";
36
+ import { subscriptionManager } from "./mcp-subscriptions";
37
+
38
+ // ── State ──
39
+ const state: DaemonState = {
40
+ startedAt: Date.now(),
41
+ filesDetected: 0,
42
+ lastEvent: null,
43
+ lastEventAt: null,
44
+ watchedFiles: new Set(),
45
+ hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
46
+ ecosystems: [],
47
+ autoHealCount: 0,
48
+ autoHealLog: [],
49
+ recentUnlinks: [],
50
+ firstTapTimestamps: new Map(),
51
+ suppressionSkippedCount: 0,
52
+ dormantTransitions: [],
53
+ totalFileBytesSaved: 0,
54
+ totalSavedTokens: 0,
55
+ fileSnapshots: new LruStringMap(10 * 1024 * 1024),
56
+ sseClients: new Set(),
57
+ customValidators: new Map(),
58
+ mistakeCache: new Map(),
59
+ seamEventLog: [],
60
+ quarantineLog: [],
61
+ };
62
+
63
+ const _ws = resolveWorkspacePaths();
64
+
65
+ let _cleanupResources: {
66
+ watcher?: ReturnType<typeof watch>;
67
+ interval?: ReturnType<typeof setInterval>;
68
+ wsMapGetTimer?: () => ReturnType<typeof setTimeout> | null;
69
+ validatorWatcher?: ReturnType<typeof fsWatch>;
70
+ db?: { close(): void };
71
+ eventBatcher?: EventBatcher;
72
+ } = {};
73
+
74
+ function cleanup() {
75
+ try { _cleanupResources.eventBatcher?.destroy(); } catch {}
76
+ try { _cleanupResources.interval && clearInterval(_cleanupResources.interval); } catch {}
77
+ try { const mt = _cleanupResources.wsMapGetTimer?.(); mt && clearTimeout(mt); } catch {}
78
+ try { _cleanupResources.watcher?.close(); } catch {}
79
+ try { _cleanupResources.validatorWatcher?.close(); } catch {}
80
+ try { _cleanupResources.db?.close(); } catch {}
81
+ try { unlinkSync(_ws.pidFile); } catch {}
82
+ try { unlinkSync(_ws.portFile); } catch {}
83
+ try { deregisterMesh(_ws.root); } catch {}
84
+ }
85
+
86
+ // ── S.E.A.M Logger ──
87
+ const GUARD_LINE = "========== GUARDED ==========";
88
+ const GUARD_PHASES = new Set(["Mutate", "Quarantine"]);
89
+
90
+ function createSeamLogger(mcp: boolean) {
91
+ const log = mcp ? console.error.bind(console) : console.log.bind(console);
92
+ return function seam(phase: string, msg: string) {
93
+ if (GUARD_PHASES.has(phase)) {
94
+ log(`\n${GUARD_LINE}`);
95
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
96
+ log(`${GUARD_LINE}\n`);
97
+ } else {
98
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
99
+ }
100
+ const ts = Date.now();
101
+ const payload = JSON.stringify({ phase, msg, ts });
102
+ // SSE 브로드캐스트
103
+ const encoder = new TextEncoder();
104
+ const sseData = encoder.encode(`data: ${payload}\n\n`);
105
+ const dead: ReadableStreamDefaultController<Uint8Array>[] = [];
106
+ for (const controller of state.sseClients) {
107
+ try { controller.enqueue(sseData); } catch { dead.push(controller); }
108
+ }
109
+ for (const c of dead) state.sseClients.delete(c);
110
+ // v1.9.0: SEAM 이벤트 링 버퍼 (최근 200개 유지)
111
+ state.seamEventLog.push({ phase, msg, ts } as SeamEventEntry);
112
+ if (state.seamEventLog.length > 200) state.seamEventLog.shift();
113
+ // MCP 구독자에게 afd://events 업데이트 알림
114
+ subscriptionManager.dispatchResourceUpdated("afd://events");
115
+ };
116
+ }
117
+
118
+ // ══════════════════════════════════════════════════════════
119
+ export function main(options: DaemonOptions = {}) {
120
+ state.ecosystems = detectEcosystem(process.cwd());
121
+
122
+ const db = initDb();
123
+ _cleanupResources.db = db;
124
+
125
+ // ── Prepared statements ──
126
+ const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
127
+ const insertAntibody = db.prepare(
128
+ "INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
129
+ );
130
+ // v1.9.0: insertAntibody 래퍼 DB 삽입 후 MCP afd://antibodies 알림 발송
131
+ function insertAntibodyAndNotify(...args: unknown[]) {
132
+ insertAntibody.run(...args);
133
+ subscriptionManager.dispatchResourceUpdated("afd://antibodies");
134
+ }
135
+ const listAntibodies = db.prepare("SELECT * FROM antibodies ORDER BY created_at DESC");
136
+ const antibodyIds = db.prepare("SELECT id FROM antibodies WHERE dormant = 0");
137
+ const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
138
+ const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
139
+ const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
140
+ const findAntibodyById = db.prepare("SELECT * FROM antibodies WHERE id = ?");
141
+ const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
142
+
143
+ // Hologram stats
144
+ const getLifetime = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_lifetime WHERE id = 1");
145
+ const updateLifetime = db.prepare(
146
+ "UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
147
+ );
148
+ const upsertDaily = db.prepare(`
149
+ INSERT INTO hologram_daily (date, requests, original_chars, hologram_chars) VALUES (?, ?, ?, ?)
150
+ ON CONFLICT(date) DO UPDATE SET requests = requests + excluded.requests,
151
+ original_chars = original_chars + excluded.original_chars,
152
+ hologram_chars = hologram_chars + excluded.hologram_chars
153
+ `);
154
+ const getDailyAll = db.prepare("SELECT date, requests, original_chars, hologram_chars FROM hologram_daily ORDER BY date DESC LIMIT 7");
155
+ const purgeOldDaily = db.prepare("DELETE FROM hologram_daily WHERE date < date('now', '-7 days')");
156
+
157
+ // ── Context Savings (wsmap + pinpoint) ──
158
+ const upsertCtxDaily = db.prepare(`
159
+ INSERT INTO ctx_savings_daily (date, type, requests, original_chars, saved_chars) VALUES (?, ?, 1, ?, ?)
160
+ ON CONFLICT(date, type) DO UPDATE SET
161
+ requests = requests + 1,
162
+ original_chars = original_chars + excluded.original_chars,
163
+ saved_chars = saved_chars + excluded.saved_chars
164
+ `);
165
+ const updateCtxLifetime = db.prepare(`
166
+ UPDATE ctx_savings_lifetime SET
167
+ total_requests = total_requests + 1,
168
+ total_original_chars = total_original_chars + ?,
169
+ total_saved_chars = total_saved_chars + ?
170
+ WHERE type = ?
171
+ `);
172
+ const getCtxSavingsDaily = db.prepare(
173
+ "SELECT date, type, requests, original_chars, saved_chars FROM ctx_savings_daily ORDER BY date DESC, type"
174
+ );
175
+ const getCtxSavingsLifetime = db.prepare(
176
+ "SELECT type, total_requests, total_original_chars, total_saved_chars FROM ctx_savings_lifetime"
177
+ );
178
+
179
+ function persistCtxSavings(type: 'wsmap' | 'pinpoint', originalChars: number, savedChars: number) {
180
+ if (savedChars <= 0) return;
181
+ try {
182
+ upsertCtxDaily.run(today(), type, originalChars, savedChars);
183
+ updateCtxLifetime.run(originalChars, savedChars, type);
184
+ } catch (err) {
185
+ console.error(`[afd] Failed to persist ctx savings:`, err instanceof Error ? err.message : err);
186
+ }
187
+ }
188
+
189
+ // ── Telemetry ──
190
+ const insertTelemetry = db.prepare(
191
+ "INSERT INTO telemetry (category, action, detail, duration_ms, timestamp) VALUES (?, ?, ?, ?, ?)"
192
+ );
193
+ function trackEvent(category: string, action: string, detail?: string, durationMs?: number) {
194
+ try { insertTelemetry.run(category, action, detail ?? null, durationMs ?? null, Date.now()); } catch { /* crash-only */ }
195
+ }
196
+
197
+ // ── Mistake History (Passive Defense) ──
198
+ const insertMistakeHistory = db.prepare(
199
+ "INSERT INTO mistake_history (file_path, mistake_type, description, antibody_id, timestamp) VALUES (?, ?, ?, ?, ?)"
200
+ );
201
+ const queryMistakesByFile = db.prepare(
202
+ "SELECT mistake_type, description, timestamp FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5"
203
+ );
204
+ const deleteMistakeOverflow = db.prepare(
205
+ "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)"
206
+ );
207
+
208
+ function recordMistake(filePath: string, mistakeType: string, description: string, antibodyId?: string) {
209
+ try {
210
+ const normalizedPath = filePath.replace(/\\/g, "/");
211
+ const truncatedDesc = description.slice(0, 200);
212
+ insertMistakeHistory.run(normalizedPath, mistakeType, truncatedDesc, antibodyId ?? null, Date.now());
213
+ deleteMistakeOverflow.run(normalizedPath, normalizedPath);
214
+ const cached = state.mistakeCache.get(normalizedPath) ?? [];
215
+ cached.unshift({ mistake_type: mistakeType, description: truncatedDesc, timestamp: Date.now() });
216
+ if (cached.length > 5) cached.length = 5;
217
+ state.mistakeCache.set(normalizedPath, cached);
218
+ } catch { /* crash-only */ }
219
+ }
220
+
221
+ try {
222
+ 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 }[];
223
+ for (const row of allMistakes) {
224
+ const normalizedPath = row.file_path.replace(/\\/g, "/");
225
+ const cached = state.mistakeCache.get(normalizedPath) ?? [];
226
+ if (cached.length < 5) {
227
+ cached.push({ mistake_type: row.mistake_type, description: row.description, timestamp: row.timestamp });
228
+ state.mistakeCache.set(normalizedPath, cached);
229
+ }
230
+ }
231
+ } catch { /* crash-only — empty cache is fine */ }
232
+
233
+ function today(): string { return new Date().toISOString().slice(0, 10); }
234
+
235
+ // Load persisted hologram stats
236
+ const persisted = getLifetime.get() as { total_requests: number; total_original_chars: number; total_hologram_chars: number } | null;
237
+ if (persisted) {
238
+ state.hologramStats.totalRequests = persisted.total_requests;
239
+ state.hologramStats.totalOriginalChars = persisted.total_original_chars;
240
+ state.hologramStats.totalHologramChars = persisted.total_hologram_chars;
241
+ }
242
+
243
+ const seam = createSeamLogger(!!options.mcp);
244
+
245
+ function persistHologramStats(originalChars: number, hologramChars: number) {
246
+ state.hologramStats.totalRequests++;
247
+ state.hologramStats.totalOriginalChars += originalChars;
248
+ state.totalSavedTokens += Math.max(0, Math.floor((originalChars - hologramChars) / 4));
249
+ state.hologramStats.totalHologramChars += hologramChars;
250
+ state.hologramStats.sessionOriginalChars += originalChars;
251
+ state.hologramStats.sessionHologramChars += hologramChars;
252
+ try {
253
+ const hs = state.hologramStats;
254
+ updateLifetime.run(hs.totalRequests, hs.totalOriginalChars, hs.totalHologramChars);
255
+ upsertDaily.run(today(), 1, originalChars, hologramChars);
256
+ purgeOldDaily.run();
257
+ } catch (err) {
258
+ console.error("[afd] Failed to persist hologram stats:", err instanceof Error ? err.message : String(err));
259
+ }
260
+ }
261
+
262
+ function snapshotFile(filePath: string) {
263
+ try {
264
+ if (existsSync(filePath)) state.fileSnapshots.set(filePath, readFileSync(filePath, "utf-8"));
265
+ } catch { /* ignore */ }
266
+ }
267
+
268
+ async function safeHologram(filePath: string, source: string): Promise<string> {
269
+ try {
270
+ const result = await generateHologram(filePath, source);
271
+ persistHologramStats(result.originalLength, result.hologramLength);
272
+ return result.hologram;
273
+ } catch {
274
+ const lines = source.split("\n");
275
+ return lines.slice(0, 50).join("\n") + (lines.length > 50 ? "\n// (truncated, AST parse failed)" : "");
276
+ }
277
+ }
278
+
279
+ function quarantineFile(originalPath: string, corruptedContent: string | null): void {
280
+ try {
281
+ mkdirSync(QUARANTINE_DIR, { recursive: true });
282
+ const now = new Date();
283
+ 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")}`;
284
+ const baseName = originalPath.replace(/[\\/]/g, "_").replace(/^_+/, "");
285
+ const quarantinePath = resolve(QUARANTINE_DIR, `${ts}_${baseName}`);
286
+ writeFileSync(quarantinePath, corruptedContent ?? "DELETED", "utf-8");
287
+ seam("Quarantine", `Saved corrupted state .afd/quarantine/${ts}_${baseName}`);
288
+ // v1.9.0: 격리 로그 업데이트 + MCP 알림
289
+ state.quarantineLog.push({ path: originalPath, ts: Date.now() } as QuarantineLogEntry);
290
+ if (state.quarantineLog.length > 100) state.quarantineLog.shift();
291
+ subscriptionManager.dispatchResourceUpdated("afd://quarantine");
292
+ } catch (err) {
293
+ seam("Quarantine", `FAILED to quarantine ${originalPath}: ${err instanceof Error ? err.message : String(err)}`);
294
+ }
295
+ }
296
+
297
+ // ── Auto-Seed Antibodies ──
298
+ function seedAntibodies() {
299
+ const immuneFiles = [
300
+ { id: "IMM-001", path: ".claudeignore" },
301
+ { id: "IMM-002", path: ".claude/hooks.json" },
302
+ { id: "IMM-003", path: "CLAUDE.md" },
303
+ ];
304
+ for (const { id, path: filePath } of immuneFiles) {
305
+ if (existsSync(filePath)) {
306
+ const content = readFileSync(filePath, "utf-8");
307
+ const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
308
+ insertAntibodyAndNotify(id, "auto-seed", filePath, JSON.stringify(patches));
309
+ state.fileSnapshots.set(filePath, content);
310
+ seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
311
+ }
312
+ }
313
+ }
314
+
315
+ const selfWrites = new Set<string>();
316
+ seedAntibodies();
317
+
318
+ // ── Dynamic Immune Synthesis ──
319
+ const validatorsDir = join(process.cwd(), VALIDATORS_DIR);
320
+
321
+ async function loadValidators() {
322
+ state.customValidators.clear();
323
+ if (!existsSync(validatorsDir)) return;
324
+ let files: string[];
325
+ try { files = readdirSync(validatorsDir).filter(f => f.endsWith(".js")); } catch { return; }
326
+ for (const file of files) {
327
+ const absPath = resolve(validatorsDir, file);
328
+ try {
329
+ const mod = await import(absPath);
330
+ const fn = mod.default ?? mod;
331
+ if (typeof fn === "function") {
332
+ state.customValidators.set(file, fn as ValidatorFn);
333
+ seam("Adapt", `🧬 Validator loaded: ${file}`);
334
+ } else {
335
+ seam("Adapt", `⚠️ Validator ${file} does not export a function — skipped`);
336
+ }
337
+ } catch (err) {
338
+ seam("Adapt", `⚠️ Failed to load validator ${file}: ${err instanceof Error ? err.message : String(err)}`);
339
+ }
340
+ }
341
+ if (state.customValidators.size > 0) {
342
+ seam("Adapt", `Dynamic Immune Synthesis: ${state.customValidators.size} active validator(s)`);
343
+ }
344
+ }
345
+
346
+ loadValidators();
347
+
348
+ try {
349
+ mkdirSync(validatorsDir, { recursive: true });
350
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
351
+ const validatorWatcher = fsWatch(validatorsDir, (_eventType, filename) => {
352
+ if (!filename?.endsWith(".js")) return;
353
+ if (reloadTimer) clearTimeout(reloadTimer);
354
+ reloadTimer = setTimeout(() => { seam("Sense", `Validator change detected: ${filename} — reloading...`); loadValidators(); reloadTimer = null; }, 200);
355
+ });
356
+ _cleanupResources.validatorWatcher = validatorWatcher;
357
+ } catch (err) {
358
+ seam("Adapt", `⚠️ Cannot watch ${VALIDATORS_DIR}: ${err instanceof Error ? err.message : String(err)}`);
359
+ }
360
+
361
+ function runCustomValidators(newContent: string, filePath: string): boolean {
362
+ for (const [name, fn] of state.customValidators) {
363
+ try {
364
+ const t0 = performance.now();
365
+ const result = fn(newContent, filePath);
366
+ const elapsed = performance.now() - t0;
367
+ if (elapsed > VALIDATOR_TIMEOUT_MS) seam("Adapt", `⚠️ Validator ${name} took ${Math.round(elapsed)}ms (>${VALIDATOR_TIMEOUT_MS}ms)`);
368
+ if (result === true) { trackEvent("validator", name, filePath); seam("Adapt", `🛡️ Custom validator ${name} flagged corruption in ${filePath}`); return true; }
369
+ } catch (err) {
370
+ seam("Adapt", `⚠️ Validator ${name} threw error — ignored: ${err instanceof Error ? err.message : String(err)}`);
371
+ }
372
+ }
373
+ return false;
374
+ }
375
+
376
+ // ── Periodic cleanup ──
377
+ const tapCleanupInterval = setInterval(() => {
378
+ const now = Date.now();
379
+ for (const [file, ts] of state.firstTapTimestamps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) state.firstTapTimestamps.delete(file); }
380
+ for (const [file, ts] of corruptionTaps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) corruptionTaps.delete(file); }
381
+ }, TAP_CLEANUP_INTERVAL_MS);
382
+ _cleanupResources.interval = tapCleanupInterval;
383
+
384
+ // ── Suppression Safety ──
385
+ function isMassEvent(now: number): boolean {
386
+ state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
387
+ return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
388
+ }
389
+
390
+ function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
391
+ const t0 = performance.now();
392
+ try {
393
+ seam("Mutate", `Restoring ${fileTarget} via antibody ${antibodyId}...`);
394
+ const patches = JSON.parse(patchOp) as PatchOp[];
395
+ let bytesWritten = 0;
396
+ for (const patch of patches) {
397
+ if (patch.op === "add" && patch.value) {
398
+ const targetPath = resolve(patch.path.replace(/^\//, ""));
399
+ assertInsideWorkspace(targetPath, _ws.root);
400
+ writeFileSync(targetPath, patch.value, "utf-8");
401
+ bytesWritten += patch.value.length;
402
+ }
403
+ }
404
+ const healMs = Math.round(performance.now() - t0);
405
+ state.autoHealCount++;
406
+ state.totalFileBytesSaved += bytesWritten;
407
+ state.autoHealLog.push({ id: antibodyId, at: Date.now(), file: fileTarget.split("/").pop() ?? fileTarget, healMs });
408
+ trackEvent("immune", "heal_hit", JSON.stringify({ antibodyId, fileTarget, bytesWritten, healMs }));
409
+ recordMistake(fileTarget, "file-deleted", `File deleted and restored via antibody ${antibodyId}`, antibodyId);
410
+ if (state.autoHealLog.length > 100) state.autoHealLog.shift();
411
+ const metrics = calcHealMetrics(bytesWritten, healMs);
412
+ const boast = maybeHealBoast(5);
413
+ const fileName = fileTarget.split("/").pop() ?? fileTarget;
414
+ (options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
415
+ // v1.9.0: 치유 완료 MCP 알림 (notifications/message)
416
+ subscriptionManager.dispatchMessage("warning", `[afd] ${fileTarget} 파일의 자가 치유가 완료되었습니다`);
417
+ } catch (err) {
418
+ seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
419
+ }
420
+ }
421
+
422
+ function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
423
+ state.recentUnlinks.push(now);
424
+ insertUnlinkLog.run(filePath, now);
425
+ if (isMassEvent(now)) { state.suppressionSkippedCount++; trackEvent("immune", "suppression", JSON.stringify({ filePath })); state.firstTapTimestamps.clear(); return null; }
426
+ const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
427
+ if (!antibody) return null;
428
+ const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
429
+ if (!fullAntibody) return null;
430
+ const previousTap = state.firstTapTimestamps.get(filePath);
431
+ if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
432
+ setAntibodyDormant.run(antibody.id);
433
+ state.firstTapTimestamps.delete(filePath);
434
+ state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
435
+ if (state.dormantTransitions.length > 100) state.dormantTransitions.shift();
436
+ trackEvent("immune", "dormant", JSON.stringify({ antibodyId: antibody.id }));
437
+ (options.mcp ? console.error : console.log)(formatDormantLog(antibody.id));
438
+ return "dormant";
439
+ }
440
+ state.firstTapTimestamps.set(filePath, now);
441
+ quarantineFile(filePath, null);
442
+ autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
443
+ return "healed";
444
+ }
445
+
446
+ // ── Smart Discovery + Watcher ──
447
+ const discovery = discoverWatchTargets(WATCH_TARGETS);
448
+ seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
449
+ if (discovery.discoveredCount > 0) {
450
+ seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
451
+ }
452
+
453
+ const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
454
+ _cleanupResources.watcher = watcher;
455
+
456
+ const immuneMap: Record<string, string> = { ".claudeignore": "IMM-001", ".claude/hooks.json": "IMM-002", "CLAUDE.md": "IMM-003" };
457
+ const corruptionTaps = new Map<string, number>();
458
+
459
+ function isInternalPath(p: string): boolean {
460
+ const normalized = p.replace(/\\/g, "/");
461
+ return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
462
+ }
463
+
464
+ function isCorrupted(oldContent: string, newContent: string, filePath: string): boolean {
465
+ if (runCustomValidators(newContent, filePath)) return true;
466
+ const trimmed = newContent.trim();
467
+ if (trimmed.length === 0) return true;
468
+ if (filePath.endsWith(".json")) {
469
+ if (trimmed === "{}" || trimmed === "[]") return true;
470
+ try { JSON.parse(newContent); } catch { return true; }
471
+ }
472
+ if (oldContent.length > 50 && newContent.length < oldContent.length * 0.1) return true;
473
+ return false;
474
+ }
475
+
476
+ // ── Event Batcher (Adaptive Debounce) ──
477
+ const eventBatcher = new EventBatcher({
478
+ debounceMs: 300,
479
+ isImmunePath: (p: string) => {
480
+ const normalized = p.replace(/\\/g, "/");
481
+ return normalized in immuneMap;
482
+ },
483
+ onImmediate: (event: string, path: string) => handleFileEvent(event, path),
484
+ onBatch: (events) => {
485
+ if (events.length > 1) {
486
+ seam("Sense", `[Batch] Processing ${events.length} events as single batch`);
487
+ }
488
+ for (const e of events) handleFileEvent(e.event, e.path);
489
+ },
490
+ });
491
+ _cleanupResources.eventBatcher = eventBatcher;
492
+
493
+ // ── Watcher Event Handler (S.E.A.M) ──
494
+ watcher.on("all", (event, path) => {
495
+ if (isInternalPath(path)) return;
496
+ if (selfWrites.has(path)) return;
497
+ eventBatcher.push(event, path);
498
+ });
499
+
500
+ function handleFileEvent(event: string, path: string) {
501
+ const _seamStart = performance.now();
502
+
503
+ state.filesDetected++;
504
+ state.lastEvent = `${event}:${path}`;
505
+ state.lastEventAt = Date.now();
506
+ state.watchedFiles.add(path);
507
+ insertEvent.run(event, path, Date.now());
508
+ // v1.9.0: 구독된 afd://history/{path} 리소스에 업데이트 알림
509
+ {
510
+ const normPath = path.replace(/\\/g, "/");
511
+ subscriptionManager.dispatchResourceUpdated(`afd://history/${normPath}`);
512
+ }
513
+
514
+ if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
515
+
516
+ if (event === "unlink") {
517
+ seam("Sense", `unlink → ${path}`);
518
+ state.fileSnapshots.delete(path);
519
+ state.watchedFiles.delete(path);
520
+ const result = handleUnlink(path, Date.now());
521
+ if (result === "healed") {
522
+ selfWrites.add(path);
523
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
524
+ seam("Mutate", `Restored ${path} from antibody snapshot`);
525
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
526
+ snapshotFile(path);
527
+ watcher.add(path);
528
+ } else if (result === "dormant") {
529
+ seam("Adapt", `Double-tap confirmed — user intentionally deleted ${path}, antibody deactivated`);
530
+ }
531
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
532
+ return;
533
+ }
534
+
535
+ if (event === "change") {
536
+ if (!existsSync(path)) return;
537
+ let newContent: string;
538
+ try { newContent = readFileSync(path, "utf-8"); } catch { return; }
539
+ const oldContent = state.fileSnapshots.get(path);
540
+ const newSize = newContent.length;
541
+
542
+ if (oldContent !== undefined && oldContent !== newContent) {
543
+ if (isAstSupported(path)) {
544
+ try {
545
+ const sdiff = semanticDiff(path, oldContent, newContent);
546
+ const breakingTag = sdiff.hasBreakingChanges ? " ⚠️ BREAKING" : "";
547
+ seam("Sense", `change → ${path} (${newSize} bytes)${breakingTag}\n [semantic] ${sdiff.summary}`);
548
+ } catch {
549
+ const diffs = lineDiff(oldContent, newContent);
550
+ seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
551
+ }
552
+ } else {
553
+ const diffs = lineDiff(oldContent, newContent);
554
+ if (diffs.length > 0) seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
555
+ else seam("Sense", `change → ${path} (${newSize} bytes, whitespace-only diff)`);
556
+ }
557
+ } else if (oldContent === undefined) {
558
+ seam("Sense", `change → ${path} (${newSize} bytes, no previous snapshot)`);
559
+ } else {
560
+ seam("Sense", `change → ${path} (${newSize} bytes, content identical — touch or metadata)`);
561
+ }
562
+
563
+ const normalizedPath = path.replace(/\\/g, "/");
564
+ const abId = immuneMap[normalizedPath];
565
+ if (abId && oldContent !== undefined) {
566
+ if (isCorrupted(oldContent, newContent, path)) {
567
+ const prevCorruption = corruptionTaps.get(path);
568
+ const now = Date.now();
569
+ if (prevCorruption && (now - prevCorruption) < DOUBLE_TAP_WINDOW_MS) {
570
+ corruptionTaps.delete(path);
571
+ state.fileSnapshots.set(path, newContent);
572
+ insertAntibodyAndNotify(abId, "auto-seed", path, JSON.stringify([{ op: "add", path: `/${path}`, value: newContent }]));
573
+ trackEvent("immune", "heal_false_positive", JSON.stringify({ filePath: path, abId }));
574
+ seam("Adapt", `Corruption double-tap on ${path} — standing down, accepting new content`);
575
+ } else {
576
+ corruptionTaps.set(path, now);
577
+ quarantineFile(path, newContent);
578
+ selfWrites.add(path);
579
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
580
+ writeFileSync(resolve(path), oldContent, "utf-8");
581
+ trackEvent("immune", "heal_hit", JSON.stringify({ filePath: path, abId }));
582
+ recordMistake(path, "corruption", `Silent corruption detected (${oldContent.length} → ${newSize} bytes) — restored`, abId);
583
+ seam("Mutate", `Silent corruption detected in ${path} (${oldContent.length} → ${newSize} bytes) — restored from snapshot`);
584
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
585
+ }
586
+ } else {
587
+ state.fileSnapshots.set(path, newContent);
588
+ const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: newContent }];
589
+ insertAntibodyAndNotify(abId, "auto-seed", path, JSON.stringify(patches));
590
+ trackEvent("immune", "heal_pass", JSON.stringify({ filePath: path, abId }));
591
+ seam("Adapt", `Antibody ${abId} updated: stored latest ${path} (${newSize} bytes) for auto-restore`);
592
+ }
593
+ } else {
594
+ state.fileSnapshots.set(path, newContent);
595
+ }
596
+ trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
597
+ return;
598
+ }
599
+
600
+ seam("Sense", `${event} → ${path}`);
601
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
602
+ }
603
+
604
+ // ── Workspace Map ──
605
+ const wsMap = createWorkspaceMap();
606
+ _cleanupResources.wsMapGetTimer = wsMap.getTimer;
607
+ watcher.on("add", () => wsMap.markDirty());
608
+ watcher.on("unlink", () => wsMap.markDirty());
609
+ wsMap.get(); // initial build
610
+
611
+ // ── Build DaemonContext ──
612
+ const ctx: DaemonContext = {
613
+ state, db: db as unknown as DaemonContext["db"], ws: _ws, options,
614
+ insertEvent, insertAntibody, listAntibodies, antibodyIds: antibodyIds as unknown as DaemonContext["antibodyIds"],
615
+ countAntibodies: countAntibodies as unknown as DaemonContext["countAntibodies"],
616
+ getDailyAll: getDailyAll as unknown as DaemonContext["getDailyAll"],
617
+ insertTelemetry: insertTelemetry as unknown as DaemonContext["insertTelemetry"],
618
+ insertMistakeHistory: insertMistakeHistory as unknown as DaemonContext["insertMistakeHistory"],
619
+ queryMistakesByFile: queryMistakesByFile as unknown as DaemonContext["queryMistakesByFile"],
620
+ deleteMistakeOverflow: deleteMistakeOverflow as unknown as DaemonContext["deleteMistakeOverflow"],
621
+ seam, persistHologramStats, persistCtxSavings, safeHologram,
622
+ getWorkspaceMap: wsMap.get, getWorkspaceMapStats: wsMap.getLastBuildStats,
623
+ today, discoveryTargets: discovery.targets,
624
+ getCtxSavingsDaily: getCtxSavingsDaily as unknown as DaemonContext["getCtxSavingsDaily"],
625
+ getCtxSavingsLifetime: getCtxSavingsLifetime as unknown as DaemonContext["getCtxSavingsLifetime"],
626
+ port: 0,
627
+ };
628
+
629
+ // ── MCP Mode ──
630
+ if (options.mcp) {
631
+ startMcpStdio(ctx);
632
+ return;
633
+ }
634
+
635
+ // ── HTTP Server ──
636
+ const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
637
+ const port = server.port;
638
+ ctx.port = port;
639
+
640
+ mkdirSync(_ws.afdDir, { recursive: true });
641
+ writeFileSync(_ws.pidFile, String(process.pid));
642
+ writeFileSync(_ws.portFile, String(port));
643
+ try { registerMesh(_ws.root, port, process.pid); } catch {}
644
+
645
+ console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
646
+
647
+ process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
648
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
649
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
650
+ }
651
+
652
+ if (import.meta.main) {
653
+ const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
654
+ main({ mcp });
655
+ }