lobstah 0.7.2 → 0.7.3

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.
@@ -32,6 +32,7 @@ The descriptor's `repo` field resolves here; the key is what dispatchers name.
32
32
  | `env` | no | Environment merged into every dispatch for this repo. |
33
33
  | `pickup` | no (`false`) | Opt this repo into `[pickup.github]` multi-repo mode. Explicit per repo — nothing becomes pickable by being configured. |
34
34
  | `pushEarly`, `draftPr`, `checkpointOnStop` | no (inherit `[limits]`) | Override remote preservation for this repo's headless dispatches. |
35
+ | `poolKeep` | no | Extra gitignore-style patterns a pool reset keeps (e.g. `["apps/ios/DerivedData/"]`), on top of the built-in keep list. See [`[pools.<name>]`](#poolsname--warm-worktrees-for-headless-dispatches). |
35
36
  | `humanGateChecks` | no | Check names that fail until a person approves the change (e.g. `["owner approval"]`). `*` matches any run of characters. On a PR of a dispatch in this repo, a failed human gate never starts a PR repair or a CI-fix continuation. A PR whose only failing checks are human gates shows `repair.status: waiting` with `heldBy: human-gate`. A worker adds gates for one PR with `lobstah report --human-gate <check>`. |
36
37
 
37
38
  `[repos.<key>.harness]` — per-repo harness defaults: `default` (`claude` \|
@@ -156,6 +157,87 @@ repos = ["homebase", "matcha"]
156
157
  repos = ["lobstah", "lavish"]
157
158
  ```
158
159
 
160
+ ## `[pools.<name>]` — warm worktrees for headless dispatches
161
+
162
+ A pool is a set of worktrees for one repo that the daemon keeps warm (checked
163
+ out, dependencies installed), with no session attached. A dispatch that names
164
+ the pool takes a free one, and lobstah resets it and starts a fresh headless
165
+ session there: a new conversation without the cold start (new worktree, full
166
+ install, first build). Pools are separate from traps; a trap never takes pool
167
+ work, and pool work is never addressed to one.
168
+
169
+ ```toml
170
+ [pools.codeclaw]
171
+ repo = "homebase"
172
+ size = 3
173
+ overflow = "headless" # or "queue"
174
+ ```
175
+
176
+ | Key | Default | Meaning |
177
+ |---|---|---|
178
+ | `repo` | — (required) | The `[repos.<key>]` the pool's worktrees check out. |
179
+ | `size` | `1` | How many worktrees the daemon keeps. Slots live at `~/.lobstah/pools/<name>/1` … `/<size>`. Lowering it leaves the extra slots on disk, unused. |
180
+ | `overflow` | `"headless"` | When every pool worktree is taken: `"headless"` runs the dispatch in a normal cold worktree (`worktrees/<id>`); `"queue"` leaves it queued until a pool worktree is free (`man tend` shows `queued work waits: no free worktree in pool <name>`). |
181
+
182
+ **Dispatching.** `lobstah dispatch --pool <name> --brief …` (`--repo` defaults
183
+ to the pool's repo and must match it if given). The OpenClaw
184
+ `lobstah_dispatch` tool takes `pool`, and `[pickup].pools` routes tracker
185
+ issues to a pool. The descriptor field is `pool`. A pool dispatch counts
186
+ against `[limits].maxConcurrent` like any headless dispatch.
187
+
188
+ **Claim and reset.** The runner takes the first free slot (its worktree lock,
189
+ the same lock every headless dispatch holds) and resets it: fetch trunk,
190
+ check out `lobstah/<id>` from `origin/<trunk>` (a daemon repair starts from
191
+ its PR's head instead), remove untracked and ignored files except the keep
192
+ list, remove production env files, then run the repo's `setup` commands. The
193
+ first status note says `pool worktree <name>/<slot>`, or `pool <name> full:
194
+ cold worktree`. Evidence records `pool: <name>/<slot>`.
195
+
196
+ The keep list (gitignore-style, matched at any depth unless anchored) is
197
+ dependency installs, build caches and local env files:
198
+ `node_modules/`, `.pnpm-store/`, `.venv/`, `vendor/bundle/`, `Pods/`,
199
+ `.turbo/`, `.next/cache/`, `.nx/cache/`, `.cache/`, `*.tsbuildinfo`,
200
+ `target/`, `.gradle/`, `.build/`, `.env*`, `.dev.vars`, plus the repo's
201
+ `poolKeep`. Any `.env*` file whose name contains `prod` is removed even so: a
202
+ production env file is never left in a pool worktree.
203
+
204
+ **Safety.** A reset never discards work. A slot with uncommitted changes
205
+ (tracked edits, or untracked files git does not ignore, outside `scratch`
206
+ paths) or commits on no remote branch is taken out of rotation instead, a
207
+ `pool-out` helm notice names it and its path, and the dispatch is served per
208
+ `overflow`. Commit and push (or discard) the work there; the next warm-up
209
+ pass finds it clean and pushed and returns it (a quiet `pool-returned`
210
+ notice).
211
+
212
+ **Release.** A slot returns to the pool when its dispatch finishes (done,
213
+ failed or cancelled): the lock is released, and the runner has pushed the
214
+ branch (`pushEarly`/`checkpointOnStop`). The warm-up then checks it; one left
215
+ with unpushed or uncommitted work goes out of rotation as above.
216
+
217
+ **Follow-ups** keep `[limits].reuseWorktree`: a follow-up of a pool dispatch
218
+ continues in the same pool worktree, same branch and HEAD, while it is free
219
+ and no pool dispatch has claimed it since. Once a pool dispatch has reset it,
220
+ the follow-up falls back exactly as when the origin's worktree is gone (a
221
+ fresh worktree from trunk, cold session with a progress note) and its first
222
+ status note says `fresh worktree (origin pool worktree <name>/<slot> was
223
+ reused by <id>)`. A follow-up dispatched with `--pool` takes that fresh
224
+ worktree from the pool; one without stays out of it.
225
+
226
+ **Warm-up.** Each tick the daemon starts a warm-up process for a pool with
227
+ something due (at most one per pool, at most once per 30s): it creates
228
+ missing slots (a detached checkout of trunk, then `setup`) while the
229
+ worktrees volume has `[limits].minFreeGB` free, fetches trunk every 10
230
+ minutes, checks slots whose dispatch has finished, and re-checks
231
+ out-of-rotation slots every 5 minutes. It takes a slot's lock before it
232
+ looks at it, so a claimed slot is never touched. Its log is
233
+ `~/.lobstah/pools/<name>/warm.log`.
234
+
235
+ **Visibility and cleanup.** `lobstah man tend` and `lobstah status` list each
236
+ pool: size, free, which dispatch holds each claimed slot, which slots are out
237
+ of rotation and why, and which are still being made. Pool worktrees live
238
+ outside `~/.lobstah/worktrees`: `lobstah cull`, the retention cull,
239
+ `releaseOnMerge` and the free-space guard never remove them.
240
+
159
241
  ## `[pickup]` — tracker loops (`lobstah pick`)
160
242
 
161
243
  | Key | Default | Meaning |
@@ -163,6 +245,7 @@ repos = ["lobstah", "lavish"]
163
245
  | `pollSecs` | `45` | Poll cadence. Outbound only — no webhooks, ever. |
164
246
  | `liveComment` | `true` | Keep one editable, marked status comment per dispatch. Routine edits are capped at once per minute; human-needed and terminal transitions still post a fresh notification comment. Falls back to transition comments if editing is unavailable. |
165
247
  | `notifyCommand` | — | Pickup's own hook, fired on tracker-report transitions with `LOBSTAH_KEY`, `LOBSTAH_UUID`, `LOBSTAH_VERB`, `LOBSTAH_NOTE`, `LOBSTAH_PR_URL`. |
248
+ | `pools` | `[]` | Pool names (`["codeclaw"]`). An issue dispatch for repo R runs in the first listed `[pools.<name>]` whose `repo` is R. Review rounds are follow-ups and keep their chain's worktree rule. |
166
249
 
167
250
  ### Token sources (both trackers)
168
251
 
package/docs/man.md CHANGED
@@ -1308,6 +1308,47 @@ replaces the custom title. `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` on the
1308
1308
  command that starts Claude Code stops that for that one process; the start
1309
1309
  command `man throw` opens sets it. Codex also sets the terminal title.
1310
1310
 
1311
+ ### Warm worktree pools
1312
+
1313
+ Bursty work (a tracker's issues, CodeClaw's delegations) wants a fresh
1314
+ conversation per issue but not a cold start. A pool
1315
+ ([`[pools.<name>]`](configuration.md#poolsname--warm-worktrees-for-headless-dispatches))
1316
+ is a set of worktrees for one repo that the daemon keeps checked out and
1317
+ installed, with no session attached:
1318
+
1319
+ ```
1320
+ lobstah dispatch --pool codeclaw --brief-text "<full brief>"
1321
+ ```
1322
+
1323
+ The runner takes a free pool worktree, resets it to `lobstah/<id>` from trunk
1324
+ (untracked files go, except dependency installs, build caches and local env
1325
+ files; production env files always go), runs the repo's `setup`, and starts a
1326
+ new headless session there. The first status note names it: `pool worktree
1327
+ codeclaw/2`. When the pool is full, `overflow = "headless"` runs the dispatch
1328
+ in a cold worktree (`pool codeclaw full: cold worktree`) and `overflow =
1329
+ "queue"` leaves it queued until a pool worktree frees up. Pool work counts
1330
+ against `maxConcurrent`, and no trap ever takes it.
1331
+
1332
+ A reset never discards work. A pool worktree with uncommitted changes or
1333
+ commits on no remote is taken out of rotation, and a `pool-out` notice names
1334
+ it (`pool worktree codeclaw/2 (~/.lobstah/pools/codeclaw/2) is out of
1335
+ rotation: 1 commit(s) on no remote branch`). Push or discard the work there;
1336
+ the daemon's next warm-up returns it.
1337
+
1338
+ Follow-ups keep the usual rule: `lobstah send <id>` to finished pool work
1339
+ continues the same conversation in the same pool worktree while no pool
1340
+ dispatch has reset it since. Once one has, the follow-up starts as it does
1341
+ when its origin's worktree is gone: a fresh worktree, a cold session with a
1342
+ progress note, and a first note that says `fresh worktree (origin pool
1343
+ worktree codeclaw/2 was reused by <id>)`.
1344
+
1345
+ `man tend` and `lobstah status` list each pool:
1346
+
1347
+ ```
1348
+ pools[1]{pool,repo,size,free,overflow,claimed,out,warming}:
1349
+ codeclaw,homebase,3,1,headless,1: 6a1f0c2e,2: 1 commit(s) on no remote branch (after 9b3d0a11),
1350
+ ```
1351
+
1311
1352
  ### Culling and disk space
1312
1353
 
1313
1354
  Worktrees are 1 to 8 GB each. `lobstah cull` sweeps what is finished: `done/`
@@ -1321,7 +1362,9 @@ when the same clean-and-pushed safety check passes; unsaved soak checkouts
1321
1362
  remain protected even without a registration. A worktree that follow-ups
1322
1363
  reused is one worktree shared by the chain: it stays while any dispatch in
1323
1364
  the chain is queued or active, and it ages from the newest dispatch that
1324
- used it.
1365
+ used it. Pool worktrees (`~/.lobstah/pools/`) are never culled, by `lobstah
1366
+ cull` or by the daemon: a pool keeps its worktrees, and the dispatch records
1367
+ that used them age out as usual.
1325
1368
 
1326
1369
  Without `--apply` it is a dry run: it measures each target and prints the
1327
1370
  sizes. A worktree is measured with one `du -sk`; where `du` is missing
@@ -396,7 +396,7 @@ evidence).
396
396
  | descriptor `pr` | `{ url, headRefName?, headSha? }`: the existing PR a dispatch works on. A PR repair and a pickup rebase chore carry it. The runner pushes no branch and opens no PR for such a dispatch; its worker pushes to the PR's head branch. |
397
397
  | push rule | What a repair or rebase brief tells its worker: push only to the PR's head branch; on a non-fast-forward rejection, fetch, rebase the commits onto the moved head again, and push with `--force-with-lease` on the head just fetched, at most three times; a hook failure from a real test or type error is not retried; when it cannot push, report `failed "push rejected: <rejection text>; moved head <sha>"` and leave the PR as it was. That report marks the PR's repair `blocked` at the moved head and posts a `push-failed` notice. |
398
398
  | checks unknown | Without `Checks: read`, the check re-reads the PR without `statusCheckRollup`: the PR state is recorded, `checks.unknown` is `no permission`, and the check's output carries the permission `error`. `pr:ready` never stands on unknown checks. |
399
- | PR record | `~/.lobstah/prs/<owner>__<repo>__<n>.json` — the PR's latest observation keyed by the PR, not by a dispatch: the evidence `pr` object (with `title`, read on every check; a title change is not a state change) plus `key`, `repo` (`<owner>/<repo>`), `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR), and `firstSeenAt` (the time of the first observation; written once, never rewritten). `firstSeenAt`, then the PR number, is the order of every PR list. A record from before `firstSeenAt` existed sorts by number at the earliest `firstSeenAt` in the set, and its next observation writes that time as its `firstSeenAt`. **Owner:** `packages/core/src/prs.ts` (`upsertPr`, `readPrs`); the one writer is the preset's observation path (`observePr`), on every observation, man-owned or dispatch-owned — a dispatch-owned one also stamps that dispatch's evidence, which stays the per-dispatch view. Tend's `pr:*` kinds and `stack-ready` aggregation (member PR attention is contained in one stack item), 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. |
399
+ | PR record | `~/.lobstah/prs/<owner>__<repo>__<n>.json` — the PR's latest observation keyed by the PR, not by a dispatch: the evidence `pr` object (with `title`, read on every check; a title change is not a state change) plus `key`, `repo` (`<owner>/<repo>`), `dispatches` (the ids whose watch observed it; empty for a human's or a culled PR), and `firstSeenAt` (the time of the first observation; written once, never rewritten). `firstSeenAt`, then the PR number, is the order of every PR list. A record from before `firstSeenAt` existed sorts by number at the earliest `firstSeenAt` in the set, and its next observation writes that time as its `firstSeenAt`. **Owner:** `packages/core/src/prs.ts` (`upsertPr`, `readPrs`); the one writer is the preset's observation path (`observePr`), on every observation, man-owned or dispatch-owned — a dispatch-owned one also stamps that dispatch's evidence, which stays the per-dispatch view. Tend's `pr:*` kinds and `stack-ready` aggregation (member readiness is contained in one stack item), 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. |
400
400
 
