lobstah 0.3.2 → 0.5.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/README.md +25 -6
- package/dist/main.js +3529 -760
- package/dist/runner.js +389 -107
- 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 +22 -0
- package/docs/lobsterman.md +141 -14
- package/docs/vocabulary.md +42 -15
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -60,6 +60,28 @@ Same three keys as the per-repo block. Precedence for every harness setting:
|
|
|
60
60
|
| `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
61
|
| `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
62
|
|
|
63
|
+
## `[helm]` — the orchestrator seat (`lobstah man helm`)
|
|
64
|
+
|
|
65
|
+
| Key | Default | Meaning |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `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. |
|
|
68
|
+
| `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. |
|
|
69
|
+
|
|
70
|
+
## `[grounds.*]` — helm territories
|
|
71
|
+
|
|
72
|
+
One helm per grounds; a repo belongs to at most one grounds (`man helm`
|
|
73
|
+
refuses on overlap or an unknown repo key). With no `[grounds.*]` configured
|
|
74
|
+
there is one implicit `fleet` grounds covering every repo — the partition
|
|
75
|
+
only exists when asked for.
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[grounds.base]
|
|
79
|
+
repos = ["homebase", "matcha"]
|
|
80
|
+
|
|
81
|
+
[grounds.aequitas]
|
|
82
|
+
repos = ["lobstah", "lavish"]
|
|
83
|
+
```
|
|
84
|
+
|
|
63
85
|
## `[pickup]` — tracker loops (`lobstah pick`)
|
|
64
86
|
|
|
65
87
|
| Key | Default | Meaning |
|
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,64 @@ 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
|
+
### The spyglass
|
|
110
|
+
|
|
111
|
+
`lobstah glass [--port <n>]` serves tend as a live web page on 127.0.0.1
|
|
112
|
+
(default port 4949): the fleet verdict and attention questions, every
|
|
113
|
+
dispatch with its full brief, status log, inbox, and evidence, each trap
|
|
114
|
+
with its lifecycle notices, message history, and catches, the notices
|
|
115
|
+
tail, the merge view, and watches — with filters, a table/cards toggle,
|
|
116
|
+
and the helm identified by name. It is strictly read-only and consumes no
|
|
117
|
+
cursor: looking through the glass changes nothing, so it needs no helm and
|
|
118
|
+
threatens nothing. Links out are copyable commands (`lobstah attach`,
|
|
119
|
+
`claude --resume`), never click-to-exec — localhost HTTP is reachable by
|
|
120
|
+
any webpage, so the glass exposes no endpoint that acts.
|
|
121
|
+
|
|
122
|
+
This is where "is the agent alive?" belongs: the helm's heartbeat age on a
|
|
123
|
+
page, not periodic proof-of-life turns in a transcript.
|
|
124
|
+
|
|
125
|
+
### The periodic report
|
|
126
|
+
|
|
127
|
+
`lobstah man tend` is the full picture on demand; `lobstah man report` is the
|
|
128
|
+
**delta** since the last acknowledged report — catches landed (with their
|
|
129
|
+
notes and PRs), attention newly arisen, what still waits, and the fleet
|
|
130
|
+
verdict. It advances a "reported through" cursor when it prints — the
|
|
131
|
+
explicit acknowledgment — so nothing is ever reported twice, and it says
|
|
132
|
+
`no change` when the delta is empty rather than re-dumping state. Standing
|
|
133
|
+
unanswered questions appear under `still-waiting` without counting as
|
|
134
|
+
change — reminders (`remindSecs`) own re-firing those.
|
|
135
|
+
|
|
136
|
+
Delivery is at-least-once by construction: the carriers that might not be
|
|
137
|
+
read (a `man wait` timeout in a background task) only **peek** at the delta,
|
|
138
|
+
so a digest lost with a dead task re-surfaces on the next timeout; only
|
|
139
|
+
`man report` (or a hook-delivered park digest, which lands in-context by
|
|
140
|
+
construction) marks it handled.
|
|
141
|
+
|
|
142
|
+
Every carrier shares the cursor (per grounds, for a helm):
|
|
143
|
+
|
|
144
|
+
- **The wait loop.** A `man wait` timeout (exit 3) prints the delta when
|
|
145
|
+
something changed, so a looping session gets periodic fleet reports for
|
|
146
|
+
free — see the loop idiom below.
|
|
147
|
+
- **The park.** A helm session's Stop-hook park delivers the digest as a wake
|
|
148
|
+
at `[helm].reportSecs` cadence — including the landed-then-idle case, where
|
|
149
|
+
the last catches finish and nothing is left in flight to wake for.
|
|
150
|
+
- **Direct call.** Anything with a clock — a gateway heartbeat, a cron — runs
|
|
151
|
+
`lobstah man report` and forwards the output when it is not `no change`.
|
|
152
|
+
Escalating a report to a human (a phone push) is the gateway layer's job,
|
|
153
|
+
not lobstah's.
|
|
154
|
+
|
|
155
|
+
The loop idiom needs no harness scheduler, because the timer is lobstah's own
|
|
156
|
+
blocking wait:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
lobstah man wait --timeout 900
|
|
160
|
+
# exit 0 → an event printed; handle it, then loop
|
|
161
|
+
# exit 3 → timeout; the delta digest printed above it when something changed
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A session running this loop reports into its own transcript whether or not a
|
|
165
|
+
human is watching — come back later and the transcript is the report.
|
|
166
|
+
|
|
98
167
|
## Getting woken instead of asked
|
|
99
168
|
|
|
100
169
|
Three escalation tiers, least to most invasive. All are built on
|
|
@@ -214,6 +283,46 @@ Zero tokens between events and works anywhere a shell does; the cost is that
|
|
|
214
283
|
each event gets a fresh context rather than a continuing liaison
|
|
215
284
|
conversation.
|
|
216
285
|
|
|
286
|
+
## The helm: signing on as the orchestrator
|
|
287
|
+
|
|
288
|
+
Soak enlists workers; `lobstah man helm` enlists the one who dispatches to
|
|
289
|
+
them. Sign-on prints the **charter** — the persona and scope fences, written
|
|
290
|
+
in Standard Technical English: triage and dispatch but never do the work,
|
|
291
|
+
leave running catches to the daemon, judge the catch not the keystrokes, stay
|
|
292
|
+
inside your grounds, report deltas not dumps. `man brief` re-injects the
|
|
293
|
+
charter at every session start, so it survives restarts and compaction
|
|
294
|
+
without anyone re-running anything. A helm registration also arms the
|
|
295
|
+
Stop-hook park by itself — no `.lobstah-man` marker, no env var — and gates
|
|
296
|
+
the park-delivered digest above.
|
|
297
|
+
|
|
298
|
+
**One helm per grounds, enforced.** A **grounds** is a named territory: the
|
|
299
|
+
subset of configured repos one orchestrator oversees (`[grounds.*]`; with
|
|
300
|
+
none configured, one implicit `fleet` grounds covers every repo). A repo
|
|
301
|
+
belongs to at most one grounds, so two orchestrators can never dispatch into
|
|
302
|
+
the same territory. The registration is one file per grounds — the data model
|
|
303
|
+
cannot hold two:
|
|
304
|
+
|
|
305
|
+
- Sign-on against a **live** foreign holder refuses with guidance;
|
|
306
|
+
`--take` is the only force path, and it is deliberate: the displaced
|
|
307
|
+
session finds a stand-down notice at its next park and stops orchestrating.
|
|
308
|
+
- A **stale** holder (heartbeat past `[helm].ttlSecs`) is claimable outright —
|
|
309
|
+
a dead orchestrator holds nothing.
|
|
310
|
+
- `lobstah man relieve` steps down; a relieved session never re-takes on its
|
|
311
|
+
own.
|
|
312
|
+
- The rule is strict: once a helm is claimed, `man wait` and `man report`
|
|
313
|
+
are reserved for the helm session (identify with `--session`), and no
|
|
314
|
+
other session parks as a lobsterman — those verbs consume the helm's
|
|
315
|
+
wakes and cursor. A grounds-scoped call is stricter still: it belongs to
|
|
316
|
+
that grounds' own helm, never a neighboring one. `man tend`/`man brief`
|
|
317
|
+
stay open to everyone; a stale helm reserves nothing. Workers never run
|
|
318
|
+
`man` verbs at all — their hookless park is `soak --wait`.
|
|
319
|
+
- An identified `man wait` heartbeats the helm while it waits (waiting is
|
|
320
|
+
liveness) and defaults its digest to the helm's own grounds.
|
|
321
|
+
- Consumption is grounds-partitioned: a helm's `man wait` and park consume
|
|
322
|
+
only events and notices for its own grounds' repos (events whose repo is
|
|
323
|
+
unknowable stay visible to all helms). Two helms never eat each other's
|
|
324
|
+
wakes.
|
|
325
|
+
|
|
217
326
|
## Soaking: a live session volunteers as a worker
|
|
218
327
|
|
|
219
328
|
Workers are usually traps lobstah sets itself — fresh headless sessions in
|
|
@@ -226,22 +335,40 @@ get.
|
|
|
226
335
|
lobstah soak --session <id> # from a worktree — the primary checkout is
|
|
227
336
|
# never claimable, so sign on from a linked
|
|
228
337
|
# worktree (git worktree add ../side -b side)
|
|
229
|
-
lobstah
|
|
338
|
+
lobstah soak --wait # hookless sessions: listen in the foreground
|
|
339
|
+
# (re-runs need no flags — identity is the
|
|
340
|
+
# worktree); exit 3 = quiet, run it again
|
|
341
|
+
lobstah stow # sign off; an open catch requeues, unread
|
|
342
|
+
# messages bounce back to the helm
|
|
230
343
|
```
|
|
231
344
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
trap
|
|
244
|
-
|
|
345
|
+
**Identity is the worktree.** Sign-on anchors a short trap id in
|
|
346
|
+
`.lobstah-trap` and prints the trap's address (`wt:<id>`); the address
|
|
347
|
+
survives session restarts — a new session in the same worktree resumes the
|
|
348
|
+
same trap (a *live* foreign session is refused: the session lock). The
|
|
349
|
+
session id (from the plugin's session-start brief) lives inside the
|
|
350
|
+
registration as the liveness principal. Once soaking, the same Stop hook
|
|
351
|
+
that parks a lobsterman parks the worker: at turn end it delivers messages
|
|
352
|
+
first, then claims bait and wakes with the brief. While it works a catch,
|
|
353
|
+
the park wakes it for `lobstah send` messages and cancels instead.
|
|
354
|
+
|
|
355
|
+
Routing follows ownership, and **addressed work is sticky**:
|
|
356
|
+
`dispatch --for wt:<trap>` waits for that trap and never falls back to a
|
|
357
|
+
headless spawn — if the trap ghosts, the orphan surfaces as a helm notice
|
|
358
|
+
(re-address, release, or cancel; `cancel` finalizes unclaimed queue items
|
|
359
|
+
with an audit record). Delivery stamps a receipt into evidence. Unaddressed
|
|
360
|
+
bait for a matching repo prefers a parked trap for `[soak].deferSecs`, then
|
|
361
|
+
the daemon spawns headless. Conversational steering goes through
|
|
362
|
+
`send wt:<trap> "..."` — a message, not bait: no branch, no catch, sender
|
|
363
|
+
stamped, bounced to the helm when undeliverable.
|
|
364
|
+
|
|
365
|
+
Liveness has two failure shapes with two remedies: a registration that
|
|
366
|
+
parked before and went quiet past `[soak].ttlSecs` is a **ghost trap** —
|
|
367
|
+
swept, catch requeued, noticed; one that **never parked** is a **defective
|
|
368
|
+
enlistment** — noticed with its diagnosis (usually a missing Stop hook →
|
|
369
|
+
`soak --wait`) and left standing so the address keeps protecting its work.
|
|
370
|
+
Nobody is conscripted: only a worktree whose session ran `soak` ever
|
|
371
|
+
receives work.
|
|
245
372
|
|
|
246
373
|
## What the daemon gives your liaison for free
|
|
247
374
|
|
package/docs/vocabulary.md
CHANGED
|
@@ -140,26 +140,53 @@ the chain.
|
|
|
140
140
|
|
|
141
141
|
## Soaking contract
|
|
142
142
|
|
|
143
|
-
A **
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
A **trap** is a worktree that volunteered as a worker seat through
|
|
144
|
+
`lobstah soak` — the validated write path; nothing else touches `soaking/`.
|
|
145
|
+
Identity is **worktree-anchored**: `.lobstah-trap` in the worktree root
|
|
146
|
+
holds a short stable id, the registration keys on it, and the address
|
|
147
|
+
(`wt:<id>`) survives session restarts. The session id inside the
|
|
148
|
+
registration is the liveness principal. **Owner:**
|
|
149
|
+
`packages/core/src/soak.ts`. **Enforcement:** sign-on is refused from a
|
|
150
|
+
repo's primary checkout, and a live foreign session in an owned worktree is
|
|
151
|
+
refused (the session lock); a stale one is adopted.
|
|
150
152
|
|
|
151
153
|
| Word | Meaning |
|
|
152
154
|
| ---- | ------- |
|
|
153
|
-
| `soak` | Sign
|
|
154
|
-
| `stow` | Sign
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
155
|
+
| `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. |
|
|
156
|
+
| `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
|
+
| 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
|
+
| 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. |
|
|
159
|
+
| 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. |
|
|
160
|
+
| 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. |
|
|
161
|
+
| 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. |
|
|
162
|
+
| 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
163
|
|
|
159
164
|
Delivery routes by ownership, same as watches: a continuation for a chain
|
|
160
|
-
claimed by a live
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
claimed by a live trap is addressed back to that trap and stays sticky.
|
|
166
|
+
Sessions are never conscripted — a thread works bait only after opting in,
|
|
167
|
+
and nothing addressed is ever silently rerouted.
|
|
168
|
+
|
|
169
|
+
## Helm contract
|
|
170
|
+
|
|
171
|
+
The **helm** is the orchestrator seat: one interactive session signed on as
|
|
172
|
+
the lobsterman for its grounds through `lobstah man helm` — the validated
|
|
173
|
+
write path; nothing else touches `helm/`. Soak enlists workers; helm enlists
|
|
174
|
+
the one who dispatches to them. **Owner:** `packages/core/src/helm.ts`.
|
|
175
|
+
**Enforcement:** one registration file per grounds — the data model cannot
|
|
176
|
+
hold two; a live foreign holder refuses sign-on without `--take`. The rule
|
|
177
|
+
is strict: once a helm is claimed, the orchestrator verbs that consume helm
|
|
178
|
+
state (`man wait`, `man report`, the lobsterman park) are reserved for the
|
|
179
|
+
helm session — any other caller is refused with guidance (or, for the hook,
|
|
180
|
+
silently ignored). Read verbs (`man tend`, `man brief`) and the enlistment
|
|
181
|
+
verbs stay open. A stale helm reserves nothing.
|
|
182
|
+
|
|
183
|
+
| Word | Meaning |
|
|
184
|
+
| ---- | ------- |
|
|
185
|
+
| `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. |
|
|
186
|
+
| `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
|
+
| 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
|
+
| 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 — for a helm session, at `[helm].reportSecs` cadence — the park itself. Change-gated: an empty delta is never delivered. |
|
|
163
190
|
|
|
164
191
|
## Exit codes
|
|
165
192
|
|