harness-delivery¶
Provenance¶
- Source:
.spec/spexcode/spec-cli/sessions/injected-context/harness-delivery/spec.md - Source SHA-256:
086a36e1e1c9e05b9a1e8a7ef173826165f680e3d254d758d72ab664d2149021
harness-delivery¶
raw source¶
SpexCode must work for a user who installs it, runs spex init, and then launches their own
claude/codex — with no SpexCode process in that launch, so nothing can pass --append-system-prompt
or --settings. Therefore everything SpexCode contributes must arrive through files the harness
auto-discovers, and getting there must cost the user zero further steps. The same materialize also feeds
the dashboard path; the dashboard is one consumer, not a prerequisite — the spec engine never needs spex
serve running. Crucially the dashboard launcher uses the SAME delivery: it materializes into the new
worktree and then launches the agent PLAINLY — no --append-system-prompt, no --settings, no hiding of
CLAUDE.md. One path for both launch modes. Hiding CLAUDE.md (the old isolation) is gone precisely because it
also suppressed the agent's own MEMORY load; with the contract delivered by discovery instead, the agent
loads its CLAUDE.md + memory normally ([[sessions-core]] launch).
expanded spec¶
spex materialize is a pure function of the spec tree's [[surface]] nodes into the flat
artifacts each consumer reads cheaply. It is the base operation of harness adaptation — the
[[harness-adapter]] seam's render step: "adapting SpexCode to a harness" means exactly materializing
into that harness's auto-discovery points, so supporting a new harness is an adapter row this one
pass loops over, never a new delivery mechanism. That framing is how the verb should be explained
wherever it is defined (help, guides, onboarding docs): not a one-time setup — a re-runnable render
whose outputs are derived, untracked, and edited only via their sources. Its anchors are GIT-NATIVE only ([[commit-surgery]]): the explicit
verbs (spex init, spex materialize), session-worktree creation, and the planted pre-commit /
post-checkout / post-merge hooks — pre-commit's materialize is UNCONDITIONAL, so every materialize input
(.plugins content, the persisted spexcode.json/spexcode.local.json, a contract file's trackedness, a
toolchain update) is picked up no later than the next commit, and checkout/merge refresh what arrives from
other branches. Session creation is its own one-render transaction: it defers the checkout hook's best-effort
refresh, copies the local snapshot, then materializes once under the creation failure/recovery boundary. A
harness event is never a trigger — the old dispatcher content-hash gate is retired, and
.plugins edits are git-transactional (they take effect at the commit/checkout/merge that carries them,
like any other source). An environment with no planted hooks (CI, a cloud agent's fresh clone) runs
spex materialize in its setup step. It materializes into the harness targets
[[harness-select]] resolves from spexcode.json (default: every native harness), writing, idempotently and
scoped per project, for each SELECTED harness:
- the hook manifest (persistent; the [[hook-dispatch]] dispatcher reads it) — in the materialized tree's
own slot (
trees/<enc-worktree>/under [[runtime]]'sruntimeRoot), NOT the worktree; per-tree because the compile is a function of THAT tree's.plugins(one global slot let the last-materialized tree's hook set leak into every other tree's dispatch); - the contract — the
surface: systemplugin bodies (in name order), assembled and written as a<!-- spexcode:start -->…<!-- spexcode:end -->block into<repo>/AGENTS.md(Codex) +<repo>/CLAUDE.md(Claude). Those contract files are generated artifacts — exactly like the shims + skills below: regenerated per clone/launch, never tracked, resident per [[residence]]'s live kind detection (exclude when wholly ours; the content filter when host prose shares the file). Plugin bodies are the ONLY contract source: there is no per-project prose file folded in, so a project's repo-local notes cannot silently become part of every agent's contract — they live in the harness file's own block-outside region (untracked, per-clone), while anything that must reach EVERY agent is a plugin node. This replaces the launch-time--append-system-promptfor self-launch (at user-message level — the ceiling for a discovered file, not system-prompt level); - the shims — each adapter's
shim().contentwritten to itsshimFile(), whatever ARTIFACT that harness auto-discovers to wire events to the dispatcher: a thin hooks JSON for claude/codex (.claude/settings.json/.codex/hooks.json, one line per event), a generated event-bus plugin for opencode (.opencode/plugins/spexcode.ts— [[opencode-harness]]), or a generated extension for pi (.pi/extensions/spexcode.ts— [[pi-harness]]). materialize writes the bytes verbatim; the shape is the adapter's fact, not this pipeline's. The post-erase empty-dir sweep covers each artifact dir AND its parent (never a checkout root), since a harness may nest its shim a level below its home; - the skills — each
surface: skillbody as<skillDir>/<name>/SKILL.md(claude.claude/skills/, codex.codex/skills/— both ship the sameSKILL.mdprimitive), loaded on demand by the node'sdescription, not always-on like the contract. The dir is the adapter'sskillDir(proj); a harness with no skill primitive gets none. Exclude-hidden like the shims (generated, no user prose); - the sub-agents — each
surface: agentbody as<agentDir>/<name>.md(claude.claude/agents/), a harness-auto-discovered Agent-tool definition carrying the node'sdesc:load-trigger andtools:allowlist, spawned on demand, not always-on. Same shape as skills, one definition per harness: the dir is the adapter'sagentDir(proj); a harness with NO agent primitive (e.g. Codex today) gets none, exactly asskillDirnull skips skills. Exclude-hidden like the shims + skills (generated, no user prose) — so the formerly-committed.claude/agents/*.mddefinitions become generated artifacts joining the same managed block; - the Codex trust — a directory-trust + per-hook
trusted_hashwritten ADDITIVELY into the user's GLOBAL~/.codex/config.toml, scoped to this project path. The hash is computed deterministically (the pinned codex-rs algorithm), so a user-self-launched codex skips its trust prompts entirely. Trust is global-only by codex's security design (a repo cannot declare itself trusted) — the one necessary scoped global write; everything else is project-local. - the content-hash marker (same per-tree slot as the manifest), a diagnostic freshness record written before the final authority; the unconditional pre-commit materialize heals a partial pass.
- the dispatch-family allowlist (same slot), atomically renamed into place LAST. It is the sole success receipt consumed by dispatch, so a killed writer leaves the preceding successful selection intact.
The pass obeys a scoped forgetting law. One tree's semantic output is exactly its current policy. The current target map reconciles by identity stamp: it removes landing points absent from that map and writes only bytes that differ, so narrowing one tree removes its contract/shim/skills without touching a sibling while an identical second pass is an operational no-op (no delete/recreate churn or watcher event). Project-scoped hook/trust wiring is installation transport, not selection state: once a tree needs it, it may remain dormant until project-wide dematerialize/uninstall. The dispatch-family allowlist in each existing tree runtime slot is the single final publication of a successful pass, and gates that shared transport before admission or input handling; retaining the transport therefore cannot activate a harness that this tree did not select. Idempotence is the same-policy case, and project-wide dematerialize clears every accessible registered tree before shared substrate. A plugin target stays exclusive ([[plugin-harness]]) and its arbitrary bundle folders remain in the same per-tree ledger.
The pass returns a materialization receipt alongside its content hash: the manifest and the exact contract, shim, skill/agent, plugin-bundle, and trust paths asserted by this run. The receipt is populated at those writes, with trust paths supplied by the adapter that performed the global write, so callers can report the selected harness footprint without maintaining a second harness-artifact inventory.
Placement is harness-fact, not preference (verified): Codex auto-discovers ONLY the repo-root ./AGENTS.md
(never .codex/AGENTS.md); Claude discovers ./CLAUDE.md or ./.claude/CLAUDE.md. Ignore is projected with
the same ownership split. Checkout-invariant machine residue (spexcode.local.json, .worktrees/, legacy
.session, and installed shared root transport) stays in the common .git/info/exclude. Selection-dependent local
shims, bundles, skills/agents, and wholly-ours contract files are one managed block in that tree's working
.gitignore. [[content-filter]] keeps a tracked host .gitignore pristine in the index, leaves an untracked
host file honestly visible, and lets a wholly generated one ignore itself. Contract and ignore payloads live
under the current tree slot; one common filter driver derives that slot from the invoking Git toplevel plus %f, so linked
trees never overwrite one another's bytes and user global ignore configuration is untouched. The Codex trust
hash remains global and project-scoped, and is removed by project-wide dematerialize/uninstall rather than a
sibling's selection change.
Materialize reads and writes only the current tree slot and current per-tree filter payload. It does not import pre-slot ledgers, common ignore projections, or other retired-format receipts, and a normal pass never enumerates registered sibling worktrees. Shared filter transport is refreshed in place; project-wide teardown may still inspect registered trees when it is explicitly removing that shared transport. Older runtime state is not a supported materialize input and must be removed through an explicit reinstall/uninstall operation.
The net ideal path: npm install spexcode → spex init → the user launches their own claude/codex, zero
further operation, no global pollution beyond the scoped Codex trust. The contract files are SpexCode-owned
generated artifacts, so a clone never carries a stale committed copy — and the only tracked source they are
assembled from is the plugin tree, so what an agent carries is always exactly what the graph says.