401
401
  Every event carries `headSha`. A dispatch-owned PR watch records work events,
402
402
  then the repair planner decides whether to follow up. One repair runs per PR
@@ -481,7 +481,7 @@ worktree is refused (the session lock); a stale one is adopted.
481
481
  | protected ref | `refs/lobstah/traps/<trapId>` in the trap's repository: the trap's last HEAD, written at sign-on and before stow, a ghost sweep, or `cull` removes the checkout. It is not a branch, so worktree removal and branch cleanup leave it and its commit in place. A missing checkout is recreated from it. A trap checkout whose revision cannot be written there is not removed by a ghost sweep or cull. |
482
482
  | trap request | A trap the human asked for from the glass's **+ New trap** button: a `trap-request` request in `requests/<id>.json` with a repo and a harness. It wakes the helm as a `trap-request` event; the helm starts it with `man throw --new --request <id>`, which takes the request's repo and harness and closes it. |
483
483
  | notice | The helm's attention channel for non-status events (`~/.lobstah/notices/`): reservations (`trap-starting`), reservations that did not start (`trap-start-failed`), trap requests from the glass (`trap-request`), sign-ons, a trap free to take work (`trap-available`), a batch throw settled (`trap-batch`), sign-offs, ghosts, defective enlistments, orphaned work, bounced messages, PRs merged or closed, watches held over the fork cap (`watch-held`), failing (`watch-failing`), and recovered (`watch-recovered`), free-space holds (`disk-held`, `disk-cleared`), worktrees released after their PR merged (`worktree-released`, one per cull pass), a repair or rebase that could not push to its PR's branch (`push-failed`), repairs stopped on a PR that made no progress (`repair-stopped`), and a human's answer to a decision (`decision-answer`; its ref is the request id in `~/.lobstah/requests/`). A trap leaves the registry only through a `trap-stowed` or `trap-ghosted` notice — the end-state is always explicit. Consumed by `man wait`/the park. A trap's start wakes once, with `trap-available` when it is listening; `trap-starting`, `trap-signed-on`, `trap-stowed`, `trap-listening` (older homes), a ghost of an idle trap, and a batch member's `trap-available` are quiet: consumed without waking anyone, shown in tend, the glass, and (sign-offs and idle ghosts) the digest's `traps` table. A batch throw wakes once, with `trap-batch` when every throw settled: counts, and the names that failed. tend always shows the recent tail. |
