lobstah 0.5.13 → 0.6.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/dist/main.js +7044 -3118
- package/dist/runner.js +1513 -1113
- package/docs/configuration.md +15 -9
- package/docs/harness/claude-code.md +14 -0
- package/docs/harness/codex.md +12 -0
- package/docs/man.md +471 -39
- package/docs/pickup.md +11 -3
- package/docs/vocabulary.md +120 -22
- package/package.json +1 -1
package/docs/man.md
CHANGED
|
@@ -54,12 +54,13 @@ the background, dispatch it with the `lobstah` CLI instead of doing it inline:
|
|
|
54
54
|
- `lobstah status [<id>]`, `lobstah ls` — check progress when I ask, not on a loop.
|
|
55
55
|
- `lobstah send <id> "<instruction>"` — steer a live dispatch, add to a queued
|
|
56
56
|
dispatch's inbox, or wake a finished chain as a follow-up. Sending to any
|
|
57
|
-
member of a finished chain follows up its newest member.
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
member of a finished chain follows up its newest member. To choose a new
|
|
58
|
+
worker, harness, or model, use `lobstah dispatch --follow-up <id> --repo
|
|
59
|
+
<key> --brief-text "<instruction>" --for <name> --harness <kind> --model <m>`.
|
|
60
60
|
- `lobstah cancel <id>` — stop one.
|
|
61
|
-
- A dispatch reporting `needs-decision`
|
|
62
|
-
|
|
61
|
+
- A dispatch reporting `needs-decision` or `blocked`: when the brief or your
|
|
62
|
+
context gives the answer, `lobstah send` it and tell me what you decided;
|
|
63
|
+
when the choice is mine, frame it with `lobstah man ask <id>` for me to answer.
|
|
63
64
|
- `done` means brief fulfilled with a branch + commits; report the evidence
|
|
64
65
|
(`~/.lobstah/state/<id>.evidence`) and never merge anything yourself.
|
|
65
66
|
```
|
|
@@ -96,6 +97,11 @@ finished member. If that member was last claimed by a trap still signed on,
|
|
|
96
97
|
the follow-up returns to that trap unless `--for` overrides it. Otherwise it
|
|
97
98
|
is unaddressed for a headless worker.
|
|
98
99
|
|
|
100
|
+
A send to a dispatch expects a reply. The worker's next note after the send
|
|
101
|
+
wakes `man wait`: a `working` or `paused` note arrives once as a `reply` event
|
|
102
|
+
with the note and the sent instruction's first line, and any other verb wakes
|
|
103
|
+
as itself. `--no-reply` sends without expecting a reply.
|
|
104
|
+
|
|
99
105
|
Attach refuses while a dispatch is `working` (two writers, one session);
|
|
100
106
|
follow the logs or `send` instead, or cancel and then attach.
|
|
101
107
|
|
|
@@ -149,17 +155,57 @@ review, a deploy) reports `paused "<note>" --waiting-on review --link <url>`
|
|
|
149
155
|
before it waits. Tend, `status`, `ls`, and the glass then show
|
|
150
156
|
`paused: waiting on review` with the link and the time waited. It is a
|
|
151
157
|
state, not a question: nothing to answer, no attention, no pet. A paused
|
|
152
|
-
headless worker is never counted as wedged and its wall clock stops
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`
|
|
158
|
+
headless worker is never counted as wedged and its wall clock stops.
|
|
159
|
+
|
|
160
|
+
A paused headless dispatch is **parked**. When the worker's turn ends on
|
|
161
|
+
`paused`, the runner ends the session, stops what the harness started,
|
|
162
|
+
and exits. The dispatch stays active and keeps its worktree lock, but it
|
|
163
|
+
holds no `maxConcurrent` slot and runs no process. It wakes into the same
|
|
164
|
+
session when an operator message reaches its inbox (`lobstah send <id>`)
|
|
165
|
+
or when its `--until` time passes, as soon as a slot is free; a waking
|
|
166
|
+
dispatch takes the slot before queued work. The first prompt of the woken
|
|
167
|
+
session says why it woke and carries the messages. A parked dispatch
|
|
168
|
+
counts in the status output, without a slot:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
$ lobstah man tend
|
|
172
|
+
active: headless: 1 of 2; traps: 0; parked: 1 (no slot)
|
|
173
|
+
parked (no slot)[1]{id,waitingOn,for,link,note}:
|
|
174
|
+
6a1f0c2e,review,80m,https://github.com/o/r/pull/7,waiting for approval
|
|
175
|
+
$ lobstah daemon status
|
|
176
|
+
slots: 1 of 2 work in use, 1 free
|
|
177
|
+
parked: 1
|
|
178
|
+
parkedOn: 6a1f0c2e waiting on review for 80m https://github.com/o/r/pull/7
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`lobstah doctor`'s `daemon` row ends in `parked: 1, no slot (…)`, and the
|
|
182
|
+
glass header shows `parked: 1 (no slot)`. A worker that reported `done` or
|
|
183
|
+
`failed` holds no slot: its runner exits within `[limits].exitGraceSecs`,
|
|
184
|
+
and a restart of the daemon does not wait for it (see [status
|
|
185
|
+
verbs](vocabulary.md#status-verbs)). A parked dispatch has no runner, so a
|
|
186
|
+
restart does not wait for it either. A paused trap is kept out of the ghost
|
|
187
|
+
sweep until `--until` or `[soak].pausedTtlSecs` (24 hours). See [Waiting
|
|
188
|
+
on](vocabulary.md#waiting-on).
|
|
189
|
+
|
|
190
|
+
**A PR's end finishes the waits on it.** When the PR a dispatch waits on
|
|
191
|
+
with `paused --waiting-on pr` or `--waiting-on review` merges, the daemon
|
|
192
|
+
finishes that dispatch `done` with the note `the PR merged: <url>`. When
|
|
193
|
+
the PR closes without merge, it finishes it `failed` with `the PR closed
|
|
194
|
+
without merge: <url>`. The PR waited on is the `--link` when it names a
|
|
195
|
+
GitHub PR, else the dispatch's own PR, else its chain's PR. Every paused
|
|
196
|
+
dispatch waiting on that PR is finished, in the whole chain. The daemon
|
|
197
|
+
does this after it observes PR watches and before its cull pass, so
|
|
198
|
+
`[limits].releaseOnMerge` releases the chain's worktrees in the same pass.
|
|
199
|
+
`report paused --waiting-on pr|review` registers the watch of the
|
|
200
|
+
dispatch's own PR when it has none, so the merge is observed.
|
|
156
201
|
|
|
157
202
|
### PR state after done
|
|
158
203
|
|
|
159
|
-
A dispatch reports
|
|
204
|
+
A dispatch reports its PR with `report <id> <verb> --pr <url>`, which
|
|
160
205
|
registers a `pr:` watch for the chain so the PR stays observed (see the
|
|
161
206
|
PR preset in [vocabulary.md](vocabulary.md#the-pr-preset); `--no-watch`
|
|
162
|
-
opts out).
|
|
207
|
+
opts out). A trap's PR is tracked from its first push. What you get
|
|
208
|
+
depends on what runs:
|
|
163
209
|
|
|
164
210
|
- **Only the helm park or `man wait`** (no service): PR state badges in
|
|
165
211
|
`man tend`, `lobstah catch`, and the glass (`merged`, `draft`, `review`,
|
|
@@ -177,15 +223,25 @@ succeeds and the watch's check records `lastError`.
|
|
|
177
223
|
|
|
178
224
|
Which commands register a watch. Only these write points register one:
|
|
179
225
|
|
|
180
|
-
- `lobstah report <id>
|
|
181
|
-
worker
|
|
226
|
+
- `lobstah report <id> <verb> --pr <url>` registers the watch for the PR the
|
|
227
|
+
worker opened. Any verb but `failed` does this. A PR already watched is
|
|
228
|
+
not registered again.
|
|
229
|
+
- `lobstah report <id> paused --waiting-on pr|review` registers the watch
|
|
230
|
+
for the dispatch's own PR. A `--link` to another PR registers nothing.
|
|
231
|
+
- `lobstah soak beat` registers the watch for a trap's PR from its first
|
|
232
|
+
push. At most once a minute, it reads the trap's branch. When the branch
|
|
233
|
+
is not trunk and has an upstream, it asks `gh pr view <branch>` for the PR.
|
|
234
|
+
It records a new PR in the dispatch's evidence, where `lobstah catch` and
|
|
235
|
+
`lobstah status <id>` show it.
|
|
182
236
|
- `lobstah watch add <key>` registers the watch you name.
|
|
183
237
|
- `lobstah watch backfill --apply` registers watches for PRs in old dispatch
|
|
184
|
-
history
|
|
238
|
+
history, and fetches the title of each PR record that has none (one
|
|
239
|
+
`gh pr view --json title` per record). Without `--apply` it only lists
|
|
240
|
+
them. Nothing runs it for you.
|
|
185
241
|
|
|
186
242
|
Read commands never register a watch: `catch`, `man tend`, `status`, `ls`,
|
|
187
|
-
`prs`, `
|
|
188
|
-
|
|
243
|
+
`prs`, `attention`, and the glass. They read PR records and watches that
|
|
244
|
+
already exist. Use `watch check-pr <key>` to force one PR-watch refresh.
|
|
189
245
|
|
|
190
246
|
The first check of a new watch is a baseline:
|
|
191
247
|
|
|
@@ -207,16 +263,99 @@ and requested review changes. It follows up the newest dispatch in the PR's
|
|
|
207
263
|
chain. A conflict brief names the PR's base branch, including a stacked
|
|
208
264
|
base. A check brief names each failed check and its details URL. The watch
|
|
209
265
|
records each attempt and stops at `[watch].maxRepairsPerPr` per head SHA.
|
|
266
|
+
Each failing check gets at most one repair round per PR and head SHA; the
|
|
267
|
+
PR record's `repair.checks` lists the checks that had their round. The
|
|
268
|
+
same rule holds for CI-fix continuations from `lobstah pick`.
|
|
269
|
+
|
|
270
|
+
**Human gates.** A check that fails until a person approves the change is
|
|
271
|
+
a human gate. It gets no repair round and no CI-fix continuation on that
|
|
272
|
+
PR. Two sources name gates: `[repos.<key>].humanGateChecks` in the config,
|
|
273
|
+
and a worker's `lobstah report <id> <verb> --human-gate "<check>"` (once
|
|
274
|
+
per check). The report records the gate in the worker's evidence
|
|
275
|
+
(`humanGates`) and on the PR record (`humanGates`). A check brief lists
|
|
276
|
+
the failing gates and tells the worker to name a gate it finds. A PR whose
|
|
277
|
+
only failing checks are human gates shows `repair.status: waiting` with
|
|
278
|
+
`heldBy: human-gate`.
|
|
210
279
|
It does not repair a PR with a person's newer commits or uncertain commit
|
|
211
280
|
ownership, a terminal PR, or a PR whose chain already has queued or active
|
|
212
281
|
work. `[watch].autoRepair`, `conflicts`, and `checks` control this behavior.
|
|
213
282
|
|
|
283
|
+
Daemon repairs run in the `chore` lane and use `[limits].choreConcurrent`.
|
|
284
|
+
A repair for a trap-built PR is addressed to its owning live trap. If the
|
|
285
|
+
trap is busy, the chore waits for up to `[watch].repairTrapWaitSecs`
|
|
286
|
+
(default 600). It then runs headless in its own checkout of the PR branch.
|
|
287
|
+
A repair for a headless-built PR runs headless and reuses its origin
|
|
288
|
+
worktree when that checkout is clean and free. A headless chore never uses
|
|
289
|
+
a trap's worktree. Only daemon-created repair chores have this bounded
|
|
290
|
+
addressed fallback. Work addressed by a person stays addressed until the
|
|
291
|
+
person releases or redirects it. `man tend`, `daemon status`, `doctor`,
|
|
292
|
+
and the glass show the repair's PR, chore lane, worker, and trap wait.
|
|
293
|
+
|
|
294
|
+
**A repair waits** while any of these is true:
|
|
295
|
+
|
|
296
|
+
- A live worker holds the PR's head branch. A live worker is an active
|
|
297
|
+
headless dispatch, or a trap with an open catch. It holds a branch when
|
|
298
|
+
its worktree has the branch checked out, when its current branch tracks
|
|
299
|
+
the branch on the remote, or when it pushed the branch during its current
|
|
300
|
+
dispatch. It holds a PR when its evidence names the PR or its chain owns
|
|
301
|
+
the PR.
|
|
302
|
+
- A live worker holds the head branch of an open PR below this PR in the
|
|
303
|
+
same stack. The stack is the one the glass shows: a PR's parent is the
|
|
304
|
+
PR whose head branch is its base branch.
|
|
305
|
+
- The PR's head, its base branch's head, or its failing checks changed less
|
|
306
|
+
than `[watch].repairSettleSecs` ago (default 600).
|
|
307
|
+
- For a checks repair: a fresh read of the latest run of each failing check
|
|
308
|
+
shows that run in progress or passed.
|
|
309
|
+
- The PR's watch is held. `lobstah cancel` on a repair dispatch holds its
|
|
310
|
+
PR's watch. `lobstah watch hold <key> [--for <id>]`
|
|
311
|
+
holds one PR's watch; with `--for`, the hold ends when that dispatch
|
|
312
|
+
ends. `lobstah watch release <key>` ends any hold.
|
|
313
|
+
|
|
314
|
+
A waiting repair is recorded on the PR record as `repair.status: waiting`,
|
|
315
|
+
with `heldBy` (`wt:<trap>`, `dispatch:<id8>`, `helm`, `hold`, `settle`,
|
|
316
|
+
`checks`, `human-gate`, or `repaired`) and `reason`. `repaired` means each
|
|
317
|
+
failing check already had its round at this head; a new commit ends it.
|
|
318
|
+
Unlike the other waits, `repaired` also raises `pr:checks` attention with
|
|
319
|
+
its reason: the round did not fix the check. A wait is not an attempt: it does not count against
|
|
320
|
+
`[watch].maxRepairsPerPr`. It raises no attention item. `man tend` lists it
|
|
321
|
+
in the `repairs waiting` table, the PR badge ends in `repair waits: <heldBy>`,
|
|
322
|
+
the glass PR modal shows the reason, and `lobstah doctor` lists it. When the
|
|
323
|
+
wait ends, the normal rules apply again.
|
|
324
|
+
|
|
325
|
+
**A repair pushes** to its PR's head branch, and so does a rebase chore
|
|
326
|
+
from pickup's merge loop. The descriptor of each names its PR (`pr`), so
|
|
327
|
+
the runner pushes no branch and opens no PR for it. The brief gives the
|
|
328
|
+
worker the push rule: push only to the PR's head branch. On a
|
|
329
|
+
non-fast-forward rejection, fetch the branch, rebase the commits onto the
|
|
330
|
+
moved head again, and push with `--force-with-lease` on the head just
|
|
331
|
+
fetched, at most three times. A push hook that fails with a real test or
|
|
332
|
+
type error is not retried: the worker fixes the error and pushes again.
|
|
333
|
+
For code already on main, the repair keeps main's version and only this
|
|
334
|
+
PR's own changes. It does not change behavior. If a conflict resolution
|
|
335
|
+
would change behavior, the worker reports `needs-decision`.
|
|
336
|
+
When the worker cannot push, it reports
|
|
337
|
+
`failed "push rejected: <rejection text>; moved head <sha>"` and leaves the
|
|
338
|
+
PR as it was. That report marks the PR's repair `blocked` at the moved head
|
|
339
|
+
(no new repair starts on that head, nor on the head the repair started
|
|
340
|
+
from) and posts a `push-failed` notice. A repair never opens a branch or a
|
|
341
|
+
PR.
|
|
342
|
+
|
|
343
|
+
Lobstah records pushes it sees in the dispatch's evidence (`pushes`): the
|
|
344
|
+
runner's own pushes, a headless worker's `git push` commands, and a trap's
|
|
345
|
+
`git push` commands (from its post-tool beat). It does not read GitHub to
|
|
346
|
+
guess who pushed. A worker that is about to push to other PRs can hold them
|
|
347
|
+
first with `lobstah watch hold <key> --for <its dispatch id>`.
|
|
348
|
+
|
|
214
349
|
Watching a PR nobody dispatched — `lobstah watch add pr:<owner>/<repo>#<n>`
|
|
215
350
|
with no `--for` — is how a helm follows a human's PR, or one whose
|
|
216
351
|
dispatch chain was culled. Every observation lands in a PR record keyed by
|
|
217
352
|
the PR, so it shows in the glass PRs tab and stacks and in tend's `pr:*`
|
|
218
353
|
attention kinds exactly like a dispatched PR (its dispatch chain column is
|
|
219
|
-
empty).
|
|
354
|
+
empty). Each PR card, PRs tab row, and PR modal header shows the PR's title
|
|
355
|
+
after its number; a stack line shows numbers only, with each title on hover.
|
|
356
|
+
`lobstah prs` prints the title, cut to 60 characters. Every check reads the
|
|
357
|
+
title again, so a rename on GitHub shows on the next check and is never
|
|
358
|
+
attention. It stays quiet while it's fine: only a failing check or a changes
|
|
220
359
|
request surfaces as a watch event; a merge or close arrives as a notice.
|
|
221
360
|
|
|
222
361
|
**PR order.** Every PR list uses one order: the glass PRs tab, the On deck
|
|
@@ -241,6 +380,15 @@ See the [attention contract](vocabulary.md#attention-contract). A PR is
|
|
|
241
380
|
something to look at, not a stall: it never flips the verdict to
|
|
242
381
|
`needs-attention` and stays out of the digest.
|
|
243
382
|
|
|
383
|
+
A question is held while a helm is signed on for its grounds and has not
|
|
384
|
+
ended a turn since the question was filed. A held question is in `man tend`,
|
|
385
|
+
`man wait`, the park, and reminders, and `lobstah attention` and `man tend`
|
|
386
|
+
mark it `held`. It is not in `attention --json` (the pet), the glass, or
|
|
387
|
+
notifyCommand. The question walks when the helm ends a turn (`man haul`)
|
|
388
|
+
without answering it; the release is recorded in `releases/<key>.json`. With
|
|
389
|
+
no helm signed on, or a helm relieved or stale past `[helm].ttlSecs`, a
|
|
390
|
+
question walks at once.
|
|
391
|
+
|
|
244
392
|
### The spyglass
|
|
245
393
|
|
|
246
394
|
The lobstah man skill brings up the glass when it takes the helm.
|
|
@@ -250,43 +398,195 @@ as a user service. `lobstah glass restart` restarts the service, or a
|
|
|
250
398
|
detached glass (stop, then `--detach`).
|
|
251
399
|
|
|
252
400
|
`lobstah glass [--port <n>]` serves tend as a live web page on 127.0.0.1
|
|
253
|
-
(default port 4949): the fleet verdict
|
|
401
|
+
(default port 4949): the fleet verdict, decision cards, every
|
|
254
402
|
dispatch with its full brief, status log, inbox, and evidence, each trap
|
|
255
403
|
with its lifecycle notices, message history, and catches, the notices
|
|
256
404
|
tail, the merge view, and watches — with filters, a table/cards toggle,
|
|
257
|
-
and the helm identified by name.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
405
|
+
and the helm identified by name. Its tabs are On deck, Dispatches, Traps,
|
|
406
|
+
PRs, Reports, and Notices. The Reports tab lists every report, unacked first,
|
|
407
|
+
then newest first, and the repo filter and the search box (title, author,
|
|
408
|
+
dispatch id, repo) apply to it. Reading the glass consumes no cursor. Each
|
|
409
|
+
live trap has a **↗ open** button at the end of its foot line. It asks the local server to focus
|
|
410
|
+
that trap's reported session link, iTerm2 session, Terminal tab, editor
|
|
411
|
+
worktree, or recorded app, in that order. The result names the step that
|
|
412
|
+
worked; app-only activation says the exact window is not known. On other
|
|
413
|
+
platforms, only a session link can open. A signed-off trap has no button; the
|
|
414
|
+
page shows its resume command as text to copy. The focus endpoint accepts
|
|
415
|
+
only a trap id in a same-origin, token-protected POST. The ⚙ popover's two
|
|
262
416
|
preferences — table or cards, and whether lobsters crawl the page — are
|
|
263
417
|
per-browser, kept in that browser's localStorage and never on disk.
|
|
264
418
|
|
|
419
|
+
On a card, the title and the badge share one line. The title shrinks first,
|
|
420
|
+
with an ellipsis, but always keeps its PR number or 8-character id. A badge of
|
|
421
|
+
12 characters or fewer always shows whole. A longer badge truncates only
|
|
422
|
+
after the title is at its minimum, never below 12 characters. The meta line
|
|
423
|
+
and the note show at most two lines. The full text of each is in its hover
|
|
424
|
+
title.
|
|
425
|
+
|
|
426
|
+
The On deck tab shows up to 8 traps. Signed-on traps come first, oldest
|
|
427
|
+
sign-on first, with name breaking ties. Traps stowed or ghosted in the last
|
|
428
|
+
hour follow, most recent sign-off first, with name breaking ties. A "+N more"
|
|
429
|
+
link opens the traps tab for the rest, in the same order. Both views show each
|
|
430
|
+
trap's current dispatch, last activity, or idle and waiting state. A dot leads
|
|
431
|
+
that state line on the deck, the traps tab cards, and the traps tab table. The
|
|
432
|
+
dot is green when the trap is working or idle and listening. It is amber when
|
|
433
|
+
the trap is parked. It is grey when the trap is not listening (its heartbeat is
|
|
434
|
+
stale) or signed off.
|
|
435
|
+
|
|
265
436
|
This is where "is the agent alive?" belongs: the helm's heartbeat age on a
|
|
266
437
|
page, not periodic proof-of-life turns in a transcript.
|
|
267
438
|
|
|
439
|
+
### Reports
|
|
440
|
+
|
|
441
|
+
A report is a markdown page of findings, with images, that a dispatch or
|
|
442
|
+
the helm files to keep.
|
|
443
|
+
|
|
444
|
+
- **Filing a worker report.** `lobstah report <id> done "<one-line note>"
|
|
445
|
+
--report <file.md> [--attach <file> ...]` (also `failed`). The page is
|
|
446
|
+
copied to the dispatch's state directory as `state/<id>/report.md`, beside
|
|
447
|
+
`attachments/`. `--attach` copies each file into that `attachments/`. An
|
|
448
|
+
image the page names by bare filename (``) resolves to
|
|
449
|
+
that directory. The note stays one line.
|
|
450
|
+
- **Filing a helm report.** `lobstah man file <file.md> [--attach <file> ...]
|
|
451
|
+
[--title <text>]`. It is stored under the helm's grounds,
|
|
452
|
+
`reports/<grounds>/<rid>/`, with the same layout, and the author is `helm`.
|
|
453
|
+
- **Title and author.** The title is `--title`, else the page's first `#`
|
|
454
|
+
heading, else the dispatch's brief title. The author is the trap name, or
|
|
455
|
+
`headless`, or `helm`. A file larger than `[limits].attachmentMaxBytes` is
|
|
456
|
+
refused, and nothing is filed.
|
|
457
|
+
- **Finding one.** `lobstah reports` lists every report, newest first: key,
|
|
458
|
+
title, author, the dispatch or helm grounds, when it was filed, and
|
|
459
|
+
whether it is acked. `lobstah status <id>` and `lobstah catch <id>` print
|
|
460
|
+
`report: <path>` when the dispatch has one.
|
|
461
|
+
- **In the glass.** The deck has a reports block after Landed: newest first,
|
|
462
|
+
unacked first, up to 8, then "+N more", which opens the Reports tab. A
|
|
463
|
+
report's card or row shows its title, then who filed it (a trap's name, a
|
|
464
|
+
headless dispatch's id, nothing for the helm), its age, and `acked`. A
|
|
465
|
+
dispatch's report renders as a
|
|
466
|
+
page at the top of its dispatch modal. A helm report opens in a modal of
|
|
467
|
+
its own; `#report/<key>` links to it. The page shows headings, lists,
|
|
468
|
+
tables, fenced code, links (in a new tab), bold, italics, and images.
|
|
469
|
+
Raw HTML in the markdown shows as text. The glass serves the markdown and
|
|
470
|
+
the images read-only, and an image only by basename from that report's
|
|
471
|
+
own attachments.
|
|
472
|
+
- **Images.** Every image in the glass (a decision card's, a report page's,
|
|
473
|
+
and an attachment's in the dispatch and trap modals) opens in an in-page
|
|
474
|
+
overlay: centered at up to 90% of the viewport over a dark backdrop, with
|
|
475
|
+
a link to open the original file. Escape, a click on the backdrop, or the
|
|
476
|
+
close button closes it. The glass serves a dispatch's and a trap's
|
|
477
|
+
attachment images read-only, by basename, from their own attachments
|
|
478
|
+
directories. Missing files, malformed names, and symlinks are not served.
|
|
479
|
+
- **Attention.** A filed report with no ack stands as the `report` attention
|
|
480
|
+
kind. Add `report` to `attentionKinds` to walk it. The desktop pet shows
|
|
481
|
+
the report's title; a pet click opens the item and acks it through
|
|
482
|
+
`lobstah attention ack <key> --by pet`. The pet's Acknowledge menu entry
|
|
483
|
+
acks without opening, and `lobstah attention ack <key>` acks a report
|
|
484
|
+
whether or not `report` is in `attentionKinds`. Opening a report in the
|
|
485
|
+
glass does not ack it. A report filed by a follow-up dispatch acks the report
|
|
486
|
+
of each dispatch before it in the chain.
|
|
487
|
+
- **Cull.** `lobstah cull` removes a dispatch's report with the rest of its
|
|
488
|
+
state. A helm report is culled when it is older than the retention window,
|
|
489
|
+
counted from when it was filed.
|
|
490
|
+
|
|
491
|
+
### Decisions
|
|
492
|
+
|
|
493
|
+
A decision is a question the helm puts to the human. Workers still ask in
|
|
494
|
+
prose (`report needs-decision "<note>"`). The helm decides first. When the
|
|
495
|
+
brief, the code, or its context gives the answer, it sends it with
|
|
496
|
+
`lobstah send <id> "<answer>"` and says what it decided in its next report.
|
|
497
|
+
When it lacks the context, or the choice is the human's (scope, product
|
|
498
|
+
behavior, money, releases, merges, anything outward-facing or hard to
|
|
499
|
+
undo), it frames a decision with `man ask`. The helm's own questions follow
|
|
500
|
+
the same rule.
|
|
501
|
+
|
|
502
|
+
- **Asking.** `lobstah man ask [<dispatch-id>] --title "<question>"
|
|
503
|
+
[--detail <file.md>] [--option "<label>"]... [--attach <file>]...` stores a
|
|
504
|
+
decision: its key (`decision:<rid>`), title, detail markdown (at most
|
|
505
|
+
64 KiB), 0 to 6 option labels, attachments, the dispatch it is about (none
|
|
506
|
+
for a question such as "cut 0.6.0?"), who asked (`helm`), and when. It is
|
|
507
|
+
stored in `~/.lobstah/decisions/<rid>/`: `decision.json`, `detail.md`, and
|
|
508
|
+
`attachments/`. The claimed helm alone may ask.
|
|
509
|
+
- **Standing.** A decision stands until it is answered or the helm withdraws
|
|
510
|
+
it with `lobstah man ask --withdraw <key>`, which removes its directory. A
|
|
511
|
+
newer `man ask` on the same dispatch replaces the older decision.
|
|
512
|
+
- **Attention.** A standing decision is attention of kind `decision`, in
|
|
513
|
+
the default `attentionKinds`. It makes the verdict `needs-attention`. A
|
|
514
|
+
worker's raw `needs-decision` or `blocked` stays a `question` until the
|
|
515
|
+
helm frames it. While a decision on the same dispatch, asked at or after
|
|
516
|
+
the question, is on disk, the `question` item is hidden: the decision
|
|
517
|
+
replaces it. Withdrawing the decision shows the question again.
|
|
518
|
+
- **In the glass.** The attention section of On deck is a list of
|
|
519
|
+
full-width decision cards, newest first. A card shows the title, the
|
|
520
|
+
detail rendered as markdown, the attachments (images inline), the
|
|
521
|
+
dispatch link and repo, and the age. The options are buttons. Below them
|
|
522
|
+
is an empty text box that grows with its content, and an attach control
|
|
523
|
+
for images and files (`.png .jpg .jpeg .gif .webp .pdf .txt .md .csv
|
|
524
|
+
.json .log .diff .patch .yaml .yml .toml .zip`, each at most
|
|
525
|
+
`[limits].attachmentMaxBytes`, at most 8). Click an option, write an
|
|
526
|
+
answer, attach files, or any mix; one **Send** submits it. With no options,
|
|
527
|
+
the text box is the whole answer. After sending, the card shows
|
|
528
|
+
`answered · <what was chosen>` and leaves on the next refresh. A raw
|
|
529
|
+
`question` the helm has not framed shows as a plain card with the
|
|
530
|
+
worker's note and the same text box. PR kinds stay in the PRs section.
|
|
531
|
+
`#decision/<key>` opens the deck at that card. Pasting with Cmd-V or
|
|
532
|
+
Ctrl-V in the text box adds each image on the clipboard as an attachment
|
|
533
|
+
named `pasted-<time>.png` (the extension follows the image format), with
|
|
534
|
+
the same size, type, and count checks as a picked file; pasted text stays
|
|
535
|
+
text.
|
|
536
|
+
- **The answer is a request.** An answer is a glass request of kind
|
|
537
|
+
`decision-answer`, stored as `~/.lobstah/requests/<id>.json` with its
|
|
538
|
+
files in `~/.lobstah/requests/<id>/`. The payload is the decision `key`,
|
|
539
|
+
`title`, `dispatch`, `lane`, `repo`, the `option`, the `text`, and the
|
|
540
|
+
stored `attachments`. `answer.json` in the decision's directory marks it
|
|
541
|
+
answered and names the request.
|
|
542
|
+
- **The POST.** Send is a same-origin POST to `/requests` with the page's
|
|
543
|
+
token (header `x-lobstah-token`), the guard the **↗ open** button uses.
|
|
544
|
+
The body is `{ "kind": "decision-answer", "payload": { "key", "option",
|
|
545
|
+
"text", "files": [{ "name", "data" }] } }`, with file data in base64. The
|
|
546
|
+
server checks the kind, the key, that the option is one of the decision's
|
|
547
|
+
labels, the text length (at most 20000 characters), and each file's size
|
|
548
|
+
and type (an image must also start with its format's signature). It
|
|
549
|
+
writes the request and runs nothing. Answering a raw question's key
|
|
550
|
+
(`<lane>:<id>`) first stores it as a decision asked by `worker`.
|
|
551
|
+
- **The event.** Writing the request posts a `decision-answer` notice. It
|
|
552
|
+
wakes the helm's `man wait` (and the Stop-hook park) once, as a
|
|
553
|
+
`decision-answer` event: the request `id`, `key`, `dispatch`, `title`,
|
|
554
|
+
`option`, `text`, `attachments` (the stored paths), `from`, and `at`. The
|
|
555
|
+
helm acts on it, usually with `lobstah send <dispatch> "<instruction>"`.
|
|
556
|
+
Answering does not message the worker.
|
|
557
|
+
- **From the terminal.** `lobstah man answer <key> [--option <label>]
|
|
558
|
+
[--text <text>] [--attach <file>]...` answers the same way, with the same
|
|
559
|
+
checks.
|
|
560
|
+
- **The pet.** The desktop pet shows a decision as its title only. A click
|
|
561
|
+
opens the glass at the card and acks the item for the pet
|
|
562
|
+
(`lobstah attention ack <key> --by pet`). The card stays in the glass until
|
|
563
|
+
the decision is answered or withdrawn.
|
|
564
|
+
- **Cull.** `lobstah cull` removes a decision once its answer is older than
|
|
565
|
+
the retention window, with the `decision-answer` request and its files. A
|
|
566
|
+
standing decision is never culled.
|
|
567
|
+
|
|
268
568
|
### The periodic report
|
|
269
569
|
|
|
270
570
|
`lobstah man tend` is the full picture on demand; `lobstah man report` is the
|
|
271
571
|
**delta** since the last acknowledged report — catches landed (with their
|
|
272
572
|
notes and PRs), attention newly arisen, what still waits, and the fleet
|
|
273
|
-
verdict. It advances a "reported through" cursor when it prints
|
|
274
|
-
|
|
573
|
+
verdict. It advances a "reported through" cursor when it prints, so nothing
|
|
574
|
+
is ever reported twice, and it says
|
|
275
575
|
`no change` when the delta is empty rather than re-dumping state. Standing
|
|
276
576
|
unanswered questions appear under `still-waiting` without counting as
|
|
277
577
|
change — reminders (`remindSecs`) own re-firing those.
|
|
278
578
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
579
|
+
A catch is reported once the helm's `man wait` watcher delivers its event or
|
|
580
|
+
`man report` prints it. The glass's `unreported` badge means no helm received
|
|
581
|
+
that catch. A `man wait` timeout and `man wait --peek` only peek at the delta;
|
|
582
|
+
a digest lost with a dead background task re-surfaces on the next timeout.
|
|
583
|
+
The Stop-hook's standing-attention reminder does not mark a catch reported.
|
|
284
584
|
|
|
285
585
|
Every carrier shares the cursor (per grounds, for a helm):
|
|
286
586
|
|
|
287
|
-
- **The wait loop.** A `man wait`
|
|
288
|
-
|
|
289
|
-
|
|
587
|
+
- **The wait loop.** A delivered `man wait` event (exit 0) advances the helm's
|
|
588
|
+
cursor through the event time. A timeout (exit 3) prints the delta when
|
|
589
|
+
something changed without advancing the cursor — see the loop idiom below.
|
|
290
590
|
- **The blocking park.** A helm session's Stop-hook park delivers the digest as a wake
|
|
291
591
|
at `[helm].reportSecs` cadence — including the landed-then-idle case, where
|
|
292
592
|
the last catches finish and nothing is left in flight to wake for.
|
|
@@ -357,6 +657,12 @@ before the worker reads it; `man tend` then shows the dispatch as
|
|
|
357
657
|
A newer `needs-decision` from the worker stands again. Set `remindSecs = 0`
|
|
358
658
|
for pure at-most-once.
|
|
359
659
|
|
|
660
|
+
A send still waiting on its reply stands in the `man haul` block as
|
|
661
|
+
`sent · <id> · <first line> · <age>`. It is listed once, then every
|
|
662
|
+
`remindSecs` until the worker's next note answers it. `man tend` shows it on
|
|
663
|
+
the dispatch as `awaiting reply · <age>`, and the glass dispatch modal shows
|
|
664
|
+
it under the inbox.
|
|
665
|
+
|
|
360
666
|
**Acknowledging, display-only.** Clicking a desktop pet opens its target and
|
|
361
667
|
runs `lobstah attention ack <item-key> --by pet`, so that pet stops walking
|
|
362
668
|
the item until its state changes (a new status entry, head, failed check, or
|
|
@@ -486,6 +792,8 @@ lobstah soak --wait # hookless sessions: listen in the foreground
|
|
|
486
792
|
# (re-runs need no flags — identity is the
|
|
487
793
|
# worktree, else the session id); exit 3 =
|
|
488
794
|
# quiet, run it again
|
|
795
|
+
lobstah soak --link <url> # store this session's link for the glass's ↗ open button
|
|
796
|
+
lobstah focus <trap> # focus a live trap from the CLI
|
|
489
797
|
lobstah stow # sign off; an open catch requeues, unread
|
|
490
798
|
# messages bounce back to the helm; removes
|
|
491
799
|
# the worktree when soak created it
|
|
@@ -493,6 +801,14 @@ lobstah stow --keep # sign off and keep the worktree
|
|
|
493
801
|
lobstah soak --name amber-gull # choose or change this trap's two-word name
|
|
494
802
|
```
|
|
495
803
|
|
|
804
|
+
`--link` accepts a Claude desktop session URL under `claude://claude.ai/`,
|
|
805
|
+
a VS Code extension URL of the form
|
|
806
|
+
`vscode://anthropic.claude-code/open?session=<id>`, or a Codex task URL of
|
|
807
|
+
the form `codex://threads/<task-id>`. It rejects all other schemes and
|
|
808
|
+
malformed links. The glass checks a stored link again before rendering it.
|
|
809
|
+
`lobstah focus <trap>` accepts the trap id with or without `wt:` and reports
|
|
810
|
+
the focus step or why it could not focus. It does not revive a signed-off trap.
|
|
811
|
+
|
|
496
812
|
**Soak can create the worktree.** From a repo's primary checkout, or with
|
|
497
813
|
`--repo <key>` from outside any configured repo, soak creates a linked
|
|
498
814
|
worktree the same way a dispatch does. It fetches trunk, adds
|
|
@@ -523,8 +839,9 @@ worktree's HEAD commit and branch in evidence.
|
|
|
523
839
|
`stow` removes a worktree only when soak created it and nothing in it exists
|
|
524
840
|
elsewhere. It keeps the worktree and prints `worktree: kept` and a `reason:`
|
|
525
841
|
when soak did not create the worktree, or when the worktree has uncommitted
|
|
526
|
-
changes, untracked files that are not ignored,
|
|
527
|
-
|
|
842
|
+
changes, untracked files that are not ignored, commits its upstream lacks,
|
|
843
|
+
or no upstream. Ignored files do not block removal. `stow --force` explicitly
|
|
844
|
+
allows removal of unsaved checkout files; `--keep` still keeps the checkout.
|
|
528
845
|
Stow runs the removal from the primary checkout, so it works from inside
|
|
529
846
|
the worktree. On removal it prints `worktree: removed`, `path:`, and
|
|
530
847
|
`returnTo: <primary checkout>`, with a help line `cd <primary>`. It
|
|
@@ -533,6 +850,8 @@ upstream: on some remote branch);
|
|
|
533
850
|
otherwise it prints `branchKept: <branch> (<reason>)`. A deleted branch
|
|
534
851
|
prints as `branchDeleted:`. `stow --wt <id>` follows the same rules. The
|
|
535
852
|
SessionEnd hook (`lobstah stow --quiet`) signs off and keeps the worktree.
|
|
853
|
+
Releasing a claim whose last report is `done` or `failed` moves it to `done/`;
|
|
854
|
+
only an unfinished catch requeues (a cancelled catch finalizes as failed).
|
|
536
855
|
|
|
537
856
|
**Identity is the worktree.** Sign-on anchors a short trap id and two-word
|
|
538
857
|
name in `.lobstah-trap` and prints both, such as `amber-gull (wt:c32a245d)`; the address
|
|
@@ -545,6 +864,15 @@ same live trap in `dispatch --for`, `send`, and `stow --wt`. Unknown names
|
|
|
545
864
|
list known names and never turn addressed bait into headless work. The id
|
|
546
865
|
remains the key in dispatch and claim records.
|
|
547
866
|
|
|
867
|
+
A dispatch shows its trap by name wherever it names the trap that claimed
|
|
868
|
+
it, ran it, or is addressed to it, including notes such as
|
|
869
|
+
`claimed by crisp-heron`. The name comes from the live registration, else
|
|
870
|
+
from the name registry, so a signed-off trap keeps its name. A trap with no
|
|
871
|
+
known name shows as `wt:<id>`. `lobstah status <id>` and `lobstah catch <id>`
|
|
872
|
+
print `trap: crisp-heron (wt:68c5da5f)` and write notes the same way.
|
|
873
|
+
`man tend`'s tables print the name alone. In the glass, the name carries
|
|
874
|
+
`wt:<id>` as its hover text, and a click opens that trap's modal.
|
|
875
|
+
|
|
548
876
|
The session id (from the plugin's session-start brief) lives inside the
|
|
549
877
|
registration as the liveness principal. The harness (claude or codex) is
|
|
550
878
|
inferred — from `CLAUDE*` / `CODEX*` in the environment, and when both are
|
|
@@ -574,14 +902,118 @@ resolves the trap from the working directory, else from the session id. A
|
|
|
574
902
|
trap that works for an hour without reporting is not swept while it beats.
|
|
575
903
|
`[soak].beat = false` turns the hook off.
|
|
576
904
|
|
|
905
|
+
`lobstah soak` prints `title: <trap name>` at sign-on. When `soak --wait`
|
|
906
|
+
delivers work, it prints `title: <name> · <short first brief line>`.
|
|
907
|
+
`report done` and `report failed` print `title: <trap name>` again. The
|
|
908
|
+
brief text has control and terminal escape sequences removed and is capped
|
|
909
|
+
at 40 characters. A trap skill sets an available session-title tool to each
|
|
910
|
+
printed title, without retrying a refusal. Stow leaves the current title
|
|
911
|
+
in place.
|
|
912
|
+
|
|
577
913
|
Liveness has two failure shapes with two remedies: a registration that
|
|
578
914
|
parked before and went quiet (no park, report, or beat) past `[soak].ttlSecs` is a **ghost trap** —
|
|
579
|
-
swept
|
|
915
|
+
swept and noticed. An unfinished catch requeues; a `done` or `failed` catch
|
|
916
|
+
finalizes in `done/`, never becoming orphaned bait. A soak-created worktree
|
|
917
|
+
is removed only when Git verifies a clean checkout with no commits absent
|
|
918
|
+
from its upstream. Dirty, unpushed, no-upstream, or unreadable checkouts stay.
|
|
919
|
+
The ghost notice includes the path, branch, modified-file count and
|
|
920
|
+
unpushed-commit count (or `unknown` when inspection fails). Other worktrees
|
|
921
|
+
stay in place. After the daemon's own tick gap exceeds `[soak].ttlSecs`,
|
|
922
|
+
it grants a full TTL after resume before sweeping any traps, allowing
|
|
923
|
+
sessions to renew their heartbeats. One that **never parked** is a **defective
|
|
580
924
|
enlistment** — noticed with its diagnosis (usually a missing Stop hook →
|
|
581
925
|
`soak --wait`) and left standing so the address keeps protecting its work.
|
|
582
926
|
Nobody is conscripted: only a worktree whose session ran `soak` ever
|
|
583
927
|
receives work.
|
|
584
928
|
|
|
929
|
+
### Reserving a trap before its session starts
|
|
930
|
+
|
|
931
|
+
```bash
|
|
932
|
+
lobstah trap reserve --repo <key> # reserve a trap; prints its name, id, and a one-time ticket
|
|
933
|
+
[--harness claude|codex] # print only that harness's start command
|
|
934
|
+
[--name amber-gull] # choose the name
|
|
935
|
+
[--deadline 180] # seconds the session has to sign on (default 180)
|
|
936
|
+
lobstah soak --ticket <ticket> # in the new session: sign on as the reserved trap
|
|
937
|
+
lobstah stow --wt amber-gull # withdraw a reservation no session has redeemed
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
`trap reserve` picks the two-word name and the `wt:` id before any session
|
|
941
|
+
exists and writes a **starting** reservation (`soaking/<id>.starting`) with a
|
|
942
|
+
deadline. `dispatch --for <name>` works on it at once: the work waits, as it
|
|
943
|
+
does for any addressed trap. `man tend` and the glass show the trap as
|
|
944
|
+
`starting`.
|
|
945
|
+
|
|
946
|
+
The output holds a one-time ticket and the command that starts the session in
|
|
947
|
+
the repo's primary checkout:
|
|
948
|
+
|
|
949
|
+
```bash
|
|
950
|
+
cd <repo> && CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 claude "/lobstah:soak --ticket <ticket>"
|
|
951
|
+
cd <repo> && codex '$lobstah:trap soak --ticket <ticket>'
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Nothing starts the session for you: a person runs the command. The session's
|
|
955
|
+
soak redeems the ticket, from `--ticket` or from the `LOBSTAH_TRAP_TICKET`
|
|
956
|
+
environment variable. It creates a worktree named after the reserved id,
|
|
957
|
+
signs on under the reserved name and id, and deletes the reservation. The
|
|
958
|
+
ticket then redeems nothing. A spent ticket left in `LOBSTAH_TRAP_TICKET` is
|
|
959
|
+
ignored; a spent `--ticket` is refused, except in the session that redeemed
|
|
960
|
+
it. A session that already mans a trap cannot redeem a ticket.
|
|
961
|
+
|
|
962
|
+
A reservation still unredeemed at its deadline **fails**: the daemon posts one
|
|
963
|
+
`trap-start-failed` notice, and the glass shows the trap as `start failed`
|
|
964
|
+
with the reason. Work addressed to it stays queued. The ticket still redeems
|
|
965
|
+
after the deadline. `lobstah stow --wt <name>` withdraws the reservation; its
|
|
966
|
+
addressed work is then orphaned bait and the helm gets a `bait-orphaned`
|
|
967
|
+
notice.
|
|
968
|
+
|
|
969
|
+
### Asking for a trap from the glass
|
|
970
|
+
|
|
971
|
+
The Traps tab and the deck's traps block have a **+ New trap** button. It
|
|
972
|
+
opens a small form: a repo (from the configured repo keys) and a harness
|
|
973
|
+
(`claude` or `codex`). Submitting it files a **trap request**: the glass POSTs
|
|
974
|
+
`{ kind: "trap-request", payload: { repo, harness } }` to `/requests` with
|
|
975
|
+
the same same-origin and page token guard as the open-window button. The
|
|
976
|
+
server checks the repo and harness, writes `~/.lobstah/requests/<id>.json`
|
|
977
|
+
(kind `trap-request`), and runs nothing. `/requests` refuses a body larger
|
|
978
|
+
than eight attachments (`[limits].attachmentMaxBytes` each, as base64) plus
|
|
979
|
+
room for text, with 413. After it reads the kind, it refuses a `trap-request`
|
|
980
|
+
larger than 4 KB with 413.
|
|
981
|
+
|
|
982
|
+
A request wakes the helm's `man wait` (and the Stop-hook park) as a
|
|
983
|
+
`trap-request` event with the request's id, repo, and harness. An open
|
|
984
|
+
request also wakes a helm that signs on later. The glass shows it at once as a
|
|
985
|
+
greyed card: `requested · <repo> · <harness> · waiting for the helm`, or
|
|
986
|
+
`waiting for a helm` when no helm is signed on.
|
|
987
|
+
|
|
988
|
+
```bash
|
|
989
|
+
lobstah trap requests # open trap requests
|
|
990
|
+
lobstah trap reserve --request <id> # reserve what the request asks for, and close it
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
`trap reserve --request <id>` takes the repo and harness from the request,
|
|
994
|
+
records the request on the reservation, and closes the request. The card
|
|
995
|
+
becomes the starting card, then the live trap when the session signs on.
|
|
996
|
+
|
|
997
|
+
Every starting card shows the start command `trap reserve` prints, with a copy
|
|
998
|
+
button: paste it into a terminal to start the session by hand. The ticket in
|
|
999
|
+
it is kept in `soaking/<id>.ticket` (mode 0600) until the reservation is
|
|
1000
|
+
redeemed or withdrawn. The glass sends it only to a page on this machine's
|
|
1001
|
+
own glass address, only on the starting card, and no notice or log carries
|
|
1002
|
+
it.
|
|
1003
|
+
|
|
1004
|
+
### The terminal tab name
|
|
1005
|
+
|
|
1006
|
+
At sign-on, soak names the session's terminal tab after the trap. It finds the
|
|
1007
|
+
tab by the tty recorded in the registration's window: a Terminal.app tab gets
|
|
1008
|
+
the name as its custom title, an iTerm2 session gets it as its session name.
|
|
1009
|
+
Other terminals are left alone. `lobstah stow` clears the name.
|
|
1010
|
+
`LOBSTAH_TERMINAL_TITLE=0` turns naming off.
|
|
1011
|
+
|
|
1012
|
+
Claude Code writes its own title to the tab, and in Terminal.app that title
|
|
1013
|
+
replaces the custom title. `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` on the
|
|
1014
|
+
command that starts Claude Code stops that for that one process; the start
|
|
1015
|
+
command `trap reserve` prints sets it. Codex also sets the terminal title.
|
|
1016
|
+
|
|
585
1017
|
### Culling and disk space
|
|
586
1018
|
|
|
587
1019
|
Worktrees are 1 to 8 GB each. `lobstah cull` sweeps what is finished: `done/`
|
|
@@ -590,9 +1022,9 @@ whose dispatch is finished or gone, stale state files, merged or closed PR
|
|
|
590
1022
|
records, and orphaned acks. It never touches queued or active work, and
|
|
591
1023
|
`git worktree remove` keeps each dispatch's branch. A worktree that soak
|
|
592
1024
|
created counts as in use while a trap registration anchors it. After that,
|
|
593
|
-
the cull and the daemon's retention and free-space culls
|
|
594
|
-
|
|
595
|
-
|
|
1025
|
+
the cull and the daemon's retention and free-space culls can remove it only
|
|
1026
|
+
when the same clean-and-pushed safety check passes; unsaved soak checkouts
|
|
1027
|
+
remain protected even without a registration. A worktree that follow-ups
|
|
596
1028
|
reused is one worktree shared by the chain: it stays while any dispatch in
|
|
597
1029
|
the chain is queued or active, and it ages from the newest dispatch that
|
|
598
1030
|
used it.
|