lobstah 0.5.0 → 0.5.7
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 +5 -2
- package/dist/main.js +3507 -1117
- package/dist/runner.js +416 -102
- package/docs/configuration.md +2 -0
- package/docs/design.md +21 -1
- package/docs/lobsterman.md +91 -87
- package/docs/vocabulary.md +128 -5
- package/package.json +1 -1
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:conflict", "pr:ready"]` | Which attention kinds `man tend` lists — and so what the desktop pet and the glass walk across the screen. Valid kinds: `question`, `landed` (opt-in), `pr:draft`, `pr:review`, `pr:checks`, `pr:conflict`, `pr:ready`; an unknown kind is a config error naming the valid set. Notify is edge-triggered and fires once per transition; attention is level-triggered and stands until its clear condition ([vocabulary.md](vocabulary.md#attention-contract)). |
|
|
17
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
|
|
package/docs/design.md
CHANGED
|
@@ -444,7 +444,27 @@ 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`.
|
|
454
|
+
|
|
455
|
+
**The session's own harness resumes it.** A session only resumes under the
|
|
456
|
+
harness that wrote it: a Codex thread id handed to Claude Code is "No
|
|
457
|
+
conversation found". The descriptor's `harness` records what was asked, not
|
|
458
|
+
what ran (a trap claims unaddressed work with whatever harness it is), so every
|
|
459
|
+
run stamps `harness` into evidence beside `sessionId` (the adapter's for a
|
|
460
|
+
headless run, the trap's for a claim), and one resolver reads it back for a
|
|
461
|
+
follow-up's fork, a daemon restart, `lobstah swap`, and `attach`. It falls back
|
|
462
|
+
to the claiming trap, then the session id's UUID version, then the descriptor.
|
|
463
|
+
A follow-up whose `--harness` names something the chain never asked for is a
|
|
464
|
+
swap: cold on that harness with the brief plus a progress note. A resume the
|
|
465
|
+
harness refuses before doing any work falls back the same way and records
|
|
466
|
+
`resume-fallback: <reason>` in the status note and evidence. It never fails
|
|
467
|
+
the dispatch. The first status note says which of these happened.
|
|
448
468
|
|
|
449
469
|
```ts
|
|
450
470
|
// inside the runner
|
package/docs/lobsterman.md
CHANGED
|
@@ -106,6 +106,47 @@ view](pickup.md#merge-view) pickup persists each tick, so PR state is at most
|
|
|
106
106
|
one poll interval stale without tend making a single network call. `--json`
|
|
107
107
|
emits the full report for dashboards and scripts to render.
|
|
108
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`, `conflicts`, `behind`, `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:conflict` (GitHub reports it conflicting with its base),
|
|
141
|
+
`pr:ready` (approved or all green, and GitHub says it can merge) — so it crawls in the glass and
|
|
142
|
+
the desktop pet until its clear condition holds; clicking it opens the PR.
|
|
143
|
+
`pr:review` and `pr:checks` stay off the screen while a worker already owns
|
|
144
|
+
them (a pickup feedback round or the watch's fix continuation in flight).
|
|
145
|
+
`attentionKinds` in `config.toml` picks the kinds; `landed` is opt-in. See
|
|
146
|
+
the [attention contract](vocabulary.md#attention-contract). A PR is
|
|
147
|
+
something to look at, not a stall: it never flips the verdict to
|
|
148
|
+
`needs-attention` and stays out of the digest.
|
|
149
|
+
|
|
109
150
|
### The spyglass
|
|
110
151
|
|
|
111
152
|
`lobstah glass [--port <n>]` serves tend as a live web page on 127.0.0.1
|
|
@@ -117,7 +158,9 @@ and the helm identified by name. It is strictly read-only and consumes no
|
|
|
117
158
|
cursor: looking through the glass changes nothing, so it needs no helm and
|
|
118
159
|
threatens nothing. Links out are copyable commands (`lobstah attach`,
|
|
119
160
|
`claude --resume`), never click-to-exec — localhost HTTP is reachable by
|
|
120
|
-
any webpage, so the glass exposes no endpoint that acts.
|
|
161
|
+
any webpage, so the glass exposes no endpoint that acts. The ⚙ popover's two
|
|
162
|
+
preferences — table or cards, and whether lobsters crawl the page — are
|
|
163
|
+
per-browser, kept in that browser's localStorage and never on disk.
|
|
121
164
|
|
|
122
165
|
This is where "is the agent alive?" belongs: the helm's heartbeat age on a
|
|
123
166
|
page, not periodic proof-of-life turns in a transcript.
|
|
@@ -144,7 +187,7 @@ Every carrier shares the cursor (per grounds, for a helm):
|
|
|
144
187
|
- **The wait loop.** A `man wait` timeout (exit 3) prints the delta when
|
|
145
188
|
something changed, so a looping session gets periodic fleet reports for
|
|
146
189
|
free — see the loop idiom below.
|
|
147
|
-
- **The park.** A helm session's Stop-hook park delivers the digest as a wake
|
|
190
|
+
- **The blocking park.** A helm session's Stop-hook park delivers the digest as a wake
|
|
148
191
|
at `[helm].reportSecs` cadence — including the landed-then-idle case, where
|
|
149
192
|
the last catches finish and nothing is left in flight to wake for.
|
|
150
193
|
- **Direct call.** Anything with a clock — a gateway heartbeat, a cron — runs
|
|
@@ -166,70 +209,27 @@ human is watching — come back later and the transcript is the report.
|
|
|
166
209
|
|
|
167
210
|
## Getting woken instead of asked
|
|
168
211
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
process is running, `man wait` runs due watch checks itself, so watching
|
|
177
|
-
works with every service stopped.
|
|
178
|
-
|
|
179
|
-
**Tier 1 — a push for the human.** Set the daemon's hook and forget it:
|
|
180
|
-
|
|
181
|
-
```toml
|
|
182
|
-
notifyCommand = "ntfy pub my-topic \"$LOBSTAH_VERB $LOBSTAH_ID: $LOBSTAH_NOTE\""
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Tier 2 — a background watcher in the liaison session.** The liaison runs
|
|
186
|
-
`lobstah man wait` as a background task; when it exits, the harness's task
|
|
187
|
-
notification wakes the session, and the printed `next:` line tells the agent
|
|
188
|
-
exactly what to do — including re-arming. One watcher per wake is inherent to
|
|
189
|
-
background tasks; the level-trigger makes the re-arm race harmless. Add to the
|
|
190
|
-
liaison instructions:
|
|
191
|
-
|
|
192
|
-
```markdown
|
|
193
|
-
After dispatching work, run `lobstah man wait` as a background task. When it
|
|
194
|
-
completes, follow its `next:` instruction, then re-arm it.
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Arm it long-lived.** The default (`--timeout 0`) waits forever and returns
|
|
198
|
-
only when something happens — that is the right idiom, not a compromise.
|
|
199
|
-
Every wake costs the session a turn, so a short timeout used as a "check in
|
|
200
|
-
periodically" mechanism buys nothing but noise: attention is level-triggered
|
|
201
|
-
and stands on disk until consumed, so a wait that dies mid-shift drops zero
|
|
202
|
-
events — the next wait delivers everything that accumulated. There is no
|
|
203
|
-
report to miss either; a quiet fleet has nothing to say. Pass `--timeout`
|
|
204
|
-
only as a dead-man's check on the channel itself (an hour, not minutes): a
|
|
205
|
-
quiet timeout prints one line, exits 3, and the re-arm proves the line was
|
|
206
|
-
alive. Liveness reassurance for the human belongs on a status surface
|
|
207
|
-
(`lobstah man tend`), never in the transcript.
|
|
208
|
-
|
|
209
|
-
**Tier 3 — park the session on a Stop hook (Claude Code only).** A Stop hook
|
|
210
|
-
that blocks on `lobstah man wait`, so the session never
|
|
211
|
-
really idles — it parks for free and continues the moment something needs it.
|
|
212
|
-
What this buys over tier 2 is not the wake. Both wake on events, and both
|
|
213
|
-
cost a turn per wake. The difference: the re-arm is **structural instead of
|
|
214
|
-
instructed**. Tier 2 works only as long as the model remembers to re-arm the
|
|
215
|
-
watcher after every wake. A forgotten re-arm, a crashed watcher, or an
|
|
216
|
-
interrupted turn leaves the session deaf until a human speaks. The Stop hook
|
|
217
|
-
fires at every turn end, no matter what the model did. Supervision cannot
|
|
218
|
-
lapse through instruction drift, and a blind stop mid-shift is impossible.
|
|
212
|
+
After `man helm`, run `lobstah man wait --session <id> --timeout 900` as a
|
|
213
|
+
background task and re-arm it after each completion. The wait registers a
|
|
214
|
+
heartbeating watcher for the session and exits with an event or a timeout
|
|
215
|
+
(exit 3). At turn end, `man haul` blocks with standing attention; in arm mode,
|
|
216
|
+
work in flight allows a stop with a live watcher and otherwise blocks with
|
|
217
|
+
the arm command. `man haul --park` or `[helm].park = "block"` makes the hook
|
|
218
|
+
wait for attention itself. Without a Stop hook, run `man wait` in the foreground.
|
|
219
219
|
|
|
220
220
|
The hook is a CLI command — `lobstah man haul` (the lobsterman hauls the
|
|
221
221
|
trapline; every orchestrator-facing command lives under `lobstah man`).
|
|
222
222
|
Install it from the project you'll run the lobsterman in:
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
|
-
# Easiest: the plugin ships the hooks + the lobsterman
|
|
225
|
+
# Easiest: the plugin ships the hooks + the lobsterman and trap skills, no settings
|
|
226
226
|
# edits — /plugin marketplace add aequitas-labs/lobstah, then
|
|
227
227
|
# /plugin install lobstah@lobstah (Claude Code and Codex v0.114+; Codex asks
|
|
228
228
|
# for a one-time hook trust review). Or wire the Claude hook by hand:
|
|
229
229
|
lobstah man init # merges the Stop hook into .claude/settings.local.json
|
|
230
230
|
lobstah man init --shared # …or the committed .claude/settings.json
|
|
231
231
|
lobstah man init --global # …or once into ~/.claude/settings.json — any
|
|
232
|
-
# directory with a .lobstah-man file then
|
|
232
|
+
# directory with a .lobstah-man file then uses the hook
|
|
233
233
|
lobstah man init --marker # also touch .lobstah-man (per-directory gate)
|
|
234
234
|
```
|
|
235
235
|
|
|
@@ -240,38 +240,33 @@ are preserved verbatim. What it writes:
|
|
|
240
240
|
{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "lobstah man haul", "timeout": 14400 }] }] } }
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
`haul`
|
|
244
|
-
`LOBSTAH_MAN=1
|
|
245
|
-
only while dispatches are in flight — conversational turns end free. On an
|
|
246
|
-
event it blocks the stop with the event as context and tells the agent the
|
|
247
|
-
session re-parks automatically; on timeout or any error it silently allows
|
|
248
|
-
the stop, leaving tier 1 as the backstop past the horizon.
|
|
249
|
-
|
|
250
|
-
Two habits worth adding to a lobsterman session's instructions: run
|
|
251
|
-
`lobstah man wait --peek` at session start (a wake consumed by a session that
|
|
252
|
-
died mid-handling is still standing state — peek resurfaces it), and treat
|
|
253
|
-
the haul context as the work order for that turn.
|
|
254
|
-
|
|
255
|
-
The trade-offs, honestly. While parked, the turn never ends, so the terminal
|
|
256
|
-
shows a running hook. Each wake appends a turn to the context, and long
|
|
257
|
-
shifts eventually compact. Without the gate, the hook parks every session in
|
|
258
|
-
the project. And parking needs a turn-end hook that
|
|
259
|
-
can block and inject a continuation. Claude Code's Stop hook can, and so can
|
|
260
|
-
Codex's since its hooks system landed (v0.114+; older Codex only has the
|
|
261
|
-
fire-and-forget notify hook, which cannot). Tier 2 is the right default.
|
|
262
|
-
Tier 3 is for a dedicated, long-lived liaison session.
|
|
243
|
+
`haul` applies to a signed-on helm or trap, or a session opted in with
|
|
244
|
+
`LOBSTAH_MAN=1` or `.lobstah-man`. Queued dispatches count as work in flight.
|
|
263
245
|
|
|
264
246
|
**Delivery guarantee.** Attention wakes are at-least-once with backoff. An
|
|
265
247
|
unanswered question is reported immediately. While it still stands, it
|
|
266
248
|
re-fires as a reminder every `remindSecs` (top-level config, default 900). So
|
|
267
249
|
a wake consumed by a session that died mid-handling resurfaces on its own.
|
|
268
|
-
Answering ends the reminders
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
250
|
+
Answering ends the reminders: a message to the dispatch newer than its
|
|
251
|
+
question — `lobstah send`, a forwarded tracker comment, anything written
|
|
252
|
+
through the inbox with provenance — means the question no longer stands, even
|
|
253
|
+
before the worker reads it; `man tend` then shows the dispatch as
|
|
254
|
+
`needs-decision (answered <n>m ago)` instead of listing it under attention.
|
|
255
|
+
A newer `needs-decision` from the worker stands again. Set `remindSecs = 0`
|
|
256
|
+
for pure at-most-once.
|
|
257
|
+
|
|
258
|
+
**Acknowledging, display-only.** Clicking a desktop pet opens its target and
|
|
259
|
+
runs `lobstah attention ack <item-key> --by pet`, so that pet stops walking
|
|
260
|
+
the item until its state changes (a new status entry, head, failed check, or
|
|
261
|
+
thread count). The ack changes only what the pet and the glass lobs display:
|
|
262
|
+
`man tend --json` still lists the item (marked `acked`), and `man wait`, the
|
|
263
|
+
park, and reminders ignore acks entirely — a human having seen a question
|
|
264
|
+
must never hide it from the orchestrator that has to answer it. The glass,
|
|
265
|
+
which has no write endpoint, hides a clicked lob per browser in localStorage
|
|
266
|
+
instead.
|
|
267
|
+
|
|
268
|
+
**Wrapper loop.** An outer loop blocking on `wait` can spawn one fresh
|
|
269
|
+
headless turn per event:
|
|
275
270
|
|
|
276
271
|
```sh
|
|
277
272
|
while out=$(lobstah man wait); do
|
|
@@ -291,9 +286,9 @@ in Standard Technical English: triage and dispatch but never do the work,
|
|
|
291
286
|
leave running catches to the daemon, judge the catch not the keystrokes, stay
|
|
292
287
|
inside your grounds, report deltas not dumps. `man brief` re-injects the
|
|
293
288
|
charter at every session start, so it survives restarts and compaction
|
|
294
|
-
without anyone re-running anything. A helm registration
|
|
295
|
-
Stop
|
|
296
|
-
the
|
|
289
|
+
without anyone re-running anything. A helm registration enables the
|
|
290
|
+
Stop hook by itself — no `.lobstah-man` marker, no env var — and
|
|
291
|
+
gates the digest above.
|
|
297
292
|
|
|
298
293
|
**One helm per grounds, enforced.** A **grounds** is a named territory: the
|
|
299
294
|
subset of configured repos one orchestrator oversees (`[grounds.*]`; with
|
|
@@ -310,7 +305,9 @@ cannot hold two:
|
|
|
310
305
|
- `lobstah man relieve` steps down; a relieved session never re-takes on its
|
|
311
306
|
own.
|
|
312
307
|
- The rule is strict: once a helm is claimed, `man wait` and `man report`
|
|
313
|
-
are reserved for the helm session (
|
|
308
|
+
are reserved for the helm session (identified by `--session`, else hook
|
|
309
|
+
stdin, else `$CLAUDE_CODE_SESSION_ID` — inside Claude Code no flag is
|
|
310
|
+
needed; a refusal names the id it resolved and from where), and no
|
|
314
311
|
other session parks as a lobsterman — those verbs consume the helm's
|
|
315
312
|
wakes and cursor. A grounds-scoped call is stricter still: it belongs to
|
|
316
313
|
that grounds' own helm, never a neighboring one. `man tend`/`man brief`
|
|
@@ -332,9 +329,11 @@ visible terminal, and whatever authenticated tooling a headless spawn can't
|
|
|
332
329
|
get.
|
|
333
330
|
|
|
334
331
|
```bash
|
|
335
|
-
lobstah soak
|
|
332
|
+
lobstah soak # from a worktree — the primary checkout is
|
|
336
333
|
# never claimable, so sign on from a linked
|
|
337
334
|
# worktree (git worktree add ../side -b side)
|
|
335
|
+
# (the id: --session, else hook stdin, else
|
|
336
|
+
# $CLAUDE_CODE_SESSION_ID)
|
|
338
337
|
lobstah soak --wait # hookless sessions: listen in the foreground
|
|
339
338
|
# (re-runs need no flags — identity is the
|
|
340
339
|
# worktree); exit 3 = quiet, run it again
|
|
@@ -347,7 +346,12 @@ lobstah stow # sign off; an open catch requeues, unread
|
|
|
347
346
|
survives session restarts — a new session in the same worktree resumes the
|
|
348
347
|
same trap (a *live* foreign session is refused: the session lock). The
|
|
349
348
|
session id (from the plugin's session-start brief) lives inside the
|
|
350
|
-
registration as the liveness principal.
|
|
349
|
+
registration as the liveness principal. The harness (claude or codex) is
|
|
350
|
+
inferred — from `CLAUDE*` / `CODEX*` in the environment, and when both are
|
|
351
|
+
set (one harness launched inside the other) from the session id's format:
|
|
352
|
+
Codex thread ids are UUIDv7, Claude Code session ids UUIDv4 — and
|
|
353
|
+
`--harness` overrides; an undecidable case refuses rather than guessing.
|
|
354
|
+
Once soaking, the same Stop hook
|
|
351
355
|
that parks a lobsterman parks the worker: at turn end it delivers messages
|
|
352
356
|
first, then claims bait and wakes with the brief. While it works a catch,
|
|
353
357
|
the park wakes it for `lobstah send` messages and cancels instead.
|
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,26 @@ 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
|
+
|
|
113
|
+
## Follow-up and swap
|
|
114
|
+
|
|
115
|
+
How a dispatch picks up a session it did not start. One resolver
|
|
116
|
+
(`resolveSessionHarness`, `packages/core/src/resume.ts`) names the harness
|
|
117
|
+
that owns a session: evidence `harness`, else the claiming trap, else the
|
|
118
|
+
session id's UUID version (v7 codex, v4 claude), else the descriptor.
|
|
119
|
+
|
|
120
|
+
| Word | Meaning |
|
|
121
|
+
| ---- | ------- |
|
|
122
|
+
| follow-up | `--follow-up <id>` forks the origin's session **under the origin's harness**, whatever the follow-up's own `--harness` says, unless that `--harness` names something the chain never asked for and the session is not, which makes it a swap. Pickup review rounds and watch continuations are follow-ups. |
|
|
123
|
+
| 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 asks for a new harness. |
|
|
124
|
+
| `resume-fallback` | Status note and evidence field (`resumeFallback`) recorded when the harness refuses a resume (not found, culled, foreign) before doing any work: the runner starts cold with the progress note and the dispatch proceeds. |
|
|
125
|
+
|
|
109
126
|
## Doctor statuses
|
|
110
127
|
|
|
111
|
-
`lobstah doctor` grades each check with one of
|
|
128
|
+
`lobstah doctor` grades each check with one of four words. **Owner:**
|
|
112
129
|
`apps/cli/src/doctor.ts`. **Enforcement:** any `fail` row exits 1.
|
|
113
130
|
|
|
114
131
|
| Status | Meaning |
|
|
@@ -116,6 +133,7 @@ Bucket transitions are atomic renames; the directory *is* the state.
|
|
|
116
133
|
| `ok` | Works as configured. |
|
|
117
134
|
| `warn` | Degraded or optional — dispatches may still run (e.g. one harness missing, daemon not running). |
|
|
118
135
|
| `fail` | Broken configuration or missing requirement — fix before relying on lobstah. |
|
|
136
|
+
| `skip` | The check does not apply on this host (e.g. no harness plugin installed). Never fails the run. |
|
|
119
137
|
|
|
120
138
|
## Watch contract
|
|
121
139
|
|
|
@@ -138,6 +156,59 @@ events stand until the owner consumes them. One continuation dispatch in
|
|
|
138
156
|
flight per watch; later events buffer and fork from the latest session in
|
|
139
157
|
the chain.
|
|
140
158
|
|
|
159
|
+
### The PR preset
|
|
160
|
+
|
|
161
|
+
`lobstah watch add pr:<owner>/<repo>#<n>` (or a github.com PR URL,
|
|
162
|
+
normalized to that key) installs the shipped check, `lobstah watch
|
|
163
|
+
check-pr`: one read-only `gh pr view` per cycle, diffed against the
|
|
164
|
+
previous observation that the cursor carries, plus — while the PR is open —
|
|
165
|
+
one read-only `gh api graphql` query for `reviewThreads { isResolved }`,
|
|
166
|
+
which `gh pr view --json` cannot return (no bodies are requested). `report <id> done --pr
|
|
167
|
+
<url>` registers the same watch owned by `dispatch:<id>` (idempotent;
|
|
168
|
+
`--no-watch` opts out). **Owner:** `packages/core/src/pr.ts` (derivation,
|
|
169
|
+
badge) and `apps/cli/src/pr-watch.ts` (check, registration, evidence).
|
|
170
|
+
|
|
171
|
+
| Word | Meaning |
|
|
172
|
+
| ---- | ------- |
|
|
173
|
+
| `pr:` key | `pr:<owner>/<repo>#<n>` — one watch per PR. |
|
|
174
|
+
| 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. |
|
|
175
|
+
| `check-completed` | A check reached a conclusion on the current head (`name`, `conclusion`, `detailsUrl`). Failing → work; passing → evidence only. |
|
|
176
|
+
| `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"). |
|
|
177
|
+
| `merge-state` | `mergeStateStatus` changed (`value`). Evidence only. |
|
|
178
|
+
| `draft` | Draft flipped (`value`). Evidence only. |
|
|
179
|
+
| `merged` / `closed` | Terminal; the check sets `done`, and the watch retires once delivered. |
|
|
180
|
+
| 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: `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), `review`, `green` (only for a mergeable merge state), `blocked`, `merge unknown`. |
|
|
181
|
+
| 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. |
|
|
182
|
+
|
|
183
|
+
Every event carries `headSha`. A dispatch-owned PR watch emits only work
|
|
184
|
+
events (a failing check; a review decision pickup doesn't own), so the
|
|
185
|
+
`owner` row is unchanged: its events always fork. The rest is evidence.
|
|
186
|
+
Merged and closed reach the helm as a `pr-merged` / `pr-closed` notice,
|
|
187
|
+
posted once by whichever process first records the open → terminal
|
|
188
|
+
transition on the **PR record** — not by event routing.
|
|
189
|
+
|
|
190
|
+
A man-owned PR watch (no `--for`) delivers as attention only what needs a
|
|
191
|
+
human (`manEvents`, beside `workEvents` in `apps/cli/src/pr-watch.ts`): a
|
|
192
|
+
failing `check-completed`, and a `review-decision` turning to
|
|
193
|
+
`CHANGES_REQUESTED`. Green checks, draft toggles, merge-state changes, and
|
|
194
|
+
approvals go to the PR record only — they show as `pr:*` kinds and in the
|
|
195
|
+
glass, never as `watch` attention.
|
|
196
|
+
|
|
197
|
+
One carrier per event kind:
|
|
198
|
+
|
|
199
|
+
| Event | Carrier |
|
|
200
|
+
| ----- | ------- |
|
|
201
|
+
| `check-completed`, failing | dispatch-owned: a continuation (pick); man-owned: a `watch` attention event |
|
|
202
|
+
| `review-decision` | dispatch-owned: a continuation unless pickup owns review feedback; man-owned: a `watch` event only for `CHANGES_REQUESTED` |
|
|
203
|
+
| `check-completed` green, `draft`, `merge-state`, approvals | the PR record (and the owner's evidence) only |
|
|
204
|
+
| `merged`, `closed` | a `pr-merged` / `pr-closed` notice from the record transition only |
|
|
205
|
+
|
|
206
|
+
Pick stays the single writer of watch progress. The inline poller (`man
|
|
207
|
+
wait`, the helm park) runs PR checks for man-owned watches as usual, and
|
|
208
|
+
only **observes** dispatch-owned ones: it writes the PR record and the
|
|
209
|
+
evidence `pr` object (and so the merged notice) and never advances their cursor or consumes
|
|
210
|
+
their events, so pick still sees and forks every one.
|
|
211
|
+
|
|
141
212
|
## Soaking contract
|
|
142
213
|
|
|
143
214
|
A **trap** is a worktree that volunteered as a worker seat through
|
|
@@ -152,7 +223,7 @@ refused (the session lock); a stale one is adopted.
|
|
|
152
223
|
|
|
153
224
|
| Word | Meaning |
|
|
154
225
|
| ---- | ------- |
|
|
155
|
-
| `soak` | Sign the worktree's trap on
|
|
226
|
+
| `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. |
|
|
156
227
|
| `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`). |
|
|
157
228
|
| 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. |
|
|
158
229
|
| 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. |
|
|
@@ -182,11 +253,63 @@ verbs stay open. A stale helm reserves nothing.
|
|
|
182
253
|
|
|
183
254
|
| Word | Meaning |
|
|
184
255
|
| ---- | ------- |
|
|
185
|
-
| `helm` | Sign a session on as the orchestrator for one grounds. Prints the charter
|
|
256
|
+
| `helm` | Sign a session on as the orchestrator for one grounds. Prints the charter and enables the Stop hook without a marker file. Re-running from the same session is an idempotent re-sign. |
|
|
257
|
+
| watcher registration | `~/.lobstah/watchers/<session-id>.json` stores a `man wait` or `soak --wait` process's PID and heartbeat. The waiter refreshes it while running and removes it on exit; a heartbeat older than five seconds is stale, and a second waiter for the same session refuses. |
|
|
258
|
+
| `man haul --park` | Make the Stop hook wait for attention while work is in flight. `[helm].park = "block"` selects the same mode. In arm mode, the hook allows a stop with a live watcher or blocks with the arm command. |
|
|
186
259
|
| `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`. |
|
|
187
260
|
| 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. |
|
|
188
261
|
| 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. |
|
|
189
|
-
| 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 —
|
|
262
|
+
| 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 — in blocking park mode, at `[helm].reportSecs` cadence — the park itself. Change-gated: an empty delta is never delivered. |
|
|
263
|
+
|
|
264
|
+
## Attention contract
|
|
265
|
+
|
|
266
|
+
**Attention** is what stands waiting for a human to look: `man tend`'s
|
|
267
|
+
`attention` list, which the desktop pet and the glass walk across the
|
|
268
|
+
screen. It is derived in one place (`apps/cli/src/tend.ts`) and nowhere
|
|
269
|
+
else. **Owner:** `apps/cli/src/tend.ts`. **Enforcement:** `attentionKinds`
|
|
270
|
+
in `config.toml` selects the kinds (an unknown kind is a config error);
|
|
271
|
+
every kind is level-triggered — it stands until its own clear condition,
|
|
272
|
+
never until someone acknowledges it.
|
|
273
|
+
|
|
274
|
+
| Kind | Stands while | Clears when |
|
|
275
|
+
| ---- | ------------ | ----------- |
|
|
276
|
+
| `question` | The dispatch's last status is `needs-decision` or `blocked`. | Any newer status entry. |
|
|
277
|
+
| `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. |
|
|
278
|
+
| `pr:draft` | Evidence `pr` is open and draft. | Ready for review, merged, or closed. |
|
|
279
|
+
| `pr:review` | Evidence `pr` is open with `review.unresolvedThreads > 0` or `review.changesRequested`. | Every thread resolved and no changes requested, or merged / closed. |
|
|
280
|
+
| `pr:checks` | Evidence `pr` is open with a failed check on the observed head. | Green on the head, or merged / closed. |
|
|
281
|
+
| `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. |
|
|
282
|
+
| `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). |
|
|
283
|
+
|
|
284
|
+
`pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
|
|
285
|
+
and carry `prUrl`, `number`, and the fields they derive from. Unconsumed
|
|
286
|
+
man-owned watch events also list, as `watch`: machinery wakes, always on.
|
|
287
|
+
Only `question` and `watch` make the verdict `needs-attention` or arise in
|
|
288
|
+
the digest; the rest are things to look at, not stalls.
|
|
289
|
+
|
|
290
|
+
**The on-the-hook rule.** A `pr:review` or `pr:checks` item is suppressed
|
|
291
|
+
while a worker owns the problem: a queued or active dispatch in the PR's
|
|
292
|
+
chain (the evidence owner and its `followUp` descendants) that is a pickup
|
|
293
|
+
feedback round — pickup's map records it with kind `review` — or the `pr:`
|
|
294
|
+
watch's fix continuation — the watch records it as `lastFollowUpId`. It
|
|
295
|
+
reappears when that dispatch finishes without clearing the condition.
|
|
296
|
+
`question`, `landed`, `pr:draft`, `pr:conflict`, and `pr:ready` are never
|
|
297
|
+
suppressed — a rebase is the human's or the helm's call, never assumed to be
|
|
298
|
+
the fix continuation's.
|
|
299
|
+
|
|
300
|
+
**Answered questions.** A `question` stands only while no message to the
|
|
301
|
+
dispatch is newer than its latest `needs-decision` / `blocked` entry. A
|
|
302
|
+
message counts when it carries provenance: `sendMessage` writes a
|
|
303
|
+
`NNN.meta.json` sidecar `{ from, at }` beside each `NNN.msg` (`from` is
|
|
304
|
+
`helm`, `session:<8>`, `terminal`, `tracker:<source>`, or `node`), and
|
|
305
|
+
`acknowledge` moves it into `handled/` with its message. Any sender counts;
|
|
306
|
+
a record without a sidecar (written before provenance existed) never does;
|
|
307
|
+
the sidecar's `at` decides, never file mtime. `man wait`, the park, and the
|
|
308
|
+
reminder loop apply the same predicate.
|
|
309
|
+
|
|
310
|
+
| Word | Meaning |
|
|
311
|
+
| ---- | ------- |
|
|
312
|
+
| 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`). |
|
|
190
313
|
|
|
191
314
|
## Exit codes
|
|
192
315
|
|