484
- | stack-ready notice | One wake for an all-ready PR stack, with the PRs in bottom-first merge order. One persistent item contains member readiness and human problems, without member lobsters; growth replaces its notice, and bottom-first merges keep its identity and ack without another wake. Its link follows the current top PR; legacy duplicates collapse silently. |
484
+ | stack-ready notice | One wake for an all-ready PR stack, with the PRs in bottom-first merge order. One persistent item contains member readiness, without member ready lobsters; while any member is not ready (a ready member still inside its settle window included) the item is quiet and reads `stack waiting: <n> of <m> ready: … — #<k> ready, confirming for <x> more min; #<j> conflicts`; growth replaces its notice, and bottom-first merges keep its identity and ack without another wake. Its link follows the current top PR; legacy duplicates collapse silently. |
485
485
 
486
486
  Delivery routes by ownership, same as watches: a continuation for a chain
487
487
  claimed by a live trap is addressed back to that trap and stays sticky.
@@ -543,8 +543,8 @@ never until someone acknowledges it.
543
543
  | `pr:review` | An open PR has unresolved review questions, or requested changes that lobstah cannot repair, has exhausted, or is configured not to repair. | Every thread resolved and no changes requested, or merged / closed. |
544
544
  | `pr:checks` | An open PR has a failed latest check, and lobstah cannot repair it, has exhausted attempts, or is configured not to repair. | Green on the head, or merged / closed. |
