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/pickup.md CHANGED
@@ -120,7 +120,7 @@ someone other than the configured identity, never a lobstah marker comment.
120
120
 
121
121
  The PR maps back to a dispatch two ways: a `lobstah/<uuid>` branch names it
122
122
  directly, and any other branch — a soaked session's PR — resolves through
123
- the PR URL its worker reported as evidence (`report done --pr <url>`). A PR
123
+ the PR URL in its worker's evidence (`report --pr <url>`, or a trap's beat). A PR
124
124
  that maps to neither is not lobstah's to answer.
125
125
 
126
126
  Each feedback **round** is keyed by the newest feedback event, so a new
@@ -267,6 +267,8 @@ The loop's doctrine, in full:
267
267
  drift from the forge's own rules.
268
268
  - **Dedup by approval, not by PR.** A specific approval merges at most once; a
269
269
  new push invalidates it and the gate waits for a fresh one.
270
+ When an approval no longer covers the head, pickup re-requests review from
271
+ that approver once per push.
270
272
  - **Stacks merge through the forge's stack-aware path.** A PR in a native
271
273
  stack goes through GitHub's asynchronous stack merge API; a standalone PR
272
274
  through the ordinary merge call. `method` is per repo and applies to both.
@@ -280,7 +282,7 @@ A PR behind its base splits deterministically:
280
282
  | Condition | Action |
281
283
  |---|---|
282
284
  | Behind, no conflict | Update the branch through the forge API, re-enter the gate next tick |
283
- | Behind, real conflict | Write a rebase chore — brief: rebase onto base, resolve, push — and re-enter the gate when it completes |
285
+ | Behind, real conflict | Write a rebase chore — brief: rebase onto base, resolve, push to the PR's branch — and re-enter the gate when it completes |
284
286
 
