lobstah 0.5.13 → 0.6.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/docs/man.md CHANGED
@@ -54,12 +54,13 @@ the background, dispatch it with the `lobstah` CLI instead of doing it inline:
54
54
  - `lobstah status [<id>]`, `lobstah ls` — check progress when I ask, not on a loop.
55
55
  - `lobstah send <id> "<instruction>"` — steer a live dispatch, add to a queued
56
56
  dispatch's inbox, or wake a finished chain as a follow-up. Sending to any
57
- member of a finished chain follows up its newest member. Use `--no-wake` to
58
- leave a message in a finished inbox without starting work (nothing reads it).
59
- A new follow-up accepts `--harness`, `--model`, and `--for wt:<trap>`.
57
+ member of a finished chain follows up its newest member. To choose a new
58
+ worker, harness, or model, use `lobstah dispatch --follow-up <id> --repo
59
+ <key> --brief-text "<instruction>" --for <name> --harness <kind> --model <m>`.
60
60
  - `lobstah cancel <id>` — stop one.
61
- - A dispatch reporting `needs-decision` is waiting on ME — surface its question
62
- immediately, then `lobstah send` my answer.
61
+ - A dispatch reporting `needs-decision` or `blocked`: when the brief or your
62
+ context gives the answer, `lobstah send` it and tell me what you decided;
63
+ when the choice is mine, frame it with `lobstah man ask <id>` for me to answer.
63
64
  - `done` means brief fulfilled with a branch + commits; report the evidence
64
65
  (`~/.lobstah/state/<id>.evidence`) and never merge anything yourself.
