# Harness Propagation

> Status: contract doc for the **seamless user/repo harness invocation** plan
> (`plans/seamless-user-repo-harness-invocation.md`, PRD
> `prd/minions-opg-2026-06-10-4.json`). This is the foundation item — it nails
> down the *current* propagation surfaces and the worktree footgun so the
> rest of the plan can extend them without reinventing the contract. Read this
> before adding a new adapter method, a new asset kind, or a new opt-out flag.

## What is a "harness"

In this doc, "harness" means any user-installed or repo-local asset that the
runtime CLI auto-discovers when it starts:

- **Skills** — `SKILL.md` files the CLI loads as reusable workflows.
- **Slash commands** — `*.md` files the CLI exposes as `/command-name`.
- **MCP servers** — JSON config entries that point at a stdio/HTTP server.

A harness is "propagated" when it reaches a dispatched agent without the user
having to register it again in Minions. The single-line goal: *if it works
when you type the same task into `claude` / `copilot` / `codex` in this repo
on this machine, it works when Minions dispatches the same task here.*

## The four propagation mechanisms

Every asset reaches the agent through exactly one of these mechanisms today.
No engine code branches on `runtime.name` for any of them — the runtime
adapter answers the contract questions and the engine fans out.

### 1. Runtime native discovery (no engine code involved)

The CLI reads its own config and indexes its own asset dirs on every spawn.
Minions does not copy, symlink, or aggregate user-level assets — it just
makes sure the relevant dirs are *readable* from the agent's cwd (see
mechanism #3 below). The actual loading is the CLI's job.

| Runtime | User skills | User commands | User MCP servers |
|---------|-------------|---------------|------------------|
| Claude (`engine/runtimes/claude.js`) | `~/.claude/skills`, `~/.agents/skills`, plus `~/.claude/plugins/<…>/skills` | `~/.claude/commands`, plugin `commands/` | `~/.claude.json → mcpServers` |
| Copilot (`engine/runtimes/copilot.js`) | `~/.copilot/skills`, `~/.agents/skills`, `~/.copilot/installed-plugins/<…>/skills` | `~/.copilot/commands` *(probe)* | `~/.copilot/mcp-config.json → mcpServers` |
| Codex (`engine/runtimes/codex.js`) | `~/.agents/skills`, `/etc/codex/skills` | not yet supported by CLI | not yet a stable contract |

The first column is the source of truth for "where does the CLI look on
this machine?". The dashboard's tooling page mirrors this via
`engine/queries.js → collectSkillFiles / collectCommandFiles`.

### 2. Cwd routing (`shared.resolveSpawnPaths`)

`engine/shared.js → resolveSpawnPaths(project, type, MINIONS_DIR)` decides
what the agent's `cwd` is:

- **Read-only types** (`meeting`, `ask`, `explore`, `plan-to-prd`, `plan`)
  → `cwd = project.localPath` (the operator's main checkout — sees
  uncommitted files).
- **Code-mutating types** (`implement`, `fix`, `review`, `test`, `verify`,
  `decompose`, `docs`) → `cwd = <worktree>` (a fresh `git worktree add`
  checked out at branch tip — only sees committed files).

Cwd routing matters because every CLI's native discovery is **rooted at
cwd**. Project-scope skills like `<repo>/.claude/skills/foo/SKILL.md` are
loaded by the CLI only if `<cwd>/.claude/skills/foo/SKILL.md` exists at
spawn time. See *The worktree-uncommitted footgun* below.

Live-checkout mode (`project.worktreeMode: 'live'`) collapses both branches
to `cwd = project.localPath` for every dispatch type, so the agent sees
the operator's working tree as-is (including uncommitted assets). The
tradeoff is single-mutating-dispatch concurrency per project — see
`docs/live-checkout-mode.md`.

### 3. `--add-dir` (`engine/spawn-agent.js → computeAddDirs`)

`computeAddDirs({ runtime, minionsDir, homeDir })` builds the list of
absolute dirs the CLI is allowed to read **outside** its cwd. The current
list:

1. `minionsDir` — always first, so playbooks, system prompt, and skill
   index are reachable from any worktree.
2. Every existing dir returned by `runtime.getUserAssetDirs({ homeDir })`
   for the resolved runtime — e.g. `~/.claude` + `~/.agents` for Claude,
   `~/.copilot` + `~/.agents` for Copilot, `~/.codex` + `~/.agents` for
   Codex.

Non-existent dirs are dropped (Claude CLI rejects unknown `--add-dir`
entries) and the list is deduped by resolved path.

`--add-dir` is the **only** mechanism today that crosses the worktree
boundary. It does not (yet) cover project-local-but-uncommitted assets —
that's plan item #5 (`harnessPropagateProjectLocal`).

