Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agents/skills/fleet-onboarding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,17 @@ registry) is worse than one that never started.
| `git` | `command -v git` | install git |
| `gh` | `command -v gh` | install the GitHub CLI: <https://cli.github.com> |
| `gh` authenticated | `gh auth status` | `gh auth login` |
| `glab`, only if this fleet works on GitLab | `command -v glab` | install the GitLab CLI: <https://gitlab.com/gitlab-org/cli> |
| `glab` authenticated | `glab auth status` | `glab auth login` (`GITLAB_HOST` for a self-hosted instance) |
| `jq` | `command -v jq` | install jq (`brew install jq`, `apt install jq`, …) |
| `thurbox-cli` | `command -v thurbox-cli` | install thurbox: <https://github.com/Thurbeen/thurbox> |
| thurbox ≥ floor | compare against `min_thurbox_version` in `extension.toml.in` | `thurbox-cli` is too old; upgrade to the floor or newer |

`gh` is required even on a fleet whose work is entirely on GitLab: it is what
builds the repo map from `registry/owners.txt`, which is a list of GITHUB
owners. `glab` is what fleet asks about a GitLab merge request, and nothing
here needs it until a task's repository lives there.

**Read the version floor from the manifest, never from memory.** It is one
number with one owner, and `extension.toml.in` records why it sits there:

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/fleet-pane/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ row** — it replaces it rather than sitting above it. It says what the task was
told to produce (`publish.method` — `no-mistakes`, `pr` or `push`), which pull
request or commit that turned out to be, what fleet last saw when it looked at
it, and how long ago it looked. The whole row is the link: Ctrl+Click it and the
terminal opens the pull request. Every word on it comes off `task.yaml`'s
terminal opens the change request. Every word on it comes off `task.yaml`'s
`publish` block, written by `collect`, `shepherd` and `reap` — the commands that
do the looking — so the pane calls no `gh` and says nothing `queue.sh show`
would not print in the same word. Colour carries the verdict, and **`green` is
Expand Down
29 changes: 18 additions & 11 deletions .agents/skills/fleet-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ rule to remember:
| POSIX hosts only | `add` | a host with a non-`tmux` `multiplexer` is how `hosts.toml` spells a Windows host, and is refused by name. Every remote command fleet runs is POSIX shell |
| session sharing must be on | `add` | `share_sessions = false` switches off the delegation that lets `session capture` see that pane, so the trust dialog could not be answered and the worker would stall unread |

**Credentials are never moved.** The host needs its OWN GitHub credentials to
clone, fetch and push; yours are not inherited and nothing sends them. Probe 2
below asks whether the host has any and refuses the dispatch when it does not.
**Credentials are never moved.** The host needs its OWN credentials for the forge
that repository lives on — GitHub or GitLab — to clone, fetch and push; yours are
not inherited and nothing sends them. The `forge` probe below asks whether the
host has any and refuses the dispatch when it does not.
Forwarding your SSH agent also fixes it and forwards every key that agent holds
— your call to make on that machine, not something a dispatch makes for you.

Expand Down Expand Up @@ -316,10 +317,16 @@ the host and re-running `dispatch` sends it:

```text
reachable it answers ssh, and answers as a POSIX shell
forge it has GitHub credentials of its own — an ssh key, or a gh login
repo --repo is a git checkout at that path ON THAT MACHINE
forge it has credentials of its own for the forge THAT repo's `origin`
names — an ssh key, or a `gh` / `glab` login
```

The repo is asked about before its forge because which forge to prove a
credential against is a fact about that checkout's `origin`: a GitLab repository
needs a GitLab credential, and a probe that named github.com flatly would pass a
host that then fails at its first `git push`.

The report names the probe that failed. A remote worker that starts and then
fails at its first `git` call looks exactly like an agent bug and is not one.

Expand Down Expand Up @@ -474,13 +481,13 @@ Three answers, and the third is not the second:
| it is not, or not from this branch | **leaves the task OPEN** and says so, loudly |
| could not run | closes the task, and says the check could not run |

"Could not run" is the forge CLI absent, no network, a pull request it cannot read, or a
"Could not run" is the forge CLI absent, no network, a change request it cannot read, or a
base branch this machine cannot see. That must never read as a pass or a fail —
CI and an offline laptop both still have to collect. `queue.sh show <ref>`
prints the method and the verdict, so both survive the scrollback.

**The head-branch check is the one a worker cannot write for itself.** Whatever
the body says, "this pull request comes from this task's branch" is a fact of
the body says, "this change request comes from this task's branch" is a fact of
the forge — which closes the hole that reading prose never could: a worker
pasting somebody else's good pull request.

