mars-agents 0.7.2

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

Generates launch bundles — the serializable artifact that a harness runtime consumes to
launch an agent with resolved routing, execution policy, tools, and prompt surface.

## Contracts

### Two launch-bundle modes

1. **Ad-hoc mode** (no `--agent`):
   - `--model` is optional; when omitted, routing selects an installed/default
     harness and leaves `routing.model`, `routing.model_token`, and
     `routing.harness_model` empty so the harness can use its own default model
   - Works from a plain directory with **no `mars.toml`**`can_run_without_project()`
     in `src/cli/mod.rs:248` supplies a synthetic `MarsContext` when project
     discovery fails and the exact ad-hoc condition is met
   - `empty_agent_profile()` is used (no agent file read, no skills, no tools)
   - Skills loading skipped (no `.mars/` store needed); tool resolution yields empty sets

2. **Agent/profile mode** (`--agent <name>`):
   - Reads `.mars/agents/<name>.md` from `ctx.project_root`
   - Requires a synced project (`mars.toml` present, `mars sync` already run)
   - Parses frontmatter YAML + markdown body from the agent file
   - Profile fields (model, harness, skills, tools, etc.) feed into policy resolution

### Warning semantics

`LaunchBundle.warnings[]` contains **user-actionable degraded states only**.
Harness-model path facts (`passthrough`, `synthesized`, `unknown` confidence,
`provider-match`, `cached-probe`) belong in `routing.*` and `provenance.*` fields —
they are expected route metadata, not problems a user can fix.

`src/build/policy/runnable.rs` enforces this at the outer boundary:
`resolve_routing()` always returns `warnings: Vec::new()`. Route facts are recorded
in `routing.harness_model_source` and `routing.harness_model_confidence`. The caller
layer (`policy/mod.rs`) owns warning promotion for actual degraded states.

Examples of REAL warnings (user can act on them):
- `"known linked harness constraints left no eligible auto-routing candidates; selecting linked harness \`codex\` without unrelated fallback"` → user should check `settings.targets`
- `"Cursor is an experimental launch-bundle target. The contract may change without notice."` → user is informed of instability risk
- `"tool 'X' is not a known <harness> tool; passing through verbatim"` → tool normalization couldn't resolve the name; user may have a typo

Examples that are NOT warnings (they go to routing/provenance fields):
- `harness_model_source: "passthrough"` — Pi or explicit harness receives the model token as-is; this is expected behavior
- `harness_model_confidence: "unknown"` — Pi/passthrough harnesses are provider-routers at runtime; unknown confidence is the correct answer

### `can_run_without_project` guard

```rust
// src/cli/mod.rs:248
fn can_run_without_project(cmd: &Command, err: &MarsError) -> bool {
    matches!(
        (cmd, err),
        (
            Command::Build(build::BuildArgs {
                command: build::BuildCommand::LaunchBundle(build::LaunchBundleArgs {
                    agent: None,
                    ..
                })
            }),
            MarsError::Config(ConfigError::ProjectRootNotFound { .. })
        )
    )
}
```

Only ad-hoc launch-bundle (`--agent` absent) bypasses project discovery. Every
other command — including agent-mode launch-bundle — requires `mars.toml` in an
ancestor directory.

### Warning accumulation pipeline

```
parse_diags           ← frontmatter parse warnings (agent mode only)
policy.warnings       ← harness resolution issues, model resolution issues,
                        linked-constraint degradation, experimental harness
prompt.warnings       ← missing skills
tool_warnings         ← unknown tool names on first-class harnesses
routing.warnings      ← always empty (see runnable.rs contract)
```

All warning vectors are `extend()`-ed into a single `LaunchBundle.warnings` field.

### Catalog refresh (`ensure_fresh`)

`resolve_policy` calls `models::ensure_fresh` on `.mars/` before harness evaluation — not a
read-only cache read. TTL and stale fallback follow [`src/models/AGENTS.md`](../../models/AGENTS.md).
`LaunchBundleRequest.models_refresh` carries `ModelsRefreshControl` from CLI
(`--refresh-models` → force sync catalog + synchronous probes;
`--no-refresh-models` → offline catalog + skip probe refresh; default → auto/background).

Catalog slugs feed `RoutingInput.catalog_model_slugs` so native harness matching aligns with
`mars models list|resolve` (same `evaluate_candidates` path).

### `harness_model` vs `candidate_slugs`

`LaunchBundle.routing.harness_model` is the **runtime model id** for the selected harness
(including Cursor effort baking in `runnable.rs`). `routing.candidate_slugs` copies assessment
probe/catalog candidates for debugging only — bundle doc comment: consumers run `harness_model`
verbatim. `routing.candidate_slugs` on the trace/report is diagnostic; do not use it for launch.

