Complete replacement makes three moves. It installs the current harness runtime. It removes orphans — files an older harness installed that the current one no longer produces. It keeps extensions — files the project added that the harness never owned. The runtime is the only thing replaced. Beyond the runtime, harness-owned content inside project-owned files is kept current in two layers — the deterministic step-4 refresh and the advisory diff-check of steps 8–9. § Project-owned files and version drift holds that contract; this section is a pointer, not a second statement of it.
-
Read the target from $ARGUMENTS. If no path is given, print the Usage block above verbatim and stop — never guess a target or operate on the current directory. Otherwise verify the path exists and detect the stack (table above).
-
Scaffold if needed. If the target has no CLAUDE.md, or its scripts/layout.toml has no [harness] table (or no layout.toml at all), the project-owned files are missing or predate the [harness] table. Run /init <target> first. /init scaffolds the committed files and asks which tool surfaces to install (claude always on; copilot, opencode, junie optional). It also resolves the channel without prompting — detected from the project's git state, defaulting a greenfield target to copy (runtime committed). Both are written to the [harness] table. A fully set-up project skips this step.
-
Read the channel and tools from scripts/layout.toml [harness]. channel (copy, manifest, or marketplace) governs orphan removal (step 6); marketplace behaves like manifest (runtime gitignored, not committed). tools is the surface set; on an upgrade materialize.py installs only these (or auto-detects the present surfaces when the key is absent) and never adds a tool the project lacks. To add or drop a tool, edit [harness] tools and re-run.
-
Replace the runtime. Preview first when the overwrite matters: harness/materialize.py <stack> <target> --dry-run prints the plan — files to create or overwrite, surfaces excluded, extras kept, the managed-chapter refresh — and writes nothing. On a gitignored-runtime channel it is the only preview of the overwrite, since no git diff exists. Then run the install:
harness/materialize.py <stack> <target>
It copies core ∪ stacks/<stack> for the resolved tools (overwriting harness-owned files = the "replace") and prints tools=…. It then prints an extras block: files under the harness-owned runtime directories — plus scripts/, minus the project-owned layout.toml and, on the generic stack, stack.sh — that this install did not produce. Each path prints on its own line between --- extras: N … --- and --- end extras ---. The script never deletes; classification is yours.
The script also runs three deterministic, marker-free refreshes of harness-owned content inside project-owned files, each reporting its own line (watch materialize.py's output):
CLAUDE.md managed chapters — the doctrine chapters from harness/claude-md/managed-chapters.md, each found by heading and rewritten in place; every other chapter untouched. An absent heading is left for step 9 to convert once.
.gitignore — the runtime paths from harness/init/core/gitignore-runtime.txt, channel-aware.
.claude/settings.json — the agent-teams env flag and a PreToolUse matcher per delivered hook.
The install ends with verification: it runs the test suites it just copied and prints verified: N vendored suite(s) pass on this host. A failure prints verify: <suite> FAILED plus the error tail and exits 1 — the installed runtime is broken on this host (broken copy, python incompatibility). Re-run the materialize; a persistent failure is a harness defect to report upstream, never the project's brief debt. The suite list is the install's own file set, so a project-authored test_*.py is never run as a suite. (--no-verify skips the run; it exists for harness-internal callers only.)
All three refreshes are ensure-present and additive. Harness-owned lines and keys are matched against the shipped template — no markers, no baseline. A project's own ignores, keys, hooks, and chapters are never rewritten, and nothing is removed. Removals and deeper divergence are what step 8's diff-check exists to catch — the residual an additive pass cannot safely decide.
-
Classify each extra. Check the declared extensions first, then disambiguate by harness history — history is what tells a renamed-away harness unit apart from a genuine project addition:
- Already a declared extension (the path is under a
[harness] extensions entry) → keep, no further checks. These are the project's own, recorded.
- A declared extra reviewer body (an agent whose name is in
[harness] extra_reviewers) → keep, and record it (step 6). The additive roster lets a project add *-reviewer agents beside the floor; their bodies live in the harness-owned agent directories but are the project's own. This rule mirrors the doctor's reviewer-roster check, so the two agree.
- Stray file inside a harness-owned skill (a file in a skill directory the current harness still owns, that this install did not produce) → orphan → remove. This applies to skill directories only — one skill is a single multi-file unit, so an unproduced file inside it is debris. An agent file is its own single-file unit, never a "stray file"; classify each agent by the per-unit rules below (renamed-away ⇒ orphan; never-owned ⇒ extension).
- A
scripts/ engine file the harness once shipped but no longer does → stale orphan → remove. Confirm with the harness source history: git -C <harness-root> log --oneline -- '*/scripts/<name>' returns commits ⇒ it was a harness engine. A scripts/ file with no harness past is the project's own tooling → keep, and record it (step 6).
- A unit the harness once owned but no longer does — a whole skill directory renamed away or dropped (e.g.
doc-review/ → the installed document-writing/), or a single agent body → stale orphan → remove. Confirm with the harness source history: for a skill, git -C <harness-root> log --oneline -- '*/.claude/skills/<name>/*' 'harness/*/<name>/*'; for an agent, git -C <harness-root> log --oneline -- '*/agents/<name>.md' '*/agents/<name>.agent.md'. Commits returned ⇒ the name was a harness unit. (When the source is a plugin with no history, fall back to the obvious rename successor this install just added, and ask if unclear.)
- A self-contained unit the harness never owned → new project extension → keep, and record it (step 6). This is a whole skill directory whose name appears nowhere in harness history, or a single-file agent with no harness counterpart and no harness past — an extra
*-reviewer is exactly this case.
- Ambiguous — when history is unclear or you cannot place it → ask the user keep-or-remove. Default to keeping over deleting.
The current harness unit set is harness/core ∪ harness/stacks/<stack>; the former set is that plus what harness history shows was renamed or removed. A name in the former-but-not-current set is an orphan, not an extension.
-
Record extensions, then apply removals.
- Record new extensions. For each kept unit not yet listed, run
python3 <harness-root>/harness/materialize.py record-extension <target> <runtime-path>. The script adds the path to [harness] extensions in scripts/layout.toml and, on a gitignored-runtime channel, appends the correctly-formed .gitignore re-include (the directory-vs-file slash rule lives in the script). Idempotent. This makes the keep durable: the doctor excludes declared extensions from the untracked check, and the next materialize keeps them without re-asking.
- Remove orphans — channel-aware, but the channel is never changed (switching is a manual, documented step — see
/init's "Channel: detect, never prompt"). Only the confirmed orphan paths are touched:
-
Propose removing harness-originated docs (migration). A project migrating from an older harness often carries handbook docs an earlier harness copied into docs/. That content now ships with the harness — as installed skills, or as reference-only docs. The docs/ copies are stale duplicates, and they are why a freshly migrated project's doctor reports handbook-refs failures. Detect and propose their removal (never auto-delete — docs/ is project-owned):
- A non-roster
docs/*.md whose basename is in the doctor's handbook denylist (harness/core/scripts/doctor-expectations.toml [handbook] denylist — agentic-harness.md, specialist-agent-workflow.md, cross-tool-strategy.md, adoption-guide.md, glossary.md, tdd-principles.md, ddd-principles.md, documentation-standards.md, harness-project-api.md) → moved to the harness.
- A non-roster
docs/*.md whose content matches an installed runtime doc — by name under .claude/skills/ (excluding .claude/skills/doctor/templates/) or a high-similarity diff → heavily overlaps the harness. Example: docs/intellij-mcp-integration.md vs the intellij-idea skill copy. The template exclusion matters: the roster briefs legitimately match their own doctor templates — that is their source, not overlap.
List each candidate with its new harness home and ask the user to remove them. The roster briefs (prd.md, system-design.md, ubiquitous-language.md, testing-principles.md, architecture-principles.md, security-principles.md, adr/) are never proposed. When the user agrees, delete the files and clean the now-dangling references the doctor's handbook-refs check flags. Remove or reword them in the citing briefs and ADRs — that prose is project-owned, so confirm the edits or hand them to /audit-docs.
-
Diff-check every template-seeded file against its shipped template (advisory). Step 4's deterministic refreshes already made the harness-owned additions current — the .gitignore runtime paths and the .claude/settings.json keys. This step is the completeness backstop over the same diffs: it catches what an additive pass cannot safely decide, and covers the files step 4 does not touch at all. For each template-seeded file, diff the shipped harness template against the project's file, classify every delta, and propose each change — applying it only on the user's OK. What the diff-check surfaces beyond step 4:
- a harness line the template dropped that still lingers in the project (
.gitignore path, settings.json matcher for a renamed hook) → propose removing it;
- a harness key the project overrode with a divergent value → surface it;
- the fully-advisory files step 4 never touches —
scripts/layout.toml data and the docs/ briefs.
Classify each delta by intent, and protect both sides. No markers are written and no version is stamped — the shipped template on disk is the reference, and model judgment stands in for a baseline. For every difference between the template and the project's file, decide which of three it is:
- A deliberate project change — the project intentionally diverged: a customized value, a section removed on purpose, a rule of its own. The project's version is coherent and specific to this project; the template carries the generic default. → Protect it. Do not propose reverting it to the template.
- A harness migration — the template evolved as the harness improved (a new section, a better default, a new key), and this project simply predates it. The template's version is a general improvement, not project-specific; the project's is the older default or absent. → Propose adopting it.
- A collision — the project customized something the harness also evolved. → Protect both: show both versions, let the human decide, never auto-resolve.
The rule runs both ways: never silently revert a deliberate project change, and never silently drop a harness migration. Without a baseline the classification cannot be proven — a project's coherent customization and a stale generic default can look alike. So the approval step, not an algorithm, is the safeguard, and a genuine collision always goes to the human. This is the same generic-vs-project call step 5 makes for orphan-vs-extension.
| Project file | Shipped template to diff against | Harness-owned in it |
|---|
.gitignore | harness/init/core/gitignore-runtime.txt | the runtime-path lines (.claude/skills/*, scripts/*, …) and the .scratch/ ledger; the project's own ignores and the !<extension>/ re-includes are theirs |
.claude/settings.json | harness/init/core/.claude/settings.json | the env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS flag and the PreToolUse matchers whose command is a .claude/hooks/*.py this install delivered (a legacy *.sh matcher is harness-era residue this diff-check proposes to prune); every other key and hook is the project's |
scripts/layout.toml | harness/init/stacks/<stack>/scripts/layout.toml | the [harness] table structure, any new keys, and the guiding comments (spec_version is a harness contract value the doctor checks); the classification data — globs, module rules, extensions — is the project's |
docs/*.md roster briefs | harness/core/.claude/skills/doctor/templates/<brief>.md | the skeleton structure — required headings, new sections; the filled-in prose is the project's |
.gitignore — step 4 already ensured the current harness paths are present, so the diff-check focuses on the residual. A path the template dropped that still lingers → propose removing it. A runtime path present on the copy channel, where the committed runtime should not be ignored → propose removing it. Read the channel from [harness] channel; never change it.
.claude/settings.json — step 4 already ensured the env flag and a matcher for every delivered hook (this subsumes the old hook-registration proposal, now automatic). The diff-check catches the residual. A matcher whose hook script the harness renamed or dropped, now inert → propose removing it. A harness key the project overrode with a divergent value → surface it. The doctor's hook-registration check (step 10) still flags any hook left unregistered.
scripts/layout.toml — step 4 does not touch this file; the diff-check owns it. A new [harness] key the template carries but the project lacks → propose it (contract drift the doctor also flags); improved guiding comments → propose them. The project's rule data is never rewritten.
docs/ briefs — a required or new section the template skeleton gained but the brief lacks → propose inserting the empty section for the owner to fill. Never overwrite authored prose. Deeper brief-quality work stays with /audit-docs.
Declines are not persisted. A proposal the user turns down is simply left alone. It re-surfaces on the next /materialize — acceptable for an occasionally-run upgrade, and it re-offers a change the user may since have reconsidered. An idempotent re-materialize (no template change, no divergence) finds no delta and proposes nothing, so the only thing that re-appears is a genuine, still-open divergence. If that repetition ever proves irritating, a declined_reconciliations skip-list in [harness] is the pre-authorized next step (ADR generalized-template-reconciliation) — deferred until the friction is real rather than assumed.
-
Review the materialized CLAUDE.md (advisory). Step 4's refresh is deterministic: it overwrites each managed chapter from its heading to the next ## . That is correct for the chapter's doctrine, but a heading-bounded overwrite cannot see what else the edit did to the file. Review the result before moving on. On a tracked CLAUDE.md, git diff -- CLAUDE.md is the precise signal — read it, and read the whole file once. If the diff is empty (an idempotent re-materialize), there is nothing to review; skip. Otherwise check five things and propose every fix — CLAUDE.md is project-owned, so confirm before editing, the same contract as steps 7–8:
- Accidental loss. In the diff's removed (
-) lines, flag any that are project-authored content, not harness doctrine — they lived inside a managed chapter and the refresh overwrote them. Propose moving them to a project-owned chapter rather than losing them. This is the one failure the deterministic replace cannot prevent: a managed chapter is harness-owned, so anything a project puts inside it is overwritten by design.
- Repetition. Any managed chapter's doctrine appearing a second time outside its chapter — a stray
### Confirmation Discipline, a duplicated memory or writing-standards note. Usually a legacy artifact. Propose removing the duplicate so the managed chapter is the single copy.
- Contradiction. A project chapter that fights a managed one — a Build/Quality-Gate chapter that re-states confirmation rules differently, a local writing rule against the managed Writing Standards. Propose reconciling it.
- Structure. Headings well-formed. Chapters in a sensible order — match the skeleton's relative layout: Memory and Agent Usage near the top; Writing Standards, Scratch Directory, Documentation Updates beside their related project chapters. No orphaned fragment left by an old edit.
- Missing improvements. A non-doctrine chapter the current skeleton (
harness/init/stacks/<stack>/CLAUDE.md) gained — new build, quality-gate, or convention guidance — that the project's CLAUDE.md lacks → propose adding it. The call is the same generic-vs-project judgment step 8 makes. This is the CLAUDE.md facet of step 8's template reconciliation; the doctrine chapters are already current from step 4, so only the project-authored chapters are compared here.
Absent headings (legacy migration). A chapter step 4 reported absent had a renamed or missing heading, so the deterministic replace had no target. Locate the equivalent section, propose inserting the matching chapter from harness/claude-md/managed-chapters.md under its canonical heading (placed to match skeleton order), and remove the old copy. Preserve genuine divergence: project-rewritten text stays as its own chapter; stack-specific skills (an IDE oracle) belong in ## Stack-specific skills, not inside a managed one.
This review is advisory and never gates the doctor. A proposal the user declines is not persisted — it re-surfaces next upgrade (step 8's "Declines are not persisted"). Ongoing — outside an upgrade — /audit-docs can run the same checks. It is also the forward edge of /harvest. Harvest pulls a generic improvement up from one project into the source. The managed chapters push it back down to every project automatically once the headings are in place; steps 8–9 propose the rest.
-
Validate and summarize. Run the doctor from the target: