mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 20:33:50 +00:00
* 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>
154 lines
7.3 KiB
Markdown
154 lines
7.3 KiB
Markdown
---
|
|
openprd: "0.3"
|
|
id: "0008"
|
|
title: "Ship the OpenFleet reference implementation"
|
|
status: Draft
|
|
authors:
|
|
- anthony@profullstack.com
|
|
created: 2026-09-13
|
|
updated: 2026-09-13
|
|
repo: profullstack/logicsrc
|
|
discussion:
|
|
implementation: packages/openfleet
|
|
tags:
|
|
- openfleet
|
|
- agents
|
|
- fleet
|
|
- swarm
|
|
- claude-code
|
|
- moshcode
|
|
- cli
|
|
supersedes:
|
|
superseded-by:
|
|
---
|
|
|
|
## 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.
|