### Alias `provider``harness_model`

Resolved in `models::resolve_harness_model` ([`src/models/.context/CONTEXT.md`](../../models/.context/CONTEXT.md)):

- **Codex / Claude:** when alias or routing `provider` matches the native harness, emit the
  **bare** canonical model id (`gpt-5.4-mini`, not `openai/gpt-5.4-mini`).
- **Pi / OpenCode:** pick a probe-listed slug (`openai-codex/gpt-5.4-mini`, etc.); use
  `provider_constraint` only to order/filter slugs, not to prefix before the probe runs.

`harness_model_source` / `harness_model_confidence` record how the id was chosen (`provider-match`,
`cached-probe`, `passthrough`) — still not user-facing warnings.

### Cursor effort → `harness_model`

When harness is `cursor` and effort is set, `resolve_routing` calls
`resolve_cursor_effort_slug` against probe slugs. Default-tier efforts (`medium`, `none`,
`auto`, `default`) select the unsuffixed base slug when present; other tiers use suffixed
slugs. Success sets `harness_model_source` / `confidence` to cached-probe confirmed and
marks effort consumed (cleared from execution policy output).

## Architecture

```
LaunchBundleRequest {agent, model, harness, effort, approval, sandbox, extra_skills}
    ├─ [agent mode] read + parse .mars/agents/<name>.md → AgentProfile + body
    ├─ [ad-hoc mode] empty_agent_profile() + "" body
    └─ resolve_policy()
           ├─ config::load_policy_resolution_config()  (aliases, harness_order, linked targets)
           ├─ models::ensure_fresh()                   (catalog for native slug + probes)
           ├─ model::resolve_model()                   (alias → model_id + provider, or unset)
           ├─ harness::resolve_harness()               (route selection, provider/candidate eval)
           ├─ execution::resolve_execution_policy()    (effort, approval, sandbox, autocompact)
           └─ runnable::resolve_routing()              (populate Routing, warnings always empty)
       ├─ resolve_effective_skills()  (profile skills filtered by harness kind)
       ├─ compile_prompt_surface()    (system instruction + supplemental docs + inventory)
       └─ resolve_bundle_tools()      (tool normalization per harness)
           └─ LaunchBundle {routing, execution_policy, prompt_surface, tools, skills_metadata, provenance, warnings}
```

`MarsContext` is the single object passed through; it carries `project_root` and
`managed_root`. For ad-hoc mode with a synthetic context, `managed_root` resolves to
`<cwd>/.mars` which may not exist — but `resolve_policy` handles missing config
gracefully (empty aliases, empty model cache).

## Rationale

Ad-hoc mode exists because callers outside a Mars-managed project (e.g., Meridian CLI,
scripting) need routing decisions without a full `mars sync` pipeline. The guard in
`can_run_without_project` scopes this bypass narrowly — only no-`--agent`
ad-hoc launch-bundles skip project discovery, with `--model` optional.

Warning semantics are split between `warnings` (user-actionable) and routing/provenance
fields (route metadata) because callers consume the bundle differently. A downstream
harness adapter reads `routing.harness_model` to know which model ID to pass to the
provider; it doesn't need to see `"passthrough"` as a warning. But a user configuring
`settings.targets = [".codex"]` needs to know that linked-harness constraints blocked
normal routing. Confusing these two categories produces noise that desensitizes users
to real warnings.

### Anti-patterns for this module

- Do NOT add warnings for harness-model path facts (`passthrough`, `synthesized`,
  `unknown` confidence). These belong in routing fields.
- Do NOT call `build_launch_bundle` with an agent name from outside a synced project —
  the agent file must exist at `.mars/agents/<name>.md`.
- Do NOT emit `eprintln!` in library code — all diagnostics go through the warnings
  vector and are surfaced by the CLI layer.

## Related docs

- [src/models/AGENTS.md]../../models/AGENTS.md — catalog `ensure_fresh`, `ModelsRefreshControl`
- [src/routing/.context/CONTEXT.md]../../routing/.context/CONTEXT.md — harness candidate
  evaluation, selection-kind vs match-evidence semantics, and `RouteDecisionReport`
  serialization surface
- [src/harness/.context/CONTEXT.md]../../harness/.context/CONTEXT.md — harness registry,
  capability snapshot, probe integration
- [src/cli/build.rs]../../cli/build.rs — CLI arg definitions and entry points
- [tests/smoke/manual/results-launch-bundle-resolver.md]../../../tests/smoke/manual/results-launch-bundle-resolver.md  smoke evidence for routing and warning behavior