Repository navigation
Per-persona claudeMd / agentsMd sidecar markdown files #44
Description
Summary
Allow each persona to attach a hand-authored CLAUDE.md / AGENTS.md to itself — job-scoped context that travels with the persona, materialized into the sandbox mount at session time so the harness reads it natively.
This fits the AgentWorkforce value prop: job-scoped + team-shareable + anti-bloat. Users get the right context only when that persona runs, instead of stuffing everything into a global CLAUDE.md.
Design
Schema
Add to PersonaSpec (top-level, sibling of tiers) and PersonaRuntime (per-tier):
claudeMd?: string; // relative POSIX path to .md file
claudeMdMode?: "overwrite" | "extend"; // default: "overwrite"
agentsMd?: string;
agentsMdMode?: "overwrite" | "extend"; // default: "overwrite"Mirror on LocalPersonaOverride and its tier shape. Path is resolved relative to the JSON file that declared the field (matters for extends cascades).
Validation (paths): non-empty, no leading /, no .., must end in .md.
Validation (modes): enum check; reject mode set without a corresponding path.
Resolution
For a given (persona, selected tier, harness) at runtime:
- Pick the field by harness:
claude→claudeMdopencode→agentsMdcodex→ none (warn + skip; no mount available)
- Tier-level wins over top-level. Mode resolves with the same cascade and is independent of the path (a tier can override the path while inheriting the mode, or vice versa).
- No cross-fallback between
claudeMdandagentsMd. If you want one file applied everywhere, set both top-level fields to the same path.
Runtime delivery — write into the sandbox mount
In runInteractive (packages/cli/src/cli.ts:558+), after the mount is established and before harness invocation:
- claude in sandbox: materialize
<mountDir>/CLAUDE.md.CLEAN_IGNORED_PATTERNS(cli.ts:429-432) already excludesCLAUDE.mdfrom sync — no real-repo pollution. - opencode in sandbox: materialize
<mountDir>/AGENTS.md. Action item: addAGENTS.mdtoCLEAN_IGNORED_PATTERNS(it is currently not excluded, so without this change it would sync back into the user's real repo). - codex /
--install-in-repo: no mount available → log a warning and continue. Never touch the user's real cwd.
Why mount-write rather than appending to systemPrompt? Native CLAUDE.md / AGENTS.md semantics — @import, hierarchy, etc. — survive. The harness reads its own convention.
packages/harness-kit/src/harness.ts does not change.
Mode behavior
Applies to both claude and opencode mount-write paths:
overwrite(default): write only the persona's resolved markdown content into<mountDir>/CLAUDE.md(orAGENTS.md). The user's real-cwd file at the same name is hidden by the existing sandbox sync exclusion.extend: read the user's real-cwdCLAUDE.md(orAGENTS.md) if it exists, then write<real-content>\n\n---\n\n<persona-content>into the mount file. If the real file does not exist, behave asoverwrite(graceful degradation, no warning). The result still lives only in the mount; the real file is never modified.
extends / merge
In mergeOverride (packages/cli/src/local-personas.ts:666-712):
- Top-level
claudeMd/agentsMd(and the modes): override replaces base wholesale, matchingdescriptionsemantics. - Tier-level: per-tier replacement, matching existing tier merge.
Path is anchored to the directory of whichever JSON declared it. Track sourcePath per parsed override internally (set in readLayerDir, local-personas.ts:283-296); resolved PersonaSpec carries the already-absolute path so downstream code doesn't re-track sources.
Built-in catalog — inline at build time
Built-in personas in personas/*.json get content inlined at generation time so installed packages don't need to ship sibling .md files for built-ins:
packages/workload-router/scripts/generate-personas.mjs: read sibling.md(relative topersonas/), emitclaudeMdContent/agentsMdContent(string) on the generated spec, drop the path field.- Runtime checks
*Contentfirst, otherwise reads from the absolute path. - Hard-fail the generator on a missing built-in
.md. - Watch mode (lines 67-87) extends to retrigger on
.mdchanges.
Packaging (prpm install) — targeted asset copy
packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:
- Replace
collectJsonFileswithcollectPersonaAssets— for each persona JSON, read itsclaudeMd/agentsMd(top-level + per-tier), resolve each relative to the JSON's dir, and add to a per-persona asset list. Reject../ absolute paths. Hard-fail on missing files at packaging time (loud failure is right at the distribution boundary). - Extend
PersonaFile(line 58) withassets: { sourcePath, basename }[]. - In
installPersonas(332-384): copy each persona's assets into<targetDir>/<id>__assets/<basename>and rewrite the JSON'sclaudeMd/agentsMdpaths in-place to point at the new location.
Validation
parseOverride(local-personas.ts:304-363) andassertTiersShape(477-500): assert string + non-empty + ends in.md+ no..+ not absolute. Reuse the style ofassertSafeRelativePath(cli.ts:407-422).- After cascade resolution: stat the absolute path; missing file → push to load
warningsand clear the field on the resolved spec (graceful in dev). - Generator: hard-fail on missing built-in
.md.
Files affected
packages/workload-router/src/index.ts— extendPersonaRuntime(lines 59-64) andPersonaSpec(140-175) withclaudeMd?,claudeMdMode?,agentsMd?,agentsMdMode?, plusclaudeMdContent?/agentsMdContent?for inlined built-ins.packages/workload-router/scripts/generate-personas.mjs— read sibling.md, emit*Content, watch.mdchanges, hard-fail on missing.packages/cli/src/local-personas.ts— extendLocalPersonaOverride(30-57); validate inparseOverride(304-363) andassertTiersShape(477-500); track per-override source path inreadLayerDir(267-298); resolve absolute paths and stat inmergeOverride(666-712) andstandaloneSpecFromOverride(566-603); surface missing-file warnings inloadLocalPersonas(734-768).packages/cli/src/cli.ts— addAGENTS.mdtoCLEAN_IGNORED_PATTERNS(429-432); carry resolved sidecar info ontoPersonaSelectioninbuildSelection(236-253); inrunInteractive(558+), pick field by harness, materialize content into the mount dir before harness invoke (with mode handling); warn-and-skip for codex /--install-in-repo.packages/cli/src/persona-install.ts— extendPersonaFile(58); replacecollectJsonFiles(165) withcollectPersonaAssets; updatecollectPersonas(212) andinstallPersonas(332) to copy referenced sidecars and rewrite JSON paths.
packages/harness-kit/src/harness.ts — no change.
Test plan
- Loader (
packages/cli/src/local-personas.test.ts)- Parse + validate top-level only, tier-only, both, malformed (
.., absolute, non-.md) - Mode validation: reject mode set without path; enum-check
- Cascade: override sets
claudeMd, base does not, and vice-versa; resolved spec carries absolute path anchored to the right layer's dir - Mode independence: tier overrides path, inherits top-level mode (and vice versa)
- Missing file produces warning, not throw
- Parse + validate top-level only, tier-only, both, malformed (
- Packaging (
packages/cli/src/persona-install.test.ts)-
npm pack→ install copies referenced.mdfiles into<id>__assets/, rewrites JSON paths, leaves unreferenced.mdfiles behind - Rejects
..and absolute paths at pack/install time - Hard-fail on missing referenced
.md
-
- CLI / runtime (
packages/cli/src/cli.test.ts)- Resolution: claude →
claudeMd; opencode →agentsMd; codex → warn + skip; tier-level wins over top-level; no cross-fallback -
overwritemode:<mountDir>/CLAUDE.mdcontains exactly the persona content (claude) /<mountDir>/AGENTS.md(opencode) -
extendmode: real cwd hasCLAUDE.md→ mount file =<real>\n\n---\n\n<persona>; real cwd has no file → mount file = persona content only (no warning) - Real cwd is never mutated in either mode
-
--install-in-repo: warn + no file written into real cwd
- Resolution: claude →
- Generator (snapshot)
- Built-in persona referencing a sibling
.mdproduces generated TS with*Contentpopulated and the path field gone - Missing
.mdhard-fails the generator
- Built-in persona referencing a sibling
- Manual smoke
- Author a local persona with
claudeMd(overwrite), runagentworkforce agent <persona>@best(claude), confirm the agent acknowledges the contents on first turn - Same persona with
claudeMdMode: "extend"in a repo that has its ownCLAUDE.md— confirm both sets of context are visible to the agent - Repeat for opencode tier (
agentsMd)
- Author a local persona with
Out of scope
- Native CLAUDE.md
@import/ hierarchy semantics for codex (codex has no mount and no file convention support in this repo). --install-in-repomode — never write into the user's real repo; warn + skip.- Cross-fallback between
claudeMdandagentsMd. - Multiple sidecars per (persona, tier).
- Inline content in JSON (only path form is accepted from authors; build-time inlining is internal to the catalog generator).
- Hot-reload of sidecar content during a live session.
Related
- Recent persona work: feat(cli): add persona pack install command #40 (installable persona sources), persona create mode (4b21bf9), preserve standalone persona inputs (fedd22e).
- Distinction from skills (which are declared by URL and materialized at session time): sidecar markdown files are author-shipped along with the persona JSON and copied at install time.
Activity