Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ packages/
opencontext OpenContext reference implementation (resolver, scopes, bundles, adapters)
openontology OpenOntology reference engine (entities, claims, queries, change sets)
openprd OpenPRD reference implementation (numbered PRDs, lifecycle, task bridge)
openfleet OpenFleet reference implementation (record, ledger, sysop verbs, Claude Code hooks)
logicsrc-mcp @profullstack/logicsrc-mcp standards MCP server
sdk SDK contract types and helpers
tui terminal UI
Expand Down Expand Up @@ -165,6 +166,7 @@ logicsrc vault … # OpenCreds (also `opencreds`)
logicsrc prd … # OpenPRD
logicsrc ontology … # OpenOntology
logicsrc context … # OpenContext (also `opencontext`)
logicsrc fleet … # OpenFleet: open, cap, tree, stop, log, hooks install
logicsrc openmcp … # OpenMCP: relays, find, call, add, probe, serve (also `openmcp`)
logicsrc openspec … # import, export, change; any other word is OpenSpec.dev's own CLI (init, list, validate, archive, show)
logicsrc mcp # the LogicSRC MCP server over stdio (also `logicsrc-mcp`)
Expand Down
16 changes: 10 additions & 6 deletions apps/logicsrc-web/src/app/openfleet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ const STEPS: Array<[string, string]> = [
["1. A human opens a fleet", "Or does nothing, and gets the implicit fleet user@host with a ceiling of depth 1 and no fleet-level approvals: each session the human starts by hand runs its subtree under the approvals it was started with. Only a human opens a fleet or sets its ceiling. A process that carries OPENFLEET_MEMBER is an agent, and the tool refuses it."],
["2. An agent spawns a swarm", "It mints a swarm id, writes swarm.spawn to the fleet's ledger with the task, the ceiling it narrowed to, and one piece per member with the paths that member owns, as data. It never widens the ceiling it inherited."],
["3. The starter writes the record, the session claims it", "Before each member starts: one JSON file under ~/.openfleet, its path in OPENFLEET_RECORD, copies of fleet, member and swarm in three more variables. The session that starts under an unclaimed record claims it by writing member.start. One that inherits an already claimed record derives its own child record, so the leak becomes a parent pointer."],
["4. Everyone reads the same tree", "claude agents, moshcode herd ps and logicsrc fleet would fold the ledger and the records into one tree: fleet, swarms, members, state, spend against budget, and a mark on every member running with approvals bypassed."],
["4. Everyone reads the same tree", "logicsrc fleet tree and moshcode fleet tree fold the ledger and the records into one tree: fleet, swarms, members, state, spend against budget, and a mark on every member running with approvals bypassed. The engine rosters, claude agents and moshcode herd ps, add liveness and the members that have no record."],
["5. The sysop stops or caps as one unit", "stop on a swarm ends every member through its own engine and writes one swarm.end. cap narrows a fleet or a running swarm, and anything already above the new ceiling is stopped. log says afterwards what happened and who did it."]
];

