Files
pbakaus_impeccable/scripts/lib/transformers/providers.js
T
41ff946121 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>
2026-06-20 02:24:18 -07:00

126 lines
4.2 KiB
JavaScript

/**
* Provider configurations for the transformer factory.
*
* Each config specifies:
* - provider: key into PROVIDER_PLACEHOLDERS (e.g. 'claude-code')
* - configDir: dot-directory name (e.g. '.claude')
* - displayName: human-readable name for log output (e.g. 'Claude Code')
* - providerTags: markdown block tags kept for this target (e.g. <codex>...</codex>)
* - frontmatterFields: which optional fields to emit beyond name + description
* - bodyTransform: optional function (body, skill) => transformed body
*/
export const PROVIDERS = {
cursor: {
provider: 'cursor',
providerTags: ['cursor'],
configDir: '.cursor',
displayName: 'Cursor',
frontmatterFields: ['license', 'compatibility', 'metadata'],
emitHooks: 'cursor',
// Cursor reads `.cursor/hooks.json`, not `.cursor/hooks/hooks.json`.
hooksManifestRel: 'hooks.json',
},
'claude-code': {
provider: 'claude-code',
providerTags: ['claude-code', 'claude'],
configDir: '.claude',
displayName: 'Claude Code',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
agentFormat: 'claude-md',
emitHooks: 'claude',
// Project-local Claude Code hooks live in `.claude/settings.json`.
hooksManifestRel: 'settings.json',
},
gemini: {
provider: 'gemini',
providerTags: ['gemini'],
configDir: '.gemini',
displayName: 'Gemini',
frontmatterFields: [],
},
codex: {
provider: 'codex',
providerTags: ['codex'],
configDir: '.codex',
displayName: 'Codex',
frontmatterFields: [],
writeOpenAIMetadata: true,
// No agentFormat: the Codex subagent ships nested inside the skill's own
// agents/ folder (see CODEX_SKILL_PROVIDERS in factory.js), which Codex
// auto-discovers on install. No top-level .codex/agents/ sidecar is emitted.
emitHooks: 'codex',
// Codex discovers project-local hooks at `.codex/hooks.json`.
hooksManifestRel: 'hooks.json',
},
agents: {
provider: 'agents',
providerTags: ['agents', 'codex'],
configDir: '.agents',
displayName: 'Codex Repo Skills',
placeholderProvider: 'codex',
frontmatterFields: [],
writeOpenAIMetadata: true,
},
github: {
provider: 'github',
providerTags: ['github'],
configDir: '.github',
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',
providerTags: ['kiro'],
configDir: '.kiro',
displayName: 'Kiro',
frontmatterFields: ['license', 'compatibility', 'metadata'],
},
opencode: {
provider: 'opencode',
providerTags: ['opencode'],
configDir: '.opencode',
displayName: 'OpenCode',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
pi: {
provider: 'pi',
providerTags: ['pi'],
configDir: '.pi',
displayName: 'Pi',
frontmatterFields: ['license', 'compatibility', 'metadata', 'allowed-tools'],
},
qoder: {
provider: 'qoder',
providerTags: ['qoder'],
configDir: '.qoder',
displayName: 'Qoder',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
'trae-cn': {
provider: 'trae-cn',
providerTags: ['trae-cn', 'trae'],
configDir: '.trae-cn',
displayName: 'Trae China',
placeholderProvider: 'trae',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
},
trae: {
provider: 'trae',
providerTags: ['trae'],
configDir: '.trae',
displayName: 'Trae',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
},
'rovo-dev': {
provider: 'rovo-dev',
providerTags: ['rovo-dev'],
configDir: '.rovodev',
displayName: 'Rovo Dev',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
};