# Changelog
All notable changes to Sloop will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.5.0] - 2026-07-26
### Added
- **A `sloop show` dashboard section for everything waiting on a human.** The
dashboard envelope gains an `attention` array — every ticket currently in
`needs_review` or `failed`, newest first, in the same row shape as `recent`
and never truncated by `-N`. The human rendering prints it between `runs:`
and `recent:`, with each row's reason, and omits the section entirely when
the array is empty. A `needs_review` ticket that had aged out of the recent
window was previously visible only as a digit in the count line.
This is an additive protocol-v1 field. No existing dashboard field changes
shape or value.
- **Tty-gated color in the human output.** `FAIL` and `failed` render red;
`warn`, `needs_review`, and a `(stalled: …)` annotation yellow; `ok` green
and `merged` dim. Everything else is unstyled. Color is emitted only when
stdout is a terminal and `NO_COLOR` is unset, and never in `--json`.
### Changed
- **The human `show` and `status` output reads without arithmetic.** `next
wake` is a countdown (`next wake in 4m12s`, or `next wake imminent` when the
deadline has passed) rather than the one raw UTC RFC3339 instant in
otherwise-local output; `--json` still carries `next_wake` as RFC3339. An
unfinished span says how long it has been running — `19:51-... (9m0s)` —
while still refusing to invent an end time. Finished spans are unchanged.
- **The ticket count line shows only what is happening.** A state prints only
when its count is non-zero, except `ready`, where an empty queue is itself
the signal, and `merged` moves to the end. Seven states with four zeros
became `tickets: 0 ready, 1 claimed, 1 needs_review, 77 merged`.
- **Ticket rows drop noise.** The `(project)` column appears only when the
displayed rows span more than one project, and the reason is suppressed only
for `merged` — a `needs_review` or `failed` row's reason is the reason to
read the row.
## [0.4.0] - 2026-07-26
### Changed
- **`sloop brief` is keyed on the stage the caller is executing, not on the
run.** The reply gains a `stage` block — `{"name", "attempt",
"result_check"}` — and `definition_of_done` now states what *that* stage
turns on instead of one run-level sentence handed to everybody.
The old text was authored in the socket handler and began "Commit your work to
the run branch" for every caller. Three kinds of worker hold a brief-capable
token, and only one of them builds anything: Sloop's own panel bootstrap tells
a reviewer to change nothing and to run `sloop brief`, which replied that its
job was to commit. The obligation now follows from the stage's `result_check`
and the caller's role, derived in one place:
| Caller | Definition of done |
| --- | --- |
| a panel reviewer | the stage passes only on its reported verdict; the work under review is not its to change |
| `result_check: reported` | the stage passes only on the worker's reported verdict |
| `result_check: { builtin: commits }` | commit the work to the run branch |
| `result_check: none` | the worker's own exit status is the verdict |
| any other check | an independent check judges the work after the worker exits |
A builder's brief is unchanged in substance: an `action: agent` stage defaults
to the commits check, so it is still told to commit to the run branch.
`attempt` is the other half of the assignment's identity. A
`fail_action: { return_to: … }` edge re-enters a stage, each execution is owed
its own report, and an agent that could not see the attempt could not tell a
retry from a first run.
Two judgement calls in the new text, both deliberate:
- **`flow.test_cmd` no longer earns a line.** It used to append "The
configured test command passes" whenever the key was set. But `test_cmd` is
spliced into every flow as its own `test` stage, judged by its own exit —
so the line told a worker to satisfy a check belonging to a *different*
stage, which is the same category of error as telling a reviewer to commit.
A flow's stages name their own tests, and a stage's brief describes that
stage.
- **A worker whose check is an independent actor is told that one exists, not
which.** Naming it would be useful, and it would also hand a worker the
flow's shape and the judge's argv — an invitation to run or game the check
rather than do the work. The stage block's `result_check` names the *kind*
of judge (`exec`, `agent`, `panel`, …), which is a fact about the caller's
own stage; the command behind it is not.
For clients: `definition_of_done` keeps its type and was always prose for an
agent to read. `stage` is additive. `ticket.acceptance` is removed — see
below.
### Removed
- **`ticket.acceptance` from the `sloop brief` reply.** It was hardcoded to
`[]` on every reply, so no client could ever have read a value out of it, and
nothing in Sloop wrote one. It is part of the documented brief shape, so its
removal is recorded here rather than left to be discovered.
- **The legacy `kind` / `cmd` / `verdict` stage grammar.** `0.3.x` named a
stage's work with `kind` and graded it with `verdict`; `0.4.0` names the same
two things with `action` and `result_check`, and the old keys no longer parse.
Every file under `.agents/sloop/flows/` is parsed eagerly when config loads,
so a flow left in the old spelling is a startup error naming the stage and the
key that replaces the one it found, not a surprise the first ticket bound to
that flow discovers. The rewrite is mechanical, the last row excepted:
| Removed | Replacement |
| --- | --- |
| `kind: agent`, `kind: build` | `action: agent` |
| `kind: exec` with `cmd: [...]` | `action: { exec: [...] }` |
| `kind: merge` | `action: { builtin: merge }` |
| `kind: sync` | `action: { builtin: sync }` |
| `verdict: exit` | `result_check: none` |
| `verdict: commits` | `result_check: { builtin: commits }` |
| `verdict: { check: [...] }` | `result_check: { exec: [...] }` |
| `verdict: reported` | `result_check: reported` |
| `on_fail: { agent: ..., attempts: N }` | `fail_action: { return_to: <stage>, attempts: N }` |
These are breaking changes with no compatibility window: there is no flag that
restores the old spelling, and a repository whose flows still use it does not
start. `sloop template flow` prints the current grammar in full, annotated,
and `docs/configuration.md` documents it.
- **`on_fail` repair blocks**, deprecated in `0.4.0-rc.1`. The last row of the
table above is the only one that is not a rename. `on_fail` attached a
*separate* repair agent to one stage and retried it within a single execution;
`fail_action: { return_to: <stage>, attempts: N }` sends the walk back to an
earlier stage and re-runs the whole span from there using the ticket's own
agent, with the failure in its prompt. Converting one is therefore a decision
about which stage should do the fixing — usually the agent stage that produced
the work the failing stage was guarding — rather than a rename. Note also that
the whole span re-runs, so no verdict earned between the target stage and the
failure survives the loop.
Two things go with the blocks. Stage rows lose their `attempts` field, the
count of retries inside one execution: it is gone from `sloop show --json` and
from the `N attempts` the stage table printed off it, while a `return_to`
re-entry is what it always was, a row of its own labelled `#2`. A client that
read the field should read `attempt` and count the rows; the envelope stays at
version `1`, and `docs/protocol.md` records the removal. And the daemon no
longer logs `flow_on_fail_deprecated`, having nothing left to warn about.
- The compatibility path that read a `0.3.x` flow snapshot off a `runs` row is
gone, now that the grammar which produced one no longer parses. The only run
it could still have served is one claimed by a `0.3.x` binary and recovered
by a `0.4.0` one. Such a run is now parked for review by id rather than
resumed: its branch and evidence are untouched, the recovery pass carries on
to every other run, and `sloop reindex` rebuilds local state from the
committed files and Git. What an operator sees is one `driver_resume_failed`
log record and evidence row naming the run, and its ticket settled to
`needs_review` rather than left claimed by a run nobody is driving. The work
on the branch is intact, so the remedy is to review and merge it by hand, or
to start the ticket again with `sloop run` once its flow file is in the
current grammar.
### Fixed
- A flow file that defines no stages is refused at parse time. Letting an agent
action sit in any position retired the "the first stage must be an agent
stage" rule, and with it the only thing that had ruled out an empty `stages:`
list; such a file loaded, and a ticket posted to it walked straight to
`complete` having run nothing. It is now a config-load error like any other
invalid flow.
- **`sloop logs` showed the oldest 64 entries and did not say so.** A bare read
returned one page anchored at the *start* of the capture, so on any run of
real length an operator saw the agent's first few seconds and nothing else —
and on a live run the same 64 lines came back every time, which is exactly
what a hung run looks like. A bare read now tails, like `tail` and
`journalctl`, and any page that hides output says so:
```
showing the last 64 of 312 entries; --tail N or --follow for more
```
`--tail N` and `--follow` are unchanged, and `--follow` still streams from
the run's first entry. The default belongs to the CLI alone: on the socket an
omitted `tail` still reads forward from the cursor. `logs` responses carry a
new `elided` count for what a tail dropped behind it, which the existing
`complete` flag cannot report — a tail reads to the end of the file however
much older output it discards, so it is always `complete`.
## [0.4.0-rc.1] - 2026-07-25
### 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 `list` → `show` 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`.
## [0.3.0] - 2026-07-21
### Added
- The read surface is now one verb. Bare `sloop` or `sloop show` prints a
dashboard: daemon status, scheduler state, and recent activity.
`sloop show <REF>` resolves ticket IDs, ticket names, run IDs and aliases,
unique run-id prefixes, and project IDs; an exact reference always wins.
Anything else becomes a case-insensitive ticket pattern over IDs and
names — a substring by default, an unanchored regular expression when the
text contains regex metacharacters — rendered as a ticket list.
`-n`/`--limit N` or the shorthand `-N` caps list rows. `--follow` streams
the shown scope's events; ticket and run followers exit when the subject
settles, and `--follow --quiet` returns only the outcome for scripting.
Exit codes are stable: `0` for success or a merged subject, `1` for
another terminal outcome or a daemon error, `2` for usage errors.
`sloop show --help` documents the whole resolution ladder. Pattern
resolution happens in the daemon: the `show` verb accepts patterns, the
`events` verb gained an optional `scope`, and the `list` verb gained an
optional `limit`, all additive within protocol version 1.
- `sloop template <kind>` prints fully commented canonical templates for
the config, flow, project, and ticket files, so a working example is
always one command away.
- `sloop show <TICKET>` now lists the ticket's runs, newest attempt first:
alias, outcome, wall-clock span, and a strip of the run's flow stages
marked `ok`, `FAIL`, `..` (running), or `-` (not reached). A ticket that
has never run prints `runs: none`.
- `sloop show <RUN>` now shows the run's timeline, a per-stage table (state,
attempts including `on_fail` retries, duration, exit code, and verdict
source), and a derived `reason` for any non-merged terminal run — for
example ``stage `test` failed (exit 1) after agent completed with
commits``. The reason comes from the stored stage and evidence rows, never
from an agent's own claim about its work. Stage names come from the run's
admitted flow snapshot, so a run reports the stages it actually had even
after the flow file changes.
- The `show` response gained `value.runs` on tickets and `value.stages`,
`value.attempt`, `value.agent_exit_code`, and the timeline fields on runs,
all additive within protocol version 1.
- `sloop logs` gained `--stage <NAME>`, `--tail <N>`, and `--follow`.
`--stage` selects one flow stage by the name the flow gives it, including
the agent stage, and rejects a name the run's flow does not define instead
of returning an empty page. `--tail` keeps the last N entries; `--follow`
streams new entries until the run settles. The three combine, so
`sloop logs <RUN> --stage test --tail 50` answers "why did the test stage
fail" in one command. Filtering happens in the daemon: the `logs` verb
gained `stage`, `tail`, and `after` arguments and a `terminal` response
field, all additive within protocol version 1.
### Changed
- The default flow's review stage is now a real gate. It previously
inherited `verdict: exit` and passed whenever the reviewer process exited
zero, so every fresh `sloop init` deployment silently approved all work.
The shipped stage now uses `verdict: reported`, and the review prompt
requires the reviewer to call `sloop verdict pass|fail --reason` exactly
once; the command, not the prose, decides the stage.
- Compact `sloop --help` now lists only the everyday operator commands
(`init`, `template`, `daemon`, `post`, `show`, `logs`). Everything else,
including the worker verb `brief`, remains available and documented under
`sloop --help --all`.
- `sloop show <RUN>` labels the agent stage's exit as `agent exit:` rather
than a bare `exit:`, which could read as "the whole run passed" on a run
whose later stage failed. The JSON `exit_code` field is unchanged.
- Ticket lists — the dashboard, pattern results, and the deprecated `list`
alias — order tickets by registration time, newest first, instead of
oldest first, so recently posted and currently running work leads the
output. State does not affect the order. Tickets registered in the same
millisecond fall back to their id's numeric ordinal, newest first.
- `sloop post` now reports every problem with a ticket file in a single
`invalid_arguments` error, one per line under the file path, instead of
stopping at the first one. A file whose frontmatter cannot be parsed at
all still fails fast with the parse error, and a file with exactly one
problem reads as it always has.
### Deprecated
- `status`, `list`, `watch`, and `wait` remain accepted as hidden
deprecated aliases of `sloop show`. They no longer appear in normal help,
and each invocation writes a note to stderr naming its replacement:
`status` and `list` point to `sloop show`, `watch` to `sloop show
--follow` (its optional scope and tail still work), and `wait` to
`sloop show --follow --quiet` (its run and timeout still work). The
aliases will be removed in a future release.
### Fixed
- `sloop reindex` now recognizes patch-equivalent merges. A run branch
whose commits were squashed or rebased onto the default branch is
detected with `git cherry` and indexed as merged; previously only true
ancestor merges counted, and reindex flipped squash-merged tickets to
`needs_review`.
## [0.2.1] - 2026-07-20
### Fixed
- Keep worker socket paths within the 104-byte macOS socket path limit by
placing them directly in the runtime directory under the short run id.
On macOS the previous layout exceeded the limit, every agent spawn failed
at socket bind, and no run could start.
## [0.2.0] - 2026-07-20
### Fixed
- Draw run-id entropy from `getrandom` on Linux, which musl libc exports,
instead of `getentropy`, which it does not, so musl builds compile.
- Wait out a stopping predecessor's lock for up to two seconds when the
daemon starts, so a stop followed by an immediate start cannot lose the
race against the old process releasing its lock.
### Added
- Initial command-line surface and local daemon implementation.
- Release automation for Homebrew formulae, crates.io publication, and signed
GitHub artifact provenance.
- Recurring and overnight activations, with `post --at` activations dispatched
at their scheduled time.
- Blocked ticket dependencies enforced during selection.
- Optional `on_fail` repair agents for exec and merge stages, including merge
conflict repair before retry.
- Activity events feed and a `sloop watch` command.
- Operator `show` resolves tickets with bodies and runs.
- Draining daemon restart.
- Settled worktree retention and default worktree branches derived from ticket
file stems.
- Per-target default model and effort, defaulting to `claude`.
- Vendor error classification from agent output.
### Changed
- Store daemon state, logs, sockets, and locks in platform-native per-user
locations instead of the repository's `.sloop` directory.
- Run start and exit moved behind coordination verbs, with typed run states
and lease renewal.
- Runs get random internal ids with ticket-derived aliases.
- Commit counts removed from run verdicts; outcomes derive from evidence only.
- Externally merged run branches reconcile out of `needs_review`, and stale
runs reconcile periodically.