---
name: migrate-dual-harness
description:
  "Migrate a single-harness repo to the dual-harness layout so both Claude Code and Codex load
  the same instructions and skills — AGENTS.md as the portable canonical doc, a thin CLAUDE.md
  `@AGENTS.md` bridge, and a `.agents/skills` dir with a `.claude/skills` symlink onto it. Use
  when asked to migrate to dual-harness, make this repo work in both Claude and Codex, or for
  agent metadata cleanup. Ends with a quality review: canonical-form convergence and
  checkup-clean are applied mechanically; conciseness / progressive-disclosure findings are
  reported, with editorial changes confirmed with the user first."
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
---

# Migrate Dual-Harness

Converge a single-harness repo (Claude-only monolithic `CLAUDE.md`, or Codex-only `AGENTS.md`
with no bridge) onto the target dual-harness layout, applying every mechanical fix automatically
and only asking the model to do the one thing a script can't: split a monolithic `CLAUDE.md`
into its portable core and its Claude-specific remainder.

**User-invoked only.** This skill sets `disable-model-invocation: true`, so nothing can
auto-trigger it as a handoff — not `catalyst-foundry:setup-catalyst` (which hits this exact
case at its rc `11` row) and not `check-project-setup.sh`'s §10 checkup warning. Both merely
*reference* `/catalyst-foundry:migrate-dual-harness` and tell the user to run it themselves;
invoke this skill directly (`/catalyst-foundry:migrate-dual-harness`) to perform the split.

## Target layout

- `AGENTS.md` — portable, tool-agnostic instructions (canonical).
- `CLAUDE.md` — thin bridge: literal line 1 is `@AGENTS.md`, then only Claude-specific notes.
- `.agents/skills/` — canonical skills dir.
- `.claude/skills` — committed **relative** symlink → `../.agents/skills`.
- `AGENTS.md` carries a `## Skills` pointer section when the repo has skills.
- No `.codex/` dir needed — Codex reads `AGENTS.md` and `.agents/skills/` natively.

## Phase 1: Resolve

Locate the backing script (lives in `catalyst-dev`, the shared framework core):

```bash
source "${CLAUDE_PLUGIN_ROOT:-plugins/foundry}/scripts/require-catalyst-dev.sh" \
    "${CLAUDE_PLUGIN_ROOT:-plugins/foundry}" || exit 1
SCRIPT="${CATALYST_DEV_SCRIPTS}/migrate-dual-harness.sh"
```

## Phase 2: Diagnose

Dry-run the classifier — it writes nothing:

```bash
bash "$SCRIPT" --repo . 2>&1
```

Branch on the exit code:

| rc | Meaning | Next step |
|----|---------|-----------|
| `0` | already `dual-ok`, or `no-harness` (out of scope for this skill) | Phase 5 (house rules) — for `no-harness`, re-run the mechanical fix per Phase 5's note — then Phase 6 |
| `10` | mechanical changes needed (bridge / skills wiring / pointer) — no monolithic problem | Phase 3 |
| `11` | `CLAUDE.md` is monolithic — needs the intelligent split | Phase 3 (mechanical parts only), then Phase 4 |
| `2` | bad usage (`--repo` not a dir, unknown flag) | fix the invocation and re-run |
| `4` | ambiguous skills state (message says exactly what conflicts) | ONE sanctioned exception, else STOP. Exception (the per-skill symlink farm): applies only when the diagnostic is the symlink-inside-the-trees refusal AND all three pre-checks pass BEFORE touching anything — (a) every entry of `.claude/skills` is a symlink resolving into `../../.agents/skills/<name>` (one real file or foreign link disqualifies), (b) neither `.claude` nor `.agents` is itself a symlink, (c) `git check-ignore --stdin` over `.agents/skills` + every `.agents/skills/<name>` + `.claude/skills` reports nothing ignored (a masked hard-stop must surface BEFORE deletion, not after). All pass → converge (delete the per-entry links — links, not content — `rmdir .claude/skills`, `ln -s '../.agents/skills' .claude/skills`), re-run the dry-run, and dispatch the NEW rc through this same table (a coexisting monolithic CLAUDE.md legitimately yields rc `11` next — continue the flow; rc `0` only when nothing else is pending). ANY pre-check failing, or any other rc-4 diagnostic → STOP: surface the script's stderr verbatim; never modify either skills tree yourself; present the conflict and ask the human operator, then re-run the classifier to verify |
| `5` | I/O error | inspect the printed message |

## Phase 3: Mechanical fix

