Skip to content

0.4.0-rc.1 - 2026-07-25

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 25 Jul 09:08

Release Notes

Added

  • result_check: { panel: {...} } puts 2 to 5 independent reviewers on a stage
    and derives its verdict from a quorum of their reports: Pass iff at least
    quorum seats reported Pass. Reviewers run one at a time in the run
    worktree with one shared prompt, so a panel never exceeds
    max_parallel_tasks, and their spawns count towards the worst-case execution
    budget a flow is admitted against. Each seat gets one-shot credentials bound
    to (run, stage, attempt, reviewer), so which report a sloop verdict call
    lands on comes from the credential and never from its arguments. A reviewer
    that exits without reporting counts as a Fail with no verdict reported.
    The aggregate is never stored — one append-only evidence row per reviewer is,
    and the verdict is recomputed from those rows by a pure function, so a
    restarted daemon reaches the same reading.
  • sloop verdict takes --confidence low|medium|high, defaulting to medium.
    It is recorded as evidence and never weighted into a panel's aggregation;
    floats are rejected. sloop show <run> lists each panel seat's verdict,
    confidence, and reason under its stage, silent seats included.
  • A new { builtin: sync } action merges the default branch into the run
    branch, inside the run worktree. It passes when that merge commits cleanly or
    there was nothing to integrate, and fails on a conflict — aborting the merge
    so a return_to target starts from a clean tree, with git's conflict output
    captured in the run log and so in the re-entered agent's prompt. Any number
    of sync stages, anywhere before the merge; the shared default-branch checkout
    is only ever read.
  • The merge stage takes { builtin: merge, ff_only: true }, which refuses the
    merge commit: the default branch either fast-forwards to the run branch head,
    or the stage fails having touched nothing. Absent, the merge policy is
    unchanged. ff_only is meaningful nowhere else and is a parse error on any
    other action.
  • sloop init writes a train flow beside default.yaml, opt-in with
    flow: train. It is the merge train: build → sync → verify → ff_only merge, with fail_action: { return_to: sync } on the merge, so a default
    branch that moves between the verification and the merge loops the train
    rather than landing a tree no stage tested. Its verify stage uses the
    repository's configured flow.test_cmd when it has one.
  • Flow stages can now bind the two fail_action forms that previously parsed
    but were rejected. fail_action: continue makes a stage advisory: its
    failure is recorded and visible in sloop show, the walk carries on, and the
    run's outcome is unchanged. fail_action: { return_to: <stage>, attempts: N }
    is a bounded backward edge: the walk re-enters an earlier stage and re-runs
    the whole span from there, so no verdict earned before the loop can reach the
    merge. Edges must point backwards, attempts is capped at 3, and a flow
    whose budgets could execute more than 32 stages is refused at parse time.
  • A re-entered agent stage is told why it is running again. Sloop appends a
    delimited previous attempt failed block to its prompt — after the ticket
    body and the worker instructions — naming the stage that failed, its resolved
    reason, and the last 100 lines of that execution's captured output. The block
    is rebuilt from the run's persisted evidence, so a daemon that restarts
    mid-loop composes the identical prompt. exec actions are handed nothing:
    their command line is fixed by the flow.
  • sloop show renders each stage execution separately, suffixing re-runs with
    their attempt (build, build#2), and a non-merged run's derived reason
    names the failure the walk actually stopped on rather than one a later
    attempt superseded.
  • sloop show tells an advisory failure apart from the one that ended the run:
    the stage table marks it advisory and the scan strip marks it warn rather
    than FAIL. A run's derived reason never names an advisory stage, since
    the walk provably continued past it, and a terminal run whose only failures
    were advisory says so. Stage rows carry a new advisory field.
  • A run whose walk stopped short says which halt it was. sloop show <run>
    appends ; return_to budget spent or ; the stage log does not replay against this flow to the derived reason where the failed stage alone does
    not explain the ending, and value.halt carries the same distinction as a
    stable token (fail_action, return_budget_exhausted, corrupt_log). It is
    re-derived by replaying the walk's own fold, so it cannot disagree with the
    driver.
  • sloop logs --stage accepts <stage>#<attempt>, selecting one execution of a
    stage a return_to edge re-entered. Parsing happens in the daemon, so every
    socket client gets it; a bare stage name still selects every execution. Log
    lines label re-runs the same way ([agent:build#2]), so the label sloop show prints is the selector to type next.

Changed

  • What Sloop called an activation is now a trigger. The scheduling model
    has three nouns — a ticket is what to do, a run is one attempt at it, and the
    third is the durable record that demand exists, the thing that makes the
    dispatcher pick a ticket up. "Activation" named the act of activating rather
    than the row waiting in a queue, so every mention of it had to be glossed
    first; "the trigger fired" and "the trigger is pinned to TICK-63" read without
    one. Scheduling, selection, claiming, and rearm behaviour are unchanged.

    Operator-visible consequences: sloop run prints trigger TR91 queued (now, ticket TICK-63); trigger ids widen from A<ordinal> to TR<ordinal>,
    because T91 reads as a ticket id beside TICK-91; and the dashboard field
    queued_activations becomes queued_triggers. On the wire, post and run
    take a trigger argument where they took activation, and post answers
    with trigger and trigger_suppressed. See "The activation → trigger
    rename" in docs/protocol.md for the full field-by-field table.

    Upgrading rewrites the database in place, ids and all. Queued triggers are
    pending work recorded in no committed file and no commit, so sloop reindex
    cannot rebuild one; the migration carries every row across instead.

  • sloop verdict --confidence on a reported stage is now recorded rather
    than discarded, so the flag means the same thing from a stage worker as from
    a panel reviewer. sloop show <run> prints it beside that stage's verdict
    source, and the stage row carries a new confidence field.

  • sloop template flow prints the current grammar as its canonical example:
    action, result_check, and fail_action written out on every stage, with
    one return_to edge, one advisory stage, and a commented panel block. The
    pre-action kind:/verdict: spelling and the deprecated on_fail are
    demoted to a short "legacy spellings" note, and sloop template ticket names
    the two flows that ship with the binary. sloop verdict --help and sloop logs --help now document who may call them and what their selectors accept.

  • Denials and parse errors use one vocabulary for one concept: a stage that
    cannot take a report says it does not use result_check: reported rather
    than naming a "verdict policy".

  • The ticket-state table in sloop --help --all states the trigger
    precondition first: ready said it was "eligible for dispatch once a run is
    queued", which reads as a caveat on a state that sounds like a promise. It
    now leads with the fact that a ready ticket runs only once a trigger is
    queued for it, and failed names both halves of the recovery — sloop retry
    returns it to ready, sloop run starts it again.

  • Captured output records, stage-process checkpoints, agent-exit checkpoints,
    and reported verdicts are all scoped to (stage, attempt) rather than stage
    alone, so a re-entered stage gets its own output, its own worker report, and
    its own exit checkpoint instead of inheriting an earlier attempt's.

Deprecated

  • The queued_activations field on status and on the show dashboard, in
    favour of queued_triggers. Both are emitted carrying the identical value,
    following the precedent set by the listshow aliases;
    queued_activations is removed in 0.5.0. The other envelope fields the
    rename touched are swaps rather than aliases — see docs/protocol.md.
  • on_fail repair blocks. fail_action: { return_to: <stage> } covers the
    same ground with the flow's own stages — a failing check stage returning to
    the stage it guards — and the daemon now logs a flow_on_fail_deprecated
    note when it admits a run whose flow uses on_fail. Nothing is removed:
    existing flows keep parsing, repairing, and settling exactly as before.

Fixed

  • A daemon spawned by a client command now calls setsid, so it leads its own
    session and process group instead of inheriting them from that client. The
    client exits as soon as its request is answered, so a daemon that stayed in
    its session died to any signal aimed at that short-lived process — a terminal
    hangup, or a supervisor reaping a finished command's process group — and the
    next client command silently started a replacement. The symptom was a daemon
    that vanished with no daemon_stopped record, and, if a run was in flight at
    the time, lost work.

  • sloop post accepts an absolute path to a ticket that reaches the repository
    through a symlink. The repository root is always canonicalized, but the path
    the operator typed was compared to it unresolved, so on macOS — where /tmp
    and /var/folders are both links into /private — posting a ticket by
    absolute path was rejected as "outside the repository". Both sides now
    resolve before the comparison. A ticket file that does not exist yet is
    unaffected: the longest existing ancestor resolves and the rest is appended,
    so a missing file inside the ticket directory still reports not found.

    This tightens one case that used to pass: a ticket path that is itself a
    symlink out of the repository is now rejected, because containment is
    decided on the file whose bytes get read rather than on the name used to
    reach it.

  • A Claude session limit now classifies as rate_limited and cools the target
    down instead of failing the ticket. The rule required stderr, but the
    claude target runs with --output-format stream-json and reports vendor
    rejections as synthetic assistant messages on stdout; its signature also only
    covered You've hit your limit, not the You've hit your session limit
    wording a real run hit. The rule now matches either stream on the wording
    every limit window shares. Without this, one vendor limit could burn several
    tickets' retry attempts and strand them in failed.

Install sloop 0.4.0-rc.1

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hamish-mackie/sloop/releases/download/v0.4.0-rc.1/sloop-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install sloop

Download sloop 0.4.0-rc.1

File Platform Checksum
sloop-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
sloop-x86_64-apple-darwin.tar.gz Intel macOS checksum
sloop-aarch64-unknown-linux-musl.tar.gz ARM64 MUSL Linux checksum
sloop-x86_64-unknown-linux-musl.tar.gz x64 MUSL Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo hamish-mackie/sloop

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>