Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ec59694
feat(core,cli,manager,delivery): declarative command kernel — dispatc…
davidfarah2003 Jul 3, 2026
c45f1c0
fix(cli): --help reaches rawArgs commands; only __-internal skips the…
davidfarah2003 Jul 3, 2026
4b3fb6d
feat(cli,manager,workspace): one launch grammar — spawn --detach abso…
davidfarah2003 Jul 3, 2026
ffed39d
fix(cli,manager): --name is a real identity override in detached mode…
davidfarah2003 Jul 3, 2026
60bc02c
fix(cli): migrate the server-resolution live smoke with the moved res…
davidfarah2003 Jul 3, 2026
3bb3f68
fix(cli): attach hint shows the configured detach key, not hardcoded …
davidfarah2003 Jul 3, 2026
78bf3c8
test(manager): start-overrides fake ep learns the #159B readiness roster
davidfarah2003 Jul 3, 2026
83bf026
fix(cli): spawn --detach outlives the manager's readiness window (#15…
davidfarah2003 Jul 3, 2026
d00d09d
test(manager): attach smokes follow attach-client into @cotal-ai/cli
davidfarah2003 Jul 3, 2026
1e10664
feat(cli,delivery): setup is configure-only; go is gone; feedback int…
davidfarah2003 Jul 3, 2026
0d5c8c0
fix(cli): up's teardown covers the manager it now starts; control-pla…
davidfarah2003 Jul 3, 2026
5149769
fix(cli): up -f hands the launch spec to the ONE control-plane manager
davidfarah2003 Jul 3, 2026
d245b77
test(cli): poll for graceful teardown in the up live smokes
davidfarah2003 Jul 3, 2026
cb1f99e
fix(cli): up -f never claims a launch the control plane didn't deliver
davidfarah2003 Jul 3, 2026
9669543
feat(cli,workspace): operator-installed CLI extensions — cotal ext ad…
davidfarah2003 Jul 3, 2026
51e792d
fix(cli): ext add fails ext-vs-ext name collisions; deterministic spe…
davidfarah2003 Jul 3, 2026
fd9f979
feat(cli,workspace,web,demo): dogfood cotal ext — web and demo move o…
davidfarah2003 Jul 3, 2026
d874aaf
fix(cli): help groups render in an explicit order, not registration o…
davidfarah2003 Jul 3, 2026
3767474
Merge pull request #170 from Cotal-AI/worktree-cli-rework-s1
davidfarah2003 Jul 3, 2026
47b9cd0
Merge pull request #171 from Cotal-AI/worktree-cli-rework-s2a
davidfarah2003 Jul 3, 2026
ef28e51
Merge pull request #173 from Cotal-AI/worktree-cli-rework-s2b
davidfarah2003 Jul 3, 2026
66d30d0
Merge pull request #175 from Cotal-AI/worktree-cli-rework-s3
davidfarah2003 Jul 3, 2026
b86e00c
Merge pull request #177 from Cotal-AI/worktree-cli-rework-s4
davidfarah2003 Jul 3, 2026
9e70b1e
docs(agents): wave coordination structure — channels, routing, spawn,…
mattwilkinsonn Jul 3, 2026
3f34c8e
docs(agents): harden replay + bring-up citations
mattwilkinsonn Jul 3, 2026
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ jobs:
# spawn-from-anywhere registry/auth-path e2e (smoke:spawn-from-anywhere:live). The last one is
# gated here AND in `pnpm check` deliberately: CLI smoke files are outside package typecheck, so
# a dynamic `await import()` boundary regression in a live smoke escapes tsc — only running it catches it.
# nats-server installed above.
run: pnpm smoke:core-boundary && pnpm smoke:preflight && pnpm smoke:server-resolution:live && pnpm smoke:spawn-from-anywhere:live
# smoke:up-stack:live / smoke:up-manifest:live e2e the full `up --detach` stack (broker +
# delivery + manager, real `cotal ps`) and the one-manager-carries-the-launch invariant of
# `up -f` (the singleton-lease race regression). smoke:ext:live e2es the operator-installed
# extension lifecycle (cotal ext) through the real binary. nats-server installed above.
run: pnpm smoke:core-boundary && pnpm smoke:preflight && pnpm smoke:server-resolution:live && pnpm smoke:spawn-from-anywhere:live && pnpm smoke:spawn-detach:live && pnpm smoke:setup-pure:live && pnpm smoke:up-stack:live && pnpm smoke:up-manifest:live && pnpm smoke:ext:live && pnpm smoke:dogfood:live

