Open Agent Rules

OAR is an open standard; this page describes Painted Wolf Code’s implementation. The specification, schemas, and conformance corpus live at openagentrules.org. How this host evaluates rules is open-agent-rules.md in the product repository.

Open Agent Rules (OAR) is a portable standard for agent guardrails: a rule is one YAML document — a lifecycle anchor, a selector, a typed condition over observed facts, and a declared effect. Painted Wolf Code accepts oar: '1.0' and fails closed on other versions. Host-specific binding and selector schemas live in the product repository.

A complete rule

Rules ship in a pack’s policy/ directory (unit id policy/<CODE>), one file per rule:

oar: '1.0'
id: TRIAGE_LITE_WRITE_FORBIDDEN        # SCREAMING_SNAKE, equals the file stem
kind: policy                           # schema | policy | invariant | detector
anchor: tool.pre_invoke                # a lifecycle anchor id
selector:                              # clauses AND; each names a declared fact
  tool: [write, edit]
requires:
  profiles: [session, tool]
when: session_posture == "spec"
effect: block                          # block | warn | nudge | allow | transform
enforcement: enforce
on_error: fail_closed
copy:
  title: Triage Lite is read-only
  what: A write tool was called during triage.
  cause: The triage-lite workflow reports findings; it does not change files.
  fix: Record the finding in your write-up, then leave triage before editing.
  instead: Branch on Code TRIAGE_LITE_WRITE_FORBIDDEN and report instead of writing
FieldRequiredNotes
oaryesVersion gate at load — '1.0'
idyesStable; doubles as the Code: agents branch on
kindyesOne of the four tiers below
anchoryesA lifecycle anchor id from the host’s catalog
effectyesWhat happens when the rule fires — see Effects
selectornoClauses AND; omitting a clause is permissive
requireswith factsDeclares the fact profiles the rule reads
when / flownoAbsent means the rule fires whenever anchor + selector match
enforcementnoDefault enforce; monitor evaluates and reports but never acts
on_errornoDefault fail_closed

Unknown fields are rejected by name. Human copy lives in copy: (title / what / cause / why / fix / instead).

Four kinds

kind fixes evaluation order—schema → policy → invariant → detector, then id. anchor: decides when the rule runs. The first block short-circuits; warnings and nudges accumulate.

KindWhat it expresses
schemaArgument and shape validity
policyContext-dependent allow/deny — profile, scope, posture
invariantPre/post conditions over turn and session state
detectorA condition delegated to a registered external validator

Where rules fire

anchor: names a lifecycle moment. Guardrails and prompt injects select on the same catalog. Scope a rule with the selector, not by minting new anchors: tool.pre_invoke + selector.tool: [read, grep].

Trigger classWhen
pre_invokeBefore a tool handler runs
tool_handlerInside a native tool’s own checks
post_toolAfter a successful invoke
post_turnA coordinator prose turn with no tool call
finalizeWorker/child completion
input / output / tool_resultModel-IO content anchors

The full catalog — every legal anchor: value — is generated from the host:

Show the full lifecycle anchor catalog (62)

tool_rejection

AnchorFires whenSurface
gate.blocked
Gate blocked
phase_gate_unmet blocks advance; coordinator kick carries failed_leaves.coordinator inform
coordinator.citation.grounding
Coordinator citation grounding
Closeout JSON rejected for untraced cited_evidence or cited_urls.coordinator inform
worker.citation.grounding
Worker citation grounding
Worker completion JSON fails evidence grounding.worker inform

workflow_event

AnchorFires whenSurface
compose.done
Compose done
Successful compose upsert; coordinator should start the workflow run.coordinator inform
phase.advanced
Phase advanced
Host auto-advances workflow phase (fallback when no on_enter.reenter_leg).phase inform
review_loop.continue
Review loop continue
Non-terminal review_loop verdict below iteration_cap — host re-prompt.phase inform
review_loop.decide
Review loop decide
review_loop reached iteration_cap — coordinator must render terminal verdict.phase inform
feedback.received
Feedback received
Pending user feedback resolves (user submit).coordinator inform
phase.exit_required
Workflow phase exit required
Turn ended while the active workflow phase requires an exit action.coordinator inform
inject.transition
Workflow transition inject
Transition guidance inject on coordinator turns.coordinator inform

manifest_on_reenter

