autonomous-flow-daemon 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +85 -46
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -337
  5. package/mcp-config.json +10 -10
  6. package/package.json +14 -6
  7. package/src/adapters/index.ts +370 -159
  8. package/src/cli.ts +162 -57
  9. package/src/commands/benchmark.ts +187 -0
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/diagnose.ts +56 -14
  13. package/src/commands/doctor.ts +243 -0
  14. package/src/commands/evolution.ts +190 -0
  15. package/src/commands/fix.ts +158 -138
  16. package/src/commands/hooks.ts +136 -0
  17. package/src/commands/lang.ts +41 -41
  18. package/src/commands/mcp.ts +129 -0
  19. package/src/commands/plugin.ts +110 -0
  20. package/src/commands/restart.ts +14 -0
  21. package/src/commands/score.ts +276 -208
  22. package/src/commands/start.ts +155 -96
  23. package/src/commands/stats.ts +103 -0
  24. package/src/commands/status.ts +157 -0
  25. package/src/commands/stop.ts +68 -49
  26. package/src/commands/suggest.ts +211 -0
  27. package/src/commands/sync.ts +567 -21
  28. package/src/commands/vaccine.ts +177 -0
  29. package/src/constants.ts +32 -8
  30. package/src/core/boast.ts +280 -265
  31. package/src/core/config.ts +49 -49
  32. package/src/core/correlation-engine.ts +265 -0
  33. package/src/core/db.ts +145 -46
  34. package/src/core/discovery.ts +65 -65
  35. package/src/core/evolution.ts +215 -0
  36. package/src/core/federation.ts +129 -0
  37. package/src/core/hologram/engine.ts +71 -0
  38. package/src/core/hologram/fallback.ts +11 -0
  39. package/src/core/hologram/go-extractor.ts +203 -0
  40. package/src/core/hologram/incremental.ts +227 -0
  41. package/src/core/hologram/py-extractor.ts +132 -0
  42. package/src/core/hologram/rust-extractor.ts +244 -0
  43. package/src/core/hologram/ts-extractor.ts +406 -0
  44. package/src/core/hologram/types.ts +27 -0
  45. package/src/core/hologram.ts +73 -243
  46. package/src/core/hook-manager.ts +259 -0
  47. package/src/core/i18n/messages.ts +309 -266
  48. package/src/core/immune.ts +8 -123
  49. package/src/core/locale.ts +88 -88
  50. package/src/core/log-rotate.ts +33 -0
  51. package/src/core/log-utils.ts +38 -0
  52. package/src/core/lru-map.ts +61 -0
  53. package/src/core/notify.ts +74 -66
  54. package/src/core/plugin-manager.ts +225 -0
  55. package/src/core/rule-engine.ts +287 -0
  56. package/src/core/rule-suggestion.ts +127 -0
  57. package/src/core/semantic-diff.ts +432 -0
  58. package/src/core/telemetry.ts +94 -0
  59. package/src/core/vaccine-registry.ts +212 -0
  60. package/src/core/validator-generator.ts +224 -0
  61. package/src/core/workspace.ts +28 -0
  62. package/src/core/yaml-minimal.ts +176 -0
  63. package/src/daemon/client.ts +78 -37
  64. package/src/daemon/event-batcher.ts +108 -0
  65. package/src/daemon/guards.ts +13 -0
  66. package/src/daemon/http-routes.ts +376 -0
  67. package/src/daemon/mcp-handler.ts +575 -0
  68. package/src/daemon/mcp-subscriptions.ts +81 -0
  69. package/src/daemon/mesh.ts +51 -0
  70. package/src/daemon/server.ts +655 -504
  71. package/src/daemon/types.ts +121 -0
  72. package/src/daemon/workspace-map.ts +104 -0
  73. package/src/platform.ts +60 -39
  74. package/src/version.ts +15 -0
  75. package/README.ko.md +0 -306
@@ -1,243 +1,73 @@
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());
48
- }
49
-
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});`);
115
- }
116
- }
117
-
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})`;
185
- }
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
- }
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 { goExtractor } from "./hologram/go-extractor";
12
+ import { rustExtractor } from "./hologram/rust-extractor";
13
+ import { fallbackL0 } from "./hologram/fallback";
14
+ import { generateIncrementalHologram, setCachedHologram } from "./hologram/incremental";
15
+ import type { HologramResult, HologramOptions, LanguageExtractor } from "./hologram/types";
16
+
17
+ // Re-export types for backward compatibility
18
+ export type { HologramResult, HologramOptions } from "./hologram/types";
19
+ export { clearHologramCache } from "./hologram/incremental";
20
+
21
+ const extractors: LanguageExtractor[] = [tsExtractor, pyExtractor, goExtractor, rustExtractor];
22
+
23
+ function detectExtractor(filePath: string): LanguageExtractor | null {
24
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
25
+ return extractors.find(e => e.extensions.includes(ext)) ?? null;
26
+ }
27
+
28
+ export async function generateHologram(
29
+ filePath: string,
30
+ source: string,
31
+ options?: HologramOptions,
32
+ ): Promise<HologramResult> {
33
+ const extractor = detectExtractor(filePath);
34
+
35
+ if (!extractor) {
36
+ return fallbackL0(filePath, source);
37
+ }
38
+
39
+ // Incremental diff mode
40
+ if (options?.diffOnly) {
41
+ try {
42
+ return await generateIncrementalHologram(filePath, source, extractor, options);
43
+ } catch {
44
+ return fallbackL0(filePath, source);
45
+ }
46
+ }
47
+
48
+ try {
49
+ const engine = await TreeSitterEngine.getInstance();
50
+ const tree = await engine.parse(source, extractor.grammarName);
51
+ const lines = extractor.extract(tree, source, options);
52
+ tree.delete();
53
+
54
+ // Cache for future incremental diffs
55
+ setCachedHologram(filePath, lines);
56
+
57
+ const hologram = lines.join("\n");
58
+ const hologramLength = hologram.length;
59
+ const savings = source.length > 0
60
+ ? Math.round((source.length - hologramLength) / source.length * 1000) / 10
61
+ : 0;
62
+
63
+ return {
64
+ hologram,
65
+ originalLength: source.length,
66
+ hologramLength,
67
+ savings,
68
+ language: extractor.grammarName,
69
+ };
70
+ } catch {
71
+ return fallbackL0(filePath, source);
72
+ }
73
+ }
@@ -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
+ }