65
66
  ```
@@ -96,6 +97,11 @@ finished member. If that member was last claimed by a trap still signed on,
96
97
  the follow-up returns to that trap unless `--for` overrides it. Otherwise it
97
98
  is unaddressed for a headless worker.
98
99
 
100
+ A send to a dispatch expects a reply. The worker's next note after the send
101
+ wakes `man wait`: a `working` or `paused` note arrives once as a `reply` event
102
+ with the note and the sent instruction's first line, and any other verb wakes
103
+ as itself. `--no-reply` sends without expecting a reply.
104
+
99
105
  Attach refuses while a dispatch is `working` (two writers, one session);
100
106
  follow the logs or `send` instead, or cancel and then attach.
101
107
 
@@ -149,17 +155,57 @@ review, a deploy) reports `paused "<note>" --waiting-on review --link <url>`
149
155
  before it waits. Tend, `status`, `ls`, and the glass then show
150
156
  `paused: waiting on review` with the link and the time waited. It is a
151
157
  state, not a question: nothing to answer, no attention, no pet. A paused
152
- headless worker is never counted as wedged and its wall clock stops, but it
153
- still holds a `maxConcurrent` slot while its process is alive. A paused
154
- trap is kept out of the ghost sweep until `--until` or
155
- `[soak].pausedTtlSecs` (24 hours). See [Waiting on](vocabulary.md#waiting-on).
158
+ headless worker is never counted as wedged and its wall clock stops.
159
+
160
+ A paused headless dispatch is **parked**. When the worker's turn ends on
161
+ `paused`, the runner ends the session, stops what the harness started,
162
+ and exits. The dispatch stays active and keeps its worktree lock, but it
163
+ holds no `maxConcurrent` slot and runs no process. It wakes into the same
164
+ session when an operator message reaches its inbox (`lobstah send <id>`)
165
+ or when its `--until` time passes, as soon as a slot is free; a waking
166
+ dispatch takes the slot before queued work. The first prompt of the woken
167
+ session says why it woke and carries the messages. A parked dispatch
168
+ counts in the status output, without a slot:
169
+
170
+ ```
171
+ $ lobstah man tend
172
+ active: headless: 1 of 2; traps: 0; parked: 1 (no slot)
173
+ parked (no slot)[1]{id,waitingOn,for,link,note}:
174
+ 6a1f0c2e,review,80m,https://github.com/o/r/pull/7,waiting for approval
175
+ $ lobstah daemon status
176
+ slots: 1 of 2 work in use, 1 free
177
+ parked: 1
178
+ parkedOn: 6a1f0c2e waiting on review for 80m https://github.com/o/r/pull/7
179
+ ```
180
+
181
+ `lobstah doctor`'s `daemon` row ends in `parked: 1, no slot (…)`, and the
182
+ glass header shows `parked: 1 (no slot)`. A worker that reported `done` or
183
+ `failed` holds no slot: its runner exits within `[limits].exitGraceSecs`,
184
+ and a restart of the daemon does not wait for it (see [status
185
+ verbs](vocabulary.md#status-verbs)). A parked dispatch has no runner, so a
186
+ restart does not wait for it either. A paused trap is kept out of the ghost
187
+ sweep until `--until` or `[soak].pausedTtlSecs` (24 hours). See [Waiting
188
+ on](vocabulary.md#waiting-on).
189
+
190
+ **A PR's end finishes the waits on it.** When the PR a dispatch waits on
191
+ with `paused --waiting-on pr` or `--waiting-on review` merges, the daemon
192
+ finishes that dispatch `done` with the note `the PR merged: <url>`. When
193
+ the PR closes without merge, it finishes it `failed` with `the PR closed
194
+ without merge: <url>`. The PR waited on is the `--link` when it names a
195
+ GitHub PR, else the dispatch's own PR, else its chain's PR. Every paused
196
+ dispatch waiting on that PR is finished, in the whole chain. The daemon
197
+ does this after it observes PR watches and before its cull pass, so
198
+ `[limits].releaseOnMerge` releases the chain's worktrees in the same pass.
199
+ `report paused --waiting-on pr|review` registers the watch of the
200
+ dispatch's own PR when it has none, so the merge is observed.
156
201
 
157
202
  ### PR state after done
158
203
 
159
- A dispatch reports `done` when its PR opens; `report done --pr <url>`
204
+ A dispatch reports its PR with `report <id> <verb> --pr <url>`, which
160
205
  registers a `pr:` watch for the chain so the PR stays observed (see the
161
206
  PR preset in [vocabulary.md](vocabulary.md#the-pr-preset); `--no-watch`
162
- opts out). What you get depends on what runs:
207
+ opts out). A trap's PR is tracked from its first push. What you get
208
+ depends on what runs:
163
209
 
164
210
  - **Only the helm park or `man wait`** (no service): PR state badges in
165
211
  `man tend`, `lobstah catch`, and the glass (`merged`, `draft`, `review`,
@@ -177,15 +223,25 @@ succeeds and the watch's check records `lastError`.
177
223
 
178
224
  Which commands register a watch. Only these write points register one:
179
225
 
180
- - `lobstah report <id> done --pr <url>` registers the watch for the PR the
181
- worker just opened.
226
+ - `lobstah report <id> <verb> --pr <url>` registers the watch for the PR the
227
+ worker opened. Any verb but `failed` does this. A PR already watched is
228
+ not registered again.
229
+ - `lobstah report <id> paused --waiting-on pr|review` registers the watch
230
+ for the dispatch's own PR. A `--link` to another PR registers nothing.
231
+ - `lobstah soak beat` registers the watch for a trap's PR from its first
232
+ push. At most once a minute, it reads the trap's branch. When the branch
233
+ is not trunk and has an upstream, it asks `gh pr view <branch>` for the PR.
234
+ It records a new PR in the dispatch's evidence, where `lobstah catch` and
235
+ `lobstah status <id>` show it.
182
236
  - `lobstah watch add <key>` registers the watch you name.
183
237
  - `lobstah watch backfill --apply` registers watches for PRs in old dispatch
184
- history. Without `--apply` it only lists them. Nothing runs it for you.
238
+ history, and fetches the title of each PR record that has none (one
239
+ `gh pr view --json title` per record). Without `--apply` it only lists
240
+ them. Nothing runs it for you.
185
241
 
186
242
  Read commands never register a watch: `catch`, `man tend`, `status`, `ls`,
187
- `prs`, `prs sync`, `attention`, and the glass. They read PR records and
188
- watches that already exist. `prs sync` refreshes existing PR watches only.
243
+ `prs`, `attention`, and the glass. They read PR records and watches that
244
+ already exist. Use `watch check-pr <key>` to force one PR-watch refresh.
189
245
 
190
246
  The first check of a new watch is a baseline:
191
247
 
@@ -207,16 +263,99 @@ and requested review changes. It follows up the newest dispatch in the PR's
207
263
  chain. A conflict brief names the PR's base branch, including a stacked
208
264
  base. A check brief names each failed check and its details URL. The watch
209
265
  records each attempt and stops at `[watch].maxRepairsPerPr` per head SHA.
266
+ Each failing check gets at most one repair round per PR and head SHA; the
267
+ PR record's `repair.checks` lists the checks that had their round. The
268
+ same rule holds for CI-fix continuations from `lobstah pick`.
269
+
270
+ **Human gates.** A check that fails until a person approves the change is
271
+ a human gate. It gets no repair round and no CI-fix continuation on that
272
+ PR. Two sources name gates: `[repos.<key>].humanGateChecks` in the config,
273
+ and a worker's `lobstah report <id> <verb> --human-gate "<check>"` (once
274
+ per check). The report records the gate in the worker's evidence
275
+ (`humanGates`) and on the PR record (`humanGates`). A check brief lists
276
+ the failing gates and tells the worker to name a gate it finds. A PR whose
277
+ only failing checks are human gates shows `repair.status: waiting` with
278
+ `heldBy: human-gate`.
210
279
  It does not repair a PR with a person's newer commits or uncertain commit
211
280
  ownership, a terminal PR, or a PR whose chain already has queued or active
212
281
  work. `[watch].autoRepair`, `conflicts`, and `checks` control this behavior.
213
282
 
283
+ Daemon repairs run in the `chore` lane and use `[limits].choreConcurrent`.
284
+ A repair for a trap-built PR is addressed to its owning live trap. If the
285
+ trap is busy, the chore waits for up to `[watch].repairTrapWaitSecs`
286
+ (default 600). It then runs headless in its own checkout of the PR branch.
287
+ A repair for a headless-built PR runs headless and reuses its origin
288
+ worktree when that checkout is clean and free. A headless chore never uses
289
+ a trap's worktree. Only daemon-created repair chores have this bounded
290
+ addressed fallback. Work addressed by a person stays addressed until the
291
+ person releases or redirects it. `man tend`, `daemon status`, `doctor`,
292
+ and the glass show the repair's PR, chore lane, worker, and trap wait.
293
+
294
+ **A repair waits** while any of these is true:
295
+
296
+ - A live worker holds the PR's head branch. A live worker is an active
297
+ headless dispatch, or a trap with an open catch. It holds a branch when
298
+ its worktree has the branch checked out, when its current branch tracks
299
+ the branch on the remote, or when it pushed the branch during its current
300
+ dispatch. It holds a PR when its evidence names the PR or its chain owns
301
+ the PR.
302
+ - A live worker holds the head branch of an open PR below this PR in the
303
+ same stack. The stack is the one the glass shows: a PR's parent is the
304
+ PR whose head branch is its base branch.
305
+ - The PR's head, its base branch's head, or its failing checks changed less
306
+ than `[watch].repairSettleSecs` ago (default 600).
307
+ - For a checks repair: a fresh read of the latest run of each failing check
308
+ shows that run in progress or passed.
309
+ - The PR's watch is held. `lobstah cancel` on a repair dispatch holds its
310
+ PR's watch. `lobstah watch hold <key> [--for <id>]`
311
+ holds one PR's watch; with `--for`, the hold ends when that dispatch
312
+ ends. `lobstah watch release <key>` ends any hold.
313
+
314
+ A waiting repair is recorded on the PR record as `repair.status: waiting`,
315
+ with `heldBy` (`wt:<trap>`, `dispatch:<id8>`, `helm`, `hold`, `settle`,
316
+ `checks`, `human-gate`, or `repaired`) and `reason`. `repaired` means each
317
+ failing check already had its round at this head; a new commit ends it.
318
+ Unlike the other waits, `repaired` also raises `pr:checks` attention with
319
+ its reason: the round did not fix the check. A wait is not an attempt: it does not count against
320
+ `[watch].maxRepairsPerPr`. It raises no attention item. `man tend` lists it
321
+ in the `repairs waiting` table, the PR badge ends in `repair waits: <heldBy>`,
322
+ the glass PR modal shows the reason, and `lobstah doctor` lists it. When the
323
+ wait ends, the normal rules apply again.
324
+
325
+ **A repair pushes** to its PR's head branch, and so does a rebase chore
326
+ from pickup's merge loop. The descriptor of each names its PR (`pr`), so
327
+ the runner pushes no branch and opens no PR for it. The brief gives the
328
+ worker the push rule: push only to the PR's head branch. On a
329
+ non-fast-forward rejection, fetch the branch, rebase the commits onto the
330
+ moved head again, and push with `--force-with-lease` on the head just
331
+ fetched, at most three times. A push hook that fails with a real test or
332
+ type error is not retried: the worker fixes the error and pushes again.
333
+ For code already on main, the repair keeps main's version and only this
334
+ PR's own changes. It does not change behavior. If a conflict resolution
335
+ would change behavior, the worker reports `needs-decision`.
336
+ When the worker cannot push, it reports
337
+ `failed "push rejected: <rejection text>; moved head <sha>"` and leaves the
338
+ PR as it was. That report marks the PR's repair `blocked` at the moved head
339
+ (no new repair starts on that head, nor on the head the repair started
340
+ from) and posts a `push-failed` notice. A repair never opens a branch or a
341
+ PR.
342
+
343
+ Lobstah records pushes it sees in the dispatch's evidence (`pushes`): the
344
+ runner's own pushes, a headless worker's `git push` commands, and a trap's
345
+ `git push` commands (from its post-tool beat). It does not read GitHub to
346
+ guess who pushed. A worker that is about to push to other PRs can hold them
347
+ first with `lobstah watch hold <key> --for <its dispatch id>`.
348
+
214
349
  Watching a PR nobody dispatched — `lobstah watch add pr:<owner>/<repo>#<n>`
