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>
This commit is contained in:
Anthony Ettinger 2026-09-13 03:58:55 -07:00 • committed by GitHub
parent 519d13c3d6
commit 9ae7ad8962
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 6355 additions and 20 deletions

View file

@ -150,7 +150,7 @@ The three lines that should have recorded the worked example's first piece, from
| `swarm.spawn` | `swarm`, `parent_swarm`, `task`, `ceiling`, `pieces` | A spawner started a swarm: its id; `parent_swarm`, the swarm of the spawner's own record, absent when the spawner is a root member or the sysop; the task in the spawner's words; the keys it narrowed; and one piece per member it minted, `{ "member", "title", "owns" }`, with the paths that member owns as data. Written before the first member starts. `by` is the spawner: a member id, or `sysop` for a swarm the human started by hand. |
| `member.start` | `member`, `session`, `swarm`, `parent`, `depth`, `engine`, `host`, `cwd`, `approvals`, `piece` | A member began, and its record is claimed. Written by the session itself with `by` its own member id, or by the starter, with `by` the starter, for an engine that cannot write it. `session` is the engine's own id when the record has none. The row a sysop tool draws in the tree; `approvals: bypass` is the mark it shows. |
| `member.spend` | `member`, `amount`, `total` | A member spent something: `amount` since the last line and `total` so far, as `<amount> <currency>` or `<n> tokens`, in the engine's own numbers. Written by an engine that can count, at intervals or at the end. Summed per swarm and per fleet against `ceiling.budget`. |
| `member.end` | `member`, `state`, `summary`, `total`, `links` | A member finished. `state` is `done`, `failed`, `stopped`, `budget`, `timeout` or `lost`. `by` says who ended it: `sysop`, the spawner, or the member itself. `summary` is the member's closing summary when it wrote one; `total` its final spend; `links` the PRs and URLs it produced. `lost` is what a sysop tool writes for a member whose engine no longer lists it and that has no end line from any writer, with `by` `sysop` when the tool runs with no `OPENFLEET_MEMBER` and that member otherwise. A member has one end line that counts: the first written, except `lost`, which the engine's or the spawner's own `member.end` supersedes whenever it arrives. A session or tool that finds an end line for a member writes none, unless that line is `lost`, which the engine's or the spawner's own `member.end` may follow and supersede; `stop` on an ended member writes nothing. |
| `member.end` | `member`, `state`, `summary`, `total`, `links` | A member finished. `state` is `done`, `failed`, `stopped`, `budget`, `timeout` or `lost`. `by` says who ended it: `sysop`, the spawner, or the member itself. `summary` is the member's closing summary when it wrote one; `total` its final spend; `links` the PRs and URLs it produced. `lost` is what a sysop tool writes for a member whose engine's roster can hold it and no longer lists it, and that has no end line from any writer: a Claude Code background job, a moshcode pane. An interactive or `-p` Claude Code session is never in `claude agents`, so no roster marks it lost. `by` is `sysop` when the tool runs with no `OPENFLEET_MEMBER` and that member otherwise. A member has one end line that counts: the first written, except `lost`, which the engine's or the spawner's own `member.end` supersedes whenever it arrives. A session or tool that finds an end line for a member writes none, unless that line is `lost`, which the engine's or the spawner's own `member.end` may follow and supersede; `stop` on an ended member writes nothing. |
| `swarm.end` | `swarm`, `state`, `summary`, `verdict` | A swarm ended as one unit: every member and every nested swarm under it has an end line at or before this one. `state` is `done` when every member ended `done`, else the first of `failed`, `stopped`, `budget`, `timeout` found among its members' end lines. `summary` is the spawner's synthesis, its closing summary of every member's summary, when it has one; `verdict` the verify result when it ran. One `swarm.end` per swarm ended, never two for the same swarm: a writer checks the ledger first and writes it only when none exists, and a synthesis that arrives after one goes into the spawner's own `member.end` `summary`. |
| `ceiling.refuse` | `member`, `action`, `key`, `wanted`, `allowed` | An engine or tool refused something because it would exceed the ceiling: `action` is `start` or `spawn`; `key` names the ceiling key; `wanted` and `allowed` say the two values. `member` is the id the refused record names when one exists, else absent; `by` is the spawner: that record's `parent`, else the caller's `OPENFLEET_MEMBER`, else the refused record's own `member` when it carries `orphan`. This is how a sysop finds out what an agent tried. |
@ -162,11 +162,11 @@ Five verbs, over `$OPENFLEET_HOME`. Two are the sysop's alone. The test is the e
|---|---|---|
| `open` | sysop only | Creates a fleet: mints the id, records the sysop, sets the ceiling from flags (`--approvals native\|bypass --budget "20 USD" --depth 2 --fan-out 4 --hosts dev,netcup --until 2h`), writes `fleet.open`, writes the id to `current`, prints it. Refuses when `OPENFLEET_MEMBER` is set. |
| `cap` | sysop only | Sets the whole ceiling of a fleet, or narrows a running swarm's, and writes `fleet.cap`. Members already above the new ceiling, a `bypass` member under a now-`native` ceiling, a member on a now-forbidden host, are stopped by the tool, each with `member.end` state `stopped`. Refuses when `OPENFLEET_MEMBER` is set. An agent narrows only at spawn time, in the `swarm.spawn` it writes. |
| `tree` | anyone | Renders one fleet, or every fleet on this host, as a tree: fleet, its swarms, each swarm's members and nested swarms, with state, engine, host, depth, spend against budget, and a mark on every member whose approvals is `bypass`. Built from the ledger and the records. When an engine's roster is readable, sessions it lists that have no record are drawn as root members of the implicit fleet and marked as coming from the roster, and a recorded member the roster no longer lists gets `member.end` state `lost`. An agent calls it on its own fleet to learn its siblings. |
| `tree` | anyone | Renders one fleet, or every fleet on this host, as a tree: fleet, its swarms, each swarm's members and nested swarms, with state, engine, host, depth, spend against budget, and a mark on every member whose approvals is `bypass`. Built from the ledger and the records. When an engine's roster is readable, sessions it lists that have no record are drawn as root members of the implicit fleet and marked as coming from the roster, and a recorded member the roster can hold and no longer lists (a background job, a pane; never an interactive session the roster does not list) gets `member.end` state `lost`. Run by the sysop, it enforces rule 6: a working member past its effective `until` is stopped through its engine and ends `timeout`, and a swarm or fleet whose summed `member.spend` has reached its budget has its members stopped, each ending `budget`, then its `swarm.end` when the swarm is complete. An agent calls it on its own fleet to learn its siblings; it stops nothing. |
| `stop` | anyone, within reach | Ends a member, a swarm, or everything in a fleet (`--fleet`) as one unit. For a swarm: nested swarms first, each with its own `swarm.end`, then the target's members through each member's own engine, then the target's `swarm.end`. An agent may stop only a swarm it spawned or a member under such a swarm; `--fleet`, an ancestor, or a sibling's swarm refuses when `OPENFLEET_MEMBER` is set. |
| `log` | anyone | Reads the ledger for a fleet, a swarm or a member: what happened, in order, who did it, what each member spent, how each ended, what was refused and why. `--since`, `--member`, `--swarm`, `--json`. |
The reference sysop tool is `logicsrc fleet`. FleetView (`claude agents`) and `moshcode fleet` would offer the same five verbs over the same files, each able to stop the members its own engine runs. None of the three ships yet.
The reference sysop tool is `logicsrc fleet`, in `@logicsrc/openfleet` 0.1.0 (logicsrc CLI 0.3.0). `moshcode fleet` offers the same five verbs over the same files from moshcode 0.99.0, and stops the members its engine runs. FleetView (`claude agents`) does not offer them yet; Claude Code takes part through the hooks `logicsrc fleet hooks install` writes, and `logicsrc fleet` stops its jobs through `claude stop`.
## Rules
@ -182,7 +182,7 @@ The reference sysop tool is `logicsrc fleet`. FleetView (`claude agents`) and `m
10. The ledger is append-only, one JSON object per line, one file per fleet per host. Every line carries `at`, `event`, `fleet`, `host` and `by`; `by` is `sysop` or a member id; a tool never writes `sysop` for an action an agent took.
11. `stop` on a swarm ends nested swarms first, each with its own `swarm.end`, then the target's members through their own engines, then writes the target's `swarm.end`: one `swarm.end` per swarm ended, never two for the same swarm, so a writer checks the ledger first.
12. A member with no record is a root member of the implicit fleet `<user>@<host>` of the account that started it, never an error. The implicit fleet's ceiling is depth 1 and hosts that host, with no fleet-level `approvals`; each root member's subtree runs under the approvals that root was started with, read from the engine's roster: Claude Code's `respawnFlags`, moshcode's `args`.
13. A session started with no `OPENFLEET_RECORD` but with an engine's child marker (`CLAUDE_JOB_DIR`, `CLAUDE_CODE_CHILD_SESSION`, `MOSHCODE_HERD_NAME`) in the environment the engine was invoked with, before it sets its own session variables, was started by something that dropped its record. For a `--bg` job the launching `claude` process makes the test, not the daemon it starts. The engine writes a root record with `orphan: true`, then checks approvals before claiming it: a refusal writes `ceiling.refuse` with `member` and `by` the record's own `member` and leaves the record unclaimed, unless `current` names an opened fleet whose ceiling says `bypass`; otherwise it claims the record with `by` its own member id, and the tree marks the row. Under one account the environment test is a convention and the ledger is the audit; nothing in 0.1 stops a process from unsetting a variable.
13. A session started with no `OPENFLEET_RECORD` but with an engine's child marker (`CLAUDE_JOB_DIR`, `CLAUDE_CODE_CHILD_SESSION`, `MOSHCODE_HERD_NAME`) in the environment the engine was invoked with, before it sets its own session variables, was started by something that dropped its record. For a `--bg` job the launching `claude` process makes the test, not the daemon it starts. In 0.1 no launcher makes that test yet: a background job dispatched with no launcher record is a clean root of the implicit fleet. The engine writes a root record with `orphan: true`, then checks approvals before claiming it: a refusal writes `ceiling.refuse` with `member` and `by` the record's own `member` and leaves the record unclaimed, unless `current` names an opened fleet whose ceiling says `bypass`; otherwise it claims the record with `by` its own member id, and the tree marks the row. Under one account the environment test is a convention and the ledger is the audit; nothing in 0.1 stops a process from unsetting a variable.
14. A sysop tool renders every recorded member from records and the ledger. Members with no record exist only in an engine's roster, and the tool reads the rosters it can (`claude agents --json`, `~/.moshcode/herd/sessions.json`) to draw them and to add liveness. No roster is required for a recorded member.
15. A record and a ledger never hold a credential. Unknown keys are kept.
@ -200,13 +200,13 @@ Had `460a4502` been started with approvals `native`, its subtree's ceiling would
## Reference implementations
None ships yet. What each tool adds, from what it records today:
Three ship: `logicsrc fleet` in `@logicsrc/openfleet` 0.1.0 (logicsrc CLI 0.3.0), Claude Code through `logicsrc fleet hooks install`, and moshcode 0.99.0, which writes records in `moshcode swarm` and offers `moshcode fleet`. What each does, on top of what it records:
**Claude Code.** Resolve the fleet from `OPENFLEET_FLEET`, else `$OPENFLEET_HOME/current`, else the implicit `<user>@<host>`. On every start, interactive, `--bg`, `-p`, or dispatched from `claude agents`: when `OPENFLEET_RECORD` names an unclaimed record, claim it; when it names a claimed one, derive a child record and claim that; when it is unset, write a root record, with `orphan: true` when the environment the `claude` command was invoked with carries a child marker; for a `--bg` job the launching process makes that test, not the daemon. A root record written with no opened fleet carries `ceiling: { approvals, depth: 1, hosts: [host] }` from its own flags, or `approvals: native` when the record carries `orphan`. Set `approvals` to `bypass` whenever the flags carry `--permission-mode bypassPermissions` or `--dangerously-skip-permissions`; refuse the start with `ceiling.refuse` when the ceiling says no, before any `member.start`; set the four variables below `OPENFLEET_HOME` in the job environment beside `CLAUDE_JOB_DIR`. Add the record's `fleet`, `swarm`, `parent`, `depth` and `approvals` to `~/.claude/jobs/<id>/state.json` and to the `claude agents --json` rows, so the roster stops being flat. Append `member.spend` from the job's token count at intervals, and `member.end` when the job first goes terminal, or when an interactive or `-p` session exits, or on SIGHUP when it can, with `summary` from the job's final output, or its SUMMARY section when the prompt asked for one, and `links` from the state file's `children` key, which holds the links a job produced and not child jobs. FleetView groups rows by fleet and swarm, marks `bypass` members, sums spend against budget, and offers `stop` on a swarm and `cap`. The in-process Agent and Workflow tools stay as they are: not members. One thing is unverified: whether the `claude agents` view passes its environment to the daemon it starts. If it does not, the view passes the four variables itself, and the fallback is a root member of the implicit fleet.
**Claude Code.** Ships as hooks. `logicsrc fleet hooks install` merges five entries into `~/.claude/settings.json` and never clobbers what is there: SessionStart resolves the record and hands the member its variables, UserPromptSubmit checks the ceiling with the permission mode the engine reports and writes `member.start` or refuses the first prompt, PreToolUse denies an Edit or Write outside `piece.owns`, Stop and SessionEnd write `member.end`. What the hooks do is what this paragraph asks of the engine. Resolve the fleet from `OPENFLEET_FLEET`, else `$OPENFLEET_HOME/current`, else the implicit `<user>@<host>`. On every start, interactive, `--bg`, `-p`, or dispatched from `claude agents`: when `OPENFLEET_RECORD` names an unclaimed record, claim it; when it names a claimed one, derive a child record and claim that; when it is unset, write a root record, with `orphan: true` when the environment the `claude` command was invoked with carries a child marker; for a `--bg` job the launching process makes that test, not the daemon. A root record written with no opened fleet carries `ceiling: { approvals, depth: 1, hosts: [host] }` from its own flags, or `approvals: native` when the record carries `orphan`. Set `approvals` to `bypass` whenever the flags carry `--permission-mode bypassPermissions` or `--dangerously-skip-permissions`; refuse the start with `ceiling.refuse` when the ceiling says no, before any `member.start`; set the four variables below `OPENFLEET_HOME` in the job environment beside `CLAUDE_JOB_DIR`. Still Claude Code's own to add: the record's `fleet`, `swarm`, `parent`, `depth` and `approvals` in `~/.claude/jobs/<id>/state.json` and in the `claude agents --json` rows, so the roster stops being flat; `member.spend` from the job's token count at intervals; and a FleetView that groups rows by fleet and swarm, marks `bypass` members, sums spend against budget, and offers `stop` on a swarm and `cap`. Until then the hooks write `member.end` when a background job first goes terminal, or when an interactive or `-p` session exits, with `summary` from the closing message, or its SUMMARY section when the prompt asked for one, `total` from the job's token count, and `links` from the state file's `children` key, which holds the links a job produced and not child jobs. The in-process Agent and Workflow tools stay as they are: not members. Two limits were verified on Claude Code 2.1.270 and shape the hooks. A background job dispatched from `claude agents` runs in a spare the daemon forked before any launcher existed, so no launcher environment reaches it: it is a root member of the implicit fleet unless a launcher writes its record and the job finds that record by another channel than the environment. And `OPENFLEET_*` exported at SessionStart reach the member's tools through `CLAUDE_ENV_FILE` but not later hooks, so the hooks key on `session_id` through `$OPENFLEET_HOME/sessions/<session_id>.json`, a file of the implementation and not of this spec.
**moshcode.** In `moshcode swarm`: mint the swarm id before the plan call, and run the planner with no `OPENFLEET_SWARM`, since its `swarm.spawn` does not exist until the plan returns; extend the planner's reply to `[{ "title", "prompt", "files" }]` and store `files` as `piece.owns`, so "do not touch bye.sh" becomes data moshcode can check instead of prose it never parses; write `swarm.spawn` with one piece per pane, member ids `<swarm>-<n>`, and the narrowing from `--agents` (`fan_out`) and `--timeout` (`until`); name each pane after its member id and write one unclaimed record per pane with `session` the pane's tmux target; add the four variables to the pane's environment line beside `MOSHCODE_HERD_NAME` and `MOSHCODE_HERD_DIR`, and keep them when deleting `ANTHROPIC_API_KEY` and `CLAUDE_CODE_SESSION_ID`. A `claude` pane claims its own record. For codex, deepseek and kimi panes moshcode writes `member.start` from the herd ledger's `submit` event, since nothing else in the pane writes a record. Record `approvals: bypass` truthfully: today `sessions.json` says `agent: false` while the pane runs `claude --dangerously-skip-permissions`. Refuse that flag with `ceiling.refuse` unless the ceiling says `bypass`. At the end, write `member.end` from the herd `end` event for every pane whose session has not written its own by then, then `swarm.end` with the synthesis as `summary` and the `--verify` result as `verdict`, then the default kill; `--keep` leaves members running and writes neither `member.end` nor `swarm.end`. When moshcode itself runs inside a member, the swarm's parent is that member. `moshcode fleet open|cap|tree|stop|log` is the sysop tool for this engine, with `herd ps` grouped by fleet and swarm.
**moshcode.** From 0.99.0, `moshcode swarm` writes the record and the ledger, and `moshcode fleet` is the sysop tool for its engine. What that means, in `moshcode swarm`: mint the swarm id before the plan call, and run the planner with no `OPENFLEET_SWARM`, since its `swarm.spawn` does not exist until the plan returns; extend the planner's reply to `[{ "title", "prompt", "files" }]` and store `files` as `piece.owns`, so "do not touch bye.sh" becomes data moshcode can check instead of prose it never parses; write `swarm.spawn` with one piece per pane, member ids `<swarm>-<n>`, and the narrowing from `--agents` (`fan_out`) and `--timeout` (`until`); name each pane after its member id and write one unclaimed record per pane with `session` the pane's tmux target; add the four variables to the pane's environment line beside `MOSHCODE_HERD_NAME` and `MOSHCODE_HERD_DIR`, and keep them when deleting `ANTHROPIC_API_KEY` and `CLAUDE_CODE_SESSION_ID`. A `claude` pane claims its own record. For codex, deepseek and kimi panes moshcode writes `member.start` from the herd ledger's `submit` event, since nothing else in the pane writes a record. Record `approvals: bypass` truthfully: today `sessions.json` says `agent: false` while the pane runs `claude --dangerously-skip-permissions`. Refuse that flag with `ceiling.refuse` unless the ceiling says `bypass`. At the end, write `member.end` from the herd `end` event for every pane whose session has not written its own by then, then `swarm.end` with the synthesis as `summary` and the `--verify` result as `verdict`, then the default kill; `--keep` leaves members running and writes neither `member.end` nor `swarm.end`. When moshcode itself runs inside a member, the swarm's parent is that member. `moshcode fleet open|cap|tree|stop|log` is the sysop tool for this engine, with `herd ps` grouped by fleet and swarm.
**logicsrc.** `logicsrc fleet open|cap|tree|stop|log`, the engine-neutral sysop tool that folds any `$OPENFLEET_HOME` into one tree and stops a member through the engine its record names.
**logicsrc.** `logicsrc fleet open|cap|tree|stop|log`, the engine-neutral sysop tool that folds any `$OPENFLEET_HOME` into one tree and stops a member through the engine its record names: `claude stop` for `claude-code`, `moshcode herd kill` for `moshcode/*`, `tmux kill-pane` for `tmux`, a signal to the pid for `claude-p`. Ships in `@logicsrc/openfleet` 0.1.0 with the logicsrc CLI 0.3.0. `tree` reads `claude agents --json --all` and `~/.moshcode/herd/sessions.json` for liveness and for members with no record, and writes `member.end` state `lost` for a recorded background job or pane its engine's roster can hold and no longer lists; an interactive or `-p` `claude` session, which `claude agents` never lists, is never marked lost. Run by the sysop, `tree` also enforces rule 6: a member past its effective `until` is stopped through its engine and ends `timeout`, a swarm or fleet whose summed `member.spend` has reached its budget has its members stopped, each ending `budget`, and each swarm touched gets its `swarm.end` once it is complete. `stop` on a claude-code member calls `claude stop` with the job id: the member id of a background job, else the first eight characters of the record's session when that is a session UUID; an interactive session with no job id cannot be stopped by the tool, which says so. Every `member.start`, `member.end` and `swarm.end` the tool or the hooks write takes a once-marker first, an exclusive create of `$OPENFLEET_HOME/fleets/<fleet>/marks/<event>.<id>` (`member.end.<id>.lost` for a `lost` line, so a real end can still follow it and take the plain one); a writer that finds the marker taken writes nothing and reports "already". moshcode uses the same paths, so the two writers never double a line. `logicsrc fleet hooks install|remove|status` and `logicsrc fleet hook <Event>` are the Claude Code side above. Every verb takes `--json`.
## What is deliberately absent
@ -258,7 +258,7 @@ None ships yet. What each tool adds, from what it records today:
| Version | Date | Change |
|---|---|---|
| 0.1 | 2026-09-13 | First publication: fleet and swarm, the record, the environment, claiming and deriving, the ledger's eight events, five sysop verbs, fifteen rules, the worked example, what Claude Code and moshcode would each add. |
| 0.1 | 2026-09-13 | First publication: fleet and swarm, the record, the environment, claiming and deriving, the ledger's eight events, five sysop verbs, fifteen rules, the worked example, what Claude Code and moshcode each add. Same day: `logicsrc fleet` and the Claude Code hooks ship in `@logicsrc/openfleet` 0.1.0, `moshcode swarm` and `moshcode fleet` in moshcode 0.99.0. |
## License