logicsrc/prd/0008-openfleet-reference-implementation.md
Anthony Ettinger 9ae7ad8962
OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks (#185)
* OpenFleet reference implementation: @logicsrc/openfleet 0.1.0 and logicsrc fleet

Ship what docs/openfleet.md describes. The new workspace package holds the
record (write once, never overwrite, 0600), the ledger (append-only JSON
Lines, merged across ledger*.jsonl by at), the ceiling rules (whole fleet
ceiling, narrowed swarm keys, a merge that never widens, refusals by key),
claiming and deriving exactly as the spec's "Claiming and deriving" and
rule 13, and fold(), which turns any $OPENFLEET_HOME plus the engine
rosters into the tree the landing page shows.

logicsrc fleet open|cap|tree|stop|log are the sysop's verbs, every one with
--json. open and cap exit 4 when OPENFLEET_MEMBER is set; stop exits 4
outside the caller's subtree, ends nested swarms first, goes through each
member's own engine (claude stop, moshcode herd kill, tmux kill-pane, a
signal for claude-p) and writes one swarm.end per swarm. tree reads claude
agents --json --all and ~/.moshcode/herd/sessions.json when it can, draws
recordless sessions as roster roots of the implicit fleet, and writes
member.end lost for a recorded member its engine no longer lists.

Claude Code takes part through hooks: logicsrc fleet hooks install merges
SessionStart, UserPromptSubmit, PreToolUse, Stop and SessionEnd into
~/.claude/settings.json without clobbering it, and logicsrc fleet hook
<Event> runs each one. SessionStart claims, derives or writes a root record
and hands the member its variables through CLAUDE_ENV_FILE; UserPromptSubmit
checks the ceiling with the permission mode the engine reports and writes
member.start, or refuses the first prompt with exit 2 and ceiling.refuse;
PreToolUse denies an edit outside piece.owns; Stop and SessionEnd write
member.end. A hand-started root takes the engine's reported approvals
before member.start, since the command line only guesses them. Hooks
never fail the engine: everything is caught and logged to hooks.log.

The spec and the landing page now say what ships, keep Status 0.1, and
record the two verified Claude Code limits: a background job dispatched from
claude agents gets no launcher environment, and OPENFLEET_* exported at
SessionStart reach the member's tools but not later hooks, so hooks key on
session_id through $OPENFLEET_HOME/sessions/<session_id>.json. PRD 0008
covers the work. CLI 0.2.1 -> 0.3.0; build and build:cli chains build the
package before the CLI; README and docs/cli.md list the group.

Tests: 95 in the package (record, ledger merge, every narrower case, the
worked example's claim and derive, the folded tree, hook install
idempotence, each hook handler including the exit-2 refusal and the
PreToolUse deny, every verb with fake deps) and 4 in the CLI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV

* OpenFleet fix round: rebuild the ceiling from the ledger, once-markers, rule 6 in tree, lost only for what a roster can hold

The review of the reference implementation against moshcode found the two
readers disagreeing on the same files. This round applies the shared
rulings so both sides read a ledger the same way.

