CI and scripts
Recipes
llx has no GitHub, GitLab or review feature, and needs none: llx decide and llx exec read
stdin and write stdout, so the CLIs you already have read the input and post the answer. Each
recipe is one line you can run as is; the outputs below are from real runs.
llx decide: one of a few options, about a second, a fraction of a cent. No tools.llx exec --mode plan: the agent reads the code and answers. Theedittool is refused, and so is a command when no one is there to approve it.llx exec --allow bash --deny edit: the agent may run commands to explore and confirm, and edits no file with its tool.llx exec -y: the agent may act (pages, commands, edits). Only on input you trust.
Both need OPENROUTER_API_KEY (exec takes any provider: docs/ci.md).
These one-liners run at a terminal, with your own gh or glab login. In CI the job that runs
the model must not hold the token that posts: read docs/ci.md for the
GitHub Action and the two-job pattern, and In a workflow below.
Review
A pull request reviewed, the comment posted:
gh pr diff 42 | llx exec --mode plan "Review this diff: bugs and risks only, a short markdown list with file:line." | gh pr comment 42 -F -- `lib.py:4` — A hard-coded GitHub token is committed in source; revoke it.
- `lib.py:10` — `line.split(sep)` without a maximum split raises ValueError for `key=a=b`.
- `lib.py:17` — Removing the blank-line guard sends empty lines to `parse`.The same on GitLab (glab mr note create reads the note from stdin):
glab mr diff 42 --raw | llx exec --mode plan "Review this diff: bugs and risks only." | glab mr note create 42A verdict as well as words, for gh pr review:
gh pr diff 42 | llx exec --mode plan --schema @review.json "Review this diff." > review.out
gh pr review 42 "--$(jq -r .verdict review.out)" -b "$(jq -r .body review.out)"
# review.json: {"type":"object","properties":{"verdict":{"enum":["approve","comment","request-changes"]},
# "body":{"type":"string"}},"required":["verdict","body"]}Gates
A step that fails when the answer is not the one expected (--expect: exit 1):
git diff origin/main... | llx decide "Does this diff add a secret, token or password?" --expect no
git diff origin/main... | llx decide "Does this change break the public API?" --expect no
gh pr diff 42 --name-only | llx decide "Does this list include a changelog?" --expect yesTriage
gh pr edit 42 --add-label "$(gh pr diff 42 | llx decide "Which label fits?" --choices bug,feature,docs,chore)"
gh issue edit 7 --add-label "$(gh issue view 7 --json title,body | llx decide "What is this issue?" --choices bug,question,feature)"When CI fails
gh run view 1234 --log-failed | llx decide "Flaky (network, timing) or a real bug?" --choices flaky,real
gh run view 1234 --log-failed | llx exec --mode plan "Why did CI fail? One paragraph, with the fix."real
CI failed because `test_values_with_equals` passes a value containing `=`, so `line.split(sep)`
returns more than two pieces. Fix `lib.py:8`: `key, value = line.split(sep, 1)`.An incident in CI
What kind of failure it was, in about a second (a repeated --choices keeps the commas of its
description):
gh run view 1234 --log-failed | llx decide "What kind of CI failure is this?" \
--choices "test=a test failed" --choices "build=the code does not compile" \
--choices "dependency=a package could not be installed" \
--choices "infra=the runner, network or a service was down" \
--choices "permissions=a token or permission was refused" --choices "timeout=a time limit was hit"infraThe whole incident, to route it: rerun what a rerun fixes, hand the rest to its owner.
gh run view 1234 --log-failed | llx exec --mode plan --schema @incident.json "Classify this CI failure." > incident.out
jq -e .retry incident.out && gh run rerun 1234 --failed
# incident.json: kind (test, build, lint, dependency, infra, permissions, timeout), severity (low, medium, high),
# retry (boolean: running it again is likely to pass), owner (author, platform, security), summary, next{"kind":"infra","severity":"medium","retry":true,"owner":"platform",
"summary":"Checkout failed because the repository URL returned HTTP 503: a transient service failure, not the code.",
"next":"Retry the job; if 503s persist, check GitHub's status."}In a workflow it runs when another one fails, and the rerun is a second job with its own token: examples/github/ci-failure.yml.
Releases
git log v1.2.0..HEAD --format=%B | llx decide "What release is this?" --choices "patch,minor=a new feature,major=a breaking change"
git log v1.2.0..HEAD --oneline | llx exec --mode plan "Release notes in markdown, grouped by kind." | gh release create v1.3.0 --notes-file -
git diff --staged | llx exec --mode plan "Describe what this diff does as a one-line conventional commit message. The message alone." | git commit -F -After a deploy
The agent opens the page in a real browser and checks it:
llx exec -y --schema '{"type":"object","properties":{"ok":{"type":"boolean"},"seen":{"type":"string"}},"required":["ok","seen"]}' \
"Open https://staging.example.com, sign up with [email protected], and check that the page welcomes you."{"ok":true,"seen":"Welcome!"}On a Linux runner it needs the full build, which needs WebKitGTK 4.1 installed before llx, and a
display: xvfb-run -a llx exec …. The headless build has no browser (docs/ci.md).
On a schedule
tail -n 200 /var/log/app.log | llx decide "Should someone be paged now?" --choices page,ignoreIn a workflow
A workflow is the same recipe with the work split in two jobs. The first runs the model with a
read-only token and answers as data; the second posts it, with the write token and no model key.
The action runs llx exec for the first:
on: pull_request
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 15
permissions: { contents: read, pull-requests: read }
outputs:
result: ${{ steps.llx.outputs.result }}
steps:
- uses: actions/checkout@v7
with: { fetch-depth: 0, persist-credentials: false }
- uses: rsaryev/llx@v0
id: llx
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
with:
model: anthropic/claude-haiku-5.5
allow: bash
deny: edit
prompt: |
Review pull request #$PR with gh and git. Bugs and risks only, as a short markdown list.
post:
needs: review
runs-on: ubuntu-latest
permissions: { pull-requests: write }
steps:
- run: gh pr comment "$PR" --body "$BODY"
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
BODY: ${{ needs.review.outputs.result }}Without the action, a script step installs llx and runs llx exec itself (docs/ci.md).
GitLab CI has the same two jobs: docs/ci.md.
Untrusted input
A diff, an issue or a log is written by whoever opened it, and may carry instructions. llx hands it to the model as data, not as yours, but keep the job safe on its own:
- run the model in a job with a read-only token, and post from a second job that has no model key and runs nothing from the pull request;
- review and triage with
--allow bash --deny edit,--mode planordecide, never-y, and name what the agent may run where the input is from strangers (--allow 'bash(gh pr view *)'); - never run
pull_request_targetwith the pull request's code checked out, and let only people with write access start the agent.
The reasons are in docs/ci.md.