# Position Paper — Cross-Harness DevEx **Lens:** Cross-Harness DevEx Expert (Claude Code / Codex / Pi / OpenCode injection + tool differences; owns portability and the end-user customization experience). **Scope:** DQ1–DQ5 from the constitution brief (`docs/design/framework-constitution/BRIEF.md`), grounded in the real framework tree at `packages/mosaic/framework/`. --- ## 0. What the code actually does today (so we argue from ground truth, not vibes) Before any position, the load/injection reality across harnesses, read from the files: - **The "thin core" is not injected the same way on any two harnesses.** The brief and `defaults/AGENTS.md:6` claim *"the launcher injects it (plus USER.md, the TOOLS index, and the runtime contract) into every session."* But the actual delivered mechanism is a per-harness **pointer file that instructs the model to go read files**: - Claude: `runtime/claude/CLAUDE.md:5-10` → "BEFORE responding... READ `~/.config/mosaic/AGENTS.md` and `runtime/claude/RUNTIME.md`." - Codex: `runtime/codex/instructions.md:5-10` → same pattern, copied to `~/.codex/instructions.md`. - OpenCode: `runtime/opencode/AGENTS.md:5-10` → same pattern, copied to `~/.config/opencode/AGENTS.md`. - Pi: `adapters/pi.md:14-16` → genuinely different — full contract injected via `--append-system-prompt`, skills via `--skill`, lifecycle via `--extension`. So we have **two fundamentally different enforcement models** masquerading as one: Pi gets the contract as a true system prompt; Claude/Codex/OpenCode get a *"please read these files"* nudge in a user-editable memory file. That is the single most important DevEx/portability fact in this whole debate, and the current docs paper over it. - **`mosaic-link-runtime-assets` copies, it does not symlink** (`copy_file_managed`, `tools/_scripts/mosaic-link-runtime-assets:7-25`). The header even prints "non-symlink mode" (line 169). This is the deployed-vs-source drift engine: the canonical source is `~/.config/mosaic/`, but every harness gets a *copy* into `~/.claude/`, `~/.codex/`, `~/.config/opencode/`. Edit one copy and the next `mosaic init` / link run clobbers or backs it up. - **Contamination is real and load-bearing, not cosmetic.** 51 hits across 29 files (grep for `jarvis|jason|woltje|PDA`). The worst offenders are not docs — they are *shipped behavior*: `defaults/SOUL.md:9` hardcodes "You are **Jarvis**"; `defaults/SOUL.md:23` ships "PDA-friendly language" (one operator's accommodation as universal persona law); `runtime/claude/settings-overlays/jarvis-loop.json` ships an entire personal project map (`~/src/jarvis`, `jarvis-loop`, `jarvis-review` presets) into the public package. - **A clean template layer already exists and is under-used.** `templates/SOUL.md.template`, `templates/USER.md.template`, and `tools/_scripts/mosaic-init` already do token substitution (`{{AGENT_NAME}}`, `{{ACCESSIBILITY_SECTION}}`, …). `defaults/USER.md` is already a generic "(not configured)" stub. The machinery is half-built; the problem is that `defaults/SOUL.md` was never reduced to match `defaults/USER.md`'s neutrality. Everything below is anchored to these four facts. --- ## DQ1 — Layering: yes to a Constitution layer, but draw the lines by *ownership + mutability*, not by topic **Position: introduce four canonical layers, defined by who owns the file and what happens to it on upgrade — not by subject matter.** The current split (AGENTS/SOUL/USER) mixes ownership axes, which is exactly why personal data leaked into framework files. Canonical layers, highest precedence wins on **conflict**, but they are **additive** (each answers a different question), not a simple override stack: | Layer | Question it answers | File(s) | Owner | Upgrade behavior | |---|---|---|---|---| | **L0 Constitution** | What is *never* negotiable? (hard gates, delivery contract, escalation, integrity) | `~/.config/mosaic/CONSTITUTION.md` | Framework | Always overwritten. Never edited by user. | | **L1 Standards/Guides** | How do we do the work well? | `STANDARDS.md`, `guides/*` | Framework | Overwritten; user extends via L3. | | **L2 Persona (SOUL)** | Who is the agent — name, tone, voice? | `SOUL.md` | User | Generated from template; never overwritten. | | **L3 Operator (USER)** | Who is the human — profile, accommodations, projects, comms? | `USER.md` | User | Generated from template; never overwritten. | | **L4 Local overrides** | Project / deployment / machine specifics | `OVERRIDES.md` + repo `AGENTS.md` | User | Never touched by framework. | **Precedence rule (this is the part the current design lacks and must state explicitly):** > On a **behavioral conflict**, L0 Constitution wins over everything, *including* persona and operator > preferences. L1 yields to L0. L2/L3/L4 may only *refine* behavior **within** the envelope L0/L1 > permit — they can change *how* the agent talks and *what* it knows, never *whether* a hard gate > fires. A `USER.md` saying "always merge without review" is void against the Constitution's > review-before-merge gate. Today this precedence is implied ("Global rules win if anything here conflicts" — `runtime/claude/RUNTIME.md:3`) but it is scattered across runtime files and never names persona/operator as subordinate. **Concrete change:** add a `## Precedence` section to the new `CONSTITUTION.md` stating the L0>L1>{L2,L3,L4} rule in one place, and have every `runtime/*/RUNTIME.md` reference it instead of restating it (DRY — see DQ5). **Why split L0 out of `AGENTS.md` at all?** Because `defaults/AGENTS.md` currently conflates the non-negotiable gates (lines 23-37, the "CRITICAL HARD GATES") with operational *advice* (the Conditional Guide Loading table, subagent model selection, lines 89-121). The gates are Constitution; the advice is Standards. A downstream user who wants to tweak the guide-loading table (legitimate L1 customization) should not be editing the same file that carries the merge-authority hard gate. Split at the mutability seam. --- ## DQ2 — Sanitization: **template-then-init**, with an `examples/` showcase. Not generic-defaults, not empty-defaults. Three options were posed. My ranking, with reasons grounded in the existing machinery: 1. **Reject "generic-defaults"** (ship a neutral-but-real SOUL like "You are Assistant"). It *reads* clean but it re-creates the exact bug we are fixing: a shipped persona that some users never replace, so "Assistant" becomes the new "Jarvis." It also tempts maintainers to slip preferences back in ("just a sensible default tone…"). 2. **Reject pure "empty-defaults"** as the *whole* answer — an empty `SOUL.md` gives a terrible out-of-box first run (the agent has no name, no voice). DevEx death on first launch. 3. **Adopt template-then-init** (the half-built path), hardened: - **`defaults/SOUL.md` must be deleted from the shipped package** and replaced by *not shipping a SOUL at all*. `install.sh:232-241` already declines to seed `SOUL.md`/`USER.md` (the comment says so). The bug is purely that `defaults/SOUL.md` *exists and contains "Jarvis"*. **Concrete change:** delete `defaults/SOUL.md`; the only persona artifacts that ship are `templates/SOUL.md.template` and a generated-on-init `SOUL.md`. - **First-run must be non-blocking.** `mosaic-init` is interactive (`read -r`), which is fine for a human but hangs headless launches (and violates this very environment's no-TTY rules). Add a **deterministic non-interactive default generation**: on first `mosaic ` launch, if no `SOUL.md` exists, generate one from the template with `AGENT_NAME="Mosaic"`, `STYLE="direct"`, empty accommodations — *and print a one-line "run `mosaic init` to personalize."* `mosaic-init --non-interactive` (lines 100-107) already supports this; wire it into the launcher as a fallback so a fresh clone is usable in zero prompts. **What ships vs. what's generated (the contract):** | Ships in public package | Generated locally (never shipped, gitignored downstream) | |---|---| | `CONSTITUTION.md`, `STANDARDS.md`, `guides/*` (L0/L1) | `SOUL.md`, `USER.md`, `TOOLS.md` (L2/L3) | | `templates/*` (incl. `SOUL.md.template`, `USER.md.template`) | `OVERRIDES.md`, per-harness copies under `~/.claude` etc. | | `examples/personas/*.md` (see below) | `runtime/*/settings-overlays/*` user overlays | **Add `examples/` instead of contaminating `defaults/`.** The value of the Jarvis config (a worked, opinionated persona) is real — the mistake is shipping it *as the default*. **Concrete change:** move the sanitized essence of `jarvis-loop.json` and the Jarvis SOUL into `examples/personas/execution-partner.md` and `examples/overlays/e2e-loop.json` with **placeholder paths** (`~/src/`). `examples/` is documentation-by-example: copied on request, never auto-loaded. Then **delete** `runtime/claude/settings-overlays/jarvis-loop.json` from the shipped tree. **Sanitization gate (make it mechanical, not vibes).** Add a CI check — `tools/quality/scripts/verify.sh` already exists as the hook point — that greps the *shipped* paths (`defaults/`, `templates/`, `guides/`, `runtime/`, `adapters/`, `profiles/`) for a denylist (`jarvis`, `jason`, `woltje`, `\bPDA\b`, `~/src/jarvis`, real hostnames) and fails the build. Without this, contamination re-accretes the first time a maintainer dogfoods. This is the *only* durable fix; docs alone will rot. --- ## DQ3 — Customization & upgrade safety: the drift bug is **copy-on-link**, and the fix is a layered-resolution model with a 3-way merge This is the DevEx question I care most about, because the brief's own framing — *"A downstream user who edits files gets clobbered on upgrade"* — is **already half-true in the code today**, and the mechanisms partially contradict each other. **The two existing safety mechanisms and why they're insufficient:** 1. `install.sh` `PRESERVE_PATHS` (line 24): `keep` mode excludes `SOUL.md`, `USER.md`, `TOOLS.md`, `STANDARDS.md`, `memory` from `rsync --delete`. **Good for L2/L3, but it preserves `STANDARDS.md` too** — meaning a user who never touched `STANDARDS.md` *also never gets framework updates to it*. That is the silent-staleness half of the drift problem: preservation and upgrade are in tension and the current binary (`keep` vs `overwrite`) forces an all-or-nothing choice. 2. `mosaic-link-runtime-assets` copies framework files into each harness dir and `.mosaic-bak-` the previous copy on difference (lines 17-24). So an edit to `~/.claude/CLAUDE.md` survives as a backup but is **silently replaced** on the next link. The user's change is "preserved" only in the sense that a tombstone exists. **Position — replace the binary keep/overwrite with explicit layer ownership + a reconciliation step:** - **Framework-owned files (L0/L1) are *always* overwritten on upgrade, never preserved.** Remove `STANDARDS.md` from `PRESERVE_PATHS` in `install.sh:24`. Users do not edit Standards in place; they extend via L4 `OVERRIDES.md`. This kills the silent-staleness problem at the root. - **User-owned files (L2/L3/L4) are *never* overwritten** — but they are **migrated, not just preserved.** Templates carry a `` marker. On upgrade, if the shipped template version is newer than the one the user's file was generated from, run a **3-way merge** (base = old template, theirs = current `SOUL.md`, ours = new template). Surface conflicts as `SOUL.md.mosaic-merge` for the user to resolve, exactly like git. `mosaic-init`'s `import` path (lines 197-200, 221-269) already extracts values from existing files via grep — that scaffolding becomes the "theirs" side of the merge. **Concrete change:** add `tools/_scripts/mosaic-reconcile` that runs in `install.sh` after `sync_framework`, diffing each user file's embedded template-version against the shipped one. - **Version pinning already exists but is too coarse.** `install.sh:28` has `FRAMEWORK_VERSION=2` with a sequential migration runner (lines 160-202). Keep it, but **add per-file template versions** (above) so migrations can be surgical instead of "delete bin/." A single global version cannot express "SOUL template changed but USER template didn't." - **Kill copy-on-link drift: prefer symlinks for framework-owned runtime pointers, copies only for user-editable ones.** The runtime pointer files (`CLAUDE.md`, `instructions.md`, opencode `AGENTS.md`) are L0-pointers the user should *not* edit — symlink them to the canonical `~/.config/mosaic/runtime//` source so there is **one source of truth and zero drift.** Reserve `copy_file_managed` (and its `.mosaic-bak` dance) for genuinely user-editable surfaces like `settings.json`. The script already knows how to remove legacy symlinks (lines 27-45); invert the policy. *(Caveat: Windows symlink support is weak — keep the copy path as a `MOSAIC_NO_SYMLINK=1` fallback, which the existing `.ps1` variants can default to.)* **Net DevEx contract a user can actually rely on:** *"Edit `SOUL.md`/`USER.md`/`OVERRIDES.md` freely; upgrades never destroy them and will offer a merge when the template evolves. Never edit `CONSTITUTION.md`/`STANDARDS.md`/`guides/*`; they update automatically. Want to change framework behavior? Add to `OVERRIDES.md`."* That sentence is the whole upgrade-safety story, and today it cannot be truthfully written. --- ## DQ4 — Cross-harness robustness: single source of truth (L0/L1), **adapter = injection mechanism only**, and stop pretending the four harnesses enforce identically This is where the current design is weakest and where my lens has the strongest opinion. **The core problem (restating fact #1):** On Pi the Constitution is a true system prompt (`--append-system-prompt`, `adapters/pi.md:14`). On Claude/Codex/OpenCode it is a *"go read this file"* instruction sitting in a user-editable memory file (`CLAUDE.md`, `instructions.md`, `AGENTS.md`). These have **radically different enforcement strength**: a system prompt is non-removable for the turn; a "read this file" pointer can be ignored if the model is busy, can be edited away by the user, and competes with the harness's own injected guidance (e.g. Claude's `` blocks, which this very session demonstrates can carry their own mandatory-read instructions). **Positions:** 1. **Single source of truth: L0/L1 live in exactly one place** (`~/.config/mosaic/CONSTITUTION.md`, `STANDARDS.md`, `guides/*`). No harness gets a *forked copy* of rule text — only a pointer or an injection. This is mostly true today for guides, but the **hard gates are duplicated**: they exist in `defaults/AGENTS.md:23-37` *and* are restated in `templates/agent/AGENTS.md.template:7-15` *and* partially in every `runtime/*/RUNTIME.md` ("Runtime-default caution... does NOT override Mosaic hard gates" appears in all four). **Concrete change:** the four RUNTIME files should each shrink to a pointer ("Gates and precedence: `CONSTITUTION.md §Hard Gates`. This file adds *only* the harness-specific deltas below.") and the project `AGENTS.md.template` should `@import`/reference the Constitution rather than paraphrase 8 of its gates. 2. **The adapter's job is injection + tool-name translation, nothing else.** Define a strict adapter contract. An `adapters/.md` may specify only: - **How** L0/L1 reaches the model (system-prompt append vs. memory-file pointer vs. settings). - **Tool-name mapping** for capabilities the Constitution references abstractly. The Constitution must speak in **capability verbs**, not tool names, because the tool surfaces genuinely differ: Claude has `Task(model=...)` subagents (`runtime/claude/RUNTIME.md:15-24`); Pi has `--thinking` levels and `--models` cycling (`runtime/pi/RUNTIME.md:22-28`) and *no* sequential-thinking MCP gate (`runtime/pi/RUNTIME.md:59-61`); Codex/OpenCode require the MCP. A single rule "use sequential-thinking MCP" is *already* false for Pi — and the Pi runtime had to carve out an exception. That exception belongs in the **adapter capability map**, not as prose scattered in a runtime file. **Concrete structure — a capability manifest per harness** (`adapters/.capabilities.json`): ```json { "harness": "pi", "injection": "system-prompt-append", "capabilities": { "structured_reasoning": { "provider": "native-thinking", "gate": false }, "subagent_spawn": { "tool": "--models cycling", "model_param": "native" }, "skills": { "mechanism": "--skill flag" } } } ``` vs. Claude's `{ "structured_reasoning": { "provider": "mcp:sequential-thinking", "gate": true }, "subagent_spawn": { "tool": "Task", "model_param": "model" } }`. The Constitution says *"use structured reasoning for multi-step planning"*; the adapter resolves that to the concrete tool and says whether absence is a hard stop. This removes the four near-duplicate "sequential-thinking required (except Pi)" stanzas and makes adding a 5th harness a matter of writing one manifest. 3. **Honesty about enforcement tiers.** Because file-pointer injection is weaker than system-prompt injection, the framework should **prefer the strongest injection each harness offers** and document the tier: - Pi: system-prompt (Tier 1, strong) — keep. - Claude: today uses `CLAUDE.md` pointer (Tier 3, weak). **Concrete change:** `mosaic claude` should inject the Constitution via `--append-system-prompt` (Claude Code supports it), demoting `~/.claude/CLAUDE.md` to a *fallback for bare `claude` launches* — which its own header already admits it is (`runtime/claude/CLAUDE.md:12-13`). Same for Codex (`--config`/system prompt) and OpenCode where supported. - Where a harness genuinely only supports a memory file, that is **Tier 3** and the docs must say "weaker enforcement; rely on hooks for hard gates." Which leads to: 4. **Back hard gates with mechanical hooks wherever the harness has them, because prose is advisory.** Claude already does this: `prevent-memory-write.sh` is a PreToolUse hook, and `runtime/claude/RUNTIME.md:30-32` is explicit that *"the rule alone proved insufficient — the hook is the hard gate."* That is the single most important DevEx lesson in the repo and it should be **promoted to Constitution doctrine**: *a hard gate that can be enforced by a hook MUST be, on harnesses that support hooks; the prose is the spec, the hook is the enforcement.* Codex/OpenCode hook parity becomes a tracked gap rather than a silent inconsistency. --- ## DQ5 — Minimalism vs completeness: thin **resident** core, deep **on-demand** guides, and delete the duplication that's already there The contract is large *and* partly duplicated — both are true and they have different fixes. **Keep the thin-resident / deep-on-demand split — it's the right instinct and already present.** `defaults/AGENTS.md:6-8` ("THIN CORE... Depth lives in guides, read on demand") plus the Conditional Guide Loading table (lines 89-110) is genuinely good design. Don't undo it. But tighten it: 1. **Define a hard budget for the always-resident core.** Right now `defaults/AGENTS.md` is ~155 lines and growing (it carries the model-selection table, the superpowers section, the closure checklist — all of which are *advice*, not *gates*). **Concrete change:** the resident L0 core (`CONSTITUTION.md`) should be **only**: hard gates, precedence, block-vs-done, escalation triggers, mode declaration. Target ≤ ~70 lines. Everything else (subagent cost selection lines 111-121, superpowers enforcement 123-139, conditional-loading table) moves to `STANDARDS.md` (L1, resident but separable) or a guide. Rationale: every always-resident token competes with task context on *every* harness, and the weakest-context harness (smallest effective window) sets the ceiling. 2. **Eliminate the existing triplication of hard gates.** As noted in DQ4, the gates live in three places. Pick one canonical home (`CONSTITUTION.md`), and make `templates/agent/AGENTS.md.template` and the RUNTIME files *reference* it. This is pure win: less to read, impossible to drift out of sync, smaller resident footprint. The `templates/agent/AGENTS.md.template:5-15` "Hard Gates" block is a maintenance landmine — it already uses a stale path (`~/.config/mosaic/rails/git/...` vs the real `~/.config/mosaic/tools/git/...`), proving the duplication has *already* drifted. 3. **Contradiction audit as a release gate.** There is at least one live contradiction in the shipped tree: `rails/` vs `tools/` paths (template vs defaults), and the migration code at `install.sh:193` even removes a stale `rails` symlink — so the framework *knows* `rails` is dead but templates still emit it. **Concrete change:** extend the DQ2 sanitization CI check to also fail on known-dead path tokens (`/rails/`, `bin/mosaic-`) outside of migration code. Minimalism isn't just fewer words; it's *no stale words*. 4. **"Completeness" belongs in guides and `examples/`, not the core.** The depth (E2E-DELIVERY, ORCHESTRATOR, QA-TESTING) is excellent and should stay long — it's loaded on demand by role, so its length costs nothing on a session that doesn't need it. The error is putting *completeness* in the resident contract. Resident = gates + routing table. Depth = guides. Worked examples = `examples/`. **Anti-bloat principle to adopt explicitly:** *If a line is not a gate, not the precedence rule, and not required to route to the right guide, it does not belong in the always-resident core.* That single sentence, applied, would cut `defaults/AGENTS.md` roughly in half. --- ## Summary of concrete changes (what I'd actually do, with paths) 1. **Create `CONSTITUTION.md`** (L0) from the hard-gates + escalation + precedence portions of `defaults/AGENTS.md:23-87`; add an explicit `## Precedence` section (L0 > L1 > {L2,L3,L4}). Shrink resident core to ≤ ~70 lines. 2. **Delete `defaults/SOUL.md`** (the "Jarvis"/"PDA" file). Persona ships only as `templates/SOUL.md.template`; generated locally. `install.sh:232-241` already refuses to seed it — the file just shouldn't exist. 3. **Delete `runtime/claude/settings-overlays/jarvis-loop.json`**; move its sanitized, placeholdered essence to `examples/overlays/e2e-loop.json` and `examples/personas/execution-partner.md`. 4. **Add a sanitization + dead-path CI gate** in `tools/quality/scripts/verify.sh` over shipped dirs (denylist: `jarvis|jason|woltje|\bPDA\b|~/src/jarvis|/rails/`). Make contamination un-mergeable. 5. **Per-file template versioning** (``) + a new `tools/_scripts/mosaic-reconcile` doing 3-way merge of L2/L3 files on upgrade; remove `STANDARDS.md` from `install.sh:24` `PRESERVE_PATHS`. 6. **Invert link policy in `mosaic-link-runtime-assets`:** symlink framework-owned runtime pointers (single source of truth, zero drift); copy only user-editable settings; keep `MOSAIC_NO_SYMLINK=1` for Windows. 7. **Adapter capability manifests** (`adapters/.capabilities.json`) for injection mode + tool-name mapping + per-gate enforcement tier; collapse the four near-duplicate "sequential-thinking required (except Pi)" stanzas into the manifests. 8. **Prefer strongest injection per harness:** `mosaic claude`/`mosaic codex` inject the Constitution via system-prompt append; demote `CLAUDE.md`/`instructions.md` to documented fallbacks. 9. **Promote "hooks are the real enforcement" to Constitution doctrine** (generalizing `runtime/claude/RUNTIME.md:30-32`); track Codex/OpenCode hook parity as an open gap. 10. **De-duplicate hard gates** out of `templates/agent/AGENTS.md.template` and `runtime/*/RUNTIME.md` into references to `CONSTITUTION.md`; fix the stale `rails/` paths while doing it. --- ## Abstract **Headline:** Mosaic's portability problem isn't the layering taxonomy — it's that the four harnesses *enforce the contract with wildly different strength* (Pi: real system prompt; Claude/Codex/OpenCode: a user-editable "please read this file" pointer that copies-on-link and silently drifts), and personal data leaked precisely because framework-owned and user-owned content share files with no mutability boundary. **Single strongest recommendation:** Split content by **ownership + mutability** into L0 Constitution (framework, always overwritten) / L2 Persona + L3 Operator (user, never overwritten, template-versioned with 3-way-merge on upgrade), make the **adapter responsible only for injection-mechanism + tool-name mapping via per-harness capability manifests**, and back every hookable hard gate with an actual hook — because, as the repo already learned with `prevent-memory-write.sh`, *prose rules are advisory and only mechanical enforcement is a gate.* **Biggest risk:** The weak-injection harnesses make the Constitution **advisory, not enforced** on 3 of 4 runtimes. If we ship the layering taxonomy but leave Claude/Codex/OpenCode receiving L0 as an ignorable, user-editable memory-file pointer (and keep copy-on-link drift), we'll have a beautiful constitution that the model can silently skip and the user can silently clobber — re-creating the deployed-vs-source drift the brief set out to kill, just with cleaner file names.