autonomous-flow-daemon 1.0.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 (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +142 -125
  3. package/README.md +119 -134
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +247 -35
  6. package/src/cli.ts +79 -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/lang.ts +41 -0
  14. package/src/commands/mcp.ts +129 -0
  15. package/src/commands/restart.ts +14 -0
  16. package/src/commands/score.ts +192 -64
  17. package/src/commands/start.ts +137 -37
  18. package/src/commands/stats.ts +103 -0
  19. package/src/commands/status.ts +157 -0
  20. package/src/commands/stop.ts +42 -9
  21. package/src/commands/sync.ts +253 -20
  22. package/src/commands/vaccine.ts +177 -0
  23. package/src/constants.ts +26 -1
  24. package/src/core/boast.ts +280 -0
  25. package/src/core/config.ts +49 -0
  26. package/src/core/db.ts +74 -3
  27. package/src/core/discovery.ts +65 -0
  28. package/src/core/evolution.ts +215 -0
  29. package/src/core/hologram/engine.ts +71 -0
  30. package/src/core/hologram/fallback.ts +11 -0
  31. package/src/core/hologram/incremental.ts +227 -0
  32. package/src/core/hologram/py-extractor.ts +132 -0
  33. package/src/core/hologram/ts-extractor.ts +320 -0
  34. package/src/core/hologram/types.ts +25 -0
  35. package/src/core/hologram.ts +64 -236
  36. package/src/core/hook-manager.ts +259 -0
  37. package/src/core/i18n/messages.ts +309 -0
  38. package/src/core/immune.ts +8 -123
  39. package/src/core/locale.ts +88 -0
  40. package/src/core/log-rotate.ts +33 -0
  41. package/src/core/log-utils.ts +38 -0
  42. package/src/core/lru-map.ts +61 -0
  43. package/src/core/notify.ts +53 -14
  44. package/src/core/rule-engine.ts +287 -0
  45. package/src/core/semantic-diff.ts +432 -0
  46. package/src/core/telemetry.ts +94 -0
  47. package/src/core/vaccine-registry.ts +212 -0
  48. package/src/core/workspace.ts +28 -0
  49. package/src/core/yaml-minimal.ts +176 -0
  50. package/src/daemon/client.ts +34 -6
  51. package/src/daemon/event-batcher.ts +108 -0
  52. package/src/daemon/guards.ts +13 -0
  53. package/src/daemon/http-routes.ts +293 -0
  54. package/src/daemon/mcp-handler.ts +270 -0
  55. package/src/daemon/server.ts +492 -273
  56. package/src/daemon/types.ts +100 -0
  57. package/src/daemon/workspace-map.ts +92 -0
  58. package/src/platform.ts +60 -0
  59. package/src/version.ts +15 -0
@@ -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
- }
@@ -0,0 +1,243 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { loadAllRules, evaluateRules } from "../core/rule-engine";
4
+ import type { DiagnosticRule } from "../core/rule-engine";
5
+ import type { PatchOp, Symptom } from "../core/immune";
6
+ import { notifyAutoHeal } from "../core/notify";
7
+ import { getSystemLanguage } from "../core/locale";
8
+
9
+ interface DoctorOptions {
10
+ fix?: boolean;
11
+ }
12
+
13
+ // ── i18n ──
14
+ const msgs = {
15
+ en: {
16
+ title: "afd doctor — Deep Health Analysis",
17
+ ruleCount: "Rules loaded",
18
+ builtIn: "built-in",
19
+ custom: "custom",
20
+ scanning: "Scanning project health...",
21
+ healthy: "All checks passed. Project is healthy!",
22
+ grade: "Health Grade",
23
+ passed: "Passed",
24
+ failed: "Failed",
25
+ findings: "Findings",
26
+ recommendation: "Recommendation",
27
+ fixable: "auto-fixable",
28
+ fixApplied: "Fixed",
29
+ fixSkipped: "Skipped (no patches)",
30
+ fixSummary: "Auto-fix complete",
31
+ fixHint: "Run `afd doctor --fix` to auto-fix {count} issue(s).",
32
+ severity: { critical: "CRITICAL", warning: "WARNING", info: "INFO" },
33
+ },
34
+ ko: {
35
+ title: "afd doctor — 딥 헬스 분석",
36
+ ruleCount: "로드된 규칙",
37
+ builtIn: "내장",
38
+ custom: "사용자",
39
+ scanning: "프로젝트 건강 상태 스캔 중...",
40
+ healthy: "모든 검사 통과. 프로젝트가 건강합니다!",
41
+ grade: "건강 등급",
42
+ passed: "통과",
43
+ failed: "실패",
44
+ findings: "발견 사항",
45
+ recommendation: "권고사항",
46
+ fixable: "자동 수정 가능",
47
+ fixApplied: "수정 완료",
48
+ fixSkipped: "건너뜀 (패치 없음)",
49
+ fixSummary: "자동 수정 완료",
50
+ fixHint: "`afd doctor --fix`로 {count}건 자동 수정 가능합니다.",
51
+ severity: { critical: "심각", warning: "경고", info: "정보" },
52
+ },
53
+ };
54
+
55
+ // ── Box Drawing ──
56
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
57
+ const W = 52;
58
+
59
+ function line(left: string, right: string) {
60
+ return `${left}${BOX.h.repeat(W)}${right}`;
61
+ }
62
+
63
+ function row(content: string) {
64
+ const visible = visualWidth(content);
65
+ const pad = Math.max(0, W - 2 - visible);
66
+ return `${BOX.v} ${content}${" ".repeat(pad)}${BOX.v}`;
67
+ }
68
+
69
+ function divider() {
70
+ return line(BOX.ml, BOX.mr);
71
+ }
72
+
73
+ function visualWidth(s: string): number {
74
+ let w = 0;
75
+ for (const ch of s) {
76
+ const cp = ch.codePointAt(0)!;
77
+ // CJK + emoji ranges → width 2
78
+ if (
79
+ (cp >= 0x1100 && cp <= 0x11ff) ||
80
+ (cp >= 0x2e80 && cp <= 0x9fff) ||
81
+ (cp >= 0xac00 && cp <= 0xd7af) ||
82
+ (cp >= 0xf900 && cp <= 0xfaff) ||
83
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
84
+ (cp >= 0x1f000 && cp <= 0x1faff) ||
85
+ (cp >= 0x20000 && cp <= 0x2fa1f)
86
+ ) {
87
+ w += 2;
88
+ } else {
89
+ w += 1;
90
+ }
91
+ }
92
+ return w;
93
+ }
94
+
95
+ function kv(label: string, value: string) {
96
+ const gap = 16 - visualWidth(label);
97
+ return row(`${label}${" ".repeat(Math.max(1, gap))}: ${value}`);
98
+ }
99
+
100
+ function severityIcon(sev: "critical" | "warning" | "info"): string {
101
+ return sev === "critical" ? "🔴" : sev === "warning" ? "🟡" : "🔵";
102
+ }
103
+
104
+ function healthGrade(passed: number, total: number): { grade: string; icon: string } {
105
+ if (total === 0) return { grade: "A+", icon: "💎" };
106
+ const pct = (passed / total) * 100;
107
+ if (pct === 100) return { grade: "A+", icon: "💎" };
108
+ if (pct >= 80) return { grade: "A", icon: "🟢" };
109
+ if (pct >= 60) return { grade: "B", icon: "🟡" };
110
+ if (pct >= 40) return { grade: "C", icon: "🟠" };
111
+ return { grade: "D", icon: "🔴" };
112
+ }
113
+
114
+ // ── Recommendations ──
115
+ const recommendations: Record<string, { en: string; ko: string }> = {
116
+ "file-missing": {
117
+ en: "Create the missing file to ensure proper AI agent behavior.",
118
+ ko: "파일을 생성하여 AI 에이전트가 정상 동작하도록 하세요.",
119
+ },
120
+ "file-empty": {
121
+ en: "Add meaningful content to the file.",
122
+ ko: "파일에 유의미한 내용을 추가하세요.",
123
+ },
124
+ "file-invalid-json": {
125
+ en: "Fix the JSON syntax error. Use a JSON validator.",
126
+ ko: "JSON 구문 오류를 수정하세요. JSON 검증기를 사용해보세요.",
127
+ },
128
+ "file-missing-line": {
129
+ en: "Add the required pattern to the file.",
130
+ ko: "필수 패턴을 파일에 추가하세요.",
131
+ },
132
+ "file-contains": {
133
+ en: "Remove the unwanted content from the file.",
134
+ ko: "파일에서 비허용 콘텐츠를 제거하세요.",
135
+ },
136
+ };
137
+
138
+ function getRecommendation(patternType: string, lang: "en" | "ko"): string {
139
+ return recommendations[patternType]?.[lang] ?? (lang === "ko" ? "수동 확인이 필요합니다." : "Manual review required.");
140
+ }
141
+
142
+ // ── Patch Applier ──
143
+ function applyPatch(patch: PatchOp): boolean {
144
+ const filePath = patch.path.replace(/^\//, "");
145
+ if (patch.op === "add") {
146
+ if (existsSync(filePath)) return false;
147
+ const dir = dirname(filePath);
148
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
149
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
150
+ return true;
151
+ }
152
+ if (patch.op === "replace") {
153
+ const dir = dirname(filePath);
154
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
155
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ // ── Main Command ──
162
+ export async function doctorCommand(opts: DoctorOptions) {
163
+ const lang = getSystemLanguage();
164
+ const m = msgs[lang];
165
+
166
+ const rules = loadAllRules();
167
+ const builtInCount = rules.filter(r => r.id.startsWith("IMM-")).length;
168
+ const customCount = rules.length - builtInCount;
169
+
170
+ // Evaluate all rules (raw mode — ignore antibody immunization for full picture)
171
+ const result = evaluateRules(rules, [], { raw: true });
172
+ const totalChecks = result.symptoms.length + result.healthy.length;
173
+ const passedCount = result.healthy.length;
174
+ const failedCount = result.symptoms.length;
175
+ const { grade, icon } = healthGrade(passedCount, totalChecks);
176
+
177
+ const output: string[] = [];
178
+ output.push(line(BOX.tl, BOX.tr));
179
+ output.push(row(`${m.title}`));
180
+ output.push(divider());
181
+ output.push(kv(m.ruleCount, `${rules.length} (${builtInCount} ${m.builtIn} + ${customCount} ${m.custom})`));
182
+ output.push(kv(m.grade, `${icon} ${grade} (${passedCount}/${totalChecks})`));
183
+ output.push(kv(m.passed, `✅ ${passedCount}`));
184
+ output.push(kv(m.failed, `❌ ${failedCount}`));
185
+
186
+ if (failedCount === 0) {
187
+ output.push(divider());
188
+ output.push(row(`✅ ${m.healthy}`));
189
+ output.push(line(BOX.bl, BOX.br));
190
+ console.log(output.join("\n"));
191
+ return;
192
+ }
193
+
194
+ // ── Findings ──
195
+ output.push(divider());
196
+ output.push(row(`${m.findings}`));
197
+ output.push(row("─".repeat(W - 4)));
198
+
199
+ let fixableCount = 0;
200
+
201
+ for (const symptom of result.symptoms) {
202
+ const sevLabel = m.severity[symptom.severity];
203
+ const hasPatches = symptom.patches.length > 0;
204
+ if (hasPatches) fixableCount++;
205
+
206
+ output.push(row(`${severityIcon(symptom.severity)} [${sevLabel}] ${symptom.id}: ${symptom.title}`));
207
+ output.push(row(` ${symptom.fileTarget}`));
208
+ output.push(row(` ${m.recommendation}: ${getRecommendation(symptom.patternType, lang)}`));
209
+ if (hasPatches) output.push(row(` 🔧 ${m.fixable}`));
210
+ output.push(row(""));
211
+ }
212
+
213
+ // ── Fix Mode ──
214
+ if (opts.fix) {
215
+ output.push(divider());
216
+ let fixedCount = 0;
217
+ for (const symptom of result.symptoms) {
218
+ if (symptom.patches.length === 0) {
219
+ output.push(row(`⏭️ ${symptom.id}: ${m.fixSkipped}`));
220
+ continue;
221
+ }
222
+ let applied = false;
223
+ for (const patch of symptom.patches) {
224
+ if (applyPatch(patch)) applied = true;
225
+ }
226
+ if (applied) {
227
+ output.push(row(`✅ ${symptom.id}: ${m.fixApplied}`));
228
+ notifyAutoHeal(symptom.id);
229
+ fixedCount++;
230
+ } else {
231
+ output.push(row(`⏭️ ${symptom.id}: ${m.fixSkipped}`));
232
+ }
233
+ }
234
+ output.push(divider());
235
+ output.push(row(`🩺 ${m.fixSummary}: ${fixedCount}/${failedCount}`));
236
+ } else if (fixableCount > 0) {
237
+ output.push(divider());
238
+ output.push(row(`💡 ${m.fixHint.replace("{count}", String(fixableCount))}`));
239
+ }
240
+
241
+ output.push(line(BOX.bl, BOX.br));
242
+ console.log(output.join("\n"));
243
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * afd evolution — Self-Evolution command
3
+ *
4
+ * Analyzes quarantined files, generates failure lessons,
5
+ * and writes them to afd-lessons.md for AI agent learning.
6
+ */
7
+
8
+ import { evolve, analyzeQuarantine, listQuarantine } from "../core/evolution";
9
+ import { getSystemLanguage } from "../core/locale";
10
+
11
+ const msgs = {
12
+ en: {
13
+ title: "afd Self-Evolution Report",
14
+ noQuarantine: "No quarantined files found. Nothing to learn from.",
15
+ noPending: "All quarantined files already learned. No new lessons.",
16
+ analyzing: "Analyzing quarantined failures...",
17
+ written: (n: number) => `${n} new lesson(s) written to afd-lessons.md`,
18
+ total: (n: number) => `Total lessons learned: ${n}`,
19
+ stats: "Quarantine Stats",
20
+ quarantined: "Quarantined",
21
+ learned: "Learned",
22
+ pending: "Pending",
23
+ lessonDetail: "New Lessons",
24
+ },
25
+ ko: {
26
+ title: "afd 자가 진화 리포트",
27
+ noQuarantine: "격리된 파일이 없습니다. 학습할 대상이 없습니다.",
28
+ noPending: "모든 격리 파일이 이미 학습되었습니다. 새로운 교훈이 없습니다.",
29
+ analyzing: "격리된 실패 사례 분석 중...",
30
+ written: (n: number) => `${n}개의 새로운 교훈이 afd-lessons.md에 기록되었습니다`,
31
+ total: (n: number) => `총 학습된 교훈: ${n}개`,
32
+ stats: "격리 통계",
33
+ quarantined: "격리됨",
34
+ learned: "학습 완료",
35
+ pending: "대기 중",
36
+ lessonDetail: "새로운 교훈",
37
+ },
38
+ };
39
+
40
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
41
+ const W = 58;
42
+
43
+ function hline(l: string, r: string) { return `${l}${BOX.h.repeat(W)}${r}`; }
44
+ function row(s: string) {
45
+ const pad = Math.max(0, W - 2 - visualWidth(s));
46
+ return `${BOX.v} ${s}${" ".repeat(pad)} ${BOX.v}`;
47
+ }
48
+ function visualWidth(s: string): number {
49
+ let w = 0;
50
+ for (const ch of s) {
51
+ const cp = ch.codePointAt(0)!;
52
+ if ((cp >= 0x1100 && cp <= 0x11ff) || (cp >= 0x2e80 && cp <= 0x9fff) ||
53
+ (cp >= 0xac00 && cp <= 0xd7af) || (cp >= 0xf900 && cp <= 0xfaff) ||
54
+ (cp >= 0x1f000 && cp <= 0x1faff) || (cp >= 0x20000 && cp <= 0x2fa1f)) w += 2;
55
+ else w += 1;
56
+ }
57
+ return w;
58
+ }
59
+
60
+ export async function evolutionCommand() {
61
+ const lang = getSystemLanguage();
62
+ const m = msgs[lang];
63
+
64
+ const entries = listQuarantine();
65
+ if (entries.length === 0) {
66
+ console.log(m.noQuarantine);
67
+ return;
68
+ }
69
+
70
+ const stats = analyzeQuarantine();
71
+ if (stats.pending === 0) {
72
+ console.log(m.noPending);
73
+ return;
74
+ }
75
+
76
+ console.log(m.analyzing);
77
+ const result = evolve();
78
+
79
+ console.log("");
80
+ console.log(hline(BOX.tl, BOX.tr));
81
+ console.log(row(`🧬 ${m.title}`));
82
+ console.log(hline(BOX.ml, BOX.mr));
83
+ console.log(row(`${m.stats}`));
84
+ console.log(row(` ${m.quarantined} : ${stats.totalQuarantined}`));
85
+ console.log(row(` ${m.learned} : ${result.totalLessons}`));
86
+ console.log(row(` ${m.pending} : 0`));
87
+ console.log(hline(BOX.ml, BOX.mr));
88
+ console.log(row(`${m.lessonDetail}`));
89
+ console.log(row(BOX.h.repeat(W - 4)));
90
+
91
+ for (const lesson of stats.lessons) {
92
+ const icon = lesson.failureType === "deletion" ? "🗑️" : "💥";
93
+ const file = lesson.entry.originalPath;
94
+ console.log(row(`${icon} ${file}`));
95
+ // Truncate suggestion to fit box
96
+ const maxSug = W - 8;
97
+ const sug = lesson.suggestion.length > maxSug
98
+ ? lesson.suggestion.slice(0, maxSug - 3) + "..."
99
+ : lesson.suggestion;
100
+ console.log(row(` ${sug}`));
101
+ }
102
+
103
+ console.log(hline(BOX.ml, BOX.mr));
104
+ console.log(row(m.written(result.lessonsWritten)));
105
+ console.log(row(m.total(result.totalLessons)));
106
+ console.log(hline(BOX.bl, BOX.br));
107
+ }
@@ -13,6 +13,9 @@ function applyPatch(patch: PatchOp): boolean {
13
13
  // Map JSON-Patch path to filesystem path (strip leading /)
14
14
  const filePath = patch.path.replace(/^\//, "");
15
15
 
16
+ // Guard: reject path traversal attempts
17
+ if (filePath.includes("..") || filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath)) return false;
18
+
16
19
  if (patch.op === "add") {
17
20
  if (existsSync(filePath)) return false; // don't overwrite
18
21
  const dir = dirname(filePath);
@@ -50,8 +53,9 @@ async function learnAntibody(symptom: Symptom): Promise<void> {
50
53
 
51
54
  async function getDaemonPort(): Promise<number> {
52
55
  const { readFileSync } = await import("fs");
53
- const { PORT_FILE } = await import("../constants");
54
- return parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
56
+ const { resolveWorkspacePaths } = await import("../constants");
57
+ const paths = resolveWorkspacePaths();
58
+ return parseInt(readFileSync(paths.portFile, "utf-8").trim(), 10);
55
59
  }
56
60
 
57
61
  export async function fixCommand() {
@@ -85,6 +89,22 @@ export async function fixCommand() {
85
89
  console.log();
86
90
  }
87
91
 
92
+ // Extract phase: inject hologram context for AI consumers
93
+ const symptomsWithHologram = diagnosis.symptoms as (Symptom & { hologram?: string })[];
94
+ const holograms = symptomsWithHologram.filter(s => s.hologram);
95
+ if (holograms.length > 0) {
96
+ console.log("[afd fix] Hologram Context (Extract phase — token-optimized file structures):\n");
97
+ for (const s of holograms) {
98
+ console.log(` --- ${s.fileTarget} ---`);
99
+ console.log(` Here is the structural hologram of the file to help you understand`);
100
+ console.log(` its interfaces without consuming too many tokens:\n`);
101
+ for (const line of s.hologram!.split("\n")) {
102
+ console.log(` ${line}`);
103
+ }
104
+ console.log(`\n Now, generate the JSON-Patch based on the structure above.\n`);
105
+ }
106
+ }
107
+
88
108
  // Back-stage: dump full JSON-Patch for AI consumers
89
109
  const allPatches = diagnosis.symptoms.flatMap(s =>
90
110
  s.patches.map(p => ({ symptomId: s.id, ...p }))
@@ -0,0 +1,136 @@
1
+ import { join } from "path";
2
+ import { resolveWorkspacePaths } from "../constants";
3
+ import {
4
+ readHooksFile,
5
+ writeHooksFile,
6
+ mergeHooks,
7
+ getHookSummary,
8
+ getAfdDesiredHooks,
9
+ type HookOwner,
10
+ type ManagedHook,
11
+ } from "../core/hook-manager";
12
+
13
+ const C = {
14
+ reset: "\x1b[0m",
15
+ bold: "\x1b[1m",
16
+ dim: "\x1b[2m",
17
+ red: "\x1b[31m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ cyan: "\x1b[36m",
21
+ };
22
+
23
+ const OWNER_COLOR: Record<HookOwner, string> = {
24
+ afd: C.cyan,
25
+ omc: C.yellow,
26
+ user: C.dim,
27
+ };
28
+
29
+ function formatHookLine(hook: ManagedHook): string {
30
+ const color = OWNER_COLOR[hook.owner];
31
+ const ownerTag = `[${hook.owner}]`.padEnd(6);
32
+ const id = hook.id.padEnd(24);
33
+ const matcher = hook.matcher || "*";
34
+ return ` ${color}${ownerTag}${C.reset} ${C.bold}${id}${C.reset} ${C.dim}${matcher}${C.reset}`;
35
+ }
36
+
37
+ export function hooksCommand(subcommand?: string): void {
38
+ const hooksPath = join(resolveWorkspacePaths().root, ".claude", "hooks.json");
39
+
40
+ if (!subcommand || subcommand === "status") {
41
+ const summary = getHookSummary(hooksPath);
42
+ const total = summary.total;
43
+ console.log("");
44
+ console.log(`${C.bold}afd hooks — Hook Manager${C.reset}`);
45
+ console.log("");
46
+ console.log(` PreToolUse (${total} hook${total !== 1 ? "s" : ""})`);
47
+ console.log(" " + "─".repeat(48));
48
+
49
+ const allInOrder: ManagedHook[] = [
50
+ ...summary.zones.afd,
51
+ ...summary.zones.omc,
52
+ ...summary.zones.user,
53
+ ];
54
+
55
+ if (allInOrder.length === 0) {
56
+ console.log(` ${C.dim}No hooks registered${C.reset}`);
57
+ } else {
58
+ for (const hook of allInOrder) {
59
+ console.log(formatHookLine(hook));
60
+ }
61
+ }
62
+
63
+ console.log("");
64
+
65
+ const orderStatus = summary.orderingOk
66
+ ? `${C.green}OK${C.reset} ${C.dim}(afd → omc → user)${C.reset}`
67
+ : `${C.yellow}DISORDERED${C.reset} — run ${C.bold}afd hooks sync${C.reset} to fix`;
68
+ console.log(` Ordering: ${orderStatus}`);
69
+
70
+ if (summary.conflicts.length === 0) {
71
+ console.log(` Conflicts: ${C.green}none${C.reset}`);
72
+ } else {
73
+ console.log(` Conflicts: ${C.yellow}${summary.conflicts.length} warning(s)${C.reset}`);
74
+ for (const c of summary.conflicts) {
75
+ const prefix = c.type === "duplicate-id" ? "✗" : "⚠";
76
+ console.log(` ${C.yellow}${prefix}${C.reset} ${c.type}: ${C.bold}${c.hookA.id}${C.reset} ↔ ${C.bold}${c.hookB.id}${C.reset}`);
77
+ console.log(` ${C.dim}${c.resolution}${C.reset}`);
78
+ }
79
+ }
80
+
81
+ console.log("");
82
+ return;
83
+ }
84
+
85
+ if (subcommand === "sync") {
86
+ console.log("");
87
+ console.log(`${C.bold}afd hooks sync${C.reset}`);
88
+ console.log("");
89
+
90
+ const config = readHooksFile(hooksPath);
91
+ if (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
92
+ config.hooks = {};
93
+ }
94
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
95
+
96
+ const result = mergeHooks(config.hooks.PreToolUse, getAfdDesiredHooks());
97
+ config.hooks.PreToolUse = result.merged;
98
+ writeHooksFile(hooksPath, config);
99
+
100
+ let anyChange = false;
101
+ if (result.changes.added.length > 0) {
102
+ console.log(` Added: ${result.changes.added.join(", ")}`);
103
+ anyChange = true;
104
+ }
105
+ if (result.changes.removed.length > 0) {
106
+ console.log(` Removed: ${result.changes.removed.join(", ")}`);
107
+ anyChange = true;
108
+ }
109
+ if (result.changes.reordered.length > 0) {
110
+ console.log(` Reordered: ${result.changes.reordered.join(", ")}`);
111
+ anyChange = true;
112
+ }
113
+ if (!anyChange) {
114
+ console.log(` ${C.green}Already in sync — no changes needed${C.reset}`);
115
+ }
116
+
117
+ if (result.conflicts.length > 0) {
118
+ console.log(` Conflicts: ${C.yellow}${result.conflicts.length} warning(s)${C.reset}`);
119
+ for (const c of result.conflicts) {
120
+ const prefix = c.type === "duplicate-id" ? "✗" : "⚠";
121
+ console.log(` ${C.yellow}${prefix}${C.reset} ${c.type}: ${C.bold}${c.hookA.id}${C.reset} ↔ ${C.bold}${c.hookB.id}${C.reset}`);
122
+ console.log(` ${C.dim}${c.resolution}${C.reset}`);
123
+ }
124
+ } else {
125
+ console.log(` Conflicts: ${C.green}none${C.reset}`);
126
+ }
127
+
128
+ console.log(` ${C.dim}hooks.json updated.${C.reset}`);
129
+ console.log("");
130
+ return;
131
+ }
132
+
133
+ console.error(`Unknown subcommand: ${subcommand}`);
134
+ console.error("Usage: afd hooks [status|sync]");
135
+ process.exit(1);
136
+ }
@@ -0,0 +1,41 @@
1
+ import { getSystemLanguage, getSupportedLanguages, setLanguageOverride } from "../core/locale";
2
+ import type { SupportedLang } from "../core/locale";
3
+ import { writeConfig, getConfigPath } from "../core/config";
4
+ import { getMessages, t } from "../core/i18n/messages";
5
+
6
+ export function langCommand(targetLang?: string, options?: { list?: boolean }) {
7
+ const currentLang = getSystemLanguage();
8
+ const msg = getMessages(currentLang);
9
+ const supported = getSupportedLanguages();
10
+
11
+ // afd lang --list
12
+ if (options?.list) {
13
+ console.log(msg.LANG_LIST_TITLE);
14
+ for (const lang of supported) {
15
+ const marker = lang === currentLang ? " ← current" : "";
16
+ console.log(` ${lang}${marker}`);
17
+ }
18
+ return;
19
+ }
20
+
21
+ // afd lang (no argument) — show current
22
+ if (!targetLang) {
23
+ console.log(t(msg.LANG_CURRENT, { lang: currentLang }));
24
+ return;
25
+ }
26
+
27
+ // afd lang <en|ko> — change language
28
+ if (!supported.includes(targetLang as SupportedLang)) {
29
+ console.error(t(msg.LANG_INVALID, { lang: targetLang, supported: supported.join(", ") }));
30
+ process.exit(1);
31
+ }
32
+
33
+ const newLang = targetLang as SupportedLang;
34
+ writeConfig({ lang: newLang });
35
+ setLanguageOverride(newLang);
36
+
37
+ // Print feedback in the NEW language
38
+ const newMsg = getMessages(newLang);
39
+ console.log(t(newMsg.LANG_CHANGED, { lang: newLang }));
40
+ console.log(t(newMsg.LANG_SAVED, { path: getConfigPath() }));
41
+ }