Nothing here is injected into your prompt.
rules.md is the task-facing directory: the read-only workflow plus a generated References index — no rule text. Your agent pulls the bodies it wants with harness_rules_read, or none at all.Why nothing is injected
The obvious design is to inline every rule into every prompt. It does not hold up. In a measured AppWorld run of a build that did exactly that — the whole corpus in context, ten administrative tools, and a check-your-mailbox-every-turn mandate — 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. So the harness uses the shape that already works for agent skills: a small, stable index that says what exists and how to reach it, with content behind a read. The corpus can grow without crowding out the task, and the agent decides what is relevant instead of a lexical scorer guessing on its behalf.The layout
rules.jsonl is the source of truth; every .md file is a projection, regenerated from the store on every change. Read the markdown freely; change rules through the store.
Scopes
scope is a free-form string on each rule, and it is the file the rule lands in. The repair agent chooses it from the failure evidence, as part of the call that writes the rule.
Silence means
global, not scoped. scoped is a considered verdict — “specific, but unnameable” — so a rule with no expressed scope takes the broadly-applicable default rather than landing in a catch-all. That inverts the v0.8 behavior, where an unspecified scope became scoped and, in practice, everything ended up in one file.RuleScopeHint metadata and a short task_summary on the turn payload to inform the choice; neither dictates it. PandaProbe enforces only filename safety and refuses a generic host or integration label, since that names where the agent ran rather than what failed.
The generated guide
rules.md sits beside the rules.jsonl store and the rules/ scope files it indexes. It has SKILL-style frontmatter declaring the four allowed read-only tools, a stable body explaining the pull workflow, and a generated References section — global first, then every other live scope alphabetically:
The guide is a derived view, not a document you maintain. Everything above the References marker comes from the packaged template and is versioned with the package; everything below is regenerated from
rules.jsonl on every rule mutation and at startup. Hand-edit either half and the next rule write overwrites it. Delete the file and it comes back.A rule file
The rule record
Guard rails on
add:
- Dedup — a rule whose normalized text matches a live rule returns the existing rule instead of creating a duplicate.
- Novelty — a proposal already covered by a live rule in the same scope resolves as
duplicateoralready_coveredinstead of adding near-identical guidance. This, not a global count, is what controls proliferation. - Cap —
max_active_rulesdefaults to0, meaning unlimited. Set a positive value as an operator safety bound; nothing is ever silently evicted. - Metric validation —
metricmust name a real evaluator metric. Validation matches it againstbreach:<metric>signatures to decide what counts as evidence, so a name matching nothing reads as “never breached” and can invert the rule’s own verdict. - Sanitization — rule text and rationale cross the trust boundary before they can ever reach a prompt.
Older stores
A record with noscope reads as the default, global — like any other unspecified field. Existing global, scoped, and custom scope files keep working untouched: nothing is migrated, moved, or duplicated, and there is no migration command. The first render regenerates rules.md and the rules/ subtree from whatever the store holds. See Upgrading to v0.9.
A scope whose rules all retire keeps its file, rewritten empty, rather than being deleted — a prior context may reference the path, and a file saying “no rules here” is a better answer than a missing one.
Retrieval
rule_retrieval (default true) and rules_context_topk (default 8) narrow the active rules inside a rendered scope file when a caller passes a query. They do not shape the system prompt, which carries no rule bodies to shape, and they do not affect the task-facing read/search/list tools — those never preselect on the agent’s behalf.
The scorer is deliberately simple and dependency-free: casefolded word tokens, tag hits weighted double over rule/rationale text, normalized by query size, ties broken by recency. breach:tool_correctness tokenizes to breach + tool_correctness, so signatures, metric names, and task words all match naturally.
Behavior at the edges:
- No query → everything renders; retrieval never hides rules without a reason.
- No overlap → ranking degrades to recency under the same top-k.
- Candidates never consume top-k slots.
- A trimmed file carries a one-line note pointing at
harness_rules_search/harness_rules_list, so every rule stays reachable.
Working with rules programmatically
harness_rules_read, harness_rules_list, harness_rules_search, and harness_rule_status — see the task toolset. Writing (harness_rule_add) belongs to the repair agent; promotion and retirement belong to validation alone.
render_markdown() renders the guide plus every scope file, candidates included. It is an explicit operator call — not the system prompt, and no longer the replay context either: a replay now receives a capability-only context and discovers rules through the same read-only tools, with only the candidate under test visible.