AnchorFires whenSurface
leg.finished
Leg finished
Worker leg reaches terminal state (re-enter / digest).coordinator inform
worker.task.finished
Worker task finished
Implement-mode task() worker reaches reactable terminal.coordinator inform

phase_on_enter

AnchorFires whenSurface
phase.entered
Phase entered
Cross-phase entry fires on_enter injects (fanout-plan/execute, topology, debate, findings, feedback-pending).phase inform
feedback.pending
Feedback pending
Phase on_enter declares request_user_feedback.coordinator inform

host_api

AnchorFires whenSurface
worker.task.started
Worker task started
Implement-mode task() spawns a worker child.worker inform
worker.leg.started
Worker leg started
Delegation leg spawns a worker child.worker inform
coordinator.closeout
Coordinator closeout
Investigate/synthesis final turn forced to the final report (no tools).coordinator inform
project.roots.changed
Project roots changed
Project crosses from one folder to two or more (POST roots).coordinator inform
project.root.detach_cancelled
Root detach cancelled work
DELETE root?force=true stopped workers, overlays, or busy sessions.coordinator inform
worker.closeout
Worker closeout
Worker child turn ends with prose-only survey (no tools).worker inform
worker.iterations.low
Worker iterations low
Worker child warned once when few tool iterations remain.worker inform
worker.cancel.closeout
Worker cancel closeout
Gracefully cancelled worker leg ends with prose-only survey.worker inform
worker.summary.trim
Worker summary trim
Worker survey exceeds parent character budget.worker inform
progress.stale
Progress checklist stale after interrupt
User abort left open checklist steps; queue a reconcile nudge.coordinator inform
authz.seal_failed
Authorization seal failed
Run-start authorization context could not be recorded; the turn did not start.coordinator inform
turn.closeout
Turn closeout
Host limit forces a final prose-only turn on a non-closeout coordinator surface.coordinator inform
turn.iterations.low
Turn iterations low
Coordinator warned once when few tool iterations remain this turn.coordinator inform
turn.spend.runway_low
Turn spend runway low
Session first crosses the spend-ceiling warning point (one-shot).coordinator inform
turn.spend.soft_stop
Turn spend soft stop
Running task crossed its spend ceiling; one bounded tool-capable landing round.coordinator inform
outbound.secret.withheld
Outbound secret withheld
Host withheld an outbound request carrying a detected secret; turn continues.coordinator inform

wait_trigger

AnchorFires whenSurface
wait.timer.fired
Wait timer fired
Coordinator wait() timer expires (until includes timer).coordinator inform
scan.finished
Scan finished
Security scan reaches terminal while coordinator waits on scan_done.coordinator inform
process.finished
Process finished
Command/verify process reaches terminal while coordinator waits on process_done.coordinator inform
worker.budget.low
Worker budget low
Worker tool runway ≤ wake buffer while job still running.coordinator inform
loop.wake
Loop wake
Host loop-wake turn; suppress kick when LLM runs ([host:loop-wake] sentinel). Non-content Anchor — no inform Binding.coordinator inform

overlay_state

AnchorFires whenSurface
overlay.promote.complete
Overlay promote complete
All write overlays integrated (zero pending).coordinator inform

observed_history

AnchorFires whenSurface
edit.follow_up.repeat
Edit follow-up repeat
Two consecutive user-visible turns each answered with coordinator inline file edits.coordinator inform
progress.missing
Progress checklist missing
Turn ran progress-gated tools with no checklist authored; queue an authoring nudge.coordinator inform
survey.summarize
Survey stacked without summarize
Stacked read/grep/find since the user-intent boundary with no summarize this round.coordinator inform
inject.ledger
Ledger receipts inject
BuildLedgerReceipts / RenderLedgerBlock system inject.coordinator inform
inject.synthesis_evidence
Synthesis evidence inject
Synthesis-surface evidence digest inject.coordinator inform

assembly_inject

