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.
- package/CHANGELOG.md +39 -0
- package/README.ko.md +142 -125
- package/README.md +119 -134
- package/package.json +11 -5
- package/src/adapters/index.ts +247 -35
- package/src/cli.ts +79 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +192 -64
- package/src/commands/start.ts +137 -37
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +42 -9
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +26 -1
- package/src/core/boast.ts +280 -0
- package/src/core/config.ts +49 -0
- package/src/core/db.ts +74 -3
- package/src/core/discovery.ts +65 -0
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -0
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -0
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +53 -14
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +492 -273
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +60 -0
- package/src/version.ts +15 -0
package/src/commands/diagnose.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/fix.ts
CHANGED
|
@@ -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 {
|
|
54
|
-
|
|
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
|
+
}
|