285
287
  Rebase chores go through the **chore lane** (`~/.lobstah/chores/`, defined in
286
288
  the [design's queue contract](design.md#queue-contract)), never the primary queue. Same descriptor schema,
@@ -291,6 +293,12 @@ queue, and `lobstah ls` stays a list of things a human asked for.
291
293
  Chores report to no tracker. The merge loop consumes the chore's status file
292
294
  directly, holds its own PR-to-chore mapping, and bounds the attempt at one: a
293
295
  failed rebase comments on the PR, applies the `needs-human` label, and stops.
296
+ A rebase chore's descriptor names its PR (`pr`): the runner pushes no branch
297
+ and opens no PR for it. The brief tells the worker to push to the PR's head
298
+ branch only and, on a non-fast-forward rejection, to fetch, rebase onto the
299
+ moved head, and push with `--force-with-lease` again, at most three times.
300
+ When it cannot push, the chore reports `failed` with the rejection text and
301
+ the moved head.
294
302
  The doctrine stays whole — the deterministic program handles everything
295
303
  mechanical, and the moment resolution requires judgment it becomes a
296
304
  supervised dispatch. It just doesn't become *work*.
@@ -341,7 +349,7 @@ cron-script fleets it replaces, whose watchers also died with their host.
341
349
 
342
350
  The merge loop is already fetching the forge's view of every candidate PR each
343
351
  tick — so it persists what it saw (`pickup/merge-view.json`): per open PR the
344
- head sha, mergeable state, and gate verdict (`waiting-approval`,
352
+ head sha, mergeable state, and gate verdict (`waiting-approval`, `stale-approval`,
345
353
  `behind-updated`, `conflict-chore:<uuid>`, `rebase-failed`, `blocked`,
346
354
  `draft`), and per PR that *left* the open set, its disposition — one extra
347
355
  lookup answers whether it merged or closed, so "merged in the last 24h" is
@@ -16,13 +16,32 @@ 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. 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. |
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. A paused headless dispatch is **parked**: no process and no slot (see [Parked](#parked)). | 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
 
23
23
  `done` and `failed` are the **terminal verbs** (`TERMINAL_VERBS`): once
24
24
  logged, process state stops mattering and the daemon finalizes.
25
25
 
26
+ A terminal verb from the worker is final. No later time limit, harness
27
+ error, kill, cancel, or daemon restart adds a verb after it. From the
28
+ moment the worker reports it:
29
+
30
+ - the wall clock stops for that dispatch;
31
+ - the dispatch holds no headless slot and does not count toward
32
+ `maxConcurrent`;
33
+ - `lobstah daemon status` and `lobstah daemon restart` do not count it as
34
+ active, so a restart needs no `--force`;
35
+ - `man tend`, the glass, and `lobstah doctor` show it as done.
36
+
37
+ At the end of that turn the runner ends the session and waits
38
+ `[limits].exitGraceSecs` (30 seconds) for the harness to exit. Then it
39
+ stops the harness and the processes it started, releases the worktree lock,
40
+ and moves the dispatch to `done/`. A cancel that arrives after the report
41
+ stops the harness at once and leaves the verb as reported. The daemon stops
42
+ a runner that is still alive `exitGraceSecs` plus `wedgeThresholdSecs` after
43
+ the report; it does not restart the dispatch.
44
+
26
45
  Source of truth: `VERBS` in `packages/core/src/types.ts`.
27
46
 
28
47
  ## Waiting on
@@ -59,12 +78,61 @@ Effects of `paused` with `--waiting-on`:
59
78
  that it sweeps as before, and the `trap-ghosted` notice says the pause
60
79
  expired.
61
80
  - 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`.
81
+ silent, and its `wallClockSecs` limit does not run while it is paused.
82
+ It is parked and holds no slot (see [Parked](#parked)).
83
+ - With `--waiting-on pr` or `--waiting-on review`: when the PR it waits on
84
+ merges, the daemon finishes the dispatch `done` (`the PR merged: <url>`);
85
+ closed without merge, `failed`. The PR is the `--link` when it names a
86
+ GitHub PR, else the dispatch's own PR, else its chain's PR. Every paused
87
+ dispatch in the chain that waits on the PR is finished. The report
88
+ registers the watch of the dispatch's own PR when it has none.
64
89
  - No attention, no notice, no pet. It is a state, not a question.
65
90
 
66
91
  Source of truth: `WAITING_ON` in `packages/core/src/types.ts`.
67
92
 
93
+ ## Parked
94
+
95
+ A headless dispatch whose worker's last report is `paused`. At the end of
96
+ that turn the runner ends the session, stops the processes the harness
97
+ started, and exits without adding a verb. The dispatch stays in `active/`
98
+ and keeps its worktree lock.
99
+
100
+ - It holds no slot: it does not count toward `maxConcurrent` or
101
+ `choreConcurrent`, and `lobstah daemon restart` needs no `--force` for it.
102
+ - It wakes when a message reaches its inbox (`lobstah send <id>`), or when
103
+ its `--until` time passes. The daemon then starts a runner that resumes
104
+ the same session, when a slot is free, before it claims queued work. The
105
+ first prompt says why it woke and carries the messages; the first status
106
+ note is `woke from pause: <why>`.
107
+ - A cancel finalizes it `failed` without a runner. A merged or closed PR
108
+ it waits on finishes it (see [Waiting on](#waiting-on)).
109
+ - `man tend` lists it in the `parked (no slot)` table and counts
110
+ `parked: N (no slot)` beside the slots; `lobstah daemon status` prints
111
+ `slots`, `parked`, and `parkedOn`; `lobstah doctor`'s `daemon` row and the
112
+ glass header show it too.
113
+
114
+ A trap's paused catch keeps its session: a trap never holds a headless
115
+ slot.
116
+
117
+ Source of truth: `isParked` and `parkedDispatches` in
118
+ `packages/core/src/slots.ts`; the runner's park in
119
+ `packages/runner/src/drive.ts`.
120
+
121
+ ## Human gate
122
+
123
+ A CI check that fails by design until a person approves the change. No
124
+ code change turns it green. A human gate starts no PR repair and no CI-fix
125
+ continuation on its PR. The gates of a PR come from
126
+ `[repos.<key>].humanGateChecks` (names; `*` matches any run of characters)
127
+ and from `lobstah report <id> <verb> --human-gate "<check>"`, which records
128
+ the name in the worker's evidence and on the PR record (`humanGates`).
129
+ Apart from gates, each failing check gets at most one repair round per PR,
130
+ check name, and head commit (`repair.checks` on the PR record;
131
+ `checkRounds` on a pick-delivered PR watch).
132
+
133
+ Source of truth: `humanGatesFor`, `repairableChecks`, and `unrepairedChecks`
134
+ in `packages/core/src/pr-repair.ts`.
135
+
68
136
  ## Reconciled state
69
137
 
70
138
  What an observer should *believe*, combining the status log with event
@@ -133,7 +201,7 @@ daemon — it never reaches a tracker.
133
201
  | --- | --- | --- |
134
202
  | `unclaimed` | Descriptor present, no runner yet. | Spawn a runner. |
135
203
  | `busy` | Runner alive, activity within the wedge threshold. | Nothing. |
136
- | `terminal` | Terminal verb logged. | Finalize once the process is gone. |
204
+ | `terminal` | Terminal verb logged. | Finalize once the process is gone. A runner still alive `exitGraceSecs` + `wedgeThresholdSecs` after the report: stop its group (SIGTERM, then SIGKILL past twice that). Never restart. |
137
205
  | `dead` | Pid verified gone (pid + process-start-time, so pid reuse can't lie). | Respawn with session resume, bounded by `maxRestartAttempts`; then `failed`. |
138
206
  | `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. |
139
207
  | `unknown` | Contradictory or missing evidence. | Touch nothing; log it. |
@@ -141,7 +209,9 @@ daemon — it never reaches a tracker.
141
209
  Dead and wedged get opposite treatment on purpose: a dead process is safe to
142
210
  respawn; a wedged one must be killed first or two writers share a worktree. A
143
211
  pending cancel preempts all of this — a cancelled dispatch finalizes as
144
- `failed` ("cancelled by request") and never re-enters the ladder.
212
+ `failed` ("cancelled by request") and never re-enters the ladder. A cancel
213
+ of a dispatch whose worker already reported `done` or `failed` stops the
214
+ runner and keeps the reported verb.
145
215
 
146
216
  Source of truth: `Classification` in `packages/supervisor/src/liveness.ts`.
147
217
 
@@ -203,7 +273,7 @@ session id's UUID version (v7 codex, v4 claude), else the descriptor.
203
273
  | Word | Meaning |
204
274
  | ---- | ------- |
205
275
  | 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. |
276
+ | 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. Use `dispatch --follow-up` to choose a new worker, harness, or model. |
207
277
  | 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
278
  | 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`. |
209
279
  | 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. |
@@ -256,10 +326,13 @@ normalized to that key) installs the shipped check, `lobstah watch
256
326
  check-pr`: one read-only `gh pr view` per cycle, diffed against the
257
327
  previous observation that the cursor carries, plus — while the PR is open —
258
328
  one read-only `gh api graphql` query for `reviewThreads { isResolved }`,
259
- which `gh pr view --json` cannot return (no bodies are requested). `report <id> done --pr
260
- <url>` registers the same watch owned by `dispatch:<id>` (idempotent;
261
- `--no-watch` opts out). `lobstah watch backfill --apply` registers watches
262
- for PRs in old dispatch history; it is a dry run without `--apply`. No other
329
+ which `gh pr view --json` cannot return (no bodies are requested). `report <id> <verb> --pr
330
+ <url>` (any verb but `failed`) registers the same watch owned by
331
+ `dispatch:<id>` (idempotent; `--no-watch` opts out). A trap's beat
332
+ (`lobstah soak beat`) registers it the same way when it finds a PR on the
333
+ trap's branch. `lobstah watch backfill --apply` registers watches
334
+ for PRs in old dispatch history and fills the title of PR records without
335
+ one; it is a dry run without `--apply`. No other
263
336
  path registers a PR watch: read commands (`catch`, `man tend`, `status`,
264
337
  `ls`, `prs`, the glass) never do. **Owner:** `packages/core/src/pr.ts`
265
338
  (derivation, badge) and `apps/cli/src/pr-watch.ts` (check, registration,
@@ -275,16 +348,27 @@ evidence).
275
348
  | `merge-state` | `mergeStateStatus` changed (`value`). The PR record carries conflicts into the repair planner. |
276
349
  | `draft` | Draft flipped (`value`). Evidence only. |
277
350
  | `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. |
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. |
351
+ | 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, and ends in `repair waits: <heldBy>` while a repair waits; tend, `catch`, and the glass share the badge. |
352
+ | waiting repair | `repair.status: waiting` on a PR record: a repair is due but is not queued. `heldBy` names the holder: `wt:<trap>` or `dispatch:<id8>` (a live worker holds the PR's head branch or the head branch of a PR below it in the stack), `helm` (the helm cancelled a repair of this PR), `hold` or `dispatch:<id8>` (`watch hold`), `settle` (the head, base head, or failing checks changed less than `[watch].repairSettleSecs` ago; `until` says when), or `checks` (the latest run of a failing check is in progress or passed). `reason` says what holds it. A wait is not an attempt and raises no attention item. |
353
+ | repair chore | A daemon repair or rebase runs in the `chore` lane under `[limits].choreConcurrent`. A repair chore for a trap-built PR waits for its live owning trap up to `[watch].repairTrapWaitSecs` (default 600), then runs headless in its own PR-branch checkout. A headless-built PR's repair runs headless in its origin worktree when safe. No headless chore uses a trap's worktree. This bounded fallback applies only to system repair chores; a person's addressed work is sticky and never falls back. Tend, daemon status, doctor, and the glass show the PR, lane, worker, and trap wait. |
354
+ | live worker | An active headless dispatch, or a trap with an open catch. It holds a branch that its worktree has checked out, that its current branch tracks, or that it pushed during its current dispatch (evidence `pushes`). It holds a PR that its evidence names or that its chain owns. |
355
+ | evidence `pushes` | `[{ branch, at }]`: the branches a dispatch pushed, as lobstah saw them. The runner records its own pushes; the runner records a headless worker's `git push` from the harness event stream (branch names only); `soak beat` records a trap's `git push`. |
356
+ | descriptor `pr` | `{ url, headRefName?, headSha? }`: the existing PR a dispatch works on. A PR repair and a pickup rebase chore carry it. The runner pushes no branch and opens no PR for such a dispatch; its worker pushes to the PR's head branch. |
357
+ | push rule | What a repair or rebase brief tells its worker: push only to the PR's head branch; on a non-fast-forward rejection, fetch, rebase the commits onto the moved head again, and push with `--force-with-lease` on the head just fetched, at most three times; a hook failure from a real test or type error is not retried; when it cannot push, report `failed "push rejected: <rejection text>; moved head <sha>"` and leave the PR as it was. That report marks the PR's repair `blocked` at the moved head and posts a `push-failed` notice. |
279
358
  | 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. |
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. |
359
+ | PR record | `~/.lobstah/prs/<owner>__<repo>__<n>.json` — the PR's latest observation keyed by the PR, not by a dispatch: the evidence `pr` object (with `title`, read on every check; a title change is not a state change) plus `key`, `repo` (`<owner>/<repo>`), `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR), and `firstSeenAt` (the time of the first observation; written once, never rewritten). `firstSeenAt`, then the PR number, is the order of every PR list. A record from before `firstSeenAt` existed sorts by number at the earliest `firstSeenAt` in the set, and its next observation writes that time as its `firstSeenAt`. **Owner:** `packages/core/src/prs.ts` (`upsertPr`, `readPrs`); the one writer is the preset's observation path (`observePr`), on every observation, man-owned or dispatch-owned — a dispatch-owned one also stamps that dispatch's evidence, which stays the per-dispatch view. Tend's `pr:*` kinds and `pr:ready` 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
360
 
282
361
  Every event carries `headSha`. A dispatch-owned PR watch records work events,
283
362
  then the repair planner decides whether to follow up. One repair runs per PR
284
363
  at a time. A first observation starts none. Repairs use the newest owning
285
364
  dispatch and the PR's observed base branch. The watch checks commit ownership
286
365
  before enqueueing. It stops after `[watch].maxRepairsPerPr` attempts per head
287
- SHA. The rest is evidence.
366
+ SHA. A repair waits (`repair.status: waiting`) while a live worker holds the
367
+ PR's head branch or a branch below it in the stack, until the PR has settled
368
+ for `[watch].repairSettleSecs`, while a failing check's latest run is in
369
+ progress or passed, and while the PR's watch is held. A wait is not an
370
+ attempt. A repair whose worker reported `failed "push rejected: ..."` is `blocked` at the
371
+ moved head and at the head it started from. The rest is evidence.
288
372
  Merged and closed reach the helm as a `pr-merged` / `pr-closed` notice,
289
373
  posted once by whichever process first records the open → terminal
290
374
  transition on the **PR record** — not by event routing. A PR whose first
@@ -293,7 +377,11 @@ record is already terminal gets the notice only when it ended in the last
293
377
 
294
378
  One pick watch cycle forks at most `[watch].maxForksPerCycle`
295
379
  continuations (default 3). Generic watches over the cap are held (`heldAt`
296
- on the watch); PR repairs wait for the next cycle.
380
+ on the watch); PR repairs wait for the next cycle. A watch hold also comes
381
+ from `lobstah watch hold <key> [--for <id>]` and from
382
+ `lobstah cancel` on a repair dispatch. The watch then carries `heldReason`,
383
+ `heldBy`, and, for `--for`, `heldFor`: the hold ends when that dispatch ends.
384
+ `lobstah watch release` ends any hold.
297
385
 
298
386
  A man-owned PR watch (no `--for`) delivers as attention only what needs a
299
387
  human (`manEvents`, beside `workEvents` in `apps/cli/src/pr-watch.ts`): a
@@ -334,14 +422,16 @@ worktree is refused (the session lock); a stale one is adopted.
334
422
  | Word | Meaning |
335
423
  | ---- | ------- |
336
424
  | `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. |
425
+ | `stow` | Sign the trap off (in the worktree, or elsewhere by session id: `--session <id>` or `$CLAUDE_CODE_SESSION_ID`); an unfinished catch requeues, done/failed finalizes in `done/`, and a cancelled catch finalizes as failed. Unread messages 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, commits absent from its upstream, or no upstream. `--force` explicitly allows discarding unsaved checkout files; `--keep` still keeps it. 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. |
338
426
  | 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. |
339
427
  | 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. |
340
428
  | 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. |
341
429
  | 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)). |
430
+ | ghost trap | A registration whose heartbeat **and** beat lapsed past `[soak].ttlSecs` **after having parked at least once**. The sweep removes the registration, requeues only unfinished catches, finalizes done/failed in `done/`, and posts a `trap-ghosted` notice. It removes a soak-created checkout only when clean and pushed to its upstream; dirty, unpushed, no-upstream, or unreadable checkouts stay, protected from later culls too. The notice includes path, branch, modified-file and unpushed-commit counts, or `unknown` when unavailable. A kept anchor preserves the address on re-soaking. A fresh report or beat keeps a working session out of the sweep; a fresh beat also holds the session lock. A catch whose last report is `paused` keeps its trap until the pause expires ([Waiting on](#waiting-on)). A daemon tick gap longer than the soak TTL starts one full TTL of resume grace before any ghost sweep. |
343
431
  | 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. |
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. |
432
+ | reserved trap | A trap `lobstah trap reserve` created before any session signs on as it: a name, a `wt:` id, and a one-time ticket, shown as `starting`. Work addressed to it waits. A session redeems the ticket with `soak --ticket` (or `LOBSTAH_TRAP_TICKET`) and signs on as that trap. Unredeemed past its deadline, it fails with a `trap-start-failed` notice and its work stays queued. `stow --wt <name>` withdraws it. |
433
+ | trap request | A trap the human asked for from the glass's **+ New trap** button: a `trap-request` request in `requests/<id>.json` with a repo and a harness. It wakes the helm as a `trap-request` event; the helm reserves it with `trap reserve --request <id>`, which closes it. |
434
+ | notice | The helm's attention channel for non-status events (`~/.lobstah/notices/`): reservations (`trap-starting`), reservations that did not start (`trap-start-failed`), trap requests from the glass (`trap-request`), sign-ons, 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`), worktrees released after their PR merged (`worktree-released`, one per cull pass), a repair or rebase that could not push to its PR's branch (`push-failed`), and a human's answer to a decision (`decision-answer`; its ref is the request id in `~/.lobstah/requests/`). A trap leaves the registry only through a `trap-stowed` or `trap-ghosted` notice — the end-state is always explicit. Consumed by `man wait`/the park; tend always shows the recent tail. |
345
435
 
346
436
  Delivery routes by ownership, same as watches: a continuation for a chain
347
437
  claimed by a live trap is addressed back to that trap and stays sticky.
@@ -375,6 +465,11 @@ verbs stay open. A stale helm reserves nothing.
375
465
 
376
466
  ## Attention contract
377
467
 
468
+ An **image overlay** is the glass's in-page view of a decision, report, or
469
+ dispatch or trap attachment image. It closes with Escape, its close button,
470
+ or a click on its backdrop. Images pasted into a decision answer are
471
+ attachments and use the same size, type, and count limits as picked files.
472
+
378
473
  **Attention** is what stands waiting for a human to look: `man tend`'s
379
474
  `attention` list, which the desktop pet and the glass walk across the
380
475
  screen. It is derived in one place (`apps/cli/src/tend.ts`) and nowhere
@@ -385,25 +480,28 @@ never until someone acknowledges it.
385
480
 
386
481
  | Kind | Stands while | Clears when |
387
482
  | ---- | ------------ | ----------- |
388
- | `question` | The dispatch's last status is `needs-decision` or `blocked`. | Any newer status entry. |
483
+ | `question` | The dispatch's last status is `needs-decision` or `blocked`. Held (`held: true`; not in the pet, the glass, or notifyCommand) while a live helm for its grounds has not ended a turn since it was filed. Hidden while a decision on the same dispatch, asked at or after it, is on disk (when `decision` is enabled). | Any newer status entry, or a message newer than it. |
484
+ | `decision` | The helm asked the human with `lobstah man ask` and no answer is recorded. Key `decision:<rid>`. | Answered (the glass or `man answer`), withdrawn (`man ask --withdraw`), or replaced by a newer ask on the same dispatch. |
389
485
  | `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. |
390
486
  | `pr:draft` | An open PR is a draft, and the user opted into this kind. | Ready for review, merged, or closed. |
391
487
  | `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
488
  | `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
489
  | `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
490
  | `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. |
491
+ | `report` | A filed report (`report --report`, `man file`) has no ack for this filing. Opt-in. Key `report:<lane>:<uuid>` or `report:helm:<grounds>:<rid>`. | Not cleared: it stays listed until culled. `lobstah attention ack <key>` acks it, and a newer report in the same chain acks the older; an acked report no longer walks. |
395
492
 
396
493
  `pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
397
494
  and carry `prUrl`, `number`, and the fields they derive from. Unconsumed
398
495
  man-owned watch events also list, as `watch`: machinery wakes, always on.
399
- Only `question` and `watch` make the verdict `needs-attention` or arise in
400
- the digest; the rest are things to look at, not stalls.
496
+ Only `question`, `decision`, and `watch` make the verdict `needs-attention`;
497
+ only `question` and `watch` arise in the digest. The rest are things to look
498
+ at, not stalls.
401
499
 
402
500
  **The on-the-hook rule.** Repairable conflict, check, and requested-review
403
501
  conditions on an owned PR stay off attention while lobstah can act. A
404
502
  queued or active pickup feedback round or watch continuation also suppresses
405
503
  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
504
+ with its reason. A waiting repair raises none. `pr:draft` is opt-in; an explicit `attentionKinds` list is
407
505
  used unchanged.
408
506
 
409
507
  **Answered questions.** A `question` stands only while no message to the
@@ -418,7 +516,7 @@ reminder loop apply the same predicate.
418
516
 
419
517
  | Word | Meaning |
420
518
  | ---- | ------- |
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`). |
519
+ | ack | `~/.lobstah/acks/<item-key>.json` — `{ key, kind, stateHash, at, by }`, written only by `lobstah attention ack` (removed by `unack`, by the CLI's `man tend` / `attention` when its `stateHash` goes stale, and by `cull` when the item is gone). **Display-only**: it hides the item from the desktop pet and the glass lobs while the item's `stateHash` is unchanged; `man tend --json` keeps the item with `acked: { at, by }`, and `man wait`, the park, reminders, and `notifyCommand` never read acks. Item keys: `<lane>:<uuid>` (question, landed), `decision:<rid>`, `pr:<owner>/<repo>#<n>` (all of a PR's `pr:*` kinds — one ack covers them), `watch:<key>`. `stateHash` covers the status entry (question, landed) or the PR's head sha plus every evidence field a `pr:*` kind stands on (not `observedAt`). |
422
520
  | 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
521
  | 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. |
424
522
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstah",
3
- "version": "0.5.13",
3
+ "version": "0.6.0",
4
4
  "description": "Harness-agnostic, token-efficient supervision framework for coding agents",
5
5
  "license": "MIT",
6
6
  "author": "aequitas labs LLC",