AnchorFires whenSurface
board.changed
Board reinjected
Pack board orientation reinjected; omit leg-finished kick on same turn.coordinator inform
inject.active_workflow
Active workflow inject
Pre-history active-workflow.md / run-context inject (InjectRenderer path).coordinator inform
inject.worker_board
Worker board inject
Worker child pack-board block at run start.worker inform
inject.worker_leg
Worker leg inject
L3 worker-leg.md inject for delegation legs.worker inform
inject.agents_md
Agents.md inject
Project AGENTS.md / agents-md standard inject prepend.coordinator inform
inject.command_jobs
Active command jobs inject
Host-authoritative ledger of live command and verify command jobs.agent inform
inject.implement_spawn
Implement spawn inject
Implement-surface spawn roster inject.coordinator inform
inject.blueprint
Blueprint inject
Workflow blueprint frontmatter inject.coordinator inform
inject.worker_task_assignment
Worker task assignment inject
Worker assignment envelope inject.worker inform
inject.worker_task_preamble
Worker task preamble inject
Worker task preamble (scope/paths) inject.worker inform
inject.scan_guidance
Scan guidance ephemeral inject
Ephemeral scan guidance summaries inject.coordinator inform

pre_invoke

AnchorFires whenSurface
tool.pre_invoke
Tool pre-invoke
Before a tool handler runs; scope with selector.tools.coordinator block transform
coordinator.pre_invoke
Coordinator pre-invoke
Coordinator-scoped pre-invoke. Prefer tool.pre_invoke + selector.profiles:[coordinator] when equivalent.coordinator block
session.pre_invoke
Session pre-invoke policy
Session/manifest policy checks.coordinator block

tool_handler

AnchorFires whenSurface
tool.handler
Tool handler
Inside native/structured tool handler (write-scope jail, inline checks).coordinator block

post_tool

AnchorFires whenSurface
tool.post_invoke
Tool post-invoke
After a completed invoke (warn/banner).coordinator block

post_turn

AnchorFires whenSurface
coordinator.post_turn
Coordinator post-turn
Coordinator no-tool prose turn. Specialized provisional ids (ungrounded/synthesis/investigate/host) map here; distinguish with when:.coordinator block inform

finalize

AnchorFires whenSurface
worker.finalize
Worker finalize
Child/worker completion summary evaluation.worker block inform

input

AnchorFires whenSurface
content.input
Content input
User + retrieved content before the model sees it (content-safety; core model.input).coordinator block transform

output

AnchorFires whenSurface
content.output
Content output
Model response before shown or acted on (content-safety; core model.output).coordinator block transform

tool_result

AnchorFires whenSurface
content.tool_result
Content tool result
Tool/RAG content before the model consumes it (content-safety; core model.tool_result).coordinator block transform

The Surface column names who the anchor fires for — coordinator, worker, or the workflow phase — followed by the effect planes the anchor declares. An anchor on the inform plane is one a binding can render prompt content at; block is where a rule rejects; transform is where content may be rewritten in place. Most anchors carry a single plane — pick the one that matches the effect you are authoring.

Effects

effectBehavior on this host
blockReject the tool call or turn with a structured Code: reject
nudgeQueue a user-role note into the agent’s next prompt
warnAppend a banner to the tool output and allow it
allowExplicit pass — device/stock policy only
transformRewrite content at a model-IO anchor

Project-contributed policy/ units may use only block, warn, or nudge. Branch on Code: — never on message text.

Conditions are CEL over facts

when: is a closed, typed CEL environment. Identifiers are registered facts — a typo’d fact name fails at load:

when: 'session_posture == "spec" && lycaon.tool_is_state'
when: mcp_schema_matched && mcp_field_bool("is_ready")

Standard-profile facts are bare names; host-specific facts are namespaced lycaon.*. The host produces facts; the rule composes them; the engine renders effects.

The fact catalogue

This is the full closed set, generated from the same engine table that builds the live CEL environment:

Show the full fact and function catalog

Structural facts

Cheap host, session, and tool state — set on every evaluation.