Expand All @@ -491,7 +498,7 @@ as it stands, `collect --allow-unverified` closes it and records that you did.
### 5b. `reap` — a session lives until its work lands, and not one turn longer

**The gate is the merge, not the conclusion.** For the two methods that end in a
pull request, `outcome: shipped` only means one is OPEN, and the session that
change request, `outcome: shipped` only means one is OPEN, and the session that
opened it is the cheap way to fix what review finds — reaping at collect time
makes that fix cost a re-spawn: a new worktree, a cold agent, the brief read
from nothing. A `push` task has no such gap — `collect` refuses to conclude it
Expand All @@ -504,16 +511,16 @@ So a task gets a state AFTER `done`:

| state | means | its session |
|---|---|---|
| `done` | the worker concluded; its pull request is open, or its already-confirmed `push` commit is about to be promoted by this same `collect` run | **kept** — the cheap way to fix what review finds |
| `landed` | the pull request merged, the pushed commit reached the base branch, or there was never an artifact | released |
| `abandoned` | the pull request was closed unmerged | released; the work is NOT on main |
| `done` | the worker concluded; its change request is open, or its already-confirmed `push` commit is about to be promoted by this same `collect` run | **kept** — the cheap way to fix what review finds |
| `landed` | the change request merged, the pushed commit reached the base branch, or there was never an artifact | released |
| `abandoned` | the change request was closed unmerged | released; the work is NOT on main |
| `stuck` / `failed` | the worker gave up | **kept** — that session is the evidence, and you decide |

`landed` comes from asking the forge, never from a worker claiming it, so it works
long after the session is gone. **Blockers clear on `landed`**, not on `done`
— a dependent task waits for the code to actually be on `main`, which is the
same bug in its other form: a task collected `shipped` once released its
dependents while its pull request sat unreviewed.
dependents while its change request sat unreviewed.