### 4. MCP suppression flags

Two flags strip otherwise-inherited MCP / instruction surfaces; everything
else flows through implicitly because the CLI reads its native config on
every spawn:

- `engine.copilotDisableBuiltinMcps` (default `true`) → `--disable-builtin-mcps`
  on Copilot. Strips Copilot's built-in `github-mcp-server` so dispatched
  agents do not try to open a parallel PR through the bundled MCP while
  Minions is already managing the PR via the host integration.
- `engine.copilotSuppressAgentsMd` (default `true`) → `--no-custom-instructions`
  on Copilot. Strips project `AGENTS.md` auto-load so Minions playbook
  prompts are not silently overridden by repo-local instructions.

There is no equivalent flag for stripping user-level skills, user-level
slash commands, or user-level MCPs. The current posture is "always inherit"
— `engine.hermeticHarness` (P-49e1c8b7, default `false`) is the per-fleet /
per-agent opt-out. When TRUE:

- `--add-dir` collapses to exactly `[minionsDir]` — every dir from
  `runtime.getUserAssetDirs({ homeDir })` is dropped.
- `harnessPropagateProjectLocal` is skipped — no `--project-harness-dir`
  flags emitted.
- `claudePreApproveWorkspaceMcps` is skipped — workspace `.mcp.json`
  servers are not pre-approved in `~/.claude.json`.

The flag is **independent** of `copilotDisableBuiltinMcps` and
`copilotSuppressAgentsMd` — those keep their existing semantics so an
operator can run hermetic Claude *and* keep Copilot's built-in MCPs off
or AGENTS.md auto-load suppressed without re-thinking each lever. Per-agent
override at `agent.hermeticHarness` (resolved via
`shared.resolveAgentHermeticHarness(agent, engine)`, mirrors
`shared.resolveAgentBareMode`).

## The worktree-uncommitted footgun

The single biggest behavioral wrinkle for new contributors:

1. The operator drops an experimental skill at
   `<repo>/.claude/skills/bar/SKILL.md` on their main checkout.
2. The skill works fine when invoked by hand:
   `cd <repo>; claude "use the bar skill"` — Claude's auto-discovery
   sees it because cwd is the main checkout.
3. The operator dispatches an `implement` work item against the same repo.
4. The engine runs `git worktree add <wt> origin/<branch>` (uncommitted
   files do not appear in a fresh worktree), then spawns the agent with
   `cwd = <wt>`.
5. Claude's discovery now happens at `<wt>` — `.claude/skills/bar/` is
   absent. The agent silently underperforms with no error message.

Read-only dispatch types (`meeting`, `ask`, `explore`, `plan-to-prd`,
`plan`) do **not** hit this footgun, because their cwd is
`project.localPath`. Mutating types do.

