lobstah 0.6.9 → 0.7.0
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/README.md +33 -1
- package/dist/main.js +7657 -5262
- package/dist/runner.js +1765 -1522
- package/docs/configuration.md +3 -1
- package/docs/man.md +163 -48
- package/docs/vocabulary.md +20 -10
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -16,6 +16,7 @@ keys of that section.
|
|
|
16
16
|
| `notifyVerbs` | `["needs-decision", "blocked", "done", "failed"]` | Which verbs fire `notifyCommand`. |
|
|
17
17
|
| `attentionKinds` | `["question", "decision", "pr:ready", "pr:review", "pr:conflict", "pr:checks"]` | Which kinds `man tend`, the glass, and the desktop pet show. `decision` is a question the helm put to the human with `lobstah man ask`; the glass shows it as a card to answer, and it hides the raw `question` it frames. Without `decision`, framed decisions do not show and raw questions do. `pr:draft`, `landed`, and `report` are valid opt-in kinds. An explicit list is used unchanged. Unknown kinds are errors. See the [attention contract](vocabulary.md#attention-contract). |
|
|
18
18
|
| `remindSecs` | `900` | An unanswered `needs-decision`/`blocked` re-fires to `man wait`/`man haul` on this interval until answered. `0` = report once only. |
|
|
19
|
+
| `readySettleSecs` | `600` | `pr:ready` stands after continuously meeting the ready conditions on the same head for this many seconds. Any not-ready observation or new head resets the timer. Each attention read re-evaluates expiry from the persisted start, even without a new forge event. `0` restores immediate ready attention; other `pr:*` kinds are unchanged. |
|
|
19
20
|
|
|
20
21
|
## `[repos.<key>]` — workspace definitions
|
|
21
22
|
|
|
@@ -109,6 +110,7 @@ send a continuation.
|
|
|
109
110
|
| `ttlSecs` | `1800` | Heartbeat age past which a registration is a ghost trap: the sweep removes it and requeues an unfinished catch, finalizes done/failed in `done/`, or finalizes a cancelled catch as failed. A fresh `lobstah report` or beat counts as liveness. If the daemon's own tick gap exceeds this TTL, it grants a full TTL after resume before sweeping any traps. |
|
|
110
111
|
| `beat` | `true` | The post-tool hook (`lobstah soak beat`) refreshes a soaking session's liveness and writes its catch's activity, at most once per 30 seconds per trap. With `false` the hook does nothing, and a trap's liveness comes from its reports and its park only. |
|
|
111
112
|
| `signOffGraceSecs` | `600` | After a trap signs off, its address is held this long: messages to it wait for a re-soak of the same worktree instead of bouncing, `send` to it queues with a warning, and work addressed to it raises no `bait-orphaned` notice. After the grace the daemon bounces the held messages. `0` bounces at sign-off. |
|
|
113
|
+
| `terminal` | unset | The terminal app a thrown trap opens in: `terminal` (Terminal.app) or `iterm` (iTerm2). A trap's roster profile (`man roster set <trap> --terminal`) overrides it. Unset, `man throw` uses the app the trap last signed on from, else Terminal.app. `man throw` opens the trap's session there through AppleScript (macOS only); the app needs Automation permission for the terminal that runs `man throw`. |
|
|
112
114
|
| `pausedTtlSecs` | `86400` (24 hours) | A trap whose catch last reported `paused` is kept out of the ghost sweep this long after the report. `report paused --until <iso|duration>` sets the expiry instead. After it, the sweep removes the trap as usual, and the notice says the pause expired. |
|
|
113
115
|
|
|
114
116
|
## `[helm]` — the orchestrator seat (`lobstah man helm`)
|
|
@@ -221,5 +223,5 @@ See [pickup.md](pickup.md) for the loop semantics these keys drive.
|
|
|
221
223
|
|---|---|
|
|
222
224
|
| `LOBSTAH_HOME` | The instance root (default `~/.lobstah`). Multiple instances = multiple homes; one daemon per home, enforced. |
|
|
223
225
|
| `LOBSTAH_MAN` | `=1` designates a session as the lobstah man for the `man haul` Stop hook. |
|
|
224
|
-
| `LOBSTAH_TRAP_TICKET` | A `
|
|
226
|
+
| `LOBSTAH_TRAP_TICKET` | A ticket `man throw` reserved. `lobstah soak` redeems it and signs the session on as the reserved trap. `--ticket` takes precedence. |
|
|
225
227
|
| `LOBSTAH_TERMINAL_TITLE` | `=0` stops soak from naming the terminal tab after the trap, and stow from clearing it. |
|
package/docs/man.md
CHANGED
|
@@ -473,7 +473,9 @@ dispatch with its full brief, status log, inbox, and evidence, each trap
|
|
|
473
473
|
with its lifecycle notices, message history, and catches, the notices
|
|
474
474
|
tail, the merge view, and watches — with filters, a table/cards toggle,
|
|
475
475
|
and the helm identified by name. Its tabs are On deck, Dispatches, Traps,
|
|
476
|
-
PRs, Reports, and
|
|
476
|
+
PRs, Reports, Notices, and Stats. The Stats tab shows catches per day as a
|
|
477
|
+
53-week heatmap, with today, this week, the total, and the top traps; it
|
|
478
|
+
reads `stats.json` from `GET /data/stats` and sends nothing. The Reports tab lists every report, unacked first,
|
|
477
479
|
then newest first, and the repo filter and the search box (title, author,
|
|
478
480
|
dispatch id, repo) apply to it. Reading the glass consumes no cursor. Each
|
|
479
481
|
live trap has a **↗ open** button at the end of its foot line. It asks the local server to focus
|
|
@@ -919,8 +921,8 @@ lobstah focus <trap> # focus a live trap from the CLI
|
|
|
919
921
|
lobstah stow # sign off; an open catch requeues, unread
|
|
920
922
|
# messages wait [soak].signOffGraceSecs for a
|
|
921
923
|
# re-soak, then bounce back to the helm;
|
|
922
|
-
#
|
|
923
|
-
lobstah stow --
|
|
924
|
+
# keeps the worktree
|
|
925
|
+
lobstah stow --remove # sign off and remove the worktree soak created
|
|
924
926
|
lobstah soak --name amber-gull # choose or change this trap's two-word name
|
|
925
927
|
```
|
|
926
928
|
|
|
@@ -967,15 +969,20 @@ trap by session id (`--session <id>`, or `$CLAUDE_CODE_SESSION_ID`), and
|
|
|
967
969
|
`send session:<id>` addresses it. With `--session`, or from inside the worktree, `report done` records the
|
|
968
970
|
worktree's HEAD commit and branch in evidence.
|
|
969
971
|
|
|
970
|
-
`stow`
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
972
|
+
`stow` keeps the worktree by default: it prints `worktree: kept` and
|
|
973
|
+
`reason: stow keeps the worktree; --remove removes it`, and a later
|
|
974
|
+
`man throw` brings the trap back in it. `stow --remove` removes a worktree
|
|
975
|
+
only when soak created it and nothing in it exists elsewhere. It keeps the
|
|
976
|
+
worktree and prints `worktree: kept` and a `reason:` when soak did not create
|
|
977
|
+
the worktree, or when the worktree has uncommitted changes, untracked files
|
|
978
|
+
that are not ignored, commits its upstream lacks, or no upstream. Ignored
|
|
979
|
+
files do not block removal. `stow --remove --force` explicitly allows removal
|
|
980
|
+
of unsaved checkout files; `--force` alone refuses. `--keep` is accepted and
|
|
981
|
+
changes nothing.
|
|
976
982
|
Stow runs the removal from the primary checkout, so it works from inside
|
|
977
983
|
the worktree. On removal it prints `worktree: removed`, `path:`, and
|
|
978
|
-
`returnTo: <primary checkout>`, with a help line `cd <primary>`.
|
|
984
|
+
`returnTo: <primary checkout>`, with a help line `cd <primary>`. Before
|
|
985
|
+
removal, the checkout's HEAD goes to the trap's protected ref. It
|
|
979
986
|
deletes the branch only when every commit on it is on its upstream (with no
|
|
980
987
|
upstream: on some remote branch);
|
|
981
988
|
otherwise it prints `branchKept: <branch> (<reason>)`. A deleted branch
|
|
@@ -1080,43 +1087,46 @@ enlistment** — noticed with its diagnosis (usually a missing Stop hook →
|
|
|
1080
1087
|
Nobody is conscripted: only a worktree whose session ran `soak` ever
|
|
1081
1088
|
receives work.
|
|
1082
1089
|
|
|
1083
|
-
###
|
|
1090
|
+
### Starting a new trap
|
|
1084
1091
|
|
|
1085
1092
|
```bash
|
|
1086
|
-
lobstah
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
lobstah soak --ticket <ticket> #
|
|
1093
|
+
lobstah man throw --new --repo <key> [--count N] [--harness claude|codex]
|
|
1094
|
+
# start N fresh traps; wait until each listens
|
|
1095
|
+
lobstah man throw --new --repo <key> --count 3 --dry-run
|
|
1096
|
+
# what would start; launches nothing
|
|
1097
|
+
lobstah soak --ticket <ticket> # what the new session runs: sign on as the reserved trap
|
|
1091
1098
|
lobstah stow --wt amber-gull # withdraw a reservation no session has redeemed
|
|
1092
1099
|
```
|
|
1093
1100
|
|
|
1094
|
-
`
|
|
1095
|
-
exists and writes a **starting** reservation
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
`starting`.
|
|
1101
|
+
`man throw --new` (the helm's) picks a two-word name and a `wt:` id for each
|
|
1102
|
+
fresh trap before any session exists and writes a **starting** reservation
|
|
1103
|
+
(`soaking/<id>.starting`) with a deadline (`--timeout`, default 180 seconds).
|
|
1104
|
+
`dispatch --for <name>` works on it at once: the work waits, as it does for
|
|
1105
|
+
any addressed trap. `man tend` and the glass show the trap as `starting`.
|
|
1099
1106
|
|
|
1100
|
-
The
|
|
1101
|
-
the
|
|
1107
|
+
The harness comes from `--harness`, else `[repos.<key>.harness].default`,
|
|
1108
|
+
else `[harness].default`; the model and effort from the same tables. The
|
|
1109
|
+
terminal is `[soak].terminal`, else Terminal.app. Throw opens the start
|
|
1110
|
+
command in a new tab in the repo's primary checkout:
|
|
1102
1111
|
|
|
1103
1112
|
```bash
|
|
1104
|
-
cd <repo> && CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 claude
|
|
1113
|
+
cd <repo> && CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 claude '/lobstah:trap soak --ticket <ticket>'
|
|
1105
1114
|
cd <repo> && codex '$lobstah:trap soak --ticket <ticket>'
|
|
1106
1115
|
```
|
|
1107
1116
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
A
|
|
1117
|
-
`trap-start-failed
|
|
1118
|
-
|
|
1119
|
-
|
|
1117
|
+
The session's soak redeems the ticket, from `--ticket` or from the
|
|
1118
|
+
`LOBSTAH_TRAP_TICKET` environment variable. It creates a worktree named after
|
|
1119
|
+
the reserved id, signs on under the reserved name and id, and deletes the
|
|
1120
|
+
reservation. The ticket then redeems nothing. A spent ticket left in
|
|
1121
|
+
`LOBSTAH_TRAP_TICKET` is ignored; a spent `--ticket` is refused, except in
|
|
1122
|
+
the session that redeemed it. A session that already mans a trap cannot
|
|
1123
|
+
redeem a ticket.
|
|
1124
|
+
|
|
1125
|
+
A throw that sees no sign-on by its deadline withdraws the reservation and
|
|
1126
|
+
posts `trap-start-failed`. A reservation found unredeemed at its deadline by
|
|
1127
|
+
the daemon (one throw did not wait for) **fails** the same way: the glass
|
|
1128
|
+
shows the trap as `start failed` with the reason, and work addressed to it
|
|
1129
|
+
stays queued. `lobstah stow --wt <name>` withdraws a reservation; its
|
|
1120
1130
|
addressed work is then orphaned bait and the helm gets a `bait-orphaned`
|
|
1121
1131
|
notice.
|
|
1122
1132
|
|
|
@@ -1140,20 +1150,125 @@ greyed card: `requested · <repo> · <harness> · waiting for the helm`, or
|
|
|
1140
1150
|
`waiting for a helm` when no helm is signed on.
|
|
1141
1151
|
|
|
1142
1152
|
```bash
|
|
1143
|
-
lobstah
|
|
1144
|
-
lobstah trap reserve --request <id> # reserve what the request asks for, and close it
|
|
1153
|
+
lobstah man throw --new --request <id> # start what the request asks for, and close it
|
|
1145
1154
|
```
|
|
1146
1155
|
|
|
1147
|
-
`
|
|
1148
|
-
|
|
1149
|
-
|
|
1156
|
+
`man throw --new --request <id>` takes the repo and harness from the request
|
|
1157
|
+
(flags may repeat them, never contradict them), records the request on the
|
|
1158
|
+
reservation, and closes the request with the result. The card becomes the
|
|
1159
|
+
starting card, then the live trap when the session signs on.
|
|
1160
|
+
|
|
1161
|
+
Every starting card shows a start command with a copy button: paste it into a
|
|
1162
|
+
terminal to start the session by hand if the throw's terminal could not open.
|
|
1163
|
+
The ticket in it is kept in `soaking/<id>.ticket` (mode 0600) until the
|
|
1164
|
+
reservation is redeemed or withdrawn. The glass sends it only to a page on
|
|
1165
|
+
this machine's own glass address, only on the starting card, and no notice or
|
|
1166
|
+
log carries it.
|
|
1167
|
+
|
|
1168
|
+
### The roster, and what a throw would do
|
|
1169
|
+
|
|
1170
|
+
Every trap that signs on gets a **roster** record
|
|
1171
|
+
(`~/.lobstah/roster/<trapId>.json`). A registration lasts while a session
|
|
1172
|
+
mans the trap, and the signed-off hold lasts for the mail grace. The roster
|
|
1173
|
+
record stays through stow, the end of the grace, a ghost sweep, and `cull`.
|
|
1174
|
+
It keeps the trap's name, repo, worktree, branch and HEAD, harness, session,
|
|
1175
|
+
the registration's model and config, and why the trap left. Its name is the
|
|
1176
|
+
trap's name: a trap that signs on again in its worktree keeps it, from any
|
|
1177
|
+
session.
|
|
1178
|
+
|
|
1179
|
+
Before stow, a ghost sweep, or `cull` removes a trap's checkout, lobstah
|
|
1180
|
+
writes the checkout's HEAD to the **protected ref**
|
|
1181
|
+
`refs/lobstah/traps/<trapId>` in the repository. It is not a branch:
|
|
1182
|
+
removing the worktree or deleting its branch leaves the ref and its commit in
|
|
1183
|
+
place.
|
|
1150
1184
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1185
|
+
```bash
|
|
1186
|
+
lobstah man roster # every trap the roster keeps
|
|
1187
|
+
lobstah man roster set <trap> --harness codex --model <m> --terminal iterm --config effort=high
|
|
1188
|
+
# save how a throw starts it; `default` or `key=` clears
|
|
1189
|
+
lobstah man throw <trap> [--timeout <secs>] # bring one trap back; wait until it listens
|
|
1190
|
+
lobstah man throw --all # every eligible trap in your grounds, at once
|
|
1191
|
+
lobstah man throw --repo <key> # every eligible trap of one repo
|
|
1192
|
+
lobstah man throw --new --repo <key> --count N # N fresh traps (see "Starting a new trap")
|
|
1193
|
+
lobstah man throw --dry-run <trap>... | --all | --repo <key> | --new ...
|
|
1194
|
+
# what a throw would do; launches nothing (--plan too)
|
|
1195
|
+
lobstah man roster forget <trap> [--force] # remove a stowed trap's record, worktree, and ref
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
`man throw --dry-run` (or `--plan`) launches nothing and writes nothing. Each
|
|
1199
|
+
row says what a throw would do and why:
|
|
1200
|
+
|
|
1201
|
+
- `resume`: the harness reopens the saved session (`claude --resume`, `codex
|
|
1202
|
+
resume`) in the trap's checkout.
|
|
1203
|
+
- `cold`: a new session under the same name, id, and checkout. The reason is
|
|
1204
|
+
one of: no saved history for the session, a session that ran in an app (the
|
|
1205
|
+
Claude desktop app's Code tab, the VS Code extension, the Codex app), or a
|
|
1206
|
+
saved profile whose harness is not the one that wrote the session.
|
|
1207
|
+
- `skip`: the trap is live or starting (reserved).
|
|
1208
|
+
- `unresolved`: a throw would refuse. The repo is no longer configured or now
|
|
1209
|
+
points at another repository, the record is incomplete, the checkout is
|
|
1210
|
+
gone and no protected ref keeps its revision, or the path now anchors
|
|
1211
|
+
another trap. A trap stowed before the roster existed is listed here until
|
|
1212
|
+
a session soaks in its worktree once.
|
|
1213
|
+
|
|
1214
|
+
`checkout` is `kept` (the checkout is adopted as it is) or `recreate` (it is
|
|
1215
|
+
added back at the protected revision). The harness, model, config, and
|
|
1216
|
+
terminal come from the saved profile, else from the trap's last sign-on. The
|
|
1217
|
+
terminal is the profile's, else `[soak].terminal`, else the app the trap last
|
|
1218
|
+
signed on from, else Terminal.app. `--all` and `--repo` stay inside the
|
|
1219
|
+
grounds: `--grounds`, else the caller's helm grounds, else every repo.
|
|
1220
|
+
|
|
1221
|
+
`man throw <trap>` does what its plan row says, for one trap (the helm's
|
|
1222
|
+
verb). A `skip` refuses: a trap that is live, or already starting, is never
|
|
1223
|
+
launched twice. Otherwise:
|
|
1224
|
+
|
|
1225
|
+
1. It reserves the trap under its **own** id and name, in its own worktree.
|
|
1226
|
+
The reservation is created exclusively, so a second throw refuses until
|
|
1227
|
+
the first returns or times out. `--for <name>` keeps working throughout.
|
|
1228
|
+
2. A `recreate` checkout is added back at `refs/lobstah/traps/<id>` on the
|
|
1229
|
+
trap's recorded branch, with the repo's `setup`. An existing branch is
|
|
1230
|
+
used only when it still contains that revision; a throw never resets a
|
|
1231
|
+
branch. A kept checkout is used as it is, dirty or not.
|
|
1232
|
+
3. It opens the start command in the terminal (Terminal.app or iTerm2,
|
|
1233
|
+
through AppleScript, macOS only). `resume` reopens the saved session from
|
|
1234
|
+
the directory its history is keyed by (Claude keys transcripts by the
|
|
1235
|
+
directory the session started in); `cold` starts a new session in the
|
|
1236
|
+
worktree. The profile's model and config become harness flags (`--model`,
|
|
1237
|
+
`--effort`, `--permission-mode` for Claude; `-m`, `model_reasoning_effort`
|
|
1238
|
+
for Codex); a setting with no flag is named as not applied. The first
|
|
1239
|
+
prompt redeems the ticket, so the session signs on as the same trap.
|
|
1240
|
+
4. It returns when the trap has signed on and parked, and prints whether it
|
|
1241
|
+
resumed or started cold, and why. Past `--timeout` (default 180 seconds)
|
|
1242
|
+
it withdraws the reservation, so a late session cannot take the trap,
|
|
1243
|
+
posts `trap-start-failed`, and leaves the roster unchanged.
|
|
1244
|
+
|
|
1245
|
+
**Batches.** `man throw --all` throws every eligible trap in the grounds
|
|
1246
|
+
(`--grounds`, else the caller's helm grounds, else every repo); `--repo`
|
|
1247
|
+
narrows to one repo; `--new --count N` starts N fresh ones. Every launch runs
|
|
1248
|
+
at once, with no limit, and the batch waits for all of them. It prints one
|
|
1249
|
+
row per trap: `resumed`, `cold`, or `new` (with why), `skipped` (live,
|
|
1250
|
+
starting, or unresolved, with why), or `failed` (with why). One trap failing
|
|
1251
|
+
does not stop the others, and the command exits 1 when any failed.
|
|
1252
|
+
|
|
1253
|
+
**Notices.** A trap's start wakes the helm once: `trap-available`, when the
|
|
1254
|
+
trap is listening. Its reservation (`trap-starting`), sign-on
|
|
1255
|
+
(`trap-signed-on`), and sign-off (`trap-stowed`) are quiet: tend, the
|
|
1256
|
+
digest's `traps` table, and the glass show them. A ghost sweep of an idle
|
|
1257
|
+
trap is quiet the same way; a ghost sweep of a trap that held a catch wakes.
|
|
1258
|
+
A batch posts no `trap-available` per trap: when every throw has settled it
|
|
1259
|
+
posts one `trap-batch` notice with the counts and the names that failed. A
|
|
1260
|
+
trap that failed to start (`trap-start-failed`) and work addressed to a trap
|
|
1261
|
+
that is gone (`bait-orphaned`) still wake.
|
|
1262
|
+
|
|
1263
|
+
**Forget.** `man roster forget <trap>` (the helm's) takes a stowed trap off
|
|
1264
|
+
the roster for good. It refuses a live or starting trap. It also refuses, and
|
|
1265
|
+
names the work, when the trap holds anything that exists nowhere else:
|
|
1266
|
+
modified or untracked files in its checkout, or commits (in the protected
|
|
1267
|
+
ref, the checkout, or its branch) that are on no remote branch and not merged
|
|
1268
|
+
to trunk. `--force` forgets anyway. Otherwise it removes the worktree soak
|
|
1269
|
+
created (one it did not create stays, minus its anchor), deletes
|
|
1270
|
+
`refs/lobstah/traps/<id>`, bounces held messages, and frees the name. A
|
|
1271
|
+
branch that still holds commits on no remote is kept even with `--force`.
|
|
1157
1272
|
|
|
1158
1273
|
### The sign-on title
|
|
1159
1274
|
|
|
@@ -1188,7 +1303,7 @@ Other terminals are left alone. `lobstah stow` clears the name.
|
|
|
1188
1303
|
Claude Code writes its own title to the tab, and in Terminal.app that title
|
|
1189
1304
|
replaces the custom title. `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` on the
|
|
1190
1305
|
command that starts Claude Code stops that for that one process; the start
|
|
1191
|
-
command `
|
|
1306
|
+
command `man throw` opens sets it. Codex also sets the terminal title.
|
|
1192
1307
|
|
|
1193
1308
|
### Culling and disk space
|
|
1194
1309
|
|
package/docs/vocabulary.md
CHANGED
|
@@ -396,7 +396,7 @@ evidence).
|
|
|
396
396
|
| descriptor `pr` | `{ url, headRefName?, headSha? }`: the existing PR a dispatch works on. A PR repair and a pickup rebase chore carry it. The runner pushes no branch and opens no PR for such a dispatch; its worker pushes to the PR's head branch. |
|
|
397
397
|
| push rule | What a repair or rebase brief tells its worker: push only to the PR's head branch; on a non-fast-forward rejection, fetch, rebase the commits onto the moved head again, and push with `--force-with-lease` on the head just fetched, at most three times; a hook failure from a real test or type error is not retried; when it cannot push, report `failed "push rejected: <rejection text>; moved head <sha>"` and leave the PR as it was. That report marks the PR's repair `blocked` at the moved head and posts a `push-failed` notice. |
|
|
398
398
|
| checks unknown | Without `Checks: read`, the check re-reads the PR without `statusCheckRollup`: the PR state is recorded, `checks.unknown` is `no permission`, and the check's output carries the permission `error`. `pr:ready` never stands on unknown checks. |
|
|
399
|
-
| PR record | `~/.lobstah/prs/<owner>__<repo>__<n>.json` — the PR's latest observation keyed by the PR, not by a dispatch: the evidence `pr` object (with `title`, read on every check; a title change is not a state change) plus `key`, `repo` (`<owner>/<repo>`), `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR), and `firstSeenAt` (the time of the first observation; written once, never rewritten). `firstSeenAt`, then the PR number, is the order of every PR list. A record from before `firstSeenAt` existed sorts by number at the earliest `firstSeenAt` in the set, and its next observation writes that time as its `firstSeenAt`. **Owner:** `packages/core/src/prs.ts` (`upsertPr`, `readPrs`); the one writer is the preset's observation path (`observePr`), on every observation, man-owned or dispatch-owned — a dispatch-owned one also stamps that dispatch's evidence, which stays the per-dispatch view. Tend's `pr:*` kinds and `pr:ready`
|
|
399
|
+
| PR record | `~/.lobstah/prs/<owner>__<repo>__<n>.json` — the PR's latest observation keyed by the PR, not by a dispatch: the evidence `pr` object (with `title`, read on every check; a title change is not a state change) plus `key`, `repo` (`<owner>/<repo>`), `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR), and `firstSeenAt` (the time of the first observation; written once, never rewritten). `firstSeenAt`, then the PR number, is the order of every PR list. A record from before `firstSeenAt` existed sorts by number at the earliest `firstSeenAt` in the set, and its next observation writes that time as its `firstSeenAt`. **Owner:** `packages/core/src/prs.ts` (`upsertPr`, `readPrs`); the one writer is the preset's observation path (`observePr`), on every observation, man-owned or dispatch-owned — a dispatch-owned one also stamps that dispatch's evidence, which stays the per-dispatch view. Tend's `pr:*` kinds and `stack-ready` aggregation and quiet stacked `pr:ready` items, the glass PRs tab and stacks, the merged/closed notice, and PR acks read records first and fall back to dispatch evidence only for a PR with no record yet. `cull` removes records merged or closed longer than its window, never open ones. |
|
|
400
400
|
|
|
401
401
|
Every event carries `headSha`. A dispatch-owned PR watch records work events,
|
|
402
402
|
then the repair planner decides whether to follow up. One repair runs per PR
|
|
@@ -467,18 +467,21 @@ worktree is refused (the session lock); a stale one is adopted.
|
|
|
467
467
|
| ---- | ------- |
|
|
468
468
|
| `soak` | Sign the worktree's trap on and take matching work. From a primary checkout, or with `--repo <key>` from outside any configured repo, it first creates a linked worktree like a dispatch does (fetch trunk, new branch from `origin/<trunk>`, `setup` commands), prints `worktree:`, `created: true`, `branch:`, and, when the session is elsewhere, `instruction: cd <path> ...`; the session works in that directory. If creation fails, nothing is signed on and the partial worktree and branch are removed. `--repo` in a linked worktree must match its repo. A session that already mans a trap re-uses it (resolved from the session id) and never gets a second worktree. `--session` is needed only on first sign-on. `--one` stows after the first catch. `--wait` registers a watcher and waits for work; a quiet timeout exits 3. Workers never run `man` verbs. |
|
|
469
469
|
| `soak title-set` (alias `trap title-set`) | Confirm this session applied the title sign-on printed (`step: Apply this title: <title>`). The registration records `titleSet` and `titleSetAt` and drops `titlePending`; sign-on is complete. Until then, SessionStart and Stop remind the session once per turn, at most three times. A trap runs `soak title-set`: Claude Code's worktree isolation refuses a command line containing `trap`. |
|
|
470
|
-
| `stow` | Sign the trap off (in the worktree, or elsewhere by session id: `--session <id>` or `$CLAUDE_CODE_SESSION_ID`); an unfinished catch requeues, done/failed finalizes in `done/`, and a cancelled catch finalizes as failed. Unread messages are held for `[soak].signOffGraceSecs` (default 600): they deliver if the same worktree re-soaks in time, else the daemon bounces them to the helm (at once when stow removed the worktree or the grace is `0`). A trap always stows itself freely; stowing someone else's (`--wt`) is steering — the claimed helm's alone. Stow closes the seat, never the session: an opted-in session can only be asked to stop, and a still-looping worker re-enlists visibly (`trap-signed-on`). By default stow removes the worktree when soak created it (`createdWorktree: true`) and prints `worktree: removed`, `path:`, and `returnTo:`;
|
|
470
|
+
| `stow` | Sign the trap off (in the worktree, or elsewhere by session id: `--session <id>` or `$CLAUDE_CODE_SESSION_ID`); an unfinished catch requeues, done/failed finalizes in `done/`, and a cancelled catch finalizes as failed. Unread messages are held for `[soak].signOffGraceSecs` (default 600): they deliver if the same worktree re-soaks in time, else the daemon bounces them to the helm (at once when stow removed the worktree or the grace is `0`). A trap always stows itself freely; stowing someone else's (`--wt`) is steering — the claimed helm's alone. Stow closes the seat, never the session: an opted-in session can only be asked to stop, and a still-looping worker re-enlists visibly (`trap-signed-on`). By default stow keeps the worktree (`worktree: kept`, `reason: stow keeps the worktree; --remove removes it`), so `man throw` can bring the trap back in it; `--keep` is accepted and changes nothing. `--remove` removes the worktree when soak created it (`createdWorktree: true`) and prints `worktree: removed`, `path:`, and `returnTo:`; it still keeps it (`worktree: kept`, `reason:`) when soak did not create it, or when it holds uncommitted changes, untracked files that are not ignored, commits absent from its upstream, or no upstream. `--remove --force` explicitly allows discarding unsaved checkout files; `--force` alone refuses. The branch is deleted (`branchDeleted:`) only when all its commits are on its upstream (with no upstream: on some remote branch); otherwise `branchKept: <branch> (<reason>)`. `--wt` follows the same rules. The SessionEnd hook (`stow --quiet`) signs off only when the ending session is the trap's registered session (another session in the worktree ending is a no-op) and keeps the worktree; SessionEnd cannot tell a restart or compaction from an exit (Claude Code's `reason` is `clear`, `resume`, `logout`, `prompt_input_exit`, or `other`; Codex's is always `other`), so the grace covers a session that signs on again. |
|
|
471
471
|
| sign-off grace | For `[soak].signOffGraceSecs` after a trap signs off (`~/.lobstah/signed-off/<trap>.json`), its address is held: `send wt:<trap>` queues and warns instead of refusing, held messages wait, and work addressed to it raises no `bait-orphaned` notice. A re-soak of the same worktree releases the hold; after the grace, the daemon bounces the held messages (`message-bounced`). |
|
|
472
472
|
| address | `--for wt:<trap>` targets one trap; `session:<id>` is an alias resolved to the trap at dispatch time. **Sticky:** addressed work is never the daemon's — it waits for its trap; an orphan (trap gone) surfaces as a `bait-orphaned` notice for the helm to re-address, release, or cancel. Delivery stamps a receipt (`deliveredTo`/`deliveredAt`) into evidence. Unaddressed work defers to a parked matching trap for `[soak].deferSecs`, then the daemon spawns headless. |
|
|
473
473
|
| message | `send wt:<trap> "<text>"` — a conversational continuation, not work: no branch, no catch, no report obligation. Delivered before bait at the trap's next park, stamped with its sender (`helm` / `session:<id>` / `terminal`); undeliverable messages bounce to the helm as notices. |
|
|
474
474
|
| catch | The active dispatch a trap claimed (`claim.json`, `by: wt:<id>`). One catch per trap; one active item per worktree. The daemon never spawns or restarts it — the session's reports are its liveness. |
|
|
475
475
|
| hook command | `lobstah hook session-start\|stop\|post-tool-use\|session-end`: the command each plugin hook runs. Each detects the session's role (helm, trap, or neither) and runs what `man brief`, `man haul`, `soak beat`, and `stow --quiet` run; those stay as aliases. Stop, PostToolUse, and SessionEnd are inert for a session with neither role; SessionStart announces the session id to every session. |
|
|
476
476
|
| beat | `lobstah soak beat` (`lobstah hook post-tool-use`), run by the plugin's post-tool hook after every tool call. It resolves the trap from the working directory, else from the session id (files only: no git, no network), refreshes the trap's beat (`soaking/<trap>.beat`, separate from the registration), and writes the claimed catch's [activity](#activity). Throttled to one per 30 seconds per trap. Inert in a session that is not soaking, or with `[soak].beat = false`. Always exits 0; errors go to `logs/beat.log`. |
|
|
477
|
-
| ghost trap | A registration whose heartbeat **and** beat lapsed past `[soak].ttlSecs` **after having parked at least once**. The sweep removes the registration, requeues only unfinished catches, finalizes done/failed in `done/`, and posts a `trap-ghosted` notice. It removes a soak-created checkout only when clean and pushed to its upstream; dirty, unpushed, no-upstream, or unreadable checkouts stay, protected from later culls too. The notice includes path, branch, modified-file and unpushed-commit counts, or `unknown` when unavailable. A kept anchor preserves the address on re-soaking. A fresh report or beat keeps a working session out of the sweep; a fresh beat also holds the session lock. A catch whose last report is `paused` keeps its trap until the pause expires ([Waiting on](#waiting-on)). A daemon tick gap longer than the soak TTL starts one full TTL of resume grace before any ghost sweep. |
|
|
477
|
+
| ghost trap | A registration whose heartbeat **and** beat lapsed past `[soak].ttlSecs` **after having parked at least once**. The sweep removes the registration, requeues only unfinished catches, finalizes done/failed in `done/`, and posts a `trap-ghosted` notice: quiet (digest and glass only) for an idle trap, a wake for one that held a catch. It removes a soak-created checkout only when clean and pushed to its upstream; dirty, unpushed, no-upstream, or unreadable checkouts stay, protected from later culls too. The notice includes path, branch, modified-file and unpushed-commit counts, or `unknown` when unavailable. A kept anchor preserves the address on re-soaking. A fresh report or beat keeps a working session out of the sweep; a fresh beat also holds the session lock. A catch whose last report is `paused` keeps its trap until the pause expires ([Waiting on](#waiting-on)). A daemon tick gap longer than the soak TTL starts one full TTL of resume grace before any ghost sweep. |
|
|
478
478
|
| defective enlistment | A stale registration that **never parked** — signed on but never listened (usually no Stop hook). Not swept: the helm gets a `trap-defective` notice with the remedy (`soak --wait`), and the registration stays so the address keeps protecting its work. |
|
|
479
|
-
| reserved trap | A trap `
|
|
480
|
-
| trap
|
|
481
|
-
|
|
|
479
|
+
| reserved trap | A trap `man throw` reserved before its session signs on: a name, a `wt:` id, and a one-time ticket, shown as `starting`. `man throw --new` reserves a fresh id and name; `man throw <name>` reserves an existing roster trap under its own id and name, in its own worktree (the reservation records the worktree). A trap has at most one reservation, so a second throw refuses. Work addressed to it waits. A session redeems the ticket with `soak --ticket` (or `LOBSTAH_TRAP_TICKET`) and signs on as that trap. A throw that sees no sign-on by its timeout withdraws the reservation; one found unredeemed past its deadline fails with a `trap-start-failed` notice and its work stays queued. `stow --wt <name>` withdraws it. A reservation made by a batch throw records the batch, and so does the registration its sign-on makes. |
|
|
480
|
+
| trap roster | One durable record per trap that ever signed on (`~/.lobstah/roster/<trapId>.json`): canonical name, repo and its git common directory, worktree, soak branch, last branch and HEAD, harness, session, the registration's model and config, window and link (navigation hints only), state (`live`, `stowed`, `ghosted`), when and why it left, its last catch, and a saved profile (harness, model, config, terminal) set with `man roster set`. Sign-on writes it; stow and the ghost sweep update it before the registration goes. Stow, the end of the sign-off grace, a ghost sweep, and `cull` never remove it. The roster's name is the trap's name: a trap that signs on again keeps it. A roster record never makes a trap live or claimable. `man roster` lists it; `man throw` reads it; `man roster forget` removes it. |
|
|
481
|
+
| protected ref | `refs/lobstah/traps/<trapId>` in the trap's repository: the trap's last HEAD, written at sign-on and before stow, a ghost sweep, or `cull` removes the checkout. It is not a branch, so worktree removal and branch cleanup leave it and its commit in place. A missing checkout is recreated from it. A trap checkout whose revision cannot be written there is not removed by a ghost sweep or cull. |
|
|
482
|
+
| trap request | A trap the human asked for from the glass's **+ New trap** button: a `trap-request` request in `requests/<id>.json` with a repo and a harness. It wakes the helm as a `trap-request` event; the helm starts it with `man throw --new --request <id>`, which takes the request's repo and harness and closes it. |
|
|
483
|
+
| notice | The helm's attention channel for non-status events (`~/.lobstah/notices/`): reservations (`trap-starting`), reservations that did not start (`trap-start-failed`), trap requests from the glass (`trap-request`), sign-ons, a trap free to take work (`trap-available`), a batch throw settled (`trap-batch`), sign-offs, ghosts, defective enlistments, orphaned work, bounced messages, PRs merged or closed, watches held over the fork cap (`watch-held`), failing (`watch-failing`), and recovered (`watch-recovered`), free-space holds (`disk-held`, `disk-cleared`), worktrees released after their PR merged (`worktree-released`, one per cull pass), a repair or rebase that could not push to its PR's branch (`push-failed`), repairs stopped on a PR that made no progress (`repair-stopped`), and a human's answer to a decision (`decision-answer`; its ref is the request id in `~/.lobstah/requests/`). A trap leaves the registry only through a `trap-stowed` or `trap-ghosted` notice — the end-state is always explicit. Consumed by `man wait`/the park. A trap's start wakes once, with `trap-available` when it is listening; `trap-starting`, `trap-signed-on`, `trap-stowed`, `trap-listening` (older homes), a ghost of an idle trap, and a batch member's `trap-available` are quiet: consumed without waking anyone, shown in tend, the glass, and (sign-offs and idle ghosts) the digest's `traps` table. A batch throw wakes once, with `trap-batch` when every throw settled: counts, and the names that failed. tend always shows the recent tail. |
|
|
484
|
+
| stack-ready notice | One wake for an all-ready PR stack, with the PRs in bottom-first merge order. Stacked per-PR ready items remain quiet; unchanged heads do not re-notify as the bottom PRs merge. |
|
|
482
485
|
|
|
483
486
|
Delivery routes by ownership, same as watches: a continuation for a chain
|
|
484
487
|
claimed by a live trap is addressed back to that trap and stays sticky.
|
|
@@ -496,14 +499,20 @@ hold two; a live foreign holder refuses sign-on without `--take`. The rule
|
|
|
496
499
|
is strict: once a helm is claimed, the orchestrator verbs that consume helm
|
|
497
500
|
state (`man wait`, `man report`, the lobstah man park) are reserved for the
|
|
498
501
|
helm session — any other caller is refused with guidance (or, for the hook,
|
|
499
|
-
silently ignored). Read verbs (`man tend`, `man brief`
|
|
500
|
-
verbs stay open
|
|
502
|
+
silently ignored). Read verbs (`man tend`, `man brief`, `man throw --dry-run`,
|
|
503
|
+
`man roster`) and the enlistment verbs stay open; `man roster set`,
|
|
504
|
+
`man roster forget`, and `man throw` are the helm's. A stale helm reserves nothing.
|
|
501
505
|
|
|
502
506
|
| Word | Meaning |
|
|
503
507
|
| ---- | ------- |
|
|
504
508
|
| lobstah man | One interactive session that takes the helm for a grounds, triages the fleet, dispatches work, and answers traps; `lobstah man` also names its CLI command group. |
|
|
505
509
|
| `helm` | Sign a session on as the orchestrator for one grounds. Prints the charter and enables the Stop hook without a marker file. Re-running from the same session is an idempotent re-sign. |
|
|
506
510
|
| watcher registration | `~/.lobstah/watchers/<session-id>.json` stores a `man wait` or `soak --wait` process's PID and heartbeat. The waiter refreshes it while running and removes it on exit; a heartbeat older than five seconds is stale, and a second waiter for the same session refuses. |
|
|
511
|
+
| `man throw --dry-run` | What a throw would do for each selected trap (`--plan` is the same), read from the [trap roster](#soaking-contract): `resume` the saved session, `cold` (a new session under the same name, id, and checkout; the row says why: no saved history, an app session, a profile that changes harness), `skip` (live or starting), or `unresolved` (repo no longer configured or now another repository, record incomplete, checkout gone with no protected ref, or the path anchors another trap). Names select traps; `--all` selects every eligible trap in the grounds; `--repo` narrows. Each row shows harness, model, config, terminal (and where each came from), checkout (`kept` or `recreate`), revision, and held messages. Read-only: it launches and writes nothing. |
|
|
512
|
+
| `man throw --all \| --repo <key> \| --new [--count N]` | A batch throw (the helm's): every eligible trap in the grounds, one repo's, or N fresh traps from the harness defaults (`--harness`, `--request <id>` for a glass trap request). Every launch runs at once, with no limit. One row per trap: `resumed`, `cold`, `new`, `skipped`, or `failed`, each with why; one failure does not stop the others, and the command exits 1 when any failed. The batch posts one `trap-batch` notice when it settles and no per-trap `trap-available`. `--dry-run` (or `--plan`) shows the plan and launches nothing. |
|
|
513
|
+
| `man roster forget <name>` | Remove a stowed trap for good (the helm's): its worktree (one soak did not create stays, minus its anchor), its protected ref, its roster record, its held messages (bounced), and its name. Refuses a live or starting trap, and refuses, naming the work, when its checkout has unsaved files or its commits are on no remote branch and not merged to trunk; `--force` forgets anyway, keeping a branch that still holds such commits. |
|
|
514
|
+
| `man throw <name>` | Bring one roster trap back and wait until it listens. The helm's. It follows the plan for that trap: a `skip` (live or starting) refuses, which fences a double launch; an `unresolved` refuses with its reason. Otherwise it reserves the trap under its own id and name, recreates a missing checkout from the protected ref on its recorded branch (an existing branch only when it still contains that revision; a kept checkout, dirty or not, is used as is), and opens the start command in the profile's terminal (Terminal.app or iTerm2, macOS): `resume` reopens the saved session from the directory its history is keyed by, `cold` starts a new one in the worktree; the first prompt redeems the ticket. It returns when the trap has signed on under its id and parked, printing which start happened and why. Past `--timeout` (default 180 s) it withdraws the reservation, posts `trap-start-failed`, and leaves the roster unchanged. Batches: see `man throw --all`. |
|
|
515
|
+
| `man roster` | List the trap roster. `man roster set <trap> --harness\|--model\|--terminal <value>\|--config <key>=<value>` saves how a throw starts the trap; `default` (or `key=`) clears a field. |
|
|
507
516
|
| `man haul --park` | Make the Stop hook wait for attention while work is in flight. `[helm].park = "block"` selects the same mode. In arm mode, the hook allows a stop with a live watcher or blocks with the arm command. |
|
|
508
517
|
| `relieve` | Step down. `--take` on another session's `helm` is the only force path: it displaces a live holder deliberately and leaves them a stand-down notice, delivered once at their next park. A holder whose heartbeat lapsed past `[helm].ttlSecs` is stale and claimable without `--take`. |
|
|
509
518
|
| grounds | A named territory: the subset of configured repos one helm oversees, from `[grounds.*]`. A repo belongs to at most one grounds (config error otherwise). No grounds configured means one implicit `fleet` grounds covering every repo. |
|
|
@@ -534,7 +543,8 @@ never until someone acknowledges it.
|
|
|
534
543
|
| `pr:review` | An open PR has unresolved review questions, or requested changes that lobstah cannot repair, has exhausted, or is configured not to repair. | Every thread resolved and no changes requested, or merged / closed. |
|
|
535
544
|
| `pr:checks` | An open PR has a failed latest check, and lobstah cannot repair it, has exhausted attempts, or is configured not to repair. | Green on the head, or merged / closed. |
|
|
536
545
|
| `pr:conflict` | An open PR conflicts with its base, and lobstah cannot repair it, has exhausted attempts, or is configured not to repair. | The merge state leaves `DIRTY`, or merged / closed. |
|
|
537
|
-
| `pr:ready` | An open, non-draft PR has no review condition, a mergeable state (`CLEAN`, `HAS_HOOKS`, or `UNSTABLE`), and no failed, pending, or unknown latest checks. It is approved or has at least one check. | Merged or closed, or the ready conditions stop holding. |
|
|
546
|
+
| `pr:ready` | An open, non-draft PR has no review condition, a mergeable state (`CLEAN`, `HAS_HOOKS`, or `UNSTABLE`), and no failed, pending, or unknown latest checks. It is approved or has at least one check, continuously on the same head for `readySettleSecs` (default 600; `0` is immediate). Attention reads re-evaluate expiry without requiring a new forge event. In a trunk-rooted same-repo stack of two or more non-fork PRs, each ready item stays visible but quiet; the stack wakes once. | Merged or closed, or the ready conditions stop holding; any not-ready observation or new head resets settling. |
|
|
547
|
+
| `stack-ready` | Every open member of a trunk-rooted same-repo base/head chain meets the identical `pr:ready` rule, including settling. Enabled with `pr:ready` (or explicitly with `stack-ready`). One notice and walking item per ready epoch, naming PRs bottom to top in merge order. Tend, digest, and the existing glass group also show partial readiness. Existing `pr:` watches discover missing links at their normal cadence; `lobstah watch add pr:<owner>/<repo>#<n>` follows a stack from any member. | Any member becomes unready or gets a new head; all ready again wakes once. Bottom-first merges and retargeting do not wake again for unchanged surviving heads. |
|
|
538
548
|
| `report` | A filed report (`report --report`, `man file`) has no ack for this filing. Opt-in. Key `report:<lane>:<uuid>` or `report:helm:<grounds>:<rid>`. | Not cleared: it stays listed until culled. `lobstah attention ack <key>` acks it, and a newer report in the same chain acks the older; an acked report no longer walks. |
|
|
539
549
|
|
|
540
550
|
`pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
|
|
@@ -563,7 +573,7 @@ reminder loop apply the same predicate.
|
|
|
563
573
|
|
|
564
574
|
| Word | Meaning |
|
|
565
575
|
| ---- | ------- |
|
|
566
|
-
| ack | `~/.lobstah/acks/<item-key>.json` — `{ key, kind, stateHash, at, by }`, written only by `lobstah attention ack` (removed by `unack`, by the CLI's `man tend` / `attention` when its `stateHash` goes stale, and by `cull` when the item is gone). **Display-only**: it hides the item from the desktop pet and the glass lobs while the item's `stateHash` is unchanged; `man tend --json` keeps the item with `acked: { at, by }`, and `man wait`, the park, reminders, and `notifyCommand` never read acks. Item keys: `<lane>:<uuid>` (question, landed), `decision:<rid>`, `pr:<owner>/<repo>#<n>` (all of a PR's `pr:*` kinds — one ack covers them), `watch:<key>`. `stateHash` covers the status entry (question, landed) or the PR's head sha plus every evidence field a `pr:*` kind stands on (not `observedAt`); for a ready PR, only
|
|
576
|
+
| ack | `~/.lobstah/acks/<item-key>.json` — `{ key, kind, stateHash, at, by }`, written only by `lobstah attention ack` (removed by `unack`, by the CLI's `man tend` / `attention` when its `stateHash` goes stale, and by `cull` when the item is gone). **Display-only**: it hides the item from the desktop pet and the glass lobs while the item's `stateHash` is unchanged; `man tend --json` keeps the item with `acked: { at, by }`, and `man wait`, the park, reminders, and `notifyCommand` never read acks. Item keys: `<lane>:<uuid>` (question, landed), `decision:<rid>`, `pr:<owner>/<repo>#<n>` (all of a PR's `pr:*` kinds — one ack covers them), `watch:<key>`. `stateHash` covers the status entry (question, landed) or the PR's head sha plus every evidence field a `pr:*` kind stands on (not `observedAt`); for a ready PR, only the head sha. A ready ack is kept across same-head CI flaps and draft toggles, without hiding checks, conflicts, or review attention during the flap. A new head re-stands ready once after settling. |
|
|
567
577
|
| pet state | `~/.lobstah/pet/state.json` — `{ pid, at, ok, command, reason, consecutiveFailures, items, lastOkAt }`. The desktop pet's one write: it rewrites the file after each attention read (about every six seconds). `command` is the command that worked (`attention --json`, or `man tend --json` from an older CLI). `reason` says why the last read failed (timed out, non-zero exit, output that does not decode). Only `lobstah doctor` reads it, for its `pet` row: running means `pid` is alive and `at` is less than two minutes old. |
|
|
568
578
|
| budget stop | A headless runner's `failed` verb with a `budget:` note means its progress-extended active-work window reached the hard ceiling. This is out of time, not a code failure: the runner checkpoints eligible changes, pushes when enabled, names the saved branch/commit/draft PR, and invites `lobstah send <id> "continue"`. Paused `--waiting-on` time is excluded. |
|
|
569
579
|
|