Conference of 7 experts (architect/moonshot/contrarian/coder/aiml/devex/steward) debated layering, sanitization, upgrade-safety, cross-harness robustness. Artifacts: BRIEF, 7 positions, 7 rebuttals, synthesis-v1, 3 red-team passes, canonical DESIGN.md, OPEN-QUESTIONS.md, MISSION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
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:6claim "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.mdandruntime/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.
- Claude:
-
mosaic-link-runtime-assetscopies, 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 nextmosaic 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:9hardcodes "You are Jarvis";defaults/SOUL.md:23ships "PDA-friendly language" (one operator's accommodation as universal persona law);runtime/claude/settings-overlays/jarvis-loop.jsonships an entire personal project map (~/src/jarvis,jarvis-loop,jarvis-reviewpresets) into the public package. -
A clean template layer already exists and is under-used.
templates/SOUL.md.template,templates/USER.md.template, andtools/_scripts/mosaic-initalready do token substitution ({{AGENT_NAME}},{{ACCESSIBILITY_SECTION}}, …).defaults/USER.mdis already a generic "(not configured)" stub. The machinery is half-built; the problem is thatdefaults/SOUL.mdwas never reduced to matchdefaults/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.mdsaying "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:
-
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…").
-
Reject pure "empty-defaults" as the whole answer — an empty
SOUL.mdgives a terrible out-of-box first run (the agent has no name, no voice). DevEx death on first launch. -
Adopt template-then-init (the half-built path), hardened:
defaults/SOUL.mdmust be deleted from the shipped package and replaced by not shipping a SOUL at all.install.sh:232-241already declines to seedSOUL.md/USER.md(the comment says so). The bug is purely thatdefaults/SOUL.mdexists and contains "Jarvis". Concrete change: deletedefaults/SOUL.md; the only persona artifacts that ship aretemplates/SOUL.md.templateand a generated-on-initSOUL.md.- First-run must be non-blocking.
mosaic-initis 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 firstmosaic <harness>launch, if noSOUL.mdexists, generate one from the template withAGENT_NAME="Mosaic",STYLE="direct", empty accommodations — and print a one-line "runmosaic initto 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/<your-project>). 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:
-
install.shPRESERVE_PATHS(line 24):keepmode excludesSOUL.md,USER.md,TOOLS.md,STANDARDS.md,memoryfromrsync --delete. Good for L2/L3, but it preservesSTANDARDS.mdtoo — meaning a user who never touchedSTANDARDS.mdalso 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 (keepvsoverwrite) forces an all-or-nothing choice. -
mosaic-link-runtime-assetscopies framework files into each harness dir and.mosaic-bak-<stamp>the previous copy on difference (lines 17-24). So an edit to~/.claude/CLAUDE.mdsurvives 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.mdfromPRESERVE_PATHSininstall.sh:24. Users do not edit Standards in place; they extend via L4OVERRIDES.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
<!-- mosaic:template-version: N -->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 = currentSOUL.md, ours = new template). Surface conflicts asSOUL.md.mosaic-mergefor the user to resolve, exactly like git.mosaic-init'simportpath (lines 197-200, 221-269) already extracts values from existing files via grep — that scaffolding becomes the "theirs" side of the merge. Concrete change: addtools/_scripts/mosaic-reconcilethat runs ininstall.shaftersync_framework, diffing each user file's embedded template-version against the shipped one. -
Version pinning already exists but is too coarse.
install.sh:28hasFRAMEWORK_VERSION=2with 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, opencodeAGENTS.md) are L0-pointers the user should not edit — symlink them to the canonical~/.config/mosaic/runtime/<h>/source so there is one source of truth and zero drift. Reservecopy_file_managed(and its.mosaic-bakdance) for genuinely user-editable surfaces likesettings.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 aMOSAIC_NO_SYMLINK=1fallback, which the existing.ps1variants 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
<system-reminder> blocks, which this very session demonstrates can carry their own mandatory-read
instructions).
Positions:
-
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 indefaults/AGENTS.md:23-37and are restated intemplates/agent/AGENTS.md.template:7-15and partially in everyruntime/*/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 projectAGENTS.md.templateshould@import/reference the Constitution rather than paraphrase 8 of its gates. -
The adapter's job is injection + tool-name translation, nothing else. Define a strict adapter contract. An
adapters/<h>.mdmay 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--thinkinglevels and--modelscycling (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/<h>.capabilities.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. -
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.mdpointer (Tier 3, weak). Concrete change:mosaic claudeshould inject the Constitution via--append-system-prompt(Claude Code supports it), demoting~/.claude/CLAUDE.mdto a fallback for bareclaudelaunches — 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:
-
Back hard gates with mechanical hooks wherever the harness has them, because prose is advisory. Claude already does this:
prevent-memory-write.shis a PreToolUse hook, andruntime/claude/RUNTIME.md:30-32is 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:
-
Define a hard budget for the always-resident core. Right now
defaults/AGENTS.mdis ~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 toSTANDARDS.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. -
Eliminate the existing triplication of hard gates. As noted in DQ4, the gates live in three places. Pick one canonical home (
CONSTITUTION.md), and maketemplates/agent/AGENTS.md.templateand the RUNTIME files reference it. This is pure win: less to read, impossible to drift out of sync, smaller resident footprint. Thetemplates/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. -
Contradiction audit as a release gate. There is at least one live contradiction in the shipped tree:
rails/vstools/paths (template vs defaults), and the migration code atinstall.sh:193even removes a stalerailssymlink — so the framework knowsrailsis 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. -
"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)
- Create
CONSTITUTION.md(L0) from the hard-gates + escalation + precedence portions ofdefaults/AGENTS.md:23-87; add an explicit## Precedencesection (L0 > L1 > {L2,L3,L4}). Shrink resident core to ≤ ~70 lines. - Delete
defaults/SOUL.md(the "Jarvis"/"PDA" file). Persona ships only astemplates/SOUL.md.template; generated locally.install.sh:232-241already refuses to seed it — the file just shouldn't exist. - Delete
runtime/claude/settings-overlays/jarvis-loop.json; move its sanitized, placeholdered essence toexamples/overlays/e2e-loop.jsonandexamples/personas/execution-partner.md. - Add a sanitization + dead-path CI gate in
tools/quality/scripts/verify.shover shipped dirs (denylist:jarvis|jason|woltje|\bPDA\b|~/src/jarvis|/rails/). Make contamination un-mergeable. - Per-file template versioning (
<!-- mosaic:template-version: N -->) + a newtools/_scripts/mosaic-reconciledoing 3-way merge of L2/L3 files on upgrade; removeSTANDARDS.mdfrominstall.sh:24PRESERVE_PATHS. - Invert link policy in
mosaic-link-runtime-assets: symlink framework-owned runtime pointers (single source of truth, zero drift); copy only user-editable settings; keepMOSAIC_NO_SYMLINK=1for Windows. - Adapter capability manifests (
adapters/<h>.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. - Prefer strongest injection per harness:
mosaic claude/mosaic codexinject the Constitution via system-prompt append; demoteCLAUDE.md/instructions.mdto documented fallbacks. - 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. - De-duplicate hard gates out of
templates/agent/AGENTS.md.templateandruntime/*/RUNTIME.mdinto references toCONSTITUTION.md; fix the stalerails/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.