FactTypeTierObservation
anchorstringcoreThe anchor id of the occurrence being evaluated; every rule at that occurrence sees the same value.
permission_profilestringstandard
tool
The permission profile in force for this call.
principalstringstandard
session
The identity on whose behalf the call is made.
principal_roleslist<string>standard
session
Roles the host attributes to the principal.
content_lengthintstandard
content-safety
Length of the content at this anchor, in Unicode code points.
content_roleslist<string>standard
content-provenance
Transport-independent role for each content segment, in segment order.
content_originslist<string>standard
content-provenance
Host-attributed origin for each content segment, in segment order.
content_authoritieslist<string>standard
content-provenance
Instruction authority the host attributes to each content segment, in segment order.
content_trust_tierslist<string>standard
content-provenance
Host trust classification for each content segment, in segment order; trust does not itself grant instruction authority.
content_sourceslist<string>standard
content-provenance
Host-defined source id for each content segment, in segment order, or empty when none exists.
content_segment_countintstandard
content-provenance
Number of structured content segments at this occurrence, matching the length of every provenance list.
content_contains_untrustedboolstandard
content-provenance
True when at least one content segment is classified as untrusted.
toolstringstandard
tool
The tool name for the current call.
tool_argsmap<string, dyn>standard
tool
The current call’s arguments, as a map.
last_assistantstringhost
lycaon.last_assistant
The last assistant message text.
turn_toolslist<string>host
lycaon.turn_tools
Tool names invoked so far this turn.
session_posturestringstandard
session
The active session posture id.
surfacestringhost
lycaon.surface
The active coordinator capability surface.
profilestringhost
lycaon.profile
The calling agent’s tool profile id.
phasestringhost
lycaon.phase
The active workflow phase id.
workers_idleboolhost
lycaon.workers_idle
True when no delegated workers are running.
active_worker_countinthost
lycaon.active_worker_count
Number of delegated workers currently running.
pending_overlay_promoteboolhost
lycaon.pending_overlay_promote
True when a promote-eligible write overlay awaits landing.
overlay_statestringhost
lycaon.overlay_state
The current write overlay’s lifecycle state.
surface_may_finishboolhost
lycaon.surface_may_finish
True when the active surface allows finishing the run.
is_host_cycle_turnboolhost
lycaon.is_host_cycle_turn
True when this turn was initiated by the host loop, not the user.
batch_phasestringhost
lycaon.batch_phase
The coordinator batch phase for this cycle.
batch_closedboolhost
lycaon.batch_closed
True when the current batch is already closed.
progress_open_itemsinthost
lycaon.progress_open_items
Count of open rows on the progress checklist.
progress_has_open_stepsboolhost
lycaon.progress_has_open_steps
True when the root progress checklist has open - [ ] rows.
has_completion_reportboolhost
lycaon.has_completion_report
True when the last assistant text parses as a coordinator completion report.
task_envelope_echoboolhost
lycaon.task_envelope_echo
True when the last assistant text echoes a worker task envelope.
closeout_surfaceboolhost
lycaon.closeout_surface
True when the active surface delivers typed JSON closeout.
progress_reconcile_neededboolhost
lycaon.progress_reconcile_needed
True when a synthesis progress update is not reconcile-only.
verify_requiredboolhost
lycaon.verify_required
True when verify is required before close for this session.
verifier_passboolhost
lycaon.verifier_pass
True when host verify() has already passed.
premature_verify_closeboolhost
lycaon.premature_verify_close
True when a progress update would close verify rows before host verify().
synthesis_wrapup_tool_forbiddenboolhost
lycaon.synthesis_wrapup_tool_forbidden
True when the tool is forbidden on the synthesis wrap-up surface.
progress_closure_armedboolhost
lycaon.progress_closure_armed
True when the progress-closure latch is armed for this root.
progress_gated_toolboolhost
lycaon.progress_gated_tool
True when the tool is progress-gated.
progress_missingboolhost
lycaon.progress_missing
True when no usable progress checklist is present.
review_loop_activeboolhost
lycaon.review_loop_active
True when the review loop is active for the session.
overlay_integrate_pendingboolhost
lycaon.overlay_integrate_pending
True when write overlays await promote or reject.
path_is_worker_branchboolhost
lycaon.path_is_worker_branch
True when the tool path resolves under a worker sandbox branch.
has_partial_worker_jobsboolhost
lycaon.has_partial_worker_jobs
True when history still holds partial worker job envelopes.
verify_has_commandboolhost
lycaon.verify_has_command
True when the verify() call already carries a command.
verify_declaredboolhost
lycaon.verify_declared
True when the project declares a verify command.
progress_closed_beyond_baselineboolhost
lycaon.progress_closed_beyond_baseline
True when closed progress rows exceed the armed baseline.
pending_user_inputboolhost
lycaon.pending_user_input
True when a user question is pending an answer.
stub_validboolhost
lycaon.stub_valid
True when the bound plan stub satisfies the required headings.
tool_allowed_for_profileboolhost
lycaon.tool_allowed_for_profile
True when the profile’s capability ceiling allows this tool.
habit_redirect_matchstringhost
lycaon.habit_redirect_match
The native-tool redirect id matched for a bash habit, or empty.
write_rootslist<string>host
lycaon.write_roots
The active write-scope root paths.
pattern_parse_okboolhost
lycaon.pattern_parse_ok
True when the call’s pattern argument parsed.
arg_validation_errorslist<string>standard
tool
Argument validation error tokens for the current call.
action_host_resourceslist<string>host
lycaon.action_host_resources
Host resource ids this action was matched against — machine-observed catalog state, never inferred from command or tool prose.
action_host_resource_denialslist<string>host
lycaon.action_host_resource_denials
Host resource ids the action was refused for.
mode_bitsstringhost
lycaon.mode_bits
The mode string observed on a chmod-style call.
tool_args_fingerprintstringstandard
tool
Stable fingerprint of the call’s arguments, for repeat detection.
posture_unresolvedboolhost
lycaon.posture_unresolved
True when the session posture is not yet resolved.
high_risk_toolboolhost
lycaon.high_risk_tool
True when the tool is classed high-risk.
tool_is_stateboolhost
lycaon.tool_is_state
True when the tool is a workflow state tool.
tool_is_delegationboolhost
lycaon.tool_is_delegation
True when the tool is a delegation tool.
tool_is_taskboolhost
lycaon.tool_is_task
True when the tool is task().
tool_is_handoffboolhost
lycaon.tool_is_handoff
True when the tool is a handoff tool.
pack_runner_taskboolhost
lycaon.pack_runner_task
True when the task targets a pack-runner agent.
agent_is_plan_writerboolhost
lycaon.agent_is_plan_writer
True when the task agent is the plan writer.
disallowed_agentboolhost
lycaon.disallowed_agent
True when the requested agent is not allowed on this surface or posture.
plan_awaiting_approvalboolhost
lycaon.plan_awaiting_approval
True when the plan is waiting on human approval.
worker_attempted_mutationboolhost
lycaon.worker_attempted_mutation
True when an implementer child invoked a file-mutation tool.
batch_ready_ignoring_progressboolhost
lycaon.batch_ready_ignoring_progress
True when wrap-up gates hold except open progress steps.
synthesis_delay_countinthost
lycaon.synthesis_delay_count
Per-cycle count of open-progress closeout delays.
review_verdict_gate_openboolhost
lycaon.review_verdict_gate_open
True when the review verdict gate is still open.
verdict_delay_countinthost
lycaon.verdict_delay_count
Per-cycle count of review-verdict delays.
scope_modestringhost
lycaon.scope_mode
The task() scope mode — read or write.
scope_paths_emptyboolhost
lycaon.scope_paths_empty
True when a write scope has no paths.
scope_too_broadboolhost
lycaon.scope_too_broad
True when a write scope is a sole repo-wide glob under parallel writes.
profile_mutation_capableboolhost
lycaon.profile_mutation_capable
True when the task agent’s tool profile can edit files, run commands, or create commits.
base_overlay_idstringhost
lycaon.base_overlay_id
The stacked base overlay id from task scope, or empty.
base_overlay_resolvesboolhost
lycaon.base_overlay_resolves
True when base_overlay_id names a live write overlay.
base_overlay_pendingboolhost
lycaon.base_overlay_pending
True when the base overlay is still pending and stackable.
active_read_countinthost
lycaon.active_read_count
Number of read workers currently running.
active_write_countinthost
lycaon.active_write_count
Number of write workers currently running.
max_workersinthost
lycaon.max_workers
The configured worker cap.
max_read_workersinthost
lycaon.max_read_workers
The configured read-worker cap.
max_write_workersinthost
lycaon.max_write_workers
The configured write-worker cap.
citation_unverifiableboolhost
lycaon.citation_unverifiable
True when a typed citation is unverifiable against the ledger.
scout_survey_evidence_presentboolhost
lycaon.scout_survey_evidence_present
True when a read scout ran a successful survey tool.
surface_claim_ungroundedboolhost
lycaon.surface_claim_ungrounded
True when a UI-complete claim lacks surface snapshot evidence.
page_measure_ungroundedboolhost
lycaon.page_measure_ungrounded
True when a layout claim lacks page geometry evidence.
agent_is_scoutboolhost
lycaon.agent_is_scout
True when the worker agent is a read scout.
agent_is_implementerboolhost
lycaon.agent_is_implementer
True when the worker agent is an implementer.
profile_surveys_project_treeboolhost
lycaon.profile_surveys_project_tree
True when the task agent’s tool profile can inventory or search the project tree.
repo_known_emptyboolhost
lycaon.repo_known_empty
True when the host measured zero indexable files in the project.
last_audit_ungroundedboolhost
lycaon.last_audit_ungrounded
True when the last assistant citation audit failed to trace.
grounding_escalatedboolhost
lycaon.grounding_escalated
True when the grounding circuit breaker has escalated.
command_not_argvboolhost
lycaon.command_not_argv
True when a command or terminal request does not provide a structured argument vector.
is_directoryboolstandard
filesystem
The path resolved to a directory.
not_foundboolstandard
filesystem
The path or artifact is missing.
path_deniedboolstandard
filesystem
Path policy denied the target.
bulk_deniedboolhost
lycaon.bulk_denied
A bulk operation was denied.
binary_deniedboolhost
lycaon.binary_denied
A binary-file operation was denied.
mode_deniedboolhost
lycaon.mode_denied
The requested mode was denied.
path_escapeboolhost
lycaon.path_escape
The path escapes the permitted root.
beyond_eofboolhost
lycaon.beyond_eof
The requested range lies beyond end of file.
not_runningboolhost
lycaon.not_running
The referenced process is not running.
unsupportedboolhost
lycaon.unsupported
The requested operation is unsupported for this target.
resource_limitboolhost
lycaon.resource_limit
A resource limit was hit.
conflictboolhost
lycaon.conflict
The operation conflicts with current state.
path_requiredboolhost
lycaon.path_required
A required path argument is missing.
id_requiredboolhost
lycaon.id_required
A required id argument is missing.
policy_deniedboolstandard
tool
A profile or scope policy denied the call.
unknown_targetboolhost
lycaon.unknown_target
The named target does not exist.
missingboolhost
lycaon.missing
A required argument or artifact is missing.
forbiddenboolhost
lycaon.forbidden
The operation is forbidden in this context.
selector_emptyboolhost
lycaon.selector_empty
The selector matched nothing.
selector_ambiguousboolhost
lycaon.selector_ambiguous
The selector matched more than one target.
reject_observationstringhost
lycaon.reject_observation
Snake_case one-off observation token for unique reject codes.
pii_entitieslist<map<string, dyn>>standard
content-safety
PII entity spans from a registered detector — the rule’s when: owns the threshold. Empty when no detector ran.
secret_matcheslist<map<string, dyn>>standard
content-safety
Secret and credential spans from a registered detector — the rule’s when: owns the threshold. Empty when no detector ran.
recent_tool_nameslist<string>host
lycaon.recent_tool_names
Recent tool names in order, for flow: sequence matching.
mcp_server_idstringstandard
mcp
MCP catalog server id for this call; empty when the call is not MCP.
mcp_tool_namestringstandard
mcp
Unqualified MCP tool name for this call.
mcp_qualified_toolstringstandard
mcp
Host tool name mcp_{server}_{tool}; equals tool for MCP calls.
mcp_server_configuredboolstandard
mcp
True when the call’s server id exists in the merged MCP catalog.
mcp_server_enabledboolstandard
mcp
True when the call’s server is enabled.
mcp_call_okboolstandard
mcp
True post-call when the MCP call succeeded; always false pre-call.
mcp_error_codestringstandard
mcp
Machine error code bridged from the MCP error, or empty — never free text.
mcp_schema_matchedboolstandard
mcp
True when at least one pack binding validated the result JSON.

