cyberuni/cyberlegion
Agent session spawning, messaging, and dispatch — the Legion and its Legate.
cyberlegion
0.4.0
Minor Changes
-
72a0511:
unit close --keep-worktreereaps a unit's record, mailbox, pane and brief while leaving its worktree on disk, and reports the retained path (retainedin TOON,retainedWorktreein JSON) so a pool can detach the checkout and spawn the next unit into it with--cwd.Because nothing is deleted, the flag skips the dirty-worktree refusal — that check only ever protected uncommitted work from
git worktree remove. The primary-checkout refusal is unchanged: neither--forcenor--keep-worktreeoverrides it. -
33a64da: Add project references and project services with fenced, resolve-or-start ownership.
project register|list|showgive a repository one stable id shared by its default checkout and every linked worktree, resolvable by id, path, or unique name from anywhere.service resolve|acquire|bind|release|handoff|verify|startgive each named project service exactly one authoritative owner. Concurrent starts converge on one reservation; failed starts are retryable; a healthy owner is only displaced by an explicit, generation-checked force; handoff and recovery bump a fencing generation thatservice verifychecks.- A service endpoint is a durable mailbox independent of its owners (
mail inbox --owner <endpoint>), exempt from prune and refused byunit close, so pending mail survives owner replacement. A pane-less owner reports that its session control is not recoverable.
Patch Changes
-
9f2fe15: Ship the agent plugin inside the npm package, and bundle the CLI's dependencies into
dist/cli.mjs.plugin.json, the generated.claude-plugin/and.codex-plugin/manifests,.plugin/pins.json,skills/, andagents/now live in the package and are published with it.An installed agent plugin is a copy of a source checkout, not an npm install, so its directory has no reliable
node_modules.dist/cli.mjsnow inlinescommanderandcyber-muxand runs with nonode_modulespresent. The library entry keeps both external, so a consumer that also uses cyber-mux shares one copy.
0.3.2
Patch Changes
-
7cb6aaa: Take
cyber-mux0.5.0, whosereadanswers with{ text, truncated? }instead of a bare string.readUnitstill returns the trailing screen as a string, so nothing in this package's own surface changes — only the shipped code behind it. -
d28a531: Hardened
FileStoreagainst the concurrency and corruption risks a filesystem-only, daemonless, multi-process mailbox is actually exposed to (surveyed fromagmsgandfirstmate, two comparable tools, in.research/messaging-robustness/evidence.md):- Every store mutation (
putMessage,putAgent,putPaneIndex,writeBrief,setMainPane) now writes atomically — a sibling temp file, thenrenameSyncinto place — instead ofwriteFileSyncon the final path directly. A process crash mid-write, or a reader racing a writer, can no longer handJSON.parsea truncated file. - Added an advisory, mkdir-based lock (
store/lock.ts,Store#withLock) for genuine read-modify-write operations such assetMainPane's rebind. It reclaims a lock abandoned by a confirmed-dead process but never steals one a live process still holds. - A corrupt record file now throws a typed, file-named
CorruptRecordErrorinstead of a bareSyntaxError; missing and corrupt records stay distinguishable. - An agent or message id that would traverse outside its intended path segment (
../, an absolute path, an embedded path separator) is now rejected with a typedInvalidIdErroron write, rather than being joined into a filename unchanged.
- Every store mutation (
-
6d242f9: Restored the license file in the published tarball. Up to 0.3.1 this package was published from a monorepo whose root license file was picked up automatically at publish time. After the move to its own repository the root file is named in lower case, which that mechanism no longer matched, so the tarball would have shipped with no license text at all — only the
license: MITfield in the manifest. The package now carries its own copy, so the license travels with the code regardless of what the repository root is called. -
1536191:
unit claim/unit claim --clear(identity.ts'sclaimPresence/clearPresence) now serialize their load-mutate-save of a standing owner's presence pointer behind the store's advisory lock, matchingsetMainPane. Two concurrent claims (or a claim racing a clear) against the same standing owner previously read-modified-wrote the record unguarded; the last-claim-wins semantics are unchanged, but the transition itself is now atomic rather than racing two processes' independent reads against each other. -
f551433: Removed the package's
prepackscript. It ranpnpm build, and the build tool writes ANSI-colored progress lines to stdout — which corrupted the JSON thatnpm pack --jsonemits, so any tool inspecting the package that way got a parse error instead of a file list. Nothing is lost by dropping it: every path that packs or publishes this package already builds first. -
19e95be:
--versionnow reports the version from the package manifest instead of a hardcoded0.0.0. Every release since 0.0.1 shipped a CLI that reported0.0.0regardless of the version actually installed, which madenpx cyberlegion@<pin> --versionuseless for confirming which build was running. The test that covered--versionasserted the literal0.0.0, so it pinned the defect rather than catching it; it now asserts against the manifest and fails loud on a placeholder.
0.3.1
Patch Changes
-
e75f4a0: Fix stale
npxversion pins in the readmes.The documented pin-to-an-exact-version examples cited versions that were never published —
cyberplace@0.7.0andcyberfleet@0.1.0return E404 — so anyone copying them got a hard failure.cyberlegion@0.1.0resolved but was two minors behind. Each now cites the current published version.
0.3.0
Minor Changes
-
82b3c24: BREAKING — the spawn wake now carries the brief instruction, and the SessionStart hook no longer injects a brief.
unit spawn's first-turn doorbell used to be content-free ("Your brief is loaded in context — read it and begin work.") because the peer's brief was injected into its context by the peer's ownmail hookSessionStart branch. That made brief pickup depend on a hook firing in the child — in the child's harness, with the hub's hook correctly installed — none of which the spawning side can observe. When the chain broke, the peer was woken and told to read a brief that was not there.The doorbell is now the instruction itself, naming the brief's file path: read your brief at
<path>, then begin work. The brief is still written to its file and still never typed into the pane, so the wake stays one bounded line however large the brief is.What changes for consumers:
spawnAndWakeis the composed operation to reach for — it spawns the peer AND delivers its first turn, deriving the doorbell's brief path from the record it just wrote.spawnalone still opens a peer that sits idle, brief unread.mail hooknever emits a## Your briefsection, on the first call or any later one.- The
spawningagent status is retired —unit spawnregisters a peeractiveoutright, since nothing flips it any more. A record migrated from an older hub may still carryspawning; reads preserve it verbatim and never coerce or normalize it. (unit registerstill assertsactive, as it always has — that is an explicit re-registration, not a read normalizing a value.) --no-wakechanged meaning without changing its flag: previously no turn, brief auto-loaded; now no turn and brief unread on disk. A caller driving the first turn itself must convey the path.
Supersedes ADR-0027; see ADR-0032.
Note for readers of the 0.2.0 entry: it describes the first-turn doorbell as waking a peer whose "brief is injected into context by its own SessionStart hook". That was accurate when it shipped and is what this release retires — the hook injects no brief, and the wake names the file.
0.2.0
Minor Changes
-
211de72: BREAKING —
unit spawn --atnow defaults totabinstead ofpane:right, and the redundantwindowplacement value is removed.- Default placement is
tab. A spawned peer opens as a new tab in the caller's current window, opened without stealing focus (herdrtab create --no-focus, tmuxnew-window -d), so it no longer shrinks the caller's pane by splitting it side-by-side. Pass--at pane:rightexplicitly for the old behavior. - herdr now honors
tab. The herdr adapter previously mis-routedtabto a right-split pane; it now opens a real herdr tab viatab create. tmux already mappedtab→new-window. windowis removed from--at. It was tmux's local name for the Tab concept, redundant withtab; the allowed set is nowpane:right | pane:down | tab | workspace.--at windowis rejected. Placement vocabulary is aligned to the canonical Session › Workspace › Tab › Pane concepts (documented in the mux node README and the website architecture page).
- Default placement is
-
ddb1458: BREAKING — realign the CLI to its real architecture (ADR-0024). Command groups now mirror the mux/legion layering instead of the retired
surfacing/wakeconcept axis:identity <verb>andsession <verb>collapse into oneunitgroup:unit register,unit whoami,unit who(now carries apanefield and a"N units"aggregate — the oldsession listfolded in),unit prune,unit spawn,unit close,unit focus,unit nudge,unit read.identity owner→unit register --standing(bare, no--handle, lists the standing records).identity bind-main/identity main→attach(bare binds;--clearunbinds;--showreads the bound pane).admin doctor/admin mode→mux doctor/mux mode.admin installfolds intoinit(which owns hook installation directly);adminnow carries onlymigrate.
Hot-path top-level aliases (
who,send,inbox,spawn) and the bare-status default action are unchanged.mail,agent, anddispatchare unchanged. -
b863089: BREAKING — dissolve the CLI's
dispatchcommand group,Storeresult-slot (resultPath/writeResult/readResult), andrealizeSubagentInstruction/selectWakePathlibrary exports. A cold subagent now returns via the caller's own Task-result (its final returned message) instead of adispatch prep/collect result file, and a warm peer returns viamail awaiton a thread instead ofdispatch channel. Routing (warm-peer vs subagent vs run-inline) and the wake-matrix decision move out of the CLI into the Legate plugin's governance (dispatch-governance/subagent-backend-governance), which now composesunit spawn+mail await+agent resolvedirectly. Verdict-schema validation is dropped for now, to return later as a dedicatedmail --verdict-schemacapability. -
59b4154: BREAKING — the multiplexer fast-path environment variables are renamed, and the multiplexer layer is now the
cyber-muxpackage rather than a copy of it carried here.$CYBERLEGION_MUX/$CYBERLEGION_MUX_PANEbecome$CYBER_MUX/$CYBER_MUX_PANE.mux doctornow printsexport CYBER_MUX=<m> CYBER_MUX_PANE=<p>, andunit spawninjects the new names into the pane it opens. The old pair is still read, but only when the new pair is absent, so a session that was already running when you upgrade keeps its identity instead of falling back to a process-tree walk that answers for a different pane. That fallback is transitional and will be removed — re-pin anything that sets these names by hand.cyber-muxis now a dependency (pinned exact).packages/cyberlegion/src/console/had been a fork of that package's source since the extraction, and the two had drifted with the package moving and the copy standing still. Every improvement landed upstream since was invisible here.- A detected multiplexer that a unit record cannot name is now refused, by name, before anything
opens.
cyber-muxdrives four backends; a unit's pane pointer can only be stored for tmux and herdr. Previously such an environment reported that no multiplexer was running, which was a lie when you were plainly inside one; opening there would have stranded a live session thatunit prunecould never reap and no caller could reach. - A
--at pane:right/pane:downspawn now splits the calling pane. Both backends default to splitting the pane a human is looking at, which matches the caller's only by coincidence and diverges exactly when a program is driving — whichunit spawnalways is.
Library consumers: the re-exported multiplexer types are now
cyber-mux's (MuxAdapter,MuxTarget,MuxPlacement), andsendissendText. Note thatsendTexttypes text without submitting it;submit(target, text)is the equivalent of the oldsend. -
7ed73d0: Owner-mail doorbell rings the bound main pane only when it is focused.
- New focus probe.
SessionAdapter.isPaneFocusedreports whether the attached client is currently viewing a pane — tri-statefocused | not-focused | unknown. tmux readspane_active+window_active+session_attached; herdr reads the pane record's ownfocusedflag (pane get). A backend that cannot report focus, or a query that errors, answersunknown. - Doorbell focus gate. When a standing-owner message is delivered, the doorbell now skips ringing the bound main pane if that pane is positively not focused — the human has roamed off it, so ringing would wake a session nobody is watching. The report stays queued in the durable owner inbox and surfaces on the pane's next SessionStart pull; nothing is lost. When the pane is focused, or focus is
unknown, the ring proceeds as before (fail-open, no regression). A peer recipient's ring is never focus-gated, and a focus probe that errors never fails the send.
- New focus probe.
-
da6935a: Add
cyberlegion mail read <id> --ack— read and acknowledge a message in one atomic step. It prints the message body (asreaddoes) and acks it in the same call, so "receive and consume" is one round-trip instead of read-then-separately-ack. It is idempotent: it always prints the body and acks only when the message is still unread, so running it on an already-acked message prints the body and succeeds (acked: false) rather than erroring like a baremail ack. An unknown message id still errors, and it composes with--ownerfor the standing owner mailbox. Baremail read(no--ack) is unchanged — it stays the non-consuming peek. -
7598bf5:
unit who --reconcile(and top-levelwho --reconcile) now also adopts live-but-unregistered panes: any live pane in the current multiplexer running a detectable harness (claude | cursor | codex) with no matching record gets a minted record — pane bound to a new id, handle derived from the pane's cwd basename (id.slice(0, 6)when no cwd is reported), statusactive,lastSeennow — so a manually-opened or hook-failed pane becomes listable, mailable, and dispatchable. Panes with no detectable harness are never adopted (herdr reports the running agent; tmux does not, so tmux adoption is deferred), a bound pane — exited included — is never re-adopted or resurrected, andunit prunestays cull-only. -
0988b81: Add
unit who --reconcile(and top-levelwho --reconcile, mirroring--all) to live-probe the current multiplexer and mark any dead-pane recordexitedbefore listing.unit prunenow reconcile-culls the same way in addition to its existing per-record liveness and staleness checks. -
38756f9:
unit spawnnow delivers the spawned peer's first turn on a fresh paned session, so a spawned pod acts on its brief with no human nudge (issue #188).A paned agent boots to an idle prompt: its brief is injected into context by its own SessionStart hook, but the model takes no turn on its own (unlike a subagent, where the caller's Task call is the turn). So
unit spawnnow rings a best-effort first-turn doorbell over the same boot-race- aware submit-verify pathunit nudgeuses, exactly mirroringmail send's delivery ring: the worktree/session/registry record is the guaranteed effect and the ring is opportunistic on top.- Best-effort, never fails the spawn. A ring that never completes within the retry budget (the
harness never reaches its prompt) is surfaced as a stderr warning; the peer is still spawned. The
spawn result carries a
rungfield. --no-wakeopts out (mirroringmail send --no-nudge) for a caller that will drive the first turn itself.
This is mechanism, not routing — it completes the spawn, it does not select a backend — so it stays within the CLI's dumb-hands charter and fixes every paned caller at once (Operator, Pod, and the Legate's
channeldispatch strategy) with no persona change. - Best-effort, never fails the spawn. A ring that never completes within the retry budget (the
harness never reaches its prompt) is surfaced as a stderr warning; the peer is still spawned. The
spawn result carries a
-
59c951c:
unit spawn --at workspacenow opens the unit's own space under a short, human-identifiable name instead of the backend's default, so a session is findable by eye rather than by reading every workspace.The name is
<code>-<subject>, capped at 30 characters including the code.- Code — a NieR YoRHa unit class read off the brief's leading action word in one fixed order:
A2-when the action tears down or reverts, else9S-when it is read-only recon (investigate, audit, review, diagnose and kin), else2B-, the build-and-change class and the default when the leading word matches no action at all. The same brief always yields the same code. - Subject —
--handlewhen given, otherwise the brief's first non-empty line: lowercased, a recognized action word and any leading article dropped, non-alphanumerics collapsed to-, then whole words taken while they fit, so the name ends on a complete word. A brief that yields nothing usable falls back to the unit's own 6-character short id.
Only a
workspaceplacement is named. Apane:*ortabplacement opens into a space the caller is already in, so it carries no name and never renames that space.Examples:
--task "add a retry budget to the mail poller"→2B-retry-budget-to-the-mail;--task "prune the stale pane index"→A2-stale-pane-index;--task "diagnose the boot race" --handle scribe→9S-scribe. - Code — a NieR YoRHa unit class read off the brief's leading action word in one fixed order:
-
9955e97: Bind a standing owner's presence — the live unit standing in for it — and ring it on delivery.
A standing owner record (
unit register --standing) is durable but has no session of its own, so it cannot take a turn. It now carries a presence pointer:unit claim <handle>binds the caller's unit as that standing owner's presence — a per-record singleton, last-claim-wins, so the pointer moves as the principal moves between units.--clearunbinds,--showreads. An unknown handle fails loud and never auto-mints:--clearis forgiving about nothing being bound, never about the owner not existing, so a typo'd handle can't report a clear it never performed.- The claim is gated on spawn capability — it probes the multiplexer and throws when there is none (no panes ⇒ no dispatch ⇒ no presence), leaving the pointer untouched. The gate is a checkable precondition, never an introspective carve-out about what kind of agent is asking: a caller in a real pane can spawn and may hold the presence; a pane-less one cannot, however it was realized.
- A presence resolves live-only — a presence whose unit has exited reads as no presence bound, so the pointer can never name a reader that isn't there.
mail sendrings a bound live presence unconditionally, falling back to the focus-gated bound main pane when none is bound. A presence is an agent expected to take the turn, not a human whose attention is the scarce resource, so it inherits the existing peer rule rather than the human-attention focus gate — which matters precisely because a presence exists to act on a delivery while the human is away.
Additive: with no presence bound, the standing-owner ring and its focus gate behave exactly as before.
Patch Changes
-
9df2bf4: Address live units only, and fail fast on a dead pane.
A handle is reusable, so over time the dead units holding a name outnumber the live one.
resolveRecipient/resolveAgentmatched on handle without filtering status, so a name could resolve to an exited unit —mail sendthen reported "delivered" for an inbox with no reader. Handles (and worktree-branch refs) now resolve to live units only, and a name matching only exited units throws and lists them. An explicit id still addresses an exited unit.nudgenow probespaneExistsbefore sending: a gone pane and a booting one both read back empty, so a dead peer was being retried ten times and reported as "never took the turn" — the boot-race shape — instead of the real cause.
0.1.0
Minor Changes
- 667163c: Add a
workspacevalue tosession spawn --at(anddispatch channel --at) that opens a genuinely new workspace/session instead of a pane inside the caller's current one. Under herdr this also creates the new worktree viaherdr worktree create, so it lands properly nested under its source workspace instead of just adding to the caller's own pane count; under tmux it opens a new detached session. - 2758ea9: Add the owner mailbox read path.
mail hooknow surfaces every standing owner's unread mail (with bodies) into a root session's injected context under a distinct## Owner mail — <handle> (<N>)heading — read-only, so a message keeps re-surfacing until explicitly acked; a session that was legion-spawned (hasspawnedBy) never gets an owner section.mail inbox,mail read, andmail acktake a new--owner <handle>selector that targets a standing owner's mailbox instead of the caller's own;--owneron a handle that is not a standing record errors rather than falling back to reading a session's inbox as an owner mailbox. - 9e24386: Add
identity owner --handle <name>to mint a standing, session-independent owner inbox — a durable recipient identity with no live session, tmux pane, or harness. Standing records are exempt fromprunestaleness checks, are listed bywho, and take precedence over a live session when a handle is shared. Bareidentity ownerlists existing standing records.
Patch Changes
- 667163c: Fix
session spawn's default worktree checkout location: it now lives as a sibling of the primary checkout (<repo>.worktrees/legion-<id>) instead of nested inside<primary>/.agents/cyberlegion/worktrees/, which pollutedgit statusin the primary checkout and confused tooling that walks the tree recursively. The directory name uses the same 6-character id slice the peer'shandlealready defaults to.
0.0.1
Patch Changes
- 7c92d8e: Mark the CLI bin shim as executable so it runs directly after install.