Per-persona claudeMd / agentsMd sidecar markdown files #44

Description

@willwashburn

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:

  1. Pick the field by harness:
    • claudeclaudeMd
    • opencodeagentsMd
    • codex → none (warn + skip; no mount available)
  2. 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).
  3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
  • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
  • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
  • Runtime checks *Content first, 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 .md changes.

Packaging (prpm install) — targeted asset copy

packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

  • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
  • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

Validation

  • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
  • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
  • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
  • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
  • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

packages/harness-kit/src/harness.tsno 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
  • Packaging (packages/cli/src/persona-install.test.ts)
    • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
    • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
    • extend mode: real cwd has CLAUDE.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
  • Generator (snapshot)
    • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
    • Missing .md hard-fails the generator
  • Manual smoke
    • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
    • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
    • Repeat for opencode tier (agentsMd)

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-repo mode — never write into the user's real repo; warn + skip.
  • Cross-fallback between claudeMd and agentsMd.
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
      Skip to content

      Per-persona claudeMd / agentsMd sidecar markdown files #44

      Description

      @willwashburn

      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:

      1. Pick the field by harness:
        • claudeclaudeMd
        • opencodeagentsMd
        • codex → none (warn + skip; no mount available)
      2. 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).
      3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
      • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
      • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
      • Runtime checks *Content first, 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 .md changes.

      Packaging (prpm install) — targeted asset copy

      packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

      • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
      • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

      Validation

      • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
      • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
      • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
      • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
      • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

      packages/harness-kit/src/harness.tsno 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
      • Packaging (packages/cli/src/persona-install.test.ts)
        • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
        • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
        • extend mode: real cwd has CLAUDE.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
      • Generator (snapshot)
        • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
        • Missing .md hard-fails the generator
      • Manual smoke
        • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
        • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
        • Repeat for opencode tier (agentsMd)

      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-repo mode — never write into the user's real repo; warn + skip.
      • Cross-fallback between claudeMd and agentsMd.
      • 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

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Per-persona claudeMd / agentsMd sidecar markdown files #44

          Description

          @willwashburn

          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:

          1. Pick the field by harness:
            • claudeclaudeMd
            • opencodeagentsMd
            • codex → none (warn + skip; no mount available)
          2. 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).
          3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
          • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
          • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
          • Runtime checks *Content first, 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 .md changes.

          Packaging (prpm install) — targeted asset copy

          packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

          • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
          • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

          Validation

          • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
          • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
          • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
          • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
          • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

          packages/harness-kit/src/harness.tsno 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
          • Packaging (packages/cli/src/persona-install.test.ts)
            • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
            • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
            • extend mode: real cwd has CLAUDE.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
          • Generator (snapshot)
            • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
            • Missing .md hard-fails the generator
          • Manual smoke
            • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
            • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
            • Repeat for opencode tier (agentsMd)

          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-repo mode — never write into the user's real repo; warn + skip.
          • Cross-fallback between claudeMd and agentsMd.
          • 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

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Per-persona claudeMd / agentsMd sidecar markdown files #44

              Description

              @willwashburn

              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:

              1. Pick the field by harness:
                • claudeclaudeMd
                • opencodeagentsMd
                • codex → none (warn + skip; no mount available)
              2. 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).
              3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
              • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
              • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
              • Runtime checks *Content first, 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 .md changes.

              Packaging (prpm install) — targeted asset copy

              packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

              • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
              • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

              Validation

              • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
              • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
              • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
              • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
              • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

              packages/harness-kit/src/harness.tsno 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
              • Packaging (packages/cli/src/persona-install.test.ts)
                • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
                • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
                • extend mode: real cwd has CLAUDE.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
              • Generator (snapshot)
                • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
                • Missing .md hard-fails the generator
              • Manual smoke
                • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
                • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
                • Repeat for opencode tier (agentsMd)

              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-repo mode — never write into the user's real repo; warn + skip.
              • Cross-fallback between claudeMd and agentsMd.
              • 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

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Per-persona claudeMd / agentsMd sidecar markdown files #44

                  Description

                  @willwashburn

                  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:

                  1. Pick the field by harness:
                    • claudeclaudeMd
                    • opencodeagentsMd
                    • codex → none (warn + skip; no mount available)
                  2. 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).
                  3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
                  • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
                  • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
                  • Runtime checks *Content first, 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 .md changes.

                  Packaging (prpm install) — targeted asset copy

                  packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

                  • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
                  • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

                  Validation

                  • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
                  • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
                  • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
                  • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
                  • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

                  packages/harness-kit/src/harness.tsno 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
                  • Packaging (packages/cli/src/persona-install.test.ts)
                    • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
                    • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
                    • extend mode: real cwd has CLAUDE.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
                  • Generator (snapshot)
                    • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
                    • Missing .md hard-fails the generator
                  • Manual smoke
                    • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
                    • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
                    • Repeat for opencode tier (agentsMd)

                  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-repo mode — never write into the user's real repo; warn + skip.
                  • Cross-fallback between claudeMd and agentsMd.
                  • 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

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Per-persona claudeMd / agentsMd sidecar markdown files #44

                      Description

                      @willwashburn

                      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:

                      1. Pick the field by harness:
                        • claudeclaudeMd
                        • opencodeagentsMd
                        • codex → none (warn + skip; no mount available)
                      2. 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).
                      3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
                      • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
                      • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
                      • Runtime checks *Content first, 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 .md changes.

                      Packaging (prpm install) — targeted asset copy

                      packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

                      • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
                      • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

                      Validation

                      • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
                      • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
                      • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
                      • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
                      • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

                      packages/harness-kit/src/harness.tsno 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
                      • Packaging (packages/cli/src/persona-install.test.ts)
                        • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
                        • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
                        • extend mode: real cwd has CLAUDE.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
                      • Generator (snapshot)
                        • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
                        • Missing .md hard-fails the generator
                      • Manual smoke
                        • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
                        • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
                        • Repeat for opencode tier (agentsMd)

                      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-repo mode — never write into the user's real repo; warn + skip.
                      • Cross-fallback between claudeMd and agentsMd.
                      • 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

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Per-persona claudeMd / agentsMd sidecar markdown files #44

                          Description

                          @willwashburn

                          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:

                          1. Pick the field by harness:
                            • claudeclaudeMd
                            • opencodeagentsMd
                            • codex → none (warn + skip; no mount available)
                          2. 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).
                          3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
                          • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
                          • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
                          • Runtime checks *Content first, 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 .md changes.

                          Packaging (prpm install) — targeted asset copy

                          packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

                          • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
                          • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

                          Validation

                          • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
                          • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
                          • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
                          • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
                          • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

                          packages/harness-kit/src/harness.tsno 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
                          • Packaging (packages/cli/src/persona-install.test.ts)
                            • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
                            • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
                            • extend mode: real cwd has CLAUDE.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
                          • Generator (snapshot)
                            • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
                            • Missing .md hard-fails the generator
                          • Manual smoke
                            • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
                            • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
                            • Repeat for opencode tier (agentsMd)

                          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-repo mode — never write into the user's real repo; warn + skip.
                          • Cross-fallback between claudeMd and agentsMd.
                          • 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

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Per-persona claudeMd / agentsMd sidecar markdown files #44

                              Description

                              @willwashburn

                              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:

                              1. Pick the field by harness:
                                • claudeclaudeMd
                                • opencodeagentsMd
                                • codex → none (warn + skip; no mount available)
                              2. 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).
                              3. No cross-fallback between claudeMd and agentsMd. 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 excludes CLAUDE.md from sync — no real-repo pollution.
                              • opencode in sandbox: materialize <mountDir>/AGENTS.md. Action item: add AGENTS.md to CLEAN_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 (or AGENTS.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-cwd CLAUDE.md (or AGENTS.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 as overwrite (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, matching description semantics.
                              • 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 to personas/), emit claudeMdContent / agentsMdContent (string) on the generated spec, drop the path field.
                              • Runtime checks *Content first, 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 .md changes.

                              Packaging (prpm install) — targeted asset copy

                              packages/cli/src/persona-install.ts currently flat-copies .json only (collectJsonFiles, line 165). Extend it:

                              • Replace collectJsonFiles with collectPersonaAssets — for each persona JSON, read its claudeMd / 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) with assets: { sourcePath, basename }[].
                              • In installPersonas (332-384): copy each persona's assets into <targetDir>/<id>__assets/<basename> and rewrite the JSON's claudeMd / agentsMd paths in-place to point at the new location.

                              Validation

                              • parseOverride (local-personas.ts:304-363) and assertTiersShape (477-500): assert string + non-empty + ends in .md + no .. + not absolute. Reuse the style of assertSafeRelativePath (cli.ts:407-422).
                              • After cascade resolution: stat the absolute path; missing file → push to load warnings and 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 — extend PersonaRuntime (lines 59-64) and PersonaSpec (140-175) with claudeMd?, claudeMdMode?, agentsMd?, agentsMdMode?, plus claudeMdContent? / agentsMdContent? for inlined built-ins.
                              • packages/workload-router/scripts/generate-personas.mjs — read sibling .md, emit *Content, watch .md changes, hard-fail on missing.
                              • packages/cli/src/local-personas.ts — extend LocalPersonaOverride (30-57); validate in parseOverride (304-363) and assertTiersShape (477-500); track per-override source path in readLayerDir (267-298); resolve absolute paths and stat in mergeOverride (666-712) and standaloneSpecFromOverride (566-603); surface missing-file warnings in loadLocalPersonas (734-768).
                              • packages/cli/src/cli.ts — add AGENTS.md to CLEAN_IGNORED_PATTERNS (429-432); carry resolved sidecar info onto PersonaSelection in buildSelection (236-253); in runInteractive (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 — extend PersonaFile (58); replace collectJsonFiles (165) with collectPersonaAssets; update collectPersonas (212) and installPersonas (332) to copy referenced sidecars and rewrite JSON paths.

                              packages/harness-kit/src/harness.tsno 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
                              • Packaging (packages/cli/src/persona-install.test.ts)
                                • npm pack → install copies referenced .md files into <id>__assets/, rewrites JSON paths, leaves unreferenced .md files 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
                                • overwrite mode: <mountDir>/CLAUDE.md contains exactly the persona content (claude) / <mountDir>/AGENTS.md (opencode)
                                • extend mode: real cwd has CLAUDE.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
                              • Generator (snapshot)
                                • Built-in persona referencing a sibling .md produces generated TS with *Content populated and the path field gone
                                • Missing .md hard-fails the generator
                              • Manual smoke
                                • Author a local persona with claudeMd (overwrite), run agentworkforce agent <persona>@best (claude), confirm the agent acknowledges the contents on first turn
                                • Same persona with claudeMdMode: "extend" in a repo that has its own CLAUDE.md — confirm both sets of context are visible to the agent
                                • Repeat for opencode tier (agentsMd)

                              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-repo mode — never write into the user's real repo; warn + skip.
                              • Cross-fallback between claudeMd and agentsMd.
                              • 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

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions