Replay an agent skill on every PR with the Reelier GitHub Action
This guide turns a recorded agent skill into a check that replays on every pull request and blocks the merge the moment the tool calls drift — with no model in the loop and 0 tokens per run. It is the plumbing behind "run agent tests in CI": a frozen .skill.md in your repo, a workflow that replays it, and a red check when a recorded assertion stops holding.
GitHub Actions is where you RUN a skill, not where Reelier captures one — reelier scan has no CI transcript to read. So you record the skill first on the coding agent you work in, commit the file, and wire the seldonframe/reelier@v1 Action to gate it. Four steps follow: prove the skill locally, add the workflow, open a PR, watch it fail on drift. Every command is real and every output is verbatim.
Before you start
- A recorded skill committed to the repo you want to guard — a plain
.skill.mdfile. Record it first on your coding agent (freeze a Claude Code session, or wrap an MCP server withreelier mcp --wrapon Cursor, Codex or Windsurf); the linked guides cover recording. - A read-only skill makes the best gate. Reelier replays MCP and tool-call steps plus the
http.get/http.postbuiltins — notBash,Read,Edit,WriteorGrep— so pick a run whose value was in the API calls, like a data pull or a status check. - A GitHub repository with Actions enabled. No account, API key or model is needed at
--max-level 0— the replay costs 0 tokens, and acloud-keyis optional.
Prove the skill passes locally first
The Action runs one command for you — reelier run <skill> --max-level 0 — so run it yourself before wiring CI. It re-executes the recorded tool calls against the live systems and checks each recorded assertion. The repo's bundled npm-info.skill.md (a single read-only http.get to the npm registry) reproduces the receipt below exactly.
The skill asserts minimally — reelier's minimal-assertion rule: a step asserts a shape, not a brittle value, and only when it recorded a clean result. Here the http.get step asserts status == 200 and nothing else. At --max-level 0 — the default — no LLM client is ever constructed, so the replay costs 0 tokens and exits 0 when every assertion holds. That exit code is what the check gates on.
reelier run npm-info.skill.md --max-level 0✓ Step 1 — Fetch registry metadata and extract version/license/tarball [passed] 461ms PASSED: 1/1 steps ok, 0 failed, 461ms total
Add the seldonframe/reelier@v1 workflow
Create .github/workflows/reelier.yml. The Action is a composite step published at seldonframe/reelier, tag @v1; after an actions/checkout@v4 it installs reelier@^0.12, runs the same reelier run <skill> --max-level 0 you just ran, writes the receipt as the job summary, and fails the check on the run's own exit code — you never wire the pass/fail yourself.
Trigger it on: pull_request so a drifted step blocks the merge, and scope it with paths: ["my-job.skill.md"] so it only fires when the skill changes; add a schedule: cron for a standing dated proof on a public repo. In the with: block, set skill: to your committed file and keep max-level: "0" — the default that needs no credentials. The full copy-paste YAML, the inputs table, and the passed / run-record-path outputs live on the platform page; this guide links it rather than restating it.
Open a pull request and read the receipt
Push the workflow and the skill on a branch and open a PR. The Action replays the skill and posts a receipt as the run's job summary: the skill name, a PASSED / FAILED verdict, and a per-step table of passed / unchecked / skipped / failed with the duration and token spend (0 at level 0).
Read the unchecked column honestly — a step that ran without error but asserted nothing is reported as unchecked, never folded into passed. A green check means every recorded assertion actually held, not merely that the CLI ran. The exact markdown the Action writes is the closing receipt below.
Watch it turn red when the tool calls drift
The gate earns its keep on the day the recorded run stops matching. If an upstream contract changes — the endpoint returns a 500, a field moves — the recorded assertion (status == 200) no longer holds, the step fails, reelier run exits non-zero, and the Action's final step re-raises that exit code. The check turns red and the PR cannot merge on a drifted tool call — regression testing for agent tool calls, with no model in the loop.
When the change was intentional, update the baseline as you would a snapshot: re-record and recompile the .skill.md, or edit the failing assertion directly — it is markdown in your repo. Replay stays read-only by default, so a PR or scheduled gate never re-fires a recorded write; a write step only re-executes under --allow-writes (idempotent writes only — a destructive step still needs --yes), which the Action does not pass.
The receipt
## Reelier replay: `npm-info.skill.md` **Skill:** `npm-info` **Result:** ✅ PASSED | steps | passed | unchecked | skipped | failed | duration | | --- | --- | --- | --- | --- | --- | | 1 | 1 | 0 | 0 | 0 | 461ms | > `unchecked` = ran without error but asserted nothing — the honest-success rule (SPEC.md §4.3) means it is never counted as a pass. _Ledger: not pushed (no `cloud-key` input set)._
The receipt the Action posts as its GitHub job summary — the markdown you see on the PR's checks tab. Produced on 2026-07-22 with reelier v0.14.0 by running the Action's own .github/scripts/gha-summary.mjs against the .reelier/runs/npm-info.jsonl record that reelier run npm-info.skill.md --max-level 0 wrote (the 461ms matches Step 1 — one record, two views). The collapsible per-step detail block is trimmed here for space; nothing else is edited. 0 tokens; cloud-key was unset, so the replay was local-only and still wrote this summary.
FAQ
The Action didn't fire on my pull request — why?
Usually the paths: filter scoped it to skill changes, so a PR that doesn't touch the .skill.md won't trigger it — drop or widen paths: to run on every PR. Also confirm the workflow file exists on the base branch: GitHub only runs the version of a workflow already on the target branch.
How do I gate on more than one skill?
The skill input takes a single path. For several skills, call the action once per skill — repeat the step (or use a matrix) with a different skill: each time. Each replay fails the check independently, so one drifted skill is enough to block the merge.
Do I need a Reelier account or a cloud-key?
No. cloud-key is optional — leave it unset for a local-only replay that still writes the job summary and still gates the check. The pass/fail depends only on reelier run's exit code, never on Cloud. Set cloud-key (as a GitHub secret) only to push the receipt to your ledger after a passing run.
- seldonframe/reelier — the open-source (AGPL-3.0) CLI and Action every command here runs
- action.yml — the Action's inputs (skill / max-level / cloud-key) and composite steps
- reelier-replay.example.yml — a ready-to-copy workflow template in the repo
- GitHub Actions: the pull_request event — how the PR trigger fires