Skip to content

Gates ​

A gate checks what a unit leaves, its commits and the files its steps wrote, and fires when a human should look first. Where it fires decides what happens:

The unit leavesA fired gate
A PR (draft_pr, pr)Puts a decision card on top of the PR; a deliver: pr PR stays a draft
A push onto your PRStill pushes, the card atop the push's summary in the report
A plan or reportMakes the post wait for your approve, the card on top

Gates go at three levels: top-level and per repo, which run on every chain that publishes commits, and on a rule. Each gate has a name and one of:

  • changed: git globs from the repo root, firing when the chain's commits touch a match;
  • command: a script, firing when it exits non-zero, its stdout the gate's line;
  • skill: a judge.
yaml
gates:
  schema: { changed: ["**/migrations/**", "**/*.prisma"] }
  routes: { command: scripts/route-diff.sh } # non-zero exit = fires, stdout = the line
  deps: { changed: [package.json, pnpm-lock.yaml] }
  risk: { skill: judge } # a model reads the chain's commits and may fire it
repos:
  acme/backend:
    gates:
      schema: { changed: ["src/db/schema/**"] } # joins the top-level schema globs
      queues: { changed: ["src/jobs/**"] } # only this repo has it

Top-level gates apply to every repo; a repo's gates only add. A gate both define fires on either's globs, and if both have a command or skill, both run and either fires it. A repo can't turn off or loosen a top-level gate.

A command diffs from $AWAYWORK_FROM: the chain's start commit, or git's automatic merge on a sync a model resolved. Never from a branch like origin/main: the clone's refs are the agent's to move. It also gets $AWAYWORK_OUT_DIR, a read-only copy of what the steps left in .away/out/: the PR body, a screenshot a step saved, any other file.

sh
#!/bin/sh
# scripts/route-diff.sh: fires when the chain adds a route
routes=$(git diff --text "$AWAYWORK_FROM" HEAD -- src/routes | grep -E '^\+ *router\.(get|post)')
[ -z "$routes" ] || { echo "$routes"; exit 1; }

The decision card ​

Gates run on every chain that publishes commits, after its steps and verify, judges last, on the chain's own commits. A fired gate puts the card first in the PR body, under the title, above the agent's text and awaywork's chain notes:

markdown
## Needs a decision

### Gates (from scripts)

- schema: migrations/20261023_add_vat_mode.ts
- routes: + GET /exports/vat   + POST /exports/vat/retry

### Judge

- risk: VAT rounding moved from the invoice total to each line → totals change on open invoices

---

With none fired, that spot holds Gates: none fired (schema, routes, deps) above the same rule. So with gates, awaywork's section comes first and the agent's text sits below the rule, a made-up card included; that's all it guarantees. No gates, no section. A changed line lists the first three matching paths and how many more; a command line is its stdout on one line, cut at 300 characters, or why it failed if it printed nothing.

By deliver:

  • pr opens as a draft, the report saying draft: gate schema fired with the card's lines under the unit.
  • draft_pr was a draft anyway: only the card is new.
  • push still pushes. Your PR's body is yours, so the card tops the push's summary in the morning report: 1 commit pushed onto feat/vat-export, gate schema fired, report below.
  • report, plan and pending_review commit nothing, so these gates don't run; a plan or report rule takes gates of its own.

Gates fail closed ​

A command that can't run, times out, is stopped or isn't run on an untrusted author's PR fires its gate with the reason: - routes: `scripts/route-diff.sh` not checked: timed out after 15 min.

The agent could rewrite the script that judges it, so right before a command runs, awaywork compares each file it names with that path at the clone's start commit, before setup or any agent (on an awaywork PR a rule took up, where that PR left the default branch). A file that differs, is new or is gone fires the gate unrun (- routes: `scripts/route-diff.sh` changed, not run), committed, uncommitted, hidden from git status or changed by verify alike.

A changed gate is the one to trust: awaywork computes it from the commits it pushes (Safety).

Details:

  • The command must name its script by repo-root path, as a word of its own: bash ./scripts/route-diff.sh or (scripts/route-diff.sh) && echo ok count, cd scripts && sh route-diff.sh doesn't.
  • A named file missing at the chain's start always fires it, so don't name one the command writes.
  • Only named files are compared, and the command runs repo code the agent may have changed: a sourced helper, code it calls, a module loaded from beside it. python3 scripts/gate.py imports an agent-committed scripts/json.py before the real json, so run Python with -I; Node looks for node_modules up from the script the same way.
  • A gate can't change .away/out/: once the gates ran, awaywork puts back what the steps left, as plain files, so a link or an executable bit in it is dropped.
  • What can't blank the diff: replace refs (the command's git ignores them); attributes like * -diff, which make it Binary files … differ (awaywork deletes .git/info/attributes first, and git reads .gitattributes files, committed or not, from the start commit, not the worktree; older git ignores that, hence --text); the steps' git config, like diff.external or a textconv driver (the command's git reads a fresh global config awaywork writes, with your identity and git-lfs's filter, no attributes file).

