跳转至

runtime

The per-session GLOBAL store dir — every harness-written runtime artifact under ~/.spexcode, keyed by session_id and grouped per-project, so the worktree stays 100% clean.

raw source

A session has runtime bookkeeping the harness scribbles for it — the lifecycle record, the originating prompt, a queued session's launch prompt, the launch script, the recorded inter-agent comms. None of it is the agent's spec/code work, and putting any of it in the worktree was the root of two problems: it polluted the tree the agent commits, and it forced a 1:1 worktree↔session identity (a path key), so two agents in one folder would clobber. So the runtime lives OUTSIDE the worktree entirely, in a per-user GLOBAL store keyed by SpexCode's governed session_id — the worktree is left pristine (zero SpexCode files), and each agent gets its own record even when several share a folder. Claude Code's harness id equals that governed id; Codex mints its own thread id, so the record stores that separately as harness_session_id once the Codex SessionStart hook reports it.

expanded spec

The store mirrors Claude's ~/.claude/projects/<enc>/ shape: a per-session dir at

<SPEXCODE_HOME or ~/.spexcode>/projects/<enc(project-root)>/sessions/<session_id>/

<enc> encodes the project root with Claude's scheme (path separators → -); the project root is the MAIN checkout — dirname of the shared git common dir — which resolves identically from main or any linked worktree, so the board (at main) and a hook (in a worktree) land on the same dir. Every per-session artifact is a file in that dir:

file written by
session.json readRecord / writeRecord — the structured lifecycle record (state): state, governed, worktree_path, node, branch, createdAt, harness_session_id, …
prompt the originating human ask (launch)
launch the deferred launch prompt of a still-queued session (launch)
launch.sh the whole launch invocation (launchScript, run via bash <abs path>)
spec-checked / spec-of-file-seen the spec-first / spec-of-file once-per-session sentinel + ledger
comms.ndjson recorded inter-agent talk (comms-edge)

layout.ts owns the seam — the one place that knows where the store sits: spexcodeHome() (the SPEXCODE_HOME override → ~/.spexcode), encodeProject() / projectKey(), runtimeRoot() (the per-PROJECT tier: projects/<enc>), sessionsRoot() (its sessions/ child — the board's enumeration dir), sessionStoreDir(id), sessionRecordPath(id), sessionArtifactPath(id, name), plus readRawRecord / listSessionIds for the board. The store has TWO tiers under one per-project dir: the per-session dirs above, AND the per-project runtime that hook-dispatch / harness-delivery render — the hook manifest, the gate's content-hash marker, the materialize lock — plus the Codex app-server socket/pid/log/lock when Codex is launched through SpexCode. Those project-level artifacts live in runtimeRoot() too, NOT the worktree. So the worktree holds ZERO SpexCode-rendered runtime; the only in-tree artifacts are the harness-discovered contract files (CLAUDE.md/ AGENTS.md block) + shims, which MUST sit in-tree for the harness to find them. sessions.ts writes through storeDir(id) (mkdir-and-return) and the full typed readRecord / writeRecord; the shell hooks reimplement the SAME path scheme in bash (the one cross-language mirror — a change to the seam must update both, noted at the layout.ts helpers). Because the only in-tree SpexCode artifacts are gitignored (the materialize shims/skills) or tracked-and-committed (the contract block in CLAUDE.md/AGENTS.md), none shows as an uncommitted change, so the Stop-gate's dirty count needs no runtime filtering, and session.json is written one-field-per-line with every key present so the hot-path hook edits it with sed, not jq (state).

session.json writes are by governed session_id (the agent/hook resolves SPEXCODE_SESSION_ID first, then falls back to the harness env var or payload for self-launched agents), so they never depend on cwd beyond the project key. close removes the worktree AND sweeps the whole per-session store dir; exit keeps both, so an offline session is still on the board and --resume-able. Codex's project app-server is not swept by closing one session because several Codex sessions and several spexcode serve processes in the same project may be using the same control plane; routing is by harness_session_id, not by socket ownership.

This is a CLEAN cut from the old per-worktree .session/ layout — there is no compat shim. An in-flight session launched under the old backend keeps its worktree .session/ until it drains; the new backend simply doesn't read it (those sessions relaunch into the global store). The old .gitignore entries for .session* are inert and may be dropped.