> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pandaprobe.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upgrading to v0.9

> Coming from v0.8: the repair agent takes over the workspace, and what that means for your code

v0.9 moves rule authoring out of your task agent and into a package-owned **repair agent**. If you were using the harness as designed in v0.8, expect three edits: set a repair model, swap `harness.toolset` for `harness.task_tools`, and pass a session id to `system_context()`.

<Note>
  Your workspace carries over. `rules.jsonl` is unchanged and every existing `global`, `scoped`, and custom scope file keeps working — no rule is migrated, moved, or duplicated.
</Note>

## Why it changed

A measured AppWorld run of the v0.8 design showed the cost of self-administration directly. Handed ten administrative tools and a standing instruction to check its mailbox, the agent spent its turns operating the harness instead of doing the task, and **nine of the thirteen rules it wrote were about gaming its own diagnostic protocol** rather than about the work.

The problem is structural: diagnosis and execution want different context, different tools, and different success criteria. So they are now different agents. See [the repair agent](/harness/loop/repair-agent).

## 1. Set a repair model

`Harness.create()` now raises `ValueError` without one, because repair makes its own model calls and no billable default is selected for you.

```python theme={null}
# v0.8
harness = Harness.create()

# v0.9
harness = Harness.create(HarnessConfig(repair_model="openai/gpt-..."))
# or: export HARNESS_REPAIR_MODEL=openai/gpt-...
```

Any LiteLLM identifier works — `openai/…`, `anthropic/…`, `bedrock/anthropic.…`, `vertex_ai/…` — and the provider's own credentials must be in the environment. `observe_only=True` still constructs without a model: it evaluates and journals without mutating anything.

<Warning>
  Mutating construction also requires `rule_validation=True` (the default). A repair-authored rule must not be able to skip the candidate lifecycle, so `Harness.create()` refuses the combination outright.
</Warning>

## 2. The task surface is four read-only tools

`HarnessToolset`, `Harness.toolset`, `Harness.shell`, `OP_SCHEMAS`, `build_toolset_from_env`, and the `pandaprobe-harness-agent` companion CLI are all gone.

```python theme={null}
# v0.8
specs, dispatch = as_anthropic_tools(harness.toolset)

# v0.9
specs, dispatch = as_anthropic_tools(harness.task_tools)
# or simply: tools = my_tools + list(harness.task_tools.specs())
```

| v0.8 tool                                                                                 | v0.9                                                                                                            |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `harness_rules_read`, `harness_rules_search`, `harness_rules_list`, `harness_rule_status` | **Kept** — the entire task surface.                                                                             |
| `harness_mailbox_list`, `harness_mailbox_read`, `harness_mailbox_ack`                     | Removed. Repair uses `harness_notice_read` / `harness_notice_ack` / `harness_notice_resolve`, package-internal. |
| `harness_trace_inspect`                                                                   | Repair-only, restricted to traces the assigned notice names.                                                    |
| `harness_rule_add`                                                                        | Repair-only, at most one candidate per episode.                                                                 |
| `harness_rule_retire`                                                                     | Removed entirely. Only [validation](/harness/closed-loop/rule-validation) retires a rule.                       |

Two semantics changed for tools you keep: `harness_rules_read` now defaults to `global` when you pass no `scope`, and `harness_rules_list` returns the `rules.md` guide plus a compact live-scope index rather than a list of rules by status.

<Note>
  Route every `harness_*` name to `harness.task_tools.call` rather than filtering names yourself. Enforcement lives in the dispatcher, so a hallucinated `harness_rule_add` returns `{"ok": false, "error": "unsupported capability ..."}` instead of reaching your domain executor.
</Note>

## 3. `system_context()` takes a session id

```python theme={null}
# v0.8
system_prompt = harness.system_context() + MY_PROMPT

# v0.9
system_prompt = harness.system_context(session_id) + MY_PROMPT
```

The same applies to `PandaHarnessHook.startup_context(session_id)` and `compose_system_preamble(...)`; the no-argument and two-argument forms are removed.

What it returns also changed. In v0.8 the preamble carried a skill root, a References index, and a `⚠ HARNESS: N pending notice(s)` banner. In v0.9 it is a **constant capability sentence** naming the four read-only tools. There is no banner, no index, and no rule text — building it reads nothing at all.

If you were parsing the banner to detect pending notices, that signal now lives in `SettleResult.repair` instead.

## 4. Read the repair outcome from settlement