`--add-dir` is not currently extended to cover this case (plan item #5),
so the only workarounds today are:

- **Commit it.** The skill becomes visible to every dispatch and every
  teammate, with the usual reviewability tradeoff.
- **Move it user-scope.** Drop it under `~/.claude/skills/bar/` instead —
  the CLI's user-skill discovery still works inside a worktree because
  `--add-dir` attaches `~/.claude`.
- **Flip to live-checkout mode.** Set `project.worktreeMode = 'live'` on
  the project so every dispatch runs in `project.localPath`. Caveats in
  `docs/live-checkout-mode.md`.

The dashboard tooling page does not yet surface this gap; that's plan
item #8 ("Harness diagnostics" panel). Until then, run
`minions doctor --harness` to get the per-runtime view of which dirs the
engine actually surfaces, and check that flagged "missing" entries
actually match the assets you expected to inherit.

## `minions doctor --harness`

One-shot CLI diagnostic that prints, per registered runtime, every dir
or file the engine would surface to a spawned agent under its scope label.
Implemented in `engine/preflight.js → runHarnessDoctor(minionsHome)` and
wired into `bin/minions.js` and `engine/cli.js` as the `--harness` mode
of the existing `minions doctor` command.

### What it prints

```
Minions Harness Propagation
  Runtime: claude
    User asset dirs (--add-dir to agents):
      ✓ /home/you/.claude                                [user]
      ✓ /home/you/.agents                                [user]
    Skill roots (CLI native discovery):
      ✓ /home/you/.claude/skills                         [user]
      ✓ /home/you/.agents/skills                         [user]
      ⚠ /repo/.claude/skills                             [project:myrepo]   (missing on disk)
      ✓ /repo/.agents/skills                             [project:myrepo]
    Skill write targets (auto-extract destinations):
      ✓ /home/you/.claude/skills                         [personal]
      ✓ /repo/.claude/skills                             [project:myrepo]
  Runtime: copilot
    User asset dirs (--add-dir to agents):
      ✓ /home/you/.copilot                               [user]
      ✓ /home/you/.agents                                [user]
    …
  Worktree --add-dir snapshot (engine fleet default: copilot):
    ✓ /home/you/.minions                                 [minions]
    ✓ /home/you/.copilot                                 [user]
    ✓ /home/you/.agents                                  [user]
  All harness paths surveyed. Missing on-disk paths are warnings, not failures —
  they're listed so you can decide whether to create the dir, populate it, or
  ignore it for this host.
```

### Exit behavior

The command exits **0 on a healthy host** even if some scoped paths do
not exist on disk. The whole point is to surface "what would the engine
attach today?" — a user that does not run Codex may legitimately have
no `~/.codex` dir, and that is not a failure. Exits non-zero only on a
true runtime/config error (e.g. registry refuses to resolve a known
runtime, or `config.json` is unparseable).

### When to run it

- After installing a new skill / command / MCP and "it didn't take" on
  the next dispatched agent — confirm Minions actually surfaces that dir
  for the runtime you're dispatching with.
- After changing `engine.defaultCli` — different runtimes have different
  asset dirs; the `Worktree --add-dir snapshot` block reflects the fleet
  default.
- After re-cloning a repo with `<repo>/.claude/skills/` — the diagnostic
  shows whether the project-scope path is on disk.
- Before reporting "agent ignored my skill" — half the time the answer is
  visible in the diagnostic output.

It does **not** check whether the assets *load successfully* inside the
CLI (no parse of `SKILL.md`, no MCP handshake). For loadability,
inspect the dashboard tooling page or run an `ask` dispatch and watch
the live output.

## Adapter contract summary

The runtime adapters answer these questions. The engine never special-cases
a runtime by name — it only calls these methods on the resolved adapter.

| Method | Signature | Used by |
|--------|-----------|---------|
| `getUserAssetDirs({ homeDir })` | → `string[]` | `engine/spawn-agent.js → computeAddDirs`; `runHarnessDoctor` |
| `getSkillRoots({ homeDir, project? })` | → `[{ scope, dir, projectName? }]` | `engine/queries.js → collectSkillFiles`; `runHarnessDoctor` |
| `getSkillWriteTargets({ homeDir, project? })` | → `{ personal, project? }` | `engine/lifecycle.js → extractSkillsFromOutput`; `runHarnessDoctor` |
| `getCommandRoots({ homeDir, project? })` | → `[{ scope, dir, projectName? }]` | `engine/queries.js → collectCommandFiles` (via `getProjectHarnesses` for project scope); `runHarnessDoctor` |
| `getMcpConfigPaths({ homeDir, project? })` | → `[{ scope, file, projectName? }]` | `runHarnessDoctor`; planned: `engine/queries.js → getStatusSlowStateMtimePaths` |

`runHarnessDoctor` renders all five surfaces as "Slash commands" and "MCP
config files" sections alongside the existing User asset dirs / Skill
roots / Skill write targets blocks. Adapters that don't yet have a stable
CLI contract for a surface (today: Codex for both commands and MCP,
Copilot for project-scope commands) return `[]` so consumers can iterate
runtimes generically without per-runtime branching.

`engine/queries.js` exposes two shared helpers — `getUserHarnesses(homeDir)`
and `getProjectHarnesses(project)` — that iterate every registered runtime
via `engine/runtimes.listRuntimes()` + `resolveRuntime()` and union the
`getSkillRoots` / `getCommandRoots` / `getMcpConfigPaths` contributions,
deduped by absolute path. The same physical dir contributed by multiple
adapters (e.g. `~/.agents/skills` exposed by Claude, Copilot, and Codex)
surfaces once with a `runtimes: [name, ...]` provenance array. Use them
from dashboard / diagnostics consumers instead of re-walking adapters by
hand; `collectSkillFiles` / `collectCommandFiles` already do.

> **Follow-up.** `getStatusSlowStateMtimePaths` still hardcodes the
> per-runtime MCP file paths; refactoring it to consume
> `getUserHarnesses(...).mcps` + `getProjectHarnesses(...).mcps` is
> tracked separately so the slow-state mtime tracker automatically picks
> up new adapters.

## Related docs

- `docs/runtime-adapters.md` — full adapter interface table.
- `docs/live-checkout-mode.md` — `project.worktreeMode: 'live'`
  contract (alternative to the worktree-add-dir story for repos where
  worktrees are unworkable).
- `docs/skills.md` — skill block format and auto-extraction targets.
- `plans/seamless-user-repo-harness-invocation.md` — full multi-item plan
  this doc is the foundation of.
- `CLAUDE.md` → "Agent Spawn" + "Adapter Contract" — load-bearing
  invariants the propagation contract piggy-backs on.
