1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# A flow is the ordered list of stages one run walks. The filename is the
# flow name: .agents/sloop/flows/release.yaml defines the flow `release`,
# and a ticket binds to it with `flow: release` or `sloop post --flow
# release`. Save this file as .agents/sloop/flows/<name>.yaml.
#
# Two flows ship inside the binary and need no file: `default`, which is
# build -> review -> merge, and `train`, which syncs and verifies before an
# `ff_only` merge. `sloop init` writes both out so they can be edited.
#
# The walk runs stages in order and halts on the first failure, unless a
# stage's `fail_action` says otherwise. Evidence is persisted per stage
# execution, so a daemon restart resumes exactly where the walk stood.
#
# Every stage is three things, and every stage below writes all three:
# - `action`: the work, which is never trusted to grade itself.
# agent the ticket's agent target, prompted by the
# ticket body
# { exec: [argv, ...] } an argv (no shell) in the run worktree
# { builtin: merge } Sloop's merge policy
# { builtin: sync } merges the default branch into the run
# branch, inside the run worktree
# - `result_check`: what decides the stage's verdict.
# none the action's own exit status: 0 passes
# reported the action must call `sloop verdict`
# { exec: [argv, ...] } an independent command decides
# { builtin: commits } passes when a new run-branch commit exists
# { panel: {...} } several reviewers report; a quorum decides
# - `fail_action`: what the walk does with a failure.
# fail halt; stages after it are never requested
# (the default)
# continue advisory: record the failure and carry on
# { return_to: <stage>, attempts: N }
# re-run the span from <stage> through this
# one, up to N times
#
# Structural rules the parser enforces:
# - Any stage may be an `agent` action, in any position and any number of
# times. Each gets its own supervised process and worker credentials.
# - An `agent` action may not use `result_check: none`. An agent that
# grades itself by exiting cleanly grades nothing.
# - At most one `{ builtin: merge }` action, and it must be last. It must
# use `result_check: none` — the merge result is the verdict — and the
# merge builtin may never be a check.
# - Any number of `{ builtin: sync }` actions, anywhere before the merge.
# Same rules otherwise: `result_check: none`, never a check.
# - `ff_only` is only meaningful on the merge stage, and writing it on any
# other action is an error rather than an ignored key.
# - Stage names must be unique.
# - `return_to` must name an earlier stage, `attempts` must be 1..=3, and
# the worst case a flow's budgets imply may not exceed 32 executions.
# Panel reviewers are spawns and count towards that budget.
stages:
# ---- an agent action -------------------------------------------------
# Spawns the ticket's agent target in the run worktree with the ticket
# body as its brief. Any number, in any position.
- name: build
action: agent
# result_check: { builtin: commits }
# Passes when the process exits 0 *and* Sloop observes at least one
# new commit on the run branch. This is the default check for an
# agent action; an agent that exits cleanly without committing
# leaves the branch for review rather than merging an empty change.
result_check:
# fail_action: fail
# Halt. Stages after this one are never requested, the ticket lands
# in `needs_review` or `failed`, and the branch is preserved. This is
# the default, written out here only because this template writes
# all three parts on every stage.
fail_action: fail
# ---- an exec action, and the one backward edge ------------------------
# Runs an argv (no shell) in the run worktree. Any number of these, in
# order, before the merge stage.
- name: test
action:
# result_check: none
# The action's own exit status is the verdict. This is the default
# for `exec`, `merge`, and `sync` actions.
result_check: none
# fail_action: { return_to: <stage>, attempts: N }
# A failing test sends the walk back to `build`, which re-runs with
# the failure in its prompt: the stage that failed, why, and the last
# 100 lines of its output. The whole span from `build` through `test`
# runs again, so no verdict earned before the fix survives it.
# `attempts` bounds the loop; when it is spent the run ends exactly
# as it would have without the edge, and `sloop show` says the
# return_to budget was spent rather than leaving it to be inferred.
fail_action:
# ---- an advisory stage ------------------------------------------------
- name: lint
action:
# result_check: { exec: [...] }
# Requires the action to exit 0, then runs a second command in the
# worktree and takes its exit code as the verdict. Use it when the
# thing that proves the stage is not the thing that does the work.
# The check command must be non-empty.
result_check:
# fail_action: continue
# Advisory: the failure is recorded and shown by `sloop show` — as
# `warn` in the scan strip and `failed advisory` in the stage table
# — but the walk carries on and the run's outcome is unchanged. Use
# it to report on something without blocking the merge on it.
fail_action: continue
# ---- a reported stage -------------------------------------------------
- name: review
# `--allowedTools Bash` lets the reviewer run tests and `sloop
# verdict`; it deliberately omits Write/Edit so the reviewer can read
# and run but not rewrite the work. `--` ends the variadic tool list
# so the prompt stays a positional.
action:
exec:
- claude
- --print
- --allowedTools
- Bash
- --
- "Read .agents/sloop/prompts/review.md and follow its instructions."
# result_check: reported
# The stage must call `sloop verdict pass|fail --reason <text>`
# exactly once. The first report is final, and a stage that ends
# without one fails with `no verdict reported`. This is the only
# check that keeps SLOOP_SOCKET and SLOOP_TOKEN in the stage's
# environment, so it is the only way a command can grade itself.
# Under `none` a reviewer that always exits 0 — as `claude --print`
# does — would approve every run.
result_check: reported
fail_action: fail
# ---- result_check: { panel: {...} } (the advanced form) --------------
# One reviewer is one uncalibrated opinion. A panel seats 2..=5 of them,
# spawns each in turn with the same prompt, and derives the verdict from
# a quorum of their reports — Pass iff at least `quorum` seats passed. A
# reviewer that never reports counts as a Fail, and `--confidence` is
# recorded but never weighted. Seat different vendors: the independence
# is the whole product, and three seats on one model mostly buy the same
# opinion three times. Every seat is a spawn, so a three-seat panel is
# three review agents each time the stage runs.
#
# result_check:
# panel:
# # A path under .agents/sloop/. One prompt for the whole panel.
# prompt: prompts/review.md
# # `target` must name an entry under `agent.targets`; `model` and
# # `effort` are optional per-seat overrides.
# reviewers:
# - { target: claude }
# - { target: codex }
# - { target: gemini, model: pro }
# # How many must pass. Optional; defaults to all of them.
# require: { quorum: 2 }
# ---- the sync builtin ------------------------------------------------
# Merges the default branch into the run branch, inside the run worktree;
# the shared default-branch checkout is only ever read. Passes when the
# merge commits or there was nothing to integrate, and fails on a
# conflict — leaving no in-progress merge behind, so a `return_to` target
# would start from a clean tree. Any number, anywhere before the merge.
#
# Placed here it is only half the pattern: stages *after* a sync judge the
# tree that will actually land, so a sync belongs before them rather than
# after. See the `train` flow `sloop init` writes beside this one for the
# whole shape, including the `ff_only` merge that makes the verified tree
# provably the one that lands.
- name: sync
action:
result_check: none
fail_action: fail
# ---- the merge builtin -----------------------------------------------
# Applies the run branch to the default branch using Sloop's merge
# policy. Optional, at most one, and must be last. A flow without one
# leaves the branch for a human to integrate.
- name: merge
action:
# ff_only: true
# Refuses the merge commit: the default branch either fast-forwards
# to the run branch or the stage fails, having touched nothing. Pair
# it with an earlier `sync` — the fast-forward can only succeed while
# the default branch is still the one that sync integrated, which is
# what makes the verified tree provably the tree that lands. Absent,
# the merge policy is exactly as it was.
#
# Only `none` is allowed here. A conflicted merge parks the ticket in
# `needs_review` with the branch preserved.
result_check: none
fail_action: fail
# A configured `flow.test_cmd` in config.yaml is inserted as an implicit
# `result_check: none` stage named `test` at index 1, so it cannot be
# combined with a flow that already has a stage named `test`.