lobstah 0.3.2 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -8
- package/dist/main.js +5640 -1027
- package/dist/runner.js +503 -143
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/lob-sprite.png +0 -0
- package/docs/assets/lob.png +0 -0
- package/docs/assets/star.png +0 -0
- package/docs/configuration.md +24 -0
- package/docs/design.md +7 -1
- package/docs/lobsterman.md +218 -23
- package/docs/vocabulary.md +149 -17
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/configuration.md
CHANGED
|
@@ -14,6 +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: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: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
18
|
| `remindSecs` | `900` | An unanswered `needs-decision`/`blocked` re-fires to `man wait`/`man haul` on this interval until answered. `0` = report once only. |
|
|
18
19
|
|
|
19
20
|
## `[repos.<key>]` — workspace definitions
|
|
@@ -52,6 +53,7 @@ Same three keys as the per-repo block. Precedence for every harness setting:
|
|
|
52
53
|
| `maxRestartAttempts` | `2` | Bounded restart ladder for dead and wedged runners. |
|
|
53
54
|
| `wallClockSecs` | `3600` | Hard per-dispatch ceiling, enforced by the runner. |
|
|
54
55
|
| `choreRetentionDays` | `7` | Completed chores age out of `chores/done/`. |
|
|
56
|
+
| `attachmentMaxBytes` | `26214400` (25 MiB) | Maximum size of each file supplied with repeatable `dispatch --attach` or `send --attach`. |
|
|
55
57
|
|
|
56
58
|
## `[soak]` — soaking sessions (`lobstah soak`)
|
|
57
59
|
|
|
@@ -60,6 +62,28 @@ Same three keys as the per-repo block. Precedence for every harness setting:
|
|
|
60
62
|
| `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. |
|
|
61
63
|
| `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. |
|
|
62
64
|
|
|
65
|
+
## `[helm]` — the orchestrator seat (`lobstah man helm`)
|
|
66
|
+
|
|
67
|
+
| Key | Default | Meaning |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `ttlSecs` | `1800` | Heartbeat age past which a helm registration is stale: the next `man helm` claims it without `--take`. The park and `man brief` heartbeat it. |
|
|
70
|
+
| `reportSecs` | `900` | Minimum seconds between park-delivered digests for a helm session. The digest is also change-gated — quiet grounds deliver nothing regardless of cadence. |
|
|
71
|
+
|
|
72
|
+
## `[grounds.*]` — helm territories
|
|
73
|
+
|
|
74
|
+
One helm per grounds; a repo belongs to at most one grounds (`man helm`
|
|
75
|
+
refuses on overlap or an unknown repo key). With no `[grounds.*]` configured
|
|
76
|
+
there is one implicit `fleet` grounds covering every repo — the partition
|
|
77
|
+
only exists when asked for.
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[grounds.base]
|
|
81
|
+
repos = ["homebase", "matcha"]
|
|
82
|
+
|
|
83
|
+
[grounds.aequitas]
|
|
84
|
+
repos = ["lobstah", "lavish"]
|
|
85
|
+
```
|
|
86
|
+
|
|
63
87
|
## `[pickup]` — tracker loops (`lobstah pick`)
|
|
64
88
|
|
|
65
89
|
| Key | Default | Meaning |
|
package/docs/design.md
CHANGED
|
@@ -444,7 +444,13 @@ task's diff. That contaminates evidence and falsely serializes merges.
|
|
|
444
444
|
|
|
445
445
|
The daemon spawns one runner per dispatch with `setsid`, then records its pid and
|
|
446
446
|
process start time. The runner drives the adapter; the daemon supervises the
|
|
447
|
-
runner and never touches the harness directly.
|
|
447
|
+
runner and never touches the harness directly. A turn that ends with the
|
|
448
|
+
worker's last report on `needs-decision`, `blocked`, or `paused` does not end
|
|
449
|
+
the session: a headless worker waiting on a question stays alive until
|
|
450
|
+
answered, cancelled, or the wall clock — the runner polls the inbox, touches the
|
|
451
|
+
event stream each poll so the wedge detector sees a live wait, delivers the
|
|
452
|
+
answer into the next turn, and stamps `working`. Only a turn ending on `working`
|
|
453
|
+
(or no report) with an empty inbox ends the session and is stamped `done`.
|
|
448
454
|
|
|
449
455
|
```ts
|
|
450
456
|
// inside the runner
|
package/docs/lobsterman.md
CHANGED
|
@@ -18,6 +18,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
20
|
|
|
21
|
+
**The helm is harness-agnostic: drive the fleet from whichever session you
|
|
22
|
+
prefer.** The contract is the CLI, not the harness — a Claude Code session,
|
|
23
|
+
a Codex session (hooks since v0.114), or anything with a terminal via the
|
|
24
|
+
foreground loop (`man wait` for the lobsterman, `soak --wait` for workers)
|
|
25
|
+
holds the same seat with the same verbs. Workers are equally mixed:
|
|
26
|
+
dispatches pick their harness per item (`--harness claude|codex`), so a
|
|
27
|
+
Codex helm can run Claude workers and the reverse. Sign-on records which
|
|
28
|
+
harness took the helm, and `swap` moves an in-flight dispatch across
|
|
29
|
+
harnesses mid-stream — the worktree, not the conversation, is the durable
|
|
30
|
+
layer.
|
|
31
|
+
|
|
21
32
|
## Set it up
|
|
22
33
|
|
|
23
34
|
1. Install lobstah, configure your repos, start the daemon
|
|
@@ -95,6 +106,106 @@ view](pickup.md#merge-view) pickup persists each tick, so PR state is at most
|
|
|
95
106
|
one poll interval stale without tend making a single network call. `--json`
|
|
96
107
|
emits the full report for dashboards and scripts to render.
|
|
97
108
|
|
|
109
|
+
### PR state after done
|
|
110
|
+
|
|
111
|
+
A dispatch reports `done` when its PR opens; `report done --pr <url>`
|
|
112
|
+
registers a `pr:` watch for the chain so the PR stays observed (see the
|
|
113
|
+
PR preset in [vocabulary.md](vocabulary.md#the-pr-preset); `--no-watch`
|
|
114
|
+
opts out). What you get depends on what runs:
|
|
115
|
+
|
|
116
|
+
- **Only the helm park or `man wait`** (no service): PR state badges in
|
|
117
|
+
`man tend`, `lobstah catch`, and the glass (`merged`, `draft`, `review`,
|
|
118
|
+
`checks 1/2 failed`, `green`), and a `pr-merged` / `pr-closed` notice
|
|
119
|
+
when the PR lands. The inline poller observes at `[pickup].pollSecs`
|
|
120
|
+
(default 45 s), one `gh pr view` per PR per cycle, and forks nothing.
|
|
121
|
+
- **With `lobstah pick`** (watch-only mode needs no tracker; `lobstah pick
|
|
122
|
+
install` runs it as a service for a steady cadence): all of the above,
|
|
123
|
+
plus CI-fix continuations — a failing check forks the chain with a brief
|
|
124
|
+
naming the PR, the check, its details URL, and the head sha — and
|
|
125
|
+
review-decision continuations for repos `[pickup.github]` doesn't cover.
|
|
126
|
+
|
|
127
|
+
`gh` must be on PATH and authenticated; if it isn't, the done report still
|
|
128
|
+
succeeds and the watch's check records `lastError`.
|
|
129
|
+
|
|
130
|
+
Watching a PR nobody dispatched — `lobstah watch add pr:<owner>/<repo>#<n>`
|
|
131
|
+
with no `--for` — is how a helm follows a human's PR, or one whose
|
|
132
|
+
dispatch chain was culled. Every observation lands in a PR record keyed by
|
|
133
|
+
the PR, so it shows in the glass PRs tab and stacks and in tend's `pr:*`
|
|
134
|
+
attention kinds exactly like a dispatched PR (its dispatch chain column is
|
|
135
|
+
empty). It stays quiet while it's fine: only a failing check or a changes
|
|
136
|
+
request surfaces as a watch event; a merge or close arrives as a notice.
|
|
137
|
+
|
|
138
|
+
An observed PR joins tend's attention list by kind — `pr:draft`,
|
|
139
|
+
`pr:review` (unresolved threads or changes requested), `pr:checks` (a red
|
|
140
|
+
head), `pr:ready` (approved or all green) — so it crawls in the glass and
|
|
141
|
+
the desktop pet until its clear condition holds; clicking it opens the PR.
|
|
142
|
+
`pr:review` and `pr:checks` stay off the screen while a worker already owns
|
|
143
|
+
them (a pickup feedback round or the watch's fix continuation in flight).
|
|
144
|
+
`attentionKinds` in `config.toml` picks the kinds; `landed` is opt-in. See
|
|
145
|
+
the [attention contract](vocabulary.md#attention-contract). A PR is
|
|
146
|
+
something to look at, not a stall: it never flips the verdict to
|
|
147
|
+
`needs-attention` and stays out of the digest.
|
|
148
|
+
|
|
149
|
+
### The spyglass
|
|
150
|
+
|
|
151
|
+
`lobstah glass [--port <n>]` serves tend as a live web page on 127.0.0.1
|
|
152
|
+
(default port 4949): the fleet verdict and attention questions, every
|
|
153
|
+
dispatch with its full brief, status log, inbox, and evidence, each trap
|
|
154
|
+
with its lifecycle notices, message history, and catches, the notices
|
|
155
|
+
tail, the merge view, and watches — with filters, a table/cards toggle,
|
|
156
|
+
and the helm identified by name. It is strictly read-only and consumes no
|
|
157
|
+
cursor: looking through the glass changes nothing, so it needs no helm and
|
|
158
|
+
threatens nothing. Links out are copyable commands (`lobstah attach`,
|
|
159
|
+
`claude --resume`), never click-to-exec — localhost HTTP is reachable by
|
|
160
|
+
any webpage, so the glass exposes no endpoint that acts. The ⚙ popover's two
|
|
161
|
+
preferences — table or cards, and whether lobsters crawl the page — are
|
|
162
|
+
per-browser, kept in that browser's localStorage and never on disk.
|
|
163
|
+
|
|
164
|
+
This is where "is the agent alive?" belongs: the helm's heartbeat age on a
|
|
165
|
+
page, not periodic proof-of-life turns in a transcript.
|
|
166
|
+
|
|
167
|
+
### The periodic report
|
|
168
|
+
|
|
169
|
+
`lobstah man tend` is the full picture on demand; `lobstah man report` is the
|
|
170
|
+
**delta** since the last acknowledged report — catches landed (with their
|
|
171
|
+
notes and PRs), attention newly arisen, what still waits, and the fleet
|
|
172
|
+
verdict. It advances a "reported through" cursor when it prints — the
|
|
173
|
+
explicit acknowledgment — so nothing is ever reported twice, and it says
|
|
174
|
+
`no change` when the delta is empty rather than re-dumping state. Standing
|
|
175
|
+
unanswered questions appear under `still-waiting` without counting as
|
|
176
|
+
change — reminders (`remindSecs`) own re-firing those.
|
|
177
|
+
|
|
178
|
+
Delivery is at-least-once by construction: the carriers that might not be
|
|
179
|
+
read (a `man wait` timeout in a background task) only **peek** at the delta,
|
|
180
|
+
so a digest lost with a dead task re-surfaces on the next timeout; only
|
|
181
|
+
`man report` (or a hook-delivered park digest, which lands in-context by
|
|
182
|
+
construction) marks it handled.
|
|
183
|
+
|
|
184
|
+
Every carrier shares the cursor (per grounds, for a helm):
|
|
185
|
+
|
|
186
|
+
- **The wait loop.** A `man wait` timeout (exit 3) prints the delta when
|
|
187
|
+
something changed, so a looping session gets periodic fleet reports for
|
|
188
|
+
free — see the loop idiom below.
|
|
189
|
+
- **The park.** A helm session's Stop-hook park delivers the digest as a wake
|
|
190
|
+
at `[helm].reportSecs` cadence — including the landed-then-idle case, where
|
|
191
|
+
the last catches finish and nothing is left in flight to wake for.
|
|
192
|
+
- **Direct call.** Anything with a clock — a gateway heartbeat, a cron — runs
|
|
193
|
+
`lobstah man report` and forwards the output when it is not `no change`.
|
|
194
|
+
Escalating a report to a human (a phone push) is the gateway layer's job,
|
|
195
|
+
not lobstah's.
|
|
196
|
+
|
|
197
|
+
The loop idiom needs no harness scheduler, because the timer is lobstah's own
|
|
198
|
+
blocking wait:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
lobstah man wait --timeout 900
|
|
202
|
+
# exit 0 → an event printed; handle it, then loop
|
|
203
|
+
# exit 3 → timeout; the delta digest printed above it when something changed
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A session running this loop reports into its own transcript whether or not a
|
|
207
|
+
human is watching — come back later and the transcript is the report.
|
|
208
|
+
|
|
98
209
|
## Getting woken instead of asked
|
|
99
210
|
|
|
100
211
|
Three escalation tiers, least to most invasive. All are built on
|
|
@@ -153,7 +264,7 @@ trapline; every orchestrator-facing command lives under `lobstah man`).
|
|
|
153
264
|
Install it from the project you'll run the lobsterman in:
|
|
154
265
|
|
|
155
266
|
```bash
|
|
156
|
-
# Easiest: the plugin ships the hooks + the lobsterman
|
|
267
|
+
# Easiest: the plugin ships the hooks + the lobsterman and trap skills, no settings
|
|
157
268
|
# edits — /plugin marketplace add aequitas-labs/lobstah, then
|
|
158
269
|
# /plugin install lobstah@lobstah (Claude Code and Codex v0.114+; Codex asks
|
|
159
270
|
# for a one-time hook trust review). Or wire the Claude hook by hand:
|
|
@@ -173,14 +284,16 @@ are preserved verbatim. What it writes:
|
|
|
173
284
|
|
|
174
285
|
`haul` gates itself twice: only a designated session parks (launch it with
|
|
175
286
|
`LOBSTAH_MAN=1 claude`, or `touch .lobstah-man` for a per-directory gate), and
|
|
176
|
-
only while dispatches are in flight —
|
|
177
|
-
event it blocks the stop with the event as context and
|
|
178
|
-
session re-parks automatically; on timeout or any error
|
|
179
|
-
the stop, leaving tier 1 as the backstop past the horizon.
|
|
287
|
+
only while dispatches are in flight — queued dispatches count. Conversational
|
|
288
|
+
turns end free. On an event it blocks the stop with the event as context and
|
|
289
|
+
tells the agent the session re-parks automatically; on timeout or any error
|
|
290
|
+
it silently allows the stop, leaving tier 1 as the backstop past the horizon.
|
|
180
291
|
|
|
181
292
|
Two habits worth adding to a lobsterman session's instructions: run
|
|
182
293
|
`lobstah man wait --peek` at session start (a wake consumed by a session that
|
|
183
|
-
died mid-handling is still standing state — peek resurfaces it
|
|
294
|
+
died mid-handling is still standing state — peek resurfaces it without
|
|
295
|
+
consuming it, and returns at once with `standing: none` and exit 0 when
|
|
296
|
+
nothing is; it never blocks, so it takes no `--timeout`), and treat
|
|
184
297
|
the haul context as the work order for that turn.
|
|
185
298
|
|
|
186
299
|
The trade-offs, honestly. While parked, the turn never ends, so the terminal
|
|
@@ -196,8 +309,23 @@ Tier 3 is for a dedicated, long-lived liaison session.
|
|
|
196
309
|
unanswered question is reported immediately. While it still stands, it
|
|
197
310
|
re-fires as a reminder every `remindSecs` (top-level config, default 900). So
|
|
198
311
|
a wake consumed by a session that died mid-handling resurfaces on its own.
|
|
199
|
-
Answering ends the reminders
|
|
200
|
-
|
|
312
|
+
Answering ends the reminders: a message to the dispatch newer than its
|
|
313
|
+
question — `lobstah send`, a forwarded tracker comment, anything written
|
|
314
|
+
through the inbox with provenance — means the question no longer stands, even
|
|
315
|
+
before the worker reads it; `man tend` then shows the dispatch as
|
|
316
|
+
`needs-decision (answered <n>m ago)` instead of listing it under attention.
|
|
317
|
+
A newer `needs-decision` from the worker stands again. Set `remindSecs = 0`
|
|
318
|
+
for pure at-most-once.
|
|
319
|
+
|
|
320
|
+
**Acknowledging, display-only.** Clicking a desktop pet opens its target and
|
|
321
|
+
runs `lobstah attention ack <item-key> --by pet`, so that pet stops walking
|
|
322
|
+
the item until its state changes (a new status entry, head, failed check, or
|
|
323
|
+
thread count). The ack changes only what the pet and the glass lobs display:
|
|
324
|
+
`man tend --json` still lists the item (marked `acked`), and `man wait`, the
|
|
325
|
+
park, and reminders ignore acks entirely — a human having seen a question
|
|
326
|
+
must never hide it from the orchestrator that has to answer it. The glass,
|
|
327
|
+
which has no write endpoint, hides a clicked lob per browser in localStorage
|
|
328
|
+
instead.
|
|
201
329
|
|
|
202
330
|
**Any-harness fallback — the wrapper loop.** Tiers 2 and 3 lean on Claude
|
|
203
331
|
Code features (background-task notifications, the Stop hook). For any other
|
|
@@ -214,6 +342,48 @@ Zero tokens between events and works anywhere a shell does; the cost is that
|
|
|
214
342
|
each event gets a fresh context rather than a continuing liaison
|
|
215
343
|
conversation.
|
|
216
344
|
|
|
345
|
+
## The helm: signing on as the orchestrator
|
|
346
|
+
|
|
347
|
+
Soak enlists workers; `lobstah man helm` enlists the one who dispatches to
|
|
348
|
+
them. Sign-on prints the **charter** — the persona and scope fences, written
|
|
349
|
+
in Standard Technical English: triage and dispatch but never do the work,
|
|
350
|
+
leave running catches to the daemon, judge the catch not the keystrokes, stay
|
|
351
|
+
inside your grounds, report deltas not dumps. `man brief` re-injects the
|
|
352
|
+
charter at every session start, so it survives restarts and compaction
|
|
353
|
+
without anyone re-running anything. A helm registration also arms the
|
|
354
|
+
Stop-hook park by itself — no `.lobstah-man` marker, no env var — and gates
|
|
355
|
+
the park-delivered digest above.
|
|
356
|
+
|
|
357
|
+
**One helm per grounds, enforced.** A **grounds** is a named territory: the
|
|
358
|
+
subset of configured repos one orchestrator oversees (`[grounds.*]`; with
|
|
359
|
+
none configured, one implicit `fleet` grounds covers every repo). A repo
|
|
360
|
+
belongs to at most one grounds, so two orchestrators can never dispatch into
|
|
361
|
+
the same territory. The registration is one file per grounds — the data model
|
|
362
|
+
cannot hold two:
|
|
363
|
+
|
|
364
|
+
- Sign-on against a **live** foreign holder refuses with guidance;
|
|
365
|
+
`--take` is the only force path, and it is deliberate: the displaced
|
|
366
|
+
session finds a stand-down notice at its next park and stops orchestrating.
|
|
367
|
+
- A **stale** holder (heartbeat past `[helm].ttlSecs`) is claimable outright —
|
|
368
|
+
a dead orchestrator holds nothing.
|
|
369
|
+
- `lobstah man relieve` steps down; a relieved session never re-takes on its
|
|
370
|
+
own.
|
|
371
|
+
- The rule is strict: once a helm is claimed, `man wait` and `man report`
|
|
372
|
+
are reserved for the helm session (identified by `--session`, else hook
|
|
373
|
+
stdin, else `$CLAUDE_CODE_SESSION_ID` — inside Claude Code no flag is
|
|
374
|
+
needed; a refusal names the id it resolved and from where), and no
|
|
375
|
+
other session parks as a lobsterman — those verbs consume the helm's
|
|
376
|
+
wakes and cursor. A grounds-scoped call is stricter still: it belongs to
|
|
377
|
+
that grounds' own helm, never a neighboring one. `man tend`/`man brief`
|
|
378
|
+
stay open to everyone; a stale helm reserves nothing. Workers never run
|
|
379
|
+
`man` verbs at all — their hookless park is `soak --wait`.
|
|
380
|
+
- An identified `man wait` heartbeats the helm while it waits (waiting is
|
|
381
|
+
liveness) and defaults its digest to the helm's own grounds.
|
|
382
|
+
- Consumption is grounds-partitioned: a helm's `man wait` and park consume
|
|
383
|
+
only events and notices for its own grounds' repos (events whose repo is
|
|
384
|
+
unknowable stay visible to all helms). Two helms never eat each other's
|
|
385
|
+
wakes.
|
|
386
|
+
|
|
217
387
|
## Soaking: a live session volunteers as a worker
|
|
218
388
|
|
|
219
389
|
Workers are usually traps lobstah sets itself — fresh headless sessions in
|
|
@@ -223,25 +393,50 @@ visible terminal, and whatever authenticated tooling a headless spawn can't
|
|
|
223
393
|
get.
|
|
224
394
|
|
|
225
395
|
```bash
|
|
226
|
-
lobstah soak
|
|
396
|
+
lobstah soak # from a worktree — the primary checkout is
|
|
227
397
|
# never claimable, so sign on from a linked
|
|
228
398
|
# worktree (git worktree add ../side -b side)
|
|
229
|
-
|
|
399
|
+
# (the id: --session, else hook stdin, else
|
|
400
|
+
# $CLAUDE_CODE_SESSION_ID)
|
|
401
|
+
lobstah soak --wait # hookless sessions: listen in the foreground
|
|
402
|
+
# (re-runs need no flags — identity is the
|
|
403
|
+
# worktree); exit 3 = quiet, run it again
|
|
404
|
+
lobstah stow # sign off; an open catch requeues, unread
|
|
405
|
+
# messages bounce back to the helm
|
|
230
406
|
```
|
|
231
407
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
410
|
+
survives session restarts — a new session in the same worktree resumes the
|
|
411
|
+
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
|
|
413
|
+
registration as the liveness principal. The harness (claude or codex) is
|
|
414
|
+
inferred — from `CLAUDE*` / `CODEX*` in the environment, and when both are
|
|
415
|
+
set (one harness launched inside the other) from the session id's format:
|
|
416
|
+
Codex thread ids are UUIDv7, Claude Code session ids UUIDv4 — and
|
|
417
|
+
`--harness` overrides; an undecidable case refuses rather than guessing.
|
|
418
|
+
Once soaking, the same Stop hook
|
|
419
|
+
that parks a lobsterman parks the worker: at turn end it delivers messages
|
|
420
|
+
first, then claims bait and wakes with the brief. While it works a catch,
|
|
421
|
+
the park wakes it for `lobstah send` messages and cancels instead.
|
|
422
|
+
|
|
423
|
+
Routing follows ownership, and **addressed work is sticky**:
|
|
424
|
+
`dispatch --for wt:<trap>` waits for that trap and never falls back to a
|
|
425
|
+
headless spawn — if the trap ghosts, the orphan surfaces as a helm notice
|
|
426
|
+
(re-address, release, or cancel; `cancel` finalizes unclaimed queue items
|
|
427
|
+
with an audit record). Delivery stamps a receipt into evidence. Unaddressed
|
|
428
|
+
bait for a matching repo prefers a parked trap for `[soak].deferSecs`, then
|
|
429
|
+
the daemon spawns headless. Conversational steering goes through
|
|
430
|
+
`send wt:<trap> "..."` — a message, not bait: no branch, no catch, sender
|
|
431
|
+
stamped, bounced to the helm when undeliverable.
|
|
432
|
+
|
|
433
|
+
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
|
|
436
|
+
enlistment** — noticed with its diagnosis (usually a missing Stop hook →
|
|
437
|
+
`soak --wait`) and left standing so the address keeps protecting its work.
|
|
438
|
+
Nobody is conscripted: only a worktree whose session ran `soak` ever
|
|
439
|
+
receives work.
|
|
245
440
|
|
|
246
441
|
## What the daemon gives your liaison for free
|
|
247
442
|
|
package/docs/vocabulary.md
CHANGED
|
@@ -14,7 +14,7 @@ anything else. The status log is append-only; the last entry wins.
|
|
|
14
14
|
| Verb | Meaning | Who acts next |
|
|
15
15
|
| --- | --- | --- |
|
|
16
16
|
| `working` | Making progress; nothing needed. | Nobody. |
|
|
17
|
-
| `needs-decision` | Blocked on a judgment call only a human (or the orchestrator) can make. The note carries the question. | Human — re-fires every `remindSecs` until answered. |
|
|
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
19
|
| `paused` | Intentionally idle; resume is expected. | Whoever paused it. |
|
|
20
20
|
| `done` | The brief is fulfilled. Terminal. Merging is never the dispatch's job. | Merge loop / reviewer. |
|
|
@@ -106,9 +106,13 @@ Work moves through two **lanes** — `work` (human-originated) and `chore`
|
|
|
106
106
|
tracker) — and three **buckets** within a lane: `queued`, `active`, `done`.
|
|
107
107
|
Bucket transitions are atomic renames; the directory *is* the state.
|
|
108
108
|
|
|
109
|
+
An **attachment** is a dispatch descriptor entry `{ name, path, bytes, type }`
|
|
110
|
+
pointing to a copied, dispatch-owned file under `state/<id>/attachments/`;
|
|
111
|
+
follow-ups reuse those paths, and messages may carry their own attachments.
|
|
112
|
+
|
|
109
113
|
## Doctor statuses
|
|
110
114
|
|
|
111
|
-
`lobstah doctor` grades each check with one of
|
|
115
|
+
`lobstah doctor` grades each check with one of four words. **Owner:**
|
|
112
116
|
`apps/cli/src/doctor.ts`. **Enforcement:** any `fail` row exits 1.
|
|
113
117
|
|
|
114
118
|
| Status | Meaning |
|
|
@@ -116,6 +120,7 @@ Bucket transitions are atomic renames; the directory *is* the state.
|
|
|
116
120
|
| `ok` | Works as configured. |
|
|
117
121
|
| `warn` | Degraded or optional — dispatches may still run (e.g. one harness missing, daemon not running). |
|
|
118
122
|
| `fail` | Broken configuration or missing requirement — fix before relying on lobstah. |
|
|
123
|
+
| `skip` | The check does not apply on this host (e.g. no harness plugin installed). Never fails the run. |
|
|
119
124
|
|
|
120
125
|
## Watch contract
|
|
121
126
|
|
|
@@ -138,28 +143,155 @@ events stand until the owner consumes them. One continuation dispatch in
|
|
|
138
143
|
flight per watch; later events buffer and fork from the latest session in
|
|
139
144
|
the chain.
|
|
140
145
|
|
|
146
|
+
### The PR preset
|
|
147
|
+
|
|
148
|
+
`lobstah watch add pr:<owner>/<repo>#<n>` (or a github.com PR URL,
|
|
149
|
+
normalized to that key) installs the shipped check, `lobstah watch
|
|
150
|
+
check-pr`: one read-only `gh pr view` per cycle, diffed against the
|
|
151
|
+
previous observation that the cursor carries, plus — while the PR is open —
|
|
152
|
+
one read-only `gh api graphql` query for `reviewThreads { isResolved }`,
|
|
153
|
+
which `gh pr view --json` cannot return (no bodies are requested). `report <id> done --pr
|
|
154
|
+
<url>` registers the same watch owned by `dispatch:<id>` (idempotent;
|
|
155
|
+
`--no-watch` opts out). **Owner:** `packages/core/src/pr.ts` (derivation,
|
|
156
|
+
badge) and `apps/cli/src/pr-watch.ts` (check, registration, evidence).
|
|
157
|
+
|
|
158
|
+
| Word | Meaning |
|
|
159
|
+
| ---- | ------- |
|
|
160
|
+
| `pr:` key | `pr:<owner>/<repo>#<n>` — one watch per PR. |
|
|
161
|
+
| 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. |
|
|
162
|
+
| `check-completed` | A check reached a conclusion on the current head (`name`, `conclusion`, `detailsUrl`). Failing → work; passing → evidence only. |
|
|
163
|
+
| `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"). |
|
|
164
|
+
| `merge-state` | `mergeStateStatus` changed (`value`). Evidence only. |
|
|
165
|
+
| `draft` | Draft flipped (`value`). Evidence only. |
|
|
166
|
+
| `merged` / `closed` | Terminal; the check sets `done`, and the watch retires once delivered. |
|
|
167
|
+
| evidence `pr` | `{ url, number, state, draft, reviewDecision, mergeStateStatus, headSha, checks: { total, passed, failed, pending }, 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. |
|
|
168
|
+
| 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. |
|
|
169
|
+
|
|
170
|
+
Every event carries `headSha`. A dispatch-owned PR watch emits only work
|
|
171
|
+
events (a failing check; a review decision pickup doesn't own), so the
|
|
172
|
+
`owner` row is unchanged: its events always fork. The rest is evidence.
|
|
173
|
+
Merged and closed reach the helm as a `pr-merged` / `pr-closed` notice,
|
|
174
|
+
posted once by whichever process first records the open → terminal
|
|
175
|
+
transition on the **PR record** — not by event routing.
|
|
176
|
+
|
|
177
|
+
A man-owned PR watch (no `--for`) delivers as attention only what needs a
|
|
178
|
+
human (`manEvents`, beside `workEvents` in `apps/cli/src/pr-watch.ts`): a
|
|
179
|
+
failing `check-completed`, and a `review-decision` turning to
|
|
180
|
+
`CHANGES_REQUESTED`. Green checks, draft toggles, merge-state changes, and
|
|
181
|
+
approvals go to the PR record only — they show as `pr:*` kinds and in the
|
|
182
|
+
glass, never as `watch` attention.
|
|
183
|
+
|
|
184
|
+
One carrier per event kind:
|
|
185
|
+
|
|
186
|
+
| Event | Carrier |
|
|
187
|
+
| ----- | ------- |
|
|
188
|
+
| `check-completed`, failing | dispatch-owned: a continuation (pick); man-owned: a `watch` attention event |
|
|
189
|
+
| `review-decision` | dispatch-owned: a continuation unless pickup owns review feedback; man-owned: a `watch` event only for `CHANGES_REQUESTED` |
|
|
190
|
+
| `check-completed` green, `draft`, `merge-state`, approvals | the PR record (and the owner's evidence) only |
|
|
191
|
+
| `merged`, `closed` | a `pr-merged` / `pr-closed` notice from the record transition only |
|
|
192
|
+
|
|
193
|
+
Pick stays the single writer of watch progress. The inline poller (`man
|
|
194
|
+
wait`, the helm park) runs PR checks for man-owned watches as usual, and
|
|
195
|
+
only **observes** dispatch-owned ones: it writes the PR record and the
|
|
196
|
+
evidence `pr` object (and so the merged notice) and never advances their cursor or consumes
|
|
197
|
+
their events, so pick still sees and forks every one.
|
|
198
|
+
|
|
141
199
|
## Soaking contract
|
|
142
200
|
|
|
143
|
-
A **
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
A **trap** is a worktree that volunteered as a worker seat through
|
|
202
|
+
`lobstah soak` — the validated write path; nothing else touches `soaking/`.
|
|
203
|
+
Identity is **worktree-anchored**: `.lobstah-trap` in the worktree root
|
|
204
|
+
holds a short stable id, the registration keys on it, and the address
|
|
205
|
+
(`wt:<id>`) survives session restarts. The session id inside the
|
|
206
|
+
registration is the liveness principal. **Owner:**
|
|
207
|
+
`packages/core/src/soak.ts`. **Enforcement:** sign-on is refused from a
|
|
208
|
+
repo's primary checkout, and a live foreign session in an owned worktree is
|
|
209
|
+
refused (the session lock); a stale one is adopted.
|
|
150
210
|
|
|
151
211
|
| Word | Meaning |
|
|
152
212
|
| ---- | ------- |
|
|
153
|
-
| `soak` | Sign
|
|
154
|
-
| `stow` | Sign
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
213
|
+
| `soak` | Sign the worktree's trap on: it parks at turn end (Stop hook) and takes matching work. `--session` only on first sign-on; re-runs infer everything from the anchor file. `--one` stows after the first catch. `--wait` parks in the foreground — the hookless path: work prints plain, a quiet timeout exits 3, re-running re-arms. Workers never run `man` verbs. |
|
|
214
|
+
| `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`). |
|
|
215
|
+
| 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. |
|
|
216
|
+
| 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. |
|
|
217
|
+
| 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. |
|
|
218
|
+
| 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. |
|
|
219
|
+
| 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. |
|
|
220
|
+
| 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. 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. |
|
|
158
221
|
|
|
159
222
|
Delivery routes by ownership, same as watches: a continuation for a chain
|
|
160
|
-
claimed by a live
|
|
161
|
-
|
|
162
|
-
|
|
223
|
+
claimed by a live trap is addressed back to that trap and stays sticky.
|
|
224
|
+
Sessions are never conscripted — a thread works bait only after opting in,
|
|
225
|
+
and nothing addressed is ever silently rerouted.
|
|
226
|
+
|
|
227
|
+
## Helm contract
|
|
228
|
+
|
|
229
|
+
The **helm** is the orchestrator seat: one interactive session signed on as
|
|
230
|
+
the lobsterman for its grounds through `lobstah man helm` — the validated
|
|
231
|
+
write path; nothing else touches `helm/`. Soak enlists workers; helm enlists
|
|
232
|
+
the one who dispatches to them. **Owner:** `packages/core/src/helm.ts`.
|
|
233
|
+
**Enforcement:** one registration file per grounds — the data model cannot
|
|
234
|
+
hold two; a live foreign holder refuses sign-on without `--take`. The rule
|
|
235
|
+
is strict: once a helm is claimed, the orchestrator verbs that consume helm
|
|
236
|
+
state (`man wait`, `man report`, the lobsterman park) are reserved for the
|
|
237
|
+
helm session — any other caller is refused with guidance (or, for the hook,
|
|
238
|
+
silently ignored). Read verbs (`man tend`, `man brief`) and the enlistment
|
|
239
|
+
verbs stay open. A stale helm reserves nothing.
|
|
240
|
+
|
|
241
|
+
| Word | Meaning |
|
|
242
|
+
| ---- | ------- |
|
|
243
|
+
| `helm` | Sign a session on as the orchestrator for one grounds. Prints the charter, arms the Stop-hook park without a marker file, and gates the periodic digest. Re-running from the same session is an idempotent re-sign. |
|
|
244
|
+
| `relieve` | Step down. `--take` on another session's `helm` is the only force path: it displaces a live holder deliberately and leaves them a stand-down notice, delivered once at their next park. A holder whose heartbeat lapsed past `[helm].ttlSecs` is stale and claimable without `--take`. |
|
|
245
|
+
| grounds | A named territory: the subset of configured repos one helm oversees, from `[grounds.*]`. A repo belongs to at most one grounds (config error otherwise). No grounds configured means one implicit `fleet` grounds covering every repo. |
|
|
246
|
+
| charter | The helm's persona and scope fences, in Standard Technical English. Printed at sign-on and re-injected by `man brief` at every session start, so it survives restarts and compaction. |
|
|
247
|
+
| digest | The delta since the reported-through cursor: catches landed, attention arisen, still-waiting, fleet verdict. Carried by `man report`, a `man wait` timeout, and — for a helm session, at `[helm].reportSecs` cadence — the park itself. Change-gated: an empty delta is never delivered. |
|
|
248
|
+
|
|
249
|
+
## Attention contract
|
|
250
|
+
|
|
251
|
+
**Attention** is what stands waiting for a human to look: `man tend`'s
|
|
252
|
+
`attention` list, which the desktop pet and the glass walk across the
|
|
253
|
+
screen. It is derived in one place (`apps/cli/src/tend.ts`) and nowhere
|
|
254
|
+
else. **Owner:** `apps/cli/src/tend.ts`. **Enforcement:** `attentionKinds`
|
|
255
|
+
in `config.toml` selects the kinds (an unknown kind is a config error);
|
|
256
|
+
every kind is level-triggered — it stands until its own clear condition,
|
|
257
|
+
never until someone acknowledges it.
|
|
258
|
+
|
|
259
|
+
| Kind | Stands while | Clears when |
|
|
260
|
+
| ---- | ------------ | ----------- |
|
|
261
|
+
| `question` | The dispatch's last status is `needs-decision` or `blocked`. | Any newer status entry. |
|
|
262
|
+
| `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. |
|
|
263
|
+
| `pr:draft` | Evidence `pr` is open and draft. | Ready for review, merged, or closed. |
|
|
264
|
+
| `pr:review` | Evidence `pr` is open with `review.unresolvedThreads > 0` or `review.changesRequested`. | Every thread resolved and no changes requested, or merged / closed. |
|
|
265
|
+
| `pr:checks` | Evidence `pr` is open with a failed check on the observed head. | Green on the head, or merged / closed. |
|
|
266
|
+
| `pr:ready` | Evidence `pr` is open, not draft, no `pr:review` condition holds, and it is approved — or every check passed with none pending. | Merged or closed (or a review condition arises). |
|
|
267
|
+
|
|
268
|
+
`pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
|
|
269
|
+
and carry `prUrl`, `number`, and the fields they derive from. Unconsumed
|
|
270
|
+
man-owned watch events also list, as `watch`: machinery wakes, always on.
|
|
271
|
+
Only `question` and `watch` make the verdict `needs-attention` or arise in
|
|
272
|
+
the digest; the rest are things to look at, not stalls.
|
|
273
|
+
|
|
274
|
+
**The on-the-hook rule.** A `pr:review` or `pr:checks` item is suppressed
|
|
275
|
+
while a worker owns the problem: a queued or active dispatch in the PR's
|
|
276
|
+
chain (the evidence owner and its `followUp` descendants) that is a pickup
|
|
277
|
+
feedback round — pickup's map records it with kind `review` — or the `pr:`
|
|
278
|
+
watch's fix continuation — the watch records it as `lastFollowUpId`. It
|
|
279
|
+
reappears when that dispatch finishes without clearing the condition.
|
|
280
|
+
`question`, `landed`, `pr:draft`, and `pr:ready` are never suppressed.
|
|
281
|
+
|
|
282
|
+
**Answered questions.** A `question` stands only while no message to the
|
|
283
|
+
dispatch is newer than its latest `needs-decision` / `blocked` entry. A
|
|
284
|
+
message counts when it carries provenance: `sendMessage` writes a
|
|
285
|
+
`NNN.meta.json` sidecar `{ from, at }` beside each `NNN.msg` (`from` is
|
|
286
|
+
`helm`, `session:<8>`, `terminal`, `tracker:<source>`, or `node`), and
|
|
287
|
+
`acknowledge` moves it into `handled/` with its message. Any sender counts;
|
|
288
|
+
a record without a sidecar (written before provenance existed) never does;
|
|
289
|
+
the sidecar's `at` decides, never file mtime. `man wait`, the park, and the
|
|
290
|
+
reminder loop apply the same predicate.
|
|
291
|
+
|
|
292
|
+
| Word | Meaning |
|
|
293
|
+
| ---- | ------- |
|
|
294
|
+
| 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`). |
|
|
163
295
|
|
|
164
296
|
## Exit codes
|
|
165
297
|
|