mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 10:36:27 +03:00
Add GitHub Copilot hook support (CLI + cloud agent) (#279)
* Add GitHub Copilot hook support (CLI + cloud agent) Wire the Impeccable design detector into GitHub Copilot's hook system so direct file edits get the same post-edit design feedback the Claude Code, Codex, and Cursor harnesses already receive. GitHub Copilot's contract differs from the existing harnesses (verified against Copilot CLI 1.0.63): - Repo-level manifest at `.github/hooks/impeccable.json` (read by both the CLI, once committed to the default branch, and the cloud/app agent). - Flat `postToolUse` entries with `bash`/`timeoutSec` and a full-match `matcher` regex; the file-editing tools are `edit` and `create`. - The stdin event uses camelCase `toolName`/`toolArgs`, where `toolArgs` is a JSON *string* carrying the touched file under `path`. - Context is injected via a top-level `additionalContext` string. Changes: - hooks.js: buildGitHubHooksManifest() + route `github` in hooksJsonFor(). - providers.js: emitHooks/hooksManifestRel for the github provider. - hook-lib.mjs: detect the github harness, normalize the camelCase event (parse the JSON-string toolArgs -> tool_input.file_path), and emit the `additionalContext` payload shape. - hook-admin.mjs / skills.mjs: install + idempotent-repair the `.github/hooks/impeccable.json` manifest (bash-aware marker stripping). - hooks.md: document GitHub Copilot as a supported harness. - Tests for the builder, routing, event normalization, and end-to-end run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cover Copilot apply_patch edits in the hook (live-verified) The first cut matched only `edit|create`, the tool names `copilot -p` uses. A live trace against Copilot CLI 1.0.63 in an interactive session showed it edits files via `apply_patch`, whose toolArgs is a raw OpenAI-format patch string (`*** Begin Patch` / `*** Add File:`), not JSON. With the narrow matcher the hook command never ran. - hooks.js / hook-admin.mjs: matcher -> `edit|create|apply_patch`. - hook-lib.mjs: normalizeGitHubEvent now routes apply_patch's raw patch string into tool_input.command (reusing the existing parseApplyPatchPaths / resolveTargetFiles plumbing) and only JSON-parses toolArgs for the edit/create/view tools. tool_name is normalized to apply_patch so the patch path is extracted even if a future build relabels the tool. - Tests: apply_patch matcher assertions, event normalization, and an end-to-end runHook covering the interactive/cloud path. Verified live: a trusted interactive `apply_patch` edit fires the hook and returns the expected `additionalContext` design reminder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review feedback + add changelog entry - hook-lib.mjs (Bugbot, low): looksLikeApplyPatch no longer misroutes an edit/create event whose edited *content* contains apply_patch markers. A real apply_patch payload is a raw string that does not parse as JSON; an edit payload is a JSON object, so only non-JSON-object strings are treated as apply_patch. Edit events keep extracting `path`. Adds a regression test. - skills.mjs (Bugbot, medium): document why `.github` is intentionally excluded from hookScriptPathForProvider. Its hook manifest is committed and shared (read by the Copilot cloud agent and teammates), so the command must stay portable via `$(git rev-parse ...)`; rewriting it to a machine-local absolute path would break those. GitHub skills are project-scoped, so the project-relative path resolves. - changelog: add an Upcoming (v3.x placeholder) entry for the Copilot hook. Version is not bumped yet (batching with other changes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
793feda5a0
commit
41ff946121
@@ -24,6 +24,7 @@ const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scr
|
||||
const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
|
||||
const CODEX_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.agents/skills/impeccable/scripts/hook.mjs';
|
||||
const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs';
|
||||
const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs';
|
||||
|
||||
export function buildClaudeSettingsManifest() {
|
||||
return {
|
||||
@@ -106,6 +107,33 @@ export function buildCursorHooksManifest() {
|
||||
};
|
||||
}
|
||||
|
||||
// GitHub Copilot reads project hooks from `.github/hooks/*.json`. Its schema
|
||||
// differs from Claude/Codex/Cursor: the event key is lowercase `postToolUse`,
|
||||
// each entry is flat (no nested `hooks` array), the command lives under `bash`
|
||||
// (with an optional `powershell` sibling), the timeout key is `timeoutSec`, and
|
||||
// `matcher` is a full-match regex (`^(?:PATTERN)$`) tested against the tool name.
|
||||
// Copilot's file-editing tool names vary by surface (verified against CLI
|
||||
// 1.0.63): `copilot -p` runs use `edit` ({path, old_str, new_str}) and `create`
|
||||
// ({path, file_text}); interactive sessions and the cloud agent use
|
||||
// `apply_patch` (a raw OpenAI-format patch string). The matcher covers all
|
||||
// three. The same manifest is honored by both the CLI and the cloud/app agent.
|
||||
// https://docs.github.com/en/copilot/reference/hooks-reference
|
||||
export function buildGitHubHooksManifest() {
|
||||
return {
|
||||
version: 1,
|
||||
hooks: {
|
||||
postToolUse: [
|
||||
{
|
||||
type: 'command',
|
||||
matcher: 'edit|create|apply_patch',
|
||||
bash: `node "${GITHUB_PROJECT_HOOK}"`,
|
||||
timeoutSec: TIMEOUT_SECONDS,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function hooksJsonFor(provider) {
|
||||
switch (provider) {
|
||||
case 'claude':
|
||||
@@ -114,6 +142,8 @@ export function hooksJsonFor(provider) {
|
||||
return buildCodexHooksManifest();
|
||||
case 'cursor':
|
||||
return buildCursorHooksManifest();
|
||||
case 'github':
|
||||
return buildGitHubHooksManifest();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ export const PROVIDERS = {
|
||||
displayName: 'GitHub Copilot',
|
||||
placeholderProvider: 'agents',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
|
||||
emitHooks: 'github',
|
||||
// GitHub Copilot discovers repo-level hooks under `.github/hooks/*.json`.
|
||||
hooksManifestRel: 'hooks/impeccable.json',
|
||||
},
|
||||
kiro: {
|
||||
provider: 'kiro',
|
||||
|
||||
Reference in New Issue
Block a user