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.
@@ -16,7 +16,7 @@ anything else. The status log is append-only; the last entry wins.
16
16
  | `working` | Making progress; nothing needed. | Nobody. |
17
17
  | `needs-decision` | Blocked on a judgment call only a human (or the orchestrator) can make. The note carries the question. A headless worker waiting on a question stays alive until answered (`lobstah send`), cancelled, or the wall clock. | Human — re-fires every `remindSecs` until answered. |
18
18
  | `blocked` | Cannot proceed for an external reason (missing access, broken dependency). | Human. |
19
- | `paused` | Intentionally idle; resume is expected. | Whoever paused it. |
19
+ | `paused` | Intentionally idle; resume is expected. With `--waiting-on`, the worker says what it waits on outside lobstah (see [Waiting on](#waiting-on)). A state, not a question: it raises no attention and does not walk the pet. | Whoever paused it, or the thing it waits on. |
20
20
  | `done` | The brief is fulfilled. Terminal. Merging is never the dispatch's job. | Merge loop / reviewer. |
21
21
  | `failed` | Cannot fulfill the brief; work preserved in the worktree. Terminal. | Human. |
22
22
 
@@ -25,6 +25,46 @@ logged, process state stops mattering and the daemon finalizes.
25
25
 
26
26
  Source of truth: `VERBS` in `packages/core/src/types.ts`.
27
27
 
28
+ ## Waiting on
29
+
30
+ What a worker waits on outside lobstah: a human review in ume, a PR review, a
31
+ deploy. A report says it with flags:
32
+
33
+ ```bash
34
+ lobstah report <id> paused "<note>" --waiting-on <kind> [--link <url>] [--until <iso|duration>]
35
+ ```
36
+
37
+ | Kind | Meaning |
38
+ | --- | --- |
39
+ | `review` | A human review of an artifact (a ume plan or result). |
40
+ | `pr` | A pull request review or merge. |
41
+ | `deploy` | A deploy or release to finish. |
42
+ | `person` | A named person to act. |
43
+ | `external` | Anything else outside lobstah. |
44
+
45
+ `--waiting-on` and `--link` are valid only with `paused`, `needs-decision`,
46
+ and `blocked`; `--until` only with `paused`. The link must be http or https.
47
+ `--until` takes an ISO time or a duration from now (`30m`, `4h`, `2d`). The
48
+ fields are stored on the status entry (`waitingOn`, `link`, `until`); the
49
+ write path (`appendStatus`) rejects anything else.
50
+
51
+ Effects of `paused` with `--waiting-on`:
52
+
53
+ - `lobstah status`, `lobstah ls`, `lobstah man tend`, and the glass show
54
+ `paused: waiting on review` with the link and the time waited. The glass
55
+ card links the URL.
56
+ - A **trap** whose catch last reported `paused` (with or without
57
+ `--waiting-on`) is not ghost-swept until `--until` passes, or, without it,
58
+ until `[soak].pausedTtlSecs` (default 24 hours) after the report. After
59
+ that it sweeps as before, and the `trap-ghosted` notice says the pause
60
+ expired.
61
+ - A **headless** worker is not classified `wedged`, however long it is
62
+ silent, and its `wallClockSecs` limit does not run while it is paused. It
63
+ still holds its slot: a live paused runner counts against `maxConcurrent`.
64
+ - No attention, no notice, no pet. It is a state, not a question.
65
+
66
+ Source of truth: `WAITING_ON` in `packages/core/src/types.ts`.
67
+
28
68
  ## Reconciled state
29
69
 
30
70
  What an observer should *believe*, combining the status log with event
@@ -40,6 +80,49 @@ The value set is the six verbs plus `unknown`. Precedence, highest first:
40
80
  4. Nothing trustworthy → `unknown`. **Absence of signal never means fine** —
41
81
  `unknown` is a prompt to look, not a synonym for idle.
42
82
 
83
+ ## Activity
84
+
85
+ What a worker is doing *now*. It comes from the event stream and from hooks,
86
+ never from the model remembering to report. The four layers, from least to
87
+ most detail: **liveness** (alive or stuck), **activity** (what it is doing
88
+ now), **narrative** (where it is in the plan: the six verbs, at milestones),
89
+ and **transcript** (everything). Liveness and activity are machine-derived.
90
+ Narrative stays with the worker.
91
+
92
+ One record per dispatch, `state/<id>.activity`: `{ at, kind, summary }`.
93
+
94
+ | Kind | Source |
95
+ | --- | --- |
96
+ | `tool` | A tool call started. The summary is the tool name and its primary target: a file path relative to the worktree, a command's first word, a URL's host. |
97
+ | `message` | The model wrote text. The summary is fixed (`writing a message`); the text is never copied. |
98
+ | `thinking` | The model is reasoning. No content. |
99
+ | `waiting` | The runner holds the run open: for an answer to a question, or for background work. |
100
+
101
+ The summary is never the tool's full input, file contents, an environment
102
+ value, or anything that looks like a secret (token prefixes, JWTs, bearer
103
+ values, `key=value` pairs with a secret-sounding key, long mixed runs of
104
+ letters and digits are replaced with `[redacted]`). It is capped at 80
105
+ characters.
106
+
107
+ Writers:
108
+
109
+ - **Headless:** the runner derives it from every event it drives. At most one
110
+ write per 10 seconds, plus one on every change of kind; a held record is
111
+ written when the window closes. Atomic write.
112
+ - **Trap:** the post-tool hook runs `lobstah soak beat`, which writes the
113
+ record for the trap's claimed catch. At most one beat per 30 seconds per
114
+ trap.
115
+
116
+ Readers: `lobstah status <id>` (`activity: <summary> (<age> ago)`),
117
+ `lobstah ls` (an `activity` column), `lobstah man tend` (the work table), and
118
+ the glass (under the verb and note on each dispatch). Past
119
+ `[limits].wedgeThresholdSecs` the line shows as **stale** (dim in the glass,
120
+ `stale:` in text, with its age). Staleness is displayed, not escalated: no
121
+ attention kind, no notice, no pet. The worker's own verb and note stay the
122
+ primary line; activity never replaces them.
123
+
124
+ Source of truth: `packages/core/src/activity.ts`.
125
+
43
126
  ## Liveness classification
44
127
 
45
128
  What the *process* is doing, independent of what it claims. Computed by
@@ -52,7 +135,7 @@ daemon — it never reaches a tracker.
52
135
  | `busy` | Runner alive, activity within the wedge threshold. | Nothing. |
53
136
  | `terminal` | Terminal verb logged. | Finalize once the process is gone. |
54
137
  | `dead` | Pid verified gone (pid + process-start-time, so pid reuse can't lie). | Respawn with session resume, bounded by `maxRestartAttempts`; then `failed`. |
55
- | `wedged` | Alive but no activity past `wedgeThresholdSecs`. | SIGKILL the group, fork the session with a nudge, same bound. |
138
+ | `wedged` | Alive but no activity past `wedgeThresholdSecs`. Never a worker whose last report is `paused` with `--waiting-on` (that is `busy`). | SIGKILL the group, fork the session with a nudge, same bound. |
56
139
  | `unknown` | Contradictory or missing evidence. | Touch nothing; log it. |
57
140
 
58
141
  Dead and wedged get opposite treatment on purpose: a dead process is safe to
@@ -120,6 +203,9 @@ session id's UUID version (v7 codex, v4 claude), else the descriptor.
120
203
  | Word | Meaning |
121
204
  | ---- | ------- |
122
205
  | follow-up | `--follow-up <id>` forks the origin's session **under the origin's harness** when the follow-up names no harness. An explicit `--harness` (recorded as `harnessExplicit: true` in the descriptor by `dispatch`, `swap`, and the node tool) that differs from the origin session's makes it a swap. A descriptor from before the record falls back to the old guess: a swap only if the harness is one the chain never asked for. Pickup review rounds and watch continuations are follow-ups. |
206
+ | send to a chain | `lobstah send <id> "<instruction>"` routes to a live member's inbox first, then a queued member's inbox. If the chain is finished, it dispatches a follow-up of its newest member, with the instruction as the brief. A second send reaches that queued follow-up. `--no-wake` instead leaves unread mail in the addressed finished dispatch. |
207
+ | worktree reuse | A headless follow-up runs in its chain's worktree (the newest chain member's that still exists) when that worktree is clean, same-repo, and free, instead of allocating a new one (`[limits].reuseWorktree`, default on). Evidence records `worktree` (the checkout) and, on reuse, `worktreeOf` (the dispatch that allocated it). `dispatchWorktree` (`packages/core/src/worktrees.ts`) is the one resolver from a dispatch id to its checkout; attach, swap, catch, tend, the glass, and the cull all use it. The first status note says `reusing worktree of <origin>` or `fresh worktree (<reason>)`. |
208
+ | release on merge | With `[limits].releaseOnMerge`, the cull pass after a PR watch records `merged` removes the worktrees of that PR's finished chain, only when each is clean and its HEAD is on the remote. Evidence records `worktreeReleased` (when). A worktree that fails a check is **kept**: `release-kept.json` holds the reason, and doctor's `disk` row shows `kept: unpushed work`. |
123
209
  | swap | Start cold on another harness with the brief plus a progress note (commits so far, uncommitted changes, and the origin's branch/PR/commits for a follow-up). `lobstah swap` does it to an active dispatch; a follow-up does it when it explicitly asks for a different harness. |
124
210
  | `resume-fallback` | Status note (`resume-fallback: <reason> — starting cold on <harness>`) and evidence field (`resumeFallback`), recorded when the harness refuses a resume (not found, culled, foreign) before doing any work. No session is left to keep, so the runner starts cold on **the dispatch's own harness** (explicit, else the configured default), not the origin's. It passes the progress note along and the dispatch proceeds. |
125
211
  | Codex desktop thread | A Codex thread whose rollout (`$CODEX_HOME/sessions/**/rollout-*-<id>.jsonl`) has a `session_meta.originator` naming the desktop app (`Codex Desktop`, `codex_work_desktop`); CLI runs say `codex_exec` or `codex_sdk_ts`. `codex exec resume` has been seen to refuse one (`thread/resume failed: no rollout found for thread id …`, e2de5dd7, even though the rollout file was on disk). So when a Codex session is a known desktop thread, the resume path skips the attempt and notes `Codex desktop thread; not resumable from the CLI, starting cold on <harness>`, and `attach` refuses with the same words. A thread with no local rollout still gets its resume attempt, and a failure goes through `resume-fallback`. |
@@ -183,19 +269,22 @@ evidence).
183
269
  | ---- | ------- |
184
270
  | `pr:` key | `pr:<owner>/<repo>#<n>` — one watch per PR. |
185
271
  | cursor | The last observation (head sha, per-check conclusions, review decision, merge state, draft, state), base64url-encoded. An unchanged PR re-emits nothing and returns the same cursor. A new head sha resets check memory. |
186
- | first observation | Cursor `0`. It is the baseline. An open PR emits no `check-completed` events: a check that already failed shows in the PR record and tend but forks nothing. A merged or closed PR emits nothing, records its state, and the watch retires; the notice is posted only when the PR ended in the last 24 hours. |
272
+ | first observation | Cursor `0`. It is the baseline. An open PR records checks and merge state but starts no repair. A merged or closed PR emits nothing, records its state, and the watch retires; the notice is posted only when the PR ended in the last 24 hours. |
187
273
  | `check-completed` | A check reached a conclusion on the current head after the baseline (`name`, `conclusion`, `detailsUrl`). Failing → work; passing → evidence only. Never emitted for a merged or closed PR. |
188
274
  | `review-decision` | The review decision changed (`value`). Work, unless `[pickup.github]` covers the repo — then pickup's feedback rule owns it ([pickup.md](pickup.md), "Feedback pickup"). |
189
- | `merge-state` | `mergeStateStatus` changed (`value`). Evidence only. |
275
+ | `merge-state` | `mergeStateStatus` changed (`value`). The PR record carries conflicts into the repair planner. |
190
276
  | `draft` | Draft flipped (`value`). Evidence only. |
191
277
  | `merged` / `closed` | Terminal; the check sets `done`, and the watch retires once delivered. Emitted only on an open → terminal change, never on the first observation. |
192
- | evidence `pr` | `{ url, number, state, draft, reviewDecision, mergeStateStatus, headSha, checks: { total, passed, failed, pending, unknown? }, review: { unresolvedThreads, changesRequested, lastReviewAt }, observedAt }`. `review.changesRequested` comes from `reviewDecision` or any reviewer's latest decisive review; `unresolvedThreads` from the GraphQL query, omitted for an observation where that query failed. Comment bodies are never stored. The object is merged into the owning dispatch's evidence on every observation. `prBadge` derives the one-word state that tend, `catch`, and the glass show: `merged`, `closed`, `draft`, `conflicts` (`DIRTY`, filled GitHub red, ahead of checks and review), `checks n/m failed`, `changes requested`, `n unresolved`, `checks n/m` (pending), `behind` (`BEHIND`, grey; not an attention kind), `checks unknown` (the watch may not read check results; never ready), `review`, `green` (only for a mergeable merge state), `blocked`, `merge unknown`. |
278
+ | evidence `pr` | `{ url, number, state, draft, reviewDecision, mergeStateStatus, headSha, checks: { total, passed, failed, pending, unknown? }, review: { unresolvedThreads, changesRequested, lastReviewAt }, observedAt }`. Check counts use only the latest run per check name and app/workflow. `CANCELLED` and `STALE` latest runs are unknown, not failed. The PR record also stores repair status, attempts, and reason. `prBadge` shows `repairing: conflict (attempt 1 of 2)` while a repair is in flight; tend, `catch`, and the glass share the badge. |
193
279
  | 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. |
194
- | 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 plus `key`, `repo` (`<owner>/<repo>`), and `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR). **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` stack suppression, 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. |
195
-
196
- Every event carries `headSha`. A dispatch-owned PR watch emits only work
197
- events (a failing check; a review decision pickup doesn't own), so the
198
- `owner` row is unchanged: its events always fork. The rest is evidence.
280
+ | 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 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` stack suppression, 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. |
281
+
282
+ Every event carries `headSha`. A dispatch-owned PR watch records work events,
283
+ then the repair planner decides whether to follow up. One repair runs per PR
284
+ at a time. A first observation starts none. Repairs use the newest owning
285
+ dispatch and the PR's observed base branch. The watch checks commit ownership
286
+ before enqueueing. It stops after `[watch].maxRepairsPerPr` attempts per head
287
+ SHA. The rest is evidence.
199
288
  Merged and closed reach the helm as a `pr-merged` / `pr-closed` notice,
200
289
  posted once by whichever process first records the open → terminal
201
290
  transition on the **PR record** — not by event routing. A PR whose first
@@ -203,9 +292,8 @@ record is already terminal gets the notice only when it ended in the last
203
292
  24 hours.
204
293
 
205
294
  One pick watch cycle forks at most `[watch].maxForksPerCycle`
206
- continuations (default 3). Each watch over the cap is held (`heldAt` on the
207
- watch): tend and `lobstah watch` list it as `held`, a `watch-held` notice
208
- names it, and it forks nothing until `lobstah watch release`.
295
+ continuations (default 3). Generic watches over the cap are held (`heldAt`
296
+ on the watch); PR repairs wait for the next cycle.
209
297
 
210
298
  A man-owned PR watch (no `--for`) delivers as attention only what needs a
211
299
  human (`manEvents`, beside `workEvents` in `apps/cli/src/pr-watch.ts`): a
@@ -218,8 +306,8 @@ One carrier per event kind:
218
306
 
219
307
  | Event | Carrier |
220
308
  | ----- | ------- |
221
- | `check-completed`, failing | dispatch-owned: a continuation (pick); man-owned: a `watch` attention event |
222
- | `review-decision` | dispatch-owned: a continuation unless pickup owns review feedback; man-owned: a `watch` event only for `CHANGES_REQUESTED` |
309
+ | `check-completed`, failing | dispatch-owned: the PR repair planner; man-owned: a `watch` attention event |
310
+ | `review-decision` | dispatch-owned: the PR repair planner for requested changes; man-owned: a `watch` event only for `CHANGES_REQUESTED` |
223
311
  | `check-completed` green, `draft`, `merge-state`, approvals | the PR record (and the owner's evidence) only |
224
312
  | `merged`, `closed` | a `pr-merged` / `pr-closed` notice from the record transition only |
225
313
 
@@ -237,20 +325,23 @@ Identity is **worktree-anchored**: `.lobstah-trap` in the worktree root
237
325
  holds a short stable id, the registration keys on it, and the address
238
326
  (`wt:<id>`) survives session restarts. The session id inside the
239
327
  registration is the liveness principal. **Owner:**
240
- `packages/core/src/soak.ts`. **Enforcement:** sign-on is refused from a
241
- repo's primary checkout, and a live foreign session in an owned worktree is
242
- refused (the session lock); a stale one is adopted.
328
+ `packages/core/src/soak.ts`. **Enforcement:** sign-on from a repo's primary
329
+ checkout, or with `--repo <key>` from outside any configured repo, creates a
330
+ linked worktree (`~/.lobstah/worktrees/soak-<trap>`, branch
331
+ `lobstah/soak-<trap>`) and signs that on; a live foreign session in an owned
332
+ worktree is refused (the session lock); a stale one is adopted.
243
333
 
244
334
  | Word | Meaning |
245
335
  | ---- | ------- |
246
- | `soak` | Sign the worktree's trap on and take matching work. `--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. |
247
- | `stow` | Sign the trap off (run it in the worktree); an open catch requeues (a cancelled one finalizes as failed) and unread messages bounce to the helm. 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`). |
336
+ | `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. |
337
+ | `stow` | Sign the trap off (in the worktree, or elsewhere by session id: `--session <id>` or `$CLAUDE_CODE_SESSION_ID`); an open catch requeues (a cancelled one finalizes as failed) and unread messages bounce to the helm. 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:`; `--keep` leaves it. It keeps the worktree (`worktree: kept`, `reason:`) when soak did not create it, or when it holds uncommitted changes, untracked files that are not ignored, or commits on no remote branch; it never forces a removal. 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 and keeps the worktree. |
248
338
  | 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. |
249
339
  | 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. |
250
340
  | 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. |
251
- | ghost trap | A registration whose heartbeat lapsed past `[soak].ttlSecs` **after having parked at least once**. The sweep removes it, requeues its catch, and posts a `trap-ghosted` notice; re-soaking the worktree restores the same address. A fresh report on the catch keeps a mid-turn session out of the sweep. |
341
+ | beat | `lobstah soak beat`, 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`. |
342
+ | ghost trap | A registration whose heartbeat **and** beat lapsed past `[soak].ttlSecs` **after having parked at least once**. The sweep removes it, requeues its catch, and posts a `trap-ghosted` notice; the worktree stays, and re-soaking restores the same address. A fresh report or a fresh 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)). |
252
343
  | 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. |
253
- | notice | The helm's attention channel for non-status events (`~/.lobstah/notices/`): sign-ons, first parks, 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`), and free-space holds (`disk-held`, `disk-cleared`). 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; tend always shows the recent tail. |
344
+ | notice | The helm's attention channel for non-status events (`~/.lobstah/notices/`): sign-ons, first parks, 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`), and worktrees released after their PR merged (`worktree-released`, one per cull pass). 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; tend always shows the recent tail. |
254
345
 
255
346
  Delivery routes by ownership, same as watches: a continuation for a chain
256
347
  claimed by a live trap is addressed back to that trap and stays sticky.
@@ -296,11 +387,11 @@ never until someone acknowledges it.
296
387
  | ---- | ------------ | ----------- |
297
388
  | `question` | The dispatch's last status is `needs-decision` or `blocked`. | Any newer status entry. |
298
389
  | `landed` | The dispatch is `done` or `failed` after its grounds' reported-through cursor (the grounds listing the repo, else `fleet`; at most 24 h back). Opt-in. | `man report` (or the helm park's digest) advances the cursor. |
