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
# The merge train. Bind a ticket to it with `flow: train` or
# `sloop post --flow train`; the `default` flow is untouched and still what
# a ticket gets when it names none.
#
# The problem it solves: what lands on the default branch is not the run
# branch, it is the *merge* of the two — a tree no stage ever tested. While
# a run is in flight the default branch keeps moving, so by the time the
# merge stage runs, the thing every earlier stage judged may no longer be
# the thing about to be integrated.
#
# The train closes that gap with ordinary stages. `sync` integrates the
# default branch into the run branch, `verify` tests the tree that produces,
# and the merge is `ff_only` — a fast-forward, which can only succeed while
# the default branch is still the one `sync` integrated. If it moved in
# between, the fast-forward is impossible, the stage fails without touching
# anything, and `return_to: sync` runs the train around again.
stages:
- name: build
# An agent action takes its prompt from the ticket, never from here.
action: agent
result_check:
fail_action: fail
# To review work before it lands, add a stage like the `default` flow's
# review here — *before* `sync`, not between `sync` and `merge`. A failed
# fast-forward re-runs only the span from `sync` through `merge`, so a
# stage in this position is judged once however many laps the train
# takes, while one inside the loop re-runs — and re-spawns its model —
# every lap. The trade is that a reviewer here sees the pre-merge tree;
# `verify` still gates the merged one. Only a check that must judge the
# exact landing tree belongs inside the loop, priced accordingly.
#
# - name: review
# action:
# exec:
# - claude
# - --print
# - --allowedTools
# - Bash
# - --
# - "Read .agents/sloop/prompts/review.md and follow its instructions."
# result_check: reported
# Merges the default branch into the run branch, inside the run worktree.
# Passes when that commits cleanly or there was nothing to integrate;
# fails on a conflict, leaving no in-progress merge behind, so the stage
# this returns to starts from a clean tree. The conflict itself lands in
# the run log, and so in the re-entered agent's prompt — as a report,
# never as conflict markers in the tree. That means this edge recovers
# work that can be *reshaped* around what the default branch now holds;
# it cannot resolve a conflict in place, so two independently correct
# changes to the same lines usually conflict again, spend the budget,
# and park the ticket in `needs_review` for a human to resolve the
# preserved branch by hand.
- name: sync
action:
result_check: none
fail_action:
# Everything that decides whether the work is good runs *after* the sync,
# because the merged tree is the one that will land. Replace this command
# with your own; `sloop init` fills in `flow.test_cmd` from config.yaml
# when you have one configured.
- name: verify
action:
result_check: none
fail_action:
# `ff_only` refuses the merge commit that would otherwise paper over a
# default branch that moved. `result_check: none` is not a shortcut: the
# merge is the one irreversible act in the flow, and nothing that runs
# after it could un-take it, so its own outcome is the only verdict it
# can honestly carry.
#
# The attempts budget is generous because a lost race is cheap: a lap
# re-runs only `sync` and `verify` — git and the test command, never an
# agent — so riding out a burst of simultaneous finishers costs no
# model spawns.
- name: merge
action:
result_check: none
fail_action: