Reviewer checks — the gates on a story
satelle runs the agent model (see the satelle-agent-model
principle): a story moves through a graph of steps, each run by a defined
agent role, and the story's status decides what is valid now. The agent's goal is
to drive the story to done; satelle is the gatekeeper of status — a status
advances only through a reviewer's accept, and always through it.
- executor — does the work and mutates the tree.
- reviewer — is limited to reviewing: an isolated, fresh-context judge that
reads the requested transition and returns one JSON verdict
{"decision":"accept"|"reject","notes":"…"}. It is read-only and never mutates — a quality-management invariant, enforced by its grant, not by trust.
Each gate is an isolated, fresh-context call: satelle builds the payload (the work item + the requested transition), spawns a fresh agent with the step's skill as its prompt and a read-only grant, and aggregates the one verdict to gate the status. satelle does the context selection; the reviewer reads what it needs through its tools. This applies to stories and tasks alike — gating is by category, kind-agnostic.
The lifecycle is authored — a derived route
The active lifecycle is authored substrate: a repo's own .satelle/workflows,
or the derived route the binary ships as the order-zero default. There is
one authored form — two files:
done.md— the obligations per category, plus park and cancel.step.md— the step catalogue and the always-on gates. Each step names itsagent, itsskills, and thereviewersgating ENTRY to it.
Order and topology are DERIVED, never authored. satelle help workflow-convert
is the key-by-key reference.
A repo's own route outranks the shipped one. A workflows doc that declares no route governs nothing: satelle refuses transitions under it, naming that guide, rather than silently falling back and dropping every gate the repo authored.
The per-noun satelle <noun> validate runs a DETERMINISTIC structure check on
every authored doc — frontmatter (OKF type), naming, a usable definition, and
for a route half its own grammar plus resolvable executor rubrics. The structure
check is code, not an LLM rubric, so it is harness-independent and never flaky.
The done gate is not mandated — it is whatever the route declares (the
author's choice).
An edge is gated only when the workflow names a reviewer skill and that skill's rubric is installed; a named-but-absent rubric is advisory, so a fresh repo keeps working until the rubrics ship.
The agents layer — how a step runs
What is injected (the skill + context subset) is satelle's; how and where an
agent role runs is the agents layer (.satelle/workflows/agents.toml). It binds each agent role to
a backend and grant, defaulting to today's behaviour — the executor runs in-loop,
the reviewer runs as an isolated agent -p with the read-only Read,Grep,Glob
grant. A repo may rebind a backend or grant without touching the workflow; the
read-only limit travels with the binding.
Engagement baseline and satelle story diff (scope gates)
On first entry into a performing/engaging state, satelle ledgers an
engagement_baseline row (git HEAD + dirty flag). Scope judges enumerate
via:
satelle story diff <id>
# or from a functional check (payload on stdin, no argv id):
satelle story diff # reads story.id from {story, from, to} on stdin
Output is JSON: files (sorted, includes untracked), stat, optional patch.
Report only — no pass/fail. The gate skill decides. Missing baseline → clear
error (pre-feature stories degrade gracefully).
Two gate kinds: LLM reviewers and functional checks
A gate is either:
- an LLM reviewer — the skill's markdown body rides as a fresh-context agent's system prompt and the agent returns the verdict (judgment: structure, intent, acceptance); or
- a functional check — a self-contained ```check script (or a
check:in frontmatter). The gate runs it in the repo root; exit 0 accepts, non-zero rejects with the output tail as notes. No LLM — the command is the decision. Like the push gate, a functional check may run real mechanism.
Shared suite evidence — record a run once, cite it from siblings
When a repo's verification suite is expensive (long integration runs, image builds), several small stories delivered at the same commit should not each re-run it. Record the run once as SHA-keyed evidence and let the siblings cite it; the gate then checks the citation instead of the clock.
Record (the story that actually ran the suite):
satelle ledger record-run --story <sty_id> --command 'make integration' --outcome green \
[--sha <commit>] [--started-at <RFC3339>] [--finished-at <RFC3339>]
--sha defaults to the current HEAD. --outcome is green or red. The
command prints the created suite_run entry — keep its id.
Cite (every sibling riding that run):
satelle ledger cite-run --story <sty_id> --run <evt_id>
A citation is a suite_citation ledger row whose refs names the run. Citing
does not validate the target — a dangling citation is a fact the gate must
be able to see. Newest citation wins if a story cites more than once.
Enumerate (what a gate reads):
satelle ledger citation <sty_id>
# or from a functional check (payload on stdin, no argv id):
satelle ledger citation # reads story.id from {story, from, to} on stdin
Output is JSON — report only, no pass/fail. Every enumerable state exits 0:
| Field | Meaning |
| --- | --- |
| cited / citations | a citation exists on this story / how many |
| run_found / dangling | the cited id resolves to a suite_run / it does not |
| run.sha, run.command, run.outcome | what ran, where, and how it ended |
| run.started_at, run.finished_at, run.recorded_at | when |
| head_sha, dirty | the worktree now |
| sha_matches_head | the cited run covers this exact commit |
Non-zero exit is reserved for genuine errors (unknown story, git unavailable).
Sample gate check block
The gate owns the rule and the refusal names — the binary only reports. Drop
this in a reviewer skill's ```check block and set EXPECTED to the suite that
gate requires. Stories carry no delivery SHA, so "this story's delivery" is
defined as clean HEAD at check time; relax or tighten the rule by editing
this script, not the binary.
#!/usr/bin/env bash
# Suite-citation gate: accept when a green run of EXPECTED covers this commit.
set -uo pipefail
EXPECTED="${EXPECTED:-make integration}" # the suite command this gate requires
f=$(satelle ledger citation) || { echo "cannot enumerate the suite citation"; exit 1; }
field() {
printf '%s' "$f" |
grep -oE "\"$1\"[[:space:]]*:[[:space:]]*(\"[^\"]*\"|true|false|[0-9]+)" |
head -1 | sed -E "s/^\"$1\"[[:space:]]*:[[:space:]]*//; s/^\"//; s/\"$//"
}
[ "$(field cited)" = true ] || {
echo "missing_citation: no suite run cited — record one and cite it:"
echo " satelle ledger record-run --story <id> --command '$EXPECTED' --outcome green"
echo " satelle ledger cite-run --story <id> --run <evt_id>"
exit 1; }
[ "$(field run_found)" = true ] || {
echo "dangling_citation: cited run $(field run_id) is not a recorded suite_run"; exit 1; }
[ "$(field outcome)" = green ] || {
echo "red_run: the cited suite run finished $(field outcome)"; exit 1; }
[ "$(field command)" = "$EXPECTED" ] || {
echo "command_mismatch: cited run ran '$(field command)', this gate requires '$EXPECTED'"; exit 1; }
[ "$(field dirty)" = false ] || {
echo "dirty_worktree: uncommitted changes are not covered by the cited run"; exit 1; }
[ "$(field sha_matches_head)" = true ] || {
echo "stale_sha: cited run is at $(field sha), HEAD is $(field head_sha) — re-run the suite and cite the new run"; exit 1; }
echo "suite citation accepted: $(field run_id) green at $(field head_sha)"
exit 0
Whether the cited command is the right suite for this story stays reviewer judgment — the check only proves the named suite was green at this commit. Note the extractor compares the JSON-encoded command, so a suite command containing quotes or backslashes needs a real JSON parser instead.
Create gate — deterministic story structure (code)
When a draft is created (opt-in per repo via [review] gate_create), satelle
checks required structure deterministically in code (no LLM): a specific
title, a clear goal in the body, and at least one numbered, testable acceptance
criterion. The structure reviewers for skills/workflows/principles are likewise
deterministic code (internal/structure), not LLM rubrics — conformance is
mechanical, so a swapped harness can never change what "valid" means.
Begin-work gate — satelle-story-intent-review (→ in_progress)
Judges readiness of intent before work starts — concrete title, clear goal, testable criteria. Unclear intent is rejected; the story stays in backlog.
Release step — release (in-loop executor)
One in-loop executor step (the driving session, not a dispatched sub-process).
It formats and stages the slice, bumps satelle.version (patch) and stamps
satelle.build in .version — mandatory, because .version is the single
source the release tag and build identity derive from — makes a conventional
commit ending in the story id (no AI attribution), and pushes to main
(trunk-based release). Pushing triggers the GitHub Actions test run and, on its
success, the version-gated release run that publishes v<version>. Rather than
block watching both runs, the step refreshes the local service during the CI
window and then records the test + release run URLs, their conclusions, and
the published tag as a PR-style summary with the story — an attachment via
satelle story attach … --file (stored on the home-keyed runtime plane, readable
via satelle story docs <id>). The satelle-story-release-review gate is the authority
on "CI is green": it judges that recorded evidence and rejects a failing, absent,
or unconcluded run.
Close gate — satelle-story-done-review (→ done)
An isolated, read-only reviewer that reads the repository to verify each
numbered acceptance criterion against concrete evidence. Unmet criteria are
rejected with specifics. done is always terminal (see satelle-done-is-last).
The close gate is declared by the workflow, not mandated by the binary — a
workflow may name it, name another, or drop it: if the user breaks their own
process, so be it. The reviewer's grant is read-only (Read,Grep,Glob); it reads
the substrate it reasons about as markdown under .satelle/ (no shell, no CLI).
Declared scoped gates — estimate/actual + integration check
Always-on gates are declared in the route, not injected by a skill tag — the
route is the sole gating authority (no hidden reviewer:always layer). A
## gate <skill> section in step.md carries an on: list of steps and runs on
the transitions into them, after that step's own reviewers:.
satelle-estimate-actual-review (on: in_progress, done) requires a recorded plan
estimate entering in_progress and the recorded actual entering done
(satelle story estimate / satelle story actual); satelle-integration-check
(on: commit) runs make integration before a commit. A step may also name
several reviewers directly (reviewers: a, b). satelle-story-cancel-review
records why an item is abandoned.
Step summary — satelle-step-summary (transparent, opt-in)
Not a gate. The step summary is declared by the route, not a hidden
always-on behaviour: a route opts in with a ## gate satelle-step-summary
section in step.md, optionally mandatory: true. Where declared, after each
transition this read-only observer records a 1–3 sentence step_summary ledger
row; a mandatory summary failure is surfaced on the ledger rather than
swallowed. A route without the gate records no summaries.
Where the rubrics live
The summariser is an embedded canonical default (internal/config/substrate/ skills) and is materialised into .satelle/skills by satelle init. The
deterministic structure checks (skills/workflows/principles/story drafts) are
code (internal/structure), not rubrics. A repo MAY override a materialised
skill — or add its own gates (this repo's push reviewer) — under
.satelle/skills/. The binary runs the gates; the substrate declares them.
See also: satelle help create-story.
Mirrored from satelle’s built-in help. Read it in the binary with
satelle help reviewer-checks, or see the canonical source in the
satelle repo.