215
350
  with no `--for` — is how a helm follows a human's PR, or one whose
216
351
  dispatch chain was culled. Every observation lands in a PR record keyed by
217
352
  the PR, so it shows in the glass PRs tab and stacks and in tend's `pr:*`
218
353
  attention kinds exactly like a dispatched PR (its dispatch chain column is
219
- empty). It stays quiet while it's fine: only a failing check or a changes
354
+ empty). Each PR card, PRs tab row, and PR modal header shows the PR's title
355
+ after its number; a stack line shows numbers only, with each title on hover.
356
+ `lobstah prs` prints the title, cut to 60 characters. Every check reads the
357
+ title again, so a rename on GitHub shows on the next check and is never
358
+ attention. It stays quiet while it's fine: only a failing check or a changes
220
359
  request surfaces as a watch event; a merge or close arrives as a notice.
221
360
 
222
361
  **PR order.** Every PR list uses one order: the glass PRs tab, the On deck
@@ -241,6 +380,15 @@ See the [attention contract](vocabulary.md#attention-contract). A PR is
241
380
  something to look at, not a stall: it never flips the verdict to
242
381
  `needs-attention` and stays out of the digest.
243
382
 
383
+ A question is held while a helm is signed on for its grounds and has not
384
+ ended a turn since the question was filed. A held question is in `man tend`,
385
+ `man wait`, the park, and reminders, and `lobstah attention` and `man tend`
386
+ mark it `held`. It is not in `attention --json` (the pet), the glass, or
387
+ notifyCommand. The question walks when the helm ends a turn (`man haul`)
388
+ without answering it; the release is recorded in `releases/<key>.json`. With
389
+ no helm signed on, or a helm relieved or stale past `[helm].ttlSecs`, a
390
+ question walks at once.
391
+
244
392
  ### The spyglass
245
393
 
246
394
  The lobstah man skill brings up the glass when it takes the helm.
@@ -250,43 +398,195 @@ as a user service. `lobstah glass restart` restarts the service, or a
250
398
  detached glass (stop, then `--detach`).
251
399
 
252
400
  `lobstah glass [--port <n>]` serves tend as a live web page on 127.0.0.1
253
- (default port 4949): the fleet verdict and attention questions, every
401
+ (default port 4949): the fleet verdict, decision cards, every
254
402
  dispatch with its full brief, status log, inbox, and evidence, each trap
255
403
  with its lifecycle notices, message history, and catches, the notices
256
404
  tail, the merge view, and watches — with filters, a table/cards toggle,
257
- and the helm identified by name. It is strictly read-only and consumes no
258
- cursor: looking through the glass changes nothing, so it needs no helm and
259
- threatens nothing. Links out are copyable commands (`lobstah attach`,
260
- `claude --resume`), never click-to-exec — localhost HTTP is reachable by
261
- any webpage, so the glass exposes no endpoint that acts. The ⚙ popover's two
405
+ and the helm identified by name. Its tabs are On deck, Dispatches, Traps,
406
+ PRs, Reports, and Notices. The Reports tab lists every report, unacked first,
407
+ then newest first, and the repo filter and the search box (title, author,
408
+ dispatch id, repo) apply to it. Reading the glass consumes no cursor. Each
409
+ live trap has a **↗ open** button at the end of its foot line. It asks the local server to focus
410
+ that trap's reported session link, iTerm2 session, Terminal tab, editor
411
+ worktree, or recorded app, in that order. The result names the step that
412
+ worked; app-only activation says the exact window is not known. On other
413
+ platforms, only a session link can open. A signed-off trap has no button; the
414
+ page shows its resume command as text to copy. The focus endpoint accepts
415
+ only a trap id in a same-origin, token-protected POST. The ⚙ popover's two
262
416
  preferences — table or cards, and whether lobsters crawl the page — are
263
417
  per-browser, kept in that browser's localStorage and never on disk.
264
418
 
419
+ On a card, the title and the badge share one line. The title shrinks first,
420
+ with an ellipsis, but always keeps its PR number or 8-character id. A badge of
421
+ 12 characters or fewer always shows whole. A longer badge truncates only
422
+ after the title is at its minimum, never below 12 characters. The meta line
423
+ and the note show at most two lines. The full text of each is in its hover
424
+ title.
425
+
426
+ The On deck tab shows up to 8 traps. Signed-on traps come first, oldest
427
+ sign-on first, with name breaking ties. Traps stowed or ghosted in the last
428
+ hour follow, most recent sign-off first, with name breaking ties. A "+N more"
429
+ link opens the traps tab for the rest, in the same order. Both views show each
430
+ trap's current dispatch, last activity, or idle and waiting state. A dot leads
431
+ that state line on the deck, the traps tab cards, and the traps tab table. The
432
+ dot is green when the trap is working or idle and listening. It is amber when
433
+ the trap is parked. It is grey when the trap is not listening (its heartbeat is
434
+ stale) or signed off.
435
+
265
436
  This is where "is the agent alive?" belongs: the helm's heartbeat age on a
266
437
  page, not periodic proof-of-life turns in a transcript.
267
438
 
439
+ ### Reports
440
+
441
+ A report is a markdown page of findings, with images, that a dispatch or
442
+ the helm files to keep.
443
+
444
+ - **Filing a worker report.** `lobstah report <id> done "<one-line note>"
445
+ --report <file.md> [--attach <file> ...]` (also `failed`). The page is
446
+ copied to the dispatch's state directory as `state/<id>/report.md`, beside
447
+ `attachments/`. `--attach` copies each file into that `attachments/`. An
448
+ image the page names by bare filename (`![tray](tray.png)`) resolves to
449
+ that directory. The note stays one line.
450
+ - **Filing a helm report.** `lobstah man file <file.md> [--attach <file> ...]
451
+ [--title <text>]`. It is stored under the helm's grounds,
452
+ `reports/<grounds>/<rid>/`, with the same layout, and the author is `helm`.
453
+ - **Title and author.** The title is `--title`, else the page's first `#`
454
+ heading, else the dispatch's brief title. The author is the trap name, or
455
+ `headless`, or `helm`. A file larger than `[limits].attachmentMaxBytes` is
456
+ refused, and nothing is filed.
457
+ - **Finding one.** `lobstah reports` lists every report, newest first: key,
458
+ title, author, the dispatch or helm grounds, when it was filed, and
459
+ whether it is acked. `lobstah status <id>` and `lobstah catch <id>` print
460
+ `report: <path>` when the dispatch has one.
461
+ - **In the glass.** The deck has a reports block after Landed: newest first,
462
+ unacked first, up to 8, then "+N more", which opens the Reports tab. A
463
+ report's card or row shows its title, then who filed it (a trap's name, a
464
+ headless dispatch's id, nothing for the helm), its age, and `acked`. A
465
+ dispatch's report renders as a
466
+ page at the top of its dispatch modal. A helm report opens in a modal of
467
+ its own; `#report/<key>` links to it. The page shows headings, lists,
468
+ tables, fenced code, links (in a new tab), bold, italics, and images.
469
+ Raw HTML in the markdown shows as text. The glass serves the markdown and
470
+ the images read-only, and an image only by basename from that report's
471
+ own attachments.
472
+ - **Images.** Every image in the glass (a decision card's, a report page's,
473
+ and an attachment's in the dispatch and trap modals) opens in an in-page
474
+ overlay: centered at up to 90% of the viewport over a dark backdrop, with
475
+ a link to open the original file. Escape, a click on the backdrop, or the
476
+ close button closes it. The glass serves a dispatch's and a trap's
477
+ attachment images read-only, by basename, from their own attachments
478
+ directories. Missing files, malformed names, and symlinks are not served.
479
+ - **Attention.** A filed report with no ack stands as the `report` attention
480
+ kind. Add `report` to `attentionKinds` to walk it. The desktop pet shows
481
+ the report's title; a pet click opens the item and acks it through
482
+ `lobstah attention ack <key> --by pet`. The pet's Acknowledge menu entry
483
+ acks without opening, and `lobstah attention ack <key>` acks a report
484
+ whether or not `report` is in `attentionKinds`. Opening a report in the
485
+ glass does not ack it. A report filed by a follow-up dispatch acks the report
486
+ of each dispatch before it in the chain.
487
+ - **Cull.** `lobstah cull` removes a dispatch's report with the rest of its
488
+ state. A helm report is culled when it is older than the retention window,
489
+ counted from when it was filed.
490
+
491
+ ### Decisions
492
+
493
+ A decision is a question the helm puts to the human. Workers still ask in
494
+ prose (`report needs-decision "<note>"`). The helm decides first. When the
495
+ brief, the code, or its context gives the answer, it sends it with
496
+ `lobstah send <id> "<answer>"` and says what it decided in its next report.
497
+ When it lacks the context, or the choice is the human's (scope, product
498
+ behavior, money, releases, merges, anything outward-facing or hard to
499
+ undo), it frames a decision with `man ask`. The helm's own questions follow
500
+ the same rule.
501
+
502
+ - **Asking.** `lobstah man ask [<dispatch-id>] --title "<question>"
503
+ [--detail <file.md>] [--option "<label>"]... [--attach <file>]...` stores a
504
+ decision: its key (`decision:<rid>`), title, detail markdown (at most
505
+ 64 KiB), 0 to 6 option labels, attachments, the dispatch it is about (none
506
+ for a question such as "cut 0.6.0?"), who asked (`helm`), and when. It is
507
+ stored in `~/.lobstah/decisions/<rid>/`: `decision.json`, `detail.md`, and
508
+ `attachments/`. The claimed helm alone may ask.
509
+ - **Standing.** A decision stands until it is answered or the helm withdraws
510
+ it with `lobstah man ask --withdraw <key>`, which removes its directory. A
511
+ newer `man ask` on the same dispatch replaces the older decision.
512
+ - **Attention.** A standing decision is attention of kind `decision`, in
513
+ the default `attentionKinds`. It makes the verdict `needs-attention`. A
514
+ worker's raw `needs-decision` or `blocked` stays a `question` until the
515
+ helm frames it. While a decision on the same dispatch, asked at or after
516
+ the question, is on disk, the `question` item is hidden: the decision
517
+ replaces it. Withdrawing the decision shows the question again.
518
+ - **In the glass.** The attention section of On deck is a list of
519
+ full-width decision cards, newest first. A card shows the title, the
520
+ detail rendered as markdown, the attachments (images inline), the
521
+ dispatch link and repo, and the age. The options are buttons. Below them
522
+ is an empty text box that grows with its content, and an attach control
523
+ for images and files (`.png .jpg .jpeg .gif .webp .pdf .txt .md .csv
524
+ .json .log .diff .patch .yaml .yml .toml .zip`, each at most
525
+ `[limits].attachmentMaxBytes`, at most 8). Click an option, write an
526
+ answer, attach files, or any mix; one **Send** submits it. With no options,
527
+ the text box is the whole answer. After sending, the card shows
528
+ `answered · <what was chosen>` and leaves on the next refresh. A raw
529
+ `question` the helm has not framed shows as a plain card with the
530
+ worker's note and the same text box. PR kinds stay in the PRs section.
531
+ `#decision/<key>` opens the deck at that card. Pasting with Cmd-V or
532
+ Ctrl-V in the text box adds each image on the clipboard as an attachment
533
+ named `pasted-<time>.png` (the extension follows the image format), with
534
+ the same size, type, and count checks as a picked file; pasted text stays
535
+ text.
536
+ - **The answer is a request.** An answer is a glass request of kind
537
+ `decision-answer`, stored as `~/.lobstah/requests/<id>.json` with its
538
+ files in `~/.lobstah/requests/<id>/`. The payload is the decision `key`,
539
+ `title`, `dispatch`, `lane`, `repo`, the `option`, the `text`, and the
540
+ stored `attachments`. `answer.json` in the decision's directory marks it
541
+ answered and names the request.
542
+ - **The POST.** Send is a same-origin POST to `/requests` with the page's
543
+ token (header `x-lobstah-token`), the guard the **↗ open** button uses.
544
+ The body is `{ "kind": "decision-answer", "payload": { "key", "option",
545
+ "text", "files": [{ "name", "data" }] } }`, with file data in base64. The
546
+ server checks the kind, the key, that the option is one of the decision's
547
+ labels, the text length (at most 20000 characters), and each file's size
548
+ and type (an image must also start with its format's signature). It
549
+ writes the request and runs nothing. Answering a raw question's key
550
+ (`<lane>:<id>`) first stores it as a decision asked by `worker`.
551
+ - **The event.** Writing the request posts a `decision-answer` notice. It
552
+ wakes the helm's `man wait` (and the Stop-hook park) once, as a
553
+ `decision-answer` event: the request `id`, `key`, `dispatch`, `title`,
554
+ `option`, `text`, `attachments` (the stored paths), `from`, and `at`. The
555
+ helm acts on it, usually with `lobstah send <dispatch> "<instruction>"`.
556
+ Answering does not message the worker.
557
+ - **From the terminal.** `lobstah man answer <key> [--option <label>]
558
+ [--text <text>] [--attach <file>]...` answers the same way, with the same
559
+ checks.
560
+ - **The pet.** The desktop pet shows a decision as its title only. A click
561
+ opens the glass at the card and acks the item for the pet
562
+ (`lobstah attention ack <key> --by pet`). The card stays in the glass until
563
+ the decision is answered or withdrawn.
564
+ - **Cull.** `lobstah cull` removes a decision once its answer is older than
565
+ the retention window, with the `decision-answer` request and its files. A
566
+ standing decision is never culled.
567
+
268
568
  ### The periodic report
269
569
 
270
570
  `lobstah man tend` is the full picture on demand; `lobstah man report` is the
271
571
  **delta** since the last acknowledged report — catches landed (with their
272
572
  notes and PRs), attention newly arisen, what still waits, and the fleet
273
- verdict. It advances a "reported through" cursor when it prints — the
274
- explicit acknowledgment — so nothing is ever reported twice, and it says
573
+ verdict. It advances a "reported through" cursor when it prints, so nothing
574
+ is ever reported twice, and it says
275
575
  `no change` when the delta is empty rather than re-dumping state. Standing
276
576
  unanswered questions appear under `still-waiting` without counting as
277
577
  change — reminders (`remindSecs`) own re-firing those.
278
578
 
279
- Delivery is at-least-once by construction: the carriers that might not be
280
- read (a `man wait` timeout in a background task) only **peek** at the delta,
281
- so a digest lost with a dead task re-surfaces on the next timeout; only
282
- `man report` (or a hook-delivered park digest, which lands in-context by
283
- construction) marks it handled.
579
+ A catch is reported once the helm's `man wait` watcher delivers its event or
580
+ `man report` prints it. The glass's `unreported` badge means no helm received
581
+ that catch. A `man wait` timeout and `man wait --peek` only peek at the delta;
582
+ a digest lost with a dead background task re-surfaces on the next timeout.
583
+ The Stop-hook's standing-attention reminder does not mark a catch reported.
284
584
 
285
585
  Every carrier shares the cursor (per grounds, for a helm):
286
586
 
287
- - **The wait loop.** A `man wait` timeout (exit 3) prints the delta when
288
- something changed, so a looping session gets periodic fleet reports for
289
- free — see the loop idiom below.
587
+ - **The wait loop.** A delivered `man wait` event (exit 0) advances the helm's
588
+ cursor through the event time. A timeout (exit 3) prints the delta when
589
+ something changed without advancing the cursor — see the loop idiom below.
290
590
  - **The blocking park.** A helm session's Stop-hook park delivers the digest as a wake
291
591
  at `[helm].reportSecs` cadence — including the landed-then-idle case, where
292
592
  the last catches finish and nothing is left in flight to wake for.
@@ -357,6 +657,12 @@ before the worker reads it; `man tend` then shows the dispatch as
357
657
  A newer `needs-decision` from the worker stands again. Set `remindSecs = 0`
358
658
  for pure at-most-once.
359
659
 
660
+ A send still waiting on its reply stands in the `man haul` block as
661
+ `sent · <id> · <first line> · <age>`. It is listed once, then every
662
+ `remindSecs` until the worker's next note answers it. `man tend` shows it on
663
+ the dispatch as `awaiting reply · <age>`, and the glass dispatch modal shows
664
+ it under the inbox.
665
+
360
666
  **Acknowledging, display-only.** Clicking a desktop pet opens its target and
361
667
  runs `lobstah attention ack <item-key> --by pet`, so that pet stops walking
362
668
  the item until its state changes (a new status entry, head, failed check, or
@@ -486,6 +792,8 @@ lobstah soak --wait # hookless sessions: listen in the foreground
486
792
  # (re-runs need no flags — identity is the
487
793
  # worktree, else the session id); exit 3 =
488
794
  # quiet, run it again
795
+ lobstah soak --link <url> # store this session's link for the glass's ↗ open button
796
+ lobstah focus <trap> # focus a live trap from the CLI
489
797
  lobstah stow # sign off; an open catch requeues, unread
490
798
  # messages bounce back to the helm; removes
491
799
  # the worktree when soak created it
@@ -493,6 +801,14 @@ lobstah stow --keep # sign off and keep the worktree
493
801
  lobstah soak --name amber-gull # choose or change this trap's two-word name
494
802
  ```
495
803
 
804
+ `--link` accepts a Claude desktop session URL under `claude://claude.ai/`,
805
+ a VS Code extension URL of the form
806
+ `vscode://anthropic.claude-code/open?session=<id>`, or a Codex task URL of
807
+ the form `codex://threads/<task-id>`. It rejects all other schemes and
808
+ malformed links. The glass checks a stored link again before rendering it.
809
+ `lobstah focus <trap>` accepts the trap id with or without `wt:` and reports
810
+ the focus step or why it could not focus. It does not revive a signed-off trap.
811
+
496
812
  **Soak can create the worktree.** From a repo's primary checkout, or with
497
813
  `--repo <key>` from outside any configured repo, soak creates a linked
498
814
  worktree the same way a dispatch does. It fetches trunk, adds
@@ -523,8 +839,9 @@ worktree's HEAD commit and branch in evidence.
523
839
  `stow` removes a worktree only when soak created it and nothing in it exists
524
840
  elsewhere. It keeps the worktree and prints `worktree: kept` and a `reason:`
525
841
  when soak did not create the worktree, or when the worktree has uncommitted
526
- changes, untracked files that are not ignored, or commits on no remote
527
- branch. Ignored files do not block removal. Stow never forces a removal.
842
+ changes, untracked files that are not ignored, commits its upstream lacks,
843
+ or no upstream. Ignored files do not block removal. `stow --force` explicitly
844
+ allows removal of unsaved checkout files; `--keep` still keeps the checkout.
528
845
  Stow runs the removal from the primary checkout, so it works from inside
529
846
  the worktree. On removal it prints `worktree: removed`, `path:`, and
530
847
  `returnTo: <primary checkout>`, with a help line `cd <primary>`. It
@@ -533,6 +850,8 @@ upstream: on some remote branch);
533
850
  otherwise it prints `branchKept: <branch> (<reason>)`. A deleted branch
534
851
  prints as `branchDeleted:`. `stow --wt <id>` follows the same rules. The
535
852
  SessionEnd hook (`lobstah stow --quiet`) signs off and keeps the worktree.
853
+ Releasing a claim whose last report is `done` or `failed` moves it to `done/`;
854
+ only an unfinished catch requeues (a cancelled catch finalizes as failed).
536
855
 
537
856
  **Identity is the worktree.** Sign-on anchors a short trap id and two-word
538
857
  name in `.lobstah-trap` and prints both, such as `amber-gull (wt:c32a245d)`; the address
@@ -545,6 +864,15 @@ same live trap in `dispatch --for`, `send`, and `stow --wt`. Unknown names
545
864
  list known names and never turn addressed bait into headless work. The id
546
865
  remains the key in dispatch and claim records.
547
866
 
867
+ A dispatch shows its trap by name wherever it names the trap that claimed
868
+ it, ran it, or is addressed to it, including notes such as
869
+ `claimed by crisp-heron`. The name comes from the live registration, else
870
+ from the name registry, so a signed-off trap keeps its name. A trap with no
871
+ known name shows as `wt:<id>`. `lobstah status <id>` and `lobstah catch <id>`
872
+ print `trap: crisp-heron (wt:68c5da5f)` and write notes the same way.
873
+ `man tend`'s tables print the name alone. In the glass, the name carries
874
+ `wt:<id>` as its hover text, and a click opens that trap's modal.
875
+
548
876
  The session id (from the plugin's session-start brief) lives inside the
549
877
  registration as the liveness principal. The harness (claude or codex) is
550
878
  inferred — from `CLAUDE*` / `CODEX*` in the environment, and when both are
@@ -574,14 +902,118 @@ resolves the trap from the working directory, else from the session id. A
574
902
  trap that works for an hour without reporting is not swept while it beats.
575
903
  `[soak].beat = false` turns the hook off.
576
904
 
905
+ `lobstah soak` prints `title: <trap name>` at sign-on. When `soak --wait`
906
+ delivers work, it prints `title: <name> · <short first brief line>`.
907
+ `report done` and `report failed` print `title: <trap name>` again. The
908
+ brief text has control and terminal escape sequences removed and is capped
909
+ at 40 characters. A trap skill sets an available session-title tool to each
910
+ printed title, without retrying a refusal. Stow leaves the current title
911
+ in place.
912
+
577
913
  Liveness has two failure shapes with two remedies: a registration that
578
914
  parked before and went quiet (no park, report, or beat) past `[soak].ttlSecs` is a **ghost trap** —
579
- swept, catch requeued, noticed, its worktree kept; one that **never parked** is a **defective
915
+ swept and noticed. An unfinished catch requeues; a `done` or `failed` catch
916
+ finalizes in `done/`, never becoming orphaned bait. A soak-created worktree
917
+ is removed only when Git verifies a clean checkout with no commits absent
918
+ from its upstream. Dirty, unpushed, no-upstream, or unreadable checkouts stay.
919
+ The ghost notice includes the path, branch, modified-file count and
920
+ unpushed-commit count (or `unknown` when inspection fails). Other worktrees
921
+ stay in place. After the daemon's own tick gap exceeds `[soak].ttlSecs`,
922
+ it grants a full TTL after resume before sweeping any traps, allowing
923
+ sessions to renew their heartbeats. One that **never parked** is a **defective
580
924
  enlistment** — noticed with its diagnosis (usually a missing Stop hook →
581
925
  `soak --wait`) and left standing so the address keeps protecting its work.
582
926
  Nobody is conscripted: only a worktree whose session ran `soak` ever
583
927
  receives work.
584
928
 
929
+ ### Reserving a trap before its session starts
930
+
931
+ ```bash
932
+ lobstah trap reserve --repo <key> # reserve a trap; prints its name, id, and a one-time ticket
933
+ [--harness claude|codex] # print only that harness's start command
934
+ [--name amber-gull] # choose the name
935
+ [--deadline 180] # seconds the session has to sign on (default 180)
936
+ lobstah soak --ticket <ticket> # in the new session: sign on as the reserved trap
937
+ lobstah stow --wt amber-gull # withdraw a reservation no session has redeemed
938
+ ```
939
+
940
+ `trap reserve` picks the two-word name and the `wt:` id before any session
941
+ exists and writes a **starting** reservation (`soaking/<id>.starting`) with a
942
+ deadline. `dispatch --for <name>` works on it at once: the work waits, as it
943
+ does for any addressed trap. `man tend` and the glass show the trap as
944
+ `starting`.
945
+
946
+ The output holds a one-time ticket and the command that starts the session in
947
+ the repo's primary checkout:
948
+
949
+ ```bash
950
+ cd <repo> && CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 claude "/lobstah:soak --ticket <ticket>"
951
+ cd <repo> && codex '$lobstah:trap soak --ticket <ticket>'
952
+ ```
953
+
954
+ Nothing starts the session for you: a person runs the command. The session's
955
+ soak redeems the ticket, from `--ticket` or from the `LOBSTAH_TRAP_TICKET`
956
+ environment variable. It creates a worktree named after the reserved id,
957
+ signs on under the reserved name and id, and deletes the reservation. The
958
+ ticket then redeems nothing. A spent ticket left in `LOBSTAH_TRAP_TICKET` is
959
+ ignored; a spent `--ticket` is refused, except in the session that redeemed
960
+ it. A session that already mans a trap cannot redeem a ticket.
961
+
962
+ A reservation still unredeemed at its deadline **fails**: the daemon posts one
963
+ `trap-start-failed` notice, and the glass shows the trap as `start failed`
964
+ with the reason. Work addressed to it stays queued. The ticket still redeems
965
+ after the deadline. `lobstah stow --wt <name>` withdraws the reservation; its
966
+ addressed work is then orphaned bait and the helm gets a `bait-orphaned`
967
+ notice.
968
+
969
+ ### Asking for a trap from the glass
970
+
971
+ The Traps tab and the deck's traps block have a **+ New trap** button. It
972
+ opens a small form: a repo (from the configured repo keys) and a harness
973
+ (`claude` or `codex`). Submitting it files a **trap request**: the glass POSTs
974
+ `{ kind: "trap-request", payload: { repo, harness } }` to `/requests` with
975
+ the same same-origin and page token guard as the open-window button. The
976
+ server checks the repo and harness, writes `~/.lobstah/requests/<id>.json`
977
+ (kind `trap-request`), and runs nothing. `/requests` refuses a body larger
978
+ than eight attachments (`[limits].attachmentMaxBytes` each, as base64) plus
979
+ room for text, with 413. After it reads the kind, it refuses a `trap-request`
980
+ larger than 4 KB with 413.
981
+
982
+ A request wakes the helm's `man wait` (and the Stop-hook park) as a
983
+ `trap-request` event with the request's id, repo, and harness. An open
984
+ request also wakes a helm that signs on later. The glass shows it at once as a
985
+ greyed card: `requested · <repo> · <harness> · waiting for the helm`, or
986
+ `waiting for a helm` when no helm is signed on.
987
+
988
+ ```bash
989
+ lobstah trap requests # open trap requests
990
+ lobstah trap reserve --request <id> # reserve what the request asks for, and close it
991
+ ```
992
+
993
+ `trap reserve --request <id>` takes the repo and harness from the request,
994
+ records the request on the reservation, and closes the request. The card
995
+ becomes the starting card, then the live trap when the session signs on.
996
+
997
+ Every starting card shows the start command `trap reserve` prints, with a copy
998
+ button: paste it into a terminal to start the session by hand. The ticket in
999
+ it is kept in `soaking/<id>.ticket` (mode 0600) until the reservation is
1000
+ redeemed or withdrawn. The glass sends it only to a page on this machine's
1001
+ own glass address, only on the starting card, and no notice or log carries
1002
+ it.
1003
+
1004
+ ### The terminal tab name
1005
+
1006
+ At sign-on, soak names the session's terminal tab after the trap. It finds the
1007
+ tab by the tty recorded in the registration's window: a Terminal.app tab gets
1008
+ the name as its custom title, an iTerm2 session gets it as its session name.
1009
+ Other terminals are left alone. `lobstah stow` clears the name.
1010
+ `LOBSTAH_TERMINAL_TITLE=0` turns naming off.
1011
+
1012
+ Claude Code writes its own title to the tab, and in Terminal.app that title
1013
+ replaces the custom title. `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` on the
1014
+ command that starts Claude Code stops that for that one process; the start
1015
+ command `trap reserve` prints sets it. Codex also sets the terminal title.
1016
+
585
1017
  ### Culling and disk space
586
1018
 
587
1019
  Worktrees are 1 to 8 GB each. `lobstah cull` sweeps what is finished: `done/`
@@ -590,9 +1022,9 @@ whose dispatch is finished or gone, stale state files, merged or closed PR
590
1022
  records, and orphaned acks. It never touches queued or active work, and
591
1023
  `git worktree remove` keeps each dispatch's branch. A worktree that soak
592
1024
  created counts as in use while a trap registration anchors it. After that,
593
- the cull and the daemon's retention and free-space culls treat it like any
594
- other worktree that no dispatch owns: it ages out after
595
- `[limits].retentionDays`. A worktree that follow-ups
1025
+ the cull and the daemon's retention and free-space culls can remove it only
1026
+ when the same clean-and-pushed safety check passes; unsaved soak checkouts
1027
+ remain protected even without a registration. A worktree that follow-ups
596
1028
  reused is one worktree shared by the chain: it stays while any dispatch in
597
1029
  the chain is queued or active, and it ages from the newest dispatch that
598
1030
  used it.