Derived facts

Computed over history and the evidence ledger by lazy providers — assembled only when a selected rule references them.

FactTypeTierObservation
unobserved_cited_pathslist<string>host
lycaon.unobserved_cited_paths
Cited file paths never observed by a tool this session.
unobserved_cited_urlslist<string>host
lycaon.unobserved_cited_urls
Cited URLs never fetched this session.
unobserved_cited_handleslist<string>host
lycaon.unobserved_cited_handles
Cited evidence handles that don’t trace to the ledger.
citation_fields_presentboolhost
lycaon.citation_fields_present
True when required typed citation fields are present.
claims_completionboolhost
lycaon.claims_completion
True when the last assistant text claims completion.
has_matching_ledger_jobboolhost
lycaon.has_matching_ledger_job
True when a ledger job matches the claimed work.
ledger_criteria_metboolhost
lycaon.ledger_criteria_met
True when the ledger records the completion criteria as met.
worker_summary_presentboolhost
lycaon.worker_summary_present
True when the worker’s finish summary is present.
worker_artifact_presentboolhost
lycaon.worker_artifact_present
True when the worker produced a required artifact.
files_touchedlist<string>host
lycaon.files_touched
Paths mutated during the run or leg.
summary_lengthinthost
lycaon.summary_length
Character length of the worker summary.

