diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 00000000..29c468e0 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,87 @@ +name: build-image + +# Builds and pushes the failproofai hook-sync container to GHCR. +# +# The hook-sync container (docker-hook-sync/) runs Claude Code in --effort ultracode mode on a +# daily schedule (a k8s CronJob managed in our infra, not this repo) to detect drift +# between every integrated agent CLI and this repo, and opens auto-sync PRs. See +# docker-hook-sync/Dockerfile for the image and scripts/sync-agent-cli-harnesses-prompt.md for +# what the agent does. +# +# The daily rebuild refreshes the @latest-pinned claude-code + failproofai npm globals. + +on: + push: + branches: [main] + paths: + - 'docker-hook-sync/**' + - '.github/workflows/build-image.yml' + schedule: + - cron: '0 8 * * *' # daily at 08:00 UTC — refresh claude-code@latest + failproofai@latest + workflow_dispatch: + inputs: + tag_suffix: + description: 'Extra tag to publish alongside :latest and :sha- (e.g. dev, hotfix). Allowed chars: [A-Za-z0-9._-], max 128. Leave empty for none.' + default: '' + required: false + push_to_ghcr: + description: 'Push the built image to GHCR. Uncheck to build-only (validate the Dockerfile without publishing).' + type: boolean + default: true + required: false + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute tags + id: tags + env: + TAG_SUFFIX: ${{ inputs.tag_suffix }} + run: | + short_sha="${GITHUB_SHA::7}" + if [ -n "$TAG_SUFFIX" ]; then + if ! printf '%s' "$TAG_SUFFIX" | grep -qE '^[A-Za-z0-9_.-]{1,128}$'; then + echo "::error::tag_suffix '$TAG_SUFFIX' has invalid chars; allowed: [A-Za-z0-9._-], max 128" + exit 1 + fi + fi + { + echo "tags<> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: docker-hook-sync + file: docker-hook-sync/Dockerfile + push: ${{ github.event_name != 'workflow_dispatch' || inputs.push_to_ghcr }} + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/sync-hook-events.yml b/.github/workflows/sync-hook-events.yml deleted file mode 100644 index ccc58680..00000000 --- a/.github/workflows/sync-hook-events.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Sync Hook Event Types - -on: - schedule: - - cron: '7 8 * * *' # daily at 08:07 UTC - workflow_dispatch: - -jobs: - sync-hooks: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v7 - - - name: Install Claude Code CLI - run: npm install -g @anthropic-ai/claude-code - - # Claude only gets its own API credentials — no GITHUB_TOKEN - - name: Analyze hook coverage and edit files - env: - ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} - ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} - FAILPROOFAI_TELEMETRY_DISABLED: "1" - CLAUDE_SKIP_SETUP_MODAL: "true" - run: | - claude \ - --model claude-sonnet-4-6 \ - --allowedTools "Read,Edit,Glob,Grep,WebFetch" \ - --dangerously-skip-permissions \ - -p "$(cat scripts/sync-hook-events-prompt.md)" - - # PR creation is handled here — GITHUB_TOKEN never touches Claude - - name: Create PR if changes detected - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - RESULT=$(cat .sync-hook-events-output.json 2>/dev/null || echo '{"changed":false}') - CHANGED=$(echo "$RESULT" | jq -r '.changed') - - if [ "$CHANGED" != "true" ]; then - echo "Hook coverage is up to date. No PR needed." - exit 0 - fi - - # Check for an existing open sync PR to avoid duplicates - EXISTING=$(gh pr list --base main --search "[auto] sync hook event types" --state open --json number --jq length) - if [ "$EXISTING" -gt 0 ]; then - echo "Sync PR already open. Skipping." - exit 0 - fi - - BRANCH="auto/sync-hook-events-$(date +%Y%m%d-%H%M)" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add src/hooks/types.ts __tests__/hooks/manager.test.ts __tests__/hooks/integrations.test.ts - git commit -m "feat: sync hook event types with upstream agent CLI docs" - git push origin "$BRANCH" - - PR_TITLE=$(echo "$RESULT" | jq -r '.prTitle') - PR_BODY=$(echo "$RESULT" | jq -r '.prBody') - gh pr create \ - --title "$PR_TITLE" \ - --body "$PR_BODY" \ - --base main \ - --head "$BRANCH" diff --git a/.gitignore b/.gitignore index d161b0e6..c26c7ee8 100644 --- a/.gitignore +++ b/.gitignore @@ -61,9 +61,6 @@ next-env.d.ts # custom hooks loader temp files *.__failproofai_tmp__.* -# sync-hook-events workflow output (ephemeral, generated in CI) -.sync-hook-events-output.json - # package manager lockfiles (bun.lock is tracked; bun.lockb is binary) package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 65155732..72d91ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.0.12-beta.0 — 2026-07-09 +### Features +- Add the `hook-sync` container (`docker-hook-sync/` + `.github/workflows/build-image.yml`, published to `ghcr.io/failproofai/hook-sync`): a single-shot daily job that clones failproofai and runs Claude Code in `--effort ultracode` mode to detect hook event-name, tool/payload-schema, and settings-file-shape drift across all seven integrated agent CLIs, then opens one auto-sync PR — the agent commits, pushes, and opens the PR itself, gated by failproofai's own `require-*-before-stop` hooks (dogfooding). Replaces the broken `sync-hook-events.yml` GitHub Action (removed, along with `scripts/sync-hook-events-prompt.md`). + ### Fixes - Codex hooks: drop the invalid top-level `version` field from `.codex/hooks.json` (Codex CLI v0.142+ rejects it with `unknown field 'version'`, refusing to start any session), and strip any leftover `version` on the next install/uninstall so previously-broken configs self-heal. Also correct the Codex `timeout` unit from `60000` to `60` — Codex reads `timeout` in seconds (its `timeout_sec` field), so the old value meant ~16.7h instead of 60s. Copilot and Cursor legitimately carry `version: 1` in their own schemas and are untouched. diff --git a/docker-hook-sync/Dockerfile b/docker-hook-sync/Dockerfile new file mode 100644 index 00000000..066d6e4e --- /dev/null +++ b/docker-hook-sync/Dockerfile @@ -0,0 +1,75 @@ +# Self-contained failproofai hook-sync container. +# +# A single-shot daily job (run by a k8s CronJob) that clones failproofai, runs +# Claude Code in `--effort ultracode` mode to detect drift between every agent-CLI +# harness we integrate with and this repo, and opens ONE auto-PR with the fix. +# +# Bundles: +# - Node 20 + npm +# - bun (REQUIRED: the cloned repo's .claude/settings.json fires hooks via +# `bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook `, so the +# dogfooded require-*-before-stop policies enforce the commit/push/PR) +# - Claude Code CLI (`claude`) — run headless with --effort ultracode +# - failproofai@latest on PATH (belt-and-suspenders; the dogfood hooks actually +# run the clone's SOURCE via bun, not this global) +# - git + gh + jq + dumb-init + `script` (PTY wrap for stream-json) +# +# Build: docker build -t hook-sync:latest docker-hook-sync/ +# Run: docker run --rm -e CLAUDE_CODE_OAUTH_TOKEN=... -e GH_TOKEN=... hook-sync:latest +# +# Every dependency is installed at build time. The daily GitHub Actions rebuild +# (.github/workflows/build-image.yml) keeps claude-code@latest + failproofai@latest fresh. +FROM node:20-bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + HOME=/home/appuser \ + BUN_INSTALL=/home/appuser/.bun \ + FAILPROOFAI_TELEMETRY_DISABLED=1 \ + PATH="/home/appuser/.bun/bin:/home/appuser/.local/bin:${PATH}" + +# Layer 1: apt deps. dumb-init for PID 1 signal handling; git+gh for the agent's +# clone/branch/push/PR flow AND the require-*-before-stop hooks; jq for the +# entrypoint's policy-config edit; bsdextrautils provides `script` (PTY wrap so +# Node line-flushes --output-format stream-json); curl/gnupg for the gh apt key +# and the bun installer; unzip is required by the bun installer. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg dumb-init jq git bsdextrautils unzip \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2: GitHub CLI (gh). Used by the agent for `gh pr create/list` and by the +# require-pr-before-stop hook. +RUN install -d /etc/apt/keyrings \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh \ + && rm -rf /var/lib/apt/lists/* + +# Layer 3: npm globals. Claude Code CLI + failproofai, pinned to latest at build +# time; the daily workflow rebuild keeps them fresh. Root install so symlinks land +# in /usr/local/bin (executable by the non-root appuser). +RUN npm install -g --omit=dev \ + @anthropic-ai/claude-code@latest \ + failproofai@latest \ + && npm cache clean --force + +# Layer 4: non-root user + chowned workspace. +RUN useradd --create-home --uid 10001 --shell /bin/bash appuser \ + && mkdir -p /workspace \ + && chown -R appuser:appuser /workspace /home/appuser + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod 0755 /usr/local/bin/entrypoint.sh + +USER appuser +WORKDIR /home/appuser + +# Layer 5: bun (installs into $BUN_INSTALL=/home/appuser/.bun, on PATH per ENV). +# The dogfood hook fast-path runs TypeScript from src/ directly — bun executes TS +# natively, so no build step is needed for the hooks to work. +RUN curl -fsSL https://bun.sh/install | bash + +ENTRYPOINT ["dumb-init", "--", "/usr/local/bin/entrypoint.sh"] diff --git a/docker-hook-sync/entrypoint.sh b/docker-hook-sync/entrypoint.sh new file mode 100644 index 00000000..a171bbe0 --- /dev/null +++ b/docker-hook-sync/entrypoint.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# +# failproofai hook-sync entrypoint (runs as PID 1 under dumb-init). +# +# Lifecycle: +# 1. Validate env (CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN). +# 2. gh auth setup-git + git identity. +# 3. Clone failproofai --depth=1; cut a fresh auto/sync-cli-harnesses- branch. +# 4. NEUTRALIZE two policies in the THROWAWAY clone's .failproofai config +# (require-ci-green-before-stop, block-read-outside-cwd) via jq, then +# `git update-index --skip-worktree` so the edit is invisible to +# git status/add and never enters the agent's commit/PR — while failproofai +# reads the edited WORKTREE file at runtime and sees them gone. The +# require-commit/push/pr-before-stop gates stay ACTIVE (dogfooding). +# 5. exec `claude --effort ultracode -p ` wrapped in a PTY so +# --output-format stream-json line-flushes to `kubectl logs -f`. +# 6. Exit with claude's return code (all output streams to stdout / the pod log). +# +# Exit codes: 0 = agent finished (claude's rc) · 64 = missing required env · +# 65 = prompt file missing in clone · 66 = unsafe WORKSPACE · else = claude's rc. +set -euo pipefail + +CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}" +GH_TOKEN="${GH_TOKEN:-}" +REPO_URL="${REPO_URL:-https://github.com/failproofai/failproofai.git}" +REPO_BRANCH_FROM="${REPO_BRANCH_FROM:-main}" +WORKSPACE="${WORKSPACE:-/workspace}" +PROMPT_PATH="${PROMPT_PATH:-scripts/sync-agent-cli-harnesses-prompt.md}" +CLAUDE_MODEL="${CLAUDE_MODEL:-claude-opus-4-8}" + +export HOME="${HOME:-/home/appuser}" +export CLAUDE_CODE_OAUTH_TOKEN +export GH_TOKEN +export GITHUB_TOKEN="${GITHUB_TOKEN:-$GH_TOKEN}" +export FAILPROOFAI_TELEMETRY_DISABLED=1 +# Keep ultracode background subagents alive in headless -p mode (default is ~10 min). +export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS="${CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS:-1800000}" + +log() { printf '[entrypoint %s] %s\n' "$(date -u +%FT%TZ)" "$*"; } + +# ---------------- 1. env validation ---------------- +if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then log "ERROR: CLAUDE_CODE_OAUTH_TOKEN not set"; exit 64; fi +if [ -z "$GH_TOKEN" ]; then log "ERROR: GH_TOKEN not set"; exit 64; fi + +# ---------------- 2. gh + git auth ---------------- +log "authenticating gh" +gh auth setup-git >/dev/null +git config --global user.name "failproofai-hook-sync" +git config --global user.email "hook-sync-bot@exosphere.host" +git config --global init.defaultBranch main + +# ---------------- 3. clone + fresh branch ---------------- +case "$WORKSPACE" in + ""|"/"|"."|"..") log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;; +esac +mkdir -p "$WORKSPACE" +if [ -n "$(ls -A "$WORKSPACE" 2>/dev/null)" ]; then + find "$WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf {} + +fi +log "cloning $REPO_URL@$REPO_BRANCH_FROM -> $WORKSPACE" +git clone --depth=1 --branch "$REPO_BRANCH_FROM" "$REPO_URL" "$WORKSPACE" +cd "$WORKSPACE" +# Fetch the base ref (shallow) so require-*-before-stop can diff against origin/. +git fetch --depth=1 origin "$REPO_BRANCH_FROM" >/dev/null 2>&1 || true +BRANCH="auto/sync-cli-harnesses-$(date -u +%Y%m%dT%H%M%SZ)" +git checkout -b "$BRANCH" +log "working on branch $BRANCH" + +# ---------------- 4. policy-config neutralization ---------------- +# enabledPolicies is a UNION across scopes with no disable field, so a policy must +# be removed from the file that lists it. --skip-worktree then hides that edit from +# git status/add (so it never lands in the PR) while failproofai still reads the +# edited worktree file at runtime. Keeps require-commit/push/pr-before-stop active. +CFG=".failproofai/policies-config.json" +if [ -f "$CFG" ]; then + cfg_tmp="$(mktemp)" + jq '.enabledPolicies |= map(select(. != "require-ci-green-before-stop" and . != "block-read-outside-cwd"))' "$CFG" > "$cfg_tmp" && mv "$cfg_tmp" "$CFG" + git update-index --skip-worktree "$CFG" + log "neutralized require-ci-green-before-stop + block-read-outside-cwd for this run (skip-worktree; not committed)" +else + log "WARN: $CFG not found; running with the repo's default policy set" +fi + +# ---------------- 5. invoke claude (ULTRACODE, headless, PTY-wrapped) ---------------- +PROMPT_FILE="$WORKSPACE/$PROMPT_PATH" +if [ ! -f "$PROMPT_FILE" ]; then log "ERROR: prompt missing at $PROMPT_FILE"; exit 65; fi + +# Node block-buffers stdout when it's a pipe; `script -qefc ... /dev/null` gives +# claude a PTY so --output-format stream-json line-flushes to the pod logs. `-e` +# ties script's exit code to claude's. +RUN_SH="$(mktemp /tmp/run-claude.XXXXXX.sh)" +cat >"$RUN_SH" <&1 +EOF +chmod +x "$RUN_SH" + +log "invoking claude --effort ultracode --model ${CLAUDE_MODEL} (prompt: $PROMPT_PATH)" +set +e +script -qefc "$RUN_SH" /dev/null +rc=$? +set -e +rm -f "$RUN_SH" +log "claude exited $rc" + +exit "$rc" diff --git a/scripts/sync-agent-cli-harnesses-prompt.md b/scripts/sync-agent-cli-harnesses-prompt.md new file mode 100644 index 00000000..550e4107 --- /dev/null +++ b/scripts/sync-agent-cli-harnesses-prompt.md @@ -0,0 +1,222 @@ +ultracode + +You are an automated **ultracode** (multi-agent) run inside a fresh clone of the +failproofai repository. Your job is to keep failproofai in sync with the upstream +documentation for **every** agent CLI we integrate with, then open exactly one +pull request with the fix — or, if a sync PR is already open, comment on it instead. + +## Run contract (read first) + +- You are running **headless and autonomously**. No human is watching; nobody can + answer a question mid-run. For reversible actions that follow from this task, + proceed without asking. Do **not** end your turn with a plan or a question — + finish the work. +- The entrypoint has **already cut a fresh branch** named `auto/sync-cli-harnesses-`. + Do all your work here; do **not** switch branches or work on `main`. +- failproofai's own Stop hooks are active in this checkout. If you make commits, you + will not be allowed to stop until you have **committed**, **pushed**, and **opened a + PR** for them. If you make **no** commits (nothing to sync, or you only commented on + an already-open sync PR), a clean working tree stops fine — so on those paths, do + **not** edit any files. +- Use ultracode / multi-agent orchestration: in Phase 1, fan out **one subagent per + CLI** so the seven harnesses are checked in parallel. + +failproofai integrates with **seven** agent CLIs. Each has its own hook surface, +tracked in `src/hooks/types.ts` and written to disk by `src/hooks/integrations.ts`: + +| CLI | Event array (`src/hooks/types.ts`) | Event map | Settings file written (`src/hooks/integrations.ts`) | +|----------------|------------------------------------|-----------|------------------------------------------------------| +| Claude Code | `HOOK_EVENT_TYPES` (canonical) | — (canonical) | `.claude/settings.json` | +| OpenAI Codex | `CODEX_HOOK_EVENT_TYPES` | `CODEX_EVENT_MAP` | `.codex/hooks.json` | +| GitHub Copilot | `COPILOT_HOOK_EVENT_TYPES` | — (already Pascal) | `.github/hooks/failproofai.json` | +| Cursor Agent | `CURSOR_HOOK_EVENT_TYPES` | `CURSOR_EVENT_MAP` | `.cursor/hooks.json` | +| OpenCode | `OPENCODE_HOOK_EVENT_TYPES` | `OPENCODE_EVENT_MAP` | `.opencode/opencode.json` + `.opencode/plugins/failproofai.mjs` | +| Pi | `PI_HOOK_EVENT_TYPES` | `PI_EVENT_MAP` | `.pi/settings.json` | +| Gemini CLI | `GEMINI_HOOK_EVENT_TYPES` | `GEMINI_EVENT_MAP` | `.gemini/settings.json` | + +## What "drift" means — three scopes + +For each verified CLI, compare the upstream docs to this repo across three scopes: + +1. **Scope 1 — hook event names.** Events documented upstream but missing from (or + removed from) the CLI's `*HOOK_EVENT_TYPES` array in `src/hooks/types.ts`. +2. **Scope 2 — tool / payload schema.** Changes to the CLI's tool names or payload + field shapes, tracked in the `*_TOOL_MAP` / `*_TOOL_INPUT_MAP` tables in + `src/hooks/types.ts` and in the per-CLI response-shape branches of + `src/hooks/policy-evaluator.ts`. +3. **Scope 3 — settings-file shape.** Changes to the structure of the config file + failproofai writes for the CLI: the hard-coded literals in + `src/hooks/integrations.ts` (matcher-wrapper vs flat-array vs packages-array vs + plugin registration; field names like `command` vs `bash`/`powershell`; + `timeout` vs `timeoutSec` and its **unit** seconds-vs-ms; presence of `version` + / `$schema` / `matcher`; the settings-file path) **and** the committed dogfood + fixture on disk. The canonical worked example is git history / PR #482, which + dropped an invalid top-level `version` field and fixed a `60000`ms→`60`s timeout + unit in `.codex/hooks.json` by editing the writer, the fixture, and the tests + together. + +## Reading rules + +- This run relaxed two policies (`require-ci-green-before-stop`, + `block-read-outside-cwd`) so you can read and edit the settings fixtures. Even so, + when you only need to **inspect** a committed fixture, prefer + `git show HEAD:` (e.g. `git show HEAD:.codex/hooks.json`) — it is never + blocked and is the reliable way to read `.claude/settings.json`, `.codex/hooks.json`, + `.cursor/hooks.json`, `.github/hooks/failproofai.json`, `.gemini/settings.json`, + `.pi/settings.json`, and `.opencode/*`. +- Do **NOT** run `failproofai policies --install` (or any `failproofai` subcommand) + to regenerate a fixture — it is blocked. Edit fixtures by hand. + +## Phase 1 — Fan out one subagent per CLI + +Spawn one subagent per CLI (seven, in parallel). Each subagent, for its CLI: + +1. `WebFetch` the docs URL(s) below and extract the authoritative lists: the hook + **event names**, the **tool names / payload field names**, and the + **settings-file schema** (top-level keys, per-entry shape, command field name, + timeout field name **and unit**, and whether `matcher` / `version` / `$schema` + are present). **Use upstream casing exactly** — Codex snake_case, Cursor + camelCase, OpenCode dot.namespaced, Pi snake_case, Claude/Copilot/Gemini + PascalCase. Do **not** normalize. +2. Compare against this repo: the CLI's `*HOOK_EVENT_TYPES` array + paired + `*EVENT_MAP` (scope 1); its `*_TOOL_MAP` / `*_TOOL_INPUT_MAP` and its branch in + `src/hooks/policy-evaluator.ts` (scope 2); its literals in + `src/hooks/integrations.ts` + its dogfood fixture read via `git show` (scope 3). +3. Return a structured report: `{ cli, status: up-to-date | drift | unverified, + scope1: {added, removed}, scope2: {...}, scope3: {...}, sources: [...] }`. + +If a docs URL 404s, redirects to a stub, or has no parseable list, mark that CLI +`unverified` and skip its diff. **Do not invent events, tools, or fields. Do not +guess from prior knowledge.** Pi is the most likely `unverified` candidate (its +surface is documented in the package source, not a clean enumeration). + +| CLI | Docs URL(s) | +|-----------|-------------| +| Claude | https://code.claude.com/docs/en/hooks (reference) · https://code.claude.com/docs/en/hooks-guide (summary) | +| Codex | https://developers.openai.com/codex/hooks | +| Copilot | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks · https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference (tool names) | +| Cursor | https://cursor.com/docs/hooks | +| OpenCode | https://opencode.ai/docs/plugins/ | +| Pi | https://www.npmjs.com/package/@mariozechner/pi-coding-agent | +| Gemini | https://geminicli.com/docs/hooks/ · https://geminicli.com/docs/reference/tools/ (tool names) | + +## Phase 2 — Decide what to do (do this BEFORE editing any files) + +Merge the subagent reports into one list of detected drift. Then, **before you edit a +single file**, decide which path you are on — editing first would leave uncommitted +changes that block a clean stop on the comment / no-op paths below. + +1. **No drift in any verified CLI** → do nothing: make no edits, open no PR, post no + comment. Stop. (`unverified` alone is not drift — the docs may just be unreachable + today.) + +2. **Drift exists — first check for an already-open sync PR:** + `gh pr list --base main --state open --search "[auto] sync agent CLI harnesses" --json number,url,headRefName`. + + - **If an open sync PR already exists, do NOT open a second PR and do NOT edit any + files.** Read what it already covers (`gh pr diff ` plus its body) and + compare against your detected drift. If that PR already covers everything you + found, stop — nothing to add. If you found drift the PR is **missing**, post ONE + comment listing exactly what's missing, then stop — + `gh pr comment --body "..."`, e.g.: + + > This open sync PR is missing newly-detected drift: + > - **Cursor**: upstream added event `afterEdit` — append to `CURSOR_HOOK_EVENT_TYPES` (+ a `CURSOR_EVENT_MAP` entry). + > - **Gemini**: the `run_shell_command` tool was renamed to `shell` — the `GEMINI_TOOL_MAP` key is stale. + > + > (hook-sync bot — please fold these into this PR rather than opening a second sync PR.) + + Never push commits to another PR's branch; only comment, and leave the working + tree clean so this run stops cleanly. + + - **If no open sync PR exists → go to Phase 3** to apply the fixes and open one. + +## Phase 3 — Apply the fixes and open one PR + +Only reached when there is real drift **and** no open sync PR. Edit only what drift +requires. + +### Scope 1 (event names) +- **Append** new events just before `] as const`, preserving upstream casing. +- **Delete** removed events; if the CLI has an `*EVENT_MAP`, also delete the same + key from that map (the `Record` is exhaustive — a stale key fails `tsc`). +- For **map-bearing CLIs** (Codex, Cursor, OpenCode, Pi, Gemini), do **NOT** invent + the canonical mapping for a newly-added event — leave it out of the `*EVENT_MAP` + so `tsc` fails intentionally, and add a reviewer-checklist item to the PR body + (see below). For **Claude and Copilot** (no map), the build stays green. +- **Fix hardcoded test counts** when the relevant array changed: + - If `HOOK_EVENT_TYPES` (Claude) changed: in `__tests__/hooks/manager.test.ts`, + update the `installs hooks for all event types` description AND both + `expect(Object.keys(written.hooks)).toHaveLength()` assertions. + - If `GEMINI_HOOK_EVENT_TYPES` changed: in `__tests__/hooks/integrations.test.ts`, + update the `expect(gemini.eventTypes).toHaveLength()` assertion AND the + matching description string. + - Locate these by searching for the current count number. + +### Scope 2 / Scope 3 (tool schema / settings-file shape) +- Apply a fix **only when it is unambiguous and needs no human judgement** — e.g. + an invalid field to remove, a wrong timeout unit, or a renamed tool id (the PR + #482 class). When you do, move the whole quartet together, exactly like #482: + the writer in `src/hooks/integrations.ts` (+ a shared helper if it dedupes + logic), the committed dogfood fixture, the unit test in + `__tests__/hooks/integrations.test.ts`, and the e2e test in + `__tests__/e2e/hooks/-integration.e2e.test.ts` — plus a `types.ts` comment + if it documents the old shape. +- If the correct fix needs judgement (which canonical event/tool a new item maps + to; an ambiguous schema change), **leave the code untouched** and record it as a + reviewer-checklist item instead. Bias toward reporting over auto-editing core + integration code. +- Do **NOT** add `*EVENT_MAP`, `*_TOOL_MAP`, or `*_TOOL_INPUT_MAP` entries for + newly-added items — the canonical mapping is a human decision. + +### CHANGELOG (required) +Add a single-line entry to `CHANGELOG.md` under `## ` where +`` is the `version` field in `package.json` (read it) and `` is +today's UTC date. If that heading does not exist yet, create it above the previous +version's section. There is no `## Unreleased` section. Use the `### Features` (or +`### Fixes` for a shape correction) subsection. + +### Commit, push, open the PR +1. Stage only the files you intentionally edited (never `git add -A`): + `src/hooks/types.ts`, `src/hooks/integrations.ts`, `src/hooks/policy-evaluator.ts`, + the touched `__tests__/...` files, the touched dogfood fixtures, and + `CHANGELOG.md` — whichever actually changed. +2. Commit (e.g. `feat: sync agent CLI harnesses with upstream docs`), then + `git push -u origin "$(git branch --show-current)"`. +3. `gh pr create --base main --title "[auto] sync agent CLI harnesses with upstream docs" --body ""`. + +The PR **body** must contain, in order: + +1. **Summary table** — one row per CLI: `| CLI | scope-1 | scope-2 | scope-3 | status |` + where status is `up to date`, `drift`, or `unverified`. +2. **Per-CLI sections** — for each CLI with drift, list the added/removed events, + tool/schema changes, and settings-shape changes. +3. **Reviewer checklist** — one unchecked box per item that needs a human decision: + - For each newly-added event on a map-bearing CLI: + `- [ ] Add \`: "???"\` to \`\` in \`src/hooks/types.ts\` (canonical Claude \`HookEventType\` chosen by reviewer)` + - For each scope-2/scope-3 change you deferred as judgement-heavy. +4. **Sources** — the docs URL(s) consulted per CLI. +5. **Unverified notes** — one line per `unverified` CLI explaining why. +6. **Final note (verbatim):** + > **CI is expected to fail on this PR if a map-bearing CLI gained new events — a + > reviewer must add the missing `*EVENT_MAP` entries (replacing `"???"`) before + > merging. For drift in Claude or Copilot only (no event map), CI should pass on + > this commit alone. CI must pass and this PR must be reviewed before merging.** + +## Constraints + +- **Only edit** these paths, and only where drift requires it: `src/hooks/types.ts`, + `src/hooks/integrations.ts`, `src/hooks/policy-evaluator.ts`, + `__tests__/hooks/manager.test.ts`, `__tests__/hooks/integrations.test.ts`, + `__tests__/e2e/hooks/-integration.e2e.test.ts`, the seven dogfood fixtures + (`.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, + `.github/hooks/failproofai.json`, `.opencode/opencode.json`, + `.opencode/plugins/failproofai.mjs`, `.pi/settings.json`, `.gemini/settings.json`), + and `CHANGELOG.md`. +- Do **NOT** edit `.failproofai/policies-config.json` (the entrypoint manages it), + `src/hooks/handler.ts`, `src/hooks/manager.ts`, or any other source file. +- Do **NOT** add entries to any `*EVENT_MAP`, `*_TOOL_MAP`, or `*_TOOL_INPUT_MAP` + for newly-added items. Removing a key when its array entry is removed IS allowed + (and required to keep the build green). +- Do **NOT** invent events, tools, or fields. If WebFetch fails or the docs don't + expose a clean list, mark the CLI `unverified` and move on. diff --git a/scripts/sync-hook-events-prompt.md b/scripts/sync-hook-events-prompt.md deleted file mode 100644 index 1318bb89..00000000 --- a/scripts/sync-hook-events-prompt.md +++ /dev/null @@ -1,179 +0,0 @@ -You are an automated agent running in GitHub Actions to keep failproofai's hook -event types in sync with the upstream documentation for **every** agent CLI we -integrate with. - -failproofai integrates with seven agent CLIs. Each one has its own hook event -surface, tracked as a separate `as const` array in `src/hooks/types.ts`: - -| CLI | Array constant | Casing convention | Has `*EVENT_MAP`? | -|----------------|-----------------------------|-------------------|--------------------| -| Claude Code | `HOOK_EVENT_TYPES` | PascalCase | no (canonical) | -| OpenAI Codex | `CODEX_HOOK_EVENT_TYPES` | snake_case | yes (`CODEX_EVENT_MAP`) | -| GitHub Copilot | `COPILOT_HOOK_EVENT_TYPES` | PascalCase | no | -| Cursor Agent | `CURSOR_HOOK_EVENT_TYPES` | camelCase | yes (`CURSOR_EVENT_MAP`) | -| OpenCode | `OPENCODE_HOOK_EVENT_TYPES` | dot.namespaced | yes (`OPENCODE_EVENT_MAP`) | -| Pi | `PI_HOOK_EVENT_TYPES` | snake_case | yes (`PI_EVENT_MAP`) | -| Gemini CLI | `GEMINI_HOOK_EVENT_TYPES` | PascalCase | yes (`GEMINI_EVENT_MAP`) | - -## Your task - -### 1. Fetch upstream docs and extract the canonical event-name list per CLI - -| CLI | Docs URL(s) | -|-----------|------------------------------------------------------------------------------------| -| Claude | https://code.claude.com/docs/en/hooks (primary, full reference table) | -| | https://code.claude.com/docs/en/hooks-guide (secondary, summary table) | -| Codex | https://developers.openai.com/codex/hooks | -| Copilot | https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks | -| Cursor | https://cursor.com/docs/hooks | -| OpenCode | https://opencode.ai/docs/plugins/ | -| Pi | https://www.npmjs.com/package/@mariozechner/pi-coding-agent | -| Gemini | https://geminicli.com/docs/hooks/ | - -For each CLI, use `WebFetch` on its docs URL(s) and extract the complete list of -hook event-type names from the lifecycle / triggers / "Available events" table -or section. Where two URLs are listed (Claude only), union the results and -prefer the reference page if they disagree. - -**Use the upstream casing exactly as it appears in the docs.** Do not normalize: -- Codex really is snake_case (`pre_tool_use`, `permission_request`, …). -- Cursor really is camelCase (`preToolUse`, `beforeSubmitPrompt`, …). -- OpenCode really is dot-namespaced (`tool.execute.before`, `session.idle`, …). -- Pi really is snake_case (`tool_call`, `user_bash`, `session_shutdown`, …). - -If a docs URL returns 404, redirects to a stub, or the page does not expose a -parseable event list, mark that CLI as `unverified` for this run and skip its -diff. **Do not invent events. Do not guess from prior knowledge.** Pi is the -most likely `unverified` candidate because its event surface is documented -primarily in the package source (`@mariozechner/pi-coding-agent` on npm), and -the README may not include a clean enumeration. - -### 2. Read the current array values - -Read `src/hooks/types.ts` and extract the current value of each -`*HOOK_EVENT_TYPES` array. - -### 3. Diff each verified CLI - -For every verified CLI, compute: -- **added**: events in upstream docs but NOT in the array -- **removed**: events in the array but NOT in upstream docs - -### 4. If no verified CLI has drift - -Write the following JSON to `.sync-hook-events-output.json` in the repo root: -```json -{ "changed": false } -``` -Then stop. (`unverified` alone is NOT drift — the docs may simply be unreachable -today.) - -### 5. If one or more verified CLIs have drift - -#### a. Update arrays in `src/hooks/types.ts` - -For each CLI with drift: -- **Append additions** just before `] as const`, preserving casing. -- **Delete removals** from the array. If the CLI has an `*EVENT_MAP` (see the - table at the top), also delete the same key from that map block — TypeScript's - `Record` is exhaustive, and a stale key would - fail the build. - -#### b. Update hardcoded test counts (only two CLIs need this) - -Other CLIs reference `*HOOK_EVENT_TYPES.length` directly in tests, so no -hardcoded-count fixup is needed for them. - -- If `HOOK_EVENT_TYPES` (Claude) changed: in `__tests__/hooks/manager.test.ts`, - update the test description string `installs hooks for all event types` - AND both `expect(Object.keys(written.hooks)).toHaveLength()` assertions to - the new total. -- If `GEMINI_HOOK_EVENT_TYPES` changed: in `__tests__/hooks/integrations.test.ts`, - update the `expect(gemini.eventTypes).toHaveLength()` assertion AND the test - description string `writeHookEntries writes the matcher-wrapper schema for all - events with matcher='*'`. - -Locate these by searching for the current count number. - -#### c. Do NOT add `*EVENT_MAP` entries for newly-added events - -Each new agent-side event needs a canonical Claude PascalCase mapping that -requires human judgement (e.g. "is this a `PreToolUse`-class event? a -`SessionStart`? a passthrough no-op?"). For Codex, Cursor, OpenCode, Pi, and -Gemini drift, **leave the corresponding `*EVENT_MAP` untouched**. The -TypeScript build will fail with `Property '' is missing in type -Record<...>` and the ` keys exactly match` test will fail — both are -intentional and surfaced in the PR body. The reviewer adds the map entries, -pushes a fixup commit, and CI goes green before merge. - -For Claude and Copilot drift only, no event map exists — the build stays green -on the auto-commit alone. - -#### d. Do NOT touch `GEMINI_TOOL_MAP` - -That table maps Gemini's tool names (not event names) and is updated through -a different process. New tools surfaced by Gemini docs are out of scope for -this prompt. - -### 6. Write the structured output - -Write to `.sync-hook-events-output.json` in the repo root: -```json -{ - "changed": true, - "diffs": { - "claude": { "added": ["..."], "removed": ["..."] }, - "codex": { "added": [], "removed": [] }, - "copilot": { "added": [], "removed": [] }, - "cursor": { "added": ["..."], "removed": [] }, - "opencode": { "added": [], "removed": [] }, - "pi": { "unverified": true }, - "gemini": { "added": [], "removed": [] } - }, - "prTitle": "[auto] sync hook event types with upstream agent CLI docs", - "prBody": "" -} -``` - -Include a key for every CLI that was checked. Use `{"unverified": true}` (and -omit `added`/`removed`) for any CLI whose docs could not be parsed. - -The `prBody` MUST be a Markdown string containing, in order: - -1. **Summary table** — one row per CLI: - `| CLI | status | added | removed |` where status is one of - `up to date`, `drift`, or `unverified`. -2. **Per-CLI sections** — one section per CLI with non-empty drift, listing - added and removed events as bullet lists. -3. **Reviewer checklist** — for every newly-added event in a CLI that has an - `*EVENT_MAP`, an unchecked checkbox reminding the reviewer to add the - mapping. Use `"???"` as the canonical value placeholder; **do not guess**: - ```text - - [ ] Add `: "???"` to `` in `src/hooks/types.ts` - (canonical Claude `HookEventType` chosen by reviewer) - ``` -4. **Sources** — a list of the docs URL(s) consulted per CLI. -5. **Unverified notes** — for each `unverified` CLI, one short line explaining - why (404, page lacked a parseable event list, etc.). -6. **Final note** (verbatim): - > **CI is expected to fail on this PR if a CLI with an `*EVENT_MAP` gained - > new events. A reviewer must add the missing map entries before merging. - > For drift in Claude or Copilot only (no event map), CI should pass on - > this commit alone. CI must pass and this PR must be reviewed before - > merging.** - -## Constraints - -- **Only edit these files**: - - `src/hooks/types.ts` - - `__tests__/hooks/manager.test.ts` - - `__tests__/hooks/integrations.test.ts` - - `.sync-hook-events-output.json` -- Do NOT run any shell commands (no git, no gh, no bun, no curl). -- Do NOT modify `src/hooks/integrations.ts`, `src/hooks/policy-evaluator.ts`, - `src/hooks/manager.ts`, `src/hooks/handler.ts`, or any other source file. -- Do NOT add entries to any `*EVENT_MAP` or to `GEMINI_TOOL_MAP` for - newly-added events. Removing keys from a map when their array entry is - removed IS allowed (and required to keep the build green). -- Do NOT invent events. If WebFetch fails or the docs don't expose a clean - event list, mark the CLI `unverified` and move on.