299
- | `pr:draft` | Evidence `pr` is open and draft. | Ready for review, merged, or closed. |
300
- | `pr:review` | Evidence `pr` is open with `review.unresolvedThreads > 0` or `review.changesRequested`. | Every thread resolved and no changes requested, or merged / closed. |
301
- | `pr:checks` | Evidence `pr` is open with a failed check on the observed head. | Green on the head, or merged / closed. |
302
- | `pr:conflict` | Evidence `pr` is open and `mergeStateStatus` is `DIRTY` (GitHub: conflicting with its base). | The merge state leaves `DIRTY` (rebased or merged clean), or merged / closed. |
303
- | `pr:ready` | Evidence `pr` is open, not draft, no `pr:review` condition holds, `mergeStateStatus` is mergeable (`CLEAN`, `HAS_HOOKS`, or `UNSTABLE` — the last only means non-required checks are red, which `pr:checks` already carries), and it is approved — or every check passed with none pending. `DIRTY`, `BEHIND`, `BLOCKED`, and `UNKNOWN` never yield ready. | Merged or closed (or a review condition arises, or the merge state stops being mergeable). |
390
+ | `pr:draft` | An open PR is a draft, and the user opted into this kind. | Ready for review, merged, or closed. |
391
+ | `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. |
392
+ | `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. |
393
+ | `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. |
394
+ | `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. |
304
395
 
