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.
@@ -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 |
@@ -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 stow --session <id> # sign off; an open catch requeues
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
- The session id comes from the plugin's session-start brief (`lobstah man
233
- brief` announces it into the conversation). Once soaking, the same Stop hook
234
- that parks a lobsterman parks the worker: at turn end it waits for bait,
235
- claims it, and wakes with the brief. While it works a catch, the park wakes
236
- it for `lobstah send` messages and cancels instead.
237
-
238
- Routing follows ownership: `dispatch --for session:<id>` targets one trap;
239
- unaddressed bait for a matching repo prefers a parked trap for
240
- `[soak].deferSecs` before the daemon spawns headless; a watch continuation
241
- for a chain a soaking session claimed is addressed back to that session. A
242
- registration whose heartbeat lapses past `[soak].ttlSecs` is a **ghost
243
- trap** — swept, its catch requeued. Nobody is conscripted: only a session
244
- that ran `soak` ever receives work.
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
 
@@ -140,26 +140,53 @@ the chain.
140
140
 
141
141
  ## Soaking contract
142
142
 
143
- A **soaking trap** is a live interactive session that volunteered as a worker
144
- through `lobstah soak` — the validated write path; nothing else touches
145
- `soaking/`. Sub-agent workers are the traps lobstah sets itself; a soaking
146
- session is a trap already in the water, and dispatch drops bait into it
147
- before building a new one. **Owner:** `packages/core/src/soak.ts`.
148
- **Enforcement:** sign-on is refused from a repo's primary checkout (never
149
- claimable) and when another session already soaks the same worktree.
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 a session on: it parks at turn end (Stop hook) and takes matching bait from the work queue. `--one` stows after the first catch. |
154
- | `stow` | Sign a session off; an open catch goes back to the queue (a cancelled one finalizes as failed). |
155
- | bait address | `--for session:<id>` on a dispatch targets one soaking session. Addressed bait waits for its trap until the registration is gone; unaddressed bait defers to a parked matching trap for `[soak].deferSecs`, then the daemon spawns headless. |
156
- | catch | The active dispatch a soaking session claimed (`claim.json` in the active dir). One catch per trap; one active item per worktree. The daemon never spawns or restarts it — the session's reports are its liveness. |
157
- | ghost trap | A registration whose heartbeat lapsed past `[soak].ttlSecs` — a lost trap that keeps fishing. The sweep hauls it out and requeues its catch. A fresh report on the catch keeps a mid-turn session out of the sweep. |
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 soaking session is addressed back to that session; once it
161
- ghosts, the same bait forks headless. Sessions are never conscripted — a
162
- thread works bait only after opting in.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstah",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Harness-agnostic, token-efficient supervision framework for coding agents",
5
5
  "license": "MIT",
6
6
  "author": "aequitas labs LLC",