lobstah 0.1.1 → 0.2.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/docs/design.md ADDED
@@ -0,0 +1,669 @@
1
+ # Lobstah — design
2
+
3
+ **Local executor for coding agents.**
4
+
5
+ Lobstah takes a dispatch descriptor and a brief, allocates an isolated worktree,
6
+ runs a coding agent, supervises it until it finishes or dies, and writes status
7
+ and evidence to disk.
8
+
9
+ It has no network interface, no credentials, and no knowledge of any tracker.
10
+
11
+ ---
12
+
13
+ ## Problem
14
+
15
+ Teams delegating work to coding agents have two bad options.
16
+
17
+ **Cloud sandboxes** — the hosted coding-agent services — cannot
18
+ reach a private toolchain, unpushed branches, local services, or credentials that
19
+ never leave a laptop. Work that needs the developer's actual environment cannot
20
+ run there.
21
+
22
+ **Running agents locally by hand** works, and nobody knows what is happening. A
23
+ Claude Code session in a terminal gives no answer to "is it still working, is it
24
+ stuck, or did it die." The failure mode is a session wedged for forty minutes on
25
+ a question nobody saw.
26
+
27
+ Lobstah is the supervision layer for the second option.
28
+
29
+ ---
30
+
31
+ ## Goals
32
+
33
+ - Run coding agents on a machine the developer controls
34
+ - Report liveness accurately, without screen scraping
35
+ - Distinguish a dead process from a wedged one and treat them differently
36
+ - Isolate concurrent work so parallel tasks cannot collide
37
+ - Recover from crashes without human intervention, within bounds
38
+ - Work standalone, with no knowledge of any particular dispatcher
39
+
40
+ ## Non-goals
41
+
42
+ Lobstah must stay uninteresting past its job. It has:
43
+
44
+ - No network listener, no outbound HTTP, no OAuth
45
+ - No Linear, Slack, GitHub, or tracker integration
46
+ - No verdicts, review UI, or artifact rendering
47
+ - No decomposition, claims, or intent model
48
+ - No merge decisions
49
+ - No hosted service
50
+
51
+ The moment Lobstah grows something that makes a team feel covered, it stops
52
+ being a component and starts competing with the thing it feeds.
53
+
54
+ ---
55
+
56
+ ## Architecture
57
+
58
+ TypeScript daemon, one repository, packages factored so a new harness does not
59
+ touch supervision.
60
+
61
+ ```
62
+ lobstah/
63
+ packages/
64
+ core/ queue contract, state machine, types
65
+ supervisor/ process liveness, wedge detection, restart ladder
66
+ worktree/ allocation, isolation, cleanup
67
+ runner/ per-dispatch child process; drives an adapter
68
+ adapters/
69
+ claude/ @anthropic-ai/claude-agent-sdk
70
+ codex/ @openai/codex-sdk
71
+ apps/
72
+ cli/ lobstah dispatch | status | cancel | daemon
73
+ node/ OpenClaw node plugin
74
+ ```
75
+
76
+ **Why not shell.** A bash fleet is the right shape when the supervisor is an
77
+ LLM — the scripts are a tool surface for a model. Lobstah's consumer is a program
78
+ reading files, so the CLI wrapper earns nothing, and the delicate parts —
79
+ generation-token validation under a lock, atomic sequence allocation, three-source
80
+ state reconciliation — are straightforward in a typed language and fragile in
81
+ shell.
82
+
83
+ ### The two-process split
84
+
85
+ The daemon does not run agents in-process. Every SDK in this category spawns its
86
+ harness CLI as a subprocess of the calling process, so an in-process session dies
87
+ with the daemon and is invisible to anything else.
88
+
89
+ ```
90
+ daemon ──spawn──> runner (one per dispatch) ──SDK──> harness CLI
91
+ │ │
92
+ │ └── writes state/<uuid>.*
93
+ └── supervises runner by pid, reconciles from state files
94
+ ```
95
+
96
+ - Daemon crash leaves runners alive and state files intact. It reconciles on
97
+ restart.
98
+ - Runner crash is visible through ordinary process supervision, and the state
99
+ files record how far it got.
100
+ - The SDK improves what happens inside the runner. It is not the durability
101
+ layer.
102
+
103
+ **Lobstah persists no conversation state.** Every harness already writes its own
104
+ transcript — Claude Code under `~/.claude/projects/`, Codex per thread. State
105
+ files track the dispatch: work item, worktree, verb, evidence. On resume, fork
106
+ the harness's own session rather than replaying a transcript into a new one.
107
+
108
+ ### Adapters
109
+
110
+ Day 1 is Claude Code and Codex. Two implementations is the minimum that
111
+ validates the interface rather than shaping it around one harness, and between
112
+ them they cover most of the installed base.
113
+
114
+ Both expose the same conceptual API, which is what makes the abstraction real:
115
+
116
+ | | Claude Agent SDK | Codex SDK |
117
+ |---|---|---|
118
+ | Start | `query()` | `codex.startThread()` |
119
+ | Turn | async message generator | `thread.run()` |
120
+ | Stream | messages + hooks | `thread.runStreamed()` |
121
+ | Continue | resume by session id | `run()` again on the thread |
122
+ | Providers | Anthropic, Bedrock, Vertex, Foundry | OpenAI |
123
+
124
+ The adapter interface normalizes to: `start`, `run`, `resume`, `cancel`, and a
125
+ typed event stream carrying turn boundaries and tool-call boundaries.
126
+
127
+ **Tool-call granularity differs and both are usable.** Codex emits `item.started`
128
+ and `item.completed` around each `command_execution` or `mcp_tool_call`, so a
129
+ hanging call is an open interval you can name. Claude's `PostToolUse` fires after
130
+ completion, so a hang shows as silence. The adapter maps both to "last tool
131
+ activity at T," and the wedge threshold stays global.
132
+
133
+ Claude has the richer intervention surface — `PreToolUse` interception,
134
+ `SubagentStart` and `SubagentStop`, and 12+ hooks against Codex's six event
135
+ types. Anything relying on interception is Claude-only and must degrade rather
136
+ than fail on Codex.
137
+
138
+ Normalize cancellation explicitly. The Codex SDK's own ports document divergence
139
+ here — the Go port sends SIGTERM with a 2s grace then SIGKILL where the
140
+ TypeScript SDK sends a single SIGTERM. Differences like that leak into restart
141
+ behavior if the adapter layer does not absorb them.
142
+
143
+ OpenCode is the Day 2 candidate. It has a TypeScript SDK, a client/server
144
+ architecture that may be easier to supervise than subprocess spawning, and 75+
145
+ providers including local models, which makes agent-agnosticism true rather than
146
+ aspirational.
147
+
148
+ ### Auth boundary
149
+
150
+ Anthropic does not permit third-party developers to offer claude.ai Pro or Max
151
+ login or subscription rate limits for products built on the Agent SDK.
152
+ Subscription usage of the SDK and `claude -p` is metered against the signed-in
153
+ plan's limits.
154
+
155
+ So Lobstah never handles a harness login. The user authenticates their own CLI
156
+ on their own machine; Lobstah invokes the authenticated binary and nothing else.
157
+ No reading, persisting, refreshing, or forwarding of harness tokens — a
158
+ non-secret route marker at most, with the harness owning its token lifecycle.
159
+
160
+ For shared automation the operator supplies an API key through repo config, which
161
+ is per-machine rather than brokered.
162
+
163
+ ---
164
+
165
+ ## OpenClaw node plugin
166
+
167
+ OpenClaw already solves the undifferentiated parts of remote dispatch. It has
168
+ a WebSocket control plane. Nodes declare `role: node` with explicit caps and
169
+ commands. Device pairing needs identity plus approval plus token. Auth fails
170
+ closed. Each node has an exec allowlist. Transport is Tailscale or SSH, not
171
+ public exposure.
172
+
173
+ Lobstah ships a node plugin that advertises a run capability and translates
174
+ inbound commands into queue descriptors. The core does not know OpenClaw exists.
175
+
176
+ ```
177
+ OpenClaw Gateway ──WS──> lobstah node plugin ──writes──> queue/
178
+ <── <──reads── state/
179
+ ```
180
+
181
+ What the plugin adds over OpenClaw's own Claude session continuation, which is
182
+ one-shot, rejects attachments, and has no workspace isolation:
183
+
184
+ - Per-work-item worktree allocation branched from trunk
185
+ - Dead-versus-wedged classification and the restart ladder
186
+ - Dispatch queue semantics with a concurrency ceiling
187
+ - Work-item-shaped evidence collection
188
+
189
+ Two independent integration surfaces driving one core is the test of whether
190
+ the queue contract is a real interface. If both drive it without the core knowing
191
+ which, it holds.
192
+
193
+ Depending on any gateway means inheriting its release cadence and its security
194
+ surface. The plugin is therefore additive. The file queue and CLI remain the primary path, and
195
+ nothing in `core` may import from `apps/node`.
196
+
197
+ ---
198
+
199
+ ## Queue contract
200
+
201
+ The dispatch surface is a directory. No port, no protocol, no authentication.
202
+
203
+ ```
204
+ ~/.lobstah/
205
+ queue/ <uuid>.json pending descriptors
206
+ active/ <uuid>/ claimed, in flight
207
+ done/ <uuid>/
208
+ state/ <uuid>.status append-only, six verbs
209
+ <uuid>.evidence branch, commits, PRs, CI refs
210
+ <uuid>.events hook telemetry
211
+ inbox/ <uuid>/NNN.msg instructions to a running agent
212
+ <uuid>/handled/ acknowledgement by rename
213
+ chores/ queue/ active/ done/ state/ second lane: internal dispatches
214
+ executor.json capabilities + heartbeat
215
+ ```
216
+
217
+ ### Descriptor
218
+
219
+ ```json
220
+ {
221
+ "id": "6f3a...",
222
+ "repo": "myapp",
223
+ "brief": "...",
224
+
225
+ "harness": "claude",
226
+ "model": "opus",
227
+ "effort": "high",
228
+ "limits": { "maxTurns": 200, "maxBudgetUsd": 5, "wallClockSecs": 3600 },
229
+ "flags": ["--add-dir", "../shared"],
230
+ "env": { "NODE_ENV": "test" },
231
+ "followUp": "9c41..."
232
+ }
233
+ ```
234
+
235
+ `id`, `repo`, and `brief` are required. Everything below the break is optional
236
+ and resolves through a precedence chain:
237
+
238
+ ```
239
+ descriptor > repo config > global config > adapter default
240
+ ```
241
+
242
+ **Structured fields are cross-harness concepts.** `model`, `effort`, and `limits`
243
+ mean something in every harness and translate differently in each. Claude takes a
244
+ thinking budget, Codex takes a reasoning effort level, others take neither. The
245
+ adapter owns the translation, and an unsupported value is dropped with a warning
246
+ rather than failing the dispatch.
247
+
248
+ **`flags` is the escape hatch**, appended verbatim to the harness invocation. It
249
+ couples the descriptor to one harness, so it should be rare. A descriptor using
250
+ only structured fields routes to any machine; one using `flags` routes only to
251
+ machines running that harness.
252
+
253
+ **`env` is per-dispatch environment**, merged over the repo's own environment.
254
+
255
+ **`followUp` forks an earlier dispatch's harness session** instead of starting
256
+ cold — the restart ladder's first rung, exposed to callers. The runner resumes
257
+ by the prior dispatch's session identity (Claude by session id, Codex by
258
+ thread id) and the original transcript survives untouched. Use it when the
259
+ follow-up is about choices that session made, as review feedback is. Skip it
260
+ when the follow-up is about the world changing after the session exited — a
261
+ rebase conflict is about commits the session never saw, so its transcript is
262
+ replay cost without signal.
263
+
264
+ **`id` is the only correlation handle.** Lobstah uses it as a directory name and
265
+ a status filename. Whoever dispatched holds the mapping from UUID to work item,
266
+ claims, and tracker — the dispatcher holds that table. Standalone, the human
267
+ knows what they dispatched. Nothing on device needs to reconstruct it, and an on-device
268
+ model to hold a mapping is more machinery than a lookup.
269
+
270
+ **`repo` must be structured, not prose.** Worktree allocation happens before the
271
+ agent starts, so the repo cannot be something the agent reads out of the brief
272
+ later. Parsing it from prose would put an LLM in the allocation path, which is
273
+ the failure it exists to avoid.
274
+
275
+ It is an opaque key, not a URL or a path. The key names a **workspace
276
+ definition** in local config: a git repository plus the execution context around
277
+ it — trunk branch, setup commands, environment, harness defaults. The dispatcher
278
+ names the key; resolution is local.
279
+
280
+ That keeps the descriptor free of machine-specific detail and lets the same
281
+ descriptor route to any machine advertising the key. It also allows one git
282
+ repository to back several keys — `myapp` and `myapp-perf` pointing at the same
283
+ checkout with different setup and limits.
284
+
285
+ If a key carries an `origin`, Lobstah clones on first use. Without one, an
286
+ unresolvable key fails the dispatch immediately rather than at agent start.
287
+
288
+ ### Claiming
289
+
290
+ Atomic rename from `queue/` to `active/`. That is what makes concurrent writers
291
+ safe without a lock.
292
+
293
+ **Lobstah decides how many to claim.** The queue holds descriptors and has no
294
+ concept of capacity. Concurrency limits live in Lobstah's config, so a writer
295
+ draining a backlog into the directory cannot over-fill the machine.
296
+
297
+ ### Watching
298
+
299
+ Watch as an optimization, poll as the guarantee. `fsevents` and `inotify` both
300
+ drop events under load and across network mounts. A directory scan every few
301
+ seconds is the contract; the watcher only reduces latency.
302
+
303
+ ### Cancellation
304
+
305
+ A `cancel` file in `active/<uuid>/`, checked by the supervisor loop between
306
+ polls. Latency equals the loop interval, which is acceptable for a task measured
307
+ in minutes.
308
+
309
+ ### Capabilities
310
+
311
+ `executor.json` advertises what this machine can serve and when it was last
312
+ alive:
313
+
314
+ ```json
315
+ {
316
+ "machineId": "chris-mbp",
317
+ "repos": ["myapp", "lobstah"],
318
+ "harnesses": ["claude", "codex"],
319
+ "maxConcurrent": 2,
320
+ "version": "0.4.1",
321
+ "heartbeat": "2026-09-01T14:22:03Z"
322
+ }
323
+ ```
324
+
325
+ A dispatcher reads this to route. A stale heartbeat is how it knows the machine
326
+ is offline. Lobstah writes it and never reads anything back.
327
+
328
+ ### The chore lane
329
+
330
+ `chores/` mirrors the primary lane — same descriptor schema, same claiming,
331
+ same supervision, its own `queue/`, `active/`, `done/`, and `state/`.
332
+
333
+ A chore is an agent run the system originates for its own operation —
334
+ judgment applied to mechanics, with no human request behind it. The lane is
335
+ bounded on both sides. Anything deterministic never becomes a dispatch at all
336
+ — that is daemon or caller code. Anything a human asked for, or that changes
337
+ what a human will review beyond mechanics, is work in the primary lane. A rebase to unblock a merge is the founding case; restacking a stack's
338
+ descendants after a squash-merge is the same shape. Chores have their own
339
+ concurrency ceiling (default 1; `maxConcurrent` governs the primary lane
340
+ only), are hidden from `ls` and `status` unless asked for, and age out of
341
+ `done/` on a short retention. The daemon treats the two lanes identically past
342
+ admission; the split is queue hygiene, not a second contract.
343
+
344
+ ### Inbox delivery
345
+
346
+ Writing to `inbox/<uuid>/` queues a message. Delivery is a separate question, and
347
+ headless execution shapes the answer.
348
+
349
+ A multiplexer-based fleet can type a doorbell into a tmux pane because its
350
+ workers are interactive TUIs sitting at a prompt. A Lobstah agent runs as `claude -p` with stdout
351
+ redirected, so there is no composer to type into and no prompt to interrupt. The
352
+ process runs many internal turns and exits once.
353
+
354
+ Three delivery points, in ascending cost:
355
+
356
+ **Between turns.** The runner drains the inbox into the next `run()` on the same
357
+ thread. Deterministic — guaranteed delivered and guaranteed read. Latency is the
358
+ remainder of the current turn. Works identically on both adapters, so this is the
359
+ default.
360
+
361
+ **Mid-turn pull, through the CLI.** The agent runs `lobstah inbox <id>` at its
362
+ own checkpoints, instructed by the injected contract. One transport for every
363
+ harness, and cheaper than MCP tools — tool schemas ride in the context every
364
+ turn, a CLI call costs the command string. It depends on the agent choosing
365
+ to look.
366
+
367
+ **Mid-turn push, through hooks.** Claude's `PreToolUse` can return additional
368
+ context, injecting the message before the next tool call. No agent cooperation
369
+ and one tool call of latency. Claude-only — Codex exposes events without
370
+ interception — so it must degrade to pull rather than fail.
371
+
372
+ Start with between-runs delivery. Mid-run steering matters less for a headless
373
+ fleet, because a headless run that needs redirecting is usually better
374
+ cancelled and re-dispatched with a corrected brief.
375
+
376
+ Acknowledgement stays the same regardless: a move into `handled/`, which is a
377
+ side effect that cannot be faked.
378
+
379
+ ---
380
+
381
+ ## CLI
382
+
383
+ The CLI never talks to the daemon. Writes are files; reads are files. That is the
384
+ main practical benefit of the directory contract.
385
+
386
+ | Command | Action |
387
+ |---|---|
388
+ | `lobstah dispatch --repo myapp --brief ./b.md` | Writes a descriptor to `queue/` |
389
+ | `lobstah status [uuid]` | Reads and reconciles from `state/` |
390
+ | `lobstah logs <uuid> [--follow]` | Tails `state/<uuid>.events` |
391
+ | `lobstah send <uuid> "<msg>"` | Writes an inbox record |
392
+ | `lobstah cancel <uuid>` | Writes a cancel marker to `active/<uuid>/` |
393
+ | `lobstah ls` | Lists queue, active, and recent done |
394
+ | `lobstah daemon` | Starts the supervisor loop |
395
+
396
+ `status` performs the same three-source reconciliation the supervisor does —
397
+ CI run state, then busy state, then the status log — so a human and the bridge
398
+ see the same answer.
399
+
400
+ Every command except `daemon` works with the daemon stopped. Dispatches written
401
+ while it is down are claimed when it comes back.
402
+
403
+ It should also utilize TOON for any output and also be self-documenting for any agent to utilize it.
404
+
405
+ ---
406
+
407
+ ## Callers
408
+
409
+ Five, all writing the same descriptor into the same directory.
410
+
411
+ | Caller | Path |
412
+ |---|---|
413
+ | CLI | `lobstah dispatch --repo myapp --brief ./b.md` |
414
+ | Remote bridge | drains a remote dispatcher's queue, writes descriptors, posts state back |
415
+ | OpenClaw node plugin | translates gateway commands into descriptors |
416
+ | Pickup add-on | polls Linear/GitHub, no webhooks — see [pickup.md](pickup.md) |
417
+ | Anything else | a cron job, a shell script, a different tracker |
418
+
419
+ The bridge and the node plugin are the only pieces holding credentials, and
420
+ neither lives in `core`. Anyone can write a third without touching Lobstah, which
421
+ is what makes the separation real rather than nominal.
422
+
423
+ ---
424
+
425
+ ## Lifecycle
426
+
427
+ ### 1. Claim
428
+
429
+ Rename the descriptor into `active/<uuid>/`. Write the brief to
430
+ `active/<uuid>/brief.md` and treat that file as the durable instruction. Never
431
+ the session transcript.
432
+
433
+ ### 2. Worktree allocation
434
+
435
+ One worktree per dispatch, branched from trunk. Never reuse a worktree across
436
+ dispatches, and never allocate a second worktree for a UUID whose first is
437
+ unaccounted for.
438
+
439
+ This prevents convenience stacking, where an agent finishing one task and
440
+ starting the next in the same worktree produces a branch containing the previous
441
+ task's diff. That contaminates evidence and falsely serializes merges.
442
+
443
+ ### 3. Spawn the runner
444
+
445
+ The daemon spawns one runner per dispatch with `setsid`, then records its pid and
446
+ process start time. The runner drives the adapter; the daemon supervises the
447
+ runner and never touches the harness directly.
448
+
449
+ ```ts
450
+ // inside the runner
451
+ const thread = await adapter.start({
452
+ id: uuid, cwd: worktree, brief, model, effort, limits, flags, env,
453
+ });
454
+ for await (const ev of thread.stream()) {
455
+ appendEvent(ev); // state/<uuid>.events
456
+ }
457
+ ```
458
+
459
+ - **The dispatch UUID is the session identity.** Claude takes it as
460
+ `--session-id`; Codex issues a thread id the adapter records on
461
+ `thread.started`. Either way the handle exists before the first token.
462
+ - **`setsid`** so the whole group is killable. Harnesses spawn children — bash
463
+ calls, MCP servers — that a bare `kill` orphans.
464
+ - **Process start time** defeats pid reuse.
465
+ - **Limits** map to `maxBudgetUsd` and turn caps through the adapter, plus a
466
+ Lobstah-owned wall-clock ceiling the SDKs do not provide.
467
+
468
+ ### 4. Supervise
469
+
470
+ Three signal levels, kept separate. None derived from another.
471
+
472
+ | Level | Source | Question |
473
+ |---|---|---|
474
+ | Runner liveness | pid + start time | Does the process exist |
475
+ | Agent activity | SDK event stream | Is a turn or tool call in flight |
476
+ | Task state | agent-declared status | What is the work doing |
477
+
478
+ **Events, not scraping.** The adapter writes normalized events to
479
+ `state/<uuid>.events` from the SDK's typed stream. No JSONL parsing and no shell
480
+ hooks for telemetry.
481
+
482
+ Tool-call granularity differs by harness and both are usable:
483
+
484
+ | | Claude | Codex |
485
+ |---|---|---|
486
+ | Turn boundary | message stream + `Stop` | `turn.started` / `turn.completed` |
487
+ | Tool start | not surfaced | `item.started` |
488
+ | Tool end | `PostToolUse` | `item.completed` |
489
+ | Hang appears as | silence | an open interval |
490
+
491
+ Codex names the hanging call; Claude only shows absence. The adapter normalizes
492
+ both to `lastToolActivityAt`, so the wedge threshold stays global.
493
+
494
+ **Classification rule.** Missing, malformed, stale, or unverified data is
495
+ `unknown`, never `idle`. Absence of signal never means done.
496
+
497
+ **Progress signals**, descending strength: tool-activity timestamp, event-file
498
+ growth, worktree mtime. Codex's `file_change` items give the third signal
499
+ directly; for Claude it stays a filesystem check, which is what covers a long
500
+ build that completes no tool call.
501
+
502
+ ### 5. Report
503
+
504
+ **Status** is append-only, six verbs, nothing else:
505
+
506
+ ```
507
+ working | needs-decision | blocked | paused | done | failed
508
+ ```
509
+
510
+ The consumer is a program, not a model. An LLM-supervised fleet tolerates odd
511
+ status lines because an LLM reads them; a bridge cannot. **The verb is validated at the write
512
+ path and anything outside the set is rejected**, rather than relying on the brief
513
+ to hold.
514
+
515
+ The agent learns the contract the way every dispatch learns everything: the
516
+ runner injects the status and inbox protocol into the prompt it composes.
517
+ Nothing is installed repo-side, and the contract versions with the daemon
518
+ instead of drifting per repo.
519
+
520
+ The log is an event log, not a state field. An agent that resumes silently writes
521
+ nothing, so the last line goes stale. Current state is reconciled in precedence
522
+ order: CI run state, then busy state, then the status log as a last resort.
523
+
524
+ **Instructions** flow the other way through `inbox/<uuid>/`. Sequenced records
525
+ written by atomic rename, acknowledged by moving into `handled/`. The
526
+ acknowledgement is a side effect the agent had to perform anyway, so it cannot be
527
+ faked. Notification is best-effort and retryable; the record is the delivery.
528
+
529
+ ### 6. Complete
530
+
531
+ Collect evidence into `state/<uuid>.evidence` — branch, commits, PR URL if
532
+ opened, CI references, transcript path — then move `active/<uuid>/` to `done/`.
533
+
534
+ `done` means the brief is fulfilled — not that the work item is finished. A PR
535
+ still faces review, feedback rounds, and merge, and "merged" is a forge
536
+ concept core is forbidden to know. A work item therefore spans dispatches —
537
+ implementation, feedback follow-ups, a rebase chore — correlated by whoever
538
+ dispatched them. **The dispatch is the unit of supervision, not the unit of
539
+ work.** Work-item completion belongs to the dispatcher's ledger and the
540
+ tracker's own lifecycle.
541
+
542
+ ---
543
+
544
+ ## Restart policy
545
+
546
+ Dead and wedged get opposite treatment.
547
+
548
+ **Dead.** Process gone, or the foreground group contains only shells.
549
+ Auto-restart, unattended. Preconditions, all hard:
550
+
551
+ - The endpoint is *positively* agent-free. Ambiguous and unreadable never qualify.
552
+ - The worktree is intact and still the recorded one.
553
+ - The prior runner's event stream is closed and its generation retired before
554
+ the replacement is armed, so a late write cannot land in the new incarnation.
555
+
556
+ **Wedged.** Alive, no tool event past threshold. Never restart automatically.
557
+ Bound it, then work a ladder:
558
+
559
+ 1. Resume the harness's own session with a nudge — Claude by session id, Codex
560
+ by thread id. Worktree plus conversation is the cheapest recovery. Fork rather
561
+ than mutate, so the original transcript survives for postmortem.
562
+ 2. Fresh session with the original brief plus a progress note from
563
+ `git log --oneline` and `git status --short`. A wedge caused by the
564
+ conversation will re-wedge on resume.
565
+ 3. Stop. Report `failed` with preserved work.
566
+
567
+ Bounded attempt count per dispatch. A silently re-restarting session against a
568
+ task nobody is watching is the failure mode that costs real money.
569
+
570
+ Never kill a process that already exited, and never race a restart against a
571
+ completed run. Two branches, kept separate in code.
572
+
573
+ ---
574
+
575
+ ## Load-bearing mechanisms
576
+
577
+ Hard-won, language-independent patterns from the fleet supervisors that came
578
+ before, reimplemented rather than ported.
579
+
580
+ | Mechanism | Purpose |
581
+ |---|---|
582
+ | Atomic rename for claim and sequence | Concurrency safety without locks |
583
+ | Acknowledgement by required side effect | An ack that cannot be faked |
584
+ | Generation tokens on hooks | A hook outliving its incarnation fails closed |
585
+ | Three-level state, strict precedence | `unknown` never collapses to `idle` |
586
+ | Positively-agent-free precondition | A false `dead` verdict launches a duplicate |
587
+ | Durable record, retryable notification | Delivery survives a lost notification |
588
+
589
+ Package factoring keeps adapters per harness and transport separated from
590
+ decisions — per-harness `case` statements scattered across call sites are the
591
+ cost of not doing this.
592
+
593
+ ---
594
+
595
+ ## Authentication and routing
596
+
597
+ Subscription authentication is individual. A shared host running a team's work on
598
+ one seat routes other people's requests through one person's seat.
599
+
600
+ **Consequence:** Lobstah is per-user. One daemon, one machine, one seat.
601
+
602
+ A shared orchestrator dispatches to per-user machines through their bridges.
603
+ Online and offline become routing conditions rather than edge cases, resolved
604
+ from `executor.json` heartbeats.
605
+
606
+ Lobstah holds no credentials for anything. The bridge, the node plugin, and the
607
+ operator's own repo config hold them.
608
+
609
+ *Anthropic's position has moved twice this year. Verify against current terms
610
+ before this becomes load-bearing.*
611
+
612
+ ---
613
+
614
+ ## Configuration
615
+
616
+ Local file. Version-controllable, greppable, no network dependency.
617
+
618
+ ```toml
619
+ [repos.myapp]
620
+ path = "~/src/myapp"
621
+ origin = "git@github.com:you/myapp.git" # optional; enables clone on first use
622
+ trunk = "main"
623
+ setup = ["pnpm install"]
624
+ env = { TURBO_TELEMETRY_DISABLED = "1" }
625
+
626
+ [repos.myapp.harness]
627
+ default = "claude"
628
+ model = "opus"
629
+ effort = "high"
630
+
631
+ [harness] # global fallback
632
+ default = "claude"
633
+ model = "sonnet"
634
+
635
+ [limits]
636
+ maxConcurrent = 2
637
+ wedgeThresholdSecs = 600
638
+ maxRestartAttempts = 2
639
+ wallClockSecs = 3600
640
+ ```
641
+
642
+ Harness settings appear at both levels. A descriptor overrides the repo, which
643
+ overrides the global, which overrides the adapter default.
644
+
645
+ ---
646
+
647
+ ## Distribution
648
+
649
+ MIT. Standalone-installable, useful with nothing else installed.
650
+
651
+ Positioning is "open source supervisor for local coding agents." The README has
652
+ to stand alone; if that README cannot be written, the component is not ready to
653
+ ship.
654
+
655
+ Two distribution paths off one core:
656
+
657
+ | Path | Surface |
658
+ |---|---|
659
+ | Standalone | `npm i -g lobstah`, file queue, CLI |
660
+ | OpenClaw plugin | agent tools + a chat command, installed into an existing gateway |
661
+
662
+ A remote dispatcher integrates the same way anything does: by writing
663
+ descriptors and reading state.
664
+
665
+ Support posture: issues accepted, no response-time commitment, no roadmap input,
666
+ contributions merged on the maintainer's schedule. Security reports and
667
+ dependency CVEs answered regardless.
668
+
669
+ ---