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
package/mcp-config.json CHANGED
@@ -1,10 +1,10 @@
1
- {
2
- "mcpServers": {
3
- "afd": {
4
- "command": "bun",
5
- "args": ["run", "src/cli.ts", "start"],
6
- "description": "Autonomous Flow Daemon — self-healing AI development environment guardian. Monitors critical config files and auto-restores them within 270ms.",
7
- "env": {}
8
- }
9
- }
10
- }
1
+ {
2
+ "mcpServers": {
3
+ "afd": {
4
+ "command": "bun",
5
+ "args": ["run", "src/cli.ts", "start"],
6
+ "description": "Autonomous Flow Daemon — self-healing AI development environment guardian. Monitors critical config files and auto-restores them within 270ms.",
7
+ "env": {}
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autonomous-flow-daemon",
3
- "version": "1.1.0",
3
+ "version": "1.9.0",
4
4
  "description": "Zero-config immunity for AI coding workflows. Self-heals broken configs in < 270ms.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "src/**/*.ts",
12
12
  "mcp-config.json",
13
13
  "README.md",
14
- "README.ko.md",
14
+ "README-ko.md",
15
15
  "CHANGELOG.md",
16
16
  "LICENSE"
17
17
  ],
@@ -26,14 +26,22 @@
26
26
  "dev-tools"
27
27
  ],
28
28
  "scripts": {
29
- "start": "bun run src/cli.ts start",
30
- "dev": "bun run src/cli.ts",
29
+ "dev": "bun --watch src/cli.ts",
30
+ "score": "bun src/cli.ts score",
31
+ "start": "bun src/cli.ts start",
31
32
  "test": "bun test"
32
33
  },
33
34
  "dependencies": {
34
35
  "chokidar": "^4.0.3",
35
36
  "commander": "^13.1.0",
36
- "typescript": "^6.0.2"
37
+ "tree-sitter-go": "^0.25.0",
38
+ "tree-sitter-rust": "^0.24.0",
39
+ "typescript": "^6.0.2",
40
+ "web-tree-sitter": "^0.26.8"
37
41
  },
38
- "license": "MIT"
42
+ "license": "MIT",
43
+ "optionalDependencies": {
44
+ "tree-sitter-python": "^0.25.0",
45
+ "tree-sitter-typescript": "^0.23.2"
46
+ }
39
47
  }
