← All docs

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 its agent, its skills, and the reviewers gating 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.