ima-claude 2.13.0 → 2.14.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  FP patterns, architecture guidance, and team standards for AI coding agents.
4
4
 
5
- **Supports Claude Code, Junie CLI, and Gemini CLI** — with an extensible adapter architecture ready for more platforms.
5
+ **Supports Claude Code, Junie CLI, Gemini CLI, and GitHub Copilot** — with an extensible adapter architecture ready for more platforms.
6
6
 
7
7
  Built by [Independent Medical Alliance](https://imahealth.org) (formerly FLCCC)
8
8
 
@@ -38,9 +38,9 @@ claude plugin update ima-claude
38
38
 
39
39
  Or use `/plugin` inside Claude Code to manage updates interactively via the **Installed** tab.
40
40
 
41
- ### Junie CLI / Gemini CLI — Multi-Platform Installer
41
+ ### Junie CLI / Gemini CLI / GitHub Copilot — Multi-Platform Installer
42
42
 
43
- For Junie, Gemini, or any non-plugin platform, use the interactive CLI installer:
43
+ For Junie, Gemini, GitHub Copilot, or any non-plugin platform, use the interactive CLI installer:
44
44
 
45
45
  ```bash
46
46
  npx ima-claude install
@@ -48,7 +48,7 @@ npx ima-claude install
48
48
 
49
49
  The installer auto-detects which platforms are available and walks you through installation:
50
50
 
51
- 1. **Detects platforms** — scans for Claude Code (`~/.claude`), Junie CLI (`~/.junie`), and Gemini CLI (`~/.gemini`)
51
+ 1. **Detects platforms** — scans for Claude Code (`~/.claude`), Junie CLI (`~/.junie`), Gemini CLI (`~/.gemini`), and GitHub Copilot (`~/.copilot`)
52
52
  2. **Shows preview** — lists all skills, agents, and platform-specific items to install
53
53
  3. **Allows exclusions** — skip specific skills or agents you don't need
54
54
  4. **Installs with feedback** — step-by-step progress for each item
@@ -56,33 +56,35 @@ The installer auto-detects which platforms are available and walks you through i
56
56
  You can also target a specific platform directly:
57
57
 
58
58
  ```bash
59
- npx ima-claude install --target junie # Junie only
60
- npx ima-claude install --target gemini # Gemini CLI only
61
- npx ima-claude install --target claude # Claude Code only (plugin recommended instead)
62
- npx ima-claude detect # Show detected platforms
59
+ npx ima-claude install --target junie # Junie only
60
+ npx ima-claude install --target gemini # Gemini CLI only
61
+ npx ima-claude install --target gh-copilot # GitHub Copilot only
62
+ npx ima-claude install --target claude # Claude Code only (plugin recommended instead)
63
+ npx ima-claude detect # Show detected platforms
63
64
  ```
64
65
 
65
66
  **What's different per platform?**
66
67
 
67
- | | Claude Code | Junie CLI | Gemini CLI |
68
- |---|---|---|---|
69
- | **Skills** | Plugin system (auto) | Copied to `~/.junie/skills/` | Copied to `~/.gemini/skills/` |
70
- | **Agents** | Plugin system (auto) | Strips `permissionMode` | Strips `model` + `permissionMode`, maps tool names |
71
- | **Hooks** | 24 Python hook scripts | No hook system — behavioral guidelines | Translated events + tool names, translator shim |
72
- | **Guidelines** | Plugin's `CLAUDE.md` injection | Generated `AGENTS.md` | Generated `GEMINI.md` |
68
+ | | Claude Code | Junie CLI | Gemini CLI | GitHub Copilot |
69
+ |---|---|---|---|---|
70
+ | **Skills** | Plugin system (auto) | Copied to `~/.junie/skills/` | Copied to `~/.gemini/skills/` | Copied to `~/.copilot/skills/` |
71
+ | **Agents** | Plugin system (auto) | Strips `permissionMode` | Strips `model` + `permissionMode`, maps tool names | Strips `model` + `permissionMode`, maps tool names, renames to `.agent.md` |
72
+ | **Hooks** | 24 Python hook scripts | No hook system — behavioral guidelines | Translated events + tool names, translator shim | Translated events + tool names, translator shim, flattened config |
73
+ | **Guidelines** | Plugin's `CLAUDE.md` injection | Generated `AGENTS.md` | Generated `GEMINI.md` | Generated `copilot-instructions.md` |
73
74
 
74
- Junie doesn't support hooks, so behaviors become guidelines in `AGENTS.md`. Gemini has hooks but uses different event names (`BeforeTool`/`AfterTool`) and tool names (`run_shell_command`, `replace`, etc.) — a translator shim normalizes input so all existing hooks work unmodified.
75
+ Junie doesn't support hooks, so behaviors become guidelines in `AGENTS.md`. Gemini and GitHub Copilot have hooks but use different event/tool names — a translator shim normalizes input so all existing hooks work unmodified. Copilot additionally uses a flat hook config format with `bash` field and `version: 1` wrapper.
75
76
 
76
77
  ### Adding New Platforms
77
78
 
78
- The installer uses an adapter pattern. Adding support for a new platform (e.g., GitHub Copilot) means creating one file implementing the `PlatformAdapter` interface:
79
+ The installer uses an adapter pattern. Adding support for a new platform means creating one file implementing the `PlatformAdapter` interface:
79
80
 
80
81
  ```
81
82
  platforms/
82
- ├── shared/types.ts # PlatformAdapter interface
83
- ├── claude/adapter.ts # Claude Code adapter
84
- ├── junie/adapter.ts # Junie CLI adapter
85
- └── gemini/adapter.ts # Gemini CLI adapter
83
+ ├── shared/types.ts # PlatformAdapter interface
84
+ ├── claude/adapter.ts # Claude Code adapter
85
+ ├── junie/adapter.ts # Junie CLI adapter
86
+ ├── gemini/adapter.ts # Gemini CLI adapter
87
+ └── gh-copilot/adapter.ts # GitHub Copilot adapter
86
88
  ```
87
89
 
88
90
  See [platforms/shared/types.ts](platforms/shared/types.ts) for the interface contract.
@@ -91,7 +93,7 @@ See [platforms/shared/types.ts](platforms/shared/types.ts) for the interface con
91
93
 
92
94
  ## What's Included
93
95
 
94
- - **Multi-Platform Installer**: Interactive CLI with auto-detection, install preview, and per-item exclusion — supports Claude Code, Junie CLI, and Gemini CLI
96
+ - **Multi-Platform Installer**: Interactive CLI with auto-detection, install preview, and per-item exclusion — supports Claude Code, Junie CLI, Gemini CLI, and GitHub Copilot
95
97
  - **48 Skills**: Foundational + FP implementation + domain expert + integration + meta-skills
96
98
  - **6 Named Agents**: Explorer (haiku), Implementer (sonnet), Reviewer (sonnet), Tester (sonnet), WP Developer (sonnet), Memory (sonnet) — enforced constraints
97
99
  - **23 Hooks**: Automatic behavioral enforcement (security, memory, workflow, Serena, code quality) — translated to guidelines for platforms without hook support
@@ -104,7 +106,7 @@ See [platforms/shared/types.ts](platforms/shared/types.ts) for the interface con
104
106
 
105
107
  ## Prerequisites
106
108
 
107
- - [Claude Code](https://claude.ai/code), [Junie CLI](https://www.jetbrains.com/help/idea/junie.html), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed
109
+ - [Claude Code](https://claude.ai/code), [Junie CLI](https://www.jetbrains.com/help/idea/junie.html), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [GitHub Copilot](https://github.com/features/copilot) installed
108
110
 
109
111
  ## MCP Servers (Highly Recommended)
110
112
 
@@ -410,7 +412,7 @@ ima-claude follows a **Persona + Skills** architecture:
410
412
  - **Platform adapters** - Shared `PlatformAdapter` interface; each platform implements detect, preview, install, and guideline generation
411
413
 
412
414
  This makes ima-claude:
413
- 1. **Multi-platform** - Same skills and agents across Claude Code, Junie CLI, and future platforms
415
+ 1. **Multi-platform** - Same skills and agents across Claude Code, Junie CLI, Gemini CLI, and GitHub Copilot
414
416
  2. **Fully standalone** - Complete system without dependencies
415
417
  3. **Consistent** - Same mindset across all interactions
416
418
  4. **Efficient** - Skills load on-demand based on context
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ var HOOKS_DIR = join(CLAUDE_DIR, "hooks");
11
11
  var COMMANDS_DIR = join(CLAUDE_DIR, "commands");
12
12
  var RULES_DIR = join(CLAUDE_DIR, "rules");
13
13
  var SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
14
- var VERSION = "2.13.0";
14
+ var VERSION = "2.14.1";
15
15
  var colors = {
16
16
  reset: "\x1B[0m",
17
17
  bright: "\x1B[1m",
@@ -1051,11 +1051,362 @@ function mergeGeminiHooksIntoSettings(hooksConfig) {
1051
1051
  log.info("Merged hooks into ~/.gemini/settings.json");
1052
1052
  }
1053
1053
 
1054
+ // platforms/gh-copilot/adapter.ts
1055
+ import { join as join5, dirname as dirname3 } from "path";
1056
+ import { homedir as homedir4 } from "os";
1057
+ import { existsSync as existsSync5, readdirSync as readdirSync5, statSync as statSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, copyFileSync as copyFileSync4 } from "fs";
1058
+ import { fileURLToPath as fileURLToPath2 } from "url";
1059
+ var __filename2 = fileURLToPath2(import.meta.url);
1060
+ var __dirname3 = dirname3(__filename2);
1061
+ var COPILOT_DIR = join5(homedir4(), ".copilot");
1062
+ var COPILOT_SKILLS_DIR = join5(COPILOT_DIR, "skills");
1063
+ var COPILOT_AGENTS_DIR = join5(COPILOT_DIR, "agents");
1064
+ var COPILOT_HOOKS_DIR = join5(COPILOT_DIR, "hooks");
1065
+ var COPILOT_GUIDELINES_FILE = join5(COPILOT_DIR, "copilot-instructions.md");
1066
+ var TOOL_MAP2 = {
1067
+ Bash: "run_terminal_command",
1068
+ Read: "read_file",
1069
+ Edit: "edit_file",
1070
+ Write: "write_file",
1071
+ Glob: "find_files",
1072
+ Grep: "search_code",
1073
+ LS: "list_directory",
1074
+ WebSearch: "web_search",
1075
+ WebFetch: "fetch_url",
1076
+ ExitPlanMode: "ExitPlanMode"
1077
+ };
1078
+ var EVENT_MAP2 = {
1079
+ PreToolUse: "preToolUse",
1080
+ PostToolUse: "postToolUse",
1081
+ UserPromptSubmit: "userPromptSubmitted",
1082
+ SessionStart: "sessionStart"
1083
+ };
1084
+ function parseFrontmatter3(content) {
1085
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1086
+ if (!match) return { frontmatter: {}, body: content };
1087
+ const frontmatter = {};
1088
+ for (const line of match[1].split("\n")) {
1089
+ const colonIdx = line.indexOf(":");
1090
+ if (colonIdx === -1) continue;
1091
+ const key = line.slice(0, colonIdx).trim();
1092
+ const value = line.slice(colonIdx + 1).trim();
1093
+ if (key) frontmatter[key] = value;
1094
+ }
1095
+ return { frontmatter, body: match[2] };
1096
+ }
1097
+ function serializeFrontmatter3(frontmatter, body) {
1098
+ const lines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`);
1099
+ return `---
1100
+ ${lines.join("\n")}
1101
+ ---
1102
+ ${body}`;
1103
+ }
1104
+ function mapToolName2(claudeName) {
1105
+ return TOOL_MAP2[claudeName] ?? claudeName;
1106
+ }
1107
+ function transformAgentForCopilot(content) {
1108
+ const { frontmatter, body } = parseFrontmatter3(content);
1109
+ const { permissionMode: _perm, model: _model, ...kept } = frontmatter;
1110
+ if (kept.tools) {
1111
+ const mapped = kept.tools.split(",").map((t) => t.trim()).map(mapToolName2).join(", ");
1112
+ kept.tools = mapped;
1113
+ }
1114
+ return serializeFrontmatter3(kept, body);
1115
+ }
1116
+ function translateMatcher2(matcher) {
1117
+ return TOOL_MAP2[matcher] ?? matcher;
1118
+ }
1119
+ function translateHookCommand2(command2) {
1120
+ const hooksDir = COPILOT_HOOKS_DIR;
1121
+ const translatorPath = join5(hooksDir, "hooks-translator.py");
1122
+ const match = command2.match(/python3\s+.*\/([^/\s]+\.py)(\s.*)?$/);
1123
+ if (!match) return command2;
1124
+ const scriptName = match[1];
1125
+ const trailingArgs = match[2] ?? "";
1126
+ return `python3 ${translatorPath} ${join5(hooksDir, scriptName)}${trailingArgs}`;
1127
+ }
1128
+ function generateCopilotHooksConfig() {
1129
+ const copilotHooks = {};
1130
+ for (const [claudeEvent, hookEntries] of Object.entries(HOOKS_CONFIG.hooks)) {
1131
+ const copilotEvent = EVENT_MAP2[claudeEvent] ?? claudeEvent;
1132
+ const flatEntries = [];
1133
+ for (const entry of hookEntries) {
1134
+ const translatedMatcher = entry.matcher ? translateMatcher2(entry.matcher) : void 0;
1135
+ for (const hook of entry.hooks) {
1136
+ const flatEntry = {};
1137
+ if (translatedMatcher) {
1138
+ flatEntry.matcher = translatedMatcher;
1139
+ }
1140
+ flatEntry.type = hook.type;
1141
+ flatEntry.bash = translateHookCommand2(hook.command);
1142
+ flatEntries.push(flatEntry);
1143
+ }
1144
+ }
1145
+ copilotHooks[copilotEvent] = flatEntries;
1146
+ }
1147
+ return { version: 1, hooks: copilotHooks };
1148
+ }
1149
+ function generateCopilotInstructionsMd() {
1150
+ return `# ima-claude: AI Coding Agent Guidelines
1151
+
1152
+ > Generated by ima-claude v${VERSION} for GitHub Copilot.
1153
+ > Source: https://github.com/Soabirw/ima-claude
1154
+
1155
+ ## Default Persona: The Practitioner
1156
+
1157
+ A 25-year software development veteran. FP-first, composition-minded, anti-over-engineering.
1158
+ Uses "we" not "I" \u2014 collaborative, humble, light-hearted. "Slow is smooth, smooth is fast."
1159
+
1160
+ **Philosophy**: Simple > Complex | Evidence > Assumptions | Native > Utilities | MVP > Enterprise
1161
+
1162
+ ---
1163
+
1164
+ ## Memory Routing
1165
+
1166
+ | Store what | Where | Why |
1167
+ |---|---|---|
1168
+ | Decisions, preferences, patterns, bugs | Vestige \`smart_ingest\` | Fades naturally if not referenced |
1169
+ | Reference material (docs, standards, PRDs) | Qdrant \`qdrant-store\` | Permanent library |
1170
+ | Session state, task progress | Serena \`write_memory\` | Project-scoped workbench |
1171
+ | Future reminders | Vestige \`intention\` | Surfaces at next session |
1172
+
1173
+ At session start, check memory before asking questions:
1174
+ - Vestige: search for user preferences and project context
1175
+ - Vestige: check for pending reminders/intentions
1176
+ - Serena: list memories if in a Serena-activated project
1177
+
1178
+ Auto-store: "I prefer..." \u2192 Vestige preference. "Let's go with X because..." \u2192 Vestige decision. "The reason this failed..." \u2192 Vestige bug.
1179
+
1180
+ After completing work: store outcome in Vestige, reference material in Qdrant, session state in Serena.
1181
+
1182
+ ---
1183
+
1184
+ ## Orchestrator Protocol
1185
+
1186
+ You are the Orchestrator. Plan and delegate. Do NOT implement directly.
1187
+ - Non-trivial work \u2192 task-planner (decompose) \u2192 task-runner (delegate)
1188
+ - Trivial = single file, < 5 lines, no judgment calls
1189
+
1190
+ ---
1191
+
1192
+ ## Available Agents
1193
+
1194
+ Delegate to named agents \u2014 they enforce tools and permissions automatically.
1195
+
1196
+ | Agent | Use For |
1197
+ |---|---|
1198
+ | \`explorer\` | File discovery, codebase exploration |
1199
+ | \`implementer\` | Feature dev, bug fixes, refactoring |
1200
+ | \`reviewer\` | Code review, security audit, FP checks |
1201
+ | \`wp-developer\` | WordPress plugins, themes, WP-CLI, forms |
1202
+ | \`memory\` | Memory search, storage, consolidation |
1203
+
1204
+ ---
1205
+
1206
+ ## Code Navigation (Serena)
1207
+
1208
+ When Serena MCP is available, **prefer Serena over read_file/search_code for code investigation.** 40-70% token savings.
1209
+
1210
+ | Instead of | Use |
1211
+ |---|---|
1212
+ | Read file to understand structure | Serena get_symbols_overview |
1213
+ | search_code for class/function definition | Serena find_symbol |
1214
+ | search_code for callers/references | Serena find_referencing_symbols |
1215
+
1216
+ Use read_file only when you need the actual implementation body of a known, specific symbol.
1217
+
1218
+ ---
1219
+
1220
+ ## Complex Reasoning
1221
+
1222
+ Use sequential thinking before acting on:
1223
+ - Debugging / root cause analysis / "why is this failing"
1224
+ - Trade-off evaluation / "which approach"
1225
+ - Architectural decisions / design choices
1226
+ - Multi-step investigations where approach may change
1227
+
1228
+ ---
1229
+
1230
+ ## MCP Tool Routing
1231
+
1232
+ | Signal | Preferred Tool |
1233
+ |---|---|
1234
+ | "latest", "2025/2026", "what's new" | Tavily search |
1235
+ | Library/framework API question | Context7 |
1236
+ | URL content extraction | Tavily extract (use advanced for complex pages) |
1237
+
1238
+ Before web tools: check internal knowledge \u2192 Context7 \u2192 then Tavily.
1239
+ Before external lookups: check Vestige memory first.
1240
+
1241
+ ---
1242
+
1243
+ ## Search Preference
1244
+
1245
+ Always prefer \`rg\` (ripgrep) over grep/find. Faster, respects .gitignore, recursive by default.
1246
+
1247
+ ---
1248
+
1249
+ ## Security
1250
+
1251
+ - Verify nonce usage and input sanitization in WordPress PHP code
1252
+ - Never concatenate user input directly into SQL \u2014 use parameterized queries
1253
+ - Check for XSS, CSRF, and OWASP top 10 vulnerabilities in written code
1254
+
1255
+ ---
1256
+
1257
+ ## Code Style
1258
+
1259
+ - Don't create custom FP utility functions (pipe, compose, curry) \u2014 use language-native patterns or established libraries
1260
+ - In WordPress JavaScript context, use jQuery patterns when jQuery is already loaded
1261
+ - Prefer Bootstrap utility classes over custom CSS overrides
1262
+ - Run \`composer dump-autoload\` after creating new PHP files
1263
+
1264
+ ---
1265
+
1266
+ ## Documentation
1267
+
1268
+ Follow the three-tier documentation system:
1269
+ - **Active** \u2014 Living docs, kept current (README, API docs, architecture)
1270
+ - **Archive** \u2014 Historical reference, rarely updated (decisions, post-mortems)
1271
+ - **Transient** \u2014 Ephemeral, git-ignored (session notes, scratch)
1272
+ `;
1273
+ }
1274
+ var GhCopilotAdapter = class {
1275
+ name = "gh-copilot";
1276
+ displayName = "GitHub Copilot";
1277
+ configDir = COPILOT_DIR;
1278
+ detect() {
1279
+ return existsSync5(COPILOT_DIR);
1280
+ }
1281
+ preview(sourceDir) {
1282
+ const skillItems = SKILLS_TO_INSTALL.map((skill) => ({
1283
+ name: skill,
1284
+ category: "skill",
1285
+ destPath: join5(COPILOT_SKILLS_DIR, skill),
1286
+ exists: existsSync5(join5(COPILOT_SKILLS_DIR, skill))
1287
+ })).filter((item) => existsSync5(join5(sourceDir, "skills", item.name)));
1288
+ const agentsDir = join5(sourceDir, "agents");
1289
+ const agentItems = existsSync5(agentsDir) ? readdirSync5(agentsDir).filter((f) => f.endsWith(".md")).map((file) => ({
1290
+ name: file.replace(/\.md$/, ""),
1291
+ category: "agent",
1292
+ destPath: join5(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md")),
1293
+ exists: existsSync5(join5(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md")))
1294
+ })) : [];
1295
+ const hookItems = HOOKS_TO_INSTALL.map((file) => ({
1296
+ name: file,
1297
+ category: "hook",
1298
+ destPath: join5(COPILOT_HOOKS_DIR, file),
1299
+ exists: existsSync5(join5(COPILOT_HOOKS_DIR, file))
1300
+ }));
1301
+ const translatorItem = {
1302
+ name: "hooks-translator.py",
1303
+ category: "hook",
1304
+ destPath: join5(COPILOT_HOOKS_DIR, "hooks-translator.py"),
1305
+ exists: existsSync5(join5(COPILOT_HOOKS_DIR, "hooks-translator.py"))
1306
+ };
1307
+ const guidelineItem = {
1308
+ name: "copilot-instructions.md",
1309
+ category: "guideline",
1310
+ destPath: COPILOT_GUIDELINES_FILE,
1311
+ exists: existsSync5(COPILOT_GUIDELINES_FILE)
1312
+ };
1313
+ return {
1314
+ platform: this.name,
1315
+ targetDir: COPILOT_DIR,
1316
+ items: [...skillItems, ...agentItems, ...hookItems, translatorItem, guidelineItem]
1317
+ };
1318
+ }
1319
+ installSkills(sourceDir, exclude) {
1320
+ ensureDir(COPILOT_SKILLS_DIR);
1321
+ const skills = exclude?.length ? SKILLS_TO_INSTALL.filter((s) => !exclude.includes(s)) : SKILLS_TO_INSTALL;
1322
+ for (const skill of skills) {
1323
+ const src = join5(sourceDir, skill);
1324
+ if (existsSync5(src) && statSync5(src).isDirectory()) {
1325
+ copyDirRecursive(src, join5(COPILOT_SKILLS_DIR, skill));
1326
+ log.step(`skill: ${skill}`);
1327
+ }
1328
+ }
1329
+ }
1330
+ installAgents(sourceDir, exclude) {
1331
+ ensureDir(COPILOT_AGENTS_DIR);
1332
+ const entries = readdirSync5(sourceDir).filter((f) => f.endsWith(".md")).filter((f) => !exclude?.includes(f.replace(/\.md$/, "")));
1333
+ for (const file of entries) {
1334
+ const content = readFileSync4(join5(sourceDir, file), "utf8");
1335
+ const transformed = transformAgentForCopilot(content);
1336
+ const destFile = file.replace(/\.md$/, ".agent.md");
1337
+ writeFileSync4(join5(COPILOT_AGENTS_DIR, destFile), transformed);
1338
+ log.step(`agent: ${destFile}`);
1339
+ }
1340
+ }
1341
+ installGuidelines(_pluginRoot) {
1342
+ ensureDir(COPILOT_DIR);
1343
+ writeFileSync4(COPILOT_GUIDELINES_FILE, generateCopilotInstructionsMd());
1344
+ log.step(`guidelines: ${COPILOT_GUIDELINES_FILE}`);
1345
+ }
1346
+ installHooks(sourceDir, exclude) {
1347
+ ensureDir(COPILOT_HOOKS_DIR);
1348
+ const hooks = exclude?.length ? HOOKS_TO_INSTALL.filter((f) => !exclude.includes(f)) : HOOKS_TO_INSTALL;
1349
+ for (const file of hooks) {
1350
+ const src = join5(sourceDir, file);
1351
+ if (existsSync5(src)) {
1352
+ copyFileSync4(src, join5(COPILOT_HOOKS_DIR, file));
1353
+ log.step(`hook: ${file}`);
1354
+ }
1355
+ }
1356
+ const shimSrc = join5(__dirname3, "hooks-translator.py");
1357
+ if (!existsSync5(shimSrc)) {
1358
+ throw new Error(`hooks-translator.py not found at ${shimSrc} \u2014 packaging error`);
1359
+ }
1360
+ copyFileSync4(shimSrc, join5(COPILOT_HOOKS_DIR, "hooks-translator.py"));
1361
+ log.step("hook: hooks-translator.py (shim)");
1362
+ const hooksConfig = generateCopilotHooksConfig();
1363
+ const hooksConfigPath = join5(COPILOT_HOOKS_DIR, "hooks.json");
1364
+ mergeCopilotHooksConfig(hooksConfigPath, hooksConfig);
1365
+ log.step("hook: hooks.json (generated for GitHub Copilot)");
1366
+ }
1367
+ postInstall() {
1368
+ log.info("GitHub Copilot install complete. Verify:");
1369
+ log.info(` Skills: ${COPILOT_SKILLS_DIR}`);
1370
+ log.info(` Agents: ${COPILOT_AGENTS_DIR}`);
1371
+ log.info(` Hooks: ${COPILOT_HOOKS_DIR}`);
1372
+ log.info(` Guidelines: ${COPILOT_GUIDELINES_FILE}`);
1373
+ }
1374
+ };
1375
+ function mergeCopilotHooksConfig(configPath, newConfig) {
1376
+ let existing = {};
1377
+ if (existsSync5(configPath)) {
1378
+ try {
1379
+ const content = readFileSync4(configPath, "utf8");
1380
+ existing = JSON.parse(content);
1381
+ } catch {
1382
+ existing = {};
1383
+ }
1384
+ }
1385
+ existing.version = newConfig.version ?? 1;
1386
+ if (!existing.hooks) {
1387
+ existing.hooks = {};
1388
+ }
1389
+ const existingHooks = existing.hooks;
1390
+ const incomingHooks = newConfig.hooks;
1391
+ for (const [event, entries] of Object.entries(incomingHooks)) {
1392
+ if (!existingHooks[event]) {
1393
+ existingHooks[event] = entries;
1394
+ continue;
1395
+ }
1396
+ const userHooks = existingHooks[event].filter(
1397
+ (h) => !h.bash?.includes("hooks-translator.py")
1398
+ );
1399
+ existingHooks[event] = [...userHooks, ...entries];
1400
+ }
1401
+ writeFileSync4(configPath, JSON.stringify(existing, null, 2) + "\n");
1402
+ }
1403
+
1054
1404
  // platforms/shared/detector.ts
1055
1405
  var ADAPTERS = [
1056
1406
  new ClaudeAdapter(),
1057
1407
  new JunieAdapter(),
1058
- new GeminiAdapter()
1408
+ new GeminiAdapter(),
1409
+ new GhCopilotAdapter()
1059
1410
  ];
1060
1411
  function detectPlatforms() {
1061
1412
  return ADAPTERS.map((adapter) => {
@@ -1069,28 +1420,28 @@ function getAdapter(name) {
1069
1420
  }
1070
1421
 
1071
1422
  // platforms/shared/installer.ts
1072
- import { join as join6 } from "path";
1423
+ import { join as join7 } from "path";
1073
1424
 
1074
1425
  // platforms/shared/types.ts
1075
- import { join as join5, resolve, dirname as dirname3 } from "path";
1076
- import { existsSync as existsSync5 } from "fs";
1077
- import { fileURLToPath as fileURLToPath2 } from "url";
1426
+ import { join as join6, resolve, dirname as dirname4 } from "path";
1427
+ import { existsSync as existsSync6 } from "fs";
1428
+ import { fileURLToPath as fileURLToPath3 } from "url";
1078
1429
  function findPackageRoot() {
1079
1430
  let dir;
1080
1431
  try {
1081
- dir = dirname3(fileURLToPath2(import.meta.url));
1432
+ dir = dirname4(fileURLToPath3(import.meta.url));
1082
1433
  } catch {
1083
1434
  dir = typeof __dirname !== "undefined" ? __dirname : resolve(".");
1084
1435
  }
1085
1436
  for (let i = 0; i < 10; i++) {
1086
- if (existsSync5(join5(dir, "package.json"))) return dir;
1087
- const parent = join5(dir, "..");
1437
+ if (existsSync6(join6(dir, "package.json"))) return dir;
1438
+ const parent = join6(dir, "..");
1088
1439
  if (parent === dir) break;
1089
1440
  dir = parent;
1090
1441
  }
1091
1442
  return process.cwd();
1092
1443
  }
1093
- var PLUGIN_SOURCE = join5(
1444
+ var PLUGIN_SOURCE = join6(
1094
1445
  findPackageRoot(),
1095
1446
  "plugins",
1096
1447
  "ima-claude"
@@ -1206,9 +1557,9 @@ ${colors.bright}Hooks${colors.reset} (${hooks.length} total)`
1206
1557
  return filter;
1207
1558
  }
1208
1559
  async function installForPlatform(adapter, options = {}) {
1209
- const skillsSource = join6(PLUGIN_SOURCE, "skills");
1210
- const agentsSource = join6(PLUGIN_SOURCE, "agents");
1211
- const hooksSource = join6(PLUGIN_SOURCE, "hooks");
1560
+ const skillsSource = join7(PLUGIN_SOURCE, "skills");
1561
+ const agentsSource = join7(PLUGIN_SOURCE, "agents");
1562
+ const hooksSource = join7(PLUGIN_SOURCE, "hooks");
1212
1563
  const preview = adapter.preview(PLUGIN_SOURCE);
1213
1564
  showPreview(adapter, preview.items);
1214
1565
  const filter = await promptExclusions(preview.items);
@@ -1276,7 +1627,7 @@ ${colors.cyan}Usage:${colors.reset}
1276
1627
 
1277
1628
  ${colors.cyan}Commands:${colors.reset}
1278
1629
  install Interactive install (auto-detects platforms)
1279
- install --target X Install for specific platform (claude, junie, gemini)
1630
+ install --target X Install for specific platform (claude, junie, gemini, gh-copilot)
1280
1631
  upgrade Upgrade installed skills to latest version
1281
1632
  detect Show detected platforms
1282
1633
  help Show this help message
@@ -1334,7 +1685,7 @@ ${colors.bright}ima-claude v${VERSION} \u2014 Multi-Platform Installer${colors.r
1334
1685
  `
1335
1686
  ${colors.yellow}No supported platforms detected.${colors.reset}`
1336
1687
  );
1337
- console.log("Install Claude Code, Junie CLI, or Gemini CLI first, then run this installer again.\n");
1688
+ console.log("Install Claude Code, Junie CLI, Gemini CLI, or GitHub Copilot first, then run this installer again.\n");
1338
1689
  return;
1339
1690
  }
1340
1691
  console.log("");
@@ -1366,7 +1717,7 @@ async function targetedInstall(targetName) {
1366
1717
  const adapter = getAdapter(targetName);
1367
1718
  if (!adapter) {
1368
1719
  log.error(`Unknown target: ${targetName}`);
1369
- console.log("Available targets: claude, junie, gemini");
1720
+ console.log("Available targets: claude, junie, gemini, gh-copilot");
1370
1721
  process.exit(1);
1371
1722
  }
1372
1723
  if (!adapter.detect()) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ima-claude",
3
- "version": "2.13.0",
4
- "description": "IMA's AI coding agent skills - FP patterns, architecture guidance, and team standards. Supports Claude Code, Junie CLI, and more.",
3
+ "version": "2.14.1",
4
+ "description": "IMA's AI coding agent skills - FP patterns, architecture guidance, and team standards. Supports Claude Code, Junie CLI, Gemini CLI, and GitHub Copilot.",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup",
@@ -0,0 +1,437 @@
1
+ import { join, dirname } from "path";
2
+ import { homedir } from "os";
3
+ import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+
6
+ import type { PlatformAdapter, InstallItem, InstallPreview } from "../shared/types";
7
+ import { ensureDir, copyDirRecursive, log, SKILLS_TO_INSTALL, HOOKS_TO_INSTALL, HOOKS_CONFIG, VERSION } from "../../scripts/utils";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ const COPILOT_DIR = join(homedir(), ".copilot");
13
+ const COPILOT_SKILLS_DIR = join(COPILOT_DIR, "skills");
14
+ const COPILOT_AGENTS_DIR = join(COPILOT_DIR, "agents");
15
+ const COPILOT_HOOKS_DIR = join(COPILOT_DIR, "hooks");
16
+ const COPILOT_GUIDELINES_FILE = join(COPILOT_DIR, "copilot-instructions.md");
17
+
18
+ // Claude Code → GitHub Copilot tool name mapping
19
+ const TOOL_MAP: Record<string, string> = {
20
+ Bash: "run_terminal_command",
21
+ Read: "read_file",
22
+ Edit: "edit_file",
23
+ Write: "write_file",
24
+ Glob: "find_files",
25
+ Grep: "search_code",
26
+ LS: "list_directory",
27
+ WebSearch: "web_search",
28
+ WebFetch: "fetch_url",
29
+ ExitPlanMode: "ExitPlanMode",
30
+ };
31
+
32
+ // Claude Code → GitHub Copilot hook event mapping
33
+ const EVENT_MAP: Record<string, string> = {
34
+ PreToolUse: "preToolUse",
35
+ PostToolUse: "postToolUse",
36
+ UserPromptSubmit: "userPromptSubmitted",
37
+ SessionStart: "sessionStart",
38
+ };
39
+
40
+ // Simple single-line YAML parser — same as Gemini/Junie adapters.
41
+ function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
42
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
43
+ if (!match) return { frontmatter: {}, body: content };
44
+
45
+ const frontmatter: Record<string, string> = {};
46
+ for (const line of match[1].split("\n")) {
47
+ const colonIdx = line.indexOf(":");
48
+ if (colonIdx === -1) continue;
49
+ const key = line.slice(0, colonIdx).trim();
50
+ const value = line.slice(colonIdx + 1).trim();
51
+ if (key) frontmatter[key] = value;
52
+ }
53
+
54
+ return { frontmatter, body: match[2] };
55
+ }
56
+
57
+ function serializeFrontmatter(frontmatter: Record<string, string>, body: string): string {
58
+ const lines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`);
59
+ return `---\n${lines.join("\n")}\n---\n${body}`;
60
+ }
61
+
62
+ function mapToolName(claudeName: string): string {
63
+ return TOOL_MAP[claudeName] ?? claudeName;
64
+ }
65
+
66
+ function transformAgentForCopilot(content: string): string {
67
+ const { frontmatter, body } = parseFrontmatter(content);
68
+
69
+ // Drop permissionMode (Copilot has no equivalent)
70
+ // Drop model (Copilot uses its own model selection)
71
+ const { permissionMode: _perm, model: _model, ...kept } = frontmatter;
72
+
73
+ // Map tool names in the tools field if present
74
+ if (kept.tools) {
75
+ const mapped = kept.tools
76
+ .split(",")
77
+ .map((t) => t.trim())
78
+ .map(mapToolName)
79
+ .join(", ");
80
+ kept.tools = mapped;
81
+ }
82
+
83
+ return serializeFrontmatter(kept, body);
84
+ }
85
+
86
+ function translateMatcher(matcher: string): string {
87
+ // MCP tool matchers pass through unchanged; only map built-in Claude tool names
88
+ return TOOL_MAP[matcher] ?? matcher;
89
+ }
90
+
91
+ function translateHookCommand(command: string): string {
92
+ // Rewrite hook commands to route through the translator shim
93
+ // Original: python3 ~/.claude/hooks/some_hook.py
94
+ // Copilot: python3 ~/.copilot/hooks/hooks-translator.py ~/.copilot/hooks/some_hook.py
95
+ const hooksDir = COPILOT_HOOKS_DIR;
96
+ const translatorPath = join(hooksDir, "hooks-translator.py");
97
+
98
+ // Extract the script filename (and any trailing args) from the original command
99
+ const match = command.match(/python3\s+.*\/([^/\s]+\.py)(\s.*)?$/);
100
+ if (!match) return command;
101
+
102
+ const scriptName = match[1];
103
+ const trailingArgs = match[2] ?? "";
104
+ return `python3 ${translatorPath} ${join(hooksDir, scriptName)}${trailingArgs}`;
105
+ }
106
+
107
+ function generateCopilotHooksConfig(): Record<string, unknown> {
108
+ const copilotHooks: Record<string, unknown[]> = {};
109
+
110
+ for (const [claudeEvent, hookEntries] of Object.entries(HOOKS_CONFIG.hooks)) {
111
+ const copilotEvent = EVENT_MAP[claudeEvent] ?? claudeEvent;
112
+
113
+ // Flatten: Claude groups multiple hooks under one matcher,
114
+ // Copilot uses one flat entry per hook command
115
+ const flatEntries: Record<string, unknown>[] = [];
116
+
117
+ for (const entry of hookEntries as Array<{ matcher?: string; hooks: Array<{ type: string; command: string }> }>) {
118
+ const translatedMatcher = entry.matcher ? translateMatcher(entry.matcher) : undefined;
119
+
120
+ for (const hook of entry.hooks) {
121
+ const flatEntry: Record<string, unknown> = {};
122
+ if (translatedMatcher) {
123
+ flatEntry.matcher = translatedMatcher;
124
+ }
125
+ flatEntry.type = hook.type;
126
+ flatEntry.bash = translateHookCommand(hook.command);
127
+ flatEntries.push(flatEntry);
128
+ }
129
+ }
130
+
131
+ copilotHooks[copilotEvent] = flatEntries;
132
+ }
133
+
134
+ return { version: 1, hooks: copilotHooks };
135
+ }
136
+
137
+ function generateCopilotInstructionsMd(): string {
138
+ return `# ima-claude: AI Coding Agent Guidelines
139
+
140
+ > Generated by ima-claude v${VERSION} for GitHub Copilot.
141
+ > Source: https://github.com/Soabirw/ima-claude
142
+
143
+ ## Default Persona: The Practitioner
144
+
145
+ A 25-year software development veteran. FP-first, composition-minded, anti-over-engineering.
146
+ Uses "we" not "I" — collaborative, humble, light-hearted. "Slow is smooth, smooth is fast."
147
+
148
+ **Philosophy**: Simple > Complex | Evidence > Assumptions | Native > Utilities | MVP > Enterprise
149
+
150
+ ---
151
+
152
+ ## Memory Routing
153
+
154
+ | Store what | Where | Why |
155
+ |---|---|---|
156
+ | Decisions, preferences, patterns, bugs | Vestige \`smart_ingest\` | Fades naturally if not referenced |
157
+ | Reference material (docs, standards, PRDs) | Qdrant \`qdrant-store\` | Permanent library |
158
+ | Session state, task progress | Serena \`write_memory\` | Project-scoped workbench |
159
+ | Future reminders | Vestige \`intention\` | Surfaces at next session |
160
+
161
+ At session start, check memory before asking questions:
162
+ - Vestige: search for user preferences and project context
163
+ - Vestige: check for pending reminders/intentions
164
+ - Serena: list memories if in a Serena-activated project
165
+
166
+ Auto-store: "I prefer..." → Vestige preference. "Let's go with X because..." → Vestige decision. "The reason this failed..." → Vestige bug.
167
+
168
+ After completing work: store outcome in Vestige, reference material in Qdrant, session state in Serena.
169
+
170
+ ---
171
+
172
+ ## Orchestrator Protocol
173
+
174
+ You are the Orchestrator. Plan and delegate. Do NOT implement directly.
175
+ - Non-trivial work → task-planner (decompose) → task-runner (delegate)
176
+ - Trivial = single file, < 5 lines, no judgment calls
177
+
178
+ ---
179
+
180
+ ## Available Agents
181
+
182
+ Delegate to named agents — they enforce tools and permissions automatically.
183
+
184
+ | Agent | Use For |
185
+ |---|---|
186
+ | \`explorer\` | File discovery, codebase exploration |
187
+ | \`implementer\` | Feature dev, bug fixes, refactoring |
188
+ | \`reviewer\` | Code review, security audit, FP checks |
189
+ | \`wp-developer\` | WordPress plugins, themes, WP-CLI, forms |
190
+ | \`memory\` | Memory search, storage, consolidation |
191
+
192
+ ---
193
+
194
+ ## Code Navigation (Serena)
195
+
196
+ When Serena MCP is available, **prefer Serena over read_file/search_code for code investigation.** 40-70% token savings.
197
+
198
+ | Instead of | Use |
199
+ |---|---|
200
+ | Read file to understand structure | Serena get_symbols_overview |
201
+ | search_code for class/function definition | Serena find_symbol |
202
+ | search_code for callers/references | Serena find_referencing_symbols |
203
+
204
+ Use read_file only when you need the actual implementation body of a known, specific symbol.
205
+
206
+ ---
207
+
208
+ ## Complex Reasoning
209
+
210
+ Use sequential thinking before acting on:
211
+ - Debugging / root cause analysis / "why is this failing"
212
+ - Trade-off evaluation / "which approach"
213
+ - Architectural decisions / design choices
214
+ - Multi-step investigations where approach may change
215
+
216
+ ---
217
+
218
+ ## MCP Tool Routing
219
+
220
+ | Signal | Preferred Tool |
221
+ |---|---|
222
+ | "latest", "2025/2026", "what's new" | Tavily search |
223
+ | Library/framework API question | Context7 |
224
+ | URL content extraction | Tavily extract (use advanced for complex pages) |
225
+
226
+ Before web tools: check internal knowledge → Context7 → then Tavily.
227
+ Before external lookups: check Vestige memory first.
228
+
229
+ ---
230
+
231
+ ## Search Preference
232
+
233
+ Always prefer \`rg\` (ripgrep) over grep/find. Faster, respects .gitignore, recursive by default.
234
+
235
+ ---
236
+
237
+ ## Security
238
+
239
+ - Verify nonce usage and input sanitization in WordPress PHP code
240
+ - Never concatenate user input directly into SQL — use parameterized queries
241
+ - Check for XSS, CSRF, and OWASP top 10 vulnerabilities in written code
242
+
243
+ ---
244
+
245
+ ## Code Style
246
+
247
+ - Don't create custom FP utility functions (pipe, compose, curry) — use language-native patterns or established libraries
248
+ - In WordPress JavaScript context, use jQuery patterns when jQuery is already loaded
249
+ - Prefer Bootstrap utility classes over custom CSS overrides
250
+ - Run \`composer dump-autoload\` after creating new PHP files
251
+
252
+ ---
253
+
254
+ ## Documentation
255
+
256
+ Follow the three-tier documentation system:
257
+ - **Active** — Living docs, kept current (README, API docs, architecture)
258
+ - **Archive** — Historical reference, rarely updated (decisions, post-mortems)
259
+ - **Transient** — Ephemeral, git-ignored (session notes, scratch)
260
+ `;
261
+ }
262
+
263
+ export class GhCopilotAdapter implements PlatformAdapter {
264
+ readonly name = "gh-copilot";
265
+ readonly displayName = "GitHub Copilot";
266
+ readonly configDir = COPILOT_DIR;
267
+
268
+ detect(): boolean {
269
+ return existsSync(COPILOT_DIR);
270
+ }
271
+
272
+ preview(sourceDir: string): InstallPreview {
273
+ const skillItems: InstallItem[] = SKILLS_TO_INSTALL.map((skill) => ({
274
+ name: skill,
275
+ category: "skill" as const,
276
+ destPath: join(COPILOT_SKILLS_DIR, skill),
277
+ exists: existsSync(join(COPILOT_SKILLS_DIR, skill)),
278
+ })).filter((item) => existsSync(join(sourceDir, "skills", item.name)));
279
+
280
+ const agentsDir = join(sourceDir, "agents");
281
+ const agentItems: InstallItem[] = existsSync(agentsDir)
282
+ ? readdirSync(agentsDir)
283
+ .filter((f) => f.endsWith(".md"))
284
+ .map((file) => ({
285
+ name: file.replace(/\.md$/, ""),
286
+ category: "agent" as const,
287
+ destPath: join(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md")),
288
+ exists: existsSync(join(COPILOT_AGENTS_DIR, file.replace(/\.md$/, ".agent.md"))),
289
+ }))
290
+ : [];
291
+
292
+ const hookItems: InstallItem[] = HOOKS_TO_INSTALL.map((file) => ({
293
+ name: file,
294
+ category: "hook" as const,
295
+ destPath: join(COPILOT_HOOKS_DIR, file),
296
+ exists: existsSync(join(COPILOT_HOOKS_DIR, file)),
297
+ }));
298
+
299
+ // Include the translator shim and generated hooks.json
300
+ const translatorItem: InstallItem = {
301
+ name: "hooks-translator.py",
302
+ category: "hook",
303
+ destPath: join(COPILOT_HOOKS_DIR, "hooks-translator.py"),
304
+ exists: existsSync(join(COPILOT_HOOKS_DIR, "hooks-translator.py")),
305
+ };
306
+
307
+ const guidelineItem: InstallItem = {
308
+ name: "copilot-instructions.md",
309
+ category: "guideline",
310
+ destPath: COPILOT_GUIDELINES_FILE,
311
+ exists: existsSync(COPILOT_GUIDELINES_FILE),
312
+ };
313
+
314
+ return {
315
+ platform: this.name,
316
+ targetDir: COPILOT_DIR,
317
+ items: [...skillItems, ...agentItems, ...hookItems, translatorItem, guidelineItem],
318
+ };
319
+ }
320
+
321
+ installSkills(sourceDir: string, exclude?: string[]): void {
322
+ ensureDir(COPILOT_SKILLS_DIR);
323
+ const skills = exclude?.length
324
+ ? SKILLS_TO_INSTALL.filter((s) => !exclude.includes(s))
325
+ : SKILLS_TO_INSTALL;
326
+ for (const skill of skills) {
327
+ const src = join(sourceDir, skill);
328
+ if (existsSync(src) && statSync(src).isDirectory()) {
329
+ copyDirRecursive(src, join(COPILOT_SKILLS_DIR, skill));
330
+ log.step(`skill: ${skill}`);
331
+ }
332
+ }
333
+ }
334
+
335
+ installAgents(sourceDir: string, exclude?: string[]): void {
336
+ ensureDir(COPILOT_AGENTS_DIR);
337
+ const entries = readdirSync(sourceDir)
338
+ .filter((f) => f.endsWith(".md"))
339
+ .filter((f) => !exclude?.includes(f.replace(/\.md$/, "")));
340
+ for (const file of entries) {
341
+ const content = readFileSync(join(sourceDir, file), "utf8");
342
+ const transformed = transformAgentForCopilot(content);
343
+ // Copilot uses .agent.md extension
344
+ const destFile = file.replace(/\.md$/, ".agent.md");
345
+ writeFileSync(join(COPILOT_AGENTS_DIR, destFile), transformed);
346
+ log.step(`agent: ${destFile}`);
347
+ }
348
+ }
349
+
350
+ installGuidelines(_pluginRoot: string): void {
351
+ ensureDir(COPILOT_DIR);
352
+ writeFileSync(COPILOT_GUIDELINES_FILE, generateCopilotInstructionsMd());
353
+ log.step(`guidelines: ${COPILOT_GUIDELINES_FILE}`);
354
+ }
355
+
356
+ installHooks(sourceDir: string, exclude?: string[]): void {
357
+ ensureDir(COPILOT_HOOKS_DIR);
358
+
359
+ // Copy hook scripts
360
+ const hooks = exclude?.length
361
+ ? HOOKS_TO_INSTALL.filter((f) => !exclude.includes(f))
362
+ : HOOKS_TO_INSTALL;
363
+ for (const file of hooks) {
364
+ const src = join(sourceDir, file);
365
+ if (existsSync(src)) {
366
+ copyFileSync(src, join(COPILOT_HOOKS_DIR, file));
367
+ log.step(`hook: ${file}`);
368
+ }
369
+ }
370
+
371
+ // Copy the translator shim from the platform directory
372
+ const shimSrc = join(__dirname, "hooks-translator.py");
373
+ if (!existsSync(shimSrc)) {
374
+ throw new Error(`hooks-translator.py not found at ${shimSrc} — packaging error`);
375
+ }
376
+ copyFileSync(shimSrc, join(COPILOT_HOOKS_DIR, "hooks-translator.py"));
377
+ log.step("hook: hooks-translator.py (shim)");
378
+
379
+ // Generate Copilot-specific hooks.json
380
+ const hooksConfig = generateCopilotHooksConfig();
381
+ const hooksConfigPath = join(COPILOT_HOOKS_DIR, "hooks.json");
382
+ mergeCopilotHooksConfig(hooksConfigPath, hooksConfig);
383
+ log.step("hook: hooks.json (generated for GitHub Copilot)");
384
+ }
385
+
386
+ postInstall(): void {
387
+ log.info("GitHub Copilot install complete. Verify:");
388
+ log.info(` Skills: ${COPILOT_SKILLS_DIR}`);
389
+ log.info(` Agents: ${COPILOT_AGENTS_DIR}`);
390
+ log.info(` Hooks: ${COPILOT_HOOKS_DIR}`);
391
+ log.info(` Guidelines: ${COPILOT_GUIDELINES_FILE}`);
392
+ }
393
+ }
394
+
395
+ function mergeCopilotHooksConfig(
396
+ configPath: string,
397
+ newConfig: Record<string, unknown>
398
+ ): void {
399
+ let existing: Record<string, unknown> = {};
400
+
401
+ if (existsSync(configPath)) {
402
+ try {
403
+ const content = readFileSync(configPath, "utf8");
404
+ existing = JSON.parse(content);
405
+ } catch {
406
+ existing = {};
407
+ }
408
+ }
409
+
410
+ // Start with version from new config
411
+ existing.version = (newConfig as Record<string, unknown>).version ?? 1;
412
+
413
+ if (!existing.hooks) {
414
+ existing.hooks = {};
415
+ }
416
+ const existingHooks = existing.hooks as Record<string, unknown[]>;
417
+ const incomingHooks = (newConfig as Record<string, unknown>).hooks as Record<string, Array<{ matcher?: string }>>;
418
+
419
+ // Merge each event type: remove old ima-claude entries, then add new ones.
420
+ // User hooks (without hooks-translator.py in the command) are preserved.
421
+ for (const [event, entries] of Object.entries(incomingHooks)) {
422
+ if (!existingHooks[event]) {
423
+ existingHooks[event] = entries;
424
+ continue;
425
+ }
426
+
427
+ // Strip all old ima-claude entries (identified by hooks-translator.py in the bash command)
428
+ const userHooks = (existingHooks[event] as Array<{ bash?: string }>).filter(
429
+ (h) => !h.bash?.includes("hooks-translator.py")
430
+ );
431
+
432
+ // Append new ima-claude entries
433
+ existingHooks[event] = [...userHooks, ...entries];
434
+ }
435
+
436
+ writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
437
+ }
@@ -0,0 +1,91 @@
1
+ # Hook-to-Copilot Translation Map
2
+
3
+ This document tracks the relationship between Claude Code hooks and their
4
+ GitHub Copilot equivalents. GitHub Copilot has full hook support (preToolUse,
5
+ postToolUse, userPromptSubmitted, sessionStart), so all hooks translate 1:1
6
+ via the hooks-translator.py shim — the same strategy as Gemini CLI.
7
+
8
+ ## Translation Strategy
9
+
10
+ Copilot's hook system is functionally equivalent to Claude Code's. The
11
+ `hooks-translator.py` shim sits between Copilot and each hook script,
12
+ translating Copilot tool names to Claude Code equivalents so hook scripts
13
+ work unmodified.
14
+
15
+ Key differences from Claude Code:
16
+ - Hook config uses `{ "version": 1, "hooks": { ... } }` format
17
+ - Event names are camelCase: `preToolUse`, `postToolUse`, `userPromptSubmitted`
18
+ - Each hook entry is flat: `{ matcher, type, bash }` (not grouped)
19
+ - Uses `bash` field (not `command`)
20
+
21
+ ## Hook → Copilot Mapping
22
+
23
+ ### Tool Redirection Hooks → Translated (via shim)
24
+
25
+ | Hook | Copilot Event | Matcher | Notes |
26
+ |------|--------------|---------|-------|
27
+ | `enforce_rg_over_grep.py` | preToolUse | run_terminal_command | Translates to Bash before hook runs |
28
+ | `webfetch_to_tavily.py` | preToolUse | fetch_url | Translates to WebFetch before hook runs |
29
+ | `websearch_to_tavily.py` | preToolUse | web_search | Translates to WebSearch before hook runs |
30
+ | `tavily_extract_advanced.py` | preToolUse | mcp__tavily__tavily-extract | MCP matcher passthrough |
31
+
32
+ ### Memory Hooks → Translated (via shim)
33
+
34
+ | Hook | Copilot Event | Notes |
35
+ |------|--------------|-------|
36
+ | `memory_bootstrap.py` | preToolUse | Runs on multiple tool matchers |
37
+ | `memory_store_reminder.py` | postToolUse | Runs on Edit/Write |
38
+ | `vestige_before_external.py` | preToolUse | Runs on Tavily/Context7 MCP tools |
39
+
40
+ ### Serena Hooks → Translated (via shim)
41
+
42
+ | Hook | Copilot Event | Notes |
43
+ |------|--------------|-------|
44
+ | `serena_over_read.py` | preToolUse | read_file → Read translation |
45
+ | `serena_over_grep.py` | preToolUse | search_code → Grep translation |
46
+ | `serena_project_check.py` | preToolUse | MCP matcher passthrough |
47
+
48
+ ### Workflow Hooks → Translated (via shim)
49
+
50
+ | Hook | Copilot Event | Notes |
51
+ |------|--------------|-------|
52
+ | `prompt_coach.py` | userPromptSubmitted | Calls Anthropic API — requires ANTHROPIC_API_KEY |
53
+ | `task_master_before_impl.py` | userPromptSubmitted | Works unmodified |
54
+ | `task_master_after_plan.py` | postToolUse | ExitPlanMode matcher passthrough |
55
+ | `jira_issue_fetch.py` | userPromptSubmitted | Works unmodified |
56
+
57
+ ### Security Hooks → Translated (via shim)
58
+
59
+ | Hook | Copilot Event | Notes |
60
+ |------|--------------|-------|
61
+ | `wp_security_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
62
+ | `sql_injection_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
63
+
64
+ ### Code Quality Hooks → Translated (via shim)
65
+
66
+ | Hook | Copilot Event | Notes |
67
+ |------|--------------|-------|
68
+ | `fp_utility_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
69
+ | `jquery_in_wordpress.py` | postToolUse | edit_file/write_file → Edit/Write translation |
70
+ | `bootstrap_utility_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
71
+ | `composer_autoload_check.py` | postToolUse | edit_file/write_file → Edit/Write translation |
72
+ | `docs_organization.py` | postToolUse | write_file → Write translation |
73
+ | `block_sed_edits.py` | preToolUse | run_terminal_command → Bash translation |
74
+
75
+ ### Sequential Thinking → Translated (via shim)
76
+
77
+ | Hook | Copilot Event | Notes |
78
+ |------|--------------|-------|
79
+ | `sequential_thinking_check.py` | userPromptSubmitted | Works unmodified |
80
+
81
+ ### Atlassian Hooks → Translated (via shim)
82
+
83
+ | Hook | Copilot Event | Notes |
84
+ |------|--------------|-------|
85
+ | `atlassian_prereqs.py` | preToolUse | MCP matcher passthrough |
86
+
87
+ ### Session Hooks
88
+
89
+ | Hook | Copilot Event | Notes |
90
+ |------|--------------|-------|
91
+ | `bootstrap.sh` | sessionStart | Content becomes copilot-instructions.md guidelines |
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """GitHub Copilot → Claude Code tool name translator shim.
3
+
4
+ Sits between GitHub Copilot and ima-claude hook scripts, translating
5
+ Copilot tool names to their Claude Code equivalents so existing hooks
6
+ work unmodified.
7
+
8
+ Usage (in hooks.json):
9
+ python3 ~/.copilot/hooks/hooks-translator.py ~/.copilot/hooks/some_hook.py
10
+ """
11
+
12
+ import json
13
+ import subprocess
14
+ import sys
15
+
16
+ # GitHub Copilot → Claude Code tool name mapping (reverse of adapter TOOL_MAP)
17
+ COPILOT_TO_CLAUDE = {
18
+ "run_terminal_command": "Bash",
19
+ "read_file": "Read",
20
+ "edit_file": "Edit",
21
+ "write_file": "Write",
22
+ "find_files": "Glob",
23
+ "search_code": "Grep",
24
+ "list_directory": "LS",
25
+ "web_search": "WebSearch",
26
+ "fetch_url": "WebFetch",
27
+ "ExitPlanMode": "ExitPlanMode",
28
+ }
29
+
30
+
31
+ def main():
32
+ if len(sys.argv) < 2:
33
+ print("Usage: hooks-translator.py <hook-script> [args...]", file=sys.stderr)
34
+ sys.exit(1)
35
+
36
+ target_script = sys.argv[1]
37
+ extra_args = sys.argv[2:]
38
+
39
+ # Read JSON from stdin
40
+ try:
41
+ raw = sys.stdin.read()
42
+ data = json.loads(raw) if raw.strip() else {}
43
+ except json.JSONDecodeError as e:
44
+ print(f"hooks-translator: invalid JSON from stdin: {e}", file=sys.stderr)
45
+ sys.exit(1)
46
+
47
+ # Translate tool_name if present
48
+ tool_name = data.get("tool_name", "")
49
+ if tool_name in COPILOT_TO_CLAUDE:
50
+ data["tool_name"] = COPILOT_TO_CLAUDE[tool_name]
51
+
52
+ # Pipe translated JSON to the actual hook script
53
+ translated = json.dumps(data)
54
+ result = subprocess.run(
55
+ ["python3", target_script] + extra_args,
56
+ input=translated,
57
+ stdout=None,
58
+ stderr=None,
59
+ text=True,
60
+ )
61
+
62
+ sys.exit(result.returncode)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -2,11 +2,13 @@ import type { DetectedPlatform, PlatformAdapter } from "./types";
2
2
  import { ClaudeAdapter } from "../claude/adapter";
3
3
  import { JunieAdapter } from "../junie/adapter";
4
4
  import { GeminiAdapter } from "../gemini/adapter";
5
+ import { GhCopilotAdapter } from "../gh-copilot/adapter";
5
6
 
6
7
  const ADAPTERS: PlatformAdapter[] = [
7
8
  new ClaudeAdapter(),
8
9
  new JunieAdapter(),
9
10
  new GeminiAdapter(),
11
+ new GhCopilotAdapter(),
10
12
  ];
11
13
 
12
14
  export function detectPlatforms(): DetectedPlatform[] {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima-claude",
3
- "version": "2.13.0",
3
+ "version": "2.14.1",
4
4
  "description": "IMA's Claude Code skills for functional programming, architecture, and team standards. 52 skills, 24 hooks, default persona, 3-tier memory system.",
5
5
  "author": {
6
6
  "name": "IMA",
@@ -13,17 +13,17 @@ Execute WP-CLI commands in Flywheel Local WP environments without disrupting Cla
13
13
  - **Recommended**: Use Kitty terminal with `$WP_LOCAL_SITE` configured (see Configuration)
14
14
  - **Alternative**: Create `.wp-local` file in project root: `echo "19efkkzWB" > .wp-local`
15
15
 
16
- **Run wp commands**:
16
+ **Run wp commands** (after alias setup in Configuration):
17
17
  ```bash
18
- bash ~/.claude/skills/wp-local/scripts/wp-local.sh plugin list
19
- bash ~/.claude/skills/wp-local/scripts/wp-local.sh db query "SELECT * FROM wp_posts LIMIT 5"
20
- bash ~/.claude/skills/wp-local/scripts/wp-local.sh user list
18
+ wpl plugin list
19
+ wpl db query "SELECT * FROM wp_posts LIMIT 5"
20
+ wpl user list
21
21
  ```
22
22
 
23
- **With shell alias** (recommended, see Configuration):
23
+ **Or run directly** (discovers install location automatically):
24
24
  ```bash
25
- wpl plugin list
26
- wpl db query "SELECT * FROM wp_users"
25
+ WP_LOCAL_SH=$(ls ~/.claude/skills/wp-local/scripts/wp-local.sh 2>/dev/null || ls ~/.claude/plugins/*/*/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh 2>/dev/null | head -1)
26
+ bash "$WP_LOCAL_SH" plugin list
27
27
  ```
28
28
 
29
29
  **Verify configuration**:
@@ -115,9 +115,15 @@ echo "journal-back" > .wp-local
115
115
 
116
116
  Add to `~/.bashrc` or `~/.zshrc`:
117
117
  ```bash
118
- alias wpl='bash ~/.claude/skills/wp-local/scripts/wp-local.sh'
118
+ wpl() {
119
+ local s=~/.claude/skills/wp-local/scripts/wp-local.sh
120
+ [[ -f "$s" ]] || s=$(ls ~/.claude/plugins/*/*/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh 2>/dev/null | head -1)
121
+ bash "$s" "$@"
122
+ }
119
123
  ```
120
124
 
125
+ This works for both local installs (`~/.claude/skills/`) and marketplace installs (`~/.claude/plugins/marketplaces/*/`).
126
+
121
127
  Reload shell:
122
128
  ```bash
123
129
  source ~/.bashrc # or source ~/.zshrc