Skip to main content
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().
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.

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.

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.
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.
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.

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.
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.
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.

3. system_context() takes a session id

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

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.
task_summary matters most: told only an opaque task id, a model has nothing to name a scope after.

Other removals

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

1

Set repair_model

HARNESS_REPAIR_MODEL or HarnessConfig(repair_model=...), plus that provider’s credentials. Or observe_only=True to evaluate without mutating.
2

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.
3

Pass the session id to system_context()

harness.system_context(session_id). Drop any banner parsing.
4

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.
5

Add a phase-boundary settle_validation()

Anywhere you snapshot, archive, or report a ruleset, run validation to a standstill first.
6

Consider task_summary

A one-line statement of what the turn was trying to do measurably improves where rules get filed.