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
@@ -0,0 +1,404 @@
1
+ import { getDaemonInfo, daemonRequest } from "../daemon/client";
2
+ import { fmtNum, visualWidth } from "../core/boast";
3
+
4
+ // ── Types ────────────────────────────────────────────────────────────────────
5
+
6
+ interface HologramEntry {
7
+ requests: number;
8
+ originalChars: number;
9
+ hologramChars: number;
10
+ savings: number;
11
+ }
12
+
13
+ interface HologramDailyRow {
14
+ date: string;
15
+ requests: number;
16
+ originalChars: number;
17
+ hologramChars: number;
18
+ }
19
+
20
+ interface HologramScore {
21
+ lifetime: HologramEntry;
22
+ today: HologramEntry | null;
23
+ daily: HologramDailyRow[];
24
+ }
25
+
26
+ interface CtxSavingsRow {
27
+ date: string;
28
+ type: string;
29
+ requests: number;
30
+ original_chars: number;
31
+ saved_chars: number;
32
+ }
33
+
34
+ interface CtxSavingsLifetimeRow {
35
+ type: string;
36
+ total_requests: number;
37
+ total_original_chars: number;
38
+ total_saved_chars: number;
39
+ }
40
+
41
+ interface ScoreData {
42
+ uptime: number;
43
+ totalEvents: number;
44
+ hologram: HologramScore;
45
+ ctxSavings: {
46
+ daily: CtxSavingsRow[];
47
+ lifetime: CtxSavingsLifetimeRow[];
48
+ };
49
+ }
50
+
51
+ // ── Locale ───────────────────────────────────────────────────────────────────
52
+
53
+ function detectKorean(): boolean {
54
+ const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
55
+ if (/ko[_\-]/i.test(lang)) return true;
56
+ try {
57
+ return Intl.DateTimeFormat().resolvedOptions().locale.startsWith("ko");
58
+ } catch { return false; }
59
+ }
60
+
61
+ const isKo = detectKorean();
62
+
63
+ const T = isKo ? {
64
+ title: "afd 토큰 대시보드",
65
+ todaySavings: "오늘의 절약",
66
+ lifetimeRoi: "누적 ROI & 분류",
67
+ weekHistory: "최근 7일 내역",
68
+ systemStatus: "시스템 상태",
69
+ startTracking:"afd_read 또는 afd_hologram 사용 시 추적 시작",
70
+ noHistory: "아직 일별 기록이 없습니다",
71
+ totalSaved: "총 절약량",
72
+ estValue: "추정 가치",
73
+ hologram: "홀로그램",
74
+ wsmap: "워크스페이스 맵",
75
+ pinpoint: "핀포인트",
76
+ requests: "요청",
77
+ uptime: "가동시간",
78
+ events: "이벤트",
79
+ updated: "갱신",
80
+ exitHint: "Ctrl+C로 종료",
81
+ labelOrig: "원본 ",
82
+ labelAct: "실제 ",
83
+ labelSaved: "절약 ",
84
+ savedSuffix: "절약됨",
85
+ todayLabel: "오늘",
86
+ } : {
87
+ title: "afd token dashboard",
88
+ todaySavings: "TODAY'S SAVINGS",
89
+ lifetimeRoi: "LIFETIME ROI & BREAKDOWN",
90
+ weekHistory: "7-DAY HISTORY",
91
+ systemStatus: "SYSTEM STATUS",
92
+ startTracking:"Use afd_read or afd_hologram to start tracking",
93
+ noHistory: "No daily history yet",
94
+ totalSaved: "Total Saved",
95
+ estValue: "Est. Value",
96
+ hologram: "Hologram",
97
+ wsmap: "W/S Map",
98
+ pinpoint: "Pinpoint",
99
+ requests: "Requests",
100
+ uptime: "Uptime",
101
+ events: "Events",
102
+ updated: "Updated",
103
+ exitHint: "Press Ctrl+C to exit",
104
+ labelOrig: "Original ",
105
+ labelAct: "Actual ",
106
+ labelSaved: "Saved ",
107
+ savedSuffix: "saved",
108
+ todayLabel: "Today",
109
+ };
110
+
111
+ // ── ANSI ──────────────────────────────────────────────────────────────────────
112
+
113
+ const C = {
114
+ reset: "\x1b[0m",
115
+ bold: "\x1b[1m",
116
+ dim: "\x1b[2m",
117
+ red: "\x1b[31m",
118
+ green: "\x1b[32m",
119
+ yellow: "\x1b[33m",
120
+ cyan: "\x1b[36m",
121
+ };
122
+
123
+ const CHARS_PER_TOKEN = 3.5;
124
+ const W = 58;
125
+ const INNER = W - 2;
126
+ const HBAR = "─".repeat(W);
127
+
128
+ function vw(s: string): number {
129
+ return visualWidth(s.replace(/\x1b\[[0-9;]*m/g, ""));
130
+ }
131
+
132
+ function row(content: string): string {
133
+ const pad = Math.max(0, INNER - vw(content));
134
+ return `│${content}${" ".repeat(pad)}│`;
135
+ }
136
+
137
+ function divider(): string {
138
+ return `├${HBAR}┤`;
139
+ }
140
+
141
+ function formatK(chars: number): string {
142
+ const tok = chars / CHARS_PER_TOKEN;
143
+ if (tok >= 1_000_000) return `${(tok / 1_000_000).toFixed(1)}M tok`;
144
+ if (tok >= 1_000) return `${(tok / 1_000).toFixed(1)}K tok`;
145
+ return `${Math.round(tok)} tok`;
146
+ }
147
+
148
+ function formatUptime(s: number): string {
149
+ if (s < 60) return `${s}s`;
150
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
151
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
152
+ }
153
+
154
+ function weekday(dateStr: string, todayStr: string): string {
155
+ if (dateStr === todayStr) return T.todayLabel;
156
+ try {
157
+ const locale = isKo ? "ko" : "en";
158
+ return new Date(dateStr + "T12:00:00").toLocaleDateString(locale, { weekday: "short" });
159
+ } catch { return ""; }
160
+ }
161
+
162
+ // ── Bar helpers ───────────────────────────────────────────────────────────────
163
+
164
+ function barSaved(pct: number, width: number): string {
165
+ const filled = Math.min(width, Math.round((pct / 100) * width));
166
+ const empty = width - filled;
167
+ return `${C.green}${"▓".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
168
+ }
169
+
170
+ function barActual(pct: number, width: number): string {
171
+ const filled = Math.min(width, Math.round((pct / 100) * width));
172
+ const empty = width - filled;
173
+ return `${C.yellow}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
174
+ }
175
+
176
+ function barOriginal(width: number): string {
177
+ return `${C.dim}${"█".repeat(width)}${C.reset}`;
178
+ }
179
+
180
+ // ── Render ────────────────────────────────────────────────────────────────────
181
+
182
+ function render(score: ScoreData, lastUpdated: string): void {
183
+ process.stdout.write("\x1b[2J\x1b[H");
184
+
185
+ const out: string[] = [];
186
+ const today = new Date().toISOString().slice(0, 10);
187
+ const h = score.hologram;
188
+ const ctx = score.ctxSavings ?? { daily: [], lifetime: [] };
189
+
190
+ // ── Header ──
191
+ out.push(`┌${HBAR}┐`);
192
+ {
193
+ const liveBadge = `${C.red}[● live]${C.reset}`;
194
+ const dateStr = `${C.dim}${today}${C.reset}`;
195
+ const title = ` ${C.cyan}${C.bold}${T.title}${C.reset} ${liveBadge} ${dateStr}`;
196
+ out.push(row(title));
197
+ }
198
+ out.push(divider());
199
+
200
+ // ── TODAY'S SAVINGS ──
201
+ // Denominator = ALL bytes processed by afd tools (hologram_daily includes small files
202
+ // since afd_read now records even full-content reads).
203
+ {
204
+ out.push(row(` ${C.cyan}${C.bold}${T.todaySavings}${C.reset}`));
205
+
206
+ const hToday = h.today ?? (h.daily[0]?.date === today ? h.daily[0] : null);
207
+ // holoOriginal = total bytes that went through afd_read (small files + large files)
208
+ // holoActual = bytes actually sent (small files at full size + large files at hologram size)
209
+ const holoOriginal = hToday?.originalChars ?? 0;
210
+ const holoActual = hToday?.hologramChars ?? 0;
211
+ const holoRequests = hToday?.requests ?? 0;
212
+
213
+ const wsmapToday = ctx.daily.find(r => r.date === today && r.type === "wsmap");
214
+ const pinpointToday = ctx.daily.find(r => r.date === today && r.type === "pinpoint");
215
+ const wsmapOriginal = wsmapToday?.original_chars ?? 0;
216
+ const wsmapSavedChars = wsmapToday?.saved_chars ?? 0;
217
+ const pinpointOriginal = pinpointToday?.original_chars ?? 0;
218
+ const pinpointSavedChars = pinpointToday?.saved_chars ?? 0;
219
+
220
+ // totalOriginal = what WOULD have been consumed without afd
221
+ // totalActual = what WAS actually consumed through afd
222
+ const totalOriginal = holoOriginal + wsmapOriginal + pinpointOriginal;
223
+ const totalActual = holoActual + (wsmapOriginal - wsmapSavedChars) + (pinpointOriginal - pinpointSavedChars);
224
+ const hasData = holoRequests > 0 || (wsmapToday?.requests ?? 0) > 0 || (pinpointToday?.requests ?? 0) > 0;
225
+
226
+ if (hasData && totalOriginal > 0) {
227
+ const barW = 20;
228
+ const savedTok = Math.max(0, totalOriginal - totalActual);
229
+ const savedPct = Math.round((savedTok / totalOriginal) * 100);
230
+ const actPct = (totalActual / totalOriginal) * 100;
231
+
232
+ out.push(row(` ${T.labelOrig}${barOriginal(barW)} ${C.dim}${formatK(totalOriginal)}${C.reset}`));
233
+ out.push(row(` ${T.labelAct}${barActual(actPct, barW)} ${C.yellow}${formatK(totalActual)}${C.reset}`));
234
+ out.push(row(` ${T.labelSaved}${barSaved(savedPct, barW)} ${C.green}${savedPct}%${C.reset} ${C.dim}(${formatK(savedTok)} ${T.savedSuffix})${C.reset}`));
235
+ } else {
236
+ out.push(row(` ${C.dim}${T.startTracking}${C.reset}`));
237
+ }
238
+ }
239
+
240
+ out.push(divider());
241
+
242
+ // ── LIFETIME ROI & BREAKDOWN ──
243
+ {
244
+ out.push(row(` ${C.cyan}${C.bold}${T.lifetimeRoi}${C.reset}`));
245
+
246
+ const lt = h.lifetime;
247
+ // hologramSavedChars = compression savings only (small files cancel out: orig == actual)
248
+ const hologramSavedChars = Math.max(0, lt.originalChars - lt.hologramChars);
249
+
250
+ const wsmapRow = ctx.lifetime.find(r => r.type === "wsmap");
251
+ const pinpointRow = ctx.lifetime.find(r => r.type === "pinpoint");
252
+ const wsmapSaved = wsmapRow?.total_saved_chars ?? 0;
253
+ const pinpointSaved = pinpointRow?.total_saved_chars ?? 0;
254
+ const totalSavedChars = hologramSavedChars + wsmapSaved + pinpointSaved;
255
+ const totalSavedTok = totalSavedChars / CHARS_PER_TOKEN;
256
+ const estValue = Math.round(totalSavedTok / 1000 * 0.003 * 100) / 100;
257
+
258
+ const totalLine = ` ${C.bold}${C.green}${T.totalSaved} ~${fmtNum(Math.round(totalSavedTok))} tok${C.reset} ${C.dim}│ ${T.estValue} $${estValue.toFixed(2)}${C.reset}`;
259
+ out.push(row(totalLine));
260
+ out.push(row(` ${C.dim}${"─".repeat(INNER - 4)}${C.reset}`));
261
+
262
+ const holoTok = Math.round(hologramSavedChars / CHARS_PER_TOKEN);
263
+ const holoColor = holoTok > 0 ? C.green : C.dim;
264
+ const holoPct = totalSavedChars > 0 ? Math.round((hologramSavedChars / totalSavedChars) * 100) : 0;
265
+ out.push(row(` ${holoColor}[${holoTok > 0 ? "✓" : "·"}] ${T.hologram.padEnd(12)} ~${formatK(hologramSavedChars).padEnd(9)} (${holoPct}%)${C.reset}`));
266
+
267
+ const wsmapTok = Math.round(wsmapSaved / CHARS_PER_TOKEN);
268
+ const wsmapColor = wsmapTok > 0 ? C.green : C.dim;
269
+ const wsmapPct = totalSavedChars > 0 ? Math.round((wsmapSaved / totalSavedChars) * 100) : 0;
270
+ out.push(row(` ${wsmapColor}[${wsmapTok > 0 ? "✓" : "·"}] ${T.wsmap.padEnd(12)} ~${formatK(wsmapSaved).padEnd(9)} (${wsmapPct}%)${C.reset}`));
271
+
272
+ const pinTok = Math.round(pinpointSaved / CHARS_PER_TOKEN);
273
+ const pinColor = pinTok > 0 ? C.green : C.dim;
274
+ const pinPct = totalSavedChars > 0 ? Math.round((pinpointSaved / totalSavedChars) * 100) : 0;
275
+ out.push(row(` ${pinColor}[${pinTok > 0 ? "✓" : "·"}] ${T.pinpoint.padEnd(12)} ~${formatK(pinpointSaved).padEnd(9)} (${pinPct}%)${C.reset}`));
276
+ }
277
+
278
+ out.push(divider());
279
+
280
+ // ── 7-DAY HISTORY ──
281
+ {
282
+ out.push(row(` ${C.cyan}${C.bold}${T.weekHistory}${C.reset}`));
283
+
284
+ // Merge hologram daily (which now includes small file reads) + wsmap/pinpoint daily
285
+ const dailyMap = new Map<string, { original: number; saved: number }>();
286
+ for (const d of h.daily) {
287
+ dailyMap.set(d.date, { original: d.originalChars, saved: d.originalChars - d.hologramChars });
288
+ }
289
+ for (const r of ctx.daily) {
290
+ const entry = dailyMap.get(r.date) ?? { original: 0, saved: 0 };
291
+ dailyMap.set(r.date, { original: entry.original + r.original_chars, saved: entry.saved + r.saved_chars });
292
+ }
293
+
294
+ const sortedDates = [...dailyMap.keys()].sort().reverse().slice(0, 7);
295
+
296
+ if (sortedDates.length > 0) {
297
+ for (const date of sortedDates) {
298
+ const { original, saved } = dailyMap.get(date)!;
299
+ const pct = original > 0 ? Math.round((saved / original) * 100) : 0;
300
+ const barW = 14;
301
+ const filled = Math.min(barW, Math.round((pct / 100) * barW));
302
+ const empty = barW - filled;
303
+ const barColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
304
+ const bar = `${barColor}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
305
+ const wd = weekday(date, today);
306
+ const dateLabel = `${C.dim}${date.slice(5)}${C.reset} ${C.dim}(${wd})${C.reset}`;
307
+ const pctColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
308
+ const tokRange = `${C.dim}${formatK(original)} → ${formatK(original - saved)}${C.reset}`;
309
+ out.push(row(` ${dateLabel} ${bar} ${pctColor}${pct}%${C.reset} │ ${tokRange}`));
310
+ }
311
+ } else {
312
+ out.push(row(` ${C.dim}${T.noHistory}${C.reset}`));
313
+ }
314
+ }
315
+
316
+ out.push(divider());
317
+
318
+ // ── SYSTEM STATUS ──
319
+ {
320
+ out.push(row(` ${C.cyan}${C.bold}${T.systemStatus}${C.reset}`));
321
+ const lt = h.lifetime;
322
+ const reqStr = `${T.requests}: ${lt.requests}`;
323
+ const uptStr = `${T.uptime}: ${formatUptime(score.uptime)}`;
324
+ const evtStr = `${T.events}: ${score.totalEvents}`;
325
+ out.push(row(` ${C.dim}${reqStr} │ ${uptStr} │ ${evtStr}${C.reset}`));
326
+ }
327
+
328
+ out.push(`└${HBAR}┘`);
329
+ out.push(` ${C.dim}${T.updated}: ${lastUpdated} | ${T.exitHint}${C.reset}`);
330
+
331
+ process.stdout.write(out.join("\n") + "\n");
332
+ }
333
+
334
+ // ── Live loop ─────────────────────────────────────────────────────────────────
335
+
336
+ export async function dashboardCommand(): Promise<void> {
337
+ const info = getDaemonInfo();
338
+ if (!info) {
339
+ const msg = isKo
340
+ ? `[afd] 데몬이 실행 중이 아닙니다. \`afd start\`를 먼저 실행하세요.`
341
+ : `[afd] Daemon not running. Run \`afd start\` first.`;
342
+ console.error(`${C.red}${msg}${C.reset}`);
343
+ process.exit(1);
344
+ }
345
+
346
+ const ac = new AbortController();
347
+
348
+ process.on("exit", () => process.stdout.write("\x1b[?25h"));
349
+ const cleanup = () => { ac.abort(); process.exit(0); };
350
+ process.on("SIGINT", cleanup);
351
+ process.on("SIGTERM", cleanup);
352
+ process.stdout.write("\x1b[?25l");
353
+
354
+ // Initial render
355
+ try {
356
+ const score = await daemonRequest<ScoreData>("/score");
357
+ render(score, new Date().toLocaleTimeString());
358
+ } catch (err) {
359
+ process.stdout.write("\x1b[?25h");
360
+ const msg = err instanceof Error ? err.message : String(err);
361
+ console.error(`${C.red}[afd] ${msg}${C.reset}`);
362
+ process.exit(1);
363
+ }
364
+
365
+ async function doRefresh() {
366
+ try {
367
+ const score = await daemonRequest<ScoreData>("/score");
368
+ render(score, new Date().toLocaleTimeString());
369
+ } catch { /* non-fatal */ }
370
+ }
371
+
372
+ const pollTimer = setInterval(doRefresh, 3000);
373
+ ac.signal.addEventListener("abort", () => clearInterval(pollTimer));
374
+
375
+ // SSE for instant updates
376
+ (async () => {
377
+ while (!ac.signal.aborted) {
378
+ try {
379
+ const res = await fetch(`http://127.0.0.1:${info.port}/events`, { signal: ac.signal });
380
+ if (!res.body) { await new Promise(r => setTimeout(r, 3000)); continue; }
381
+
382
+ const reader = res.body.getReader();
383
+ const dec = new TextDecoder();
384
+ let buf = "";
385
+
386
+ while (!ac.signal.aborted) {
387
+ const { value, done } = await reader.read();
388
+ if (done) break;
389
+ buf += dec.decode(value, { stream: true });
390
+ const frames = buf.split("\n\n");
391
+ buf = frames.pop() ?? "";
392
+ for (const frame of frames) {
393
+ if (frame.trim() && frame.includes("data:")) await doRefresh();
394
+ }
395
+ }
396
+ } catch {
397
+ if (ac.signal.aborted) break;
398
+ await new Promise(r => setTimeout(r, 3000));
399
+ }
400
+ }
401
+ })();
402
+
403
+ await new Promise<void>(resolve => ac.signal.addEventListener("abort", resolve));
404
+ }
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync, mkdirSync, existsSync } from "fs";
2
2
  import { dirname } from "path";
3
- import { daemonRequest } from "../daemon/client";
3
+ import { daemonRequest, getDaemonInfo } from "../daemon/client";
4
4
  import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
5
5
  import { notifyAutoHeal } from "../core/notify";
6
6
 
@@ -18,6 +18,9 @@ interface AutoHealResponse {
18
18
  function applyPatch(patch: PatchOp): boolean {
19
19
  const filePath = patch.path.replace(/^\//, "");
20
20
 
21
+ // Guard: reject path traversal attempts
22
+ if (filePath.includes("..") || filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath)) return false;
23
+
21
24
  if (patch.op === "add") {
22
25
  if (existsSync(filePath)) return false;
23
26
  const dir = dirname(filePath);
@@ -52,10 +55,50 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
52
55
  process.exit(1);
53
56
  }
54
57
 
58
+ // Helper: fetch past mistakes for passive defense injection
59
+ async function fetchPastMistakes(): Promise<string[]> {
60
+ if (!isA2A) return [];
61
+ try {
62
+ const info = getDaemonInfo();
63
+ if (!info) return [];
64
+ // Query all recent mistakes (not file-specific in healthy path)
65
+ const resp = await fetch(`http://127.0.0.1:${info.port}/mistake-history?file=*`, {
66
+ signal: AbortSignal.timeout(500),
67
+ });
68
+ // Fall back to empty if the wildcard isn't supported
69
+ return [];
70
+ } catch { return []; }
71
+ }
72
+
73
+ async function fetchMistakesForFiles(files: string[]): Promise<string[]> {
74
+ if (!isA2A) return [];
75
+ const warnings: string[] = [];
76
+ try {
77
+ const info = getDaemonInfo();
78
+ if (!info) return [];
79
+ for (const file of files.slice(0, 3)) {
80
+ try {
81
+ const resp = await fetch(`http://127.0.0.1:${info.port}/mistake-history?file=${encodeURIComponent(file)}`, {
82
+ signal: AbortSignal.timeout(500),
83
+ });
84
+ const data = await resp.json() as { mistakes: { mistake_type: string; description: string }[] };
85
+ for (const m of data.mistakes.slice(0, 3)) {
86
+ warnings.push(`Previous mistake on ${file}: '${m.description}'. Be careful.`.slice(0, 200));
87
+ }
88
+ } catch { /* skip this file */ }
89
+ }
90
+ } catch { /* crash-only */ }
91
+ return warnings;
92
+ }
93
+
55
94
  // No symptoms — nothing to do
56
95
  if (diagnosis.symptoms.length === 0) {
57
96
  if (isA2A) {
58
- console.log(JSON.stringify({ status: "healthy", symptoms: [], healed: [] }));
97
+ const output: Record<string, unknown> = { status: "healthy", symptoms: [], healed: [] };
98
+ // Inject past mistakes even when healthy (proactive warning)
99
+ const pastMistakes = await fetchPastMistakes();
100
+ if (pastMistakes.length > 0) output.pastMistakes = pastMistakes;
101
+ console.log(JSON.stringify(output));
59
102
  } else {
60
103
  console.log("[afd diagnose] System healthy.");
61
104
  }
@@ -116,15 +159,15 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
116
159
  if (applied) {
117
160
  // Notify daemon of auto-heal event
118
161
  try {
119
- await fetch(
120
- `http://127.0.0.1:${getDaemonPort()}/auto-heal/record`,
121
- {
162
+ const info = getDaemonInfo();
163
+ if (info) {
164
+ await fetch(`http://127.0.0.1:${info.port}/auto-heal/record`, {
122
165
  method: "POST",
123
166
  headers: { "Content-Type": "application/json" },
124
167
  body: JSON.stringify({ id: symptom.id }),
125
168
  signal: AbortSignal.timeout(1000),
126
- }
127
- );
169
+ });
170
+ }
128
171
  } catch {
129
172
  // Non-critical — don't block
130
173
  }
@@ -137,15 +180,14 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
137
180
  }
138
181
 
139
182
  if (isA2A) {
140
- console.log(JSON.stringify({ status: healed.length > 0 ? "healed" : "no-action", healed, skipped }));
183
+ const output: Record<string, unknown> = { status: healed.length > 0 ? "healed" : "no-action", healed, skipped };
184
+ // Inject past mistakes for healed files (passive defense)
185
+ const affectedFiles = diagnosis.symptoms.map(s => s.fileTarget ?? s.id).filter(Boolean);
186
+ const pastMistakes = await fetchMistakesForFiles(affectedFiles);
187
+ if (pastMistakes.length > 0) output.pastMistakes = pastMistakes;
188
+ console.log(JSON.stringify(output));
141
189
  } else {
142
190
  if (healed.length > 0) console.log(`[afd diagnose] Auto-healed: ${healed.join(", ")}`);
143
191
  if (skipped.length > 0) console.log(`[afd diagnose] Skipped (unknown): ${skipped.join(", ")}`);
144
192
  }
145
193
  }
146
-
147
- function getDaemonPort(): number {
148
- const { readFileSync } = require("fs");
149
- const { PORT_FILE } = require("../constants");
150
- return parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
151
- }