const VERBS: Array<[string, string, string]> = [
["open", "sysop only", "mint a fleet, name the human, set the ceiling, write fleet.open"],
["cap", "sysop only", "set a fleet's ceiling or narrow a running swarm's; stop whatever is now above it"],
["tree", "anyone", "the fleet as a tree, from the ledger and the records, with liveness and recordless members from the engine rosters it can read"],
["tree", "anyone", "the fleet as a tree, from the ledger and the records, with liveness and recordless members from the engine rosters it can read; run by the sysop it also stops what is past its deadline or over its budget"],
["stop", "within reach", "end a member, a swarm, or a whole fleet as one unit; an agent reaches only what it spawned"],
["log", "anyone", "the ledger for a fleet, a swarm or a member: what happened, who did it, what it cost"]
];
Expand Down Expand Up @@ -197,9 +197,13 @@ export default function OpenFleetPage(): ReactNode {
<div className="section-head">
<h2>Five verbs</h2>
<p>
Proposed for <code style={mono}>logicsrc fleet</code>, for Claude Code&apos;s{" "}
<code style={mono}>claude agents</code> and for <code style={mono}>moshcode fleet</code>,
each able to stop the members its own engine runs. None ships yet.
<code style={mono}>logicsrc fleet</code> ships them in{" "}
<code style={mono}>@logicsrc/openfleet</code> 0.1.0 (logicsrc CLI 0.3.0) and{" "}
<code style={mono}>moshcode fleet</code> in moshcode 0.99.0, each able to stop the
members its own engine runs. Claude Code joins through{" "}
<code style={mono}>logicsrc fleet hooks install</code>, which makes every session a
recorded member; its own <code style={mono}>claude agents</code> view does not offer the
verbs yet.
</p>
</div>
<table style={table}>
Expand Down Expand Up @@ -250,7 +254,7 @@ export default function OpenFleetPage(): ReactNode {
<li>
<Link href="/docs/openfleet">Specification</Link>: the terms, the record, the
environment, claiming and deriving, the ledger&apos;s eight events, five verbs, fifteen
rules, the worked example, and what Claude Code and moshcode would each add
rules, the worked example, and what Claude Code and moshcode each add
</li>
<li>
<Link href="/asdlc">ASDLC</Link>: a swarm is one fan-out made durable;{" "}
Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ accounts
social
email
credentials
fleet
openspec
plugins
tui
Expand Down
18 changes: 9 additions & 9 deletions docs/openfleet.md

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"apps/*"
],
"scripts": {
"build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/agentad run build && npm --workspace @logicsrc/ans run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/agentstack run build && npm --workspace @logicsrc/agentswarm run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/plugin-agentbbs run build && npm --workspace @logicsrc/plugin-agentgit run build && npm --workspace @logicsrc/plugin-agentmail run build && npm --workspace @logicsrc/plugin-credential-sharing run build && npm --workspace @logicsrc/openontology run build && npm --workspace @logicsrc/openprd run build && npm --workspace @logicsrc/opencontext run build && npm --workspace @logicsrc/opencreds run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build",
"build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/agentad run build && npm --workspace @logicsrc/ans run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/agentstack run build && npm --workspace @logicsrc/agentswarm run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/plugin-agentbbs run build && npm --workspace @logicsrc/plugin-agentgit run build && npm --workspace @logicsrc/plugin-agentmail run build && npm --workspace @logicsrc/plugin-credential-sharing run build && npm --workspace @logicsrc/openontology run build && npm --workspace @logicsrc/openprd run build && npm --workspace @logicsrc/opencontext run build && npm --workspace @logicsrc/opencreds run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/openfleet run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build",
"start": "npm --workspace @logicsrc/web run start",
"test": "npm run test --workspaces --if-present",
"check": "npm run build && npm run test",
"schemas:validate": "npm --workspace @logicsrc/validators run validate:fixtures",
"test:contract": "npm --workspace @logicsrc/commandboard-api run test:contract && npm --workspace @logicsrc/web run test:contract",
"test:e2e": "npm --workspace @logicsrc/commandboard-web run test:e2e && npm --workspace @logicsrc/web run test:e2e",
"build:cli": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/plugin-agentbbs run build && npm --workspace @logicsrc/plugin-credential-sharing run build && npm --workspace @logicsrc/openontology run build && npm --workspace @logicsrc/openprd run build && npm --workspace @logicsrc/opencontext run build && npm --workspace @logicsrc/opencreds run build && npm --workspace @logicsrc/tui run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/cli run build"
"build:cli": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/plugin-agentbbs run build && npm --workspace @logicsrc/plugin-credential-sharing run build && npm --workspace @logicsrc/openontology run build && npm --workspace @logicsrc/openprd run build && npm --workspace @logicsrc/opencontext run build && npm --workspace @logicsrc/opencreds run build && npm --workspace @logicsrc/tui run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/openfleet run build && npm --workspace @logicsrc/cli run build"
},
"devDependencies": {
"@types/node": "^24.10.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logicsrc/cli",
"version": "0.2.1",
"version": "0.3.0",
"description": "LogicSRC CLI: every LogicSRC standard and tool as one command.",
"type": "module",
"main": "./dist/index.js",
Expand All @@ -18,6 +18,7 @@
"@logicsrc/account-core": "file:../account-core",
"@logicsrc/opencontext": "file:../opencontext",
"@logicsrc/opencreds": "file:../opencreds",
"@logicsrc/openfleet": "file:../openfleet",
"@logicsrc/openmcp": "^0.3.1",
"@logicsrc/openontology": "file:../openontology",
"@logicsrc/openprd": "file:../openprd",
Expand Down
99 changes: 99 additions & 0 deletions packages/cli/src/fleet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { Deps } from "@logicsrc/openfleet/commands";
import { registerFleetCommands } from "./fleet.js";

/** A program shaped like the real one: positional options on, no process.exit. */
function program(): Command {
const p = new Command();
p.name("logicsrc").enablePositionalOptions().exitOverride();
return p;
}

const OPENFLEET_KEYS = ["OPENFLEET_HOME", "OPENFLEET_RECORD", "OPENFLEET_FLEET", "OPENFLEET_MEMBER", "OPENFLEET_SWARM"];

describe("logicsrc fleet", () => {
let home: string;
let saved: Record<string, string | undefined>;
let out: string[];
let err: string[];
let deps: Partial<Deps>;

beforeEach(() => {
home = mkdtempSync(join(tmpdir(), "logicsrc-fleet-"));
saved = Object.fromEntries(OPENFLEET_KEYS.map((key) => [key, process.env[key]]));
out = [];
err = [];
deps = {
env: { OPENFLEET_HOME: home, HOME: home },
now: () => new Date("2026-09-13T06:00:00Z"),
exec: async () => ({ code: 0, stdout: "", stderr: "" }),
kill: () => undefined,
rosters: {},
stdin: async () => "",
write: (line) => out.push(line),
error: (line) => err.push(line),
stdout: (text) => out.push(text),
stderr: (text) => err.push(text),
host: "dev",
user: "anthony",
};
});

afterEach(() => {
for (const key of OPENFLEET_KEYS) {
if (saved[key] === undefined) delete process.env[key];
else process.env[key] = saved[key];
}
rmSync(home, { recursive: true, force: true });
process.exitCode = 0;
});

it("registers the group with the five verbs and the hook verbs", () => {
const p = program();
registerFleetCommands(p, deps);
const fleet = p.commands.find((command) => command.name() === "fleet");
expect(fleet).toBeDefined();
expect(fleet!.commands.map((command) => command.name()).sort()).toEqual(["cap", "hook", "hooks", "log", "open", "stop", "tree"]);
expect(fleet!.description()).toContain("OpenFleet");
});

it("opens a fleet, then shows it in the tree and the log, over the injected home", async () => {
const p = program();
registerFleetCommands(p, deps);
await p.parseAsync(["node", "logicsrc", "fleet", "open", "--approvals", "bypass", "--depth", "2", "--json"]);
expect(process.exitCode).toBe(0);
expect(JSON.parse(out[0])).toMatchObject({ fleet: "fleet-20260913", sysop: "anthony@dev", ceiling: { approvals: "bypass", depth: 2, hosts: ["dev"] } });
expect(readFileSync(join(home, "current"), "utf8")).toBe("fleet-20260913\n");

const tree = program();
registerFleetCommands(tree, deps);
await tree.parseAsync(["node", "logicsrc", "fleet", "tree"]);
expect(out[1]).toBe("fleet-20260913 (fleet, sysop anthony@dev, approvals bypass, depth 2, hosts dev)");

const log = program();
registerFleetCommands(log, deps);
await log.parseAsync(["node", "logicsrc", "fleet", "log", "--json"]);
expect(JSON.parse(out[2])).toMatchObject({ event: "fleet.open", by: "sysop", fleet: "fleet-20260913", host: "dev" });
});

it("refuses the sysop's verbs with exit 4 when the process is an agent", async () => {
const p = program();
registerFleetCommands(p, { ...deps, env: { ...deps.env, OPENFLEET_MEMBER: "create-two-0541-1" } });
await p.parseAsync(["node", "logicsrc", "fleet", "open"]);
expect(process.exitCode).toBe(4);
expect(err[0]).toContain("OPENFLEET_MEMBER=create-two-0541-1");
expect(out).toEqual([]);
});

it("inspects the hooks of a settings file given by flag, never the real one", async () => {
const file = join(home, "settings.json");
const p = program();
registerFleetCommands(p, deps);
await p.parseAsync(["node", "logicsrc", "fleet", "hooks", "status", "--settings-file", file, "--json"]);
expect(JSON.parse(out[0])).toMatchObject({ file, present: false, installed: false });
});
});
26 changes: 26 additions & 0 deletions packages/cli/src/fleet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Command } from "commander";
import { registerOpenFleetCommands, type Deps } from "@logicsrc/openfleet/commands";

/**
* `logicsrc fleet …`
*
* The commands themselves live in `@logicsrc/openfleet`, the same package that
* holds the record, the ledger and the Claude Code hooks, so the sysop tool
* and the engine side read the same files through the same code. The spec
* treats the five verbs (open, cap, tree, stop, log) and their refusals as a
* conformance surface; keeping them in one place is how they stay one contract.
*
* `deps` is injectable so the umbrella tests drive the group with a fake
* clock, environment, roster and process runner, and never touch
* `~/.openfleet` or a real engine.
*/
export function registerFleetCommands(program: Command, deps: Partial<Deps> = {}): void {
const fleet = program
.command("fleet")
.description(
"OpenFleet: agents under a human. open, cap, tree, stop and log over $OPENFLEET_HOME, " +
"plus the Claude Code hooks (`hooks install`) that make every session a recorded member.",
);

registerOpenFleetCommands(fleet, deps);
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { parsePositiveInteger } from "./numeric-options.js";
import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./openspec.js";
import { registerOpenContextCommands } from "./context.js";
import { registerOpenCredsCommands } from "./creds.js";
import { registerFleetCommands } from "./fleet.js";
import { registerOntologyCommands } from "./ontology.js";
import { registerPrdCommands } from "./prd.js";
import { registerOpenMcpCommands } from "./openmcp.js";
Expand Down Expand Up @@ -1057,6 +1058,7 @@ registerOpenContextCommands(program);
registerOpenCredsCommands(program);
registerOntologyCommands(program);
registerPrdCommands(program);
registerFleetCommands(program);
registerOpenMcpCommands(program);
registerMcpCommands(program);
// Every other word under `logicsrc openspec` is OpenSpec.dev's own CLI.
Expand Down
2 changes: 1 addition & 1 deletion packages/logicsrc-mcp/src/standards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ describe("MCP: OpenPRD", () => {
it("reports the next free id and the allowed lifecycle moves", async () => {
const client = await connect();
// Asserted against the live prd/ directory, so this advances with every PRD added.
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0008");
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0009");

const moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } });
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };
Expand Down
Loading
Loading