mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 12:54:03 +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>
126 lines
5.5 KiB
TypeScript
126 lines
5.5 KiB
TypeScript
/**
|
|
* Shared fixtures for the tests: a temp home, the implicit fleet of the
|
|
* spec's worked example (`anthony@dev`), and the ledger and records that
|
|
* morning should have produced. Excluded from the build.
|
|
*/
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { append, implicitFleet, writeRecord, type Env, type ImplicitFleet } from "./store.js";
|
|
import type { FleetRecord } from "./types.js";
|
|
|
|
export const DEV: ImplicitFleet = implicitFleet({ user: "anthony", host: "dev" });
|
|
export const FLEET = DEV.id;
|
|
|
|
export function tempHome(): string {
|
|
return mkdtempSync(join(tmpdir(), "openfleet-test-"));
|
|
}
|
|
|
|
export function cleanup(dir: string): void {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
const OPENFLEET_KEYS = ["OPENFLEET_HOME", "OPENFLEET_RECORD", "OPENFLEET_FLEET", "OPENFLEET_MEMBER", "OPENFLEET_SWARM"] as const;
|
|
|
|
/** Snapshot the OPENFLEET_* keys of process.env and hand back a restore, so one test's environment never leaks into another file's. */
|
|
export function snapshotEnv(): () => void {
|
|
const saved: Record<string, string | undefined> = {};
|
|
for (const key of OPENFLEET_KEYS) saved[key] = process.env[key];
|
|
return () => {
|
|
for (const key of OPENFLEET_KEYS) {
|
|
if (saved[key] === undefined) delete process.env[key];
|
|
else process.env[key] = saved[key];
|
|
}
|
|
};
|
|
}
|
|
|
|
/** A clean environment for a test: a home and nothing else OpenFleet knows. */
|
|
export function envFor(home: string, extra: Env = {}): Env {
|
|
return { OPENFLEET_HOME: home, HOME: home, ...extra };
|
|
}
|
|
|
|
export const ROOT: FleetRecord = {
|
|
openfleet: "0.1",
|
|
fleet: FLEET,
|
|
sysop: FLEET,
|
|
member: "460a4502",
|
|
depth: 0,
|
|
engine: "claude-code",
|
|
host: "dev",
|
|
cwd: "/home/anthony",
|
|
started: "2026-09-13T04:55:00Z",
|
|
approvals: "bypass",
|
|
ceiling: { approvals: "bypass", depth: 1, hosts: ["dev"] },
|
|
};
|
|
|
|
export const PIECE_CEILING = { approvals: "bypass" as const, depth: 1, fan_out: 4, hosts: ["dev"], until: "2026-09-13T06:11:01Z" };
|
|
|
|
export const PIECE_1: FleetRecord = {
|
|
openfleet: "0.1",
|
|
fleet: FLEET,
|
|
sysop: FLEET,
|
|
member: "create-two-0541-1",
|
|
parent: "460a4502",
|
|
swarm: "create-two-0541",
|
|
task: "create two ...",
|
|
piece: { title: "create hello.sh bash", owns: ["hello.sh"] },
|
|
depth: 1,
|
|
engine: "claude-code",
|
|
host: "dev",
|
|
cwd: "/home/anthony/.claude/jobs/460a4502/tmp/swarm-live",
|
|
started: "2026-09-13T05:41:01Z",
|
|
approvals: "bypass",
|
|
ceiling: { ...PIECE_CEILING },
|
|
};
|
|
|
|
export const PIECE_2: FleetRecord = {
|
|
...PIECE_1,
|
|
member: "create-two-0541-2",
|
|
piece: { title: "create bye.sh bash", owns: ["bye.sh"] },
|
|
engine: "moshcode/claude",
|
|
session: "create-two-0541-2",
|
|
};
|
|
|
|
export const PLANNER = "9f1c2d3e-0000-4000-8000-000000000001";
|
|
|
|
/**
|
|
* The morning up to the moment the two pieces are written and unclaimed:
|
|
* the root, its planner swarm of one (done), and `swarm.spawn` for
|
|
* create-two-0541 with both records on disk.
|
|
*/
|
|
export function seedWorkedExample(home: string): void {
|
|
writeRecord(home, ROOT);
|
|
append(home, FLEET, { at: "2026-09-13T04:55:01Z", event: "member.start", by: "460a4502", member: "460a4502", session: "460a4502", depth: 0, engine: "claude-code", cwd: "/home/anthony", approvals: "bypass" }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:40:50Z", event: "swarm.spawn", by: "460a4502", swarm: "460a4502-1", task: 'claude -p "Split the task below into at most 4 ..."', ceiling: {}, pieces: [{ member: PLANNER }] }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:40:51Z", event: "member.start", by: PLANNER, member: PLANNER, session: "4242", swarm: "460a4502-1", parent: "460a4502", depth: 1, engine: "claude-p", approvals: "bypass" }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:40:59Z", event: "member.end", by: PLANNER, member: PLANNER, state: "done" }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:40:59Z", event: "swarm.end", by: PLANNER, swarm: "460a4502-1", state: "done" }, { host: "dev" });
|
|
append(
|
|
home,
|
|
FLEET,
|
|
{
|
|
at: "2026-09-13T05:41:01Z",
|
|
event: "swarm.spawn",
|
|
by: "460a4502",
|
|
swarm: "create-two-0541",
|
|
task: "create two ...",
|
|
ceiling: { fan_out: 4, until: "2026-09-13T06:11:01Z" },
|
|
pieces: [
|
|
{ member: "create-two-0541-1", title: "create hello.sh bash", owns: ["hello.sh"] },
|
|
{ member: "create-two-0541-2", title: "create bye.sh bash", owns: ["bye.sh"] },
|
|
],
|
|
},
|
|
{ host: "dev" },
|
|
);
|
|
writeRecord(home, PIECE_1);
|
|
writeRecord(home, PIECE_2);
|
|
}
|
|
|
|
/** The rest of the morning: both pieces claimed, the first done. */
|
|
export function seedWorkedExampleToEnd(home: string): void {
|
|
seedWorkedExample(home);
|
|
append(home, FLEET, { at: "2026-09-13T05:41:12Z", event: "member.start", by: "create-two-0541-1", member: "create-two-0541-1", session: "172ffd83", swarm: "create-two-0541", parent: "460a4502", depth: 1, engine: "claude-code", cwd: PIECE_1.cwd, approvals: "bypass", piece: PIECE_1.piece }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:41:13Z", event: "member.start", by: "create-two-0541-2", member: "create-two-0541-2", session: "create-two-0541-2", swarm: "create-two-0541", parent: "460a4502", depth: 1, engine: "moshcode/claude", cwd: PIECE_2.cwd, approvals: "bypass", piece: PIECE_2.piece }, { host: "dev" });
|
|
append(home, FLEET, { at: "2026-09-13T05:41:36Z", event: "member.end", by: "create-two-0541-1", member: "create-two-0541-1", state: "done", summary: "Created hello.sh, mode -rwxrwxr-x, prints hello." }, { host: "dev" });
|
|
}
|