lobstah 0.5.11 → 0.5.13

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.
@@ -14,7 +14,7 @@ keys of that section.
14
14
  |---|---|---|
15
15
  | `notifyCommand` | — | Exec'd by the daemon on wake-worthy status transitions with `LOBSTAH_ID`, `LOBSTAH_LANE`, `LOBSTAH_VERB`, `LOBSTAH_NOTE`, `LOBSTAH_AT` in the environment. Fire-and-forget; point it at ntfy, a Slack helper, anything. |
16
16
  | `notifyVerbs` | `["needs-decision", "blocked", "done", "failed"]` | Which verbs fire `notifyCommand`. |
17
- | `attentionKinds` | `["question", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready"]` | Which attention kinds `man tend` lists — and so what the desktop pet and the glass walk across the screen. Valid kinds: `question`, `landed` (opt-in), `pr:draft`, `pr:review`, `pr:checks`, `pr:conflict`, `pr:ready`; an unknown kind is a config error naming the valid set. Notify is edge-triggered and fires once per transition; attention is level-triggered and stands until its clear condition ([vocabulary.md](vocabulary.md#attention-contract)). |
17
+ | `attentionKinds` | `["question", "pr:ready", "pr:review", "pr:conflict", "pr:checks"]` | Which kinds `man tend`, the glass, and the desktop pet show. `pr:draft` and `landed` 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
19
 
20
20
  ## `[repos.<key>]` — workspace definitions
@@ -26,9 +26,11 @@ The descriptor's `repo` field resolves here; the key is what dispatchers name.
26
26
  | `path` | yes | The git clone worktrees are allocated from (`~/` expands). |
27
27
  | `trunk` | yes (default `main`) | Branch dispatches start from (`origin/<trunk>`). |
28
28
  | `origin` | no | Enables clone-on-first-use when `path` doesn't exist. |
29
- | `setup` | no | Commands run in each fresh worktree, in order (e.g. `["pnpm install"]`). |
29
+ | `setup` | no | Commands run in each fresh worktree, in order (e.g. `["pnpm install"]`). A follow-up that reuses its origin's worktree does not run them again unless a lockfile at the worktree root (`pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `Cargo.lock`, `go.sum`, and others) or the commands changed since they last ran there. |
30
+ | `scratch` | no | Repo-relative paths (e.g. `["tmp", ".cache"]`) whose untracked files do not count as uncommitted changes when a follow-up decides whether to reuse its origin's worktree. Tracked changes anywhere, and untracked files elsewhere, still do. |
30
31
  | `env` | no | Environment merged into every dispatch for this repo. |
31
32
  | `pickup` | no (`false`) | Opt this repo into `[pickup.github]` multi-repo mode. Explicit per repo — nothing becomes pickable by being configured. |
33
+ | `pushEarly`, `draftPr`, `checkpointOnStop` | no (inherit `[limits]`) | Override remote preservation for this repo's headless dispatches. |
32
34
 
33
35
  `[repos.<key>.harness]` — per-repo harness defaults: `default` (`claude` \|
34
36
  `codex`), `model`, `effort`.
@@ -53,15 +55,28 @@ Same three keys as the per-repo block. Precedence for every harness setting:
53
55
 
54
56
  | Key | Default | Meaning |
55
57
  |---|---|---|
56
- | `maxConcurrent` | `2` | Work-lane dispatches running at once. |
57
- | `choreConcurrent` | `1` | Chore-lane ceiling (rebases and other machine-originated runs). |
58
- | `wedgeThresholdSecs` | `600` | No tool activity for this long while alive = wedged → killed and forked with a nudge. |
58
+ | `maxConcurrent` | `2` | Headless work-lane runners the daemon may run at once. Trap-claimed catches use their own sessions and do not spend these slots. |
59
+ | `choreConcurrent` | `1` | Headless chore-lane runner ceiling (rebases and other machine-originated runs). |
60
+ | `wedgeThresholdSecs` | `600` | No tool activity for this long while alive = wedged → killed and forked with a nudge. Also the age past which `status`, `ls`, `man tend`, and the glass show a dispatch's activity line as stale. |
59
61
  | `maxRestartAttempts` | `2` | Bounded restart ladder for dead and wedged runners. |
60
- | `wallClockSecs` | `3600` | Hard per-dispatch ceiling, enforced by the runner. |
62
+ | `wallClockSecs` | `3600` | Initial active-work window. Progress extends it, up to `maxWallClockSecs`; time paused with `report paused --waiting-on` does not count. |
63
+ | `maxWallClockSecs` | `4 × wallClockSecs` | Hard active-work ceiling across restarts. |
64
+ | `pushEarly` | `true` | For a new chain, push each new committed HEAD to its non-trunk branch on `origin` within 10 seconds. A rejected push is noted and retried only after HEAD moves. A follow-up whose chain already has a PR does not push automatically; its worker pushes to the existing PR's head branch. |
65
+ | `draftPr` | `true` | For a new chain, after first push, adopt an existing PR or open one draft PR when `gh` is available. A follow-up with a chain PR keeps that PR and its watch; it does not open another. |
66
+ | `checkpointOnStop` | `true` | Before a nonterminal stop, checkpoint eligible tracked and untracked files. A new chain then pushes; a follow-up with a chain PR leaves the checkpoint local for its worker to push. Ignored files and secret/build denylist paths are excluded. Set all three switches to `false` for prior runner behavior. |
67
+
68
+ A runner extends its active-work window when a fresh activity event or new HEAD
69
+ shows progress at the boundary. The elapsed budget and current window are
70
+ persisted across restarts; a pause with `--waiting-on` does not spend active
71
+ time. At the hard ceiling, the status verb remains `failed` for compatibility,
72
+ but its note starts `budget:` and tells the man what work was saved and to
73
+ send a continuation.
61
74
  | `backgroundWaitSecs` | `1800` | A turn that ends without a report is held open this long while background work the worker started is still running (a push behind a slow pre-push gate); the harness wakes the worker when it settles. Heartbeats keep the wedge detector off the wait. Keep it below `wallClockSecs`, which still ends the run. |
62
75
  | `choreRetentionDays` | `7` | Completed chores age out of `chores/done/`. |
63
76
  | `attachmentMaxBytes` | `26214400` (25 MiB) | Maximum size of each file supplied with repeatable `dispatch --attach` or `send --attach`. |
64
77
  | `retentionDays` | `0` (off) | The daemon culls finished dispatches (done and failed) older than this many days: their `done/` entries, worktrees, state files, and stale PR records and acks. Branches are kept. A dispatch whose PR is still open is kept. Queued and active dispatches are never culled. The pass runs at most once per hour and culls at most 10 dispatches per pass, so it cannot stall the claim loop. Suggested: `14`, the same window as `lobstah cull`. |
78
+ | `releaseOnMerge` | `false` (off) | When a PR watch records `merged` for a PR, the daemon's next cull pass removes the worktree of the dispatch that owns the PR, and of every dispatch in its follow-up chain that ran on that PR (its own worktree or a shared one). It uses the retention cull's removal path, its hourly throttle, and its limit of 10 per pass, and runs even when `retentionDays` is `0`. It releases a worktree only when every dispatch in the chain is finished (done or failed) and none is queued or active, `git status --porcelain` is empty, and after a fetch `git branch -r --contains HEAD` is not empty. Otherwise it keeps the worktree, records why, and `lobstah doctor`'s `disk` row shows `kept: unpushed work`; the next pass checks again. Uncommitted changes and unpushed commits are never deleted. A PR closed without merge releases nothing. A trap's worktree is never released. Branches (local and remote), `done/` entries, state, and evidence are kept; `lobstah catch` prints `worktree: released on merge (<time>)`. One `worktree-released` notice per pass lists what was released. |
79
+ | `reuseWorktree` | `true` | A follow-up (`--follow-up <id>`) runs in the worktree of the newest dispatch in its chain whose worktree still exists, on the branch and HEAD where that dispatch stopped. Trunk is fetched; `setup` runs again only if a lockfile changed. It reuses only when the worktree is clean (untracked files under the repo's `scratch` paths excepted), belongs to the same repo, and no other dispatch runs in it; otherwise it allocates a fresh worktree as usual. A dirty worktree is never cleaned or reset. The first status note says which: `reusing worktree of <origin>` or `fresh worktree (<reason>)`. A lock file in the worktree's git dir (`lobstah.lock`) keeps a second runner out; a lock whose dispatch has finished is stale. A shared worktree is kept by the cull and the free-space guard while any dispatch in the chain is queued or active, and it ages from the newest dispatch that used it. `false` allocates a fresh worktree for every dispatch. Headless dispatches only: a trap works in its own worktree. |
65
80
  | `minFreeGB` | `0` (off) | Free space the worktrees volume (`~/.lobstah/worktrees`) must have before the daemon claims work, because each claim creates a worktree (1 to 8 GB). Below the limit, the daemon first removes finished worktrees, oldest first, until the limit is met or none are left (this runs even when `retentionDays` is `0`; open-PR and live worktrees are kept, branches are kept). If space is still short, the work stays in the queue and is not failed: `man tend` and the glass show it as `held: 3.2 GB free, needs 10 GB`. The daemon checks again on every tick. One `disk-held` notice marks the start of a hold and one `disk-cleared` notice marks its end. Suggested: `10`. |
66
81
 
67
82
  ## `[soak]` — soaking sessions (`lobstah soak`)
@@ -69,7 +84,9 @@ Same three keys as the per-repo block. Precedence for every harness setting:
69
84
  | Key | Default | Meaning |
70
85
  |---|---|---|
71
86
  | `deferSecs` | `90` | A soaking session whose park heartbeat is this fresh holds unaddressed matching bait — the daemon waits instead of spawning. Addressed bait (`--for session:<id>`) waits regardless, until the registration is gone. |
72
- | `ttlSecs` | `1800` | Heartbeat age past which a registration is a ghost trap: the sweep removes it and requeues its open catch (or finalizes a cancelled one as failed). A fresh `lobstah report` on the catch counts as liveness too. |
87
+ | `ttlSecs` | `1800` | Heartbeat age past which a registration is a ghost trap: the sweep removes it and requeues its open catch (or finalizes a cancelled one as failed). A fresh `lobstah report` on the catch counts as liveness too, and so does a fresh beat. |
88
+ | `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. |
89
+ | `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. |
73
90
 
74
91
  ## `[helm]` — the orchestrator seat (`lobstah man helm`)
75
92
 
@@ -90,6 +107,10 @@ Same three keys as the per-repo block. Precedence for every harness setting:
90
107
  | Key | Default | Meaning |
91
108
  |---|---|---|
92
109
  | `maxForksPerCycle` | `3` | The most continuation (CI-fix) dispatches one watch cycle of `lobstah pick` may fork. Each watch over the cap is held: its events stay buffered, `man tend` and `lobstah watch` list it as `held`, one `watch-held` notice names the held watches, and it forks nothing until `lobstah watch release <key>` (or `--all`). |
110
+ | `autoRepair` | `true` | On a dispatch-owned PR, fork a repair follow-up for a conflict, failed current check, or requested review changes. `false` leaves check-event delivery and attention as before. |
111
+ | `conflicts` | `true` | Repair conflicts when `autoRepair` is on. Set `false` to show conflict attention without a repair. |
112
+ | `checks` | `true` | Repair failed current checks when `autoRepair` is on. Set `false` to show check attention without a repair. |
113
+ | `maxRepairsPerPr` | `2` | Maximum repair follow-ups for one PR head SHA. When the limit is reached and the issue remains, `pr:conflict`, `pr:checks`, or `pr:review` attention names the limit. |
93
114
 
94
115
  ## `[grounds.*]` — helm territories
95
116
 
@@ -111,6 +132,7 @@ repos = ["lobstah", "lavish"]
111
132
  | Key | Default | Meaning |
112
133
  |---|---|---|
113
134
  | `pollSecs` | `45` | Poll cadence. Outbound only — no webhooks, ever. |
135
+ | `liveComment` | `true` | Keep one editable, marked status comment per dispatch. Routine edits are capped at once per minute; human-needed and terminal transitions still post a fresh notification comment. Falls back to transition comments if editing is unavailable. |
114
136
  | `notifyCommand` | — | Pickup's own hook, fired on tracker-report transitions with `LOBSTAH_KEY`, `LOBSTAH_UUID`, `LOBSTAH_VERB`, `LOBSTAH_NOTE`, `LOBSTAH_PR_URL`. |
115
137
 
116
138
  ### Token sources (both trackers)
package/docs/design.md CHANGED
@@ -702,7 +702,7 @@ with [Preact](https://preactjs.com) and [htm](https://github.com/developit/htm)
702
702
 
703
703
  Components are pure functions of the store. A new snapshot re-renders the
704
704
  page and Preact's reconciliation changes only the DOM whose data changed:
705
- rows, cards, and lobs are keyed, so an unchanged row keeps its node, an open
705
+ rows, cards, stack groups, and lobs are keyed, so an unchanged row keeps its node, an open
706
706
  modal keeps its nodes across ticks, and scroll positions stay where the
707
707
  reader left them — no per-section hashing or manual node preservation. Only
708
708
  the active tab's section renders.
@@ -33,9 +33,13 @@ row, and the session-start brief says when the plugin is behind.
33
33
  | ----- | ------------ |
34
34
  | SessionStart hook (`lobstah man brief`) | Prints the session id and a one-line fleet state. A session that is neither helm nor trap gets the two sign-on commands, with the id filled in. |
35
35
  | Stop hook (`lobstah man haul`) | At turn end, blocks with standing attention, or with the command to arm a watcher while work is in flight. Inert unless the session holds the helm or is soaking. |
36
- | SessionEnd hook (`lobstah stow --quiet`) | Signs a soaking session off when it ends. |
36
+ | PostToolUse hook (`lobstah soak beat`) | After every tool call in a soaking session: refreshes the trap's liveness and writes its catch's activity (the tool name and its primary target, redacted). At most once per 30 seconds per trap. No network, no git; always exits 0, errors go to `~/.lobstah/logs/beat.log`. Inert when not soaking or with `[soak].beat = false`. |
37
+ | SessionEnd hook (`lobstah stow --quiet`) | Signs a soaking session off when it ends and keeps its worktree. |
37
38
  | `man` skill | The orchestrator: the helm, the charter, dispatching, tending, getting woken. |
38
- | `trap` skill | The worker: soaking from a linked worktree, the `wt:` address, the six report verbs. |
39
+ | `trap` skill | The worker: soaking (in a linked worktree, or in one that soak creates), the `wt:` address, the six report verbs, and `paused --waiting-on` before waiting on something external (for ume, the non-blocking push when the await cannot run as a tracked background task). |
40
+
41
+ `lobstah send <id> "<instruction>"` steers live or queued work and wakes a
42
+ finished dispatch as a follow-up.
39
43
 
40
44
  Slash commands, each a shortcut for a CLI verb:
41
45
 
@@ -44,7 +48,7 @@ Slash commands, each a shortcut for a CLI verb:
44
48
  | `/lobstah:helm [grounds]` | `lobstah man helm` |
45
49
  | `/lobstah:relieve` | `lobstah man relieve` |
46
50
  | `/lobstah:tend` | `lobstah man tend` |
47
- | `/lobstah:soak` | `lobstah soak` (from a linked worktree) |
51
+ | `/lobstah:soak` | `lobstah soak` |
48
52
  | `/lobstah:stow` | `lobstah stow` |
49
53
 
50
54
  Claude Code also loads the `man` and `trap` skills on its own when you ask
@@ -34,9 +34,10 @@ says when the plugin is behind.
34
34
  | ----- | ------------ |
35
35
  | SessionStart hook (`lobstah man brief`) | Prints the session id and a one-line fleet state. A session that is neither helm nor trap gets the two sign-on commands, with the id filled in. |
36
36
  | Stop hook (`lobstah man haul`) | Parks the session at turn end while work is in flight and wakes it when something needs attention. Inert unless the session holds the helm or is soaking. |
37
- | SessionEnd hook (`lobstah stow --quiet`) | Signs a soaking session off when it ends. |
38
- | `man` skill | The orchestrator: the helm, the charter, dispatching, tending, getting woken. |
39
- | `trap` skill | The worker: soaking from a linked worktree, the `wt:` address, the six report verbs. |
37
+ | PostToolUse hook (`lobstah soak beat`) | After a tool call in a soaking session: refreshes the trap's liveness and writes its catch's activity. Needs Codex 0.117.0+ (see [below](#post-tool-hook-what-codex-has)). Older Codex ignores the event, and a trap's liveness then comes from its reports and its park only. |
38
+ | SessionEnd hook (`lobstah stow --quiet`) | Signs a soaking session off when it ends and keeps its worktree. |
39
+ | `man` skill | The orchestrator: the helm, the charter, dispatching, tending, getting woken, and sending a follow-up to finished work. |
40
+ | `trap` skill | The worker: soaking (in a linked worktree, or in one that soak creates), the `wt:` address, the six report verbs, and `paused --waiting-on` before external waits. |
40
41
 
41
42
  There are no slash commands: the Codex plugin layout has no commands
42
43
  directory. The skills run the same `lobstah` commands as the README
@@ -60,6 +61,41 @@ command. The desktop task's supplied skill catalog names
60
61
  `lobstah:lobsterman` and `lobstah:trap`; *the desktop picker and its exact
61
62
  inserted mention remain to be verified from the desktop UI*.
62
63
 
64
+ ## Post-tool hook: what Codex has
65
+
66
+ Codex has a `PostToolUse` hook event, with the same `hooks.json` shape as
67
+ Claude Code. Verified in the openai/codex source at commit `e07e58c`:
68
+
69
+ - `codex-rs/hooks/src/lib.rs` lists `PostToolUse` among the hook event names,
70
+ and `codex-rs/config/src/hook_config.rs` maps the `PostToolUse` key.
71
+ - The stdin payload (`PostToolUseCommandInput`, `codex-rs/hooks/src/schema.rs`)
72
+ carries `session_id`, `cwd`, `hook_event_name`, `tool_name`, `tool_input`,
73
+ and `tool_response`: the fields `lobstah soak beat` reads.
74
+ - It arrived in 0.117.0 for shell commands only (openai/codex#15531), gained
75
+ `apply_patch` and MCP tools in 0.124.0 (#18391, #18385), and other local
76
+ function tools in 0.135.0 (#23757).
77
+ - It does not fire for hosted tools such as web search, and it fires only
78
+ when the tool call succeeds (a shell command that exits non-zero still
79
+ counts).
80
+
81
+ Docs: <https://developers.openai.com/codex/hooks>.
82
+
83
+ So on Codex 0.117.0+ a trap beats after its tool calls, like a Claude Code
84
+ trap. On 0.114 to 0.116, Codex has no post-tool hook: a trap's liveness
85
+ comes from its reports and its park only, and a long stretch of work
86
+ without a report can be swept after `[soak].ttlSecs`.
87
+
88
+ ## Waiting on something external
89
+
90
+ Before a trap waits on a review, PR, deploy, person, or other external event,
91
+ it reports `paused "<note>" --waiting-on <kind> --link <url>`. Resume with a
92
+ `working` report. A Codex task cannot run an await as a tracked background
93
+ task: use the external tool's non-blocking form and end the turn. See
94
+ [Waiting on](../vocabulary.md#waiting-on).
95
+
96
+ `lobstah send <id> "<instruction>"` steers live or queued work and wakes a
97
+ finished dispatch as a follow-up.
98
+
63
99
  ## The session id and `--session`
64
100
 
65
101
  Codex exports no session-id variable, so the CLI cannot find the id on its
@@ -68,11 +104,13 @@ brief prints the task id. Pass it on first sign-on:
68
104
 
69
105
  ```bash
70
106
  lobstah man helm --session <task-id> # the helm
71
- lobstah soak --session <task-id> # a trap, from a linked worktree
107
+ lobstah soak --session <task-id> # a trap
72
108
  ```
73
109
 
74
110
  After sign-on, the Stop hook gets the id from Codex on stdin. A trap's
75
- identity is its worktree, so `lobstah soak --wait` needs no flags. Outside
111
+ identity is its worktree, so `lobstah soak --wait` in that worktree needs no
112
+ flags. Outside the trap's worktree, `soak --wait`, `report`, and `stow`
113
+ take `--session <task-id>`. Outside
76
114
  the hook, `man wait`, `man report`, and `man relieve` still take
77
115
  `--session <task-id>`.
78
116
 
package/docs/man.md CHANGED
@@ -17,6 +17,13 @@ you ⇄ liaison (interactive Claude Code / Codex session)
17
17
  The liaison never watches the workers — the daemon does that with no model in
18
18
  the loop. The liaison reads `lobstah status` when you ask, which is the
19
19
  token-efficiency point: supervision is a filesystem read, not a conversation.
20
+ Headless runners preserve new-chain committed work on `origin` as HEAD moves
21
+ and open one draft PR when `gh` is available. On a nonterminal stop they
22
+ checkpoint eligible worktree files and push once more. A follow-up whose
23
+ chain already has a PR keeps that PR and its watch. It does not push or open
24
+ a second PR automatically; its worker pushes to the existing PR's head
25
+ branch. The final status note names the saved branch, commit, and PR, so the
26
+ lobstah man can send a continuation without losing work.
20
27
 
21
28
  **The helm is harness-agnostic: drive the fleet from whichever session you
22
29
  prefer.** The contract is the CLI, not the harness — a Claude Code session,
@@ -45,7 +52,11 @@ the background, dispatch it with the `lobstah` CLI instead of doing it inline:
45
52
  - `lobstah dispatch --repo <key> --brief-text "<full brief>"` — returns an id.
46
53
  Write briefs that stand alone; the worker has no other context.
47
54
  - `lobstah status [<id>]`, `lobstah ls` — check progress when I ask, not on a loop.
48
- - `lobstah send <id> "<instruction>"` — steer a running dispatch.
55
+ - `lobstah send <id> "<instruction>"` — steer a live dispatch, add to a queued
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>`.
49
60
  - `lobstah cancel <id>` — stop one.
50
61
  - A dispatch reporting `needs-decision` is waiting on ME — surface its question
51
62
  immediately, then `lobstah send` my answer.
@@ -70,6 +81,21 @@ use by hand. So the crew is inspectable with tools you already have:
70
81
  the Claude/Codex desktop apps' session lists) show dispatch sessions too —
71
82
  they're stored where the harness always stores them.
72
83
 
84
+ A follow-up (`lobstah dispatch --follow-up <id>`) resumes the origin's
85
+ conversation and, by default, its worktree too: it runs in the checkout of
86
+ the newest dispatch in its chain, on the branch and HEAD where that dispatch
87
+ stopped, without a second dependency install. It allocates a fresh worktree
88
+ instead when that checkout has uncommitted changes, is gone, or another
89
+ dispatch runs in it; its first status note says which (`reusing worktree of
90
+ <origin>` or `fresh worktree (<reason>)`). `lobstah catch` prints the
91
+ `worktree` a dispatch ran in, and `worktreeOf` when it reused one.
92
+ `[limits].reuseWorktree = false` turns reuse off. Normally, use
93
+ `lobstah send <id> "<instruction>"` for a continuation: it delivers to the
94
+ live or queued member of the chain, or creates a follow-up of the newest
95
+ finished member. If that member was last claimed by a trap still signed on,
96
+ the follow-up returns to that trap unless `--for` overrides it. Otherwise it
97
+ is unaddressed for a headless worker.
98
+
73
99
  Attach refuses while a dispatch is `working` (two writers, one session);
74
100
  follow the logs or `send` instead, or cancel and then attach.
75
101
 
@@ -108,6 +134,26 @@ view](pickup.md#merge-view) pickup persists each tick, so PR state is at most
108
134
  one poll interval stale without tend making a single network call. `--json`
109
135
  emits the full report for dashboards and scripts to render.
110
136
 
137
+ The work table's `activity` column shows what each live dispatch is doing
138
+ now, with its age: `Edit src/a.ts (12s ago)`. It comes from the runner's
139
+ event stream (headless) or the post-tool hook (a trap), never from the
140
+ worker's reports, so it stays current when the worker forgets to report.
141
+ Past `[limits].wedgeThresholdSecs` it reads `stale: … (14m ago)`. A long
142
+ silence is shown, not escalated: it raises no attention and no notice. The
143
+ worker's verb and note stay the primary line. `lobstah status <id>` prints
144
+ the same line as `activity:`, and `lobstah ls` has an `activity` column. See
145
+ [Activity](vocabulary.md#activity).
146
+
147
+ A worker that waits on something outside lobstah (a ume review, a PR
148
+ review, a deploy) reports `paused "<note>" --waiting-on review --link <url>`
149
+ before it waits. Tend, `status`, `ls`, and the glass then show
150
+ `paused: waiting on review` with the link and the time waited. It is a
151
+ 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).
156
+
111
157
  ### PR state after done
112
158
 
113
159
  A dispatch reports `done` when its PR opens; `report done --pr <url>`
@@ -143,18 +189,27 @@ watches that already exist. `prs sync` refreshes existing PR watches only.
143
189
 
144
190
  The first check of a new watch is a baseline:
145
191
 
146
- - An open PR: checks that already failed show in the PR record and in tend,
147
- but they fork no CI-fix dispatch. Only a check that fails after the
148
- baseline, or a failed check on a new head sha, forks one.
192
+ - An open PR: the first observation records its checks and merge state, but
193
+ starts no repair. Later observations may start a bounded repair on a PR
194
+ lobstah owns.
149
195
  - A merged or closed PR: the check records `MERGED` or `CLOSED` in the PR
150
196
  record and retires the watch. It forks nothing and raises no attention. A
151
197
  `pr-merged` / `pr-closed` notice is posted once only when the PR ended in
152
198
  the last 24 hours.
153
199
 
154
200
  One watch cycle forks at most `[watch].maxForksPerCycle` continuations
155
- (default 3). Watches over the cap are held and listed as `held` in
156
- `man tend` and `lobstah watch`. `lobstah watch release <key>` (or `--all`)
157
- lets them fork again.
201
+ (default 3). Generic watches over the cap are held and listed as `held` in
202
+ `man tend` and `lobstah watch`. PR repairs wait for the next cycle.
203
+ `lobstah watch release <key>` (or `--all`) releases held watches.
204
+
205
+ For a dispatch-owned PR, the watch repairs conflicts, failed current checks,
206
+ and requested review changes. It follows up the newest dispatch in the PR's
207
+ chain. A conflict brief names the PR's base branch, including a stacked
208
+ base. A check brief names each failed check and its details URL. The watch
209
+ records each attempt and stops at `[watch].maxRepairsPerPr` per head SHA.
210
+ It does not repair a PR with a person's newer commits or uncertain commit
211
+ ownership, a terminal PR, or a PR whose chain already has queued or active
212
+ work. `[watch].autoRepair`, `conflicts`, and `checks` control this behavior.
158
213
 
159
214
  Watching a PR nobody dispatched — `lobstah watch add pr:<owner>/<repo>#<n>`
160
215
  with no `--for` — is how a helm follows a human's PR, or one whose
@@ -164,15 +219,25 @@ attention kinds exactly like a dispatched PR (its dispatch chain column is
164
219
  empty). It stays quiet while it's fine: only a failing check or a changes
165
220
  request surfaces as a watch event; a merge or close arrives as a notice.
166
221
 
167
- An observed PR joins tend's attention list by kind — `pr:draft`,
168
- `pr:review` (unresolved threads or changes requested), `pr:checks` (a red
169
- head), `pr:conflict` (GitHub reports it conflicting with its base),
170
- `pr:ready` (approved or all green, and GitHub says it can merge) — so it crawls in the glass and
171
- the desktop pet until its clear condition holds; clicking it opens the PR.
172
- `pr:review` and `pr:checks` stay off the screen while a worker already owns
173
- them (a pickup feedback round or the watch's fix continuation in flight).
174
- `attentionKinds` in `config.toml` picks the kinds; `landed` is opt-in. See
175
- the [attention contract](vocabulary.md#attention-contract). A PR is
222
+ **PR order.** Every PR list uses one order: the glass PRs tab, the On deck
223
+ PR stacks, `lobstah prs`, and the `stack #…` lines and the `work` table of
224
+ `man tend`. Open stacks come first, then finished ones. Within each group, the
225
+ stack whose newest PR was first seen last is on top. Within a stack, PRs are
226
+ in stack position. A PR sorts by its record's `firstSeenAt`, then by number.
227
+ A new observation does not change the order. The order changes when a PR
228
+ opens, merges, closes, or changes its base so that it joins or leaves a
229
+ stack. When two PRs share a head branch, the parent is the one first seen
230
+ last. The attention list is in standing order: the time each condition
231
+ started. `observedAt` is shown as the time of the last check.
232
+
233
+ An observed PR joins tend's attention list by kind. `pr:ready` needs a
234
+ mergeable, non-draft PR with no failed, pending, or unknown current checks.
235
+ `pr:conflict` and `pr:checks` appear for an owned PR only when repair is off,
236
+ blocked, or exhausted; their notes say why. Requested review changes on an
237
+ owned PR follow the same repair rule. Unresolved review questions remain
238
+ `pr:review` attention. Drafts are not attention by default; users can add
239
+ `pr:draft` to `attentionKinds`. The glass and desktop pet use the same list.
240
+ See the [attention contract](vocabulary.md#attention-contract). A PR is
176
241
  something to look at, not a stall: it never flips the verdict to
177
242
  `needs-attention` and stays out of the digest.
178
243
 
@@ -302,6 +367,23 @@ must never hide it from the orchestrator that has to answer it. The glass,
302
367
  which has no write endpoint, hides a clicked lob per browser in localStorage
303
368
  instead.
304
369
 
370
+ **The pet's read.** Every six seconds the pet runs `lobstah attention --json`.
371
+ It prints `{ "attention": [...] }`: the same items, with the same fields, that
372
+ `man tend --json` puts under `attention`, and nothing else. If that command
373
+ fails (an older CLI exits 2 on the unknown flag), the pet runs
374
+ `man tend --json` instead. The pet reads the child's output while the child
375
+ runs, so a report of any size works. Each read may take 10 seconds; then the
376
+ pet stops the child and keeps its current windows. After three failed reads in
377
+ a row it writes one line with the reason to `~/.lobstah/logs/pet.log`, and one
378
+ more line when reads work again. After every read it writes
379
+ `~/.lobstah/pet/state.json`. `lobstah doctor` reads that file for its `pet`
380
+ row: installed or not, running or not, and whether the last read worked:
381
+
382
+ ```
383
+ pet ok installed; running (pid 812); last read worked 4s ago (`lobstah attention --json`, 3 walking)
384
+ pet warn installed; running (pid 812); last read failed 2s ago, 3 in a row: `lobstah attention --json` timed out; `lobstah man tend --json` timed out; last worked 5m ago
385
+ ```
386
+
305
387
  **Wrapper loop.** An outer loop blocking on `wait` can spawn one fresh
306
388
  headless turn per event:
307
389
 
@@ -393,23 +475,77 @@ visible terminal, and whatever authenticated tooling a headless spawn can't
393
475
  get.
394
476
 
395
477
  ```bash
396
- lobstah soak # from a worktree — the primary checkout is
397
- # never claimable, so sign on from a linked
398
- # worktree (git worktree add ../side -b side)
478
+ lobstah soak # in a linked worktree: sign it on; in a
479
+ # repo's primary checkout: create a linked
480
+ # worktree and sign that on
399
481
  # (the id: --session, else hook stdin, else
400
482
  # $CLAUDE_CODE_SESSION_ID)
483
+ lobstah soak --repo <key> # outside any configured repo: create a
484
+ # worktree for <key> and sign it on
401
485
  lobstah soak --wait # hookless sessions: listen in the foreground
402
486
  # (re-runs need no flags — identity is the
403
- # worktree); exit 3 = quiet, run it again
487
+ # worktree, else the session id); exit 3 =
488
+ # quiet, run it again
404
489
  lobstah stow # sign off; an open catch requeues, unread
405
- # messages bounce back to the helm
490
+ # messages bounce back to the helm; removes
491
+ # the worktree when soak created it
492
+ lobstah stow --keep # sign off and keep the worktree
493
+ lobstah soak --name amber-gull # choose or change this trap's two-word name
406
494
  ```
407
495
 
408
- **Identity is the worktree.** Sign-on anchors a short trap id in
409
- `.lobstah-trap` and prints the trap's address (`wt:<id>`); the address
496
+ **Soak can create the worktree.** From a repo's primary checkout, or with
497
+ `--repo <key>` from outside any configured repo, soak creates a linked
498
+ worktree the same way a dispatch does. It fetches trunk, adds
499
+ `~/.lobstah/worktrees/soak-<trap>` on a new branch `lobstah/soak-<trap>`
500
+ from `origin/<trunk>`, and runs the repo's `setup` commands. Without
501
+ `--repo`, soak run outside a configured repo fails with an error that names
502
+ `--repo`. In a linked worktree, `--repo` must match that worktree's repo.
503
+ Soak prints `worktree: <path>`, `created: true`, and `branch:`. When the
504
+ session is not inside the worktree, it also prints
505
+ `instruction: cd <path> and work in that directory from now on`, and its
506
+ help lines carry `--session <id>`. The session changes into that directory
507
+ before it takes work. If creation fails (setup fails, the
508
+ `[limits].minFreeGB` check fails, or trunk cannot be fetched), soak signs
509
+ nothing on, removes the partial worktree and its branch, and prints the
510
+ cause. The address is `wt:<trap>`. The registration records
511
+ `createdWorktree: true`, and `.lobstah-trap` records `createdBy: "soak"`,
512
+ the session id, the repo, and the branch.
513
+
514
+ Soak is idempotent. A session that already mans a trap re-uses it when it
515
+ runs `soak` or `soak --wait` outside a linked worktree: the trap is resolved
516
+ from the session id, and no second worktree is created. A worktree that soak
517
+ created for the same session and repo is also re-used after a ghost sweep.
518
+ From outside the worktree, `soak --wait`, `report`, and `stow` find the
519
+ trap by session id (`--session <id>`, or `$CLAUDE_CODE_SESSION_ID`), and
520
+ `send session:<id>` addresses it. With `--session`, or from inside the worktree, `report done` records the
521
+ worktree's HEAD commit and branch in evidence.
522
+
523
+ `stow` removes a worktree only when soak created it and nothing in it exists
524
+ elsewhere. It keeps the worktree and prints `worktree: kept` and a `reason:`
525
+ 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.
528
+ Stow runs the removal from the primary checkout, so it works from inside
529
+ the worktree. On removal it prints `worktree: removed`, `path:`, and
530
+ `returnTo: <primary checkout>`, with a help line `cd <primary>`. It
531
+ deletes the branch only when every commit on it is on its upstream (with no
532
+ upstream: on some remote branch);
533
+ otherwise it prints `branchKept: <branch> (<reason>)`. A deleted branch
534
+ prints as `branchDeleted:`. `stow --wt <id>` follows the same rules. The
535
+ SessionEnd hook (`lobstah stow --quiet`) signs off and keeps the worktree.
536
+
537
+ **Identity is the worktree.** Sign-on anchors a short trap id and two-word
538
+ name in `.lobstah-trap` and prints both, such as `amber-gull (wt:c32a245d)`; the address
410
539
  survives session restarts — a new session in the same worktree resumes the
411
540
  same trap (a *live* foreign session is refused: the session lock). The
412
- session id (from the plugin's session-start brief) lives inside the
541
+ name is reserved across all traps known to this lobstah home, including
542
+ stowed and swept traps. `--name` sets or changes it; malformed and taken
543
+ names are refused. The bare name, `wt:<name>`, and `wt:<id>` all address the
544
+ same live trap in `dispatch --for`, `send`, and `stow --wt`. Unknown names
545
+ list known names and never turn addressed bait into headless work. The id
546
+ remains the key in dispatch and claim records.
547
+
548
+ The session id (from the plugin's session-start brief) lives inside the
413
549
  registration as the liveness principal. The harness (claude or codex) is
414
550
  inferred — from `CLAUDE*` / `CODEX*` in the environment, and when both are
415
551
  set (one harness launched inside the other) from the session id's format:
@@ -430,9 +566,17 @@ the daemon spawns headless. Conversational steering goes through
430
566
  `send wt:<trap> "..."` — a message, not bait: no branch, no catch, sender
431
567
  stamped, bounced to the helm when undeliverable.
432
568
 
569
+ A soaking session proves it is alive three ways: its park heartbeat, its
570
+ reports, and its **beat**. The plugin's post-tool hook runs
571
+ `lobstah soak beat` after tool calls: at most once per 30 seconds it
572
+ refreshes the trap's beat and writes the claimed catch's activity. The hook
573
+ resolves the trap from the working directory, else from the session id. A
574
+ trap that works for an hour without reporting is not swept while it beats.
575
+ `[soak].beat = false` turns the hook off.
576
+
433
577
  Liveness has two failure shapes with two remedies: a registration that
434
- parked before and went quiet past `[soak].ttlSecs` is a **ghost trap** —
435
- swept, catch requeued, noticed; one that **never parked** is a **defective
578
+ 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
436
580
  enlistment** — noticed with its diagnosis (usually a missing Stop hook →
437
581
  `soak --wait`) and left standing so the address keeps protecting its work.
438
582
  Nobody is conscripted: only a worktree whose session ran `soak` ever
@@ -444,7 +588,14 @@ Worktrees are 1 to 8 GB each. `lobstah cull` sweeps what is finished: `done/`
444
588
  entries older than the window (`--older-than <days>`, default 14), worktrees
445
589
  whose dispatch is finished or gone, stale state files, merged or closed PR
446
590
  records, and orphaned acks. It never touches queued or active work, and
447
- `git worktree remove` keeps each dispatch's branch.
591
+ `git worktree remove` keeps each dispatch's branch. A worktree that soak
592
+ 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
596
+ reused is one worktree shared by the chain: it stays while any dispatch in
597
+ the chain is queued or active, and it ages from the newest dispatch that
598
+ used it.
448
599
 
449
600
  Without `--apply` it is a dry run: it measures each target and prints the
450
601
  sizes. A worktree is measured with one `du -sk`; where `du` is missing
@@ -476,8 +627,24 @@ The daemon can do this on its own. Two `[limits]` keys turn it on
476
627
  note column, and one `disk-held` notice reaches the helm. When space
477
628
  returns, one `disk-cleared` notice follows and claiming resumes.
478
629
 
479
- `lobstah doctor` prints a `disk` row: free space on the worktrees volume, both
480
- limits, and the count and age of the worktrees a cull could remove.
630
+ A third key, `releaseOnMerge`, frees a worktree as soon as its PR merges
631
+ instead of waiting out `retentionDays`. When a PR watch records `merged`, the
632
+ next cull pass removes the worktree of the dispatch that owns the PR and of
633
+ every dispatch in its follow-up chain that ran on that PR. It checks first:
634
+ every dispatch in the chain is finished, the worktree is clean, and its HEAD
635
+ is on the remote after a fetch. A worktree that fails a check is kept and
636
+ listed as `kept: unpushed work`. A PR closed without merge releases nothing,
637
+ and a trap's worktree is never released. Branches and the dispatch record
638
+ stay; `lobstah catch` says `worktree: released on merge (<time>)`. One
639
+ `worktree-released` notice per pass lists what went.
640
+
641
+ `lobstah doctor` prints a `disk` row: free space on the worktrees volume, the
642
+ limits, the count and age of the worktrees a cull could remove, and any
643
+ merged PR's worktree that `releaseOnMerge` kept, with the reason:
644
+
645
+ ```
646
+ disk warn 412.3 GB free on ~/.lobstah/worktrees; minFreeGB 10; retentionDays 14; 3 cullable worktree(s), oldest 9d; releaseOnMerge on; kept: unpushed work (1 worktree(s) of merged PRs: 6a1f0c2e HEAD is not on the remote)
647
+ ```
481
648
 
482
649
  ## What the daemon gives your liaison for free
483
650
 
package/docs/pickup.md CHANGED
@@ -54,6 +54,16 @@ fire-and-forget, never blocking the loop. Point it at whatever the host
54
54
  already has (a Slack helper, `openclaw message send`); lobstah stays free of
55
55
  messaging vendors.
56
56
 
57
+ **Live status.** With `[pickup].liveComment = true` (the default), pickup
58
+ creates one marked, editable comment per dispatch on Linear or GitHub. It
59
+ shows the current verb, waiting reason/link, latest activity and staleness,
60
+ elapsed time, attempt, branch, last commit, commits ahead of trunk, and draft
61
+ PR when known. Pickup edits it at most once per minute unless the verb changes,
62
+ and does not rewrite it when the underlying status is unchanged. A
63
+ `needs-decision`, `blocked`, `failed`, or `done` transition also gets a new
64
+ comment so humans are notified. If the tracker cannot edit the live comment,
65
+ pickup continues with the older transition-comment behavior.
66
+
57
67
  **No LLM in the loop.** Issue-to-descriptor translation is mechanical. Judgment
58
68
  about an ambiguous issue belongs to the dispatched agent, which reports
59
69
  `needs-decision` — not to pickup. Fleet setups that wake an LLM to poll a