305
396
  `pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
306
397
  and carry `prUrl`, `number`, and the fields they derive from. Unconsumed
@@ -308,15 +399,12 @@ man-owned watch events also list, as `watch`: machinery wakes, always on.
308
399
  Only `question` and `watch` make the verdict `needs-attention` or arise in
309
400
  the digest; the rest are things to look at, not stalls.
310
401
 
311
- **The on-the-hook rule.** A `pr:review` or `pr:checks` item is suppressed
312
- while a worker owns the problem: a queued or active dispatch in the PR's
313
- chain (the evidence owner and its `followUp` descendants) that is a pickup
314
- feedback round — pickup's map records it with kind `review` — or the `pr:`
315
- watch's fix continuation — the watch records it as `lastFollowUpId`. It
316
- reappears when that dispatch finishes without clearing the condition.
317
- `question`, `landed`, `pr:draft`, `pr:conflict`, and `pr:ready` are never
318
- suppressed — a rebase is the human's or the helm's call, never assumed to be
319
- the fix continuation's.
402
+ **The on-the-hook rule.** Repairable conflict, check, and requested-review
403
+ conditions on an owned PR stay off attention while lobstah can act. A
404
+ queued or active pickup feedback round or watch continuation also suppresses
405
+ its review or check item. A blocked or exhausted repair raises attention
406
+ with its reason. `pr:draft` is opt-in; an explicit `attentionKinds` list is
407
+ used unchanged.
320
408
 
321
409
  **Answered questions.** A `question` stands only while no message to the
322
410
  dispatch is newer than its latest `needs-decision` / `blocked` entry. A
@@ -331,6 +419,8 @@ reminder loop apply the same predicate.
331
419
  | Word | Meaning |
332
420
  | ---- | ------- |
333
421
  | 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), `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`). |
422
+ | 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. |
423
+ | 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. |
334
424
 
335
425
  ## Exit codes
336
426
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstah",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Harness-agnostic, token-efficient supervision framework for coding agents",
5
5
  "license": "MIT",
6
6
  "author": "aequitas labs LLC",