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.toml— the obligations per category, plus park and cancel.step.toml— 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).
With [output] compact mode on for story-diff (see satelle help compact-output), patch loses its index lines and offloads noisy/
whitespace-only hunks behind a <<ccr:HASH,KIND,SIZE>> marker that satelle retrieve <hash> resolves exactly — --json prints the plain form instead.
Implementation-exit reviewers also receive that same enumeration on the
transition payload as diff (files, stat, patch) whenever a baseline exists —
no executor attachment and no shell. docs, prior_verdicts, route_drift,
and diff are facts the binary attaches; the skill decides. A missing
baseline is a no_baseline marker, never a refused transition. The patch is
capped under its own ceiling so it cannot starve the plan.
satelle story proof (test enumeration)
Same family as satelle story diff and satelle ledger citation: the binary
reports, the gate skill decides. It lists tests added or changed since the
engagement baseline. Report only — no pass/fail; it does not run tests.
satelle story proof <id>
# or from a functional check (payload on stdin, no argv id):
satelle story proof # reads story.id from {story, from, to} on stdin
JSON fields: story_id, baseline, head, dirty, state, tests
(path + language + functions when parseable), skipped (path + reason),
non_test_files (changed paths the classifier did not treat as tests).
state is ok, no_baseline, foreign_tree, or no_git. Every enumerable
state exits 0, including no baseline and no new tests. Non-zero only for an
unknown id or a non-story item.
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]] entry in step.toml 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]] entry naming
satelle-step-summary, 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.