```python theme={null}
settlement = await harness.settle(session_id)

if settlement.repair is not None:
    settlement.repair.status              # "candidate_added" | "no_proposal" | ...
    settlement.repair.candidate_rule_ids
    settlement.repair.selected_scope
```

`settle()` now covers evaluation, notice persistence, **and** one bounded repair attempt. It still does not wait for a validation round — a replay can need the very resource the current turn holds.

## 5. The workspace guide is `rules.md`

The generated task-facing guide is `<harness_root>/rules.md`, beside the `rules.jsonl` store and `rules/` scope files it indexes (previously `harness_guide.md`, and `harness_rules.md` before that).

There is no rename step and no migration command: `rules.md` is regenerated from `rules.jsonl` on every rule mutation and at startup, so it appears on first construction with your existing rules correctly indexed. A carried-over workspace may keep an old, unreferenced guide file — delete it.

## 6. The default scope is `global`

A new rule defaults to `global` rather than `scoped`, and a task-facing read with no `scope` argument defaults to `global`.

The reasoning: `scoped` is now a *considered verdict* — "this rule is specific, but no meaningful stable name could be determined" — rather than a catch-all for anything unclassified. Silence should mean "broadly applicable", not "unclassifiable". The repair agent prefers a real contextual name (an application, workflow, or domain from the evidence) whenever one is justified.

Existing rules keep whatever scope they were filed under.

## 7. Optionally, feed the scope decision

Two new optional fields on the turn payload improve where rules get filed. Neither is required, and neither dictates the outcome.

```python theme={null}
harness.on_turn_end({
    "session_id": session_id,
    "turn_index": n,
    "end_state": end_state(),
    "task_summary": "Refund order 1017 and confirm the customer was notified.",
    "rule_scope_hints": [
        RuleScopeHint(key="payments", description="Payment workflows.").to_json()
    ],
})
```

`task_summary` matters most: told only an opaque task id, a model has nothing to name a scope after.

## Other removals

| Removed                                                    | Note                                                                                               |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `HarnessConfig.concurrent_eval`, `HARNESS_CONCURRENT_EVAL` | One eval run has covered every metric since v0.8; nothing read the flag.                           |
| `Resolution.kind == "legacy"`                              | Every resolution names what happened; an unrecognized kind reads as `no_proposal`.                 |
| `severity: "relative"`                                     | The pre-v0.8 alias. Unknown severities now read as `breach` rather than de-escalating to advisory. |
| `HarnessConfig.legacy_rules_file`                          | There is one guide filename and no rename path.                                                    |

`DiagnosticNotice.recommended_scope` and `RepairAssignment.recommended_scope` are now `str | None`, where `None` means "no host recommendation" — deliberately distinct from recommending the default.

## New, worth adopting

* **`harness.settle_validation(timeout=...)`** at a phase boundary, before you snapshot or report a ruleset. `settle()` deliberately skips validation; this is the call that runs it to a standstill so a candidate that earned a verdict actually receives one.
* **`validation_round_budget_s`** if candidates accrue faster than replays decide them. Past the budget, remaining candidates get the cheap forward-trial verdict instead of no verdict at all.
* **`replay_env_wait_timeout_s`** plus `ReplayContext.mark_execution_started()` if your replay queues for a shared environment. Without it, queueing is charged to the execution budget and a replay that never ran is recorded as inconclusive evidence.
* **`drain_validation(timeout=...)`** now returns whether it actually drained, and `harness.validation_pending` reports rounds in flight.

## A quick checklist

<Steps>
  <Step title="Set repair_model">
    `HARNESS_REPAIR_MODEL` or `HarnessConfig(repair_model=...)`, plus that provider's credentials. Or `observe_only=True` to evaluate without mutating.
  </Step>

  <Step title="Swap toolset for task_tools">
    Replace `harness.toolset` with `harness.task_tools`. Delete any reference to `harness.shell`, `HarnessToolset`, `OP_SCHEMAS`, or the `pandaprobe-harness-agent` CLI.
  </Step>

  <Step title="Pass the session id to system_context()">
    `harness.system_context(session_id)`. Drop any banner parsing.
  </Step>

  <Step title="Update tool-name lists and notice parsers">
    Task agents get four reads. If you asserted on ten tool names, or read notices from your agent's code, both need revisiting.
  </Step>

  <Step title="Add a phase-boundary settle_validation()">
    Anywhere you snapshot, archive, or report a ruleset, run validation to a standstill first.
  </Step>

  <Step title="Consider task_summary">
    A one-line statement of what the turn was trying to do measurably improves where rules get filed.
  </Step>
</Steps>
