Skip to content

OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks - #185

Merged
ralyodio merged 4 commits into
masterfrom
openfleet-impl
Sep 13, 2026
Merged

ralyodio merged 4 commits into
masterfrom
openfleet-impl

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

OpenFleet reference implementation: @logicsrc/openfleet, logicsrc fleet, and Claude Code hooks

Implements the OpenFleet specification shipped in #179. The spec described what Claude Code, moshcode and a logicsrc fleet tool would each add; this PR builds the logicsrc side, and the Claude Code side through hooks. The moshcode side lands in moshcoder/moshcode 0.99.0.

@logicsrc/openfleet 0.1.0 (new workspace package): the record and ledger library. Home under ~/.openfleet (OPENFLEET_HOME), records at fleets/<fleet>/members/<member>.json, one append-only ledger.jsonl per fleet per host merged by at, the eight events, the ceiling merge (fleet base, swarm narrowings root to leaf, swarm caps last, never widening), the claim rule (an unclaimed record is claimed by the first session that writes its member.start; a claimed one makes a child derive), root and orphan records, and a fold that renders the landing page's tree from the files alone, with engine rosters joined for liveness.

logicsrc fleet (CLI 0.2.1 to 0.3.0): open and cap for the sysop alone (refused with exit 4 from any process carrying OPENFLEET_MEMBER), tree, stop (through each member's own engine: claude stop, moshcode herd kill, tmux, or a pid; nested swarms first; one swarm.end) and log (--since, --member, --swarm, --json). tree also enforces deadlines and budgets.

Claude Code through hooks (logicsrc fleet hooks install|remove|status): merges five hook entries into ~/.claude/settings.json without touching anyone else's (matched by command text, removed only ours, write then rename). SessionStart claims or derives the record named by OPENFLEET_RECORD, else writes a root record, exports the five variables to the session's tools, and tells the model its member id and piece. The first prompt writes member.start with approvals taken from the live permission mode, or refuses with ceiling.refuse and exit 2. PreToolUse denies Edit and Write outside the member's owned paths. Stop and SessionEnd write member.end with the summary. Verified with real claude -p sessions: a hand-written unclaimed record was claimed, all five variables reached the Bash tool, a write outside the owned paths was denied while the owned one went through, and member.end carried the final message.

Two limits found while probing, now stated in the spec: a background job dispatched from claude agents receives no launcher environment, so it becomes a root member unless a launcher writes its record; and env-file exports reach the member's tools but not later hooks, so hooks key on the session id through sessions/<session_id>.json.

Also: docs and the landing page now say what ships instead of "none ships yet"; PRD 0008; README and docs/cli.md mention the group.

Verification. @logicsrc/openfleet vitest suite, @logicsrc/cli suite, tsc --noEmit on the CLI, npm run test:contract, next build, an end-to-end run of every verb against a temp home, two live claude -p hook gates, and a cross-tool interop run in which moshcode 0.99.0 wrote a swarm and logicsrc fleet tree rendered it. Three adversarial review rounds; every blocker and major fixed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV

ralyodio and others added 2 commits September 13, 2026 10:06
…icsrc 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
…s, 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
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

22 finding(s)

HIGH/CRITICAL: 3 | MEDIUM: 10 | LOW: 9

Severity Rule Location
HIGH secret-private-key plugins/credential-sharing/src/providers/ssh.ts:129
HIGH js-host-header-trust apps/commandboard-web/server.js:27
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
MEDIUM js-unescaped-html-sink apps/commandboard-web/src/main.ts:19
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/[[...slug]]/page.tsx:71
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/layout.tsx:88
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/pricing/page.tsx:46
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/components/breadcrumbs.tsx:43
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/components/site-shell.tsx:34
MEDIUM redos-nested-quantifier packages/opencontext/src/ids.ts:10
MEDIUM redos-nested-quantifier packages/opencontext/src/ids.ts:11
MEDIUM js-timing-unsafe-mac-compare packages/opencontext/src/validate.ts:208
MEDIUM redos-nested-quantifier packages/openontology/src/ids.ts:20
LOW secret-generic-credential apps/pwa/test/appbar.test.mjs:10
LOW secret-aws-access-key packages/opencontext/src/permissions.test.ts:260
LOW secret-private-key packages/opencontext/src/permissions.test.ts:261
LOW secret-stripe-key packages/opencontext/src/permissions.test.ts:262
LOW secret-aws-access-key packages/opencontext/src/security.test.ts:228
LOW secret-private-key plugins/credential-sharing/src/providers/ssh.test.ts:7
LOW secret-database-url plugins/credential-sharing/src/rekey.test.ts:33
LOW secret-openai-key plugins/credential-sharing/src/vault-encryption.test.ts:21
LOW secret-generic-credential plugins/credential-sharing/src/vault-encryption.test.ts:21

Snippets are redacted; ThreatCrush never prints matched credential material.

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
@ralyodio
ralyodio merged commit 9ae7ad8 into master Sep 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant