logicsrc/packages/openfleet
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
..
src OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks (#185) 2026-09-13 10:58:55 +00:00
package.json OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks (#185) 2026-09-13 10:58:55 +00:00
README.md OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks (#185) 2026-09-13 10:58:55 +00:00
tsconfig.json OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks (#185) 2026-09-13 10:58:55 +00:00

@logicsrc/openfleet

Reference implementation of OpenFleet, the record an agent session carries about where it sits: which human answers for it, who spawned it, for what task, at what depth, and under what ceiling. A fleet is every agent session one human, its sysop, is answerable for. A swarm is the set of sessions one spawner starts to do one task.

This package holds the record, the ledger, the ceiling rules, claiming and deriving, the folded tree, the five sysop verbs, and the Claude Code hooks. moshcode writes the same files from moshcode swarm and reads them with moshcode fleet; one tree shows both engines.

Install

npm install -g @logicsrc/cli
logicsrc fleet --help

The verbs live in this package and the umbrella CLI wraps them as logicsrc fleet. To use the library directly:

npm install @logicsrc/openfleet

The files

Everything lives under $OPENFLEET_HOME, default ~/.openfleet:

Path What
fleets/<fleet>/members/<member>.json One record per member, written before it starts, never changed after member.start.
fleets/<fleet>/ledger.jsonl The append-only ledger for this host. ledger.<host>.jsonl copies from other hosts are merged by at.
fleets/<fleet>/marks/<event>.<id> Once-markers for member.start, member.end and swarm.end, so two writers never double a line.
current The fleet the account's next root member joins. Absent means the implicit <user>@<host>.
sessions/<session_id>.json The Claude Code hooks' own lookup, not part of the spec.

Files are 0600 and directories 0700.

The five verbs

# Sysop only: mint a fleet, set its whole ceiling, write fleet.open and current.
logicsrc fleet open team --approvals bypass --budget "20 USD" --depth 2 --fan-out 4 --hosts dev,netcup --until 2h

# Sysop only: set a fleet's whole ceiling, or narrow a running swarm's. Members now above it are stopped.
logicsrc fleet cap team-20260913 --approvals native
logicsrc fleet cap create-two-0541 --fan-out 2

# Anyone: the tree, from the ledger, the records and the engine rosters. Run by the sysop it also
# stops what is past its deadline or over its budget (rule 6) and marks lost what its engine no longer lists.
logicsrc fleet tree
logicsrc fleet tree anthony@dev --json

# Anyone, within reach: end a member, a swarm (nested swarms first), or a whole fleet through each member's engine.
logicsrc fleet stop create-two-0541
logicsrc fleet stop team-20260913 --fleet

# Anyone: the ledger, in order, with who did it.
logicsrc fleet log --swarm create-two-0541
logicsrc fleet log --since 2h --json

Every verb takes --json. open and cap refuse with exit 4 when the process carries OPENFLEET_MEMBER: that process is an agent. stop refuses outside the caller's own subtree the same way. An engine that will not end a member leaves it without an end line and the verb exits 3.

stop goes through the member's own engine: claude stop <job id> for claude-code, moshcode herd kill for moshcode/*, tmux kill-pane for tmux, a signal to the pid for claude-p.

Claude Code hooks

logicsrc fleet hooks install    # merges five entries into ~/.claude/settings.json, never clobbers
logicsrc fleet hooks status
logicsrc fleet hooks remove     # takes out only ours

With the hooks installed every Claude Code session becomes a recorded member: SessionStart claims the record OPENFLEET_RECORD names, derives a child under a claimed one, or writes a root record; UserPromptSubmit checks the ceiling with the permission mode the engine reports and writes member.start, or refuses the first prompt with exit 2; PreToolUse denies an Edit or Write outside piece.owns; Stop and SessionEnd write member.end. A hook never fails the engine. logicsrc fleet hook <Event> is the entry point the settings file calls.

Environment

Variable Meaning
OPENFLEET_HOME The root directory. Default ~/.openfleet.
OPENFLEET_RECORD The absolute path of this member's record.
OPENFLEET_FLEET A copy of the record's fleet; in the sysop's shell, the fleet new roots join.
OPENFLEET_MEMBER A copy of the record's member. Present means this process is an agent.
OPENFLEET_SWARM A copy of the record's swarm, when it carries one.

Library

import { context, claimOrDerive, startMember, endMember, fold, renderTree, readLedger } from "@logicsrc/openfleet";
import { registerOpenFleetCommands } from "@logicsrc/openfleet/commands";

context(env) answers which fleet and which member a process is. claimOrDerive is the engine-side rule from the spec's "Claiming and deriving". fold builds the tree tree renders. registerOpenFleetCommands(command, deps) mounts the verbs on a commander command with every world-touching dependency injectable.

License

MIT. The specification text is CC BY 4.0.