# Split config rendering + per-project seat tooling — design

audience: AI coding agents first. slug: `split-config-rendering`
source request: owner 2026-08-15 — k3s vs workstation instruction differences (hooks, AGENTS.md/CLAUDE.md, testing doctrine) need a clean seam; owner proposed shared base + @k3s.md/@workstation.md includes and asked for a better way; agreed direction: render-at-ship-time from one source. Plus: "it will also require the correct tooling in k3s per project."
status context: PROPOSED — design recorded; implementation not started. Companion caveat already landed: k3s-contained agents run tests locally (docs/plans/2026-08-15-landq-gate-offload.md, owner caveat section).

## Decisions (agreed 2026-08-15)

1. **One canonical source, rendered per target — NEVER runtime includes.** Include syntax is Claude-Code-only; codex and pi read AGENTS.md as plain text. The canonical doc keeps the shared ~95% once, with environment-tagged blocks (`<!-- env:workstation -->` / `<!-- env:k3s -->` fenced sections). The existing shippers render the final flat file for their destination: deploy-local for the laptop, fleet convergence (push_claude_home) for boxes/seats. Every CLI sees a complete ordinary file.
2. **NEVER twin files** (k3s.md + workstation.md as peers) — the rot mode of split configs is the silently diverging twin, not the split.
3. **Hooks are environment-aware, not environment-forked.** One hook codebase shipped identically everywhere; behavior branches at runtime on the environmental fact (containment marker / registry-listed host — same detection the local-testing caveat mandates). No per-target hook copies, ever.
4. **Freshness seam = render stamps.** The renderer embeds the canonical source hash into every rendered file (tool-shims snapshot pattern); convergence re-renders on change; an audit compares stamps to detect drift. No new sync machinery — the shippers that exist are the seam.

## Per-project seat tooling (owner addition)

A k3s seat running project X needs X's toolchain, not a guess:
- **Each enrolled project declares its tooling** in a manifest the seat provisioner consumes — extend the existing `devtools.json` vendor pattern (modules/buildbox) to a per-project scope: project repo carries (or the fleet registry maps) `project-tools` entries (runtime + version: pnpm/bun/node, cargo, php, python, plus project-pinned CLIs). NEVER bake every project's toolchain into the base seat image — image stays generic; project layer applies at seat provision/claim time, idempotent, version-pinned (the mise-drift lesson: unpinned managers resolve differently per box).
- Missing declaration → seat provisioning FAILS VISIBLY for that project ("project X declares no tooling") rather than launching an agent into a container where its first build dies mysteriously.
- Same manifest serves the workstation (it already has the tools; the manifest becomes the audit source) — one declaration, two consumers.

## Non-goals

No new daemon, no config service. Rendering is a build step inside existing shippers. Per-run tool overrides are out of scope (routing D6 covers per-run model/cli choice, not toolchains).

## Testing (when implemented)

Renderer: tagged-block selection both targets, stamp embedding, unknown tag fails closed. Convergence: rendered files land on boxes with correct env blocks (assert a k3s-only line absent on workstation and vice versa). Tooling: seat provision with declared manifest installs pinned versions; undeclared project refused visibly.