- name: Manager lifecycle e2e (real broker + real agents)
# Real end-to-end for #159 Part B: the startAgent presence-race (started via real presence / failed
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ with no silent fallback.
- **`@cotal-ai/tmux`** (`extensions/tmux`): the tmux integration: a driver over the tmux CLI
plus a self-registering `tmux` Runtime and `TerminalLayout` provider.
- **`@cotal-ai/cli`** (`implementations/cli`): the mesh CLI: `up`, `join`, `watch`, `console`,
`web`, `spawn`, `mint`, `channels`, `history`.
`spawn`, `mint`, `channels`, `history`, `ext` (operator-installed command extensions).
- **`cotal-web`** (`implementations/web`) / **`@cotal-ai/demo`** (`implementations/demo`): the
browser dashboard and the demo trace generator as `cotal ext`-installable extension packages —
they peer-depend on core + workspace (linked to the binary's copies at add time) and
self-register their command.
- **`@cotal-ai/manager`** (`implementations/manager`): the agent supervisor: spawns and manages
nodes via a pluggable Runtime (`pty` built-in; `tmux` and `cmux` via extensions), with `start`/`stop`/`ps`/`attach` and
a WebSocket attach endpoint.
Expand Down
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,42 +77,44 @@ JetStream has run in production for years. We didn't invent the hard parts.

## Quick start

One command brings up a local mesh, the web dashboard, your agent's connector, and a quick
guided demo:
One command configures your machine — plugin, personas, connector — then a second brings up the
local mesh:

```bash
npx cotal-ai setup --full # needs Node 20+; NATS ships bundled
npx cotal-ai setup # configure: needs Node 20+; NATS ships bundled — installs the plugin, seeds personas
npx cotal-ai up # start the mesh + delivery daemon + a detached manager
```

One command, guided setup. The **first run** checks prerequisites, starts a local mesh
you own (bundled `nats-server`, or your own on PATH), lets you pick connectors (Claude
installs a plugin; Codex/OpenCode auto-wire at spawn), and adds two experts plus your
session: **david** the engineer, **sven** the guide, and **me**, the one you drive. It
then offers a Claude-driven demo with david and sven helping. If a step fails, it hands
you to an interactive Claude with the failure context, then retries.
Guided setup, **configure-only**. The **first run** checks prerequisites (locates
`nats-server` — bundled, or your own on PATH), lets you pick connectors (Claude installs a
plugin; Codex/OpenCode auto-wire at spawn), and adds two experts plus your session: **david**
the engineer, **sven** the guide, and **me**, the one you drive. It **launches nothing** and
prints the commands to start things. If a step fails, it hands you to an interactive Claude
with the failure context, then retries.
Comment on lines +88 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether a "codex" connector exists anywhere in the repo.
rg -ni 'codex' --type=ts --type=md -g '!**/node_modules/**'

Repository: sealedsecurity/Cotal

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## README excerpt\n'
sed -n '80,100p' README.md

printf '\n## Connector/setup docs references\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'Claude|OpenCode|Codex|connector|picker|found = \{ claude, opencode \}' docs README.md .

printf '\n## Likely setup implementation files\n'
fd -i 'setup-internals.md|runFirstRun|first run|setup' .

Repository: sealedsecurity/Cotal

Length of output: 50379


Remove the stray “Codex” connector mention
README.md:89-90 only lists Claude and OpenCode as connectors; this should say “OpenCode auto-wires at spawn.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 88 - 93, The README setup description still mentions
a Codex connector, but the supported connector list should only reference Claude
and OpenCode. Update the setup blurb around the guided first-run flow so it says
Claude installs a plugin and OpenCode auto-wires at spawn, and remove any Codex
mention from that sentence while keeping the rest of the onboarding text
unchanged.

Source: Coding guidelines


The mesh is **JWT-authed** by default (sender authenticity + per-agent ACLs, with the
server-side delivery daemon for the durable backstop); pass `cotal setup --open` for a
frictionless open, loopback-only, live-only mesh (no auth, no daemon).
Then bring the mesh up with `cotal up`. It is **JWT-authed** by default (sender authenticity +
per-agent ACLs, with the server-side delivery daemon for the durable backstop) and starts a
detached **manager** alongside, so `cotal spawn --detach` works right after; pass `cotal up --open`
for a frictionless open, loopback-only, live-only mesh (no auth, no daemon).

> [!NOTE]
> **Want each teammate in its own terminal?** Run `setup` inside a **[cmux](https://cmux.com)** pane and Cotal opens a
> tab per agent, or inside a **[tmux](https://github.com/tmux/tmux/wiki)** session and it opens a window per agent. Otherwise they run in the background
> on the same mesh, watched with `cotal console` or the dashboard.
> **Want each teammate in its own terminal?** Run the manager with `cotal supervise --runtime cmux`
> (a **[cmux](https://cmux.com)** tab per agent) or `--runtime tmux` (a **[tmux](https://github.com/tmux/tmux/wiki)** window per agent). Otherwise they run in the
> background on the same mesh, watched with `cotal console` or the dashboard.

When you're all set up, here are the commands you'll use most:

```bash
cotal up --detach # start the mesh (+ delivery daemon + manager)
cotal spawn me # drive a session: talk to your agent; it messages and spawns peers
cotal spawn david # bring in an expert teammate (also: sven, the guide)
cotal console # watch the mesh live: presence, channels, messages
cotal web # the same, in the browser
cotal go # resume later
cotal web # the same, in the browser (once: cotal ext add cotal-web)
cotal down # stop everything
```

> [!TIP]
> **Using a coding agent?** `cotal setup` brings up a **manager**, an endpoint that lets your agent
> **Using a coding agent?** `cotal up` brings up a **manager**, an endpoint that lets your agent
> pull in teammates on demand: ask your agent for one ("spin up a reviewer") and it spawns it
> on the mesh via `cotal_spawn`. See [docs/claude-code-integration.md](docs/claude-code-integration.md).

Expand Down
21 changes: 14 additions & 7 deletions bin/cotal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
* (@cotal-ai/manager). The root just picks which surfaces to pull in; `runCli` resolves
* whatever registered. A new surface (another connector, a control client …) is one more import line.
*/
import { runCli } from "@cotal-ai/cli"; // self-registers up / down / join / watch / spawn / console / setup
import "@cotal-ai/manager"; // self-registers supervise / cmux / start / stop / ps / attach
// NOTE: registration order across these imports is NOT guaranteed (tsx's entry interop can
// evaluate the smaller daemon graphs first) — display order is a non-goal here; `help` ranks
// its groups explicitly (GROUP_ORDER in @cotal-ai/cli).
import { runCli } from "@cotal-ai/cli"; // self-registers the base surface incl. spawn (foreground + --detach) / stop / ps / attach
import "@cotal-ai/manager"; // self-registers `supervise` — the agent-supervisor daemon
import "@cotal-ai/delivery"; // self-registers `deliver` — the server-side Plane-3 delivery daemon
import "@cotal-ai/connector-claude-code"; // registers the `claude` connector that spawn / start resolve
import "@cotal-ai/connector-claude-code"; // registers the `claude` connector that spawn resolves
import "@cotal-ai/connector-opencode"; // registers the `opencode` connector (native in-process plugin)
import "@cotal-ai/connector-hermes"; // registers the `hermes` connector (Nous Research gateway as a mesh peer)
import "@cotal-ai/cmux"; // opt into the cmux integration — registers the `cmux` runtime + TerminalLayout providers
Expand All @@ -26,10 +29,14 @@ process.stdout.on("error", (e: NodeJS.ErrnoException) => {
throw e;
});

// The manager's default agent type is "cotal"; make it a real Claude coder so a bare
// cotal_spawn / `cotal start --name x` (no --agent) brings up a Claude Code session.
// The manager's default agent type is "cotal" (StartAgentOpts.agent ?? "cotal"); make it a real
// Claude coder so a bare cotal_spawn / `cotal spawn --detach <persona>` (no --agent) brings up a
// Claude Code session. Revisited at the start→spawn merge (stage 2a): still needed — the default
// rides the MANAGER side of the control plane, not the removed CLI verb.
registry.register({ ...claudeConnector, name: "cotal" });

// Bare `cotal` prints help; explicit `cotal setup` runs guided setup.
// Bare `cotal` prints help; explicit `cotal setup` runs guided setup. The published binary is
// the ONE composition root that loads operator-installed extensions (`cotal ext add …`) —
// library roots keep the explicit-import model.
const argv = process.argv.slice(2);
await runCli(registry, argv);
await runCli(registry, argv, { extensions: true });
4 changes: 3 additions & 1 deletion bin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"@cotal-ai/connector-opencode": "workspace:*",
"@cotal-ai/core": "workspace:*",
"@cotal-ai/delivery": "workspace:*",
"@cotal-ai/manager": "workspace:*"
"@cotal-ai/manager": "workspace:*",
"@cotal-ai/workspace": "workspace:*",
"@cotal-ai/connector-core": "workspace:*"
},
"files": [
"dist",
Expand Down
156 changes: 156 additions & 0 deletions bin/smoke/dogfood-live.smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* DOGFOOD LIVE e2e (stage 4): the REAL `cotal-web` + `@cotal-ai/demo` packages installed through
* the REAL `cotal ext` mechanism and exercised against a REAL mesh — the full operator journey:
*
* A. `web` left the core surface (unknown command; the built-in count shrank).
* B. `cotal ext add ./implementations/web` — the first real MULTI-PEER extension: BOTH
* @cotal-ai/core and @cotal-ai/workspace get linked to this binary's copies (provenance
* proves it); help + <TAB> list `web` from the manifest cache.
* C. `cotal up --detach` (JWT auth) then `cotal web`: the dashboard serves /, /app.js (packaged
* assets) and /api/meta over HTTP against the live mesh — the admin-mint + purger-pre-mint
* path, exactly as an operator runs it.
* D. `@cotal-ai/demo` on an OPEN mesh: `cotal demo --once` replays the scripted trace; the
* traffic LANDS in real channel history.
* E. `ext remove` both; `web` is unknown again.
*
* Needs dist built (the packages install per their `files: ["dist"]`), `nats-server` + npm on
* PATH. Sandboxes COTAL_HOME/XDG_CONFIG_HOME + a temp root; kills only its own pids.
* Run: pnpm smoke:dogfood:live
*/
import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

const AUTH_PORT = 14361;
const OPEN_PORT = 14363;
const WEB_PORT = 14371;
const REPO = resolve(import.meta.dirname, "..", "..");

const sandbox = mkdtempSync(join(tmpdir(), "cotal-dogfood-"));
const configDir = join(sandbox, "xdg");
const home = join(sandbox, "home");
const root = join(sandbox, "proj");
for (const d of [configDir, home, root]) mkdirSync(d, { recursive: true });

let pass = 0;
const ok = (name: string, cond: boolean, extra?: unknown) => {
if (!cond) throw new Error(`FAIL: ${name}${extra !== undefined ? ` — ${JSON.stringify(extra)}` : ""}`);
pass++;
console.log(` ✓ ${name}`);
};

const env = { ...process.env, XDG_CONFIG_HOME: configDir, COTAL_HOME: home };
const realNode = spawnSync("which", ["node"], { encoding: "utf8" }).stdout.trim();
const tsxCli = join(REPO, "node_modules", "tsx", "dist", "cli.mjs");
const binCotal = join(REPO, "bin", "cotal.ts");
const cotal = (args: string[], timeout = 180_000) =>
spawnSync(realNode, [tsxCli, binCotal, ...args], { encoding: "utf8", env, cwd: root, timeout });
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const alive = (pid: number) => {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
};

let webChild: ReturnType<typeof spawn> | undefined;
const ownPids: number[] = [];
try {
// -- A: web left the core surface ---------------------------------------------------------------
{
const r = cotal(["web", "--help"]);
ok("`cotal web` is unknown before the extension is installed", r.status === 1 && /unknown command: web/.test(r.stderr), r.stderr.slice(0, 150));
}

// -- B: install the REAL cotal-web package (multi-peer link) --------------------------------------
{
const r = cotal(["ext", "add", join(REPO, "implementations", "web")]);
ok("ext add cotal-web exits 0", r.status === 0, (r.stdout + r.stderr).slice(-500));
ok("core peer linked to this binary's copy", /→ wrote @cotal-ai\/core link/.test(r.stderr), r.stderr.slice(-400));
ok("workspace peer linked to this binary's copy", /→ wrote @cotal-ai\/workspace link/.test(r.stderr), r.stderr.slice(-400));
ok("the add names the contributed `web` command", /web/.test(r.stdout), r.stdout);
const help = cotal(["--help"]);
ok("--help lists web (from the cache, no import)", help.status === 0 && /web\s+.*dashboard/.test(help.stdout), help.stdout.slice(-400));
const comp = cotal(["__complete", "web", "--"]);
ok("<TAB> offers web's cached flags", comp.status === 0 && /--port/.test(comp.stdout), comp.stdout);
}

// -- C: the dashboard runs against a real JWT-authed mesh -----------------------------------------
{
const up = cotal(["up", "--detach", "--server", `nats://127.0.0.1:${AUTH_PORT}`]);
ok("up --detach (auth) exits 0", up.status === 0, (up.stdout + up.stderr).slice(-400));
for (const f of ["nats.pid", "delivery.pid", "manager.pid"] as const) {
const pid = Number(readFileSync(join(root, ".cotal", f), "utf8").trim());
ownPids.push(pid);
}
webChild = spawn(realNode, [tsxCli, binCotal, "web", "--port", String(WEB_PORT), "--no-open"], {
env,
cwd: root,
stdio: ["ignore", "pipe", "pipe"],
});
let webErr = "";
webChild.stderr?.on("data", (d: Buffer) => (webErr += d.toString()));
let page: Response | undefined;
for (let i = 0; i < 30 && !page; i++) {
await sleep(1000);
page = await fetch(`http://127.0.0.1:${WEB_PORT}/`).catch(() => undefined);
}
ok("the extension `web` command serves the dashboard", page?.status === 200, webErr.slice(-400));
const html = await page!.text();
ok("dashboard page is the real asset (packaged via files:[dist])", /<html|<!doctype/i.test(html) && html.length > 200, html.slice(0, 120));
const appJs = await fetch(`http://127.0.0.1:${WEB_PORT}/app.js`);
ok("static asset /app.js serves", appJs.status === 200);
const meta = (await (await fetch(`http://127.0.0.1:${WEB_PORT}/api/meta`)).json()) as { space?: string };
ok("live /api/meta answers with the mesh's space", typeof meta.space === "string" && meta.space.length > 0, meta);
webChild.kill("SIGTERM");
const down = cotal(["down"]);
ok("down stops the auth mesh", down.status === 0, down.stderr.slice(-200));
}

// -- D: the demo trace generator on an OPEN mesh ---------------------------------------------------
{
const up = cotal(["up", "--detach", "--open", "--server", `nats://127.0.0.1:${OPEN_PORT}`]);
ok("up --detach --open exits 0", up.status === 0, (up.stdout + up.stderr).slice(-400));
const add = cotal(["ext", "add", join(REPO, "implementations", "demo")]);
ok("ext add @cotal-ai/demo exits 0 (private package, by path)", add.status === 0, (add.stdout + add.stderr).slice(-400));
const r = cotal(["demo", "--once", "--interval", "25", "--server", `nats://127.0.0.1:${OPEN_PORT}`], 240_000);
ok("cotal demo --once completes a full trace", r.status === 0, (r.stdout + r.stderr).slice(-400));
ok("demo announced its agents", /agents live in space/.test(r.stdout), r.stdout.slice(0, 300));
// The trace really landed on the mesh: read channel history through the wire protocol.
const { CotalEndpoint } = await import("@cotal-ai/core");
const ep = new CotalEndpoint({
space: "main",
servers: `nats://127.0.0.1:${OPEN_PORT}`,
channels: [],
consume: false,
registerPresence: false,
watchPresence: false,
card: { name: "probe", kind: "endpoint" },
});
ep.on("error", () => {});
await ep.start();
const hist = await ep.channelHistory("general", { limit: 50 });
await ep.stop();
ok("demo traffic landed in real channel history", hist.length > 0, hist.length);
const down = cotal(["down"]);
ok("down stops the open mesh", down.status === 0, down.stderr.slice(-200));
}

// -- E: remove both; the surface shrinks back ------------------------------------------------------
{
ok("ext remove cotal-web exits 0", cotal(["ext", "remove", "cotal-web"]).status === 0);
ok("ext remove @cotal-ai/demo exits 0", cotal(["ext", "remove", "@cotal-ai/demo"]).status === 0);
const r = cotal(["web"]);
ok("`cotal web` is unknown again after remove", r.status === 1 && /unknown command: web/.test(r.stderr), r.stderr.slice(0, 150));
}

console.log(`\nDOGFOOD LIVE SMOKE OK ✅ (${pass} checks)`);
} finally {
webChild?.kill("SIGKILL");
spawnSync(realNode, [tsxCli, binCotal, "down"], { encoding: "utf8", env, cwd: root });
for (const p of ownPids) if (alive(p)) { try { process.kill(p, "SIGTERM"); } catch { /* gone */ } }
rmSync(sandbox, { recursive: true, force: true });
}
Loading