v1.4.0
Running several agents against one game (see TEAM-MODE.md). A Spawn identity is a property of a project directory, not of a session, so one git worktree per agent gives you a team with no connection registry and no agent runtime in this server.
Added
- Team mode, off unless
SPAWN_TEAM=1or a ledger already exists. Adds six tools and leaves the solo tool list at 27.spawn_team_init: create the shared ledger if absent and register this worktree under a label. The ledger lives in the repo's common.git/spawn-team/, so every worktree resolves to the same path with no configuration, it is scoped to one game, and it cannot be committed by accident.SPAWN_TEAM_DIRoverrides for agents that are not worktrees of one repo. Warns when a worktree has no key of its own, when its key came from the MCP config rather than its.env, and when its variant differs from the rest of the team.spawn_team_status: the whole team at a glance: every agent, how far behind head each local rail is, whose worktree has unresolved conflict receipts, open claims, recent pushes, and head vs published.spawn_team_claim/spawn_team_release: ownership ofgame.jsonkey paths (entities.player) andscripts/globs (scripts/hud/**). Everything exceptscripts/**is claimed by key path, because that is wherespawn_initputs the whole spec; a pattern that looks like a file path outsidescripts/is rejected rather than silently never matching.spawn_team_add: stands up a new agent in one call: writes its variant, trades its one-time key for its own token, scaffolds the project, and registers it. Called before the worktree exists it returns the exactgit worktree addcommand instead of running it, because this server executes no subprocesses. It refuses to proceed without a distinct key for the new agent: sharing one token would make every agent indistinguishable on the version rail.spawn_team_brief: a ready-to-paste opening prompt for one builder or the whole team, covering who it is, its worktree, what it owns, what teammates own, whether it is behind head, and the working rules. Text out; the LLM decides what to do with it. This is the dispatch affordance, and it is deliberately not an agent runtime.
- Pushes serialise and rebase in team mode.
spawn_pushtakes a ledger-wide lock, and from inside it "behind head" can only mean a teammate landed a push since your last sync, so it pulls first. A clean rebase costs nothing and the 409 never happens; a rebase that collides stops the push with your work intact and the colliding paths named.force: trueskips it, being a deliberate whole-replace. The lock's stale window is longer than the HTTP timeout so a slow push cannot have its lock stolen mid-flight. - Claim warnings on push, and attribution instead of "someone else". Changes are diffed against the base rails, so exactly this agent's own edits get checked against other agents' claims, and collisions are reported alongside the successful push rather than blocking it. Every push is logged to
pushes.jsonl, so a 409 names the teammate who took the version and what they touched, andspawn_latestsays whose push you just pulled. - The session latch.
spawn_push,spawn_latest applyLocal,spawn_revoke, andspawn_play_openbind to the first project directory they see and refuse a second, because identity, the.spawn/base-versionrail, and the single Chromium session all belong to one directory. Read-only tools still inspect any worktree, and provisioning (spawn_bootstrap,spawn_init) stays unlatched so a new worktree can be set up from anywhere. - A globally configured
SPAWN_PROJECT_DIRis refused in team mode, with the reasoning in the error rather than a bare rejection: it would resolve every session to one.env, so every agent would push to the same rail as the same connection while appearing to work in its own worktree, and nothing would error. An explicitprojectDirargument is unambiguous and never refused.
Fixed
- A pull no longer silently discards your
game.jsonedits.spawn_latestused to overwrite the file wholesale, with no receipt and no mention in the sync summary, even thoughspawn_initputs the entire spec in it. Script sources were the only content with a merge story. There is now a.spawn/base-game.jsonrail and a three-way merge by key path: keys only you moved stay, keys only upstream moved fast-forward, and a key both sides moved keeps your value, lands in the reportedconflictslist as a dotted path, and writes agame.json.theirsreceipt that blocksspawn_pushuntil you resolve it. Same contract the script receipts already had. - The project
.envnow wins over the process env forSPAWN_AGENT_KEYandSPAWN_VARIANT_ID; process env remains the fallback for a project that carries none. A key in the MCP config used to override every project, which madespawn_bootstraplook like it had done nothing and pinned every checkout to one connection.spawn_statusreports where each credential came from.
Notes
- Existing projects have no
.spawn/base-game.json, so the first pull after upgrading keeps the old whole-replace behaviour, copies the previousgame.jsonto.spawn/replaced-game.jsonwhen it would drop anything, and establishes the rail. Pulls merge from then on.spawn_statusreportshasSpecRail. world/*.jsonoverlays are still not reconciled: they re-apply over pulled content at compile time. Disjoint overlays compose fine, overlapping ones do not. In practicespawn_initputs the whole spec ingame.jsonand leavesworld/empty, so the key-path merge covers the common case.- The shared
.gitis located by reading.gitdirectly (a directory in a main checkout, agitdir:pointer pluscommondirin a worktree) rather than by runninggit rev-parse. This server still spawns no subprocesses. - Tool count is unchanged at 27 solo, and 33 with team mode on.
spawn_init's scaffolding is now shared withspawn_team_add, so provisioning a teammate's worktree cannot drift from provisioning your own.