Ceiling (R-A, R-B, R-C, R1, R6, R10, R15, R17): memberCeiling rebuilds the
effective ceiling from the ledger on every read. The latest fleet-target
fleet.cap (else fleet.open, else the implicit fleet's) replaces the copy in
a record, so a sysop's widening cap reaches running members; then each
swarm.spawn narrowing down the path, then swarm caps last. In the implicit
fleet a parentless record's own approvals enters at the root; a ceiling a
writer left without the key is never read as native, and startMember fills
it with the engine's word while the record is unclaimed. A fleet.open or
cap with no hosts means the host it was written on (R23).

Once-markers (R-G, R28): member.start, member.end and swarm.end each take
an exclusive create under fleets/<fleet>/marks/<event>.<id> before the
append; a lost end takes <id>.lost so a real end can still supersede it.
The hooks let a real end follow a lost line (R9).

tree (R-F, R20): run by the sysop it enforces rule 6, stopping a member
past its effective until with state timeout and the members of a swarm or
fleet at its budget with state budget, then writes swarm.end for each swarm
touched once it is complete. An agent's tree stops nothing. lost is written
only for a member its engine's roster can hold: a claude-code background job
(8-hex member or session) or a moshcode pane, never an interactive session
claude agents does not list (R-E, R3, R14). A nested swarm is drawn under
the member that spawned it and its row shows the effective ceiling (R25).

stop and cap (R-D, R-H, R22, R27): swarm.end is written only once every
member and every nested swarm has an end line that counts; an engine that
will not end a member leaves it without an end line and the verb exits
non-zero. claude stop takes the job id: the member of a background job,
else the first eight characters of a session UUID; an interactive session
with no job id cannot be stopped and the tool says so. cap on a swarm
refuses a key that would widen. A derived claude-code job is named by its
job id and carries no pid.

Also: R-I (endMember ends only the engine-minted swarm of one), R35 (a
derived record's guessed approvals corrected at UserPromptSubmit), R32
(the UserPromptSubmit hook passes only exit 2 through), R31 (package
README), R36 (rule 13 says the launcher test is unimplemented in 0.1),
docs and PRD 0008 updated for lost, rule 6 and the markers. 113 openfleet
tests, 93 CLI tests, contract green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV

* openfleet hooks: no member.end for a member that never started

A first prompt refused by the ceiling still lets the session wind down through Stop and SessionEnd; those handlers now write nothing when the ledger holds no member.start for the member, so a refused member is never drawn as done.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV

* logicsrc-mcp test: the next free PRD id is 0009 now that PRD 0008 exists

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 10:58:55 +00:00

7.3 KiB

openprd id title status authors created updated repo discussion implementation tags supersedes superseded-by
0.3 0008 Ship the OpenFleet reference implementation Draft
anthony@profullstack.com
2026-09-13 2026-09-13 profullstack/logicsrc packages/openfleet
openfleet
agents
fleet
swarm
claude-code
moshcode
cli

Problem

The OpenFleet specification (docs/openfleet.md) was published on 2026-09-13 from one incident: a Claude Code background job asked moshcode to split a task across two agents, and afterwards nobody, human or agent, could say who had started either worker, why, under what ceiling, or with whose approval. The spec names the record, the ledger and five sysop verbs, and named logicsrc fleet as the reference sysop tool. It shipped with the line "None of the three ships yet". A standard nothing implements is prose; the incident repeats every morning until the files exist and something writes them.

Goals

  • A human can open a fleet, set its ceiling, see every agent session under them as one tree, stop a swarm as one unit, and read afterwards what happened and who did it, from one command: logicsrc fleet.
  • Every Claude Code session on a box with the hooks installed becomes a recorded member: it claims the record its starter wrote, or derives its own, or is a root member of the implicit fleet, and it refuses to run above the ceiling it was started under.
  • moshcode and Claude Code write the same files the same way, so one tree shows both engines' members without either reading the other's roster.

Non-Goals

  • No orchestration: splitting a task, choosing an engine, verifying, and synthesising stay in moshcode swarm and @logicsrc/agentswarm.
  • No change to Claude Code itself. The engine side ships as hooks over its own settings file; FleetView grouping and member.spend at intervals wait on the engine.
  • No network surface, no signed ledger lines, no freeze, no adopt: the 0.2 questions stay open.

Users

  • The sysop: one developer answerable for every agent session on their box, who wants to see the tree and stop the wrong part of it.
  • An agent that spawns a swarm and wants its children to know who they are and what they own.
  • A later session inspecting a member's record to answer the five questions the incident could not.

Requirements

  • R1 [P0] @logicsrc/openfleet 0.1.0: the record (write once, never overwrite), the ledger (append-only, 0600, merged across ledger*.jsonl by at), the ceiling rules (whole fleet ceiling, narrowed swarm keys, merge that never widens, refusal by key), claim and derive exactly as the spec's "Claiming and deriving", and fold into the tree the landing page shows.
  • R2 [P0] logicsrc fleet open|cap|tree|stop|log with the spec's flags; open and cap exit 4 when OPENFLEET_MEMBER is set; stop exits 4 outside the caller's subtree; stop ends nested swarms first and writes one swarm.end per swarm, only once every member and every nested swarm has an end line that counts, and exits non-zero when an engine would not end a member; cap on a swarm refuses a key that would widen; every verb takes --json.
  • R2a [P0] Rule 6 lives in tree, run by the sysop: a working member past its effective until is stopped through its engine and ends timeout; a swarm or fleet whose summed member.spend in the budget's unit has reached its budget has its members stopped, each ending budget; each swarm touched gets its swarm.end when complete. An agent's tree stops nothing.
  • R2b [P0] The effective ceiling is rebuilt from the ledger on every read: the latest fleet-target fleet.cap (else fleet.open, else the implicit fleet's) replaces the copy in a record, widening included; then each swarm.spawn narrowing down the path, then swarm caps last. In the implicit fleet a parentless record's own approvals enters at the root, and the engine fills a ceiling a writer left without the key.
  • R3 [P0] stop goes through the member's own engine: claude stop for claude-code, moshcode herd kill for moshcode/*, tmux kill-pane for tmux, a signal for claude-p. Never a shell string.
  • R4 [P0] Claude Code hooks: logicsrc fleet hook <Event> for SessionStart, UserPromptSubmit, PreToolUse, Stop and SessionEnd, and logicsrc fleet hooks install|remove|status that merges into ~/.claude/settings.json and never clobbers it. A hook never fails the engine; a refused start exits 2 before any member.start.
  • R5 [P1] tree reads claude agents --json --all and ~/.moshcode/herd/sessions.json when it can, draws recordless sessions as roots of the implicit fleet, and writes member.end state lost for a recorded background job or pane its engine's roster can hold and no longer lists. claude agents lists background jobs only, so an interactive or -p session (a UUID member with no job id) is never marked lost by it.
  • R6 [P1] The spec and the landing page say what ships, keep Status: 0.1, and record the two verified Claude Code limits (no launcher environment reaches a dispatched background job; exported variables reach tools but not later hooks).
  • R7 [P1] Tests cover record and ledger IO, every narrower case, the worked example's claim and derive, the folded tree, hook install idempotence, and each hook handler, including the exit-2 refusal and the PreToolUse deny.

UX Notes

logicsrc fleet tree prints the tree the landing page shows: fleet header, root members, swarms nested under their spawner, members with engine, state, [bypass], owns. log prints one line per event, oldest first, with who did it. Refusals name the key, what was wanted and what was allowed.

Tech Stack

TypeScript, NodeNext, commander 14, vitest 4. No workspace dependencies beyond the CLI's file:../openfleet link. Node 18+ (the installer's floor), so the tree is plain text rather than a TUI.

Monetization

None. It is the reference implementation of an open standard.

Success Metrics

  • The worked example's morning can be replayed against a temp home and logicsrc fleet tree prints the tree the spec shows.
  • A Claude Code session started with the hooks installed appears in logicsrc fleet tree with the right approvals mark without anyone editing a file by hand.

Risks & Open Questions

  • A background job dispatched from claude agents gets no launcher environment, so a launcher that wants it in a swarm must write its record and pass the path another way (a --settings hook command, or a lookup by the job's cwd and intent). Until then it is a root of the implicit fleet.
  • User-level hooks fire for every claude -p a tool makes, so each becomes a swarm of one and, at depth 1 in the implicit fleet, is refused on depth. The spec lists this as an open question; the hooks enforce the letter of it.
  • A ledger check before a write is not exclusion, so member.start, member.end and swarm.end each take a once-marker first: an exclusive create of fleets/<fleet>/marks/<event>.<id> (.lost suffixed for a lost end, so a real end can still supersede it). moshcode uses the same paths. A marker taken by a writer that then crashed before appending leaves the line unwritten until someone clears the marker by hand; 0.1 accepts that over a doubled audit line.