Judges ​

A skill gate is a judge: a model that reads the chain's commits and fires when it finds something a human must decide. After the scripts, awaywork runs that skill in the chain's clone as one more model run, routed on the rule's providers and models like a step, told which commit to diff from, where the read-only copy of the steps' .away/out/ is, and to change nothing. Writing fires to .away/out/verdict fires the gate, the first ten lines of its report.md going into the card's Judge part.

The built-in judge looks for schema and data changes, public routes and API contracts, money and rounding, auth, infra, performance-sensitive paths, dependency changes, anything irreversible. Name your own skill to judge by your rules: yours or a built-in, never the repo's. A gate's instructions, files relative to the config, reach its judge's prompt (risk: { skill: judge, instructions: [judges/money.md] }); the rule's and repo's don't, being written for the agent it judges.

Details:

  • Any other verdict, or none, adds nothing. Scripts set the minimum; a judge only adds, and can't clear a script's gate, even one with its name.
  • It reads what the agent wrote, // reviewed: safe comments included, so it can be fooled, and then it just doesn't fire.
  • It can't change what's published: a commit, moved HEAD, uncommitted change, changed .away/out/ file or agent config it leaves (.claude/settings.local.json too, though git ignores it) is put back, and the gate fires with the judge changed the clone; awaywork dropped its changes. A worker process that outlived the watchdog could still change a file after that.
  • It fails closed: a judge that errors, times out, is stopped or can't run fires with judge didn't run: <why>, so a run out of quota drafts its deliver: pr PR.
  • Each judge is one more model run per published chain, on the steps' quota, logged as acme-backend-412: gate risk, judge on claude, its own log beside what the chain published (out/judge-risk-judge.log).

Gates of a rule's own ​

A rule takes gates of its own, for a check only its units need. On a rule that opens a PR or pushes, a planned one included, they run with the top-level and repo gates, on top of them, and one that fires does what theirs does:

yaml
queue:
  - skill: implement
    where: { labels: ui }
    deliver: pr
    gates:
      proof: { command: scripts/has-screenshot.sh } # reads $AWAYWORK_OUT_DIR/screenshot.png
      copy: { changed: ["src/locales/**"] }

Like the others, they run only when the chain has commits: one that left none reports, ungated. The exception is a chain that left no commits and has on_done to write, say a review that approves, so the fix after it is passed over: the rule's own gates run first, on $AWAYWORK_OUT_DIR alone (no $AWAYWORK_FROM, and changed matches nothing), and one that fires holds on_done's comment and move back, its line in the unit's report. The unit is still handled: nothing retries it and awaywork approve has nothing to approve, so the comment and the move are yours to make, or change the item to run it again. Top-level and repo gates don't run there.

A rule whose skills all report, as review does, could never run them, so its items are skipped with set deliver: report (review commits nothing, so its gates would never run), and awaywork doctor warns about the rule.

A deliver: plan or report rule's own gates are the only ones it has (top-level and repo ones never run there: no diff). They run on what the step left, and one that fires makes the post wait for your approve, the card on top:

yaml
queue:
  - skill: plan
    deliver: plan
    wait: false # goes ahead unless a gate fires
    gates:
      product: { skill: judge, instructions: [judges/product.md] } # your rules, from the config only
      size: { command: scripts/plan-size.sh } # reads $AWAYWORK_OUT_DIR/plan.yml
  • A command gets $AWAYWORK_OUT_DIR, a read-only copy of the step's .away/out/: report.md, the post's text, a plan's plan.yml, any other file it wrote. No $AWAYWORK_FROM: nothing was committed.
  • A judge gets the same copy, told to judge those files against the item, not a diff. The built-in judge looks for product calls: acceptance criteria that can go two ways, UX or copy, scope the card doesn't settle, data kept or shown to someone new, a contract another team owns.
  • The post is what the step left: awaywork restores .away/out/ before posting, and a judge that changed it fires.
  • None fired: Gates: none fired (product, size) tops the post, which under wait: false goes ahead, its mark line naming the gates that ran clear.
  • On a planned rule that reports, one that fires makes the task's result wait, and holds the card's other tasks until you answer.
  • A rule gate takes no name a top-level or repo gate has, and on a plan or report rule no changed: there's no diff. A pending_review rule takes no gates.