545
545
  | `pr:conflict` | An open PR conflicts with its base, and lobstah cannot repair it, has exhausted attempts, or is configured not to repair. | The merge state leaves `DIRTY`, or merged / closed. |
546
- | `pr:ready` | An open, non-draft PR has no review condition, a mergeable state (`CLEAN`, `HAS_HOOKS`, or `UNSTABLE`), and no failed, pending, or unknown latest checks. It is approved or has at least one check, continuously on the same head for `readySettleSecs` (default 600; `0` is immediate). Attention reads re-evaluate expiry without requiring a new forge event. In a trunk-rooted same-repo stack of two or more non-fork PRs, member attention appears only inside the one stack item; the stack wakes once. | Merged or closed, or the ready conditions stop holding; any not-ready observation or new head resets settling. |
547
- | `stack-ready` | Every open member of a trunk-rooted same-repo base/head chain meets the identical `pr:ready` rule, including settling. Enabled with `pr:ready` (or explicitly with `stack-ready`). One persistent item per stack, born at the bottom PR and carried by surviving membership through growth, restacks and merges. Member ready/watch items and human problems are grouped inside it, not separate lobsters; Approval Gate failures never create an item. Links open the current top PR, with member links inside. Tend, digest, and the existing glass group also show partial readiness. Existing `pr:` watches discover missing links at their normal cadence; `lobstah watch add pr:<owner>/<repo>#<n>` follows a stack from any member. | Any member becomes unready or gets a new head; all ready again wakes once. Growth replaces the notice and wakes once when the larger stack is ready. Bottom-first merges keep the item and ack without a wake for unchanged surviving heads; a remaining lone PR returns to normal behavior. Legacy duplicates collapse without a wake. |
546
+ | `pr:ready` | An open, non-draft PR has no review condition, a mergeable state (`CLEAN`, `HAS_HOOKS`, or `UNSTABLE`), and no failed, pending, or unknown latest checks. It is approved or has at least one check, continuously on the same head for `readySettleSecs` (default 600; `0` is immediate). Attention reads re-evaluate expiry without requiring a new forge event. In a trunk-rooted same-repo stack of two or more non-fork PRs, member readiness appears only inside the one stack item; the stack wakes once. | Merged or closed, or the ready conditions stop holding; any not-ready observation or new head resets settling. |
547
+ | `stack-ready` | Every open member of a trunk-rooted same-repo base/head chain meets the identical `pr:ready` rule, including settling. Enabled with `pr:ready` (or explicitly with `stack-ready`). One persistent item per stack, born at the bottom PR and carried by surviving membership through growth, restacks and merges. Member ready/watch items are grouped inside it, not separate lobsters; a member's human problem (`pr:conflict`, `pr:checks`, `pr:review`, `pr:draft`) also stands as that member's own item and walks by its own kind. Approval Gate failures never create an item. Links open the current top PR, with member links inside. Only an all-ready stack walks (pet, glass lob, helm wake); a partly ready one is a quiet standing row in tend (shown as `stack-waiting`), the digest, and the existing glass group, worded `stack waiting: <n> of <m> ready`, with a settling member written as `ready, confirming for <x> more min`. Existing `pr:` watches discover missing links at their normal cadence; `lobstah watch add pr:<owner>/<repo>#<n>` follows a stack from any member. | Any member becomes unready or gets a new head; all ready again wakes once. Growth replaces the notice and wakes once when the larger stack is ready. Bottom-first merges keep the item and ack without a wake for unchanged surviving heads; a remaining lone PR returns to normal behavior. Legacy duplicates collapse without a wake. |
548
548
  | `report` | A filed report (`report --report`, `man file`) has no ack for this filing. Opt-in. Key `report:<lane>:<uuid>` or `report:helm:<grounds>:<rid>`. | Not cleared: it stays listed until culled. Opening it on the glass acks it (`by: glass`, the first time kept; no wake, and a CLI or API read does not), `lobstah attention ack <key>` acks it, and a newer report in the same chain acks the older; an acked report no longer walks, and `man tend`'s `viewed` column shows when it was acked. Refiling it stands it again until it is opened again. |
549
549
 
550
550
  `pr:*` kinds read only the `pr:` watch's evidence — never a forge call —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstah",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Harness-agnostic, token-efficient supervision framework for coding agents",
5
5
  "license": "MIT",
6
6
  "author": "aequitas labs LLC",