```text
topic/01-drop-idle-default landed https://…/pull/999 is merged
Expand Down
13 changes: 9 additions & 4 deletions .agents/skills/thurbox-session/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,23 @@ window, and the git worktrees. Only the TUI is local. Three consequences:
- **The `BRIEF.md` trick needs the file on the remote.** `Write` puts it on your
machine. Copy it over (`scp` / `ssh 'cat >'`) into the remote worktree, or the
worker reads nothing.
- **The remote needs its own GitHub credentials** to clone, fetch, and push.
Yours are not inherited. Forwarding your SSH agent fixes it, but forwards
every key the agent holds — decide that before reaching for it.
- **The remote needs its own credentials for that repository's forge** — GitHub
or GitLab — to clone, fetch, and push. Yours are not inherited. Forwarding
your SSH agent fixes it, but forwards every key the agent holds — decide that
before reaching for it.

Before spawning remotely, check all three, in this order:

```bash
ssh <host> true # reachable?
ssh <host> 'ssh -T git@github.com' # can it reach GitHub?
ssh <host> 'ls -d <repo-path>' # does the repo exist there?
ssh <host> 'ssh -T git@$(...origin's host...)' # can it reach THAT forge?
```

The third one asks the host named by that checkout's `origin`, not github.com:
a GitLab repository needs a GitLab credential, and GitLab's welcome banner reads
`Welcome to GitLab, @you!` where GitHub's says `successfully authenticated`.

Until all three pass, **spawn locally**. A remote worker will start and then
fail at its first `git` call, which looks like an agent bug and is not one.

Expand Down
53 changes: 32 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,20 @@ names every path and the reason for each.
- `scripts/lib/forge.py` — the FORGE seam. Everything fleet knows about a
change request — a pull request on GitHub, a merge request on GitLab — it
asks this module for; `scripts/lib/queue.py` runs no forge CLI itself and
builds no forge URL. GitHub, through `gh`, is the one implementation shipped,
and it is a CONFIGURATION and not an assumption. The file's own header owns
the interface and how to add another. Two things follow: a repository is
identified by HOST plus path (`github.com/Thurbeen/fleet`), because a bare
`owner/repo` names two different repositories once two forges exist; and
`queue-selftest.sh` drives `collect`, the landing check and `shepherd`
through a second, fake forge with no network and no `gh` behind it, which is
what keeps the seam honest rather than merely asserted.
builds no forge URL. TWO implementations ship — GitHub through `gh`, GitLab
through `glab` — and each is a CONFIGURATION and not an assumption: which
hosts one owns comes from that CLI's own variable (`GH_HOST`, `GITLAB_HOST`),
so a self-hosted instance is the ordinary case and not a special one. The
file's own header owns the interface and how to add a third. Two things
follow: a repository is identified by HOST plus path
(`github.com/Thurbeen/fleet`), because a bare `owner/repo` names two
different repositories once two forges exist; and `queue-selftest.sh` drives
`collect`, the landing check and `shepherd` through a second forge with no
network behind it — §13 through a fake one, §14 through the real GitLab
adapter over recorded `glab` output in `scripts/fixtures/glab/` (whose README
says which files are recorded and which are constructed). `gh` on those
sections' PATH is a tripwire, which is what keeps the seam honest rather than
merely asserted.
- `orchestration/reconcile/` — the reconciler's runtime state: its supervisor's
pid, the heartbeat proving its loop is ticking, its log, the advisory `nudge`
flag and the `down` flag. Written by `./scripts/reconcile.sh` and created on
Expand Down Expand Up @@ -120,9 +126,11 @@ The loop, driven by `./scripts/queue.sh`:
(`./scripts/session-trust.sh`), because sending one into that dialog is how
every fleet-spawned worker used to break. A task may name a `--host` from
thurbox's `hosts.toml` and run on that machine instead; `--repo` is then a
path THERE, three probes run before anything is spawned, and the brief and
the result travel by ssh so that completion stays one model. No host means
no change.
path THERE, three probes run before anything is spawned — reachable, the
repo is there, and it has its own credentials for the forge THAT repo's
`origin` names, which is why a GitLab checkout is not probed against
github.com — and the brief and the result travel by ssh so that completion
stays one model. No host means no change.
5. **Completion is two things you read, never something that interrupts you.**
`queue.sh watch` folds `thurbox-cli watch`'s event stream into each task's
record and closes nothing; `queue.sh collect` reads the `result.md` the
Expand All @@ -132,11 +140,11 @@ The loop, driven by `./scripts/queue.sh`:
is the half no record can hold: the goal in your words, the decisions, what
went wrong, the outcome. Write those into it while you still know them.
6. **Release is a third thing, and it is not manual.** `outcome: shipped`
means a pull request is OPEN, or, for a task whose declared publish method is
`push`, a commit already on the base branch — and that session is kept as the
cheap way to fix what review finds. A task moves to `landed` only when the
FORGE says its artifact merged (immediately, for `push`, since there is no
pull request to wait on), and `queue.sh reap` — which `collect` runs itself —
means a change request is OPEN, or, for a task whose declared publish method
is `push`, a commit already on the base branch — and that session is kept as
the cheap way to fix what review finds. A task moves to `landed` only when
the FORGE says its artifact merged (immediately, for `push`, since there is
nothing open to wait on), and `queue.sh reap` — which `collect` runs itself —
deletes the session and its worktree then. It never touches one thurbox says
is working or blocked, nor one a worker gave up in: that session is the
evidence. `reap --dry-run` says what it would do. Blockers clear on `landed`
Expand All @@ -146,17 +154,20 @@ The loop, driven by `./scripts/queue.sh`:
which still prints how many it is hiding. `stuck` and `failed` are not
terminal for that, `list --archived` and `show <ref>` still reach it, and
`add` un-archives.
7. **The pull request outlives the task, so `queue.sh shepherd` is a fourth
7. **The change request outlives the task, so `queue.sh shepherd` is a fourth
thing, run as reflexively as `collect`** — which names it whenever it closed
a task that left a PR open. It asks the FORGE for every open PR on the repos
the queue's tasks name, not the tasks' recorded artifacts. A PR is linked
back by artifact or head branch; an unlinked one is
a task that left one open. It asks the FORGE for every open change request
on the repos the queue's tasks name, not the tasks' recorded artifacts. One
is linked back by artifact or head branch; an unlinked one is
still classified and merged, it just has no session to fix it. It merges
only in the repos `AUTO_MERGE_REPOS` names in `scripts/lib/queue.py` — each
entry host-qualified, and one that names no forge refused rather than
matched — and only for a PR whose head branch is in that repo, opened by
matched — and only for one whose head branch is in that repo, opened by
someone who can
push there, carrying a `no-mistakes` attestation for its **current** head.
Squash is the only method fleet merges by, and a forge or a project that
forbids squash — a GitLab project can — is a refusal fleet RECORDS rather
than a merge by some other method.
That attestation gate is the one thing the declared publish
method moves: a task that was declared `no-mistakes` and carries none gets a
fixer, one that was never asked for one is recorded `green` and handed back
Expand Down
23 changes: 13 additions & 10 deletions FLEET.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# FLEET.md — standing context for the control-plane session

You are the **Mission Control** session: the long-lived control plane for its
owner's work across GitHub — whichever accounts and orgs are listed in
`registry/owners.txt`.
owner's work across GitHub and GitLab. `registry/owners.txt` is the map you
cover — GitHub accounts and orgs, because repository DISCOVERY still runs on
`gh`; which forge a task's change request lives on is a separate question,
answered per repository by `scripts/lib/forge.py`.

The SESSION is called Mission Control, and it wears a mark in front of that:
thurbox has no per-session icon field, so the glyph the TUI shows can only live
Expand Down Expand Up @@ -84,20 +86,21 @@ YAML by hand. Nothing to push — the map is gitignored.
and `shepherd` keep its facts current as you run them. Write the goal,
decisions and outcome into it in your own words — that half never comes
from a record. It is gitignored and not backed up by the repo.
6. **`shepherd`, as reflexively as `collect`.** The pull request outlives the
task, and `collect` names `shepherd` whenever it closed one that left a PR
open. It asks the forge for every open PR on the queue's repos, not just
recorded artifacts, dispatches a fixer for one that conflicts, fails a
check, was reviewed with changes requested, or was declared `no-mistakes`
6. **`shepherd`, as reflexively as `collect`.** The change request outlives the
task, and `collect` names `shepherd` whenever it closed one that left one
open. It asks the forge for every open change request on the queue's repos,
not just recorded artifacts, dispatches a fixer for one that conflicts, fails
a check, was reviewed with changes requested, or was declared `no-mistakes`
and carries no attestation for its current head, and squash-merges one that
clears every gate in the repos `AUTO_MERGE_REPOS` allows — entries there
name their forge (`github.com/Thurbeen/fleet`), because a bare `owner/repo`
name their forge (`github.com/Thurbeen/fleet`,
`gitlab.example.com/acme/group/widgets`), because a bare `owner/repo`
is two different repositories once two forges are configured. It writes down
what it saw either way, so a task's record says `checks-running` or
`unattested` and not just `shipped`.
7. Review the PRs; the operator merges every one `shepherd` did not. Sessions
7. Review them; the operator merges every one `shepherd` did not. Sessions
release themselves once their artifact lands on the base branch — a merged
pull request, or, for a task that published by pushing directly, the
change request, or, for a task that published by pushing directly, the
commit itself — `collect` reaps them, `queue.sh reap --dry-run` shows what
it would do — see `AGENTS.md`.
8. **`refuel` a worker that hit its agent's token limit and never reported —
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

# fleet

A **control plane** for your work across GitHub. You hand it a goal; it splits
the goal into tasks, runs an AI agent session on each one in a real repo, and
gives you back pull requests to review.
A **control plane** for your work across GitHub and GitLab. You hand it a goal;
it splits the goal into tasks, runs an AI agent session on each one in a real
repo, and gives you back change requests — pull requests, merge requests — to
review.

It is one repo holding two things: a **map** of your projects, and the
**orchestration** of the agent sessions run against them, using
Expand Down Expand Up @@ -63,7 +64,9 @@ before it writes a thing.
Run it twice and it converges. It hands you a guarded block to add yourself:
the pane's slot in your thurbox `layout.lua`.

Requires `gh` (authenticated), `jq`, and `thurbox-cli` **2.19.0 or newer**.
Requires `jq` and `thurbox-cli` **2.19.0 or newer**, plus the CLI for each forge
you work on: `gh` for GitHub, `glab` for GitLab, each authenticated. `gh` is not
optional even on a GitLab-only fleet — it is what builds the repo map.

That done, open the Mission Control session in thurbox and give it a goal.

Expand All @@ -75,7 +78,7 @@ it. Onboarding installs it and `F3` opens and closes it. It **displays and
does not control** — `./scripts/queue.sh` stays the only thing that writes.

It reads the same four files per task and no fifth: the plan, the progress, the
outcome, and the pull request. A column is narrow, so it draws only what you
outcome, and the change request. A column is narrow, so it draws only what you
would act on from a glance and leaves the rest to `./scripts/queue.sh show`.

![The queue pane in a thurbox column beside the session list: the account's fuel
Expand Down
2 changes: 1 addition & 1 deletion extension.toml.in
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
# nothing resolves those names, that half cost none of the argument above.
# Existing workers keep the name they were spawned with: there is no rename
# verb for them either, and none of the sequences above is worth running on a
# session that will be reaped when its pull request merges.
# session that will be reaped when its change request merges.

name = "fleet"
description = "Control-plane session: the repo map and thurbox orchestration"
Expand Down
Loading