@feigi/fleet-ctl 3.82.19 → 3.82.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/fleet-merge-bot.agent.md +6 -6
- package/package.json +1 -1
- package/scripts/tier-outcomes.mjs +46 -6
- package/skills/run-team/SKILL.md +178 -579
- package/skills/run-team/references/ci-and-staleness.md +47 -0
- package/skills/run-team/references/ci-watcher.md +138 -3
- package/skills/run-team/references/close-out.md +34 -1
- package/skills/run-team/references/failure-handling.md +43 -0
- package/skills/run-team/references/finisher.md +185 -0
- package/skills/run-team/references/instruments.md +20 -0
- package/skills/run-team/references/isolation.md +10 -0
- package/skills/run-team/references/ledger.md +4 -0
- package/skills/run-team/references/member-lifecycle.md +18 -0
- package/skills/run-team/references/phase-0.md +50 -0
- package/skills/run-team/references/pull.md +36 -1
- package/skills/run-team/references/reviewers.md +81 -0
- package/skills/run-team/references/shell-traps.md +10 -9
- package/skills/run-team/references/tick-rows.md +35 -0
|
@@ -134,3 +134,50 @@ than being detected, and nothing else can write the reason afterwards:
|
|
|
134
134
|
```bash
|
|
135
135
|
~/.fleet/bin/fleet-run fleet-heartbeat.mjs --stop "budget exhausted"
|
|
136
136
|
```
|
|
137
|
+
|
|
138
|
+
Skipping it is not fatal — the next reader still reports the stop, it just
|
|
139
|
+
reports that the beat stopped *without a recorded reason*, which is all it can
|
|
140
|
+
honestly say about a death that named nothing.
|
|
141
|
+
|
|
142
|
+
## Nothing to do: why the beat, and what its lines mean
|
|
143
|
+
|
|
144
|
+
- **Nothing to do right now** → **do not end your turn.** Arm the heartbeat
|
|
145
|
+
(SKILL.md's **Nothing to do: arm the beat**). A turn that ends on a drained
|
|
146
|
+
queue is the stall itself: nothing external will wake you, and the
|
|
147
|
+
level-check you are one command away from never runs. Listed on its own
|
|
148
|
+
because it is the only wake with no trigger — every other row of SKILL.md's
|
|
149
|
+
wake list arrives; this one is the absence of one.
|
|
150
|
+
|
|
151
|
+
- `… Ns of Ms remain → re-issue this command now, do not end your turn` — no
|
|
152
|
+
command blocks for a whole interval (omp backgrounds one at 60s, which is
|
|
153
|
+
why the CI gate holds its wait in one `eval` cell polling `gh run view`).
|
|
154
|
+
Issue it again. That is still one blocking call per turn, never an idle turn.
|
|
155
|
+
|
|
156
|
+
**The beat does not stop on idleness.** An empty shortlist and an empty supply are
|
|
157
|
+
not a reason to end the run; they are the state a newly triaged ticket arrives
|
|
158
|
+
into. The run ends when you drain it, when the budget goes, or when this
|
|
159
|
+
context does — not because there was briefly nothing to do.
|
|
160
|
+
Zeroes on the cockpit are not yet proof the pipeline is idle.
|
|
161
|
+
|
|
162
|
+
## Rule: record, tick, act, beat
|
|
163
|
+
|
|
164
|
+
**Rule: record, tick, act, beat.** Every wake — a member report, a review
|
|
165
|
+
notification, a label, a CI run reaching a terminal state, a `CONFLICT` line, a
|
|
166
|
+
heartbeat, the maintainer's drain — is one `fleet-event.mjs` call, which makes
|
|
167
|
+
the wake's writes and ends in one `fleet-tick.mjs` invocation, and you do what
|
|
168
|
+
it prints. There are no hand-run
|
|
169
|
+
refill or dispatch edges left. Record first, because the tick reads the run off
|
|
170
|
+
`.fleet/ledger.md` and nothing else of yours: a wake you have not written down is
|
|
171
|
+
one it cannot see, and it names the same dispatch again. Every record goes
|
|
172
|
+
through `ledger.mjs` (references/ledger.md), which `fleet-event.mjs` drives — never a
|
|
173
|
+
hand edit. What each kind writes is in the header comment of `fleet-event.mjs`.
|
|
174
|
+
|
|
175
|
+
## The merge bot does not wait for the finisher's report
|
|
176
|
+
|
|
177
|
+
The `ready-to-merge` Monitor catches hand-added labels as well as a finisher's
|
|
178
|
+
own. The bot does not wait for that finisher's report: its rebase is
|
|
179
|
+
server-side, `gh pr update-branch --rebase`, so nothing on the finisher's
|
|
180
|
+
worktree has to happen first. **You own the watcher, not the bot.** Arm one
|
|
181
|
+
yourself, `persistent: true`, seeded before the loop so handled PRs do not
|
|
182
|
+
re-fire. **One rebase per PR, at merge time.** Any behind-count you hand a bot
|
|
183
|
+
is expired on arrival; say so.
|
|
@@ -1,6 +1,141 @@
|
|
|
1
|
-
# The CI watcher:
|
|
1
|
+
# The CI watcher: its block, and why it is shaped the way it is
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SKILL.md's **Own the CI waits** arms the persistent Monitor from the block
|
|
4
|
+
below, verbatim. The rules beside the block follow it; the evidence for each
|
|
5
|
+
rule is under the heading naming it.
|
|
6
|
+
|
|
7
|
+
## The block
|
|
8
|
+
|
|
9
|
+
**A probe that cannot read is a transition event, not silence.** So check the REST budget *before* polling — `gh api rate_limit` is itself
|
|
10
|
+
unmetered (`gh api rate_limit --jq '.resources.core.remaining'`, re-confirmed
|
|
11
|
+
2026-09-08) — and give an unreadable `ci-state` payload the same treatment one
|
|
12
|
+
level down. The shape is **one outer tick loop with an inner pass over the open
|
|
13
|
+
PRs**, because the two causes live at different levels — the budget is one
|
|
14
|
+
account, the payload is one PR:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
budget_out= hidden_out= list_out= cap_out= blind= # seed the latches: set -u reads each below before tick 1 ever assigns one
|
|
18
|
+
pr_cap=200 # the open-PR read's --limit; gh stops there with exit 0 and no warning
|
|
19
|
+
while :; do # one tick
|
|
20
|
+
rl=$(gh api rate_limit --jq '.resources.core.remaining' 2>/dev/null || echo ERR)
|
|
21
|
+
case "$rl" in ''|*[!0-9]*) rl=ERR;; esac # a non-numeric read is an outage, not a budget
|
|
22
|
+
if [ "$rl" = ERR ] || [ "$rl" -lt 200 ]; then
|
|
23
|
+
if [ -z "$budget_out" ]; then # ONE global latch: a quota outage hits every PR
|
|
24
|
+
echo "WATCHER DEGRADED: core REST budget=$rl — CI polling paused, silence is NOT green"
|
|
25
|
+
budget_out=1
|
|
26
|
+
fi
|
|
27
|
+
else # every RECOVERED waits for the pass gates below
|
|
28
|
+
prs=$(gh pr list --state open --limit "$pr_cap" --json number --jq '.[].number' 2>/dev/null) || prs=ERR
|
|
29
|
+
if [ "$prs" = ERR ]; then
|
|
30
|
+
if [ -z "$list_out" ]; then # global latch: this cause is account-level too
|
|
31
|
+
echo "WATCHER DEGRADED: cannot list open PRs — CI polling paused, silence is NOT green"
|
|
32
|
+
list_out=1
|
|
33
|
+
fi
|
|
34
|
+
else # a list or cap RECOVERED waits for the pass gates too
|
|
35
|
+
count=$(printf '%s\n' "$prs" | grep -c .) || : # grep -c exits 1 on zero matches; set -e must not take it
|
|
36
|
+
if [ "$count" -ge "$pr_cap" ] && [ -z "$cap_out" ]; then # global latch: a full page may have been cut there
|
|
37
|
+
echo "WATCHER DEGRADED: open-PR list hit its --limit $pr_cap — PRs past it are NOT watched, silence on them is NOT green"
|
|
38
|
+
cap_out=1
|
|
39
|
+
fi
|
|
40
|
+
# gh pr list is GraphQL, so $rl still holds. The pass is budgeted at 4 core
|
|
41
|
+
# REST units per listed PR on top of the 200 floor.
|
|
42
|
+
# A shortfall here, or at the Actions-bucket gate below, pauses this tick's pass.
|
|
43
|
+
if [ "$rl" -lt $((200 + 4 * count)) ]; then
|
|
44
|
+
if [ -z "$budget_out" ]; then
|
|
45
|
+
echo "WATCHER DEGRADED: core REST budget=$rl < $((200 + 4 * count)) needed for $count PRs — CI polling paused, silence is NOT green"
|
|
46
|
+
budget_out=1
|
|
47
|
+
fi
|
|
48
|
+
else
|
|
49
|
+
# The workflow and run lookups bill a second core bucket that
|
|
50
|
+
# `rate_limit` does not report. Its count is only in the response
|
|
51
|
+
# headers of a lookup that bills it: one 1-unit read per tick, 4 units
|
|
52
|
+
# per listed PR on the same 200 floor. Recovery for every global latch
|
|
53
|
+
# waits until this gate clears too, so no line says polling resumed
|
|
54
|
+
# while this one still pauses it.
|
|
55
|
+
hrl=$(gh api -i 'repos/{owner}/{repo}/actions/workflows?per_page=1' 2>/dev/null | awk 'tolower($1)=="x-ratelimit-remaining:" {sub(/\r$/, "", $2); print $2; exit}')
|
|
56
|
+
case "$hrl" in ''|*[!0-9]*) hrl=ERR;; esac # a missing header is an outage, not a budget
|
|
57
|
+
if [ "$hrl" = ERR ] || [ "$hrl" -lt $((200 + 4 * count)) ]; then
|
|
58
|
+
if [ -z "$hidden_out" ]; then
|
|
59
|
+
need=; [ "$hrl" = ERR ] || need=" < $((200 + 4 * count)) needed for $count PRs" # an unreadable header has no amount to compare
|
|
60
|
+
echo "WATCHER DEGRADED: unreported Actions core REST bucket=$hrl$need — CI polling paused, silence is NOT green"
|
|
61
|
+
hidden_out=1
|
|
62
|
+
fi
|
|
63
|
+
else
|
|
64
|
+
if [ -n "$list_out" ]; then
|
|
65
|
+
echo "WATCHER RECOVERED: open-PR list readable again — CI polling resumed"
|
|
66
|
+
list_out=
|
|
67
|
+
fi
|
|
68
|
+
if [ -n "$cap_out" ] && [ "$count" -lt "$pr_cap" ]; then
|
|
69
|
+
echo "WATCHER RECOVERED: open-PR list under its --limit $pr_cap — every open PR watched again"
|
|
70
|
+
cap_out=
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "$budget_out" ]; then
|
|
73
|
+
echo "WATCHER RECOVERED: core budget=$rl — CI polling resumed"
|
|
74
|
+
budget_out=
|
|
75
|
+
fi
|
|
76
|
+
if [ -n "$hidden_out" ]; then
|
|
77
|
+
echo "WATCHER RECOVERED: unreported Actions core REST bucket=$hrl — CI polling resumed"
|
|
78
|
+
hidden_out=
|
|
79
|
+
fi
|
|
80
|
+
for pr in $(printf '%s\n' "$prs"); do # inline $(...): `for pr in $prs` is ONE iteration under zsh
|
|
81
|
+
st=$(~/.fleet/bin/fleet-run ci-state.mjs --pr "$pr" 2>/dev/null) || : # not-green exits non-zero; the payload is the verdict
|
|
82
|
+
if ! printf '%s' "$st" | jq -e '.verdict and .verdict != "rate-limited"' >/dev/null 2>&1; then
|
|
83
|
+
case " $blind " in *" $pr "*) ;; *) # latch keyed BY PR: this cause is per-PR
|
|
84
|
+
echo "WATCHER DEGRADED: no usable ci-state reading for #$pr — silence is NOT green"
|
|
85
|
+
blind="$blind $pr" ;;
|
|
86
|
+
esac
|
|
87
|
+
continue # inner continue — still reaches the tick sleep
|
|
88
|
+
fi
|
|
89
|
+
case " $blind " in *" $pr "*)
|
|
90
|
+
echo "WATCHER RECOVERED: ci-state reading #$pr again — CI polling resumed"
|
|
91
|
+
keep=; for b in $(printf '%s\n' "$blind"); do [ "$b" = "$pr" ] || keep="$keep $b"; done; blind=$keep ;;
|
|
92
|
+
esac
|
|
93
|
+
: # your normal handling of $st for this PR; a run at a terminal state is the CI wake: fleet-event.mjs ci "$pr"
|
|
94
|
+
done
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
sleep 120 # the block's only pacing, once per tick
|
|
100
|
+
done
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**It has to latch** — one line entering the degraded state, one leaving it,
|
|
104
|
+
never one per tick. **Latch each cause at the level its cause lives
|
|
105
|
+
at**: the budget, the unreported Actions bucket, the open-PR list and that
|
|
106
|
+
list's cap are one global flag each — all account-level — but the payload latch is keyed by PR, and a
|
|
107
|
+
shared scalar for those is an unlatched line wearing a latch's clothes — one
|
|
108
|
+
persistently blind PR alongside one healthy PR re-clears the flag on every tick,
|
|
109
|
+
emitting a DEGRADED and a false RECOVERED pair forever. The recovery line is
|
|
110
|
+
what says the watch is alive again, so emit it on the first good pass even when
|
|
111
|
+
nothing about the CI state changed; without it a reader cannot tell a recovered
|
|
112
|
+
watcher from a dead one.
|
|
113
|
+
|
|
114
|
+
**Guard every probe, not just the ones with a verdict field.** Capture its status (`prs=$(…) || prs=ERR`), latch it globally, and fall
|
|
115
|
+
through to the same tail sleep. **Then iterate with the inline `$(printf '%s\n'
|
|
116
|
+
"$prs")` form, never a bare `for pr in $prs`** — the assigned-variable trap in
|
|
117
|
+
**Shell traps**, in references/shell-traps.md, which carries the mechanism and its
|
|
118
|
+
measurements. It
|
|
119
|
+
applies to **every** loop in this block, the `blind`-latch removal included:
|
|
120
|
+
under a bare `for b in $blind` a recovered PR never leaves the list and the block
|
|
121
|
+
emits a fresh RECOVERED every tick thereafter — the per-tick volume the latch
|
|
122
|
+
exists to prevent, wearing a latch's clothes again.
|
|
123
|
+
|
|
124
|
+
**A readable list can still be a short one.** Pass an explicit `--limit`
|
|
125
|
+
(`pr_cap`: one variable for the flag and the check, so the two cannot drift)
|
|
126
|
+
and treat a full page as its own degraded cause — the "no silent caps" rule
|
|
127
|
+
`candidates.mjs`'s `refuseIfCapped()` and `fleet-tick.mjs`'s `PR_LIMIT` already
|
|
128
|
+
enforce. It latches globally like the list failure but does not pause polling:
|
|
129
|
+
the PRs it did return are real, so the pass still watches them, and the
|
|
130
|
+
DEGRADED line names the rest as unwatched until a pass comes back under the cap.
|
|
131
|
+
|
|
132
|
+
**Judge `ci-state` on its payload, never its exit code** — the same rule as
|
|
133
|
+
`merge-gate.mjs` applies on the merge path, applied to the watcher. Empty
|
|
134
|
+
or unparseable output is the degraded case; parseable JSON reading
|
|
135
|
+
`verdict: "not-green"` is normal. An exhausted quota may also name itself,
|
|
136
|
+
`verdict: "rate-limited"` on stdout — likewise degraded, and likewise not a
|
|
137
|
+
reading, which is why the gate above tests the verdict rather than mere
|
|
138
|
+
parseability: that payload is perfectly good JSON.
|
|
4
139
|
|
|
5
140
|
## The pass is budgeted at 4 core REST units per listed PR
|
|
6
141
|
|
|
@@ -28,7 +163,7 @@ as a CI reading. Require the field, then compare it.
|
|
|
28
163
|
inside the per-PR pass multiplies by the number of open PRs (8 open PRs → 960s
|
|
29
164
|
of pause per tick, against the 24s outage reset measured below), and a degraded
|
|
30
165
|
branch that `continue`s the *outer* loop skips the tail sleep and busy-spins
|
|
31
|
-
probe pairs during exactly the outage it is reporting. Every degraded branch of
|
|
166
|
+
probe pairs during exactly the outage it is reporting. Every degraded branch of this file's watcher block falls through to the same single sleep instead.
|
|
32
167
|
|
|
33
168
|
## The budget is gated at the floor and again at the pass
|
|
34
169
|
|
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
`closeout.mjs finish` runs the scrape, the tier-outcomes check, the metrics refresh, the commit of the metrics files, and the phase-0 receipt move, and SKILL.md's **End of run** says how to drive it. This file is how each of those works, for the controller to read when a step refuses, and how `closeout.mjs reopen` undoes a drain. The verdict rules — what `closed_own_ticket` and `minted_false_claim` mean, and that a value not in hand is left blank — stay in SKILL.md under **Guard: accumulate per PR**, because the controller still rules them.
|
|
4
4
|
|
|
5
|
+
- **The run ends, or the maintainer says drain** → `fleet-event.mjs drain --reason "<reason>"`
|
|
6
|
+
first (below). The marker is on disk, so the tick
|
|
7
|
+
holds the implementer row at `HOLD (draining)` from then on — for a
|
|
8
|
+
replacement controller too — and keeps the review, fix-applier and merge rows
|
|
9
|
+
firing until the open PRs merge. The drain starts the end of the run; it does
|
|
10
|
+
not end it. **End of run**, in this order:
|
|
11
|
+
1. `~/.fleet/bin/fleet-run fleet-event.mjs drain --reason "<reason>"` — runs
|
|
12
|
+
`closeout.mjs begin --reason "<reason>"` (references/reaping.md), then the tick. A claim it refuses prints `NEED hand-release #<N> <blocker>`: that
|
|
13
|
+
refusal is the finding, so run `worktree-audit.sh` and decide by hand.
|
|
14
|
+
`begin` selects a claim with no PR and a live or `bailed` implementer token
|
|
15
|
+
on every run, so a claim you keep leaves `begin` at exit 4 on every run:
|
|
16
|
+
release the rest by hand and run `begin` again.
|
|
17
|
+
2. Act on the review, fix-applier and merge rows until the tick prints
|
|
18
|
+
`ACTION: run over → fleet-heartbeat.mjs --stop "<reason>"`, which it does
|
|
19
|
+
once the run is draining with no open PR and no live implementer.
|
|
20
|
+
3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"` — scrapes
|
|
21
|
+
the run's member facts, checks the tier-outcomes rows, refreshes the
|
|
22
|
+
metrics, commits the three metrics files and opens their PR, sets the
|
|
23
|
+
phase-0 receipt aside as `.fleet/phase0.previous.json`, then records
|
|
24
|
+
`fleet-heartbeat.mjs --stop` last, because every reader takes a recorded
|
|
25
|
+
stop as the end of the run. A `NEED tier-outcomes append <pr>` is a PR
|
|
26
|
+
the ledger marks reviewed with no verdict row: append it with the verdict
|
|
27
|
+
you rule (**Guard: accumulate per PR**) and run `finish` again. A reviewed
|
|
28
|
+
PR that closes no issue, a `Refs`-only step for instance, takes no row: the
|
|
29
|
+
check names it in a `note` line and never asks for one. A step it fails is
|
|
30
|
+
explained in references/close-out.md. Exit 0 is the run closed.
|
|
31
|
+
Drain only when a documented trigger fired, and name the trigger in `--reason`.
|
|
32
|
+
**Undrain** only when the maintainer asks, never a drain that budget or
|
|
33
|
+
context exhaustion caused (references/close-out.md).
|
|
34
|
+
|
|
5
35
|
## Scrape the member facts and check the tier rows
|
|
6
36
|
|
|
7
37
|
**Then record the run's member facts — do not author them.** Scrape EVERY session
|
|
@@ -55,7 +85,10 @@ and none could be compared, so nothing from that run was checked: append the
|
|
|
55
85
|
run's member rows if they are missing, or blank each named row's `tier` and say
|
|
56
86
|
why in `note`.
|
|
57
87
|
Each WARNING names a PR this run's ledger marks `reviewed=` that has no row yet
|
|
58
|
-
— append it before committing if it is an implementer PR.
|
|
88
|
+
— append it before committing if it is an implementer PR. A reviewed PR that
|
|
89
|
+
closes no issue is named in a `note` line instead: it takes no row. A line
|
|
90
|
+
saying the PR's closing references could not be read comes before that PR's
|
|
91
|
+
WARNING and says why.
|
|
59
92
|
|
|
60
93
|
## Drain the pending ticket-features rows and refresh the cost guard
|
|
61
94
|
|
|
@@ -33,3 +33,46 @@ while one poll spent on a member that had in fact stopped costs only the poll.
|
|
|
33
33
|
The refuter's own observation rule in the fix-applier body stops a
|
|
34
34
|
member from creating that state; this one stops you from misreading whoever
|
|
35
35
|
reaches it anyway, and neither replaces the other.
|
|
36
|
+
|
|
37
|
+
## Audit every worktree before dispatching replacements
|
|
38
|
+
|
|
39
|
+
Audit every worktree before dispatching replacements with
|
|
40
|
+
`~/.fleet/bin/fleet-run worktree-audit.sh` — it reports each worktree's
|
|
41
|
+
ahead-count and uncommitted files, the committed-vs-uncommitted distinction that
|
|
42
|
+
decides whether a replacement redoes or destroys work.
|
|
43
|
+
|
|
44
|
+
## A failed review, and verifying a reported SHA
|
|
45
|
+
|
|
46
|
+
**A throw, an empty return, or a notification whose status is not `completed`
|
|
47
|
+
is a failure event, not a clean review.** **Retry once
|
|
48
|
+
under the next runner letter, then stop:** the `review-pr-<pr#>` runner
|
|
49
|
+
already retried once inside its cell and reports `failed` with both errors, so
|
|
50
|
+
the retry is a second runner under the next letter the PR's rows do not carry
|
|
51
|
+
yet — `review-pr-<pr#>-b` after a first runner — recorded as
|
|
52
|
+
`review=member:<that name>`. A second failure is reported, and the PR stays at
|
|
53
|
+
review-due. A
|
|
54
|
+
runner member that is killed rather than failed follows **Failure
|
|
55
|
+
handling** (fresh name, inherited state stated).
|
|
56
|
+
|
|
57
|
+
**Verify every reported SHA.** `~/.fleet/bin/fleet-run verify-sha.sh
|
|
58
|
+
<branch> <sha>` before enqueueing — a member can commit in a nested worktree,
|
|
59
|
+
leaving the SHA on a stray branch while its report reads normally. Exit codes as
|
|
60
|
+
`inflight.sh`: `# 0 reachable, 1 not reachable, 2 unanswerable`. Not reachable
|
|
61
|
+
(exit 1) → flag, do not enqueue, do not return the ticket to the shortlist until the
|
|
62
|
+
maintainer rules. **Exit 2 is not a verdict** — the question could not be
|
|
63
|
+
answered, and every cause of it reaches you as one shape: no JSON on stdout at
|
|
64
|
+
all, the cause on stderr alone. Read that stderr, fix what it names and re-run;
|
|
65
|
+
escalate if it repeats. Never read exit 2 as `not reachable`, and never record it
|
|
66
|
+
as a stray commit — the flag and the maintainer's ruling above are what exit 1
|
|
67
|
+
earns, and spending them on a probe that never answered costs a member its
|
|
68
|
+
ticket over a failure a re-run would have cleared.
|
|
69
|
+
## Never force a rebase to start
|
|
70
|
+
|
|
71
|
+
**Never force a rebase to start.** No `git clean`, `git checkout .`,
|
|
72
|
+
`git reset --hard`, `git stash`. Uncommitted changes may exist nowhere else.
|
|
73
|
+
Non-empty `git status --porcelain -unormal` → stop and report — the explicit
|
|
74
|
+
mode, never bare `--porcelain`, or `status.showUntrackedFiles = no` reads an
|
|
75
|
+
empty answer here and licenses the rebase over work this check exists to
|
|
76
|
+
protect. Stashing empties it either way, so that check and
|
|
77
|
+
`no-undo-audit.sh` both go quiet on work nothing else holds.
|
|
78
|
+
|
|
@@ -2,6 +2,139 @@
|
|
|
2
2
|
|
|
3
3
|
Evidence for the finisher rules the finisher's agent body, `agents/fleet-finisher.agent.md`, and SKILL.md's **The finisher gate** state, keyed by the rule each justifies.
|
|
4
4
|
|
|
5
|
+
## The fix-applier pushes and exits; the finisher is dispatched fresh
|
|
6
|
+
|
|
7
|
+
**The fix-applier pushes and exits — it does not hold the CI wait.** You own the
|
|
8
|
+
persistent Monitor; a turn-based member re-reading `gh pr checks` each idle cycle
|
|
9
|
+
rebuilds a 100k-token context for nothing the Monitor lacks. The body
|
|
10
|
+
carries the report-last rule and the brief says to push, write the report and exit; you do not have to restate either. When the
|
|
11
|
+
diff-validating `check` job is green **and no
|
|
12
|
+
heavy job is in `failure`** (the heavy diff-validating suites — not the
|
|
13
|
+
`rebase-check` currency gate; a `skipped` heavy job is behind-count staleness and
|
|
14
|
+
fine — **solely** off that count, which is a condition to establish rather than
|
|
15
|
+
infer, see the five-condition note in the fix-applier body) — or
|
|
16
|
+
`ci-state.mjs` reads `verdict: "no-ci"`, its bullet in **The finisher gate's
|
|
17
|
+
tests, in full** — **and once the PR's review
|
|
18
|
+
has returned and its fix-applier, if one was dispatched, has sent its report,
|
|
19
|
+
which the CI-completes edge in that section states in full** — dispatch a
|
|
20
|
+
**finisher** — a fresh small agent, not the fix-applier resumed. Render its brief
|
|
21
|
+
with `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<pr#> …`
|
|
22
|
+
(`--kind repair` once an earlier finisher reported `labelled` without the label landing), record it with
|
|
23
|
+
`ledger.mjs dispatch <pr#> finisher-pr-<pr#>` before the call, then **dispatch
|
|
24
|
+
it with the `agent` that command printed and omit `model` on the call** — the
|
|
25
|
+
tier lives in that definition's frontmatter, `agents/fleet-finisher.agent.md`,
|
|
26
|
+
as the `@smol:low` route, which resolves through the operator's `modelRoles`.
|
|
27
|
+
Its four duties are a checklist — audit the
|
|
28
|
+
worktree, confirm every deferral has a tracker home and re-run the acceptance
|
|
29
|
+
mutation, apply one release label, report — and the
|
|
30
|
+
merge gate downstream still catches whatever it gets wrong. **Never dispatch
|
|
31
|
+
one while you still owe the fix-applier a ruling**: the pinned SHA is only as good
|
|
32
|
+
as the guarantee nothing else is inbound. **The test is your OUTBOX, not the
|
|
33
|
+
member's last message** — anything you have decided that it has not received,
|
|
34
|
+
including a ruling you have since **withdrawn or reversed**. **That is the test
|
|
35
|
+
of THIS condition only**: an empty outbox says nothing is inbound from you, and
|
|
36
|
+
nothing about whether the member considers itself finished, which is what the
|
|
37
|
+
report condition above is for. A withdrawal you
|
|
38
|
+
recorded only in the ledger is undelivered: the member still holds the original
|
|
39
|
+
and will act on it. Relay everything — reversals too — then dispatch.
|
|
40
|
+
|
|
41
|
+
## The finisher gate's tests, in full
|
|
42
|
+
|
|
43
|
+
- **Monitor: CI run completes** → `fleet-event.mjs ci <pr>`, which binds the run
|
|
44
|
+
(`ci-state.mjs --pr <N>`) and **records it as
|
|
45
|
+
`ci=<run-id>:<attempt>:<conclusion>`, not just "CI green"** — in the
|
|
46
|
+
ticket's ledger row, which outlives your context. The key is what the sibling
|
|
47
|
+
bullet below turns on: it tells an edge already spent from one that never
|
|
48
|
+
fired. The whole key, never the conclusion alone: **a
|
|
49
|
+
conclusion is not stable** (references/ci-and-staleness.md), because a rerun rewrites the run in
|
|
50
|
+
place. The diff-validating `check` job green with no heavy job (the
|
|
51
|
+
diff-validating suites, not the `rebase-check` currency gate) in `failure`
|
|
52
|
+
→ a finisher CANDIDATE (gated by *The fix-applier pushes and exits; the
|
|
53
|
+
finisher is dispatched fresh*), a `check`
|
|
54
|
+
**failure** that is not solely staleness → the **red-check fixer** (SKILL.md's
|
|
55
|
+
**Then dispatch a red-check fixer**), behind the same review-returned and
|
|
56
|
+
fix-applier-reported gate as a finisher. A `check`-green board whose heavy jobs are merely
|
|
57
|
+
`skipped` (behind-count staleness, the normal case after a merge) still labels — do NOT
|
|
58
|
+
gate on `ci-state --quiet` exit 0, which a behind PR never reaches. **This
|
|
59
|
+
edge fires for every CI run the PR produces, and the implementer's own
|
|
60
|
+
firing comes first for every PR** — `ci.yml` triggers on `pull_request`, so
|
|
61
|
+
opening the PR at the end of `next-ticket` step 7 starts a run within seconds,
|
|
62
|
+
while the review returns 20-40 minutes later. That first firing is
|
|
63
|
+
the ordinary case for every PR, not a rarity, and no review has run at all
|
|
64
|
+
when it arrives. **Green is therefore not the gate: dispatch a finisher only
|
|
65
|
+
once the PR's review has returned AND its fix-applier, if one was dispatched,
|
|
66
|
+
has sent its report — pushed is not reported**, and a push before that report
|
|
67
|
+
is mid-work on a SHA the member may still move, which its own prompt tells it
|
|
68
|
+
not to hand over. A push is therefore not an exit, and a report that has not
|
|
69
|
+
arrived is asked for by name rather than read as one. Neither shape is caught
|
|
70
|
+
by an empty outbox: on the implementer's firing there is no fix-applier to owe
|
|
71
|
+
anything to, so the outbox is *vacuously* empty, and on a fix-applier's
|
|
72
|
+
mid-work push nothing has been sent since the dispatch, so it is *genuinely*
|
|
73
|
+
empty. **The outbox is the last test, not the only one, and it stands wherever
|
|
74
|
+
a fix-applier exists** — that is exactly where a ruling you still owe it is
|
|
75
|
+
outstanding, so never dispatch off it while you do. Empty your outbox to it
|
|
76
|
+
first — including any ruling you have withdrawn or reversed — then dispatch
|
|
77
|
+
(*The fix-applier pushes and exits; the finisher is dispatched fresh*); a final report is not proof it stopped. A PR that needs nothing fixed
|
|
78
|
+
reaches a finisher through the **fix-applier reports `no-op`** bullet below,
|
|
79
|
+
never by waiting for a second CI event this head will never produce. The
|
|
80
|
+
same goes for a red one: a red that fired before the review never fires
|
|
81
|
+
again, so the **fix-applier reports `no-op`** and **review result lands**
|
|
82
|
+
bullets are also the only routes to the red-check fixer there.
|
|
83
|
+
Then run the tick. These edges dispatch it, each the finisher's own: **Monitor: CI run completes** binding a red on a head whose review has returned and whose fix-applier, if any, has reported; **A fix-applier reports `no-op`**, or a SHA you have already bound, when that head's bound conclusion is red; and **A review result lands** with no finding and nothing to file, when that head's bound conclusion is red.
|
|
84
|
+
- **A fix-applier reports `no-op`, or a SHA you have already bound** → dispatch
|
|
85
|
+
the finisher **now**, against the existing head — when that head's bound
|
|
86
|
+
conclusion is red, the red-check fixer instead (SKILL.md's **Then dispatch a
|
|
87
|
+
red-check fixer**), never a finisher and never a wait. No push means no new run, and
|
|
88
|
+
the Monitor above is edge-keyed on `<run-id>:<attempt>:<conclusion>` — that
|
|
89
|
+
head's terminal state already fired once and will never fire again, so waiting
|
|
90
|
+
for the CI event waits forever. This stays a common outcome on a clean PR:
|
|
91
|
+
`suggestion` is the band a clean diff produces, and every out-of-scope or
|
|
92
|
+
refuted one is filed as an issue rather than committed. An edge-only label path
|
|
93
|
+
therefore strands exactly the PRs with nothing wrong with them. **Reconcile, do
|
|
94
|
+
not wait for an event** governs here too, not only the implementer row. **This
|
|
95
|
+
bullet and the one above are a pair, and on a clean PR this one carries the
|
|
96
|
+
whole route to a finisher**: a `no-op` report satisfies the gate above by
|
|
97
|
+
itself — the review has returned, because a fix-applier exists only after it
|
|
98
|
+
did, and the member has reported — so nothing further is owed and no further
|
|
99
|
+
event is coming. Edit either bullet without the other and what strands is exactly the PR that needed no fixes. Edit one of these bullets without the others and what strands is exactly the red-only PR.
|
|
100
|
+
- **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → no workflow run will ever
|
|
101
|
+
complete for this repo, so the CI-run-completion edge above never fires and
|
|
102
|
+
waiting for it stalls the whole PR — the same silent-stall shape reported
|
|
103
|
+
before this verdict existed. Dispatch the finisher off the reviewer's final
|
|
104
|
+
verdict instead, the moment it lands — and, same as above, never while you still
|
|
105
|
+
owe it a ruling — or hold anything else it has not received, a ruling you have
|
|
106
|
+
withdrawn or reversed included. The finisher's gate is then the
|
|
107
|
+
`--declare-no-ci` declaration, not a `check` job: with it, label off the
|
|
108
|
+
reviewer's verified suite run; **without it, do not label** — report that this
|
|
109
|
+
repo has no CI configured and no declaration, and stop. Absence never reads as
|
|
110
|
+
pass. See `review-and-fix.md` step 6.
|
|
111
|
+
|
|
112
|
+
Gate on the `check` job, **not** on `ci-state --quiet` exit 0: a behind PR never
|
|
113
|
+
reaches full green, so an exit-0 gate strands it unlabelled. The finisher reads
|
|
114
|
+
per-job state (`ci-state.mjs` without `--quiet`, or its `jobs`), since `--quiet`
|
|
115
|
+
drops `jobs`, `missing` and `dropped`. Normal path, not only kill-recovery.
|
|
116
|
+
|
|
117
|
+
**`ci-state.mjs` reads `verdict: "no-ci"`** — no `check` job exists in this repo
|
|
118
|
+
to gate on, and that is not a third way to skip the wait. **The
|
|
119
|
+
`--declare-no-ci` declaration is caller-side, and reading it back out of the
|
|
120
|
+
payload verifies nothing**: the flag is echoed into `reasons` and flips the exit
|
|
121
|
+
code, so re-running with it and finding it there confirms only that you passed
|
|
122
|
+
it. Passing it yourself is not a second opinion. Check the two facts it stands
|
|
123
|
+
for instead:
|
|
124
|
+
|
|
125
|
+
1. `ci-state.mjs` **without** the flag still reads `verdict: "no-ci"`. A repo
|
|
126
|
+
whose CI is merely misconfigured — workflow files present under other names,
|
|
127
|
+
or `.github/workflows/` not a directory — exits 2 there rather than reading no-ci,
|
|
128
|
+
so this is the check that separates real absence from misconfiguration, the
|
|
129
|
+
case that must never ship silently.
|
|
130
|
+
2. The reviewer's own final verdict reports a green `testCmd` run on the SHA it
|
|
131
|
+
pushed. `review-and-fix.md` step 3 requires it to, and that run is the only
|
|
132
|
+
gate this repo actually has.
|
|
133
|
+
|
|
134
|
+
Both → add the label, and say it rests on the reviewer's suite run, not on CI.
|
|
135
|
+
Either missing → halt here, same as a dirty worktree: report `no CI configured,
|
|
136
|
+
no verified suite run on record` and add no label.
|
|
137
|
+
|
|
5
138
|
## An unanswered question from the member is an outbox item
|
|
6
139
|
|
|
7
140
|
**An unanswered question from the member is an outbox item, and it blocks
|
|
@@ -119,3 +252,55 @@ its token re-dispatches a finisher into your take-off window. The label goes
|
|
|
119
252
|
before the approval because removing the artifact the merge bot gates on is
|
|
120
253
|
the reliable stop, where messaging the bot races it, and a label has been
|
|
121
254
|
observed holding until after an abort message arrived.
|
|
255
|
+
|
|
256
|
+
## A final report is not proof the member stopped
|
|
257
|
+
|
|
258
|
+
**A final report is not proof the member stopped.** Observed twice: a ruling
|
|
259
|
+
relayed after dispatch produced a new commit mid-audit, and a fix-applier that
|
|
260
|
+
reported "nothing outstanding" resumed on a ledger-only withdrawal and was
|
|
261
|
+
cycling mutants through the worktree when the finisher audited it. Both finishers
|
|
262
|
+
correctly halted. Note what that costs to detect: two reads of the same worktree
|
|
263
|
+
a minute apart showed *different* mutants, so a member's report and any single
|
|
264
|
+
`git status` are each valid only at their instant.
|
|
265
|
+
|
|
266
|
+
## The standing CI facts are in the fix-applier body
|
|
267
|
+
|
|
268
|
+
**The standing CI facts are in the fix-applier body, not in per-event messages** —
|
|
269
|
+
otherwise you send "your red is staleness, do not rebase" once per member per
|
|
270
|
+
merge. `review-and-fix.md` states them; the body says they apply.
|
|
271
|
+
|
|
272
|
+
## A halt's cause comes from one script
|
|
273
|
+
|
|
274
|
+
A halt at duty 1 reads identical from a bare SHA mismatch whatever caused it,
|
|
275
|
+
and the causes `finisher-audit.mjs` names are the ones seen so far, not a
|
|
276
|
+
closed list; the finisher's agent body gives it the cause from one script
|
|
277
|
+
instead of asking anyone.
|
|
278
|
+
|
|
279
|
+
## Why a `past-pin` halt needs the tick
|
|
280
|
+
|
|
281
|
+
`past-pin` needs the tick because the dead-review path cannot reach it: once a
|
|
282
|
+
review has **returned**, its `reviewed=` token holds the PR out of `review-due=`,
|
|
283
|
+
and settling that review `=failed` re-queues nothing — that path covers only a
|
|
284
|
+
review that died before returning. A head that moved past `reviewed=` with no
|
|
285
|
+
such halt — a fix-applier's push — stays not due: duty 2 verifies what it
|
|
286
|
+
applied. **A rebase never gets a `reviewed=`:** the earlier review still
|
|
287
|
+
counts, because dispositions verdicts are tied to its head. Never write a
|
|
288
|
+
`reviewed=` you did not read off a review result file.
|
|
289
|
+
|
|
290
|
+
## Resolving a finisher that died
|
|
291
|
+
|
|
292
|
+
**Resolving a finisher that died.** A finisher settled `failed` or `killed` left
|
|
293
|
+
its PR without `ready-to-merge`, and nothing else notices: the PR is out of the
|
|
294
|
+
merge queue, and the tick dispatches no finisher for it. The cockpit flags a
|
|
295
|
+
REVIEW-column open PR without the label, whose latest finisher attempt settled
|
|
296
|
+
so, `finisher:failed` or `finisher:killed` at severity 4 — one token per
|
|
297
|
+
outcome, neither the implementer's bare `killed` flag nor `unlabelled`. You
|
|
298
|
+
resolve it by hand: investigate why the finisher died, or dispatch the
|
|
299
|
+
next-suffix finisher after the finisher gate: render its brief with
|
|
300
|
+
`dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<M>-<x>
|
|
301
|
+
…` (`--kind repair` once an earlier finisher reported `labelled` without the
|
|
302
|
+
label landing), then `ledger.mjs dispatch <M> finisher-pr-<M>-<x>`. The flag
|
|
303
|
+
clears when a later attempt settles — `labelled`
|
|
304
|
+
(then `unlabelled` takes over while the label is still off), `halted:<cause>`,
|
|
305
|
+
or a live `-b` — or when the PR carries `ready-to-merge`, merges or closes.
|
|
306
|
+
You never add the label yourself, and the tick does not retry.
|
|
@@ -42,3 +42,23 @@ refusal on that is noise.
|
|
|
42
42
|
You just changed the instrument set under your own check, and this is the
|
|
43
43
|
only edit that legitimately does; skip it and the next gate refuses on your own
|
|
44
44
|
fix, which teaches you to ignore the refusal.
|
|
45
|
+
|
|
46
|
+
## Landing a mid-run tooling fix from the main checkout
|
|
47
|
+
|
|
48
|
+
**Re-pin first** — `~/.fleet/bin/fleet-run instruments.sh
|
|
49
|
+
--pin`. Then the ledger line, save the
|
|
50
|
+
rationale, one line to the maintainer. **Then land it from the main checkout,
|
|
51
|
+
if the fix was edited in it** — until you do, the next tick reads
|
|
52
|
+
`MAIN-CHECKOUT-DIRTY` naming your own paths. Commit the fix on a branch and
|
|
53
|
+
switch back to `main` before the next tick, and never `--record` while on that
|
|
54
|
+
branch: the switch back restores the tree the baseline recorded, so the hold
|
|
55
|
+
clears itself with nothing re-recorded, and the fix leaves the checkout until
|
|
56
|
+
its PR merges. A `HEAD …` entry is none of your paths and clears the same way.
|
|
57
|
+
Re-baseline only a fix you leave edited in the checkout: run
|
|
58
|
+
`~/.fleet/bin/fleet-run main-checkout.mjs --check` first, and only when the
|
|
59
|
+
paths it names are exactly the ones you just edited, run `--record` — never
|
|
60
|
+
before the commit, because the switch back would then read your own paths
|
|
61
|
+
dirty again. Any other path on that line is a stray write — leave the hold for
|
|
62
|
+
the maintainer, because `--record` would certify it with yours. Live members
|
|
63
|
+
hold the old text — re-brief only if it changes what they do *now*.
|
|
64
|
+
|
|
@@ -310,3 +310,13 @@ by the member rather than by the controller's guesswork.
|
|
|
310
310
|
It owns the object-store rule, the two-tree split, snapshot provisioning, and
|
|
311
311
|
which suites a snapshot cannot validly run. Your only duty is to *not*
|
|
312
312
|
contradict it.
|
|
313
|
+
|
|
314
|
+
## Every brief carries the absolute scratch path
|
|
315
|
+
|
|
316
|
+
`<scratch>` in this file is the path it prints, resolved before you send anything: every
|
|
317
|
+
brief that carries one carries the absolute path you passed it — an
|
|
318
|
+
implementer's `<scratch>/impl-<N>/`, the recipe deriver's
|
|
319
|
+
`<scratch>/derive-recipe/`, a fix-applier's, a finisher's and a merge bot's
|
|
320
|
+
`<scratch>/pr<N>/…`, and a review runner's `scratch` argument, which is the
|
|
321
|
+
root itself. A member whose prompt names no path asks you by name and never
|
|
322
|
+
makes one up.
|
|
@@ -101,3 +101,7 @@ The ledger half reports its own readability as its own field, `ledger.ok`, the p
|
|
|
101
101
|
**The suite slots wedge the same ways, and clear the same way.**
|
|
102
102
|
|
|
103
103
|
`~/.fleet/suite-slots/<i>.lock` is built on the ledger's lock primitive, so an empty or unparseable `<i>.lock` and a leftover `<i>.lock.reap` are permanent, a zombie holder is transient, and a dead holder is taken over. A wait that expired with exit 75 names each wedged file — remove it by hand once no suite run is running, the way the Writer policy section above removes `<file>.lock`.
|
|
104
|
+
|
|
105
|
+
## Adding a token
|
|
106
|
+
|
|
107
|
+
Token additions go through `ledger.mjs token`. `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row. Re-typing the row with `row` replaces the whole line, so it drops the tokens you do not restate.
|
|
@@ -176,3 +176,21 @@ Dispatching the next member on top of an unchecked or mismatched dispatch multip
|
|
|
176
176
|
## Ledger reads and their buffer
|
|
177
177
|
|
|
178
178
|
`board.mjs` reads `ledger.mjs read` with an explicit `maxBuffer`, so a payload past node's default is carried rather than killed mid-flight and reported as an unreachable tool — which is how the blind cockpit came back at HTTP 200 even after the pipe cut was gone.
|
|
179
|
+
|
|
180
|
+
## A carried implementer after `/resume` holds the whole tick
|
|
181
|
+
|
|
182
|
+
**After `/resume`, a carried implementer can hold the whole tick.** It holds when the check given this run's `session` finds no `<member>.jsonl` and exits 2, writing nothing. `HOLD (tier unchecked impl-<N>)` then stops the whole tick, not only that member's ticket. It clears once the member settles: one settled `killed` or `released` gets `tier-unverifiable=` from the same check, and one settled any other way ran, so pass the directory that dispatched it, as above. Until it settles, follow the carried token's reconciliation in references/phase-0.md, the "Resume the claims a dead run left without a PR" section, under its `prior: ancestor` branch: `proc://` reads `running` → wait; dead → the Member-killed row.
|
|
183
|
+
|
|
184
|
+
A mismatch on a ticket whose issue is already CLOSED holds nothing: the tick probes that issue and drops it, since there is nothing left to replace.
|
|
185
|
+
|
|
186
|
+
## The test command every gate runs
|
|
187
|
+
|
|
188
|
+
**The test command every gate runs.** Pass the same command to the review, to the fix-applier, and to the finisher — whose duty-2 mutation gate runs it too — so every gate runs one command. The fix-applier and the finisher run what you hand them, so hand them the wrapped string as `<testCmd>` — `~/.fleet/bin/fleet-run suite-slot.mjs '<command>'`, each `'` inside the command written `'\''` — and it is still one command for every gate. The review wraps its own one run, so its args carry the bare command; a wrapped string handed to it anyway is harmless, the inner wrapper being the nested case. Omit it from the review args and the review (`review-core.mjs`, in a runner) reads the same cache itself, refusing outright when there is none; the fix-applier has no such fallback, so substituting `<testCmd>` with nothing leaves it no gate at all. See `references/isolation.md` (the stack-free test command).
|
|
189
|
+
|
|
190
|
+
## Dispatching the members a wake names, in full
|
|
191
|
+
|
|
192
|
+
- **The reviewer.** **Dispatch the review, never wait on it, and act when its result lands. That is the default path.** `DISPATCH review PR#<M>`: a `task` member named `review-pr-<pr#>`, agent `fleet-review-runner`, its prompt the five args `pr`, `branch`, `worktree`, `testCmd` and `scratch`. Record `review=member:review-pr-<pr#>` on the PR's row before the call. `scratch` is the scratch root itself, never a `pr<N>` partition (`references/reviewers.md`). Write the `review=` token with `ledger.mjs review <pr> <token>`; `ledger.mjs` refuses a bad grammar. The tick names every PR owed one, oldest first, and each gets one review, once per PR. The tick counts in-flight reviews off the ledger and nowhere else, so a review you launched and never recorded is one it names again. The runner writes the result file and reports its path and `reviewed=` token.
|
|
193
|
+
A row with `review=` and no `reviewed=` is a review in flight, and the tick counts it as a live review unit unless gh reports its PR MERGED or CLOSED.
|
|
194
|
+
- **The fix-applier.** `DISPATCH fix-pr PR#<M>` on a `reviewed=` count that survived or unverified findings, with no fix-applier live: name `fix-pr-<pr#>` (the next suffix if that name is on record), never the PR's implementer; render with `dispatch-prompt.mjs render fix-applier --kind review --member fix-pr-<pr#> …`, then record `ledger.mjs dispatch <pr#> fix-pr-<pr#>` before the call. Its brief carries the PR number, the worktree abs path, the PR's branch, the same `testCmd` you passed the review, and the **path** `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's latest `reviewed=` — never the findings. Every `<scratch>` in that brief is the absolute path `scratch-root.mjs` printed. Its rulings are the fix-applier body's (`agents/fleet-fix-applier.agent.md`); you hold none. It holds every finding and you hold none, so there is nothing for you to rank, relay or pre-rule — and a report from a refuter it spawns that surfaces to you is its to retrieve, never yours to scan or pass on. It answers the hold and nothing else: survived or unverified findings of a returned review the row also carries stay unanswered through its settle, so the tick prints `DISPATCH fix-pr PR#<M>` again once it lands, and that one is a review fix-applier, dispatched as above. On a PR carrying `ready-to-merge`, the push moves the head after the label, so the next merge bot refuses it `head-moved-after-label-#<M>`: a fresh finisher, per **Failure handling**.
|
|
195
|
+
- **The conflict hold.** `DISPATCH fix-pr PR#<M>` on a conflict hold: the row carries an unresolved `conflict-hold:#<M>`, the merge bot's or one you recorded off the tick's `CONFLICT` line. It is the same slot with a different job, and no review file to hand over. Name it `fix-pr-<M>`, or the next suffix (`-b`, `-c` …) when that name is on record; render with `dispatch-prompt.mjs render fix-applier --kind conflict --member <name> …`; dispatch with the `agent` `ledger.mjs dispatch` printed, in the PR's existing worktree. Read the definition off that output (`fleet-fix-applier` whatever the row's `tier=`), never recall it. Settle `applied:<head>`, or `no-op` if no conflict was left, which lifts the merge hold; a dispatched-but-live one does not, and `failed`/`killed` leave the PR fix-due for a replacement.
|
|
196
|
+
- **Then dispatch a red-check fixer** — on a `check`-job `failure` bound to the PR's current head that is not **solely** staleness: a `rebase-check` red, or heavy jobs `skipped`, that comes only off the behind-count gets no fixer (the five-condition note in `agents/fleet-fix-applier.agent.md` establishes that). Name `fix-pr-<M>`, or the next free suffix; render with `dispatch-prompt.mjs render fix-applier --kind red-check --member <name> …`, record with `ledger.mjs dispatch <pr#> <name>` before the call, and dispatch with the printed `agent` in the PR's existing worktree. While a review or a review fix-applier on the PR is live or still owes its report — a dispositions mismatch awaiting its retry and a conflict hold awaiting its fix-applier included — the red-check fixer does not fire: their push starts a new run, and a fixer racing them would work on a SHA that is still moving. The common case, the implementer's own first CI firing turning red before any review exists, still dispatches the review against that head as normal. The tick prints no `DISPATCH` for it, so you reconcile it yourself, and it always goes after the review, never before. It is told outright not to watch, poll or wait for CI: your persistent Monitor owns the next run on the pushed head, and the finisher gate — review returned AND fix-applier report sent — applies to this member the same way. Settle it `applied:<head>` once a fix is pushed. Settle it `no-op` only when the red turns out not to be this member's to fix — a flake that passes on rerun, or staleness — with nothing pushed; `no-op` counts as landed, so it never stands for "could not fix". Settle it `failed` when it cannot reach green, which the **Failure handling** row "Reviewer, fix-applier or red-check fixer cannot reach green" covers. Its three dispatch edges are under **The finisher gate**; their rationale is in `references/member-lifecycle.md` (The red-check fixer's three edges).
|
|
@@ -12,6 +12,14 @@ child's stderr. `--session <dir>` overrides the variable.
|
|
|
12
12
|
Exit 3 is a step that failed: its line carries the refusal, so fix the cause
|
|
13
13
|
and run it again.
|
|
14
14
|
|
|
15
|
+
`phase0.mjs` runs the start sequence in
|
|
16
|
+
its fixed order — branch, fast-forward, pin, baseline, preflight, ruleset,
|
|
17
|
+
tier-roles, metrics, cockpit, rotate, reclaim, recipe, fold-in, stranded,
|
|
18
|
+
shortlist — prints one `phase0: <step>: <status>` line per step with the
|
|
19
|
+
`NEED` lines last, and records each step in `.fleet/phase0.json`, which the
|
|
20
|
+
tick reads: it prints `PHASE0-INCOMPLETE` and holds every dispatching row
|
|
21
|
+
until the receipt is whole.
|
|
22
|
+
|
|
15
23
|
## Fast-forward the checkout
|
|
16
24
|
|
|
17
25
|
**Fast-forward the checkout before you trust these rules — you are reading
|
|
@@ -441,3 +449,45 @@ else. Exit 2 is a list that did not answer — a `gh` or `git`
|
|
|
441
449
|
failure, a list at its cap, a worktree whose presence cannot be told, or,
|
|
442
450
|
after `ancestor`, a ledger that cannot be read — and is never nothing
|
|
443
451
|
stranded.
|
|
452
|
+
|
|
453
|
+
## Build the Shortlist
|
|
454
|
+
|
|
455
|
+
1. **Build the Shortlist** — `~/.fleet/bin/fleet-run shortlist.mjs`, then read
|
|
456
|
+
`.fleet/shortlist.json`. The script runs the cheap filters and nothing else
|
|
457
|
+
— no ticket body is read here, because a Pull reads one ticket in full at
|
|
458
|
+
the moment it admits it:
|
|
459
|
+
|
|
460
|
+
- **Candidate scan** — `candidates.mjs --require-label ready-for-agent
|
|
461
|
+
--with-dropped`, oldest-first, never re-ranked. Its row count is
|
|
462
|
+
`scanned`, the supply; the rows its to-spec filter removed are
|
|
463
|
+
`dropped`, counted nowhere.
|
|
464
|
+
- **Dependency scan**, `next-ticket` step 2, on each row's `d` array. Open
|
|
465
|
+
blocker → drop.
|
|
466
|
+
- **Live Exclusions** — a ticket whose ledger row reads `excluded ·
|
|
467
|
+
behind-pr:#M` or `excluded · behind-issue:#M` stays out while its premise
|
|
468
|
+
holds (references/pull.md, **Exclusion**).
|
|
469
|
+
- **In-flight check** — `inflight.sh <N>` per survivor. Exit 1 is taken, and
|
|
470
|
+
any other non-zero exit is a check that could not answer, which is never
|
|
471
|
+
free. Both drop the ticket, logged by number.
|
|
472
|
+
|
|
473
|
+
It writes the survivors, in order, to `.fleet/shortlist.json` —
|
|
474
|
+
`{scanned, shortlist: [{n, t}], dropped: [{n, reason}]}` — resolved against the git common dir by
|
|
475
|
+
`fleet-dir.mjs`, the one locator `ledger.mjs` resolves `.fleet/ledger.md`
|
|
476
|
+
with too, so you, the tick and a member in its own worktree all name one
|
|
477
|
+
file. **Exit 0 is the file written,
|
|
478
|
+
an empty shortlist included** — the empty queue is an answer. **Exit 2 is a
|
|
479
|
+
refusal**: the scan, the ledger read or the write did not answer, and the
|
|
480
|
+
previous file stands untouched, because an empty file written then would
|
|
481
|
+
read as "no work". Never read 2 as empty.
|
|
482
|
+
|
|
483
|
+
Every probe fails CLOSED: an unreadable blocker counts as open, an
|
|
484
|
+
unreadable premise leaves its exclusion standing, an unanswered in-flight
|
|
485
|
+
check counts as taken. A ticket dropped wrongly waits one refresh; a ticket
|
|
486
|
+
admitted wrongly costs a Pull's full read at best and two members on one
|
|
487
|
+
ticket at worst.
|
|
488
|
+
|
|
489
|
+
**`--require-label ready-for-agent` mandatory, no fallback** — the script
|
|
490
|
+
fixes it, and you never run `candidates.mjs --allow-fallback` by hand to
|
|
491
|
+
widen the list: `ready-for-human` needs a human to brainstorm first and you
|
|
492
|
+
have no channel to one mid-flight.
|
|
493
|
+
|