Engine counters

Engine-owned per-session state, exposed read-only; rules mutate them via on_fire, never in CEL.

FactTypeTierObservation
fire_countintcoreThis rule’s declared counter increments this session, less resets.
repeat_countinthost
lycaon.repeat_count
Consecutive identical tool-call repeats.
fruitless_search_runinthost
lycaon.fruitless_search_run
Consecutive search calls that returned nothing.
breaker_countintcoreGrounding circuit-breaker escalation count.
same_code_reject_runinthost
lycaon.same_code_reject_run
Consecutive identical rejects sharing one Code:.
code_reject_totalinthost
lycaon.code_reject_total
Times one tool was rejected with one Code: this session, across every args shape — unlike the run, it survives a reworded call and an interleaved success.

Detector facts

Scores and spans from registered detectors — async, cached, probabilistic. The rule's when: owns every threshold.

FactTypeTierObservation
prompt_injection_scoredoublestandard
content-safety
Injection score from a registered detector (0–1) — the rule’s when: owns the threshold.
jailbreak_scoredoublestandard
content-safety
Jailbreak score from a registered detector (0–1) — the rule’s when: owns the threshold.

Parameterized observation functions

FunctionSignatureTierObservation
fire_count_of(string) -> intcoreNumber of times the named rule’s fire counter has incremented in this session, less resets.
breaker_count_of(string) -> intcoreNumber of times the named rule’s circuit-breaker counter has incremented in this session, less resets.
path_outside_scope(string) -> boolstandard
filesystem
True when the named tool’s path arguments resolve outside its active scope.
source_includes(string) -> boolhost
lycaon.source_includes
True when the request sources include the named provider id.
tool_arg_string(string) -> stringstandard
tool
The named tool argument as a string; empty when absent or not a string.
tool_arg_int(string) -> intstandard
tool
The named tool argument as an integer; zero when absent or not an integer.
tool_arg_bool(string) -> boolstandard
tool
The named tool argument as a boolean; false when absent or not a boolean.
mcp_server_configured_for(string) -> boolstandard
mcp
True when the MCP catalog contains the named server.
mcp_server_enabled_for(string) -> boolstandard
mcp
True when the named server is configured and enabled.
mcp_has_field(string) -> boolstandard
mcp
True when a schema binding projected the named key on the last match.
mcp_field_bool(string) -> boolstandard
mcp
The projected boolean field; false when missing or mistyped.
mcp_field_string(string) -> stringstandard
mcp
The projected string field; empty when missing.
mcp_field_int(string) -> intstandard
mcp
The projected integer field; zero when missing.
host_resource_status_for(string) -> stringhost
lycaon.host_resource_status_for
Observed status of the named host resource; empty when the catalog has no such entry.
host_resource_policy_for(string) -> stringhost
lycaon.host_resource_policy_for
Configured policy for the named host resource; empty when the catalog has no such entry.

Tiercore and standard facts are part of the Open Agent Rules vocabulary and behave the same on any conforming engine. host facts are Painted Wolf’s own observations; they are published under the namespace shown beside them, and a rule that uses one is not portable. Reach for them freely — they are what most Painted Wolf rules are made of.

Stateful rules, on_fire: side-effects, and detector seams are in open-agent-rules.md.

Conformance

The published corpus checks engine conformance. Project fixtures test your own rules with the same evaluator:

{
  "name": "triage-lite-write-blocked",
  "rule":     { "...": "an OAR document validating against oar.schema.json" },
  "input":    { "anchor": "tool.pre_invoke",
                "facts": { "tool": "write", "session_posture": "spec" } },
  "expected": { "decision": "block", "code": "TRIAGE_LITE_WRITE_FORBIDDEN" }
}
pw rules test                          # every rule this project contributes
pw rules test policy/MY_RULE.yaml      # one rule and its fixtures
pw rules test --json

Green means the document validates, its when: compiles against the fact catalogue, and every fixture’s expected decision is the decision the engine reaches.