Applies to rc `10` and rc `11` alike (rc `11` still gets the skills/pointer wiring — only the
`CLAUDE.md` split itself is deferred to Phase 4):

```bash
bash "$SCRIPT" --repo . --fix 2>&1
```

- After fixing from rc `10`, re-run the Phase 2 dry-run and require rc `0`.
- After fixing from rc `11`, re-run the Phase 2 dry-run and expect rc `11` again (the monolithic
  split is still outstanding) — proceed to Phase 4.
- rc `4` at any point: dispatch through the Phase 2 table's rc-4 row — it is the single
  authoritative statement of the one sanctioned exception (the pre-validated per-skill symlink
  farm convergence) and the hard stop for everything else.

## Phase 4: Intelligent split (rc 11 only)

This is the one step a script cannot do — reconciling prose requires model judgment.

1. **Read `CLAUDE.md` IN FULL.** Not a partial read — every line needs a classification
   decision below.
2. **Partition every line/section** into:
   - **Claude-specific** — `.claude/` paths, Claude Code plugin/marketplace/session mechanics,
     `claude` CLI invocations, MCP registration commands that are Claude-scoped.
   - **Portable** — everything else (architecture, conventions, workflow rules, anything that
     applies regardless of which agent is driving).
   - **`@`-import lines are classified by their TARGET, not by their syntax** (the `@` mechanism
     is Claude-only, but what it imports usually isn't): read the imported file; if its content
     is portable guidance (e.g. `@docs/development.md`), reference it from `AGENTS.md` as a
     plain read-this path (like the Reference Docs pattern) so Codex sees it too, and the
     `@`-import may stay in the bridge for Claude's auto-load. Only an import whose target is
     genuinely Claude-specific stays bridge-only. Never let portable guidance survive solely
     behind an `@`-import Codex cannot interpret.
3. **Merge the portable content into `AGENTS.md`**:
   - If `AGENTS.md` doesn't exist, create it from the portable content.
   - If `AGENTS.md` already exists, merge without duplicating — **existing `AGENTS.md` sections
     stay authoritative on conflict**; note any discrepancy to the user rather than silently
     overwriting it.
4. **Rewrite `CLAUDE.md`** as: line 1 `@AGENTS.md`, a blank line, then only the Claude-specific
   remainder under clear headings.
5. **Conservation check**: every H2/H3 heading and every non-blank line of the *original*
   `CLAUDE.md` must appear — verbatim or explicitly merged — in exactly one of the two files.
   List anything dropped and why (this should be nothing; if something doesn't fit either
   bucket cleanly, default to portable rather than dropping it).
6. Re-run the mechanical fix and require rc `0`:

   ```bash
   bash "$SCRIPT" --repo . --fix 2>&1
   ```

## Phase 5: House rules

Run the seeder so the repo also carries the "Working the Loop" reflexes (independent of the
dual-harness layout, but always worth syncing while touching the agent docs):

```bash
bash "${CATALYST_DEV_SCRIPTS}/ensure-agent-house-rules.sh" --repo . --fix 2>&1
```

- **`no-harness` repos:** the seeder creates the `AGENTS.md`/`CLAUDE.md` pair from scratch (the
  doc-pair itself is out of scope for `migrate-dual-harness.sh` — see Phase 2's `no-harness` note),
  even when Phase 3 already wired the skills tree. The freshly created `AGENTS.md` can't yet carry
  the `## Skills` pointer, so re-run the mechanical fix to pick it up (and the `@AGENTS.md` bridge,
  if still missing) **before** the final verification:

  ```bash
  bash "$SCRIPT" --repo . --fix 2>&1
  ```

## Phase 6: Verify

Re-run the classifier and confirm rc `0`:

```bash
bash "$SCRIPT" --repo . 2>&1
```

Print a summary table **rendered from the OBSERVED state, never from this template's example
values** — check each artifact and report what actually exists (a repo with no skills tree
legitimately reaches rc 0 with no `.agents/skills`, no symlink, and no pointer; those rows must
say `none`, not claim artifacts that don't exist):

```
── Dual-Harness Migration ──────────────────────
  Docs:        <e.g. AGENTS.md canonical, CLAUDE.md → @AGENTS.md bridge>
  Skills:      <.agents/skills/ (.claude/skills → symlink) | none>
  Pointer:     <## Skills section present in AGENTS.md | n/a (no skills)>
  House rules: <Working the Loop block current | absent>
  Status:      <rc 0 classification>
```

## Phase 7: Quality review (always — even when Phases 3-5 were no-ops)

The migration is not done when the classifier passes; the docs must also be GOOD. This review
phase always RUNS (it is part of the skill's contract — the frontmatter description discloses
it), but its WRITE authority is tiered. Start by reading the FINAL `AGENTS.md` and `CLAUDE.md`
IN FULL (Phases 5-6 may have changed them since any earlier read; a "no findings" verdict from
stale or unread files is invalid): gates 1 and 4 (canonical form, checkup-clean) are
mechanical and applied directly; gates 2 and 3 (conciseness, progressive disclosure) are
editorial — REPORT findings always, but apply only trivially-safe fixes directly (exact
duplicates from the merge, dead boilerplate the split itself created) and **ask the user before
any other editorial change or relocation**. Never silently rewrite prose the user authored:

1. **Canonical form, not merely functional.** Variants that happen to work still get converged.
   The one **sanctioned rc-4 exception** (recognized and executed in Phase 2, where rc 4 first surfaces — restated here as the canonical-form rule): when the classifier's rc-4 diagnostic is the
   symlink-inside-the-trees refusal AND inspection proves `.claude/skills/` is a pure per-skill
   symlink farm (EVERY entry is a symlink resolving into `../../.agents/skills/<name>` — one
   real file or foreign link disqualifies it), converge it: delete the per-entry links (links,
   not content), `rmdir .claude/skills`, `ln -s '../.agents/skills' .claude/skills`. Every OTHER
   rc-4 remains a hard stop per Phase 2. Also converge: a prose "see AGENTS.md" pointer becomes
   the literal `@AGENTS.md` line 1; the bridge's Claude-specific notes live under one clear
   heading. Re-run the classifier after converging — rc 0 required.
2. **Concise.** The bridge carries ONLY what is genuinely Claude-specific — no restated
   AGENTS.md content, no filler ("this file provides guidance to…" boilerplate dies here).
   AGENTS.md states each rule once; duplicated guidance introduced by the merge is collapsed to
   the single authoritative statement.
3. **Progressive disclosure.** AGENTS.md is the always-loaded top layer: it should carry the
   rules an agent needs on every task, and POINT at everything else — deep reference material
   belongs in `docs/` (or the repo's equivalent) behind a short "read on demand" pointer, and
   skill knowledge belongs in the skill files, referenced by path, never inlined. If the merge
   produced a wall of detail, move the detail out to a referenced doc and leave the pointer —
   content is RELOCATED, never dropped — and the Phase 4 conservation check widens accordingly:
   a line is "accounted for" when it lives in `AGENTS.md`, `CLAUDE.md`, **or a doc that one of
   them references by path**; list every relocation (source heading → target file) in the
   summary.
4. **Checkup-clean.** The satisfiable gate: the classifier dry-run exits `0`, AND — when the
   repo is Catalyst-configured — running `check-project-setup.sh` produces NO §3 snippet
   warning and NO §10 dual-harness warning (§10's green line, when it prints, confirms it).
   Warnings from unrelated sections (webhooks, thoughts, config keys) do NOT count against
   this gate — report them as observations only.

Report what the review changed (or "no findings") in the final summary.

## Important

- **Never commit.** After verifying, report the changed files and suggest a commit message
  (e.g. `chore(meta): migrate <repo> to dual-harness layout`) — the user commits.
- **Never delete or overwrite instruction content.** The only case that removes a tree is
  `.claude/skills` and `.agents/skills` both existing as real, byte-identical directories — the
  script collapses that to a symlink, guarded by `diff -r`.
  The TWO sanctioned tree-removal cases: (1) a duplicate `.claude/skills` proven byte-identical
  to `.agents/skills` (script-guarded by `diff -r` + mode compare), and (2) the pre-validated
  per-skill symlink FARM (Phase 2 rc-4 exception) — where only symlinks and the then-empty
  directory are removed, never file content.
- **Idempotent.** Re-running any phase against an already-converged repo is a no-op.
- **rc `4` is a stop sign, not a retry loop** — except the single Phase-2-sanctioned
  per-skill-symlink-farm convergence (see Phase 2 and Phase 7 rule 1). Every other ambiguous
  state (two real trees with different content, a symlink pointing somewhere unexpected) means
  the agent must never modify either skills tree itself — surface the script's stderr
  diagnostic verbatim and present the conflict to the human operator; resolving it is their
  decision, not the agent's. Re-run the classifier (Phase 2) to verify only after they've
  resolved it.
