autonomous-flow-daemon 1.1.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -1,243 +1,71 @@
1
- import ts from "typescript";
2
-
3
- export interface HologramResult {
4
- hologram: string;
5
- originalLength: number;
6
- hologramLength: number;
7
- savings: number; // percentage 0-100
8
- }
9
-
10
- export function generateHologram(filePath: string, source: string): HologramResult {
11
- const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
12
- const lines: string[] = [];
13
-
14
- for (const stmt of sf.statements) {
15
- const line = extractNode(stmt, source);
16
- if (line) lines.push(line);
17
- }
18
-
19
- const hologram = lines.join("\n");
20
- const originalLength = source.length;
21
- const hologramLength = hologram.length;
22
- const savings = originalLength > 0
23
- ? Math.round((originalLength - hologramLength) / originalLength * 1000) / 10
24
- : 0;
25
-
26
- return { hologram, originalLength, hologramLength, savings };
27
- }
28
-
29
- function extractNode(node: ts.Node, source: string): string | null {
30
- // Import declarations — keep as-is
31
- if (ts.isImportDeclaration(node)) {
32
- return node.getText().replace(/\s+/g, " ").trim();
33
- }
34
-
35
- // Export declarations (re-exports)
36
- if (ts.isExportDeclaration(node)) {
37
- return node.getText().replace(/\s+/g, " ").trim();
38
- }
39
-
40
- // Export assignment (export default ...)
41
- if (ts.isExportAssignment(node)) {
42
- return `export default ${getTypeName(node.expression)};`;
43
- }
44
-
45
- // Type alias
46
- if (ts.isTypeAliasDeclaration(node)) {
47
- return collapseWhitespace(node.getText());
1
+ /**
2
+ * Hologram Engine — Language Dispatcher
3
+ *
4
+ * Routes file parsing to the appropriate tree-sitter extractor based on extension.
5
+ * Falls back to L0 (full source) for unsupported languages or parse errors.
6
+ */
7
+
8
+ import { TreeSitterEngine } from "./hologram/engine";
9
+ import { tsExtractor } from "./hologram/ts-extractor";
10
+ import { pyExtractor } from "./hologram/py-extractor";
11
+ import { fallbackL0 } from "./hologram/fallback";
12
+ import { generateIncrementalHologram, setCachedHologram } from "./hologram/incremental";
13
+ import type { HologramResult, HologramOptions, LanguageExtractor } from "./hologram/types";
14
+
15
+ // Re-export types for backward compatibility
16
+ export type { HologramResult, HologramOptions } from "./hologram/types";
17
+ export { clearHologramCache } from "./hologram/incremental";
18
+
19
+ const extractors: LanguageExtractor[] = [tsExtractor, pyExtractor];
20
+
21
+ function detectExtractor(filePath: string): LanguageExtractor | null {
22
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
23
+ return extractors.find(e => e.extensions.includes(ext)) ?? null;
24
+ }
25
+
26
+ export async function generateHologram(
27
+ filePath: string,
28
+ source: string,
29
+ options?: HologramOptions,
30
+ ): Promise<HologramResult> {
31
+ const extractor = detectExtractor(filePath);
32
+
33
+ if (!extractor) {
34
+ return fallbackL0(filePath, source);
48
35
  }
49
36
 
50
- // Interface
51
- if (ts.isInterfaceDeclaration(node)) {
52
- return extractInterface(node);
53
- }
54
-
55
- // Enum
56
- if (ts.isEnumDeclaration(node)) {
57
- return extractEnum(node);
58
- }
59
-
60
- // Class
61
- if (ts.isClassDeclaration(node)) {
62
- return extractClass(node);
63
- }
64
-
65
- // Function declaration
66
- if (ts.isFunctionDeclaration(node)) {
67
- return extractFunction(node);
68
- }
69
-
70
- // Variable statement (const/let/var — may contain arrow functions, objects, etc.)
71
- if (ts.isVariableStatement(node)) {
72
- return extractVariableStatement(node);
73
- }
74
-
75
- // Fallback: skip unknown top-level statements
76
- return null;
77
- }
78
-
79
- function extractInterface(node: ts.InterfaceDeclaration): string {
80
- const mods = getModifiers(node);
81
- const name = node.name.text;
82
- const ext = node.heritageClauses
83
- ? " " + node.heritageClauses.map(h => h.getText()).join(", ")
84
- : "";
85
- const members = node.members.map(m => {
86
- const text = collapseWhitespace(m.getText()).replace(/;$/, "");
87
- return " " + text + ";";
88
- }).join("\n");
89
- return `${mods}interface ${name}${ext} {\n${members}\n}`;
90
- }
91
-
92
- function extractEnum(node: ts.EnumDeclaration): string {
93
- const mods = getModifiers(node);
94
- const name = node.name.text;
95
- const members = node.members.map(m => collapseWhitespace(m.getText())).join(", ");
96
- return `${mods}enum ${name} { ${members} }`;
97
- }
98
-
99
- function extractClass(node: ts.ClassDeclaration): string {
100
- const mods = getModifiers(node);
101
- const name = node.name?.text ?? "Anonymous";
102
- const ext = node.heritageClauses
103
- ? " " + node.heritageClauses.map(h => h.getText()).join(", ")
104
- : "";
105
-
106
- const members: string[] = [];
107
- for (const member of node.members) {
108
- if (ts.isPropertyDeclaration(member)) {
109
- members.push(" " + extractProperty(member) + ";");
110
- } else if (ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
111
- members.push(" " + extractMethodSignature(member) + ";");
112
- } else if (ts.isConstructorDeclaration(member)) {
113
- const params = extractParams(member.parameters);
114
- members.push(` constructor(${params});`);
37
+ // Incremental diff mode
38
+ if (options?.diffOnly) {
39
+ try {
40
+ return await generateIncrementalHologram(filePath, source, extractor, options);
41
+ } catch {
42
+ return fallbackL0(filePath, source);
115
43
  }
116
44
  }
117
45
 
118
- return `${mods}class ${name}${ext} {\n${members.join("\n")}\n}`;
119
- }
120
-
121
- function extractFunction(node: ts.FunctionDeclaration): string {
122
- const mods = getModifiers(node);
123
- const name = node.name?.text ?? "anonymous";
124
- const typeParams = node.typeParameters
125
- ? `<${node.typeParameters.map(t => t.getText()).join(", ")}>`
126
- : "";
127
- const params = extractParams(node.parameters);
128
- const ret = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
129
- const async = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
130
- return `${mods}${async}function ${name}${typeParams}(${params})${ret} {…}`;
131
- }
132
-
133
- function extractVariableStatement(node: ts.VariableStatement): string {
134
- const mods = getModifiers(node);
135
- const keyword = node.declarationList.flags & ts.NodeFlags.Const ? "const"
136
- : node.declarationList.flags & ts.NodeFlags.Let ? "let" : "var";
137
-
138
- const decls = node.declarationList.declarations.map(d => {
139
- const name = d.name.getText();
140
- const typeAnn = d.type ? ": " + collapseWhitespace(d.type.getText()) : "";
141
-
142
- if (d.initializer) {
143
- // Arrow function or function expression
144
- if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
145
- const fn = d.initializer;
146
- const async = hasModifier(fn, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
147
- const typeParams = fn.typeParameters
148
- ? `<${fn.typeParameters.map(t => t.getText()).join(", ")}>`
149
- : "";
150
- const params = extractParams(fn.parameters);
151
- const ret = fn.type ? ": " + collapseWhitespace(fn.type.getText()) : "";
152
- return `${name} = ${async}${typeParams}(${params})${ret} => {…}`;
153
- }
154
- // Object/array/other — just show type or truncated value
155
- if (typeAnn) return `${name}${typeAnn}`;
156
- return `${name} = …`;
157
- }
158
-
159
- return `${name}${typeAnn}`;
160
- });
161
-
162
- return `${mods}${keyword} ${decls.join(", ")};`;
163
- }
164
-
165
- function extractProperty(node: ts.PropertyDeclaration): string {
166
- const mods = getMemberModifiers(node);
167
- const name = node.name.getText();
168
- const type = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
169
- const optional = node.questionToken ? "?" : "";
170
- return `${mods}${name}${optional}${type}`;
171
- }
172
-
173
- function extractMethodSignature(node: ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration): string {
174
- const mods = getMemberModifiers(node);
175
- const name = node.name.getText();
176
- const async = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
177
-
178
- if (ts.isGetAccessor(node)) {
179
- const ret = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
180
- return `${mods}get ${name}()${ret}`;
181
- }
182
- if (ts.isSetAccessor(node)) {
183
- const params = extractParams(node.parameters);
184
- return `${mods}set ${name}(${params})`;
46
+ try {
47
+ const engine = await TreeSitterEngine.getInstance();
48
+ const tree = await engine.parse(source, extractor.grammarName);
49
+ const lines = extractor.extract(tree, source, options);
50
+ tree.delete();
51
+
52
+ // Cache for future incremental diffs
53
+ setCachedHologram(filePath, lines);
54
+
55
+ const hologram = lines.join("\n");
56
+ const hologramLength = hologram.length;
57
+ const savings = source.length > 0
58
+ ? Math.round((source.length - hologramLength) / source.length * 1000) / 10
59
+ : 0;
60
+
61
+ return {
62
+ hologram,
63
+ originalLength: source.length,
64
+ hologramLength,
65
+ savings,
66
+ language: extractor.grammarName,
67
+ };
68
+ } catch {
69
+ return fallbackL0(filePath, source);
185
70
  }
186
-
187
- const md = node as ts.MethodDeclaration;
188
- const typeParams = md.typeParameters
189
- ? `<${md.typeParameters.map(t => t.getText()).join(", ")}>`
190
- : "";
191
- const params = extractParams(md.parameters);
192
- const ret = md.type ? ": " + collapseWhitespace(md.type.getText()) : "";
193
- return `${mods}${async}${name}${typeParams}(${params})${ret}`;
194
- }
195
-
196
- function extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): string {
197
- return params.map(p => {
198
- const name = p.name.getText();
199
- const optional = p.questionToken ? "?" : "";
200
- const type = p.type ? ": " + collapseWhitespace(p.type.getText()) : "";
201
- const rest = p.dotDotDotToken ? "..." : "";
202
- return `${rest}${name}${optional}${type}`;
203
- }).join(", ");
204
- }
205
-
206
- function getModifiers(node: ts.Node): string {
207
- const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
208
- if (!mods) return "";
209
- const relevant = mods
210
- .filter(m => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword || m.kind === ts.SyntaxKind.DeclareKeyword)
211
- .map(m => m.getText());
212
- return relevant.length ? relevant.join(" ") + " " : "";
213
- }
214
-
215
- function getMemberModifiers(node: ts.Node): string {
216
- const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
217
- if (!mods) return "";
218
- const relevant = mods
219
- .filter(m =>
220
- m.kind === ts.SyntaxKind.PublicKeyword ||
221
- m.kind === ts.SyntaxKind.PrivateKeyword ||
222
- m.kind === ts.SyntaxKind.ProtectedKeyword ||
223
- m.kind === ts.SyntaxKind.StaticKeyword ||
224
- m.kind === ts.SyntaxKind.ReadonlyKeyword ||
225
- m.kind === ts.SyntaxKind.AbstractKeyword
226
- )
227
- .map(m => m.getText());
228
- return relevant.length ? relevant.join(" ") + " " : "";
229
- }
230
-
231
- function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
232
- const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
233
- return mods?.some(m => m.kind === kind) ?? false;
234
- }
235
-
236
- function getTypeName(node: ts.Node): string {
237
- if (ts.isIdentifier(node)) return node.text;
238
- return "…";
239
- }
240
-
241
- function collapseWhitespace(s: string): string {
242
- return s.replace(/\s+/g, " ").trim();
243
71
  }
@@ -0,0 +1,259 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { resolveHookCommand } from "../platform";
4
+
5
+ export type HookOwner = "afd" | "omc" | "user";
6
+
7
+ export interface HookEntry {
8
+ id?: string;
9
+ matcher?: string;
10
+ command: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export interface HooksConfig {
15
+ hooks?: Record<string, HookEntry[]>;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export interface ManagedHook {
20
+ id: string;
21
+ matcher: string;
22
+ command: string;
23
+ owner: HookOwner;
24
+ }
25
+
26
+ export interface HookConflict {
27
+ type: "matcher-overlap" | "duplicate-id";
28
+ hookA: ManagedHook;
29
+ hookB: ManagedHook;
30
+ resolution: string;
31
+ }
32
+
33
+ export interface MergeResult {
34
+ merged: HookEntry[];
35
+ conflicts: HookConflict[];
36
+ changes: { added: string[]; removed: string[]; reordered: string[] };
37
+ }
38
+
39
+ export interface HookSummary {
40
+ zones: Record<HookOwner, ManagedHook[]>;
41
+ conflicts: HookConflict[];
42
+ orderingOk: boolean;
43
+ total: number;
44
+ }
45
+
46
+ /**
47
+ * Canonical set of afd-managed hooks.
48
+ * Only hooks in this set are removed during `stop --clean`.
49
+ * This prevents accidental deletion of user hooks with an `afd-` prefix
50
+ * (e.g., project-local `afd-read-gate` scripts).
51
+ */
52
+ export const KNOWN_AFD_HOOKS = new Set(["afd-auto-heal"]);
53
+
54
+ /** afd's canonical desired hooks — authoritative source for merge. */
55
+ export function getAfdDesiredHooks(): HookEntry[] {
56
+ return [
57
+ {
58
+ id: "afd-auto-heal",
59
+ matcher: "Write|Edit|MultiEdit",
60
+ command: resolveHookCommand(),
61
+ },
62
+ ];
63
+ }
64
+
65
+ /** Classify a hook's owner by id prefix. */
66
+ export function classifyOwner(id: string): HookOwner {
67
+ if (id.startsWith("afd-")) return "afd";
68
+ if (id.startsWith("omc-")) return "omc";
69
+ return "user";
70
+ }
71
+
72
+ /** Classify a list of hook entries into owner zones. */
73
+ export function classifyHooks(entries: HookEntry[]): Map<HookOwner, ManagedHook[]> {
74
+ const zones = new Map<HookOwner, ManagedHook[]>([
75
+ ["afd", []],
76
+ ["omc", []],
77
+ ["user", []],
78
+ ]);
79
+ let anonIdx = 0;
80
+ for (const entry of entries) {
81
+ const id = entry.id ?? `user-anonymous-${anonIdx++}`;
82
+ const owner = classifyOwner(id);
83
+ zones.get(owner)!.push({
84
+ id,
85
+ matcher: entry.matcher ?? "",
86
+ command: entry.command,
87
+ owner,
88
+ });
89
+ }
90
+ return zones;
91
+ }
92
+
93
+ /** Detect conflicts between hooks from different owners. */
94
+ export function detectConflicts(hooks: ManagedHook[]): HookConflict[] {
95
+ const conflicts: HookConflict[] = [];
96
+
97
+ // 1. Duplicate ID check
98
+ const idMap = new Map<string, ManagedHook>();
99
+ for (const hook of hooks) {
100
+ if (idMap.has(hook.id)) {
101
+ conflicts.push({
102
+ type: "duplicate-id",
103
+ hookA: idMap.get(hook.id)!,
104
+ hookB: hook,
105
+ resolution: `Remove or rename one of the duplicate '${hook.id}' hooks`,
106
+ });
107
+ } else {
108
+ idMap.set(hook.id, hook);
109
+ }
110
+ }
111
+
112
+ // 2. Matcher overlap check (cross-owner only, O(n^2) — safe for <20 hooks)
113
+ for (let i = 0; i < hooks.length; i++) {
114
+ for (let j = i + 1; j < hooks.length; j++) {
115
+ if (hooks[i].owner === hooks[j].owner) continue;
116
+ const matcherA = hooks[i].matcher || "*";
117
+ const matcherB = hooks[j].matcher || "*";
118
+ const setA = new Set(matcherA.split("|").map(s => s.trim()));
119
+ const setB = new Set(matcherB.split("|").map(s => s.trim()));
120
+ const aIsWild = setA.has("*") || setA.has("");
121
+ const bIsWild = setB.has("*") || setB.has("");
122
+ const overlap =
123
+ aIsWild || bIsWild || [...setA].some(m => setB.has(m));
124
+ if (overlap) {
125
+ const shared =
126
+ aIsWild || bIsWild
127
+ ? "*"
128
+ : [...setA].filter(m => setB.has(m)).join("|");
129
+ conflicts.push({
130
+ type: "matcher-overlap",
131
+ hookA: hooks[i],
132
+ hookB: hooks[j],
133
+ resolution: `Both hooks trigger on '${shared}'. Verify they don't conflict in behavior.`,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ return conflicts;
140
+ }
141
+
142
+ /** Read hooks.json from disk, returning empty config on missing/invalid file. */
143
+ export function readHooksFile(hooksPath: string): HooksConfig {
144
+ if (!existsSync(hooksPath)) return { hooks: {} };
145
+ try {
146
+ return JSON.parse(readFileSync(hooksPath, "utf-8")) as HooksConfig;
147
+ } catch {
148
+ return { hooks: {} };
149
+ }
150
+ }
151
+
152
+ /** Write hooks.json to disk, creating parent directory if needed. */
153
+ export function writeHooksFile(hooksPath: string, config: HooksConfig): void {
154
+ mkdirSync(dirname(hooksPath), { recursive: true });
155
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
156
+ }
157
+
158
+ /**
159
+ * Merge current hook entries with the desired afd hooks.
160
+ * Ordering guarantee: afd → omc → user.
161
+ * afd zone is fully authoritative — desired list overwrites existing afd hooks.
162
+ * omc and user zones are preserved as-is from the current file.
163
+ */
164
+ export function mergeHooks(
165
+ current: HookEntry[],
166
+ desiredAfd: HookEntry[],
167
+ ): MergeResult {
168
+ const changes: MergeResult["changes"] = { added: [], removed: [], reordered: [] };
169
+
170
+ const zones = classifyHooks(current);
171
+ const afdExisting = zones.get("afd")!;
172
+ const omcHooks = zones.get("omc")!;
173
+ const userHooks = zones.get("user")!;
174
+
175
+ // Build merged afd zone from desired list (authoritative)
176
+ const mergedAfd: HookEntry[] = desiredAfd.map(desired => {
177
+ const existing = afdExisting.find(h => h.id === desired.id);
178
+ if (!existing) changes.added.push(desired.id!);
179
+ return desired;
180
+ });
181
+
182
+ // Track explicitly removed afd hooks (only canonical ones)
183
+ for (const existing of afdExisting) {
184
+ if (KNOWN_AFD_HOOKS.has(existing.id) && !desiredAfd.find(d => d.id === existing.id)) {
185
+ changes.removed.push(existing.id);
186
+ }
187
+ }
188
+
189
+ // Detect reordering (only if no add/remove)
190
+ if (changes.added.length === 0 && changes.removed.length === 0) {
191
+ const originalOrder = current.map(h => h.id ?? "");
192
+ const newOrder = [
193
+ ...mergedAfd.map(h => h.id!),
194
+ ...omcHooks.map(h => h.id),
195
+ ...userHooks.map(h => h.id),
196
+ ];
197
+ const hasReordering =
198
+ originalOrder.length !== newOrder.length ||
199
+ originalOrder.some((id, i) => id !== (newOrder[i] ?? ""));
200
+ if (hasReordering) {
201
+ changes.reordered.push("hooks reordered to afd → omc → user zones");
202
+ }
203
+ }
204
+
205
+ const merged: HookEntry[] = [
206
+ ...mergedAfd,
207
+ ...omcHooks.map(({ id, matcher, command }) => ({ id, matcher, command })),
208
+ ...userHooks.map(({ id, matcher, command }) => ({ id, matcher, command })),
209
+ ];
210
+
211
+ const allManaged: ManagedHook[] = [
212
+ ...mergedAfd.map(h => ({
213
+ id: h.id!,
214
+ matcher: h.matcher ?? "",
215
+ command: h.command,
216
+ owner: "afd" as HookOwner,
217
+ })),
218
+ ...omcHooks,
219
+ ...userHooks,
220
+ ];
221
+ const conflicts = detectConflicts(allManaged);
222
+
223
+ return { merged, conflicts, changes };
224
+ }
225
+
226
+ /** Get a summary of current hook state for display and status commands. */
227
+ export function getHookSummary(hooksPath: string): HookSummary {
228
+ const config = readHooksFile(hooksPath);
229
+ const entries = config.hooks?.PreToolUse ?? [];
230
+ const zones = classifyHooks(entries);
231
+
232
+ const allHooks: ManagedHook[] = [
233
+ ...(zones.get("afd") ?? []),
234
+ ...(zones.get("omc") ?? []),
235
+ ...(zones.get("user") ?? []),
236
+ ];
237
+ const conflicts = detectConflicts(allHooks);
238
+
239
+ // Check ordering invariant: afd(0) → omc(1) → user(2)
240
+ const ownerPriority: Record<HookOwner, number> = { afd: 0, omc: 1, user: 2 };
241
+ let orderingOk = true;
242
+ let lastPriority = -1;
243
+ for (const entry of entries) {
244
+ const id = entry.id ?? "";
245
+ const priority = ownerPriority[classifyOwner(id)];
246
+ if (priority < lastPriority) {
247
+ orderingOk = false;
248
+ break;
249
+ }
250
+ lastPriority = priority;
251
+ }
252
+
253
+ return {
254
+ zones: Object.fromEntries(zones) as Record<HookOwner, ManagedHook[]>,
255
+ conflicts,
256
+ orderingOk,
257
+ total: entries.length,
258
+ };
259
+ }
@@ -45,6 +45,8 @@ export interface MessageDict {
45
45
  SCORE_HOLOGRAM_EFFICIENCY: string;
46
46
  SCORE_HOLOGRAM_EMPTY: string;
47
47
  SCORE_HOLOGRAM_HINT: string;
48
+ SCORE_HOLOGRAM_TODAY: string;
49
+ SCORE_HOLOGRAM_LIFETIME: string;
48
50
  SCORE_IMMUNE_TITLE: string;
49
51
  SCORE_ANTIBODIES: string;
50
52
  SCORE_LEVEL: string;
@@ -79,6 +81,19 @@ export interface MessageDict {
79
81
  DAEMON_NOT_RUNNING: string;
80
82
  DAEMON_NOT_RESPONDING: string;
81
83
  DAEMON_START_FAILED: string; // "{path}"
84
+ DAEMON_RESTARTING: string;
85
+
86
+ // ── Setup checklist ──
87
+ SETUP_HEADER: string; // "{ecosystem}"
88
+ SETUP_HOOKS_NEW: string;
89
+ SETUP_HOOKS_OK: string;
90
+ SETUP_MCP_NEW: string;
91
+ SETUP_MCP_OK: string;
92
+ SETUP_MCP_SKIP: string;
93
+ SETUP_STATUS_NEW: string;
94
+ SETUP_STATUS_OK: string;
95
+ SETUP_STATUS_SKIP: string;
96
+ SETUP_DONE: string;
82
97
  }
83
98
 
84
99
  const en: MessageDict = {
@@ -133,6 +148,8 @@ const en: MessageDict = {
133
148
  SCORE_HOLOGRAM_EFFICIENCY: "Efficiency",
134
149
  SCORE_HOLOGRAM_EMPTY: "No hologram requests yet.",
135
150
  SCORE_HOLOGRAM_HINT: "Use: GET /hologram?file=<path>",
151
+ SCORE_HOLOGRAM_TODAY: "Today",
152
+ SCORE_HOLOGRAM_LIFETIME: "All-time",
136
153
  SCORE_IMMUNE_TITLE: "Immune System",
137
154
  SCORE_ANTIBODIES: "Antibodies",
138
155
  SCORE_LEVEL: "Level",
@@ -163,6 +180,18 @@ const en: MessageDict = {
163
180
  DAEMON_NOT_RUNNING: "[afd] No daemon running.",
164
181
  DAEMON_NOT_RESPONDING: "[afd] Daemon not responding. Cleaning up stale PID files.",
165
182
  DAEMON_START_FAILED: "[afd] Failed to start daemon. Check logs: {path}",
183
+ DAEMON_RESTARTING: "[afd] 🔄 Restarting daemon...",
184
+
185
+ SETUP_HEADER: "[afd] Setting up {ecosystem} ecosystem:",
186
+ SETUP_HOOKS_NEW: " [+] Hook injected into PreToolUse",
187
+ SETUP_HOOKS_OK: " [=] Hook already present",
188
+ SETUP_MCP_NEW: " [+] MCP server registered in .mcp.json",
189
+ SETUP_MCP_OK: " [=] MCP server already registered",
190
+ SETUP_MCP_SKIP: " [-] MCP registration not available",
191
+ SETUP_STATUS_NEW: " [+] StatusLine configured",
192
+ SETUP_STATUS_OK: " [=] StatusLine already configured",
193
+ SETUP_STATUS_SKIP: " [-] StatusLine not available",
194
+ SETUP_DONE: "[afd] Zero-touch setup complete. All channels active.",
166
195
  };
167
196
 
168
197
  const ko: MessageDict = {
@@ -217,6 +246,8 @@ const ko: MessageDict = {
217
246
  SCORE_HOLOGRAM_EFFICIENCY: "압축 효율",
218
247
  SCORE_HOLOGRAM_EMPTY: "아직 홀로그램 요청이 없습니다.",
219
248
  SCORE_HOLOGRAM_HINT: "사용법: GET /hologram?file=<경로>",
249
+ SCORE_HOLOGRAM_TODAY: "오늘",
250
+ SCORE_HOLOGRAM_LIFETIME: "누적",
220
251
  SCORE_IMMUNE_TITLE: "면역 시스템",
221
252
  SCORE_ANTIBODIES: "항체 수",
222
253
  SCORE_LEVEL: "방어 레벨",
@@ -247,6 +278,18 @@ const ko: MessageDict = {
247
278
  DAEMON_NOT_RUNNING: "[afd] 실행 중인 데몬을 찾을 수 없습니다.",
248
279
  DAEMON_NOT_RESPONDING: "[afd] 데몬이 응답하지 않네요. 남은 PID 파일을 정리합니다.",
249
280
  DAEMON_START_FAILED: "[afd] 데몬 시작 실패. 로그를 확인해 보세요: {path}",
281
+ DAEMON_RESTARTING: "[afd] 🔄 데몬을 재시작합니다...",
282
+
283
+ SETUP_HEADER: "[afd] {ecosystem} 에코시스템 설정 중:",
284
+ SETUP_HOOKS_NEW: " [+] PreToolUse 훅 주입 완료",
285
+ SETUP_HOOKS_OK: " [=] 훅 이미 설정됨",
286
+ SETUP_MCP_NEW: " [+] MCP 서버 등록 완료 (.mcp.json)",
287
+ SETUP_MCP_OK: " [=] MCP 서버 이미 등록됨",
288
+ SETUP_MCP_SKIP: " [-] MCP 등록 미지원",
289
+ SETUP_STATUS_NEW: " [+] StatusLine 설정 완료",
290
+ SETUP_STATUS_OK: " [=] StatusLine 이미 설정됨",
291
+ SETUP_STATUS_SKIP: " [-] StatusLine 미지원",
292
+ SETUP_DONE: "[afd] 제로터치 설정 완료. 모든 채널 활성화.",
250
293
  };
251
294
 
252
295
  const dictionaries: Record<SupportedLang, MessageDict> = { en, ko };