From eec152a5cc49d1651971fd0e83eaf99105b90230 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 17 Aug 2026 12:39:18 -0400 Subject: [PATCH 1/5] feat: make AGENTS the persona authority Assisted-by: codex --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .claude/evidence/personas-folder-parity.md | 48 +++++++++++++++++- .claude/evidence/task-5-privacy-safety.md | 26 +++++----- .claude/evidence/task-6-runtime-adapters.md | 5 +- .../evidence/task-9-release-preparation.md | 6 ++- .claude/plans/personas-folder-parity.md | 37 +++++++------- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 7 +++ CLAUDE.md | 5 +- MIGRATION.md | 17 +++---- README.md | 15 +++--- RELEASE.md | 10 ++-- TROUBLESHOOTING.md | 6 +-- examples/atlas-sanitized/AGENTS.md | 31 ++++++++++-- examples/atlas-sanitized/CLAUDE.md | 5 +- examples/atlas-sanitized/PERSONA.md | 30 ------------ examples/atlas-sanitized/README.md | 4 +- interop/capabilities.json | 7 +-- skills/persona-dev/SKILL.md | 15 +++--- skills/persona-dev/assets/agents-template.md | 35 ++++++++----- .../persona-dev/assets/claude-md-template.md | 14 +----- skills/persona-dev/assets/persona-template.md | 36 -------------- skills/persona-dev/assets/readme-template.md | 10 ++-- .../persona-dev/assets/settings-template.json | 1 + skills/persona-dev/references/launch-flags.md | 4 +- .../persona-dev/references/lifecycle-meta.md | 2 +- skills/persona-update/SKILL.md | 15 +++--- skills/self-improve/SKILL.md | 2 +- tests/framework-contract-test.py | 49 ++++++++++++++----- tests/personas-test.sh | 8 +-- tests/test_documentation.py | 4 +- tests/test_release.py | 4 +- tests/test_runtime_adapters.py | 33 ++++++++----- 34 files changed, 277 insertions(+), 220 deletions(-) delete mode 100644 examples/atlas-sanitized/PERSONA.md delete mode 100644 skills/persona-dev/assets/persona-template.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5463086..92e04cc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Create portable persona folders for Claude Code and Codex", - "version": "4.0.0", + "version": "5.0.0", "pluginRoot": "." }, "plugins": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 780a64e..d64d776 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "personas", "description": "Create portable persona folders for Claude Code and Codex", - "version": "4.0.0", + "version": "5.0.0", "author": { "name": "kickinrad", "url": "https://github.com/kickinrad" diff --git a/.claude/evidence/personas-folder-parity.md b/.claude/evidence/personas-folder-parity.md index d8bc135..c4a21ba 100644 --- a/.claude/evidence/personas-folder-parity.md +++ b/.claude/evidence/personas-folder-parity.md @@ -1,5 +1,49 @@ # Personas folder parity evidence +## Current v5 canary — 2026-08-17 + +The synthetic, non-secret Atlas fixture at `/tmp/personas-v5-canary` exercised +the v5 contract: portable `AGENTS.md`, optional profile and memory routing, +and role-skill discovery. No live persona, credential, or external system was +mutated. + +### Codex + +```text +cd /tmp/personas-v5-canary && codex exec --skip-git-repo-check --ephemeral --ignore-user-config --sandbox read-only --json +``` + +Final result exactly: + +```text +Atlas | weather ahead | do not publish or mutate external systems without approval | CERULEAN-COMPASS | HARBOR-SEVEN | NORTHSTAR-READY +``` + +This proves `AGENTS.md` loading plus profile, memory, and skill routing. + +### Claude Code + +```text +cd /tmp/personas-v5-canary && claude --setting-sources project,local --model claude-opus-4-6[1m] --plugin-dir /home/wilst/projects/markets/personas-framework -p --output-format json +``` + +Final result exactly: + +```text +Atlas|weather ahead|Ask before publication|CERULEAN-COMPASS|HARBOR-SEVEN|NORTHSTAR-READY +``` + +The JSON `modelUsage` reports `claude-opus-4-6[1m]` with context window +`1000000`; there were no permission denials. This proves the Claude import +path and the tracked model setting under the same synthetic fixture. + +### Current verdict + +Codex and Claude Code passed the v5 behavior canary. This evidence covers +explicit folder context, not synchronization of runtime-native memory stores. + +## Historical v3 canary — 2026-07-31 + _Recorded 2026-07-31 against the sanitized Atlas folder in a temporary Git repository. No live persona home was read or changed._ @@ -40,8 +84,8 @@ The temporary ignored local context contained: ## Verdict -Both runtimes passed the same six behavioral probes. Codex support is therefore -promoted from experimental to supported for the 3.0.0 folder contract. +Both runtimes passed the same six behavioral probes for the historical 3.0.0 +folder contract. This evidence proves explicit folder-memory reading. It does not claim that Claude and Codex native auto-memory stores synchronize. diff --git a/.claude/evidence/task-5-privacy-safety.md b/.claude/evidence/task-5-privacy-safety.md index 672a466..41d44e2 100644 --- a/.claude/evidence/task-5-privacy-safety.md +++ b/.claude/evidence/task-5-privacy-safety.md @@ -1,4 +1,7 @@ -# Task 5 — Privacy and publishing safety evidence +# Historical Task 5 — Privacy and publishing safety evidence + +> Historical evidence only. It documents superseded guards and Cloud machinery; +> it is not current v5 privacy or runtime proof. Date: 2026-07-28 @@ -15,10 +18,11 @@ Date: 2026-07-28 opens no listener, and emits no telemetry. Runtime-provider requests occur only when the user starts that runtime. -## Guard contract +## Retired guard evidence -`scripts/public-repo-guard.sh` is deterministic and does not parse a user shell -command. Invoke it at a Git/release seam: +At Task 5, `scripts/public-repo-guard.sh` was the deterministic guard. The +commands below record that retired implementation; they are not v5 procedures +and must not be invoked as current release checks. ```bash scripts/public-repo-guard.sh --check-staged @@ -29,10 +33,10 @@ scripts/public-repo-guard.sh --check-repository The generated PreToolUse hook is advisory only. It is deliberately not the security boundary. -## Claude Cloud contract +## Retired Claude Cloud evidence -The Cloud creation flow is separately authorized: obtain/validate the GitHub -repository first, then run: +Task 5's Cloud flow required a separately authorized GitHub repository and the +following now-retired preflight: ```bash .claude/hooks/public-repo-guard.sh --cloud-preflight OWNER/REPO @@ -45,17 +49,17 @@ unauthenticated, or empty evidence exits 2 before profile or memory context is loaded, written, or committed. This task did not contact GitHub or create a repository. -## Required integration owned by Task 6/root +## Superseded integration handoff -Add these checks to the repository CI/release workflow before artifact creation -or publish: +Task 5 handed the following checks to Task 6. The v5 contract retired that +machinery; this block is historical evidence, not an outstanding requirement: ```yaml - run: test "${{ github.event.repository.private }}" = true - run: scripts/public-repo-guard.sh --check-tree "${{ github.sha }}" ``` -The generated SessionStart hook treats the committed +The then-generated SessionStart hook treated the committed `.persona-cloud-repository` marker as the Cloud signal and executes the offline `--check-cloud-binding` before it supplies Cloud context. Exact private visibility remains enforced at creation, local verification, and CI. Cloud diff --git a/.claude/evidence/task-6-runtime-adapters.md b/.claude/evidence/task-6-runtime-adapters.md index 76117f3..7547c05 100644 --- a/.claude/evidence/task-6-runtime-adapters.md +++ b/.claude/evidence/task-6-runtime-adapters.md @@ -1,4 +1,7 @@ -# Task 6 — Runtime adapter acceptance +# Historical Task 6 — Runtime adapter acceptance + +> Historical evidence only. It predates the 5.0.0 `AGENTS.md` authority and +> import-only Claude adapter, so it is not current runtime proof. Date: 2026-07-28 diff --git a/.claude/evidence/task-9-release-preparation.md b/.claude/evidence/task-9-release-preparation.md index a28dec0..c7720c8 100644 --- a/.claude/evidence/task-9-release-preparation.md +++ b/.claude/evidence/task-9-release-preparation.md @@ -1,4 +1,8 @@ -# Task 9 — Release preparation +# Historical Task 9 — Release preparation (3.0.0) + +> Historical evidence only. It does not prove the current 5.0.0 contract, +> whose release metadata and validation live in the current source release +> surfaces and tests. Date: 2026-07-30 diff --git a/.claude/plans/personas-folder-parity.md b/.claude/plans/personas-folder-parity.md index bb9c22a..6008143 100644 --- a/.claude/plans/personas-folder-parity.md +++ b/.claude/plans/personas-folder-parity.md @@ -1,15 +1,17 @@ -# Plan: Personas as Folders, With Runtime Parity +# Plan: Personas v5 as Folders, With Runtime Parity _Created 2026-07-31 from the accepted simplification and parity direction._ ## Execution status -Implemented on 2026-07-31 as Persona Manager `3.0.0`. +Current contract: Personas `5.0.0`, with `AGENTS.md` as the sole portable +persona definition and an import-only `CLAUDE.md` adapter. The completed 3.0 +work below is historical planning context, not current authority. - Tasks 0–6 and 8–9 are complete in source. -- Task 7's finished-folder canaries passed in Claude Code 2.1.220 and Codex CLI - 0.146.0; evidence is recorded in - [personas-folder-parity.md](../evidence/personas-folder-parity.md). +- The current v5 canary is recorded in + [personas-folder-parity.md](../evidence/personas-folder-parity.md): Codex + and Claude Code loaded the synthetic Atlas fixture without runtime mutation. - Codex also discovers the repository's local marketplace and Persona Manager package. The plugin was not installed into the maintainer's live Codex or Claude configuration during source validation. @@ -38,7 +40,7 @@ product. 2. **Parity is observable behavior.** Runtime files may differ, but identity, working doctrine, skill availability, and explicit folder memory must behave equivalently. -3. **One portable authority.** `PERSONA.md` owns identity, voice, role, +3. **One portable authority.** `AGENTS.md` owns identity, voice, role, boundaries, and shared operating doctrine. 4. **Native adapters stay thin.** `CLAUDE.md`, `AGENTS.md`, `.claude/`, and `.codex/` contain only what their runtime needs to load the portable source. @@ -55,9 +57,8 @@ product. ```text atlas/ -├── PERSONA.md # portable identity and operating doctrine -├── CLAUDE.md # Claude Code entry point -├── AGENTS.md # Codex entry point +├── AGENTS.md # portable identity and operating doctrine +├── CLAUDE.md # Claude Code import of AGENTS.md ├── README.md # human orientation ├── skills/ # shared role workflows ├── .claude/ @@ -97,7 +98,7 @@ Therefore: 1. A sanitized persona can be understood without running a command: its identity, instructions, skills, runtime adapters, and private-state boundary are visible in the folder. -2. A newly generated persona contains `PERSONA.md`, `CLAUDE.md`, `AGENTS.md`, +2. A newly generated persona contains `AGENTS.md`, importing `CLAUDE.md`, shared `skills/`, minimal `.claude/` configuration, and minimal `.codex/` configuration. 3. Claude Code and Codex independently pass a clean-room identity probe that @@ -185,7 +186,7 @@ Replace file-presence parity with observable persona adoption. **Work** -1. Define the portable authority as `PERSONA.md` and document what remains in +1. Define the portable authority as `AGENTS.md` and document what remains in `CLAUDE.md` and `AGENTS.md`. 2. Define one sanitized acceptance persona with a distinctive name, role, voice rule, refusal boundary, and role skill. @@ -264,9 +265,9 @@ receiving approval. **Work** -1. Add `PERSONA.md` as the single source for identity, voice, boundaries, and +1. Add `AGENTS.md` as the single source for identity, voice, boundaries, and runtime-neutral procedure. -2. Make the runtime entry files thin, explicit loaders of `PERSONA.md`, shared +2. Make `CLAUDE.md` an explicit import of `AGENTS.md`, with shared skills, and optional ignored user context. 3. Have `persona-dev` write the approved folder directly with normal agent file operations; rely on plan review, diffs, and Git for recovery. @@ -302,12 +303,12 @@ domain model. **Work** -1. Make `CLAUDE.md` load `PERSONA.md`, shared skills, and optional ignored user +1. Make `CLAUDE.md` import `AGENTS.md`; `AGENTS.md` routes optional user context. 2. Reduce `.claude/settings.json` to settings needed for persona behavior; remove repository-policy and plugin-installation management. 3. Retain an output-style adapter only if the Claude acceptance probe shows it - materially improves persona adoption beyond `PERSONA.md`. + materially improves persona adoption beyond `AGENTS.md`. 4. Add no default hook without a named behavior that instructions or skills cannot provide reliably. @@ -337,13 +338,13 @@ Give Codex equivalent outcomes through its documented native surfaces. **Work** -1. Make `AGENTS.md` load `PERSONA.md`, shared skills, and optional ignored user +1. Make `AGENTS.md` the portable definition, route optional user context, and context without referring to Claude as the canonical runtime. 2. Generate minimal trusted-project `.codex/config.toml` settings only where they affect persona behavior. 3. Map any retained persona-critical Claude lifecycle behavior to documented Codex hook events. Do not create hooks merely for structural symmetry. -4. Express voice and response shape through `PERSONA.md`/`AGENTS.md`, since +4. Express voice and response shape through `AGENTS.md`, since Codex has no documented first-class equivalent to Claude output styles. 5. Verify the installed Codex plugin exposes the same shared skills. @@ -445,7 +446,7 @@ Explain the simple product that now exists. 1. Lead with “a persona is a folder” and show the folder immediately. 2. Show one plugin install and one guided creation request per supported runtime. -3. Explain `.claude/` and `.codex/` as native adapters around `PERSONA.md`. +3. Explain `.claude/` and `.codex/` as native adapters around `AGENTS.md`. 4. Explain portable folder memory versus runtime-native memory in one short section. 5. Reduce Cloud guidance to publishability, private-repository recommendation, diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f26435f..3cf386c 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "personas", "description": "Create portable persona folders for Claude Code and Codex", - "version": "4.0.0", + "version": "5.0.0", "author": { "name": "kickinrad", "url": "https://github.com/kickinrad" diff --git a/CHANGELOG.md b/CHANGELOG.md index b019b39..a65e59d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 5.0.0 — 2026-08-17 + +### Changed + +- Made `AGENTS.md` the single portable persona definition. +- Reduced `CLAUDE.md` to a native import of that definition. + ## 4.0.0 — 2026-08-06 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 10198d8..7593e1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,9 @@ This repository contains Personas framework mechanics for persona lifecycle. Persona homes are external, independent repositories and are evidence—not normalization targets. +This repository-development file is not a generated persona adapter; the +persona `CLAUDE.md` import-only budget does not apply here. + ## Authorities - Framework source owns lifecycle procedure, templates, runtime declarations, @@ -29,7 +32,7 @@ plugin-shipped `self-improve` skill is canonical. 1. Snapshot source and preserve unrelated work before editing. 2. Work in an isolated migration branch or worktree. -3. Keep this release aligned at `4.0.0`; do not create a documentation-only or +3. Keep this release aligned at `5.0.0`; do not create a documentation-only or staging version bump. 4. Declare runtime support in each plugin's `interop/capabilities.json`. Claude support is native only where proven. Missing adapters are diff --git a/MIGRATION.md b/MIGRATION.md index 055374c..25ba053 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,14 +1,13 @@ -# Migration to Personas 4.0.0 +# Migration to Personas 5.0.0 -Version 3.0.0 returned to the original product model: a persona is a folder. -Version 4.0.0 keeps that contract and renames the plugin itself to `personas`. +Version 5.0.0 makes `AGENTS.md` the one portable persona definition; Claude +Code imports it through a deliberately thin `CLAUDE.md` adapter. ## Target folder ```text -PERSONA.md -CLAUDE.md AGENTS.md +CLAUDE.md skills/ .claude/settings.json .codex/config.toml @@ -20,10 +19,10 @@ the migration before writing. ## Move portable meaning once -Move shared identity, voice, role, and boundaries into `PERSONA.md`. Reduce -`CLAUDE.md` and `AGENTS.md` to native entry points that load it. Keep reusable -role procedure in `skills/`. Claude Code and Codex then consume the same -portable persona through their own native entry files. +Move shared identity, voice, role, and boundaries into `AGENTS.md`. Reduce +`CLAUDE.md` to an `@AGENTS.md` import. Keep reusable role procedure in +`skills/`. Claude Code and Codex then consume the same portable persona through +their native entry files. Do not bulk-replace persona-owned content. Preserve `user/profile.md`, `user/memory/`, local settings, and integrations. diff --git a/README.md b/README.md index 377bbf4..2e69be9 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@ Markdown, shared skills, and small native adapters for **Claude Code local**, ```text atlas/ -├── PERSONA.md # identity, role, voice, and boundaries -├── CLAUDE.md # Claude Code entry point -├── AGENTS.md # Codex entry point +├── AGENTS.md # portable identity, role, voice, and boundaries +├── CLAUDE.md # Claude Code import of AGENTS.md ├── skills/ # reusable role workflows ├── .claude/settings.json # native Claude project settings ├── .codex/config.toml # native Codex project settings @@ -66,12 +65,12 @@ inspect a finished folder first. ## How the folder works -`PERSONA.md` is the portable source of truth. It contains the collaborator's +`AGENTS.md` is the portable source of truth. It contains the collaborator's identity and behavior without referring to a particular AI runtime. -Claude Code discovers `CLAUDE.md`. Codex discovers `AGENTS.md`. Each native -entry point loads the same `PERSONA.md`, optional local user context, and -relevant workflows under `skills/`. +Codex discovers `AGENTS.md`. Claude Code discovers `CLAUDE.md`, which imports +that same definition. Reusable workflows live under `skills/`; private local +context remains outside the portable definition. The `.claude/` and `.codex/` directories contain only native project settings. They are adapters, not competing persona definitions. Personas adds no @@ -121,7 +120,7 @@ For a few instructions, you should. Personas becomes useful when the collaborator has a distinct role, reusable workflows, private local context, or needs to work in both Claude Code and Codex. -It adds a neutral persona definition, shared skills, native runtime entry +It adds one portable persona definition, shared skills, native runtime entry points, and a careful update workflow while keeping every file readable. ## Runtime support diff --git a/RELEASE.md b/RELEASE.md index f130ba6..02e664e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,13 +1,13 @@ -# Personas 4.0.0 release preparation +# Personas 5.0.0 release preparation -Personas 4.0.0 keeps personas simple: readable folders with one +Personas 5.0.0 keeps personas simple: readable folders with one portable definition, shared role skills, and small native adapters for Claude Code and Codex. Highlights: -- `PERSONA.md` owns portable identity and behavior; -- Claude Code and Codex receive native project entry points and settings; +- `AGENTS.md` owns portable identity and behavior; +- Claude Code imports `AGENTS.md`; Codex receives it natively; - optional explicit folder memory works without claiming native-memory sync; - Cloud uses the same publishable folder with no GitHub token or visibility protocol; @@ -31,5 +31,5 @@ Read [MIGRATION.md](MIGRATION.md), [SUPPORT.md](SUPPORT.md), and 1. Review the final diff and complete source gate. 2. Confirm the recorded Claude and Codex parity evidence remains current. 3. Commit and push only with explicit authorization. -4. Create tag `personas--v4.0.0` at the final green release commit. +4. Create tag `personas--v5.0.0` at the final green release commit. 5. Publish the release notes and observe hosted CI. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index e9a1782..3281c52 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -14,7 +14,7 @@ content until ownership is clear. ## The persona does not sound or behave right -Confirm the runtime loaded its native entry file, then read `PERSONA.md`. +Confirm the runtime loaded its native entry file, then read `AGENTS.md`. Identity, voice, role, and boundaries belong there. Repeated multi-step procedure belongs in a role skill. Avoid copying the same correction into both `CLAUDE.md` and `AGENTS.md`. @@ -23,8 +23,8 @@ procedure belongs in a role skill. Avoid copying the same correction into both Start Codex from the persona folder and confirm `AGENTS.md` is discovered. Trust the project if Codex asks before applying `.codex/config.toml`. Verify -that `AGENTS.md` points to `PERSONA.md`, optional `user/` context, and the -relevant `skills/` directory. +that `AGENTS.md` contains the portable definition and the relevant `skills/` +directory. Codex support was earned by the release canary. Do not describe a new loader or setting as supported until it passes the same parity probes. diff --git a/examples/atlas-sanitized/AGENTS.md b/examples/atlas-sanitized/AGENTS.md index bee7258..f471a61 100644 --- a/examples/atlas-sanitized/AGENTS.md +++ b/examples/atlas-sanitized/AGENTS.md @@ -1,6 +1,27 @@ -# Atlas in Codex +# Atlas -Read `PERSONA.md` as the authority for identity, voice, role, boundaries, and -working doctrine. Read `user/profile.md` and `user/memory/MEMORY.md` when they -exist locally, and never invent them when absent. Read the relevant workflow -under `skills/` before using it. +> 🧭 A careful reviewer for small software changes. + +## Role and authority + +Atlas reviews bounded software changes, identifies the most consequential risk, +and recommends the smallest useful verification step. Act on reversible work +once the outcome is clear; ask when a material choice cannot be discovered or +safely assumed. Use `vault:curator` for Vault discovery and mutation when that +capability is available. Read `user/profile.md` and `user/memory/MEMORY.md` +when present; their absence is normal. + +## Voice + +Be clear, direct, warm, and concrete. Lead with evidence, distinguish findings +from optional improvements, and never manufacture certainty. + +## Boundaries + +- Never invent access, evidence, memories, or completed work. +- Ask before destructive actions, external publication, or credential use. +- Keep private local context out of Git and runtime-specific mechanics out of + this portable definition. + +Find reusable role procedures in `skills/`; read the relevant `SKILL.md` +completely before following it. diff --git a/examples/atlas-sanitized/CLAUDE.md b/examples/atlas-sanitized/CLAUDE.md index e7592b6..0a84931 100644 --- a/examples/atlas-sanitized/CLAUDE.md +++ b/examples/atlas-sanitized/CLAUDE.md @@ -1,6 +1,3 @@ # Atlas in Claude Code -Read `PERSONA.md` as the authority for identity, voice, role, boundaries, and -working doctrine. Read `user/profile.md` and `user/memory/MEMORY.md` when they -exist locally, and never invent them when absent. Read the relevant workflow -under `skills/` before using it. +@AGENTS.md diff --git a/examples/atlas-sanitized/PERSONA.md b/examples/atlas-sanitized/PERSONA.md deleted file mode 100644 index 5c7d51d..0000000 --- a/examples/atlas-sanitized/PERSONA.md +++ /dev/null @@ -1,30 +0,0 @@ -# Atlas - -> 🧭 A careful reviewer for small software changes. - -## Role - -Atlas reviews bounded software changes, identifies the most consequential risk, -and recommends the smallest useful verification step. - -## Working approach - -1. Establish the intended behavior and changed surface. -2. Trace the affected path before judging it. -3. Separate correctness findings from optional improvements. -4. Lead with evidence and a concrete next step. - -## Voice - -Be direct, calm, and concise. Explain technical risk in plain language. - -## Boundaries - -- Never claim a test ran without its output. -- Do not expand a bounded review into unrelated refactoring. -- Ask before destructive changes or external publication. -- Treat `user/` as private local context that never belongs in Git. - -## Skills - -Use `skills/atlas-review/SKILL.md` for a structured change review. diff --git a/examples/atlas-sanitized/README.md b/examples/atlas-sanitized/README.md index daa5cd4..a66895b 100644 --- a/examples/atlas-sanitized/README.md +++ b/examples/atlas-sanitized/README.md @@ -6,9 +6,9 @@ credentials, or external services. Open the folder and inspect: -- `PERSONA.md` for Atlas's shared identity and working behavior; +- `AGENTS.md` for Atlas's portable identity and working behavior; - `CLAUDE.md` and `.claude/settings.json` for the Claude Code adapter; -- `AGENTS.md` and `.codex/config.toml` for the Codex adapter; +- `.codex/config.toml` for Codex settings; - `skills/atlas-review/SKILL.md` for one portable role workflow. The example intentionally has no `user/` directory. A real local persona may diff --git a/interop/capabilities.json b/interop/capabilities.json index a1ff0e7..5f9c800 100644 --- a/interop/capabilities.json +++ b/interop/capabilities.json @@ -1,9 +1,9 @@ { - "schemaVersion": 4, + "schemaVersion": 5, "component": "personas", - "version": "4.0.0", + "version": "5.0.0", "residency": {"claude-code": "on-demand", "codex": "on-demand"}, - "portableAuthority": "PERSONA.md", + "portableAuthority": "AGENTS.md", "portableSkills": ["persona-dev", "persona-update", "self-improve"], "folderMemory": { "path": "user/memory/MEMORY.md", @@ -19,6 +19,7 @@ "claude-code": { "status": "native", "instructions": "CLAUDE.md", + "imports": ["AGENTS.md"], "projectSettings": ".claude/settings.json", "skills": true, "acceptanceProbes": ["identity", "voice", "boundary", "skill", "profile", "explicit-memory"] diff --git a/skills/persona-dev/SKILL.md b/skills/persona-dev/SKILL.md index bfb5a28..fee7c73 100644 --- a/skills/persona-dev/SKILL.md +++ b/skills/persona-dev/SKILL.md @@ -17,13 +17,12 @@ private local context, and external knowledge sources distinct. choosing a home across native, WSL, Windows, Desktop, or Cowork. Read `references/launch-flags.md` when configuring a Claude launcher. Read `references/research-toolkit.md` when the persona needs a new capability. -3. Present one complete folder plan. Include `PERSONA.md`, both runtime entry - files, shared skills, minimal native settings, ignored local context, and - any requested integration. Write nothing before approval. +3. Present one complete folder plan. Include the portable `AGENTS.md`, importing + `CLAUDE.md`, shared skills, minimal native settings, ignored local context, + and any requested integration. Write nothing before approval. 4. After approval, create the folder directly with normal file operations: - - `PERSONA.md` owns identity, voice, role, boundaries, and shared doctrine; - - `CLAUDE.md` is the Claude Code entry point; - - `AGENTS.md` is the Codex entry point; + - `AGENTS.md` owns identity, voice, role, boundaries, and shared doctrine; + - `CLAUDE.md` imports `AGENTS.md` for Claude Code; - `.claude/settings.json` and `.codex/config.toml` contain minimal native project settings; - `skills/` contains portable role workflows; @@ -34,8 +33,8 @@ private local context, and external knowledge sources distinct. visibility, or memory simulation. 7. Keep everything tracked in Git safe to publish. Never put profile, memory, local settings, connections, or credentials in tracked files. -8. Inspect the resulting tree and exact diff. Confirm both entry files load - `PERSONA.md`, optional user context, and relevant shared skills. +8. Inspect the resulting tree and exact diff. Confirm `AGENTS.md` is the only + resident persona authority and `CLAUDE.md` imports it. 9. Ask separately before plugin installation, repository creation, external connection, publishing, or changes to an existing persona's private data. diff --git a/skills/persona-dev/assets/agents-template.md b/skills/persona-dev/assets/agents-template.md index 5256662..b3e8f7f 100644 --- a/skills/persona-dev/assets/agents-template.md +++ b/skills/persona-dev/assets/agents-template.md @@ -1,16 +1,27 @@ -# {PersonaName} in Codex +# {PersonaName} -This folder is {PersonaName}'s persona home. +> {emoji} {role description without personal facts} -Before acting: +## Role and authority -1. Read `PERSONA.md` as the authority for identity, voice, role, boundaries, - and shared operating doctrine. -2. Read `user/profile.md` and `user/memory/MEMORY.md` when they exist locally. - Their absence is normal in a fresh or cloud checkout; never invent their - contents. -3. Read the relevant role workflow under `skills/` before using it. +{PersonaName} helps with {role description without personal facts}. Act on +reversible work once the outcome is clear; ask when a material choice cannot be +discovered or safely assumed. Use `vault:curator` for Vault discovery and +mutation when that capability is available. Read `user/profile.md` and +`user/memory/MEMORY.md` when present; their absence is normal. -Apply `PERSONA.md` throughout the session. Codex project configuration may -refine runtime mechanics, but it does not override the portable persona -definition. +## Voice + +Be clear, direct, warm, and concrete. Lead with the useful result, then include +only detail that changes understanding or action. When an explanation misses, +restate the missing premise plainly; do not manufacture certainty. + +## Boundaries + +- Never invent access, evidence, memories, or completed work. +- Ask before destructive actions, external publication, or credential use. +- Keep private local context out of Git and runtime-specific mechanics out of + this portable definition. + +Find reusable role procedures in `skills/`; read the relevant `SKILL.md` +completely before following it. diff --git a/skills/persona-dev/assets/claude-md-template.md b/skills/persona-dev/assets/claude-md-template.md index e3e4fa8..f30591e 100644 --- a/skills/persona-dev/assets/claude-md-template.md +++ b/skills/persona-dev/assets/claude-md-template.md @@ -1,15 +1,3 @@ # {PersonaName} in Claude Code -This folder is {PersonaName}'s persona home. - -Before acting: - -1. Read `PERSONA.md` as the authority for identity, voice, role, boundaries, - and shared operating doctrine. -2. Read `user/profile.md` and `user/memory/MEMORY.md` when they exist locally. - Their absence is normal in a fresh or cloud checkout; never invent their - contents. -3. Read the relevant role workflow under `skills/` before using it. - -Apply `PERSONA.md` throughout the session. Claude-specific settings may refine -runtime mechanics, but they do not override the portable persona definition. +@AGENTS.md diff --git a/skills/persona-dev/assets/persona-template.md b/skills/persona-dev/assets/persona-template.md deleted file mode 100644 index 79c70b5..0000000 --- a/skills/persona-dev/assets/persona-template.md +++ /dev/null @@ -1,36 +0,0 @@ -# {PersonaName} - -> {emoji} {role description without personal facts} - -## Role - -{PersonaName} helps with {role description without personal facts}. - -## Working approach - -1. Understand the desired outcome and relevant context. -2. Act on reversible work once the outcome is clear. Ask only when a material - choice cannot be discovered or safely assumed. -3. Use the smallest suitable workflow from `skills/`. -4. Keep the current result, decision, and next action visible. Show evidence - for consequential claims and verify completed work. - -## Voice - -Be clear, direct, warm, and concrete. Lead with the useful result, then disclose -only detail that changes understanding or action. Prefer readable sentences to -compressed shorthand. When Wils says "wait" or signals that an explanation did -not land, back up and re-pitch it with the missing premise in plain language. -Do not perform a fictional character or manufacture certainty. - -## Boundaries - -- Never invent access, evidence, memories, or completed work. -- Ask before destructive actions, external publication, or credential use. -- Treat `user/` as private local context that never belongs in Git. -- Keep runtime-specific mechanics out of this portable definition. - -## Skills - -Role workflows live under `skills/`. Read the relevant `SKILL.md` completely -before following it. diff --git a/skills/persona-dev/assets/readme-template.md b/skills/persona-dev/assets/readme-template.md index 56bb655..ab0c8a6 100644 --- a/skills/persona-dev/assets/readme-template.md +++ b/skills/persona-dev/assets/readme-template.md @@ -4,15 +4,13 @@ ## Use -Open this folder in Claude Code or Codex. Each runtime loads its native entry -file, then reads the shared definition in `PERSONA.md` and relevant workflows -under `skills/`. +Open this folder in Claude Code or Codex. Codex loads the portable definition in +`AGENTS.md`; Claude Code imports that same definition through `CLAUDE.md`. ## Folder map -- `PERSONA.md` — portable identity, role, voice, and boundaries. -- `CLAUDE.md` — Claude Code entry point. -- `AGENTS.md` — Codex entry point. +- `AGENTS.md` — portable identity, role, voice, boundaries, and Codex entry. +- `CLAUDE.md` — Claude Code import of `AGENTS.md`. - `.claude/settings.json` — native Claude project settings. - `.codex/config.toml` — native Codex project settings. - `skills/` — portable role workflows. diff --git a/skills/persona-dev/assets/settings-template.json b/skills/persona-dev/assets/settings-template.json index 9cf7810..d0f4382 100644 --- a/skills/persona-dev/assets/settings-template.json +++ b/skills/persona-dev/assets/settings-template.json @@ -1,4 +1,5 @@ { + "model": "claude-opus-4-6[1m]", "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true, diff --git a/skills/persona-dev/references/launch-flags.md b/skills/persona-dev/references/launch-flags.md index 0f616cb..9ab395c 100644 --- a/skills/persona-dev/references/launch-flags.md +++ b/skills/persona-dev/references/launch-flags.md @@ -1,8 +1,8 @@ # Claude launch flags Read this reference only when configuring the Claude launcher for a persona. -Codex loads the persona through its workspace and `AGENTS.md`; it does not use -`.claude-flags`. +Codex loads the persona through its workspace and `AGENTS.md`; Claude Code +imports that definition through `CLAUDE.md`. Codex does not use `.claude-flags`. ## Contract diff --git a/skills/persona-dev/references/lifecycle-meta.md b/skills/persona-dev/references/lifecycle-meta.md index 2524ac5..b8fcd03 100644 --- a/skills/persona-dev/references/lifecycle-meta.md +++ b/skills/persona-dev/references/lifecycle-meta.md @@ -2,7 +2,7 @@ | Concern | Owning surface | |---|---| -| Identity, voice, role, boundaries | `PERSONA.md` | +| Identity, voice, role, boundaries | `AGENTS.md` | | Claude Code loading and mechanics | `CLAUDE.md`, `.claude/` | | Codex loading and mechanics | `AGENTS.md`, `.codex/` | | Reusable role procedure | `skills//SKILL.md` | diff --git a/skills/persona-update/SKILL.md b/skills/persona-update/SKILL.md index 24552af..a2b6d37 100644 --- a/skills/persona-update/SKILL.md +++ b/skills/persona-update/SKILL.md @@ -11,16 +11,19 @@ Reconcile framework mechanics without normalizing the persona. voice, user data, role procedure, integrations, and external knowledge. 2. Resolve the installed Personas root and read the current templates under `skills/persona-dev/assets/`. Stop if the plugin root is unavailable. -3. Compare the folder with the portable contract: `PERSONA.md`, `CLAUDE.md`, - `AGENTS.md`, shared skills, `.claude/settings.json`, `.codex/config.toml`, - `.gitignore`, and optional ignored `user/` context. +3. Compare the folder with the portable contract: authoritative `AGENTS.md`, + importing `CLAUDE.md`, shared skills, `.claude/settings.json`, + `.codex/config.toml`, `.gitignore`, and optional ignored `user/` context. 4. Classify each difference as a framework adapter change, persona-owned customization, obsolete machinery, or ambiguity. 5. Present the reconciliation plan before writing. Preserve persona-owned meaning and ask where ownership is ambiguous. -6. Remove retired Cloud markers, visibility workflows, publishing guards, - framework stamps, and generated hooks only after showing their exact paths - and receiving approval for the persona-local change. +6. Remove a retired portable definition only after its meaning is closed into + `AGENTS.md`, its references are closed, and approval covers the persona-local + change. Before retiring a nonempty `.claude/output-styles/*.md`, review its + voice cues and consolidate useful cues into `AGENTS.md` only when active + persona docs still need them; otherwise preserve it for review. Remove other + retired machinery only after showing its exact paths. 7. Inspect the final tree and diff. Batch updates repeat this procedure independently for each persona. diff --git a/skills/self-improve/SKILL.md b/skills/self-improve/SKILL.md index 0597bd5..48453c4 100644 --- a/skills/self-improve/SKILL.md +++ b/skills/self-improve/SKILL.md @@ -9,7 +9,7 @@ Improve the persona without duplicating authority. ## Route the change -- Identity, voice, role, and boundaries belong in `PERSONA.md`. +- Identity, voice, role, and boundaries belong in `AGENTS.md`. - A recurring multi-step procedure belongs in one portable role skill. - Explicit persona memory belongs in ignored `user/memory/MEMORY.md` with user approval. diff --git a/tests/framework-contract-test.py b/tests/framework-contract-test.py index 48a4091..690ed4b 100644 --- a/tests/framework-contract-test.py +++ b/tests/framework-contract-test.py @@ -9,7 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] -VERSION = "4.0.0" +VERSION = "5.0.0" ASSETS = ROOT / "skills/persona-dev/assets" @@ -31,21 +31,48 @@ def test_persona_assets_define_one_portable_folder(self) -> None: "codex-config-template.toml", "gitignore-template", "memory-template.md", - "persona-template.md", "profile-template.md", "readme-template.md", "settings-template.json", } self.assertEqual({path.name for path in ASSETS.iterdir() if path.is_file()}, expected) - persona = (ASSETS / "persona-template.md").read_text(encoding="utf-8") - self.assertIn("# {PersonaName}", persona) - self.assertIn("## Voice", persona) - self.assertIn("## Boundaries", persona) - for adapter in ("claude-md-template.md", "agents-template.md"): - text = (ASSETS / adapter).read_text(encoding="utf-8") - self.assertIn("PERSONA.md", text) - self.assertIn("user/profile.md", text) - self.assertIn("user/memory/MEMORY.md", text) + agents = (ASSETS / "agents-template.md").read_text(encoding="utf-8") + claude = (ASSETS / "claude-md-template.md").read_text(encoding="utf-8") + self.assertLessEqual(len(agents.split()), 300) + self.assertLessEqual(len(claude.split()), 80) + for phrase in ("## Role and authority", "## Voice", "## Boundaries", "vault:curator", "skills/"): + self.assertIn(phrase, agents) + for forbidden in ("PERSONA.md", "## Working approach", "Before acting:", "enabledPlugins", "extraKnownMarketplaces", "1. "): + self.assertNotIn(forbidden, agents) + self.assertEqual(claude.splitlines()[-1], "@AGENTS.md") + self.assertNotIn("PERSONA.md", claude) + + def test_portable_skills_stay_compact_and_current(self) -> None: + for skill in (ROOT / "skills").glob("*/SKILL.md"): + with self.subTest(skill=skill): + text = skill.read_text(encoding="utf-8") + self.assertLessEqual(len(text.split()), 500) + self.assertNotIn("PERSONA.md", text) + + def test_active_contract_has_no_duplicate_or_legacy_authority(self) -> None: + active = ( + ROOT / "interop/capabilities.json", + ROOT / "README.md", + ROOT / "MIGRATION.md", + ROOT / "RELEASE.md", + ROOT / "SUPPORT.md", + ROOT / "TROUBLESHOOTING.md", + ROOT / "ACTIVATION.md", + *(ROOT / "skills").rglob("*"), + *(ROOT / "examples/atlas-sanitized").rglob("*"), + ) + for path in active: + if not path.is_file(): + continue + with self.subTest(path=path): + text = path.read_text(encoding="utf-8", errors="ignore") + self.assertNotIn("PERSONA.md", text) + self.assertFalse((ASSETS / "persona-template.md").exists()) def test_no_runtime_or_cli_enforcement_product_remains(self) -> None: for relative in ( diff --git a/tests/personas-test.sh b/tests/personas-test.sh index 297351c..a4523a7 100644 --- a/tests/personas-test.sh +++ b/tests/personas-test.sh @@ -16,21 +16,23 @@ check() { fi } -echo "Testing: personas 4.0.0" +echo "Testing: personas 5.0.0" for manifest in .claude-plugin/plugin.json .codex-plugin/plugin.json; do version=$(jq -r '.version // empty' "$ROOT/$manifest") - [[ "$version" == 4.0.0 ]] && check "$manifest version" pass || check "$manifest version" "expected 4.0.0, got ${version:-missing}" + [[ "$version" == 5.0.0 ]] && check "$manifest version" pass || check "$manifest version" "expected 5.0.0, got ${version:-missing}" done while IFS= read -r -d '' skill; do grep -q '^---$' "$skill" && check "frontmatter: ${skill#"$ROOT"/}" pass || check "frontmatter: ${skill#"$ROOT"/}" missing done < <(find "$ROOT/skills" -name SKILL.md -type f -print0) -for relative in PERSONA.md CLAUDE.md AGENTS.md .claude/settings.json .codex/config.toml skills/atlas-review/SKILL.md; do +for relative in CLAUDE.md AGENTS.md .claude/settings.json .codex/config.toml skills/atlas-review/SKILL.md; do [[ -f "$ROOT/examples/atlas-sanitized/$relative" ]] && check "example: $relative" pass || check "example: $relative" missing done +[[ ! -e "$ROOT/examples/atlas-sanitized/PERSONA.md" ]] && check "example has no legacy portable definition" pass || check "example has no legacy portable definition" present + secret_hits="" while IFS= read -r -d '' file; do if grep -qE '(eyJ[A-Za-z0-9_-]{20,}|GOCSPX-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}|BEGIN[ ]PRIVATE[ ]KEY)' "$file" 2>/dev/null; then diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 6f9886a..7a66ca0 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -34,7 +34,7 @@ def test_first_screen_explains_folder_outcome_support_and_install(self) -> None: "claude code local", "claude code cloud", "codex", - "persona.md", + "agents.md", "/plugin marketplace add kickinrad/personas-framework", ): self.assertIn(phrase, first_screen) @@ -71,7 +71,6 @@ def test_memory_and_cloud_claims_are_plain_and_truthful(self) -> None: def test_sanitized_example_is_a_complete_persona_folder(self) -> None: expected = { - "PERSONA.md", "CLAUDE.md", "AGENTS.md", "README.md", @@ -84,6 +83,7 @@ def test_sanitized_example_is_a_complete_persona_folder(self) -> None: self.assertEqual(actual, expected) all_text = "\n".join((EXAMPLE / path).read_text(encoding="utf-8") for path in expected) self.assertNotRegex(all_text, r"(?i)(password|private key|real integration)") + self.assertNotIn("PERSONA.md", all_text) def test_local_markdown_links_resolve(self) -> None: for document in DOCS: diff --git a/tests/test_release.py b/tests/test_release.py index c495f70..efc35f7 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] -VERSION = "4.0.0" +VERSION = "5.0.0" class ReleasePreparationTest(unittest.TestCase): @@ -30,7 +30,7 @@ def test_release_docs_cover_layout_boundary_rollback_and_deferred_work(self) -> rollback = (ROOT / "ROLLBACK.md").read_text(encoding="utf-8") support = (ROOT / "SUPPORT.md").read_text(encoding="utf-8") old_layout = "plugins/" + "persona-manager/" - for phrase in (old_layout, "user/profile.md", "PERSONA.md", "Codex", "Mesh"): + for phrase in (old_layout, "user/profile.md", "AGENTS.md", "Codex", "Mesh"): self.assertIn(phrase, migration) self.assertIn("d3a0ed1d29177f85df9cdc28f4e51378ed0da8d9", rollback) self.assertIn("tag", release) diff --git a/tests/test_runtime_adapters.py b/tests/test_runtime_adapters.py index b3eff54..23c99fc 100644 --- a/tests/test_runtime_adapters.py +++ b/tests/test_runtime_adapters.py @@ -29,7 +29,6 @@ def render(name: str) -> str: def create_fixture(home: Path, *, private_context: bool = True) -> None: destinations = { - "persona-template.md": "PERSONA.md", "claude-md-template.md": "CLAUDE.md", "agents-template.md": "AGENTS.md", "readme-template.md": "README.md", @@ -57,18 +56,27 @@ def create_fixture(home: Path, *, private_context: bool = True) -> None: class RuntimeAdapterTest(unittest.TestCase): - def test_folder_has_one_portable_authority_and_two_native_adapters(self) -> None: + def test_folder_has_one_portable_authority_and_one_native_import(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) / "atlas" create_fixture(home) - self.assertTrue((home / "PERSONA.md").is_file()) claude = (home / "CLAUDE.md").read_text(encoding="utf-8") - codex = (home / "AGENTS.md").read_text(encoding="utf-8") - for adapter in (claude, codex): - for source in ("PERSONA.md", "user/profile.md", "user/memory/MEMORY.md", "skills/"): - self.assertIn(source, adapter) - self.assertNotIn("translate Claude", codex) - self.assertNotIn("canonicalRuntime", codex) + agents = (home / "AGENTS.md").read_text(encoding="utf-8") + self.assertFalse((home / "PERSONA.md").exists()) + self.assertEqual(claude.splitlines()[-1], "@AGENTS.md") + self.assertIn("## Role and authority", agents) + self.assertIn("vault:curator", agents) + self.assertIn("skills/", agents) + self.assertIn("user/profile.md", agents) + self.assertIn("user/memory/MEMORY.md", agents) + for forbidden in ( + "PERSONA.md", + "Before acting:", + "## Working approach", + "1. ", + "output-style", + ): + self.assertNotIn(forbidden, agents) def test_native_settings_are_minimal_parseable_and_hook_free(self) -> None: with tempfile.TemporaryDirectory() as directory: @@ -77,6 +85,7 @@ def test_native_settings_are_minimal_parseable_and_hook_free(self) -> None: claude = json.loads((home / ".claude/settings.json").read_text(encoding="utf-8")) codex = tomllib.loads((home / ".codex/config.toml").read_text(encoding="utf-8")) self.assertEqual(claude["sandbox"]["enabled"], True) + self.assertEqual(claude["model"], "claude-opus-4-6[1m]") self.assertNotIn("hooks", claude) self.assertEqual(codex["sandbox_mode"], "workspace-write") self.assertFalse(codex["sandbox_workspace_write"]["network_access"]) @@ -92,11 +101,13 @@ def test_private_folder_context_is_optional_and_ignored(self) -> None: self.assertIn("user/", ignore) self.assertTrue((local / "user/memory/MEMORY.md").is_file()) self.assertFalse((cloud / "user").exists()) - self.assertTrue((cloud / "PERSONA.md").is_file()) + self.assertTrue((cloud / "AGENTS.md").is_file()) + self.assertFalse((cloud / "PERSONA.md").exists()) def test_capability_claims_name_executable_parity_evidence(self) -> None: capabilities = json.loads((ROOT / "interop/capabilities.json").read_text(encoding="utf-8")) - self.assertEqual(capabilities["portableAuthority"], "PERSONA.md") + self.assertEqual(capabilities["portableAuthority"], "AGENTS.md") + self.assertEqual(capabilities["runtimes"]["claude-code"]["imports"], ["AGENTS.md"]) self.assertEqual(capabilities["runtimes"]["codex"]["status"], "native") for runtime in ("claude-code", "codex"): probes = capabilities["runtimes"][runtime]["acceptanceProbes"] From 40b6b88bc51b603d1bf91dc47f5bed37d5e4f32a Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 18 Aug 2026 17:44:02 -0400 Subject: [PATCH 2/5] test: verify live persona fleet contract Assisted-by: codex --- .../atlas-sanitized/.claude/settings.json | 1 + tests/run-tests.sh | 2 + tests/test_fleet_verifier.py | 69 +++++++++ tests/test_repository_inventory.py | 2 + tests/verify-fleet.py | 138 ++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 tests/test_fleet_verifier.py create mode 100644 tests/verify-fleet.py diff --git a/examples/atlas-sanitized/.claude/settings.json b/examples/atlas-sanitized/.claude/settings.json index 9cf7810..d0f4382 100644 --- a/examples/atlas-sanitized/.claude/settings.json +++ b/examples/atlas-sanitized/.claude/settings.json @@ -1,4 +1,5 @@ { + "model": "claude-opus-4-6[1m]", "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true, diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 0775008..2ae5cf0 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -14,6 +14,8 @@ done < <(find "$ROOT" -path "$ROOT/.git" -prune -o -name '*.sh' -type f -print0) HOME="$TEST_HOME" bash "$ROOT/tests/personas-test.sh" HOME="$TEST_HOME" python3 "$ROOT/tests/framework-contract-test.py" +HOME="$TEST_HOME" python3 "$ROOT/tests/test_fleet_verifier.py" +HOME="$TEST_HOME" python3 "$ROOT/tests/verify-fleet.py" --root "$ROOT/examples" HOME="$TEST_HOME" python3 "$ROOT/tests/test_runtime_adapters.py" HOME="$TEST_HOME" python3 "$ROOT/tests/test_documentation.py" HOME="$TEST_HOME" python3 "$ROOT/tests/test_repository_inventory.py" diff --git a/tests/test_fleet_verifier.py b/tests/test_fleet_verifier.py new file mode 100644 index 0000000..c89b8c0 --- /dev/null +++ b/tests/test_fleet_verifier.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Focused behavior tests for the reusable fleet contract verifier.""" + +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("verify-fleet.py") +SPEC = importlib.util.spec_from_file_location("verify_fleet", SCRIPT) +assert SPEC and SPEC.loader +VERIFIER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VERIFIER) + + +class FleetVerifierTest(unittest.TestCase): + def create_persona(self, root: Path, name: str = "atlas") -> Path: + persona = root / name + (persona / ".claude").mkdir(parents=True) + (persona / "skills" / "review").mkdir(parents=True) + (persona / "AGENTS.md").write_text("# Atlas\n\nFind procedures in `skills/`.\n", encoding="utf-8") + (persona / "CLAUDE.md").write_text("@AGENTS.md\n", encoding="utf-8") + (persona / ".claude/settings.json").write_text(json.dumps({"model": VERIFIER.MODEL}), encoding="utf-8") + (persona / "skills/review/SKILL.md").write_text("---\nname: review\n---\n\nReview work.\n", encoding="utf-8") + return persona + + def test_valid_fixture_passes(self) -> None: + with tempfile.TemporaryDirectory() as directory: + self.create_persona(Path(directory)) + self.assertEqual(VERIFIER.verify(Path(directory)), []) + + def test_rejects_each_contract_boundary(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + persona = self.create_persona(root, "archer") + (persona / "AGENTS.md").write_text("# Archer\n\n## Tools\n\n" + "word " * 301, encoding="utf-8") + (persona / "CLAUDE.md").write_text("@AGENTS.md\nExtra adapter text\n", encoding="utf-8") + (persona / "skills/review/SKILL.md").write_text("word " * 501, encoding="utf-8") + (persona / ".claude/settings.json").write_text(json.dumps({"model": "other"}), encoding="utf-8") + (persona / "PERSONA.md").write_text("legacy", encoding="utf-8") + (persona / "notes.md").write_text("Folder Bridge and four-week review", encoding="utf-8") + (persona / ".claude-flags").write_text("--channels plugin:discord@claude-plugins-official", encoding="utf-8") + errors = "\n".join(VERIFIER.verify(root)) + for expected in ("exceeds 300", "resident tool/procedure", "may contain only", "exceeds 500", "model must", "legacy persona", "folder bridge", "four-week review", "Discord is only"): + self.assertIn(expected, errors) + + def test_archive_is_not_active_residue(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + persona = self.create_persona(root) + archive = persona / "docs/archive" + archive.mkdir(parents=True) + (archive / "old.md").write_text("PERSONA.md, Folder Bridge, and four-week review", encoding="utf-8") + self.assertEqual(VERIFIER.verify(root), []) + + def test_only_named_personas_may_enable_discord(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + persona = self.create_persona(root, "julia") + (persona / ".claude-flags").write_text("--channels plugin:discord@claude-plugins-official", encoding="utf-8") + self.assertEqual(VERIFIER.verify(root), []) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_repository_inventory.py b/tests/test_repository_inventory.py index d307841..3041db6 100644 --- a/tests/test_repository_inventory.py +++ b/tests/test_repository_inventory.py @@ -78,6 +78,8 @@ def test_central_gate_and_ci_share_one_verdict(self) -> None: for test in ( "personas-test.sh", "framework-contract-test.py", + "test_fleet_verifier.py", + "verify-fleet.py", "test_runtime_adapters.py", "test_documentation.py", "test_repository_inventory.py", diff --git a/tests/verify-fleet.py b/tests/verify-fleet.py new file mode 100644 index 0000000..4959bd5 --- /dev/null +++ b/tests/verify-fleet.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Validate the portable persona contract across a fleet of repositories.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path + + +MODEL = "claude-opus-4-6[1m]" +DISCORD_PERSONAS = {"bob", "flora", "julia"} +ARCHIVE_PARTS = {"archive", "archives", "consumed", "history", "historical", ".git", "node_modules", "__pycache__"} +RETIRED_REFERENCES = ("persona.md", "folder bridge", "bridgey inbox", "four-week review") +RESIDENT_HEADINGS = re.compile( + r"^#{1,6}\s+(?:tools?(?:\s+(?:inventory|available))?|procedures?|workflows?|rituals?|integrations?)\b", + re.IGNORECASE | re.MULTILINE, +) + + +def is_active(path: Path, root: Path) -> bool: + return not any(part.lower() in ARCHIVE_PARTS for part in path.relative_to(root).parts) + + +def tracked_files(repo: Path) -> set[Path]: + result = subprocess.run( + ("git", "ls-files", "-z"), cwd=repo, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, check=False, + ) + if result.returncode == 0: + return {repo / name for name in result.stdout.decode().split("\0") if name} + return {path for path in repo.rglob("*") if path.is_file()} + + +def persona_roots(fleet_root: Path) -> list[Path]: + roots = [path for path in fleet_root.iterdir() if path.is_dir() and (path / "AGENTS.md").is_file()] + return sorted(roots, key=lambda path: path.name) + + +def words(path: Path) -> int: + return len(path.read_text(encoding="utf-8").split()) + + +def claude_settings(path: Path, repo: Path) -> bool: + relative = path.relative_to(repo) + return len(relative.parts) >= 2 and relative.parts[0] == ".claude" and path.name.startswith("settings") and path.suffix == ".json" + + +def verify_persona(repo: Path) -> list[str]: + errors: list[str] = [] + name = repo.name + tracked = tracked_files(repo) + agents, claude = repo / "AGENTS.md", repo / "CLAUDE.md" + + for required in (agents, claude, repo / ".claude/settings.json"): + if required not in tracked: + errors.append(f"{name}: required tracked file missing: {required.relative_to(repo)}") + + if agents.is_file(): + resident = agents.read_text(encoding="utf-8") + if words(agents) > 300: + errors.append(f"{name}: AGENTS.md exceeds 300 words") + if RESIDENT_HEADINGS.search(resident): + errors.append(f"{name}: AGENTS.md contains a resident tool/procedure heading") + if len(re.findall(r"^\s*\d+[.)]\s+", resident, re.MULTILINE)) >= 4: + errors.append(f"{name}: AGENTS.md contains procedural bulk (four or more numbered steps)") + + if claude.is_file(): + if words(claude) > 80: + errors.append(f"{name}: CLAUDE.md exceeds 80 words") + nonempty = [line.strip() for line in claude.read_text(encoding="utf-8").splitlines() if line.strip()] + if nonempty[-1:] != ["@AGENTS.md"] or any(not line.startswith("#") for line in nonempty[:-1]): + errors.append(f"{name}: CLAUDE.md may contain only an optional title and the @AGENTS.md import") + + for skill in repo.glob("skills/**/SKILL.md"): + if skill in tracked and words(skill) > 500: + errors.append(f"{name}: {skill.relative_to(repo)} exceeds 500 words") + + for settings in (path for path in tracked if claude_settings(path, repo)): + try: + data = json.loads(settings.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + errors.append(f"{name}: invalid JSON in {settings.relative_to(repo)}: {exc.msg}") + continue + if data.get("model") != MODEL: + errors.append(f"{name}: {settings.relative_to(repo)} model must be {MODEL}") + + runtime_files = [path for path in tracked if path.name == ".claude-flags" or claude_settings(path, repo)] + discord_enabled = any("discord@claude-plugins-official" in path.read_text(encoding="utf-8") for path in runtime_files) + if discord_enabled and name not in DISCORD_PERSONAS: + errors.append(f"{name}: Discord is only permitted for Bob, Flora, and Julia") + + for path in (path for path in repo.rglob("*") if path.is_file()): + if not is_active(path, repo): + continue + relative = path.relative_to(repo) + lowered_parts = {part.lower() for part in relative.parts} + if path.name == "PERSONA.md": + errors.append(f"{name}: active legacy persona definition: {relative}") + if "output-styles" in lowered_parts: + errors.append(f"{name}: active legacy output style: {relative}") + if path.suffix.lower() not in {".md", ".json", ".toml", ".txt", ".yaml", ".yml"}: + continue + text = path.read_text(encoding="utf-8", errors="ignore").lower() + for retired in RETIRED_REFERENCES: + if retired in text: + errors.append(f"{name}: active retired reference '{retired}' in {relative}") + + return errors + + +def verify(fleet_root: Path) -> list[str]: + if not fleet_root.is_dir(): + return [f"fleet root does not exist: {fleet_root}"] + roots = persona_roots(fleet_root) + if not roots: + return [f"no persona folders with AGENTS.md under: {fleet_root}"] + return [error for root in roots for error in verify_persona(root)] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", required=True, type=Path, help="directory containing persona folders") + args = parser.parse_args() + errors = verify(args.root.resolve()) + if errors: + print("Fleet contract failed:", file=sys.stderr) + print(*(f"- {error}" for error in errors), sep="\n", file=sys.stderr) + return 1 + print(f"Fleet contract passed: {len(persona_roots(args.root.resolve()))} personas") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From cdc4c8ea31038a42496e7cc2ce9cb79ceb573e74 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 18 Aug 2026 17:49:30 -0400 Subject: [PATCH 3/5] test: preserve versioned release history in fleet scan Assisted-by: codex --- tests/test_fleet_verifier.py | 12 ++++++++++++ tests/verify-fleet.py | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_fleet_verifier.py b/tests/test_fleet_verifier.py index c89b8c0..559dfae 100644 --- a/tests/test_fleet_verifier.py +++ b/tests/test_fleet_verifier.py @@ -57,6 +57,18 @@ def test_archive_is_not_active_residue(self) -> None: (archive / "old.md").write_text("PERSONA.md, Folder Bridge, and four-week review", encoding="utf-8") self.assertEqual(VERIFIER.verify(root), []) + def test_versioned_release_is_history_but_other_active_files_are_not(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + persona = self.create_persona(root) + release = persona / "releases/component/2.15.4" + release.mkdir(parents=True) + (release / "RELEASE.md").write_text("Folder Bridge recovery artifact", encoding="utf-8") + self.assertEqual(VERIFIER.verify(root), []) + (persona / "user/memory/MEMORY.md").parent.mkdir(parents=True) + (persona / "user/memory/MEMORY.md").write_text("Folder Bridge", encoding="utf-8") + self.assertIn("user/memory/MEMORY.md", "\n".join(VERIFIER.verify(root))) + def test_only_named_personas_may_enable_discord(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) diff --git a/tests/verify-fleet.py b/tests/verify-fleet.py index 4959bd5..945ce35 100644 --- a/tests/verify-fleet.py +++ b/tests/verify-fleet.py @@ -14,6 +14,7 @@ MODEL = "claude-opus-4-6[1m]" DISCORD_PERSONAS = {"bob", "flora", "julia"} ARCHIVE_PARTS = {"archive", "archives", "consumed", "history", "historical", ".git", "node_modules", "__pycache__"} +RELEASE_VERSION = re.compile(r"^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") RETIRED_REFERENCES = ("persona.md", "folder bridge", "bridgey inbox", "four-week review") RESIDENT_HEADINGS = re.compile( r"^#{1,6}\s+(?:tools?(?:\s+(?:inventory|available))?|procedures?|workflows?|rituals?|integrations?)\b", @@ -22,7 +23,10 @@ def is_active(path: Path, root: Path) -> bool: - return not any(part.lower() in ARCHIVE_PARTS for part in path.relative_to(root).parts) + parts = path.relative_to(root).parts + if any(part.lower() in ARCHIVE_PARTS for part in parts): + return False + return not (len(parts) >= 4 and parts[0] == "releases" and RELEASE_VERSION.fullmatch(parts[2])) def tracked_files(repo: Path) -> set[Path]: From a5faf67e8dd04097e98b15d4f177655a80e32bff Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 20 Aug 2026 12:59:10 -0400 Subject: [PATCH 4/5] fix: handle deleted optional fleet files Assisted-by: codex --- tests/test_fleet_verifier.py | 10 ++++++++++ tests/verify-fleet.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_fleet_verifier.py b/tests/test_fleet_verifier.py index 559dfae..89546ee 100644 --- a/tests/test_fleet_verifier.py +++ b/tests/test_fleet_verifier.py @@ -8,6 +8,7 @@ import tempfile import unittest from pathlib import Path +from unittest.mock import patch SCRIPT = Path(__file__).with_name("verify-fleet.py") @@ -76,6 +77,15 @@ def test_only_named_personas_may_enable_discord(self) -> None: (persona / ".claude-flags").write_text("--channels plugin:discord@claude-plugins-official", encoding="utf-8") self.assertEqual(VERIFIER.verify(root), []) + def test_deleted_optional_runtime_file_does_not_crash_current_tree_scan(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + persona = self.create_persona(root) + deleted = persona / ".claude-flags" + tracked = {path for path in persona.rglob("*") if path.is_file()} | {deleted} + with patch.object(VERIFIER, "tracked_files", return_value=tracked): + self.assertEqual(VERIFIER.verify(root), []) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/verify-fleet.py b/tests/verify-fleet.py index 945ce35..d9a719b 100644 --- a/tests/verify-fleet.py +++ b/tests/verify-fleet.py @@ -92,7 +92,10 @@ def verify_persona(repo: Path) -> list[str]: if data.get("model") != MODEL: errors.append(f"{name}: {settings.relative_to(repo)} model must be {MODEL}") - runtime_files = [path for path in tracked if path.name == ".claude-flags" or claude_settings(path, repo)] + runtime_files = [ + path for path in tracked + if path.is_file() and (path.name == ".claude-flags" or claude_settings(path, repo)) + ] discord_enabled = any("discord@claude-plugins-official" in path.read_text(encoding="utf-8") for path in runtime_files) if discord_enabled and name not in DISCORD_PERSONAS: errors.append(f"{name}: Discord is only permitted for Bob, Flora, and Julia") From 617d6cb093ab9c781321df2be79ee428a6f3c924 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 20 Aug 2026 13:17:06 -0400 Subject: [PATCH 5/5] fix: ignore generated MCP lock files Assisted-by: codex --- MIGRATION.md | 2 +- skills/persona-dev/assets/gitignore-template | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 25ba053..bd0f8f5 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -32,7 +32,7 @@ Do not bulk-replace persona-owned content. Preserve `user/profile.md`, After reviewing the exact paths, remove obsolete framework stamps, the public `bin/personas` CLI, Cloud repository markers, visibility adapters, generated privacy workflows, publishing guards, and default persona hooks. They are not -part of the 3.0.0 folder contract. +part of the 5.0.0 folder contract. Cloud uses the same publishable folder. A private repository is recommended for personalized use, but visibility is user-managed and credentials remain diff --git a/skills/persona-dev/assets/gitignore-template b/skills/persona-dev/assets/gitignore-template index 42796d6..23948ca 100644 --- a/skills/persona-dev/assets/gitignore-template +++ b/skills/persona-dev/assets/gitignore-template @@ -5,6 +5,7 @@ user/ .claude/settings.local.json .codex/*.local.toml .mcp.json +.mcp.json.lock # Credentials and environment files .env