mars-agents 0.6.4

Agent package manager for .agents/ directories
Documentation
# src/harness/

Two files with a clean boundary: `registry.rs` is pure data; `host.rs` is I/O collected once per command.

## Contracts

### `registry.rs` — canonical harness vocabulary

`harness::registry` is the **only** owner of valid harness identity. No other
module should validate harness names, define provider candidate orders, or
maintain a list of known harness binaries independently.

- `HarnessId`: `Claude | Codex | Pi | OpenCode | Cursor`
- `HarnessClass`: `Native { provider }` (claude↔anthropic, codex↔openai) |
  `ProbeBacked` (pi, opencode) | `UniversalPassthrough` (cursor)
- `parse(name)` / `is_known(name)` — case-insensitive, trim-safe
- `provider_candidate_order(provider)` — canonical evaluation order for a given provider
- `UNKNOWN_PROVIDER_FALLBACK_ORDER``[Pi, OpenCode, Cursor]` for unknown/non-native providers
- `native_harness_for_provider(provider)` — only returns `Some` for anthropic→Claude, openai→Codex

**Invariant:** Adding a new harness means one change here (new descriptor in `DESCRIPTORS`)
plus registration in `all()` and `names()`. Nothing else needs updating.

### `host.rs` — capability snapshot

`CapabilitySnapshot` must be collected **once per command invocation** and shared.
Re-collecting mid-command risks probe inconsistency and unnecessary subprocess spawns.

- `collect_capability_snapshot(options)` — collects for all known harnesses
- `collect_capability_snapshot_with_resolver(options, resolver)` — testable variant with injected PATH
- `CapabilityCollectionOptions { offline, allow_probe_refresh }` — caller controls probe refresh
- `ExecutableResolver` trait — cross-platform PATH lookup; `PathExecutableResolver` is the production impl
- `AuthState`: `NotApplicable` (Pi/OpenCode/Cursor), `Authenticated`, `Unauthenticated`, `Unknown`

`CapabilitySnapshot` fields:
- `executable: BTreeMap<HarnessId, ExecutableState>` — PATH lookup result per harness
- `auth: BTreeMap<HarnessId, AuthState>` — auth probe result (only meaningful for Native harnesses)
- `opencode: CachedProbeOutcome` — OpenCode capability probe from disk cache
- `pi: CachedPiProbeOutcome` — Pi capability probe from disk cache

## Architecture

```
registry.rs   ←── pure static data (no I/O, no env reads)
                    HarnessId, HarnessDescriptor, HarnessClass
                    provider orders, normalization, native affinity

host.rs       ←── I/O at collection time only
                    ExecutableResolver → PATH lookup (once per harness)
                    native_auth_state → subprocess probe (claude/codex only)
                    opencode_cache/pi_cache → disk reads
                    → CapabilitySnapshot (cloneable, shared across command)
```

## Rationale

Before this module: harness names and validity were scattered across
`models::harness`, `compiler::agents::HarnessKind`, and hardcoded error strings.
Divergence between modules was a live risk.

`ExecutableResolver` as a trait enables fake PATH injection in tests (`FakeResolver`
with a `HashMap<String, ExecutableState>`) without touching the filesystem.

Windows `.cmd`/`.bat` fallback via the `which` crate lives in
`PathExecutableResolver::resolve` and **only** here. No other module should
implement its own extension-suffix loop.

## Patterns

**Tests:** Inject `FakeResolver` to control which harnesses are "installed"
without real binaries:

```rust
let mut resolver = FakeResolver::default();
resolver.map.insert("pi".to_string(), ExecutableState::Found { path: PathBuf::from("/tmp/pi") });
let snapshot = collect_capability_snapshot_with_resolver(&options, &resolver);
```

**Offline/test options:**

```rust
let options = CapabilityCollectionOptions { offline: true, allow_probe_refresh: false };
```

This prevents probe refresh and uses stale cache (or returns empty probe result).

**Do NOT** add harness-name validation logic outside this module. Use `registry::parse()` or `registry::is_known()`.