sloop-daemon 0.5.0

Agentic coding scheduler — a daemon that runs background coding agents autonomously in isolated git worktrees
Documentation
# 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: { builtin: commits }
    # 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: { exec: [cargo, test, --all-targets] }
    # 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: { return_to: build, attempts: 2 }

  # ---- an advisory stage ------------------------------------------------
  - name: lint
    action: { exec: [cargo, clippy, --all-targets, "--", "-D", warnings] }
    # 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: { exec: [cargo, fmt, "--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: { builtin: sync }
    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: { builtin: merge }
    # 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`.