@@ -1,159 +1,370 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
- import { join, dirname } from "path";
3
- import { resolveHookCommand } from "../platform";
4
-
5
- export interface HarnessSchema {
6
- configFiles: string[];
7
- ignoreFile: string | null;
8
- rulesFile: string | null;
9
- hooksFile: string | null;
10
- }
11
-
12
- export interface EcosystemAdapter {
13
- name: string;
14
- detect(cwd: string): boolean;
15
- getHarnessSchema(): HarnessSchema;
16
- injectHooks?(cwd: string): { injected: boolean; message: string };
17
- configureStatusLine?(cwd: string): { configured: boolean; message: string };
18
- }
19
-
20
- const AFD_HOOK_MARKER = "afd-auto-heal";
21
-
22
- interface HooksConfig {
23
- hooks?: Record<string, HookEntry[]>;
24
- [key: string]: unknown;
25
- }
26
-
27
- interface HookEntry {
28
- matcher?: string;
29
- command: string;
30
- id?: string;
31
- [key: string]: unknown;
32
- }
33
-
34
- export const ClaudeCodeAdapter: EcosystemAdapter = {
35
- name: "Claude Code",
36
- detect(cwd: string): boolean {
37
- return (
38
- existsSync(join(cwd, ".claude")) ||
39
- existsSync(join(cwd, "CLAUDE.md"))
40
- );
41
- },
42
- getHarnessSchema(): HarnessSchema {
43
- return {
44
- configFiles: [".claude/settings.json", ".claude/settings.local.json", "CLAUDE.md"],
45
- ignoreFile: ".claudeignore",
46
- rulesFile: "CLAUDE.md",
47
- hooksFile: ".claude/hooks.json",
48
- };
49
- },
50
- injectHooks(cwd: string): { injected: boolean; message: string } {
51
- const hooksPath = join(cwd, ".claude", "hooks.json");
52
- const hookCommand = resolveHookCommand();
53
-
54
- const newHook: HookEntry = {
55
- id: AFD_HOOK_MARKER,
56
- matcher: "",
57
- command: hookCommand,
58
- };
59
-
60
- let config: HooksConfig;
61
- if (existsSync(hooksPath)) {
62
- try {
63
- config = JSON.parse(readFileSync(hooksPath, "utf-8"));
64
- } catch {
65
- config = { hooks: {} };
66
- }
67
- } else {
68
- mkdirSync(dirname(hooksPath), { recursive: true });
69
- config = { hooks: {} };
70
- }
71
-
72
- if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
73
- config.hooks = {};
74
- }
75
- if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
76
-
77
- // Check if already injected
78
- const existing = config.hooks.PreToolUse.find(
79
- (h: HookEntry) => h.id === AFD_HOOK_MARKER
80
- );
81
- if (existing) {
82
- // Update command in case path changed
83
- existing.command = hookCommand;
84
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
85
- return { injected: false, message: "Auto-heal hook already present (updated)" };
86
- }
87
-
88
- config.hooks.PreToolUse.push(newHook);
89
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
90
- return { injected: true, message: "Auto-heal hook injected into PreToolUse" };
91
- },
92
- configureStatusLine(cwd: string): { configured: boolean; message: string } {
93
- const settingsPath = join(cwd, ".claude", "settings.local.json");
94
- const statusScript = join(cwd, ".claude", "statusline-command.js").replace(/\\/g, "/");
95
-
96
- // Only configure if the statusline-command.js exists
97
- if (!existsSync(statusScript)) {
98
- return { configured: false, message: "No statusline-command.js found" };
99
- }
100
-
101
- let settings: Record<string, unknown>;
102
- if (existsSync(settingsPath)) {
103
- try {
104
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
105
- } catch {
106
- settings = {};
107
- }
108
- } else {
109
- mkdirSync(dirname(settingsPath), { recursive: true });
110
- settings = {};
111
- }
112
-
113
- const expectedCommand = `node ${statusScript}`;
114
- const current = settings.statusLine as { type?: string; command?: string } | undefined;
115
-
116
- if (current?.type === "command" && current?.command === expectedCommand) {
117
- return { configured: false, message: "Status line already configured" };
118
- }
119
-
120
- settings.statusLine = { type: "command", command: expectedCommand };
121
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
122
- return { configured: true, message: "Status line configured with afd integration" };
123
- },
124
- };
125
-
126
- export const CursorAdapter: EcosystemAdapter = {
127
- name: "Cursor",
128
- detect(cwd: string): boolean {
129
- return existsSync(join(cwd, ".cursorrules"));
130
- },
131
- getHarnessSchema(): HarnessSchema {
132
- return {
133
- configFiles: [".cursorrules", ".cursor/settings.json"],
134
- ignoreFile: ".cursorignore",
135
- rulesFile: ".cursorrules",
136
- hooksFile: null,
137
- };
138
- },
139
- };
140
-
141
- const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter];
142
-
143
- export interface DetectionResult {
144
- adapter: EcosystemAdapter;
145
- confidence: "primary" | "secondary";
146
- }
147
-
148
- export function detectEcosystem(cwd: string): DetectionResult[] {
149
- const results: DetectionResult[] = [];
150
- for (const adapter of adapters) {
151
- if (adapter.detect(cwd)) {
152
- results.push({
153
- adapter,
154
- confidence: results.length === 0 ? "primary" : "secondary",
155
- });
156
- }
157
- }
158
- return results;
159
- }
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { resolveHookCommand } from "../platform";
4
+ import {
5
+ readHooksFile,
6
+ writeHooksFile,
7
+ mergeHooks,
8
+ getAfdDesiredHooks,
9
+ KNOWN_AFD_HOOKS,
10
+ } from "../core/hook-manager";
11
+
12
+ export interface HarnessSchema {
13
+ configFiles: string[];
14
+ ignoreFile: string | null;
15
+ rulesFile: string | null;
16
+ hooksFile: string | null;
17
+ }
18
+
19
+ export interface EcosystemAdapter {
20
+ name: string;
21
+ detect(cwd: string): boolean;
22
+ getHarnessSchema(): HarnessSchema;
23
+ injectHooks?(cwd: string): { injected: boolean; message: string };
24
+ configureStatusLine?(cwd: string): { configured: boolean; message: string };
25
+ registerMcp?(cwd: string): { registered: boolean; message: string };
26
+ removeHooks?(cwd: string): { removed: boolean; message: string };
27
+ unregisterMcp?(cwd: string): { removed: boolean; message: string };
28
+ }
29
+
30
+ const AFD_HOOK_MARKER = "afd-auto-heal";
31
+
32
+ interface HooksConfig {
33
+ hooks?: Record<string, HookEntry[]>;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ interface HookEntry {
38
+ matcher?: string;
39
+ command: string;
40
+ id?: string;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export const ClaudeCodeAdapter: EcosystemAdapter = {
45
+ name: "Claude Code",
46
+ detect(cwd: string): boolean {
47
+ return (
48
+ existsSync(join(cwd, ".claude")) ||
49
+ existsSync(join(cwd, "CLAUDE.md"))
50
+ );
51
+ },
52
+ getHarnessSchema(): HarnessSchema {
53
+ return {
54
+ configFiles: [".claude/settings.json", ".claude/settings.local.json", "CLAUDE.md"],
55
+ ignoreFile: ".claudeignore",
56
+ rulesFile: "CLAUDE.md",
57
+ hooksFile: ".claude/hooks.json",
58
+ };
59
+ },
60
+ injectHooks(cwd: string): { injected: boolean; message: string } {
61
+ const hooksPath = join(cwd, ".claude", "hooks.json");
62
+ const config = readHooksFile(hooksPath);
63
+
64
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
65
+ config.hooks = {};
66
+ }
67
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
68
+
69
+ const before = config.hooks.PreToolUse.length;
70
+ const result = mergeHooks(config.hooks.PreToolUse, getAfdDesiredHooks());
71
+ config.hooks.PreToolUse = result.merged;
72
+ writeHooksFile(hooksPath, config);
73
+
74
+ const added = result.changes.added.length > 0;
75
+ const after = config.hooks.PreToolUse.length;
76
+ if (added) {
77
+ return { injected: true, message: "Auto-heal hook injected into PreToolUse" };
78
+ }
79
+ return {
80
+ injected: false,
81
+ message: `Auto-heal hook already present (${after} hook${after !== 1 ? "s" : ""} total, ordering: afd → omc → user)`,
82
+ };
83
+ },
84
+ configureStatusLine(cwd: string): { configured: boolean; message: string } {
85
+ const settingsPath = join(cwd, ".claude", "settings.local.json");
86
+ const statusScript = join(cwd, ".claude", "statusline-command.js").replace(/\\/g, "/");
87
+
88
+ // Only configure if the statusline-command.js exists
89
+ if (!existsSync(statusScript)) {
90
+ return { configured: false, message: "No statusline-command.js found" };
91
+ }
92
+
93
+ let settings: Record<string, unknown>;
94
+ if (existsSync(settingsPath)) {
95
+ try {
96
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
+ } catch {
98
+ settings = {};
99
+ }
100
+ } else {
101
+ mkdirSync(dirname(settingsPath), { recursive: true });
102
+ settings = {};
103
+ }
104
+
105
+ const expectedCommand = `node ${statusScript}`;
106
+ const current = settings.statusLine as { type?: string; command?: string } | undefined;
107
+
108
+ if (current?.type === "command" && current?.command === expectedCommand) {
109
+ return { configured: false, message: "Status line already configured" };
110
+ }
111
+
112
+ settings.statusLine = { type: "command", command: expectedCommand };
113
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
114
+ return { configured: true, message: "Status line configured with afd integration" };
115
+ },
116
+ registerMcp(cwd: string): { registered: boolean; message: string } {
117
+ const mcpPath = join(cwd, ".mcp.json");
118
+ const serverScript = "src/daemon/server.ts";
119
+ const expectedArgs = ["run", serverScript, "--mcp"];
120
+
121
+ let config: Record<string, unknown>;
122
+ if (existsSync(mcpPath)) {
123
+ try {
124
+ config = JSON.parse(readFileSync(mcpPath, "utf-8"));
125
+ } catch {
126
+ config = {};
127
+ }
128
+ } else {
129
+ config = {};
130
+ }
131
+
132
+ const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
133
+ const existing = mcpServers.afd as { command?: string; args?: string[] } | undefined;
134
+
135
+ if (existing?.command === "bun" &&
136
+ JSON.stringify(existing.args) === JSON.stringify(expectedArgs)) {
137
+ return { registered: false, message: "MCP server already registered in .mcp.json" };
138
+ }
139
+
140
+ mcpServers.afd = {
141
+ command: "bun",
142
+ args: expectedArgs,
143
+ };
144
+ config.mcpServers = mcpServers;
145
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
146
+ return { registered: true, message: "MCP server 'afd' registered in .mcp.json" };
147
+ },
148
+ removeHooks(cwd: string): { removed: boolean; message: string } {
149
+ const hooksPath = join(cwd, ".claude", "hooks.json");
150
+ if (!existsSync(hooksPath)) {
151
+ return { removed: false, message: "No hooks file found" };
152
+ }
153
+ try {
154
+ const config: HooksConfig = JSON.parse(readFileSync(hooksPath, "utf-8"));
155
+ const arr = config.hooks?.PreToolUse;
156
+ if (!arr) return { removed: false, message: "No PreToolUse hooks" };
157
+
158
+ // Only remove hooks that are in the canonical KNOWN_AFD_HOOKS set.
159
+ // User hooks with an `afd-` prefix (e.g., afd-read-gate) are preserved.
160
+ const before = arr.length;
161
+ config.hooks!.PreToolUse = arr.filter(
162
+ (h: HookEntry) => !KNOWN_AFD_HOOKS.has(h.id ?? "")
163
+ );
164
+ const removed = before - config.hooks!.PreToolUse.length;
165
+ if (removed === 0) return { removed: false, message: "No afd-managed hooks found" };
166
+
167
+ writeHooksFile(hooksPath, config);
168
+ return { removed: true, message: `Removed ${removed} afd-managed hook${removed !== 1 ? "s" : ""} from PreToolUse` };
169
+ } catch {
170
+ return { removed: false, message: "Failed to parse hooks file" };
171
+ }
172
+ },
173
+ unregisterMcp(cwd: string): { removed: boolean; message: string } {
174
+ const mcpPath = join(cwd, ".mcp.json");
175
+ if (!existsSync(mcpPath)) {
176
+ return { removed: false, message: "No .mcp.json found" };
177
+ }
178
+ try {
179
+ const config = JSON.parse(readFileSync(mcpPath, "utf-8"));
180
+ const servers = config.mcpServers as Record<string, unknown> | undefined;
181
+ if (!servers?.afd) return { removed: false, message: "afd not in .mcp.json" };
182
+ delete servers.afd;
183
+ if (Object.keys(servers).length === 0) delete config.mcpServers;
184
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
185
+ return { removed: true, message: "MCP server 'afd' removed from .mcp.json" };
186
+ } catch {
187
+ return { removed: false, message: "Failed to parse .mcp.json" };
188
+ }
189
+ },
190
+ };
191
+
192
+ export const CursorAdapter: EcosystemAdapter = {
193
+ name: "Cursor",
194
+ detect(cwd: string): boolean {
195
+ return existsSync(join(cwd, ".cursorrules")) || existsSync(join(cwd, ".cursor"));
196
+ },
197
+ getHarnessSchema(): HarnessSchema {
198
+ return {
199
+ configFiles: [".cursorrules", ".cursor/settings.json"],
200
+ ignoreFile: ".cursorignore",
201
+ rulesFile: ".cursorrules",
202
+ hooksFile: ".cursor/hooks.json",
203
+ };
204
+ },
205
+ injectHooks(cwd: string): { injected: boolean; message: string } {
206
+ // Cursor supports hooks via .cursor/hooks.json (same format as Claude Code)
207
+ const hooksPath = join(cwd, ".cursor", "hooks.json");
208
+ const hookCommand = resolveHookCommand();
209
+
210
+ const newHook: HookEntry = {
211
+ id: AFD_HOOK_MARKER,
212
+ matcher: "",
213
+ command: hookCommand,
214
+ };
215
+
216
+ let config: HooksConfig;
217
+ if (existsSync(hooksPath)) {
218
+ try {
219
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
220
+ } catch {
221
+ config = { hooks: {} };
222
+ }
223
+ } else {
224
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
225
+ config = { hooks: {} };
226
+ }
227
+
228
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
229
+ config.hooks = {};
230
+ }
231
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
232
+
233
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
234
+ if (existing) {
235
+ existing.command = hookCommand;
236
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
237
+ return { injected: false, message: "Cursor: auto-heal hook already present (updated)" };
238
+ }
239
+
240
+ config.hooks.PreToolUse.push(newHook);
241
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
242
+ return { injected: true, message: "Cursor: auto-heal hook injected" };
243
+ },
244
+ };
245
+
246
+ export const WindsurfAdapter: EcosystemAdapter = {
247
+ name: "Windsurf",
248
+ detect(cwd: string): boolean {
249
+ return existsSync(join(cwd, ".windsurfrules")) || existsSync(join(cwd, ".windsurf"));
250
+ },
251
+ getHarnessSchema(): HarnessSchema {
252
+ return {
253
+ configFiles: [".windsurfrules", ".windsurf/settings.json"],
254
+ ignoreFile: ".windsurfignore",
255
+ rulesFile: ".windsurfrules",
256
+ hooksFile: ".windsurf/hooks.json",
257
+ };
258
+ },
259
+ injectHooks(cwd: string): { injected: boolean; message: string } {
260
+ const hooksPath = join(cwd, ".windsurf", "hooks.json");
261
+ const hookCommand = resolveHookCommand();
262
+
263
+ const newHook: HookEntry = {
264
+ id: AFD_HOOK_MARKER,
265
+ matcher: "",
266
+ command: hookCommand,
267
+ };
268
+
269
+ let config: HooksConfig;
270
+ if (existsSync(hooksPath)) {
271
+ try {
272
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
273
+ } catch {
274
+ config = { hooks: {} };
275
+ }
276
+ } else {
277
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true });
278
+ config = { hooks: {} };
279
+ }
280
+
281
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
282
+ config.hooks = {};
283
+ }
284
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
285
+
286
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
287
+ if (existing) {
288
+ existing.command = hookCommand;
289
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
290
+ return { injected: false, message: "Windsurf: auto-heal hook already present (updated)" };
291
+ }
292
+
293
+ config.hooks.PreToolUse.push(newHook);
294
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
295
+ return { injected: true, message: "Windsurf: auto-heal hook injected" };
296
+ },
297
+ };
298
+
299
+ export const CodexAdapter: EcosystemAdapter = {
300
+ name: "Codex",
301
+ detect(cwd: string): boolean {
302
+ return existsSync(join(cwd, "codex.md")) || existsSync(join(cwd, ".codex"));
303
+ },
304
+ getHarnessSchema(): HarnessSchema {
305
+ return {
306
+ configFiles: ["codex.md", ".codex/settings.json"],
307
+ ignoreFile: ".codexignore",
308
+ rulesFile: "codex.md",
309
+ hooksFile: ".codex/hooks.json",
310
+ };
311
+ },
312
+ injectHooks(cwd: string): { injected: boolean; message: string } {
313
+ const hooksPath = join(cwd, ".codex", "hooks.json");
314
+ const hookCommand = resolveHookCommand();
315
+
316
+ const newHook: HookEntry = {
317
+ id: AFD_HOOK_MARKER,
318
+ matcher: "",
319
+ command: hookCommand,
320
+ };
321
+
322
+ let config: HooksConfig;
323
+ if (existsSync(hooksPath)) {
324
+ try {
325
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
326
+ } catch {
327
+ config = { hooks: {} };
328
+ }
329
+ } else {
330
+ mkdirSync(join(cwd, ".codex"), { recursive: true });
331
+ config = { hooks: {} };
332
+ }
333
+
334
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
335
+ config.hooks = {};
336
+ }
337
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
338
+
339
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
340
+ if (existing) {
341
+ existing.command = hookCommand;
342
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
343
+ return { injected: false, message: "Codex: auto-heal hook already present (updated)" };
344
+ }
345
+
346
+ config.hooks.PreToolUse.push(newHook);
347
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
348
+ return { injected: true, message: "Codex: auto-heal hook injected" };
349
+ },
350
+ };
351
+
352
+ const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CodexAdapter];
353
+
354
+ export interface DetectionResult {
355
+ adapter: EcosystemAdapter;
356
+ confidence: "primary" | "secondary";
357
+ }
358
+
359
+ export function detectEcosystem(cwd: string): DetectionResult[] {
360
+ const results: DetectionResult[] = [];
361
+ for (const adapter of adapters) {
362
+ if (adapter.detect(cwd)) {
363
+ results.push({
364
+ adapter,
365
+ confidence: results.length === 0 ? "primary" : "secondary",
366
+ });
367
+ }
368
+ }
369
+ return results;
370
+ }