SDK

Workflows, agents, prompts, rules, and themes ship as extension packs: plain YAML and Markdown loaded by the app. These pages are the authoring guides. Engine contracts — Resolve algebra, prompt assembly, the host threat model — live in the product repository.

A pack is a directory in a git repo; a project override is a file committed under {repo}/.paintedwolf/.

What you can build

SurfaceYou writeGuide
Your first packA workflow, a rule, and an inject — end to end in ten minutesBuild your first pack
Extension packsA directory of units + extension.yaml, installable from a git URLExtension packs
Custom workflowsworkflow.yaml manifests — phases, gates, transitionsCustom workflows
Open Agent RulesPortable guardrail YAML — typed conditions, declared effectsOpen Agent Rules
AgentsPersona YAML with tool profiles as the capability ceilingAgents
PromptsTemplate overlays and lifecycle injectsPrompts
MCP & scannersTyped result bindings; scanner rows stay in SettingsMCP & scanners
ThemesColours for a closed token vocabulary, with a contrast floorThemes
ReferenceDirectory map, CLI, schemas, stabilityReference
Host APILocal REST + SSE for sessions, extensions, MCP, and scannersHost API

Put pw on your PATH before the tutorial — see Projects.

The three layers

The app combines bundled and device-installed packs with reviewed project customization:

LayerWhereScope
Bundledshipped inside the appThe stock packs — always current with the app
Device~/.config/paintedwolf/Installed packs and desired state for every project on this machine
Project{repo}/.paintedwolf/Committed guidance, overrides, and suggested device installs

Packs install on the device. A project can suggest packs and disable eligible shared units; it cannot replace device ownership or strip device enforcement. The property table is on Extension packs → Unit kind properties.

Vocabulary tables on these pages are generated from the v1 engine. Packs declare host compatibility with compatibility.extension_api. See Reference for schemas and the stability contract.

Build your first pack

This tutorial builds Triage Lite: a slash command that investigates, reports, and waits for a human before changing anything. It combines a workflow, an Open Agent Rule, and a prompt inject. Every example below is tested by the engine.

You need the app installed and the pw CLI on your path (setup).

1. The manifest

Make a directory called triage-lite and put this in it. The manifest is what turns a directory into a pack:

extension.yaml
manifest_version: 1
id: acme/triage-lite
name: Triage Lite
version: 1.0.0
description: A minimal pack — one workflow, one rule, one inject template.
compatibility:
  extension_api: "^1.0.0"
dependencies:
  painted-wolf/platform:
    version: "^1.0.0"

compatibility.extension_api is the host-contract gate — a SemVer range the host must satisfy. A mismatched range leaves the pack installed but non-contributing (extension_api_incompatible). dependencies names packs that must be contributing (with their own version ranges); every pack depends on painted-wolf/platform. Prompt wording in stock packs updates with the app and is not a separate version field — see Extension packs.

2. The workflow

A workflow is an ordered list of phases, each declaring what has to be true before the run moves on. Ours has two: investigate, then done.

workflows/triage-lite/workflow.yaml
id: triage-lite
version: 1.0.0
name: Triage Lite
description: Look at the problem, write up what you found, and stop for a human.
trigger: /triage-lite
request:
  question: What problem should this triage investigate?
initial_posture: spec
blueprint:
  id: triage
  file: triage-lite.md
injects:
  - on: phase.entered
    selector:
      workflow: triage-lite
      phase: investigate
    effect: inform
    render: triage-lite-plan
    tier: workflow
phases:
  - id: investigate
    activity_label: Investigating the problem
    blueprint_write: true
    coordinator_surface: plan_research
    surface_template: agents/coordinator-surface-orchestrate.md
    mode_refs: [plan-research]
    complete_when: gates_satisfied
    gates: [human_approval]
    advance:
      when_gate_met: auto
    human_approval:
      blueprint: triage-lite.md
    next: done
  - id: done
    activity_label: Done
    coordinator_surface: await_user
    surface_template: agents/coordinator-surface-plan.md
    mode_refs: [await-user-input]
    terminal: true
    complete_when: orchestration_complete

Three declarations do the work:

  • Every phase names its surface. coordinator_surface, surface_template, and mode_refs are required, including on the terminal phase. The surface is the tool ceiling.
  • The gate is a human. gates: [human_approval] renders an Approve button. Chat never approves. advance.when_gate_met: auto advances as soon as the gate is met.
  • The inject is the pack’s own guidance. The injects: entry fires when the phase is entered and renders a template from this pack’s guidance/ directory.

Every field the loader understands is in the manifest reference.

3. The rule

A workflow says what the run does. A rule says what it may never do. This one blocks writes for as long as the run holds the spec posture:

policy/TRIAGE_LITE_WRITE_FORBIDDEN.yaml
oar: '1.0'
id: TRIAGE_LITE_WRITE_FORBIDDEN
kind: policy
anchor: tool.pre_invoke
selector:
  tool:
    - write
    - edit
requires:
  profiles:
    - session
    - tool
when: session_posture == "spec"
effect: block
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.
  why: Triage runs in the spec posture, where the project tree stays untouched.
  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
x-paintedwolf-emit: rule:triage_lite
x-paintedwolf-message: Triage Lite is a read-only workflow.
x-paintedwolf-scenarios:
  - id: default
    vars:
      tool: write
      path: src/main.go
      profile: coordinator
    expect_contains:
      - 'Rejected:'
      - 'Code: TRIAGE_LITE_WRITE_FORBIDDEN'
      - leave triage

The filename is the rule id and the Code: an agent receives when it fires. when: uses the typed OAR condition language over the closed fact catalogue; unknown facts fail at load. copy: supplies the rejection guidance, and x-paintedwolf-scenarios: tests it.

4. Prove the rule both ways

A rule that only fires is half-tested. Put conformance fixtures in a conformance/ directory beside policy/ — given these facts, expect this decision:

conformance/write-blocked.json
{
  "name": "triage-lite-write-blocked",
  "rule": {
    "oar": "1.0",
    "id": "TRIAGE_LITE_WRITE_FORBIDDEN",
    "kind": "policy",
    "anchor": "tool.pre_invoke",
    "selector": { "tool": ["write", "edit"] },
    "requires": { "profiles": ["session", "tool"] },
    "when": "session_posture == \"spec\"",
    "effect": "block",
    "enforcement": "enforce",
    "on_error": "fail_closed",
    "x-paintedwolf-emit": "rule:triage_lite"
  },
  "input": {
    "anchor": "tool.pre_invoke",
    "facts": { "tool": "write", "session_posture": "spec" }
  },
  "expected": { "decision": "block", "code": "TRIAGE_LITE_WRITE_FORBIDDEN" }
}
conformance/write-allowed-outside-spec.json
{
  "name": "triage-lite-write-allowed-outside-spec",
  "rule": {
    "oar": "1.0",
    "id": "TRIAGE_LITE_WRITE_FORBIDDEN",
    "kind": "policy",
    "anchor": "tool.pre_invoke",
    "selector": { "tool": ["write", "edit"] },
    "requires": { "profiles": ["session", "tool"] },
    "when": "session_posture == \"spec\"",
    "effect": "block",
    "enforcement": "enforce",
    "on_error": "fail_closed",
    "x-paintedwolf-emit": "rule:triage_lite"
  },
  "input": {
    "anchor": "tool.pre_invoke",
    "facts": { "tool": "write", "session_posture": "build" }
  },
  "expected": { "decision": "allow" }
}

Same rule, same tool, different posture — one blocks, one doesn’t. The rule document itself is a portable Open Agent Rules document; the fixture format is the engine’s own runner format.

5. The inject template

Finally, the guidance the coordinator sees when the investigate phase opens. Templates use the bounded pongo2 syntax with a Markdown body:

guidance/triage-lite-plan.md
Triage Lite is running. Report what you find; do not change files.

{% if workflow_phase %}Phase: {{ workflow_phase }}.{% endif %}
Finish with a short write-up, then ask for the human's sign-off.

The render: triage-lite-plan in the manifest resolves to this file: the host adds the directory and the .md for you.

6. Validate and install

From the directory that contains triage-lite/:

pw extensions install path:./triage-lite
pw extensions validate
pw workflow validate triage-lite/workflows/triage-lite/workflow.yaml
pw rules test triage-lite/policy

Expect a contributing pack, zero workflow errors, and one rule with two passing scenarios.

install path: links the folder on this device, so the pack you keep editing is the pack the app loads. Repositories can suggest the pack to teammates through their extension settings.

If a fixture disagrees, rules test exits 1 with scenario_mismatch. The author loop is write, validate, read the code, and fix.

7. See it in the app

Open Settings → Extensions. acme/triage-lite is listed with its three units. Type /triage-lite in a session and the workflow starts; the investigate phase opens with your inject in the coordinator’s context, and the run stops at the approval button.

Iterating: edit the folder, hit Reload from disk, start a new turn. Installed-from-folder packs re-read their source on Reload, so there is no reinstall step while you work.

8. Read the prompt you shipped

A template that fails to parse is an error. A template with a typo’d variable is not — it renders empty and reports nothing, so rendering it is the only way to catch that:

pw prompts render --agent implementer --project . --check

That renders a full persona through the same layers a live session uses, and --check adds the heading contract and the byte budget on top. Drop --agent and pass a template ref instead to render one file, with --var k=v for context.

Where to go next

You wantRead
Surfaces, gates, and the generated manifest field listCustom workflows
The fact catalogue and anchor list your rules select onOpen Agent Rules
Overlay one prompt file and add a phase injectPrompts
Shipping this to a team, and the provide − disabled ⊕ own algebraExtension packs
Diagnostic codes, CLI reference, stability promisesReference

Extension packs

A pack is a directory of units—commands, workflows, rules, guidance, agents, and tool policy—plus extension.yaml. Installed packs compose with the stock catalog under one deterministic rule. The engine contract is extend.md in the product repository.

Resolve: provide − disabled ⊕ own

Every enabled pack contributes every unit id it ships (provide). Your desired state can subtract single units or whole packs (disabled), and when two packs provide the same unit id, it names the winner (own). Everything else fails closed:

RuleDetail
ConflictMultiple providers and no own → the unit is not loaded
OrderNot an input — same desired state and content always produce the same catalog
No remove:A pack can never tombstone another pack’s units
Platformpainted-wolf/platform cannot be disabled or removed

disabled and own work per unit, while pack enablement covers the whole set. A referenced unit that is disabled or unresolved fails the load by name.

Pack layout

my-pack/
  extension.yaml        # manifest (required)
  policy/               # Open Agent Rules, one file per rule
  guidance/             # kick + inject + tool-feedback templates
  workflows/            # <id>/workflow.yaml manifests
  agents/               # persona YAML + prompts/ (incl. _persona-contract.yaml)
  tools/                # catalogs, profiles/, schemas/<name>.yaml
  approvals/            # **/explain.yaml copy + rules/<id>.yaml policy
  playbooks/            # worker leg checklists
  skills/               # <name>/SKILL.md task procedures (device-only units)
  host/bindings/        # anchor bindings (units)
  host/user-notices/    # notice copy, one file per code (units)
  host/detection-packs/ # Sigma packs (units) — device-only, never ownable
  host/credential-slots/ # credential-slot recognition (units) — device-only, never ownable
  shared/               # archetypes/, partials/ — include-target trees; units/ — per-turn prompt units
  mcp_bindings/         # typed MCP result projections (device scope)
  contributions/        # device-scoped commands, menus, keybindings, editor-actions,
                        #   search-sources, operations, themes, configuration,
                        #   mcp-requirements
  profiles/             # named enable/disable/own sets (not units)

Unit kind properties

Packs install on the device. A project’s extensions.yaml may suggest an install and disable eligible units from that catalog:

ClassWhat the project may disable
SharedWorkflows, agents, guidance, and shared templates
AdditiveCannot disable device-resolved policy, approvals, or playbooks
Device-onlyCannot disable these units through project extension settings

own: chooses between colliding unit ids in device settings. Contribution and detection-pack ids include the providing pack, so own: never applies to them. Project skills live separately under .paintedwolf/skills/ or .agents/skills/.

The generated table is authoritative for ids, class, and whether own: is allowed:

DirectoryUnit idProject scopeown:What it ships
policy/policy/<CODE>AdditiveyesOpen Agent Rules — one flat YAML unit per rule, filename equal to its SCREAMING_SNAKE id.
guidance/guidance/<stem>SharedyesKick and inject templates — the markdown the host renders into agent prompts at lifecycle anchors. Nested stems allowed.
workflows/workflows/<id>SharedyesWorkflow manifests (<id>/workflow.yaml), plus workflows/_templates/<name> and workflows/_topologies/<name> for parameterized recipes and fan-out patterns.
agents/agents/<id>SharedyesAgent persona YAML plus their prompt markdown under agents/prompts/<name>.
tools/tools/<path>Device-onlyyesTool catalogs and tool profiles — which tools an agent may use, sticky or deferred.
approvals/approvals/<path>AdditiveyesApproval catalogs — the human checkpoint definitions workflows reference.
playbooks/playbooks/<path>AdditiveyesWorker checklists delegated agents follow.
skills/skills/<name>Device-onlyyesTask procedures in the open Agent Skills format — one folder per skill holding a SKILL.md the agent reads when it is relevant. A skill is guidance rather than capability. The host reads that one file, never runs anything else in the folder, and never acts on an allowed-tools field, so the usual tool policy and approvals still apply. Project skills are not pack units — put them under .paintedwolf/skills/ or .agents/skills/.
host/bindings/host/bindings/<stem>Device-onlyyesAnchor bindings — which rendered guidance a lifecycle anchor emits. Unioned across packs, so a fork can take one over with own.
host/detection-packs/host/detection-packs/<pack-id>:<id>/{pack,fixtures,rules/<slug>}Device-onlyneverSigma detection packs — manifests, rules, and fixtures that may add an approval ask or hold a mediated connection. Never ownable at any scope; a project may only enable or disable a pack the device already has via .paintedwolf/detection-packs.yaml.
host/credential-slots/host/credential-slots/<pack-id>:<name>Device-onlyneverCredential-slot recognition — one flat YAML file naming the environment keys, config keys, flags, commands, and tool arguments where a literal credential may appear. Requires the host.credential_slots capability. Units add to the bundled baseline across packs and are never ownable; a project cannot add or disable them. A match is only a possible-credential observation for rules to act on.
host/user-notices/host/user-notices/<code>Device-onlyyesUser-facing copy for one host notice code, plus the reserved defaults stem. The rest of host/ is operator YAML the host reads by a fixed pack path — pack content, not units.
shared/shared/<path>SharedyesShared partials, prompt archetypes, and diagnostics copy other units include.
mcp_bindings/mcp_bindings/<id>Device-onlyyesTyped projections of MCP tool results into rule facts — a JSON Schema plus field paths, no Go required.
contributions/commands/contributions/commands/<pack-id>:<stem>Device-onlyneverA command your pack adds to the app: a title, the surface it belongs to, an optional condition for when it applies, and exactly one action to run. Crossbar offers it unless the command sets palette: false.
contributions/menus/contributions/menus/<pack-id>:<stem>Device-onlyneverWhere an existing command appears — an application menu or a context menu — plus its order within that slot. A placement adds no authority of its own; the command still decides what running it does.
contributions/keybindings/contributions/keybindings/<pack-id>:<stem>Device-onlyneverThe default chord for a command, per platform, and which surface owns the keystroke. Anyone can rebind it afterwards, and their choice wins.
contributions/editor-actions/contributions/editor-actions/<pack-id>:<stem>Device-onlyneverA prompt-backed action over what the editor has in hand — a caret, a selection, a symbol, a file, or a finding. Pick one host preset and it fixes the tools the turn may use and the files it may write; nothing in the unit widens either.
contributions/themes/contributions/themes/<pack-id>:<stem>Device-onlyneverA colour palette, and optionally an icon treatment, over the app’s own token vocabulary. Themes restyle and never behave — see Themes.
contributions/configuration/contributions/configuration/<pack-id>:<stem>Device-onlyneverOne typed setting your pack exposes, with a required default and whether it can be set per device, per project, or both. It appears in Settings beside the pack.
contributions/mcp-requirements/contributions/mcp-requirements/<pack-id>:<stem>Device-onlyneverAn MCP provider your pack needs and the tools it must offer. The app tells the user what to connect instead of failing partway through a command.
contributions/search-sources/contributions/search-sources/<pack-id>:<stem>Device-onlyneverA prefix-activated search lane backed by one read-only MCP tool. It runs only after the user selects the source or types its prefix.
contributions/operations/contributions/operations/<pack-id>:<stem>Device-onlyneverA typed, presentation-free action that commands or other operations can reference. Cross-pack references require a direct manifest dependency.

Contributions

contributions/ adds to the interface: one YAML document per file. Every vocabulary is closed — an unknown action, slot, or token id is a build error — and pw extensions validate compiles the same graph the app does.

One command owns its title, relevance, enablement, typed input or interaction, action, and result. Crossbar, menus, keybindings, and editor placements are invocation routes for that command; commands appear in Crossbar unless they set palette: false.

contributions/search-sources/ adds an explicit provider-backed source. It runs only after the user selects that source or types its prefix, and selecting a result invokes a command from the same pack.

Direct contribution references stay inside the declaring pack. Cross-pack behavior goes through a typed operation under contributions/operations/ and a direct manifest dependency.

Packs never ship processes. MCP provider bodies, scanner engines, and binaries are forbidden. A pack may refer to them with requires_mcp / requires_scanners, which blocks the pack from contributing until the operator enables the dependency.

The manifest

# extension.yaml
manifest_version: 1
id: acme/triage
name: Acme Triage
version: 1.0.0
compatibility:
  extension_api: "^1.0.0"   # SemVer range over the host extension contract
dependencies:
  painted-wolf/platform:
    version: "^1.0.0"

manifest_version is the YAML document shape. compatibility.extension_api is the host contract — currently 1.0.0. A range that does not include the host leaves the pack installed but non-contributing (extension_api_incompatible).

What compatibility covers

SurfaceHow it moves
Host contract (unit kinds, resolve rules, Binding/Anchor envelope, load shapes)Declared as compatibility.extension_api
Prompt and guidance wording in stock packsShips with the app — no separate prompt version
Units you own:Your body stays effective until you change the fork or clear own
Units you add under new idsIndependent of stock prose updates

Prefer new unit ids. Use own when you need to replace one colliding id.

Approval explanations

When a tool call needs consent, approvals/**/explain.yaml is the copy in that dialog — what is about to change, who it touches, and what happens if it was the wrong call. Required fields: what_changes, who_affected, if_wrong, allow_line, plus at least one scenarios render sample. Copy fields are pongo2 templates.

Approval rules

Packs may ship additive policy under approvals/rules/<rule-id>.yaml. Each file is one document with exactly category (tool, command, mcp, path, host, write_root, host_resource), pattern, and effect (ask or deny only) — any other key fails the load. A write_root pattern must be an absolute directory. Any deny outranks ask. Packs never grant durable reuse — see Approvals.

Detection packs (Sigma)

Detection packs are device-only and never ownable. Import them under Settings → Approvals → Detections, or ship them under host/detection-packs/ in a pack installed on the device. A repository cannot define a rule; it may only turn a pack the device already has on or off ({repo}/.paintedwolf/detection-packs.yaml — id + enabled only).

Authoring — supported Sigma subset, fixtures, and field sets — is in the product repository: detection-packs.md and the rule schema. See Approvals for the user-facing behavior.

Playbook units

A playbooks/ unit is the numbered checklist a delegated worker follows for one kind of leg. Matching and merge rules are in prompt-assembly.md.

Skills

A skills/ unit is one folder holding a SKILL.md. Pack skills are device-only; project skills live under .paintedwolf/skills/ — see Unit kind properties. Which agents may read a skill is a persona field — see Agents.

skills: selects what an agent may read: an exact name list or all. The local decision model ranks available skills against the request and preloads the best fit when it clears the relevance threshold. The agent can also call skills_read with a plain-language need; the host selects one matching skill and returns its instructions. The catalog stays out of the prompt, so a pack can ship many skills without each one costing prompt bytes on every turn.

To open a referenced file, call skills_read again with the resolved skill identifier as need and the relative path as resource. An exact skill identifier also works when local ranking is unavailable. Reading a skill adds guidance, not tools or permissions.

The description is all the model reads, so it does the work. Write one clear sentence naming the work the skill serves and when it applies. Pack bodies use isolated pongo2 templates with no includes; project skill bodies are never templated. Skills use the open Agent Skills format.

Host content

host/bindings/, host/user-notices/, host/detection-packs/, and host/credential-slots/ are resolve units (device-only). Other host/ paths are fixed parts of the stock configuration, not arbitrary files a third-party pack can contribute. Prefer a workflow or rule for customization. The full path map is in extend.md.

Shared trees

shared/archetypes/ and shared/partials/ hold reusable include targets. See Prompts. shared/units/ holds instruction text that renders only on the turns that need it. See Prompt units.

Desired state

Device desired state and its exact lock control installation. Missing device desired state enables the stock packs.

FilePurpose
~/.config/paintedwolf/extensions.yamlDevice packs, enablement, disabled, and own (format: 1)
~/.config/paintedwolf/extensions.lock.yamlExact device resolution (lock_format: 1)
{repo}/.paintedwolf/extensions.yamlCommitted install suggestions and eligible unit disables (format: 1)

A repository can recommend a pack without installing it:

format: 1
suggest:
  - id: acme/triage
    source: https://example.com/acme-triage.git
    version: "^1.0.0"
disabled: []

The user reviews the suggestion and installs it on the device. A suggestion may select a version range or exact ref; project disabled entries add to device disables without changing package identity or ownership.

Profiles

A pack can ship named desired-state recipes under profiles/*.yaml. Applying a profile writes desired state only; installing a pack never auto-applies one.

Installing

pw extensions install https://example.com/acme-triage.git --version 1.0.0
pw extensions validate

Sources: git URLs, file:///abs/dir, or path:relative/dir. Release installs resolve into ~/.config/paintedwolf/extensions/<id>/revisions/…; path: installs link the folder. Their contents may contain directories and regular files only; filesystem links and special files are refused. Stock packs are never cloned. Install only packs you trust — pack YAML is the same trust class as your own configuration.

Settings → Extensions manages the device scope. Context → Extensions inspects the catalog and disables eligible units for the project. The local API is /v1/extensions* — see the Host API. The full CLI is in the Reference.

Changes apply immediately. API mutation requests require the current revision as expected_revision; a stale write returns extension_state_changed (409), and desired state plus lock publish atomically.

Resolve diagnostics

Validation and the Extensions UI report stable codes — branch on the code, never the message. error means something you asked for is not in effect, and those are the codes that fail pw extensions validate:

Show all extension diagnostic codes
CodeSeverityMeaning
extension_api_incompatibleerrorThe pack’s compatibility.extension_api range does not cover this app’s extension contract. The pack stays installed and contributes nothing.
requires_unmeterrorA pack listed under requires is missing or not contributing, so this pack is blocked.
requires_scanners_unmetwarningThe pack declares requires_scanners and the named scanner is not enabled and runnable.
pack_disabledinfoThe pack is set enabled false in desired state; none of its units are provided.
pack_invaliderrorThe pack has an error and contributes nothing. Other packs are unchanged.
pack_integrityerrorInstalled files do not match the lock. The pack contributes nothing.
pack_needs_reloadwarningA linked folder changed on disk. The pack still contributes; Reload from disk records the new files.
unit_disabledinfoThe unit id is listed under disabled in desired state and was omitted from the effective catalog.
conflicterrorMore than one enabled pack provides this unit id and no own entry names a winner — the unit is not loaded (fail closed).
ownedinfoA multi-provider collision was resolved by an own entry; the named pack’s body is effective.
own_unknown_packwarningAn own entry names a pack that is not installed or enabled.
platform_missingerrorThe mandatory painted-wolf/platform pack is not contributing — session boot fails closed.
mcp_binding_invaliderrorAn mcp_bindings unit failed to load — bad schema, missing fields, or an unsupported transform.
credential_slots_invaliderrorA host/credential-slots unit was rejected — an unknown field, a duplicate key, a bad name, or a document over its limits. The catalog view is refused whole rather than quietly dropping the unit.
mcp_binding_duplicate_keyerrorTwo binding units declare the same fact key; bindings fail closed until the collision is resolved.
tool_schema_invaliderrorA tools/schemas/<name> unit failed to load — a bad tool name stem or a schema document the loader rejects.
tool_schema_missingerrorA pack ships this tools/schemas unit but resolve did not load it (disabled, or an unresolved conflict) — without it the tool loses its description and parameter schema, so the drop is surfaced rather than silent.
approval_rule_invaliderrorAn approvals/rules unit was rejected — an unknown field, an extra YAML document, or a value outside the allowed category and effect vocabulary. The catalog view is refused whole rather than quietly dropping the rule.
path_missingerrorA pack installed from a linked folder no longer has a readable source directory — restore the folder and Reload.
skill_name_invaliderrorThe skill’s name breaks the Agent Skills spec (empty, or over 64 characters).
skill_name_mismatcherrorThe name in the frontmatter does not match the folder the SKILL.md sits in.
skill_frontmatter_invaliderrorThe SKILL.md frontmatter could not be parsed, so the skill produced nothing.
skill_field_missingerrorA required frontmatter field is absent — a skill needs a description.
skill_field_invaliderrorA frontmatter field is present but outside its limit, such as a description over 1024 characters.
skill_compatibility_invaliderrorThe skill’s compatibility text is over the 500-character limit.
skill_too_largeerrorThe SKILL.md body exceeds the 64 KiB host limit.
skill_template_invaliderrorA pack skill’s body is not a valid template, so reading it would fail mid-turn. The skill does not load.
skill_catalog_fullwarningThe 128-skill catalog cap was reached and the remaining project skills were set aside. Pack skills always keep their place.
skill_shadowedwarningThis project skill lost its id to an earlier project copy or to a device-resolved skill of the same name.
skill_host_resources_invaliderrorThe skill’s metadata.host_resources list is malformed or exceeds the 32-entry bound.
skill_host_resources_unmetwarningThe skill declares host resources that are not observed or not permitted here, so it stays out of the prompt index.
project_scope_refusederrorA project tried to disable a protected device-resolved unit. The disable is refused by id.
desired_state_rejectederrorThe stored desired state could not be applied, so the app started on the packs it ships with. The refusal is recorded against that file state rather than retried every turn.
own_refusedwarningDesired state named an own winner for a provider-scoped kind whose unit ids already include the providing pack.
detection_pack_invaliderrorA detection pack unit failed to load — a bad manifest, rule, or fixtures document — so that pack does not contribute matches.
detection_pack_foreign_uniterrorA pack contributed a detection unit whose pack id belongs to another provider. Detection content is single-provider; the foreign unit is refused.
detection_pack_id_takenerrorTwo providers declared the same detection pack id; the provenance winner remains active.
project_trust_offwarningThe project’s extension settings are not applying because Extension settings is off for the device or for this project.
detection_rehearsal_failederrorA detection pack’s fixtures did not rehearse cleanly through the approval or connection adapter, so the pack is not trusted to match live events.
project_pack_suggestedinfoA project suggests this pack; installation remains a device choice.
unit_not_regularerrorThe file behind this unit id is not a regular file (symlink, device, FIFO), so its bytes were not published.
member_missingerrorA suite lists a member pack that is not installed, so the suite can never be complete until it is.
member_disabledwarningA suite’s member pack is present but disabled in desired state — the suite reports partial rather than complete.
suite_conflicterrorTwo suites that declare each other under conflicts_with both have enabled members; disable one side.
meta_compatibilityerrorA suite’s compatibility range does not cover this app’s extension contract, so the suite is ignored.
meta_invaliderrorA meta.yaml failed to parse or validate — unknown fields, missing id, or an unresolvable extends parent.
meta_duplicate_iderrorTwo discovered suites claim the same id; neither is trusted until the collision is resolved.
extends_unresolvederrorA suite’s extends parent was never discovered, so the inherited membership cannot be computed.
extends_co_resolveerrorA suite and the parent it extends provide the same unit id — resolving them together leaves the unit in conflict.

Recipes

  1. Additive — ship new unit ids only; install, enable, validate. Stock stays fully effective.
  2. Swap — fork one thin pack plus a profile that disables the stock leaf; every other stock pack still updates with the app.
  3. Surgical own — own a single unit id to your fork; the rest of the stock pack stays effective.

Custom workflows

A workflow is a manifest: an ordered list of phases, each declaring what must be true (gates) before the run advances. Project workflows live at {repo}/.paintedwolf/workflows/<id>/workflow.yaml and surface in the app exactly like the bundled ones. The engine contract is workflows.md in the product repository.

A complete manifest

# {repo}/.paintedwolf/workflows/triage/workflow.yaml
id: triage
version: 1.0.0
name: Triage
description: Fan out read-only scouts, then report — no code changes.
trigger: /triage
request:
  question: What should the scouts look into?
phases:
  - id: investigate
    activity_label: Investigating
    coordinator_surface: orchestrate_plan
    surface_template: agents/coordinator-surface-orchestrate.md
    mode_refs: [plan-research]
    complete_when: gates_satisfied
    gates: [human_approval]
    next: done
  - id: done
    activity_label: Done
    coordinator_surface: await_user
    surface_template: agents/coordinator-surface-plan.md
    mode_refs: [await-user-input]
    terminal: true
    complete_when: orchestration_complete

Every phase in a project workflow — the terminal one included — sets all three surface fields and an activity_label. Overlays are stricter here than the shipped manifests, and this is the most common first validation failure. The top-level request: block is required too: it is the question the app asks when someone starts the workflow without saying what it is for.

A run keeps the id and version it started with. To change what a shipped workflow does, add a new version beside the old one and mark the old one retired: true: new runs take the new version, runs already under way finish on theirs, and the retired manifest stays exactly as it shipped.

Build your first pack walks one manifest end to end.

Phase anatomy

KeyRole
activity_labelThe short status line the app shows while the run is in this phase — required
complete_whenThe gate expression that lets the phase finish
gatesLeaves that must all be met when complete_when: gates_satisfied
next / terminalThe spine — where the run goes, or where it ends
coordinator_surface, surface_template, mode_refsWhich tools and prompt the coordinator gets — explicit and required in project overlays
transitionsOptional off-spine choices, fired by the human or the coordinator

Terminal phases must complete with orchestration_complete. A phase that declares evidence_passed:verify requires a passing host check: the project’s test command when one is set, otherwise a successful check on the current files; see Editing.

Coordinator surfaces

coordinator_surface must name one of the shipped surfaces. The surface names the phase’s tools and its exit — how a deliverable leaves turns on it. The tools are a floor offered on every model call plus the tools a turn loads when the request needs them; the table lists both. Naming a surface that isn’t in this list is a load error, a phase that sets coordinator_surface must also set surface_template, and a phase gated on topology_report_delivered must bind a surface whose exit is report — that is what gives the final report its citation grounding.

SurfaceExitWhat the coordinator is doingTools
implement_routingnoneDeciding what to dispatch next — a lean set with no file mutation, so routing turns stay cheap.recall task worker_cancel answer_decision extend_worker_budget decline_worker_budget update_progress promote_overlay pack_board list_dir web_search fetch_url wait scan_query scan_summary scan_list scan_compare ask_user surface_note request_tools
implement_dispatchnoneWorkers are in flight; the coordinator dispatches, cancels, answers decisions, and records progress.recall task worker_cancel answer_decision extend_worker_budget decline_worker_budget update_progress promote_overlay reject_overlay wait ask_user submit_verdict surface_note
implement_synthesisreportThe wrap-up turn — read and search the tree to write a grounded report; no dispatch, no writes.recall read grep find jq list_dir survey_repo summarize scan_pack web_search fetch_url render_view view_image view_video update_progress scan_query scan_summary scan_list scan_compare
orchestrate_planplanPlanning a run — read, search, and scan the project, then fan out a plan or advance the workflow.recall read grep find jq list_dir survey_repo summarize web_search fetch_url scan_pack scan_query scan_summary scan_list scan_compare fanout_plan workflow_advance ask_user surface_note request_tools
recon_reconcilenoneReconciling scout evidence and choosing a report or one targeted follow-up.recall read grep jq list_dir summarize pack_board workflow_transition ask_user surface_note
implement_overlay_promotenonePromote-eligible write overlays are queued; the coordinator inspects, promotes, or rejects them before starting new work.recall promote_overlay reject_overlay preview_overlay worker_cancel answer_decision extend_worker_budget decline_worker_budget update_progress task pack_board read grep jq list_dir survey_repo summarize skills_read web_search fetch_url http_request process_list process_signal command verify secret_generate secret_list secret_revoke wait scan_query scan_summary scan_list terminal_open terminal_send terminal_read terminal_snapshot terminal_close ask_user surface_note request_tools render_view view_image view_video capture_page measure_page page_open page_act page_snapshot page_close
implement_investigatereportThe coordinator works the tree itself — read, edit, run, and verify — with task available when a leg is worth parallelizing.recall read grep summarize skills_read find list_dir update_progress ask_user surface_note request_tools survey_repo jq stat wc write edit replace_lines restore_version code_rewrite jq_edit diff source_history delete copy move mkdir chmod chown extract_archive command command_output command_stop process_list process_signal verify wait terminal_open terminal_send terminal_read terminal_snapshot terminal_close git_status git_diff git_log git_show git_blame git_ref git_branches git_compare git_commit git_restore git_checkout git_merge git_stash git_stash_list render_view view_image view_video capture_page measure_page page_open page_act page_snapshot page_close secret_generate secret_list secret_revoke http_request web_search fetch_url scan_pack scan_query scan_summary scan_list scan_compare task pack_board worker_cancel answer_decision extend_worker_budget decline_worker_budget
implement_parknoneSibling work is still running; lifecycle tools only while the coordinator waits.recall task pack_board wait worker_cancel answer_decision extend_worker_budget decline_worker_budget update_progress ask_user surface_note
workflow_composeplanComposing a workflow manifest from the conversation and persisting it to the catalog.recall workflow_compose workflow_compose_from_template workflow_catalog_summaries workflow_persist state_start ask_user surface_note
plan_stubplanDrafting the first blueprint stub — write the document, record progress, look things up.recall read write edit replace_lines restore_version update_progress grep skills_read web_search fetch_url secret_generate secret_list secret_revoke ask_user surface_note
plan_researchnoneResearching the blueprint and dispatching research workers, then advancing when the picture is complete.recall read write edit replace_lines restore_version update_progress grep list_dir survey_repo summarize skills_read web_search fetch_url secret_generate secret_list secret_revoke pack_board task worker_cancel extend_worker_budget decline_worker_budget answer_decision wait workflow_advance ask_user surface_note
plan_approveplanFinishing the blueprint for the human’s approval — revise the document, no worker dispatch.recall read write edit replace_lines restore_version update_progress grep skills_read web_search fetch_url secret_generate secret_list secret_revoke workflow_transition surface_note
plan_reviewverdictThe critique loop — revise the blueprint, dispatch the reviewer, and record the verdict that ends the phase.recall read grep list_dir survey_repo summarize skills_read write edit replace_lines restore_version secret_generate secret_list secret_revoke update_progress pack_board task worker_cancel extend_worker_budget decline_worker_budget answer_decision wait submit_verdict ask_user surface_note
survey_executenoneDispatching catalog survey legs from the workflow’s agent list.recall task worker_cancel extend_worker_budget decline_worker_budget answer_decision update_progress pack_board list_dir wait scan_query scan_summary scan_list scan_compare ask_user surface_note
review_adjudicateverdictWeighing survey or options evidence, spawning the reviewer, and recording the verdict.recall read grep list_dir survey_repo summarize scan_query scan_summary scan_list pack_board task worker_cancel extend_worker_budget decline_worker_budget answer_decision wait submit_verdict update_progress ask_user surface_note
decision_adjudicateverdictRecording a decision — the same verdict exit as review, plus the writes that put the chosen option in place.recall read write edit replace_lines restore_version secret_generate secret_list secret_revoke grep list_dir survey_repo summarize skills_read pack_board task worker_cancel extend_worker_budget decline_worker_budget answer_decision wait submit_verdict update_progress ask_user surface_note
await_usernoneA human answer is outstanding; the coordinator may ask, wait, and read, but not act on the project.recall ask_user wait workflow_advance read grep list_dir surface_note
await_hostnoneAnswering the user and reading status while the host runs and advances the phase; the coordinator does not advance it.recall read grep find list_dir scan_query scan_summary scan_list scan_compare pack_board answer_decision ask_user surface_note
plan_executenoneTurning an approved blueprint into changes — writing directly or dispatching legs, including workflow rally departures.recall read write edit replace_lines update_progress grep skills_read web_search fetch_url secret_generate secret_list secret_revoke pack_board task worker_cancel wait extend_worker_budget decline_worker_budget answer_decision delegate_dispatch ask_user surface_note
observe_investigatereportThe read-only counterpart of implement_investigate — read, search, and report on the tree, with git history, scans, and task loadable, but no tool that changes the project.recall read grep summarize survey_repo skills_read find list_dir jq stat wc update_progress ask_user surface_note request_tools git_status git_diff git_log git_show git_blame git_ref git_branches git_compare git_stash_list diff source_history scan_pack scan_query scan_summary scan_list scan_compare web_search task pack_board wait worker_cancel answer_decision extend_worker_budget decline_worker_budget
observe_planplanRead-only planning — read, search, and scan the project, then fan out a plan or advance the workflow. No fetch_url, since a fetched body written to disk would change the project.recall read grep find jq list_dir survey_repo summarize web_search scan_pack scan_query scan_summary scan_list scan_compare fanout_plan workflow_advance ask_user surface_note request_tools
observe_synthesisreportThe read-only wrap-up turn — one report grounded in this chat’s receipts; no dispatch, no writes, no fetch_url.recall read grep find jq list_dir survey_repo summarize scan_pack web_search view_image view_video update_progress scan_query scan_summary scan_list scan_compare
no_folder_allowlistNo folder is open — an allow-list intersected with whatever surface the phase names, so filesystem tools never appear.recall web_search fetch_url wait update_progress answer_decision task worker_cancel extend_worker_budget decline_worker_budget ask_user surface_note

Coordinator task() calls use a structured brief: goal and done_when are required, while known_facts, constraints, and context_refs carry only what a cold-start worker needs. Workers do not receive the parent chat transcript, and an omitted max_tool_loops takes the default for the leg’s scope — 12 rounds for a read survey, 20 for a write overlay.

Posture

Every run carries a session posture: a coordinator approval bucket that decides which tool profile and rule set the coordinator gets. This is a different axis from the approval posture in Settings — see Approvals.

The shipped set is closed — overlays may retune these, but cannot add a fifth:

PostureWhat the coordinator may doTool profile
specCoordinator specifies (plans/docs); no delegation dispatch
buildImplementation + delegation closeout
orchestrateMulti-worker topology sessions
vetReadonly coordinator; security proof required at closeout
initial_posture: spec
phases:
  - id: draft
    next: implement
  - id: implement
    on_enter:
      set_posture: build

A run records the session’s posture before it begins and restores it when it ends. To retune one for a project, add {repo}/.paintedwolf/postures.yaml. Each of the four ids accepts tool_profile, rules, description, and label.

The gate kit

Project workflows compose from the public gate kit. This vocabulary is generated from the engine — it is the exact set the host evaluates:

Show the full workflow gate kit

Static leaves

GateMet when
gates_satisfiedThe phase completes once every leaf in its gates: list is met. The workhorse complete_when for proof-style phases.
orchestration_completeThe run’s orchestration is closed out — required complete_when on every terminal phase.
child_run_completeA child workflow started with invoke_workflow finished successfully.
child_run_failedThe invoked child workflow run ended in failure — pair with a recovery phase.
choice_transition_requiredThe phase completes only through one of its declared choice transitions.
human_approvalThe human granted this phase’s approval request, declared under the phase’s human_approval: block.

Parameterized leaves

Append an argument after the prefix — evidence_passed:verify, hitl_consulted:approve.

PrefixMet when
var_equals:A named run variable equals the expected value.
var_truthy:A named run variable is set and truthy.
user_decision_received:The user answered the named decision request.
user_feedback_received:User feedback arrived for the named request.
user_decision:The user’s recorded decision matches the named option.
hitl_consulted:The human was consulted during the named phase.
evidence_passed:The named evidence check recorded a pass — for example evidence_passed:verify.
phase_is:The run is currently in the named phase.
phase_skipped:The named phase was skipped.
gate_passed:The named gate leaf already passed earlier in the run.
obligation_settled:The phase’s declared host obligation of the named kind has settled.

Bundled-only leaves

These appear in the shipped workflows but are rejected in project overlays (domain_leaf_on_overlay) — they depend on host internals the generic kit doesn't expose:

plan_stub_valid · research_satisfied · topology_stage_complete · delegation_closeout_complete · delivery_gates_passed · closeout_gates_passed · recon_or_board_ready · worker_cycle_ready · topology_report_delivered · fanout_planned · parallel_stages_complete · implement_workflow_ready · options_selection_valid

Choice transitions

A phase may offer named exits off the next: spine. Transitions render as buttons in the app and as a tool on the coordinator surface.

transitions:
  - id: critique
    to: review
    actors: [human, coordinator]
    label: Run critique

Planned fan-out

fanout_plan records a bounded set of worker legs; it does not start them. Dispatch the recorded legs from a following phase with parallel_task and a worker_cycle_ready gate. Both that gate and fanout_planned are bundled-only leaves, so a project overlay cannot gate on them.

Reports

A workflow can end in a downloadable PDF of its own closeout:

controls:
  report:
    enabled: true

Download report appears once the run finishes. The PDF carries the closeout prose, the evidence behind it, any scan findings with the counts they came from, and the verdict a review_loop phase recorded. Verdict members render as your verdict_schema names them, and a claims-typed member renders as a list of statements with the evidence backing each one.

It is assembled from the run’s stored records, so it needs the grounded closeout a topology_report_delivered phase produces — which is why that phase must bind a report-exit surface. That leaf is bundled-only too: a report is something a pack’s workflow ships, not something a project overlay adds.

Manifest reference

Generated from the loader — every field it parses, and nothing it doesn’t:

Show every workflow manifest field (175)

Top level

FieldTypeMeaning
idstringStable workflow id — the catalog key, the own/disabled unit id (workflows/<id>), and what a child invoke_workflow names.
versionstringManifest version, required. Registry keys are id@version, so two versions of a workflow can be loaded side by side.
attachblockWhether this workflow attaches to a session on its own rather than waiting for its trigger.
requestblockThe opening question the workflow asks before it runs, so a trigger typed with no arguments still starts with a request in hand.
extendsstringInherit another manifest in the same directory and override selected keys; lists merge, scalars replace. Resolved before validation.
namestringDisplay name in the workflow catalog and the run header.
retiredboolRemoves this version from the start catalog. Runs already using it keep resolving it and finish on it; ship changed behaviour as a new version beside it.
descriptionstringOne-line summary shown beside the name in the catalog.
triggerstringThe slash command that starts the run (/triage). Routing is by exact command — the host never infers a workflow from prose.
initial_posturestringThe session posture the run starts in; the run restores the session’s previous posture when it ends.
iconstringCatalog icon name for the app’s workflow list.
featuredboolSurfaces the workflow in the app’s featured list rather than only the full catalog.
requires_repoboolThe workflow refuses to start with no folder open — set it when every phase assumes a project tree.
coordinator_profilestringTool profile the coordinator runs under for the whole run, when the workflow needs something other than the posture default.
surface_profilestringA named surface profile supplying per-phase surface defaults. Project overlays must still set the surface fields explicitly.
agents[]blockAgent bindings this run may dispatch, including the tool ceiling each agent receives.
gates[]stringRun-level gate leaves that must be met before the run can close out, in addition to each phase’s own gates.
rules[]stringRule unit paths this workflow activates for the length of the run — validated at load, so a missing rule file fails the manifest.
topologystringThe fan-out topology this run binds to; phases then bind to its stages with bind_topology_stage.
controlsblockRun-wide behavior switches — who advances phases, what happens on pause, stop, and decision rejection.
phases[]blockThe ordered phase list — the run’s spine. Each entry declares what must be true before the run leaves it.
parametersmap[string]blockTyped run parameters with defaults. Values land in run variables under params.<name> and can be read by gates.
blueprintblockDeclares the run’s governing document — the plan the workflow writes and the human approves.
presets[]blockNamed parameter recipes for the same manifest — one workflow, several ways to start it.
injects[]blockWorkflow-tier inform bindings — prompt content rendered at a lifecycle anchor while this run is active.

attach

FieldTypeMeaning
policystringOnly session_create is accepted — the workflow attaches when a session is created (this is how the ambient implement workflow runs). Omit it for ordinary slash-command workflows.

request

FieldTypeMeaning
cadencestringWhen the question is asked — once at the start of the run (default), or each_turn for a workflow that takes a fresh request every turn.
questionstringThe prompt shown to the user. Required whenever a request: block is present.
defaultstringThe value used when the user answers with nothing. Omit it to require an answer.

agents

FieldTypeMeaning
idstringAgent id available to the workflow; any id not listed here is rejected at dispatch.
toolsstringTool profile or explicit tool ceiling applied when this workflow dispatches the agent.
spawnboolWhether this agent may dispatch its own child agents while working for the workflow.

controls

FieldTypeMeaning
phase_advancestringhost hands phase advance to the host instead of the coordinator; omit it to let the coordinator advance with workflow_advance.
default_execution_modestringDefault coordinator execution mode for the run: investigate (work the tree directly), orchestrate (dispatch), or state_derived (follow the run state).
on_decision_rejectblockWhat the run does when the human rejects a decision it asked for.
on_pauseblockWhat happens to work in flight when the run pauses.
on_stopblockWhat a stop tears down.
content_reviewblockRun-wide human review of edits — which tools and paths must be shown to the human before they apply.
reportblockEnd-of-run report behavior.

controls.on_decision_reject

FieldTypeMeaning
pauseboolPause the run on rejection so the human can redirect it.
cancelboolCancel the run outright on rejection.

controls.on_pause

FieldTypeMeaning
hold_pendingboolHold queued work instead of discarding it, so a resume picks up where it stopped.
cancel_runningboolCancel workers that are already running rather than letting them finish.

controls.on_stop

FieldTypeMeaning
cancel_workersboolCancel every worker the run dispatched.
abort_delegationboolAbort the delegation batch rather than letting queued legs drain.
session_abortboolAbort the session’s current turn as well as the run.

controls.content_review

FieldTypeMeaning
tools[]stringTool names whose edits require review (typically the write tools).
paths[]stringPath globs whose edits require review.

controls.report

FieldTypeMeaning
enabledboolProduce the run’s closing report when the run completes.
findings_labelstringHeading this run’s report uses for its findings section, when the default wording does not fit the work.
briefblockThe report’s rating — the question a reader asks, the facts each finding that needs attention must answer, and the levels those answers decide. The host rates every such finding and the worst sets the level; omit it and the report states completeness alone.
retriesintHow many times the coordinator is sent back to repair a refused closing report — a document defect or a citation that fails grounding — before retrying stops. Must be positive. Applies to the phase that delivers the run’s report unless that phase sets controls.retries; omit it for the system default.

controls.report.brief

FieldTypeMeaning
questionstringThe question the rating answers on page 1, such as “How serious is it?”. Required.
dimensions[]blockThe facts every rated finding answers. At least one is required; ids must be unique.
levels[]blockThe rating scale, most severe first — at least two levels. Every level but the last needs conditions; the last has none and is what a run with no match states.
basis[]stringDimension ids whose value phrases explain, on the rating’s basis line, why the deciding finding set the level.

controls.report.brief.dimensions

FieldTypeMeaning
idstringAnswer key the agent fills and level conditions name.
labelstringColumn heading for this dimension in the report’s table of rated findings.
questionstringThe factual question the agent answers for each finding.
allow_unknownboolAccept unknown as an answer. The host tries an unknown as every declared value, so an open question yields a range of levels rather than a guess.
values[]blockThe declared answers — at least two, with unique ids. unknown is reserved; declare allow_unknown instead.

controls.report.brief.dimensions.values

FieldTypeMeaning
idstringAnswer id the agent submits and level conditions list.
labelstringHow the answer reads in the rated-findings table and in the agent’s prompt. Required.
phrasestringWhat this answer means to a reader, used on the basis line when the dimension is listed in basis.

controls.report.brief.levels

FieldTypeMeaning
labelstringThe level’s name on the scale, unique within the brief. Required.
answerstringThe sentence the report opens with when this level is the rating, such as “High risk”. Defaults to the label.
meansstringWhat the level asks of the reader, shown under the rating. Required.
tonestringColour for the level — critical, high, medium, low, good, or neutral. It orders nothing; position in the list is severity.
when[]map[string][]stringAlternative conditions; a finding meets the level when every dimension a condition names holds one of the listed answers.

phases

FieldTypeMeaning
idstringPhase id, unique within the manifest; next:, transitions.to, and every gate that names a phase use it.
activity_labelstringShort user-facing label shown while this phase is active.
complete_whenstringThe condition that lets the phase finish — gates_satisfied for proof-style phases, orchestration_complete on terminal phases.
entry_whenstringOptional condition the run must satisfy to enter the phase at all; an unmet entry condition skips it. Must be a known gate leaf.
invoke_workflowblockRun another workflow as a child from this phase. The phase must use complete_when gates_satisfied with child_run_complete in its gates.
invoke_triggerstringWhen the child starts — phase_enter (default) or coordinator_tool, letting the coordinator decide the moment.
nextstringThe phase the run advances to on the spine. Omit it only on a terminal phase.
child_nextstringThe phase to advance to instead of next when this workflow is itself running as a child, so an invoked run can rejoin its parent early.
child_complete_whenstringThe complete_when to use when this workflow runs as a child — a phase can finish on different terms as a subroutine than it does standalone.
child_gates[]stringThe gates that replace gates: when child_complete_when applies. The two are set together.
on_enterblockEffects applied once as the run enters the phase.
on_reenterblockEffects applied when the run comes back to a phase it already visited — a returning worker, a loop, a rejected review.
controlsblockPhase-scoped overrides of the run-wide controls: block.
gates[]stringThe leaves that must all be met when complete_when is gates_satisfied. Compose them from the gate kit above.
bind_topology_stagestringBind the phase to a named stage of the manifest’s topology, so the stage’s fan-out belongs to this phase.
bind_parallel_group[]stringParallel group names whose legs this phase owns — how one phase waits on several concurrent legs.
parallel_taskblockConcurrency ceiling for workers dispatched from this phase.
fanoutblockPolicy for the fanout_plan tool on this phase.
touchblockThe paths this phase’s delegated work is expected to touch.
terminalboolMarks the end of the spine. Terminal phases must complete with orchestration_complete.
coordinator_surfacestringThe capability surface the coordinator holds during this phase — one of the shipped surface ids. Required, with surface_template, in project overlays.
surface_templatestringThe coordinator prompt file for the surface. Required whenever coordinator_surface is set.
mode_refs[]stringAdditional coordinator mode templates layered into the phase prompt.
advanceblockWho advances once the phase’s gate is met.
loopblockWhere the phase goes when its loop iterates.
intake[]stringIntake keys collected from the human in this phase; each answer lands in run variables under intake.<key> and can gate the phase.
human_approvalblockDeclares this phase’s approval request — the human checkpoint the app renders as a button.
review_loopblockThe reviewer-verdict convention — dispatch a reviewer, record a structured verdict as evidence, iterate up to a cap.
depth_paramstringNames a depth-typed parameter that scales this phase — none skips it, light and thorough widen the fan-out.
transitions[]blockNamed exits off the next: spine, rendered as buttons and offered to the coordinator as a tool.
blueprint_writeboolDeclares that this phase’s coordinator authors or revises the run’s Blueprint. Composition checks that the bound surface can actually write one.
explainblockA progress note for a phase held by host obligations or a topology binding; presentation only, with no effect on gates or model history.

phases.invoke_workflow

FieldTypeMeaning
workflow_idstringThe child workflow’s id.
versionstringThe child manifest version to run — pinned, so a new version of the child cannot change this run silently.
blueprintstringWhat governs the child run — none, inherit to bind it to the parent’s approved Blueprint, or own to give the child one of its own.

phases.on_enter

FieldTypeMeaning
set_posturestringMove the session posture on entry — this is what keeps a planning phase from starting to edit files.
set_execution_modestringForce the coordinator’s execution mode for this phase instead of deriving it from run state.
prompt_coordinatorboolPrompt the coordinator immediately on entry rather than waiting for the next user turn.
request_user_feedbackblockAsk the human a question as the phase opens; the answer lands in run variables and can gate the phase. An open-ended prompt on the first phase takes the slash remainder as the answer.
obligations[]blockHost work the phase starts and waits on until the matching obligation gate settles.

phases.on_enter.request_user_feedback

FieldTypeMeaning
promptstringThe question text shown to the human. Required.
response_typestringtext (default), single_choice, or multi_choice. Choice types require at least two options.
options[]stringThe offered choices — only valid with a choice response type.
allow_otherboolLet the human answer outside the offered options.

phases.on_enter.obligations

FieldTypeMeaning
kindstringRegistered obligation kind.

phases.on_reenter

FieldTypeMeaning
inject_kickstringGuidance template rendered to the coordinator on re-entry.
reenter_legstringThe delegation leg to resume on re-entry.

phases.controls

FieldTypeMeaning
content_reviewblockHuman review of edits for this phase only.
closeoutstringSet to gated to hold completion while the phase’s gates remain open.
retriesintHow many times this phase’s coordinator is sent back to repair a refused closeout — a report document defect or a citation that fails grounding — before retrying stops. Must be positive; omit it for the system default.

phases.controls.content_review

FieldTypeMeaning
tools[]stringTool names whose edits require review during this phase.
paths[]stringPath globs whose edits require review during this phase.

phases.parallel_task

FieldTypeMeaning
max_workersintMaximum workers in flight at once. Must be greater than zero when the block is present.
max_read_workersintSub-ceiling for read-only workers.
max_write_workersintSub-ceiling for workers that may write — the knob that limits overlay churn.

phases.fanout

FieldTypeMeaning
require_threat_modelboolWhen true, fanout_plan is rejected unless threat_model is set.
max_attemptsintHow many times one leg may be retried before the fan-out settles it unfinished. 1 to 3; requires fanout_planned.

phases.touch

FieldTypeMeaning
paths[]stringPath globs handed to workers as their focus paths for this phase.

phases.advance

FieldTypeMeaning
when_gate_metstringauto advances the moment the gate passes; coordinator leaves the leave-turn to the coordinator.

phases.loop

FieldTypeMeaning
exitstringnext leaves down the spine; self re-enters this phase — the shape behind review and verify loops.

phases.human_approval

FieldTypeMeaning
blueprintstringThe blueprint id being approved; the host records the approved content hash so a later edit invalidates the approval.
readinessstringA condition that must hold before the approval is even offered, so the human is not asked to approve half-finished work.

phases.review_loop

FieldTypeMeaning
coverage_reviewers[]stringA subset of required_agents that assess coverage independently and return a structured coverage assessment. Requires a coverage_review verdict member and a reconciles_phase that carries candidate coverage; the verdict is admitted only with a current assessment from each one.
followup_attemptsintFocused investigations each open claim may get after a non-terminal verdict, each followed by fresh review. Only completed investigations count; a question that runs out needs an explicit terminal outcome.
reconciles_phasestringThe earlier phase this review reconciles against. Must name a different phase.
require_inventory_accountedboolRefuses a terminal verdict until the bound scans settle and every scanner group is linked by a claim or selected by a set-aside. A refused attempt does not use up a review round.
include_scan_inventoryboolPut the run’s scan inventory in front of the reviewer.
evidence_keystringThe evidence key the verdict is recorded under; satisfy the phase with evidence_passed:<key>. Required, and the manifest must reference it in a gate.
iteration_capintMaximum review rounds before the loop stops asking (default 3).
verdict_schemamap[string]stringField name to type for the structured verdict the reviewer submits.
required_agents[]stringAgent ids the review loop must be able to dispatch; the host refuses the phase if any are missing.
if_spawnable[]stringReviewer ids required only when the phase’s initial spawn roster admits them.
claim_statusesmap[string]stringMaps each status word this review may give a claim to held, failed, or open — the host reads the class, never the word. Required when verdict_schema carries a claims member and rejected otherwise.
brief_labelstringNames this review for a reader who never saw the workflow, such as “Second opinion”, and lists it as a check on the report’s first page. Only valid when the verdict carries claims.

phases.transitions

FieldTypeMeaning
idstringStable wire key for the transition — pick it like an API name.
tostringDestination phase id. Self-edges are rejected.
actors[]stringWho may fire it — human, coordinator, or both. Required.
labelstringButton text shown to the human. Required.
whenstringOptional readiness condition; while it is false the transition is not armed. Omit it to keep the exit always available on this phase.

phases.explain

FieldTypeMeaning
summarystringOne-line title for the phase’s progress note, at most 72 characters.
bodystringExplanation of the host-held work, at most 480 characters.

parameters

FieldTypeMeaning
typestringThe parameter type. depth is the typed one the host understands (none / light / thorough) and the only type depth_param accepts.
defaultstringThe value used when neither a preset nor the start request supplies one.

blueprint

FieldTypeMeaning
idstringBlueprint id referenced by human_approval.blueprint and by depth-parameter stamping. Required when the block is present.
filestringBlueprint filename under the project’s .paintedwolf/blueprints/ convention directory.
frontmatter[]stringFrontmatter keys the document must carry, so downstream phases can rely on them.

presets

FieldTypeMeaning
idstringPreset id; recorded in run variables as workflow.preset_id.
namestringDisplay name in the catalog. Defaults to the id.
descriptionstringOne-line summary of what this preset changes.
triggerstringAn optional slash command that starts the workflow with this preset already applied.
paramsmap[string]stringParameter values this preset overlays on the manifest defaults. Required — a preset that changes nothing is rejected.

injects

FieldTypeMeaning
onstringThe anchor id that fires the inject, from the same catalog rules bind to.
selectorblockNarrows when the inject fires. Clauses AND together; an omitted clause matches everything.
effectstringinform — a workflow inject renders prompt content. Blocking is the rules plane, not this one.
whenstringOptional CEL condition over the same fact catalogue rules use, evaluated when the anchor fires.
renderstringThe guidance template to render, by template ref — a guidance/ file your pack ships.
tierstringMust be workflow (the default) — a manifest can only author workflow-tier bindings. Standalone bindings ship as host/bindings/ units.
dedupyamlRaw dedup policy for this inject, following the anchor catalog’s dedup grammar — omit it to inherit the anchor’s own policy.

injects.selector

FieldTypeMeaning
surfacestringFire only for this anchor surface — coordinator, worker, or phase.
phasestringFire only in the named phase.
workflowstringFire only for the named workflow id.
toolstringFire only for a single tool name.
tools[]stringFire for any tool in the list.
profiles[]stringFire only for agents running under these tool profiles.
surfaces[]stringFire only on these coordinator surfaces.
session_posture[]stringFire only in these session postures.

Shapes

The full block shapes — blueprint, human_approval, review_loop, invoke_workflow, parallel_task, and run variables — are in the product repository’s workflows.md. The shape the tutorial uses is the human checkpoint:

- id: approve
  coordinator_surface: plan_approve
  surface_template: agents/coordinator-surface-plan.md
  mode_refs: [plan-stub]
  complete_when: gates_satisfied
  gates: [human_approval]
  advance:
    when_gate_met: auto
  human_approval:
    blueprint: plan
    readiness: plan_stub_valid
  next: build

Chat never approves — Approve is a host action. advance.when_gate_met: auto is required on a surface that has no workflow_advance tool.

Validate before you run

pw workflow validate              # project overlays + bundled base
pw workflow validate --json       # machine-readable diagnostics

Green means the manifest loads, every phase is leaveable with the tools and gates the host evaluates, and every catalog reference resolves. It cannot prove model behavior. Diagnostics carry stable codes — the generated table is in the Reference.

What overlays can’t do

  • Bundled-only gates — domain leaves are rejected with domain_leaf_on_overlay.
  • Implicit surfaces — overlays must set coordinator_surface, surface_template, and mode_refs per phase.
  • Routing on prose — triggers are explicit slash commands; Approve is a host action button, never a chat phrase.

Phase-entry guidance is an injects: entry — see Prompts.

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

A tool your fix: or instead: names in backticks must be one every agent the rule can reach actually holds, or the pack is held out — a blocked agent needs an exit it can execute. The anchor and selector.tool decide that audience; x-paintedwolf-audience: [<tool profile>] narrows it when your when: is stricter than they express.

Write instead: as the one call to make next. When an agent keeps hitting the same rule, the host escalates and carries that line verbatim in place of your fix:, so an instead: that only forbids something leaves the agent with no move.

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 (74)

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
coordinator.report.document
Coordinator report document
Run report rejected for document fields it still owes (claims without findings, scanner groups without an assessment or set-aside, invalid fields).coordinator inform
worker.citation.grounding
Worker citation grounding
Worker completion JSON fails evidence grounding.worker inform
tool.rejected
Tool rejection
An intrinsic typed tool failure before its rejection feedback is delivered. This event does not claim to undo completed tool effects.coordinator block

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_reenter.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
scan.delta
Scan delta observed
An automatic delta scan of files the session’s project changed finished with findings introduced or fixed.coordinator inform
worker.budget.raised
Worker budget raised
The coordinator raised a running worker’s tool-round ceiling.worker inform
worker.budget.declined
Worker budget declined
The coordinator declined a running worker’s open request for more tool rounds.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_canceled
Root detach canceled work
DELETE root?force=true stopped workers, overlays, or busy sessions.coordinator inform
worker.closeout
Worker closeout
Worker child finish turn offers only complete_leg.worker inform
worker.iterations.low
Worker iterations low
Any worker is told once per ceiling when its runway reaches the host threshold.worker inform
worker.cancel.closeout
Worker cancel closeout
Canceled worker leg finish turn offers only complete_leg.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
A wait() deadline expires.coordinator inform
scan.finished
Scan finished
Security scan reaches terminal while an agent waits on scan_done.coordinator inform
process.finished
Process finished
Command/verify process or held call reaches terminal while an agent waits on process_done.coordinator inform
process.refused
Process refused
The kernel refused a running command or verify job an operation no earlier result showed; the job keeps running.coordinator inform
worker.budget.requested
Worker budget requested
A running worker asked its coordinator for more tool rounds with request_budget.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
turn.survey.streak
Turn survey streak
Consecutive tool batches this turn ran only read-only tools; the host names the streak so the model acts on what it has or states the blocker.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.skill_procedure
Preloaded skill procedure
The selected procedure for the current turn.agent inform
inject.skill_pointer
Skill pointer
One line naming the skill the turn’s first loadable tool call fitted, when the host did not read it.agent inform
inject.tool_procedures
Available tool procedures
Operating guidance derived from the exact tools offered in this model request.agent inform
inject.command_jobs
Live handles inject
Host-authoritative ledger of live command jobs and held tool calls.agent inform
inject.source_changes
Source change brief inject
Ledger-recorded files other actors changed between the previous turn boundary and this one, actor-classified; working-set paths itemized, the rest counted. Observed git ref movements (checkout, commit, pull …) lead the block so file rows read against their cause.coordinator 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 mode and focus 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 call is dispatched; scope with selector.tool.coordinator block
coordinator.pre_invoke
Coordinator pre-invoke
Coordinator observations before a tool call. Prefer tool.pre_invoke with selector.profile when equivalent.coordinator block
session.pre_invoke
Session pre-invoke policy
Before a prompt-loop tool call, with its session posture and tool arguments observable.coordinator block

tool_handler

AnchorFires whenSurface
tool.handler
Tool handler
After argument validation and before dispatch to the tool implementation.coordinator block

post_tool

AnchorFires whenSurface
tool.post_invoke
Tool post-invoke
Complete tool results, including failures, before logging and delivery.coordinator block
credential.assignment
Credential assignment
One literal credential assignment observed on a completed tool call. Rules decide quality policy from measured material and provenance.coordinator block transform

post_turn

AnchorFires whenSurface
coordinator.post_turn
Coordinator post-turn
An assembled agent turn after provisional checks and before turn completion. Implements core agent.post_turn.agent block inform
coordinator.closeout_check
Coordinator closeout check
A provisional closeout claim checked against workflow and evidence observations; a block requests repair before turn completion.coordinator block

finalize

AnchorFires whenSurface
worker.finalize
Worker finalize
A complete worker result before persistence and delivery to its parent. Implements core agent.finalize.worker block inform
worker.report_check
Worker report check
A provisional worker report or completion claim checked against observed evidence; a block requests repair before final delivery.worker block

input

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

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
transformRedact, replace, or annotate content. Delivered only at tool.post_invoke, content.output, content.tool_result, and credential.assignment; a transform at any other anchor fails the load, because rewriting model input would blur which role said what

The app lists effective rules read-only under Settings → Approvals. Branch on Code: — never on message text.

Conditions are typed expressions over facts

when: uses the closed, typed OAR condition language. Identifiers are registered facts — a typo’d fact name fails at load:

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

Standard-profile facts are bare names; host-specific facts are namespaced paintedwolf.*. 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 declares the live condition 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
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_argsmapstandard
tool
The current call’s arguments, as a map.
last_assistantstringhost
paintedwolf.last_assistant
The last assistant message text.
turn_toolslist<string>host
paintedwolf.turn_tools
Tool names invoked so far this turn.
session_posturestringstandard
session
The active session posture id.
surfacestringhost
paintedwolf.surface
The active coordinator capability surface.
profilestringhost
paintedwolf.profile
The calling agent’s tool profile id.
phasestringhost
paintedwolf.phase
The active workflow phase id.
workers_idleboolhost
paintedwolf.workers_idle
True when no delegated workers are running.
worker_spawn_blockedboolhost
paintedwolf.worker_spawn_blocked
True at a task() call when workers in flight already fill the total cap, or the read or write cap for the requested scope.
active_worker_countinthost
paintedwolf.active_worker_count
Number of delegated workers currently running.
pending_overlay_promoteboolhost
paintedwolf.pending_overlay_promote
True when a promote-eligible write overlay awaits landing.
overlay_statestringhost
paintedwolf.overlay_state
The current write overlay’s lifecycle state.
surface_may_finishboolhost
paintedwolf.surface_may_finish
True when the active surface allows finishing the run.
is_host_cycle_turnboolhost
paintedwolf.is_host_cycle_turn
True when this turn was initiated by the host loop, not the user.
batch_phasestringhost
paintedwolf.batch_phase
The coordinator batch phase for this cycle.
batch_closedboolhost
paintedwolf.batch_closed
True when the current batch is already closed.
progress_open_itemsinthost
paintedwolf.progress_open_items
Count of open rows on the progress checklist.
progress_has_open_stepsboolhost
paintedwolf.progress_has_open_steps
True when the root progress checklist has open - [ ] rows.
has_completion_reportboolhost
paintedwolf.has_completion_report
True when the last assistant text parses as a coordinator completion report.
task_envelope_echoboolhost
paintedwolf.task_envelope_echo
True when the last assistant text echoes a worker task envelope.
closeout_surfaceboolhost
paintedwolf.closeout_surface
True when the active surface delivers typed JSON closeout.
progress_reconcile_neededboolhost
paintedwolf.progress_reconcile_needed
True when a synthesis progress update is not reconcile-only.
verify_requiredboolhost
paintedwolf.verify_required
True when verify is required before close for this session.
verifier_passboolhost
paintedwolf.verifier_pass
True when host verify() has already passed.
synthesis_wrapup_tool_forbiddenboolhost
paintedwolf.synthesis_wrapup_tool_forbidden
True when the tool is forbidden on the synthesis wrap-up surface.
progress_closure_armedboolhost
paintedwolf.progress_closure_armed
True when the progress-closure latch is armed for this root.
progress_gated_toolboolhost
paintedwolf.progress_gated_tool
True when the tool is progress-gated.
progress_missingboolhost
paintedwolf.progress_missing
True when no usable progress checklist is present.
review_loop_activeboolhost
paintedwolf.review_loop_active
True when the review loop is active for the session.
path_is_worker_branchboolhost
paintedwolf.path_is_worker_branch
True when the tool path resolves under a worker sandbox branch.
verify_has_commandboolhost
paintedwolf.verify_has_command
True when the verify() call already carries a command.
verify_declaredboolhost
paintedwolf.verify_declared
True when the project declares a verify command.
progress_closed_beyond_baselineboolhost
paintedwolf.progress_closed_beyond_baseline
True when closed progress rows exceed the armed baseline.
progress_reconciled_since_armboolhost
paintedwolf.progress_reconciled_since_arm
True when the progress checklist changed after the closure baseline armed.
pending_user_inputboolhost
paintedwolf.pending_user_input
True when a user question is pending an answer.
stub_validboolhost
paintedwolf.stub_valid
True when the bound plan stub satisfies the required headings.
tool_allowed_for_profileboolhost
paintedwolf.tool_allowed_for_profile
True when the profile’s capability ceiling allows this tool.
habit_redirect_matchstringhost
paintedwolf.habit_redirect_match
The native-tool redirect id matched for a bash habit, or empty.
write_rootslist<string>host
paintedwolf.write_roots
The active write-scope root paths.
pattern_parse_okboolhost
paintedwolf.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.
arg_validation_reasonstringstandard
tool
Primary argument validation reason for the current call, when set.
arg_validation_fieldstringstandard
tool
Argument field named by the validation reason, when set.
action_host_resourceslist<string>host
paintedwolf.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
paintedwolf.action_host_resource_denials
Host resource ids the action was refused for.
confine_appliedboolhost
paintedwolf.confine_applied
True when a confinement profile was applied to the action.
network_modestringhost
paintedwolf.network_mode
Network mode from the applied confinement, when set.
denial_subjectstringhost
paintedwolf.denial_subject
Subject named by a confinement denial, when set.
confine_signalslist<string>host
paintedwolf.confine_signals
Closed-set confinement signal tokens for the action.
failed_stageslist<string>host
paintedwolf.failed_stages
Command lines of the finished invocation’s failed stages; empty when every stage succeeded or no process was spawned.
process_runningboolhost
paintedwolf.process_running
True when the command is still running at the time of the host’s confinement observation.
sandbox_refusalslist<string>host
paintedwolf.sandbox_refusals
Operations the kernel reported refusing for the command.
refused_write_pathslist<string>host
paintedwolf.refused_write_paths
Paths the kernel reported refusing for writing; empty when none.
refused_write_grantslist<string>host
paintedwolf.refused_write_grants
The write_root capability grants that would open the refused write paths a reviewed grant can open, with grants already covered by a parent directory dropped; empty when none.
refused_read_pathslist<string>host
paintedwolf.refused_read_paths
Paths the kernel reported refusing for reading; empty when none.
refused_read_grantslist<string>host
paintedwolf.refused_read_grants
The read_path capability grants that would open the refused read paths a reviewed grant can open, with grants already covered by a parent directory dropped; empty when none.
refused_socket_pathslist<string>host
paintedwolf.refused_socket_paths
Local socket paths the kernel refused to connect to.
refused_connect_portslist<string>host
paintedwolf.refused_connect_ports
Ports the kernel refused for outbound connections.
refused_listen_portslist<string>host
paintedwolf.refused_listen_ports
Ports the kernel refused for local listeners.
refused_signalslist<string>host
paintedwolf.refused_signals
Process signals the kernel refused to deliver.
unsandboxed_refusalslist<string>host
paintedwolf.unsandboxed_refusals
Refused operations whose recovery requires running outside the command sandbox.
mode_bitsstringhost
paintedwolf.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
paintedwolf.posture_unresolved
True when the session posture is not yet resolved.
high_risk_toolboolhost
paintedwolf.high_risk_tool
True when the tool is classed high-risk.
tool_is_stateboolhost
paintedwolf.tool_is_state
True when the tool is a workflow state tool.
tool_is_delegationboolhost
paintedwolf.tool_is_delegation
True when the tool is a delegation tool.
tool_is_taskboolhost
paintedwolf.tool_is_task
True when the tool is task().
tool_payload_chunkableboolhost
paintedwolf.tool_payload_chunkable
True when the tool accepts a free-form payload that can span several calls.
tool_is_handoffboolhost
paintedwolf.tool_is_handoff
True when the tool is a handoff tool.
pack_runner_taskboolhost
paintedwolf.pack_runner_task
True when the task targets a pack-runner agent.
agent_is_plan_writerboolhost
paintedwolf.agent_is_plan_writer
True when the task agent is the plan writer.
disallowed_agentboolhost
paintedwolf.disallowed_agent
True when the requested agent is not allowed on this surface or posture.
plan_awaiting_approvalboolhost
paintedwolf.plan_awaiting_approval
True when the plan is waiting on human approval.
worker_artifact_measuredboolhost
paintedwolf.worker_artifact_measured
True when the host could determine whether an implementer produced an artifact; false when the workspace probe failed, so a missing artifact is not proven.
deferred_unactivatedinthost
paintedwolf.deferred_unactivated
Count of deferred tools the session has not activated.
worker_attempted_mutationboolhost
paintedwolf.worker_attempted_mutation
True when an implementer child invoked a file-mutation tool.
batch_ready_ignoring_progressboolhost
paintedwolf.batch_ready_ignoring_progress
True when wrap-up gates hold except open progress steps.
synthesis_delay_countinthost
paintedwolf.synthesis_delay_count
Per-cycle count of open-progress closeout delays.
review_verdict_gate_openboolhost
paintedwolf.review_verdict_gate_open
True when the review verdict gate is still open.
verdict_delay_countinthost
paintedwolf.verdict_delay_count
Per-cycle count of review-verdict delays.
closeout_gates_openboolhost
paintedwolf.closeout_gates_open
True when a gated closeout has unsatisfied completion gates and no human response pending.
closeout_gate_open_leavesstringhost
paintedwolf.closeout_gate_open_leaves
Unsatisfied closeout gate leaves.
closeout_gate_delay_countinthost
paintedwolf.closeout_gate_delay_count
Per-cycle count of gated-closeout delays.
workflow_report_phase_pendingboolhost
paintedwolf.workflow_report_phase_pending
True when a completion report is due before the run reaches its report phase.
phase_obligation_pendingboolhost
paintedwolf.phase_obligation_pending
True while the active phase waits on host obligations.
phase_obligation_kindsstringhost
paintedwolf.phase_obligation_kinds
Comma-separated kinds of the pending obligations.
scope_modestringhost
paintedwolf.scope_mode
The task() scope mode — read or write.
profile_mutation_capableboolhost
paintedwolf.profile_mutation_capable
True when the task agent’s tool profile can edit files, run commands, or create commits.
base_overlay_idstringhost
paintedwolf.base_overlay_id
The stacked base overlay id from task scope, or empty.
base_overlay_resolvesboolhost
paintedwolf.base_overlay_resolves
True when base_overlay_id names a live write overlay.
base_overlay_checkedboolhost
paintedwolf.base_overlay_checked
True once the host has validated this task() call’s stacked base overlay, so base_overlay_resolves and base_overlay_pending carry a verdict rather than their default.
base_overlay_pendingboolhost
paintedwolf.base_overlay_pending
True when the base overlay is still pending and stackable.
active_read_countinthost
paintedwolf.active_read_count
Number of read workers currently running.
active_write_countinthost
paintedwolf.active_write_count
Number of write workers currently running.
max_workersinthost
paintedwolf.max_workers
The configured worker cap.
max_read_workersinthost
paintedwolf.max_read_workers
The configured read-worker cap.
max_write_workersinthost
paintedwolf.max_write_workers
The configured write-worker cap.
citation_unverifiableboolhost
paintedwolf.citation_unverifiable
True when a typed citation is unverifiable against the ledger.
scout_survey_evidence_presentboolhost
paintedwolf.scout_survey_evidence_present
True when a read scout ran a successful survey tool.
surface_claim_ungroundedboolhost
paintedwolf.surface_claim_ungrounded
True when a UI-complete claim lacks surface snapshot evidence.
page_measure_ungroundedboolhost
paintedwolf.page_measure_ungrounded
True when a layout claim lacks page geometry evidence.
agent_is_scoutboolhost
paintedwolf.agent_is_scout
True when the worker agent is a read scout.
agent_is_implementerboolhost
paintedwolf.agent_is_implementer
True when the worker agent is an implementer.
profile_surveys_project_treeboolhost
paintedwolf.profile_surveys_project_tree
True when the task agent’s tool profile can inventory or search the project tree.
profile_fetches_urlsboolhost
paintedwolf.profile_fetches_urls
True when the task agent’s tool profile can retrieve a URL.
repo_known_emptyboolhost
paintedwolf.repo_known_empty
True when the host measured zero indexable files in the project.
last_audit_ungroundedboolhost
paintedwolf.last_audit_ungrounded
True when the last assistant citation audit failed to trace.
grounding_escalatedboolhost
paintedwolf.grounding_escalated
True when the grounding circuit breaker has escalated.
command_not_argvboolhost
paintedwolf.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
paintedwolf.bulk_denied
A bulk operation was denied.
binary_deniedboolhost
paintedwolf.binary_denied
A binary-file operation was denied.
mode_deniedboolhost
paintedwolf.mode_denied
The requested mode was denied.
path_escapeboolhost
paintedwolf.path_escape
The path escapes the permitted root.
beyond_eofboolhost
paintedwolf.beyond_eof
The requested range lies beyond end of file.
not_runningboolhost
paintedwolf.not_running
The referenced process is not running.
unsupportedboolhost
paintedwolf.unsupported
The requested operation is unsupported for this target.
resource_limitboolhost
paintedwolf.resource_limit
A resource limit was hit.
conflictboolhost
paintedwolf.conflict
The operation conflicts with current state.
path_requiredboolhost
paintedwolf.path_required
A required path argument is missing.
id_requiredboolhost
paintedwolf.id_required
A required id argument is missing.
policy_deniedboolstandard
tool
A profile or scope policy denied the call.
unknown_targetboolhost
paintedwolf.unknown_target
The named target does not exist.
missingboolhost
paintedwolf.missing
A required argument or artifact is missing.
forbiddenboolhost
paintedwolf.forbidden
The operation is forbidden in this context.
selector_emptyboolhost
paintedwolf.selector_empty
The selector matched nothing.
selector_ambiguousboolhost
paintedwolf.selector_ambiguous
The selector matched more than one target.
reject_observationstringhost
paintedwolf.reject_observation
Snake_case one-off observation token for unique reject codes.
rejection_codestringhost
paintedwolf.rejection_code
The structured Code: the tool refused this call with — MCP_CALL_FAILED for an MCP server error; empty when the call was not refused. It identifies the failure and never selects rules.
recent_tool_nameslist<string>host
paintedwolf.recent_tool_names
Recent tool names in order, for flow: sequence matching.
mcp_provider_idstringstandard
mcp
MCP catalog provider 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_{provider}_{tool}; equals tool for MCP calls.
mcp_provider_configuredboolstandard
mcp
True when the call’s provider id exists in the merged MCP catalog.
mcp_provider_enabledboolstandard
mcp
True when the call’s provider 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.
credential_lengthinthost
paintedwolf.credential_length
Length of the literal credential candidate at credential.assignment, in Unicode code points.
credential_distinct_wordsinthost
paintedwolf.credential_distinct_words
Distinct case-folded words in the candidate, split on whitespace, hyphen, underscore, dot, and comma.
credential_longest_runinthost
paintedwolf.credential_longest_run
Longest unbroken run of letters, digits, hyphen, underscore, plus, slash, and equals in the candidate.
credential_run_classesinthost
paintedwolf.credential_run_classes
How many of lower-case, upper-case, and digit classes the longest run contains; equal-length ties take the larger count.
credential_fingerprintstringhost
paintedwolf.credential_fingerprint
Device HMAC fingerprint of the candidate, never its plaintext; the stock credential rules scope their counters by it. The provider fails if the device fingerprinter is unavailable.
credential_harvestedboolhost
paintedwolf.credential_harvested
True when the root session’s harvest already holds the candidate’s fingerprint. The provider fails if provenance lookup is unavailable.
credential_ignoredboolhost
paintedwolf.credential_ignored
True when current project and device decisions classify the exact candidate as non-secret. Protected evidence takes precedence.
editorconfig_mismatchboolhost
paintedwolf.editorconfig_mismatch
True when an agent write conflicts with the applicable EditorConfig rules.
syntax_check_overriddenboolhost
paintedwolf.syntax_check_overridden
True when a write landed with an explicit syntax_override_reason, so parser validation of the named paths was skipped. The reason and paths reach the rule’s copy.
source_analysis_unavailableboolhost
paintedwolf.source_analysis_unavailable
True when a tool returned a partial result because one or more files could not be parsed for source analysis.
http_request_web_pageboolhost
paintedwolf.http_request_web_page
True when http_request returned a remote HTML document as raw markup while fetch_url was on the turn’s plan. The exchange stands; the fact only lets a rule point at the better tool.

Derived facts

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

FactTypeTierObservation
worktree_stale_pathslist<string>host
paintedwolf.worktree_stale_paths
Agent-policy files whose Git index entry the command changed while the worktree kept its previous clean content; empty when the index did not change or no Git repository applies.
worktree_leftover_pathslist<string>host
paintedwolf.worktree_leftover_paths
Agent-policy files the command removed from the Git index while the file stayed in the worktree; empty when none.
worktree_conflict_pathslist<string>host
paintedwolf.worktree_conflict_paths
Agent-policy files that gained unmerged Git index stages during the command; empty when none.
unobserved_cited_pathslist<string>host
paintedwolf.unobserved_cited_paths
Cited file paths never observed by a tool this session.
unobserved_cited_urlslist<string>host
paintedwolf.unobserved_cited_urls
Cited URLs never fetched this session.
unobserved_cited_handleslist<string>host
paintedwolf.unobserved_cited_handles
Cited evidence handles that don’t trace to the ledger.
citation_fields_presentboolhost
paintedwolf.citation_fields_present
True when required typed citation fields are present.
claims_completionboolhost
paintedwolf.claims_completion
True when the last assistant text claims completion.
has_matching_ledger_jobboolhost
paintedwolf.has_matching_ledger_job
True when a ledger job matches the claimed work.
ledger_criteria_metboolhost
paintedwolf.ledger_criteria_met
True when the ledger records the completion criteria as met.
worker_summary_presentboolhost
paintedwolf.worker_summary_present
True when the worker’s finish summary is present.
worker_artifact_presentboolhost
paintedwolf.worker_artifact_present
True when the worker produced a required artifact.
files_touchedlist<string>host
paintedwolf.files_touched
Paths mutated during the run or leg.
summary_lengthinthost
paintedwolf.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 conditions.

FactTypeTierObservation
fire_countintcoreThis rule’s declared counter increments this session, less resets.
repeat_countinthost
paintedwolf.repeat_count
Consecutive identical tool-call repeats.
fruitless_search_runinthost
paintedwolf.fruitless_search_run
Consecutive search calls that returned nothing.
breaker_countintcoreGrounding circuit-breaker escalation count.
same_code_reject_runinthost
paintedwolf.same_code_reject_run
Consecutive identical rejects sharing one Code:.
code_reject_responsesinthost
paintedwolf.code_reject_responses
Model responses this session in which one tool was rejected with one Code:, across every args shape — unlike the run, it survives a reworded call and an interleaved success. Calls in the same response count once, and the count stops at the escalation threshold.

Detector facts

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

FactTypeTierObservation
secret_matcheslist<map>standard
secrets
Secret and credential spans from a registered detector — the rule’s when: owns the threshold. Empty when no detector ran.

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
paintedwolf.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_provider_configured_for(string) -> boolstandard
mcp
True when the MCP catalog contains the named provider.
mcp_provider_enabled_for(string) -> boolstandard
mcp
True when the named provider 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
paintedwolf.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
paintedwolf.host_resource_policy_for
Configured policy for the named host resource; empty when the catalog has no such entry.

Tier — core 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: parses and type-checks against the fact catalogue, and every fixture’s expected decision is the decision the engine reaches.

Agents

An agent is a YAML persona, a markdown prompt, and a row in its pack’s persona contract. Packs ship all three; a project can override the prompt. This page covers the persona and its capability boundary; the prompt side has its own page. The reject contract is agent-contract.md.

Persona YAML

# agents/plan-writer.yaml
id: plan-writer
name: Plan Writer
description: Creates and updates blueprint markdown under .paintedwolf/blueprints only
tool_profile: plan_write_only
exec_mode: none
capabilities: [plan, write]
topology_roles: [worker]
system_prompt_template: agents/plan-writer.md

The persona names what the agent is for; its tool_profile is the enforcement boundary. There is no tool list here — naming one fails the load, because two places to state an agent’s tools is one place to disagree. topology_roles decides which turn the agent runs on: coordinator, worker, gate, or several.

skills: selects what the agent may read: an exact name list, all for the full catalog, or omission for none. The host can preload the best-fitting skill for the turn, and the agent can request another through skills_read by describing what it needs. See Skills.

Persona row

Your pack lists each of its agents in one persona contract:

# agents/prompts/_persona-contract.yaml
version: 2

agents:
  plan-writer:
    archetype: plan_write
    playbook_ids: [worker-leg-default]
    delta:
      role_title: Plan Writer
      focus: "Author plan files; do not implement product code."
      process:
        - "Write or edit blueprint markdown per the leg brief."

The archetype supplies the worker protocol — the coordination loop, this session’s tool surface, and how a leg finishes. Archetypes come from the platform pack; your row binds an agent to one and fills it in with delta. An agent with no row has no archetype, so a pack shipping one does not contribute.

Rows travel with the agents they describe: the host merges every installed pack’s rows, so disabling your pack retires its agents and rows together.

Tool profiles

A profile is the capability ceiling — which tools may reach the agent at all:

# tools/profiles/plan_review_readonly.yaml
id: plan_review_readonly
description: Read-only plan review — read and grep on plan paths
tools:
  read: sticky        # sticky = always available; true = deferred via request_tools
  grep: sticky
deny_tools: [write, edit, command, delegate_*]  # wildcards allowed

A profile grants at most. Some tools also belong to one kind of session and are withheld elsewhere whatever a profile says: surface_note posts to the chat the user is reading, so it stays off a spawned worker child, while complete_leg and request_decision act on a leg and stay off sessions the user addressed. One profile serves both kinds, so the rule sits on the tool.

The host preloads tools suited to the turn within that ceiling. The agent can also call request_tools with a plain-language need to load additional allowed tools. Neither path widens the profile’s permissions.

Native tools are engine-implemented — you can’t add one from a pack. The extension paths are: profiles, coordinator surfaces, and MCP for genuinely new tools.

Native tools

These are the ids a profile’s tools: / deny_tools: or a rule’s selector.tool may name. Generated from the engine’s tool manifest:

Show the full native tool catalog (64)

Filesystem

ToolWhat it does
readRead a file and return its content or structure in JSON; unscoped large files return a structure digest. For literal content, set mode / offset / limit / ranges / symbol. Replaces command: cat / head / sed -n.
writeCreate a new file, or write a whole file from scratch: HTML, JavaScript, CSS, Markdown, config, any text; also replaces a file’s full content, or appends with append: true. Supported source must parse (MUTATION_BROKE_PARSE). Scope violation: WRITE_SCOPE_DENIED. Read existing editable files in an earlier response; concurrent edits rebase against it. EDITOR_DOCUMENT_READ_REQUIRED or EDITOR_DOCUMENT_CHANGING requires rereading.
editReplace an exact span of text in an existing file: a line, a block, or a name, in place. Batch independent edits to distinct files. Read in an earlier response (same-batch read+edit rejects: EDITOR_DOCUMENT_READ_REQUIRED). Copy old_string after line-number tab; unique unless replace_all. Parse-breaking splices: MUTATION_BROKE_PARSE. Replaces command: sed -i / perl -pi in-place edit.
replace_linesAtomically replace or reindent line ranges. Use direct fields once, or operations for coupled non-overlapping seams in one snapshot. Read existing editable files in an earlier response before applying changes; concurrent edits rebase against that read. EDITOR_DOCUMENT_READ_REQUIRED or EDITOR_DOCUMENT_CHANGING requires read and recomputation.
restore_versionRestore a file to a previously retained source version as a forward mutation in the source ledger. Use source_history to inspect available versions or diffs.
code_rewriteRewrite a code shape across path or paths[]. $VAR matches one node; $$$VAR a sequence. Narrow bare $A | $B patterns (CODE_REWRITE_PATTERN_TOO_BROAD). Parse-breaking rewrites are refused (MUTATION_BROKE_PARSE). For search, use grep structural: true. Track changed, blocked (code/details), failed, unobserved_files and truncated separately: blocked is not no-match, and successful siblings may already be saved. Address each cause without repeating saved mutations; report uncovered scope. dry_run previews without writing. Replaces command: ast-grep / comby / gofmt -r structural rewrite.
grepSearch files with RE2 text patterns or tree-sitter structural patterns. Recurses under path with optional path_glob, searches ignored files unless include_ignored is false, and skips engine/VCS metadata. Wide results may return a view digest. Replaces command: rg / grep -r.
findBounded recursive directory walk — canonical for ** name_glob. Default type=any returns files and directories together. Wide unscoped walk at path="." (2000+ paths) returns view digest (selected=0) with distribution + FIND_OVERFLOW_NARROW — pass name_glob when you can name a filename pattern. A concrete path (not “.”) or name_glob returns literal results[] (same as list_dir targeted). Explicit max_results or offset also forces a literal page. Replaces command: find(1) recursive walk, tree(1).
jqQuery JSON, YAML, or TOML via sandboxed jq. Pass limit/offset for literal values[]. Replaces command: jq, yq.
jq_editUpdate a JSON, YAML, or TOML document with a jq program and write the result in the same format, in place or to dest. The program returns the whole document (.a.b = 1, del(.x), .items |= map(...)). Unchanged values keep their literals, key order, and YAML comments; syntax a rewrite would lose is refused (JQ_EDIT_LOSSY).
statFile metadata (mode, size, mtime, entry_count for directories) Replaces command: stat(1).
wcCount lines, bytes, and words in files or recursively in directories. bytes is the true file size; truncated marks counts taken from a capped scan. Replaces command: wc -l.
list_dirLocate repository areas with {“path”:"."}. A named directory or explicit listing controls return entries instead. Use summarize to explain the area. Replaces command: ls -la.
chmodChange approval bits on repo-relative paths. Replaces command: chmod(1).
deleteRemove files and empty directories within write scope; the person can undo each removal. Replaces command: rm(1).
copyCopy files within write scope (files only; never use command cp). Replaces command: cp(1).
moveRename files within write scope (never use command mv); a replaced destination can be undone. Replaces command: mv(1).
mkdirCreate directories within write scope (never use command mkdir). Replaces command: mkdir(1).
chownChange file ownership within write scope (never use command chown). Replaces command: chown(1).
diffUnified diff between two repo-relative file paths (read-scope only; never use command diff). Replaces command: diff(1).
extract_archiveUnpack .zip or .tar.gz into a write-scoped directory (replaces unzip/tar xzf; HITL ask by default). Replaces command: unzip.

Git

ToolWhat it does
git_statusGit working tree status JSON (available, branch, head_short, upstream, ahead/behind, dirty, staged/unstaged/untracked counts, files with porcelain XY status one entry per file — untracked directories are expanded; available:false on non-git projects). Pages of 80 files: read files_total, use next_offset to continue when more coverage is needed; exhaust pages when the task requires the complete selection. paths narrows the selection to repo-relative prefixes (a file, or a directory and everything under it). summary:true returns counts and groups without file entries (default group depth 1). group_depth adds groups: which areas changed and how much, rolled up by leading path segments, largest first — the survey to run before paging files on a large tree. Informational only — dirty is normal; does not require cleanup. Replaces command: git status.
git_diffGit diff JSON, one page of files in git’s order (available, files with per-file insertions/deletions and, unless stat, that file’s hunks in diff; available:false on non-git projects). Pages hold up to 80 files and 5 KiB of hunks by default: read files_total, use next_offset for more files when needed; exhaust the selection for tasks requiring complete coverage. wire_spill_path retains the captured page. A clipped file has diff_lines and diff_spill_path: read that path with offset/limit to recover its original hunks; file offsets do not continue within a file. paths narrows to repo-relative paths or directories; base_ref compares against a commit or branch; untracked adds new files as additions. Replaces command: git diff.
git_logRead paged commit history from a branch, tag, commit, or revision range. Use ref for revisions and path only for a file. main..feature lists commits unique to feature; main…feature lists both sides. Follow next_offset to continue. Replaces command: git log.
git_commitStage and commit paths with a message; amend=true replaces HEAD with the selected paths, including new files, leaving unrelated staged changes alone. Success JSON: available, hash, paths, and uncommitted_paths/uncommitted_count for dirty files this commit left alone. Failure JSON: available=false, error, optional hook_failed, output_tail (last hook/CLI lines), and hint. Empty paths stages only files this session wrote; name paths explicitly to commit work this session did not write (max 500 paths per commit). Replaces command: git add paths && git commit -m, git commit –amend -m message – paths.
git_showShow commit or file at ref JSON (available, show with kind diff/blob). Replaces command: git show.
git_blamePer-line blame JSON (available, lines with commit/author/date/content). Replaces command: git blame.
git_restoreRestore explicit paths in the Git worktree, index, or both. Defaults to Git’s ordinary worktree-from-index behavior. This discards selected state — use only when the user explicitly asked to revert named paths. Replaces command: git restore.
git_refResolve refs to SHAs JSON (available, refs with ref/sha/peeled). Replaces command: git rev-parse.
git_branchesList local branches JSON (available, branches with name/current). Replaces command: git branch.
git_compareCompare frozen commit IDs: ahead/behind counts for head_ref relative to base_ref, plus all merge bases.
git_checkoutSwitch to an existing local branch, or explicitly create and switch to a new one. Rehearses and reviews file changes before applying. Preserves unrelated local edits; refuses conflicting edits. Reports the actual branch and HEAD even after failure. Replaces command: git checkout.
git_mergeMerge a revision into the current branch. Rehearses and reviews file effects. Conflicts are explicit and retain the merge state: edit resolutions, then continue with explicit paths; abort only when asked to discard merge work. Check status and after state, not diagnostics, for success. Replaces command: git merge.
git_stashSave explicit repository paths temporarily, apply a saved stash, or explicitly drop one. Save preserves unrelated edits and index entries. Apply retains the stash even on success; use drop separately after verifying restoration. Conflicts and partial effects are reported in structured state. Replaces command: git stash.
git_stash_listList stashes with immutable IDs and current selectors for apply/drop. Replaces command: git stash list.

Meta

ToolWhat it does
request_toolsLoad tools for the next concrete operation
verifyRun a verification check and report its terminal outcome. Bare verify runs the selected project command. With no selected command, supply the documented check appropriate to the task. A different command or pipeline is rejected when a project command is selected; use command(verification: true) for targeted checks. Selecting a command does not require it after every edit. outcome is passed, failed, or unverifiable. declared_command_match identifies the selected command, not its success. A live handle has no verdict; wait on it without starting a duplicate. Both verify and command accept the same reviewed capabilities. A boundary refusal does not explain every failure in a mixed run. Report blocked validation when no justified recovery remains.
survey_repoOne catalog map (Tier 1 digest, selected=0), not a briefing. Call when you still cannot name a destination and the ask is unknown layout (layout_overview), duplicated env/config/stores (ssot_drift), or where HTTP routes/handlers live (api_routes). Then summarize the destination. For how/what on a named file/package/dir, use summarize(path=…) — never survey_repo for depth on a path you can already say.
summarizeExplain a named file or area with a bounded, source-cited briefing. Path sets scope; task ranks within it. Use next_actions only when more evidence is needed.
skills_readRead the most relevant skill for a described need, or a referenced file from that skill.
surface_notePost a concise, grounded mid-turn note for the user, then keep working — this does not end the turn. Use it for meaningful progress or expectation changes. Say what happened and what comes next. Cite at least one cited_evidence or cited_urls entry. Carries artifact_ids, which is how a still reaches the user mid-run. For a question use ask_user; the final answer is the no-tool closeout.
complete_legFinish this worker leg when the assignment is done or blocked. The host builds the parent report from these fields plus disk proof.
recallLook up what was already observed instead of observing it again. Reach for it when a finished worker leg’s envelope omits a detail the leg saw, when a continuation record or a [compacted …] banner names a handle whose body is gone from context, or — from a root session — when an earlier session on this project may already have looked (widen). It reads the recorded index, not the working tree: use grep or read for files as they are now. Branch on the result’s resolution, never on the row count: no_match_in_scope means this scope contains records, but none matched this query; scope_empty means nothing here has been observed yet (dispatch or observe it yourself; do not search again), record_deleted means it existed and is gone, executor_degraded is never evidence of absence. Do what next_action says. Each hit states observed_at and currency — a changed hit describes the file as it was, not as it is. An unchanged query searches the same recorded scope; fewer terms or corrected filters can recover missed matches.
source_historyQuery the source ledger for file effects (mode=effects), current line authorship (mode=lines), this session’s paths (mode=mine), historical version content (mode=version), or version diffs (mode=diff). Includes uncommitted state. Actors: you / agent / user / external / mixed / unknown. Missing history is reported explicitly.
secret_generateGenerate random secret material and return an opaque {{paintedwolf-secret:…}} reference for tool execution.
secret_listList visible managed secret references and metadata, never values.
secret_revokeRevoke a managed secret reference while retaining provider-screening evidence.

Visual

ToolWhat it does
render_viewRasterize SVG or HTML into an authored visual: UI/product mockups (especially before building), diagrams, comparisons, timelines, or charts. Use when spatial layout improves clarity; never as runtime evidence — use capture_page for observed UI. Host injects an offline design kit (OFL fonts, Lucide-adapted icons, theme tokens, spacing/type scales, media placeholders). No CDN links, scripts, or external https assets — use kit font-family names, #kit-* icons, and –kit-* CSS variables. Project images/CSS load offline via http://lycaon.asset/. Presets establish width and a minimum height floor (desktop: 1280×720); under fit: content, the canvas expands for taller documents. For full-screen mockups (games, pages), make root markup fill 100% width and match theme (‘dark’ for dark visuals) to avoid unused light margins. Result reports the canvas it laid out plus the kit catalog.
view_imageInspect an image file (.svg, .png, .jpg, .jpeg, .webp, .gif) or a session visual and perceive its rendered pixels and extracted text. Returns format metadata and extracted text, and emits a visual capture for multimodal models.
view_videoSee moments of a screen recording or other video (.mp4, .mov, .webm), including an attached one. Returns one sheet of frames, each labeled with its timestamp, plus the video’s duration and size. Without times_ms it spreads count frames across start_ms to end_ms (default: the whole video); name exact times_ms to look closer at a moment, and crop to read a region at full resolution.
capture_pageOne-shot open→act→snapshot→close; prefer page_* for live/authenticated state. Use project_dir for a static tree or url plus process_handle for a loopback app. Targets are exclusive. Returns structure, requests, errors, console, and a screenshot; use filmstrip for settled sequence claims and timeline for anything that happens while the page is painting.
measure_pageMeasure selector geometry in CSS pixels, and whether the pointer can reach each element or what covers or clips it. Use id for an existing live page, or one exclusive project_dir/loopback url target. annotate adds boxes; page_geometry remains the numeric evidence.
page_openOpen a live page; returns an id held across turns. The page has its own cookies and storage, which persist until page_close. Up to max_pages stay open per session; re-opening a held target reloads it with the given viewport and routes. Prefer capture_page for a single check. Bindings match capture_page (project_dir or url).
page_actDrive a held page with trusted input. Each step aims where its target receives the pointer; the result reports that point plus the requests, errors, and console output the drive caused. With record, the drive is also a timeline seen as a labeled contact sheet.
page_snapshotUniversal {state, snapshot, log, artifact} for a held page (does not close it).
page_closeTear down a held page; optional final snapshot.

Terminal

ToolWhat it does
terminal_openLive host terminal (pty): run a command-class argv command. Prefer command when you only need exit code/tail for yourself. Use this tool when the session must be held or the user should see the screen — then terminal_snapshot. Returns an id held across turns plus the requested observation (screen by default). Never supply known plaintext via later terminal_send — use env/credential helpers. An opaque {{paintedwolf-secret:…}} reference may be sent when an interactive prompt is the only supported input; the host resolves and screens it without showing the value. Takes the same capability_request as command except network destinations: a held session cannot hold one-action direct network authority, so run that under command.
terminal_sendWrite literal text and/or named control tokens ({Enter}, {Ctrl-C}, …) to a held pty, then return the settled vt grid. Never send known plaintext passwords, tokens, or passphrases. An opaque {{paintedwolf-secret:…}} reference is allowed when the interactive prompt is the consuming boundary; the host resolves and screens it.
terminal_readRead incremental pty output since the last read (idle/timeout bounded).
terminal_snapshotUser-visible photograph of the held terminal session (CLI output or TUI), parallel to capture_page for pages. Returns one bounded VT screen and a receipt, never a duplicate main-stream log. Use for any held session — not only full-screen TUIs. Settles on output quiescence; does not advance terminal_read’s cursor.
terminal_closeTear down a held pty (graceful then kill). It returns lifecycle acknowledgement only; use terminal_read or terminal_snapshot before closing when output matters.

Processes

ToolWhat it does
process_listInspect host processes after approval. Returns names, executable paths, UID, parent PID, and opaque instance references; no argv or environment. Use pid for a known target. Use next_after_pid for another page of the changing snapshot. Unavailable metadata does not mean exited. Use command_output and command_stop for jobs you launched with command.
process_signalSend a signal to exact host process instances after approval. Pass opaque references returned by process_list in this task. PID reuse or changed identity rejects; names, bare PIDs, groups, and descendants are not targets. TERM requests graceful termination; KILL can discard unsaved work. Delivered means the signal was accepted, not that exit was observed. Inspect the result for each target. Use command_stop for command job handles.

Network

ToolWhat it does
http_requestSend one bounded request to an API, webhook, or service endpoint and return its status, headers, and raw body. Replaces command: ad hoc HTTP API request.

bash is not in this manifest by design. It is surface-gated — only the phases whose coordinator surface lists it can reach it at all — and every command is separately allow-listed and confined before it runs; see Security & privacy. Profiles may still deny it explicitly, and most read-only work has a native tool that replaces it.

Tools an MCP server contributes are named mcp_<server>_<tool> and can be referenced from profiles and selectors with the same syntax as the ids above.

Write-worker file tools run against a complete private snapshot. Reads stay on that snapshot, and removing a file records a deletion for promotion. preview_overlay reports whether current source verification would let the overlay land. Promotion requires that verification and lands as one recoverable transaction. Each worker overlay ends with one promote_overlay or reject_overlay call.

For host process work, prefer process_list and process_signal, which use instance-bound references. When a command needs broader access, declare capability_request.process_control or capability_request.host_execution on each call, as with direct_ip. Chat approval can cover subsequent calls, but the declaration is still required. Host execution removes the sandbox for that command and supports passwordless sudo -n. See the execution boundaries for the distinction and approval scope.

The agent contract

Rejected tool calls come back as a structured reject ending in a stable Code: (Rejected / Cause / Fix / Code:). Design your prompts to branch on codes — the codes are agent-public stable; the message text is not. The codes themselves are emitted by Open Agent Rules.

Prompts

Prompts are budgeted pongo2 templates. A layered loader lets a project override one file while the rest continues to update with the app. Pack authors declare host compatibility through compatibility.extension_api, not a prompt version.

The template dialect, archetypes, heading contract, playbooks, and assembly order are in the product repository: agent-prompt-template.md and prompt-assembly.md.

The four planes

PlaneWhat it isPack path
PersonaThe agent’s stable system prompt, plus the prompt units each turn loadsagents/prompts/, shared/archetypes/, shared/partials/, shared/units/
System injectsPer-turn context blocksguidance/ inject templates
Tool feedbackThe rendered body of a structured reject or bannerguidance/reject/
KicksShort imperative nudges at lifecycle momentsguidance/ kick templates

Templates own layout only. The host owns facts, tool lists, and char budgets.

The loader

Every template — entry files and every {% include %} target — resolves through layered roots, higher wins:

site  >  {repo}/.paintedwolf/prompt_files/  >  bundled

In a multi-root project, the active root’s .paintedwolf/prompt_files/ takes precedence over the primary root’s. Resolution is per-file.

Three rules govern it:

  • Fail closed. A parse error in your winning overlay is an error, never a silent fallback to the bundled copy.
  • Override the file, keep the tree. Replacing a partial updates every prompt that includes it.
  • Use the bounded syntax. Interpolation, conditionals, short loops, and statically named includes are supported. Dynamic includes, inheritance, macros, imports, stateful operations, random operations, and body-buffering tags are rejected.

A project prompt_files/ entry or a pack unit you own: holds that path at your bytes. Unowned bundled files keep updating with the app.

Always render and read an override the way a session will:

pw prompts render --agent implementer --project . --check
pw prompts render partials/finish-handoff.md --project . --var focus=triage

--check enforces required headings and the byte budget. Reading the result catches empty or misspelled variables.

Prompt units

Instruction text that only some turns need ships as a unit: one markdown file under shared/units/<stem>.md, front matter first, then the template body.

---
description: >-
  Planning and applying Terraform changes: plan to a file, read the plan,
  and apply only that saved plan.
slot: procedures
attaches: [command]
hosts: [coordinator, worker]
---
Run `terraform plan -out=tfplan` and read the plan before anything else. Apply only `tfplan`, never a fresh plan.
FieldMeaning
descriptionThe work the unit serves, in at most 60 words. It is the only thing the local decision model reads about the unit.
slotWhere it renders: orientation, conduct, evidence, execution, or procedures.
orderPosition within the slot. Optional.
attachesTools the unit follows. It renders only while one of them is offered.
modesExecution mode families it applies to. Optional.
hostscoordinator, worker, or both.

A unit attached only to tools a turn may load follows their load decision, so a procedure arrives with its tool and not before. Any other unit renders by default and is left out only when the local decision model is confident the request doesn’t need it. An omission holds only while every turn since the chat’s last cold start agrees with it, so a unit a later request needs comes back. A turn starts cold on the chat’s first turn, after an idle gap longer than the provider keeps its cache, on a new model or provider, or after the history is compacted. Warm turns leave the prompt’s prefix alone unless a unit has to come back, which keeps it cacheable. Without the model, every unit renders. Only the platform pack contributes always-on prose; a pack unit always carries a decision, so no pack can make itself permanent.

Write the description for the request, not the file: name what someone is asking for when this text matters. A malformed pack unit is reported and skipped, and the rest of the pack still loads.

Lifecycle injects

Prompt content that fires at a moment is an inform binding on the same anchor catalog used by rules: rules block, bindings inform.

A workflow can ship phase-entry guidance with an injects: entry — no engine code:

# workflow.yaml
injects:
  - on: phase.entered
    selector: { surface: phase, phase: plan, workflow: recon-pack }
    effect: inform
    render: coordinator-recon-pack-plan     # <your pack>/guidance/coordinator-recon-pack-plan.md
    tier: workflow

Workflow-tier bindings only fire inside their workflow, and they win over builtin bindings for the same anchor. Cross-workflow guidance lives under host/bindings/ with tier: builtin. The binding grammar is in agent-prompt-template.md.

Overriding from a project

You want toDo
Adjust one agent’s personaOverride agents/<id>.md under {repo}/.paintedwolf/prompt_files/
Change a shared behavior everywhereOverride the partial, not each agent
Add phase guidance to your workflowinjects: in your workflow.yaml
Ship personas + prompts to other reposPut them in an extension pack under agents/

MCP & scanners

MCP providers and scanner engines are enabled in Settings, never installed by a pack. This page covers the SDK seams packs can reference; for setup, see Settings. Catalog merge, trust gates, and the runtime contract are in mcp.md.

Provider catalog and tool policy

Three layers merge, project over device over distro:

LayerFile
Distroshipped with the app
Device~/.config/paintedwolf/mcp.yaml
Project{repo}/.paintedwolf/mcp.yaml

Each mcp.yaml has one closed top-level providers: list. command: defines local stdio; url: defines streamable HTTP. The host derives class from those fields. Device entries may use stdio env, or HTTP token / headers / OAuth. Project entries are limited to credential-free loopback HTTP.

tool_loading: auto is the default: small tool sets load directly and larger sets are found in the same turn. Use tool_loading: always when every eligible model request should include that provider’s tool definitions.

Agent tool registration reads the device catalog. A project-declared provider is visible in that project’s Settings and reachable by Test connection; it never becomes a callable host tool.

Typed result bindings

A device-scope pack can ship mcp_bindings/ — a JSON Schema plus field paths that project a tool’s result into typed facts your rules evaluate. Project desired state cannot add, disable, or own mcp_bindings units.

# mcp_bindings/widget.yaml
id: widget
provider_id: fixture
tool_name: widget
schema:
  type: object
  required: [status]
  properties:
    status: { type: string }
fields:
  - key: widget_status
    type: string
    path: /status
  - key: is_ready
    type: bool
    path: /status
    equals: ready
when: mcp_schema_matched && mcp_field_bool("is_ready")

Duplicate fact keys across binding units fail closed at validate. Generic MCP facts (mcp_provider_enabled, mcp_call_ok, mcp_error_code, …) are always available without a binding.

Scanner integration

Scanners follow the same polarity: enabling and running them is a Settings decision. Packs must not ship scanner engines — they may only declare requires_scanners to gate guidance until the operator enables a named scanner. A project may enable a known scanner by id ({repo}/.paintedwolf/scanners.yaml — id + enabled only) but can’t define one.

See Security for the bundled catalog. Install check: pw scan engines check. Snapshot and watch policy live in scan-supply-chain.md.

Themes

A theme is one YAML file in a pack: literal colours assigned to a closed set of interface and syntax tokens. Themes restyle and never behave. There is no expression language, no selector, and no stylesheet — a value is #rrggbb or #rrggbbaa and nothing else.

Start from a working palette

pw extensions new-theme --from charcoal --id acme/kit:nightshade --name Nightshade > \
  my-pack/contributions/themes/nightshade.yaml

The file compiles before you change a thing. It lists only the decisions the palette’s author made and omits anything the host can derive, so every line in it is a line you can edit. Pass --full to print the complete vocabulary instead, and --from daylight to fork the light palette.

Six colours is a complete theme

id: acme/kit:nightshade
name: Nightshade
appearance: dark

tokens:
  background: "#14121a"
  text: "#e2ddf0"
  accent: "#b08ae0"
  danger: "#ff6b81"
  warning: "#e8b04b"
  status-positive: "#6fd0a0"

syntax:
  keyword: "#c792ea"
  string: "#c3e88d"
  comment: { color: "#8b93b8", italic: true }

Those six tokens are the decisions nothing can make for you. Every other token has a host derivation, and every syntax scope you leave unset folds to its parent — keyword.control follows keyword, an unset root follows body text. Set as few or as many as you like; both compile to a complete theme.

What the host derives, and what it will not

Tints, diff washes, scrollbar mixes, and shadows are expressions over the tokens you set, so they follow your palette without appearing in your file. You cannot set them, rename them, or add to them.

The host fits line and word diff fills to the palette so code stays readable on both layers. Themes set diff-add-hue and diff-delete-hue; the host derives diff-add-line, diff-add-word, diff-delete-line, and diff-delete-word. Dim syntax colors produce quieter fills.

Two pairs are easy to conflate:

  • Diff colours are separate from status colours. diff-add-hue and diff-delete-hue default to your success and danger colours, and you can override just those two to make every diff surface colourblind-safe without changing what success and failure mean elsewhere.
  • Status inks are derived from status hues. warning is a fill, a wash, and a dot; warning-text is caution copy on a surface. The second is derived from the first and darkened or lightened only as far as readability requires, so you declare one colour and both jobs work.

Readability is checked, not assumed

A small closed list of pairs — body text, status copy, accent labels at rest and under the pointer, both selection bands, diff hues, and the product mark — is measured when your theme compiles. Fall under a minimum and the build fails with the pair, the ratio, and the reason. pw extensions validate prints the measured ratio for every gated pair, so you can see your margin rather than only that you passed.

Colors for multiple windows

A theme can declare a generative palette for multi-window editing. The main window uses main; additional windows sample anchors in OKLCH, staying within the declared hue excursion and chroma range. The same window color paints the active file tab’s fill and top accent, its local caret and selection, and its cursors, selections, and labels in other windows. Local selections use a stronger tint; peer selections use a lighter fill with a muted continuous outline following their complete boundary, including wrapped and empty lines. Scrollbar ticks use the same window colors; the Window selections category is enabled by default and can be turned off in Editor settings.

window_colors:
  main: "#b08ae0"
  anchors: ["#6fd0a0", "#81a1c1", "#e8b04b", "#c792ea"]
  hue_spread: 24
  chroma_min: 0.04
  chroma_max: 0.16

main defaults to accent-signal; anchors default to accent, status-positive, and cost-workers. Supply 1–12 opaque hex anchors. hue_spread is 0–180 degrees around each anchor (default 24); chroma_min and chroma_max are 0–0.4, ordered low to high (defaults 0.04 and 0.16). A subdued theme can narrow both the hue excursion and chroma range. Zero chroma creates a monochrome family.

Early window colors are chosen for perceptual separation from the main color and each other. Later slots continue sampling deterministically, without cycling a fixed list. Stable window slots preserve assignments when windows close or reopen a file; switching themes repaints the same identities. The client maps colors into sRGB and corrects caret and label contrast, including Increase Contrast. Selection fills stay subtle and preserve text readability. No palette can make arbitrarily many colors visually distinct, so window labels remain available and never rely on color alone.

Colors for agent chats

Agent chats get their own recipe, so what an agent reads or is about to change never looks like another window. Every chat shares one muted hue; each is a lightness step from main.

agent_colors:
  main: "#6e7689"
  lightness_step: 0.1
  chroma: 0.025

main defaults to text-muted. lightness_step is 0.02–0.25 (default 0.1) and chroma is 0–0.12 (default 0.03).

Beyond colour

A theme may also set its handwriting and quiet the product mark:

  • icons.stroke — weight, cap, and join for every stroked glyph, including ones you never redraw. Weight multiplies each slot’s width, so relative emphasis survives.
  • icons.geometry — replace the glyph inside a control from a closed shape vocabulary. The frame stays the host’s, so a themed glyph cannot resize a control or carry its own colour.
  • brand.logomark: hidden — quiet the product mark while keeping its layout box. The brand-field token paints the plate behind its bars and defaults to product black.

Token vocabulary

The complete vocabulary — every token, its fill rule, every syntax scope and what it folds to, every icon slot, and the contrast floor — is in theme-tokens.md. A JSON Schema ships alongside the app, so an editor pointed at theme-unit.schema.json completes token ids as you type.

Themes install like any other pack content: see Extension packs. Users pick one in Settings → General → Display; see Settings.

Enable the pack on the device and its themes are available everywhere, including before a project is open. A repository can suggest a theme pack; you choose whether to install it and which theme to use.

Reference

The project overlay directory

Everything under {repo}/.paintedwolf/ that you author is committed with your code:

PathPurpose
extensions.yamlSuggested device installs and eligible unit disables (format: 1)
workflows/<id>/workflow.yamlCustom workflows
AGENTS.mdProject guidance per the agents.md standard; agent edits require human approval — see agents-md-standard.md
mcp.yamlMCP overlay — loopback HTTP only
scanners.yamlScanner enables (id + enabled only)
detection-packs.yamlDetection-pack enables (id + enabled only)
postures.yamlSession posture retuning — the four shipped ids only
prompt_files/Prompt template overrides
approvals.yaml, limits.yaml, verify.yamlSettings overlays — see Settings
blueprints/Workflow-governed planning documents

Host-managed evidence, caches, checkpoints, and worktrees live under ~/.config/paintedwolf/, outside the project overlay. Credentials never belong in a repo.

CLI

Put the bundled pw binary on your PATH first — see Projects. Everyday project commands (pw ., ls, logs, completion) live there.

pw extensions install <url|file://…|path:…> [--version RANGE|--ref REF]
pw extensions install-meta <url|file://…|path:…> [--version RANGE|--ref REF]
pw extensions remove <pack-id>
pw extensions remove-meta <meta-pack-id>
pw extensions update <pack-id>
pw extensions reload <pack-id>
pw extensions lock [--json]
pw extensions validate [--project DIR|--device] [--json]
pw extensions inspect-command <command-id> [--project DIR|--device] [--json]
pw extensions apply-profile <pack-id> <profile-name>
pw extensions disable|enable <pack-id|unit-id>
pw extensions disable|enable <unit-id> --project DIR
pw extensions own <unit-id> <pack-id>
pw extensions enable-suite|disable-suite <meta-pack-id>
pw extensions new-theme --id <provider:name> --name <display> [--from daylight|charcoal] [--full] [--out FILE]

pw workflow validate [--bundled] [--json] [--project DIR] [PATH…]

pw rules test [--project DIR] [--json] [PATH…]

pw prompts render <template-ref> [--project DIR] [--var k=v]… [--json]
pw prompts render --agent <id> [--project DIR] [--check] [--json]

pw scan engines list|check|validate [--project DIR]

Installation, updates, ownership, and profiles are device operations. validate and inspect-command accept a project context; disable and enable accept --project DIR for eligible units. validate exits 1 whenever something you asked for is not in effect — a pack that failed integrity, failed to load, or is missing a dependency — so it slots into CI.

rules test and prompts render are the verify half of the author loop. Both are read-only and both exit 1 when something is wrong:

VerbExits 1 when
rules testA rule fails schema validation, its when: does not compile, or a fixture disagrees. --json emits a stable code
prompts render --checkA rendered persona is missing a required heading or exceeds its byte budget

Workflow diagnostics

workflow validate emits stable codes — branch on code, never on message text. Generated from the engine registry:

Show all workflow diagnostic codes
CodeMeaningFix
ambient_unresolveddefault_ambient_workflow {{workflow_id}}@{{version}} is not in the registry.Fix registry.yaml to point at a shipped ambient workflow.
attach_session_create_on_overlayCustom overlay manifest cannot declare attach.policy session_create.Remove attach.policy session_create; ambient attachment belongs to the platform registry.
blueprint_writer_missing_surfaceBlueprint writer phase {{phase}} in workflow {{workflow}} has no coordinator surface.Bind the phase to a coordinator surface that exposes write, edit, or replace_lines.
blueprint_writer_requiredWorkflow {{workflow}} consumes a Blueprint but no phase declares blueprint_write.Mark every phase whose coordinator authors or revises the Blueprint with blueprint_write: true.
blueprint_writer_surface_read_onlyBlueprint writer phase {{phase}} in workflow {{workflow}} binds read-only surface {{surface}}.Bind the phase to a surface that exposes write, edit, or replace_lines.
blueprint_writer_without_blueprintPhase {{phase}} in workflow {{workflow}} declares blueprint_write without a workflow Blueprint.Declare the workflow Blueprint or remove blueprint_write from the phase.
boolexpr_parseBoolean expression parse error: {{detail}}.Fix the complete_when / entry_when / when expression syntax.
catalog_only_predicateCatalog-only condition {{leaf}} cannot gate bundled workflows.Use a shipped host predicate, or keep this leaf for catalog-only compose stubs.
decision_gate_phase_mismatchDecision gate {{leaf}} does not match phase {{phase}}.Align decision gate and intake key.
decision_gate_phase_missinguser_decision_received gate references missing phase {{phase}}.Align the gate with a phase that declares intake decisions.
depth_param_missingPhase {{phase}} depth_param {{param}} is not a depth-typed parameter.Declare parameters.{{param}} with type: depth.
domain_leaf_on_overlayDomain leaf {{leaf}} is bundled-only and cannot appear in custom overlays.Use a generic gate-kit leaf such as evidence_passed:verify, hitl_consulted:<phase>, or human_approval. See docs/workflows.md#custom-workflows.
extends_cycleextends cycle detected.Remove the cycle in the extends chain.
extends_depth_exceededextends depth exceeded.Shorten the extends chain (max depth 3).
extends_errorextends error: {{detail}}.Fix the extends chain.
extends_not_allowedextends parent {{extends}} is not in the compose policy allowlist.Extend an allowed parent (see compose-policy.yaml).
extends_requiredSession compose requires extends when require_extends is enabled.Set extends: to an allowed parent.
feedback_gate_phase_mismatchFeedback gate {{leaf}} does not match phase {{phase}} feedback key.Align gate leaf and on_enter.request_user_feedback.
feedback_gate_phase_missinguser_feedback_received gate references missing phase {{phase}}.Align the gate suffix with a phase that declares request_user_feedback.
feedback_options_requiredPhase {{phase}}: choice feedback requires at least two options.Add options: with two or more entries.
forbidden_predicateForbidden predicate {{leaf}}.{{replacement_hint}}
human_approval_blueprint_requiredPhase {{phase}} uses human_approval but the workflow does not declare a blueprint.Add a workflow blueprint and have the phase approve that document.
intake_catalog_errorIntake catalog error on phase {{phase}}: {{detail}}.Fix the intake: block.
intake_gate_missingPhase {{phase}} declares intake but lacks the matching decision/consulted gate.Add user_decision_received or hitl_consulted for the intake key.
intake_gate_phase_mismatchIntake gate {{leaf}} does not match phase {{phase}}.Align hitl_consulted / intake keys with the phase id.
intake_key_unknownIntake key {{key}} on phase {{phase}} is unknown.Use a declared intake key.
invalid_execution_modeInvalid execution mode {{mode}}.Use a valid execution mode.
invalid_iconUnknown icon slot {{icon}}.Name a host icon slot. An unknown name falls back to the generic workflow mark.
invalid_postureUnknown posture {{posture}}.Use a valid session posture id.
invoke_child_has_invokeChild workflow {{workflow_id}}@{{version}} declares invoke_workflow (nesting depth 1 only).Remove invoke_workflow from the child manifest.
invoke_unresolvedinvoke_workflow target {{workflow_id}}@{{version}} is not in the registry.Point invoke_workflow at a known id@version.
load_errorFailed to load workflow catalog: {{detail}}.Fix YAML syntax or path errors and re-run validate.
merchandising_incompleteCatalog workflow is missing merchandising field {{field}}.Set name, description, and trigger so the workflow can be listed and chosen.
missing_coordinator_surfacePhase {{phase}} prompts the coordinator or waits on topology_report_delivered and must name coordinator_surface.Set phases[{{phase}}].coordinator_surface to a surface that compiles the leave tools for that phase.
missing_gate_feedbackGate leaf {{leaf}} has no gate-feedback YAML.Add config/packs/painted-wolf/platform/guidance/gate-feedback for this leaf.
missing_gated_closeout_askPhase {{phase}} declares controls.closeout: gated but surface {{surface}} does not offer ask_user.Add ask_user to the resolved surface tools, or drop controls.closeout from {{phase}}. A gated phase holds a bare-prose finish, so it must leave the coordinator a way to raise a human fork.
missing_hitl_toolsPhase {{phase}} implies HITL tools but surface {{surface}} is missing {{tool}}.Add {{tool}} to the resolved surface tools (ask_user and wait are required when gates/on_enter imply tool-ask or intake).
missing_kick{{kick}} does not name an anchor in the catalog whose binding renders a guidance template in any enabled pack, or in this manifest's own pack.Point inject_kick at a catalog anchor id (for example worker.task.finished) whose inform binding has a render template, or point render at a template that exists.
missing_mode_refMode ref {{mode_ref}} does not resolve to a prompt file ({{path}}).Add the coordinator-mode partial under config/packs/painted-wolf/platform/agents/prompts/.
missing_review_spawn_agentreview_loop on phase {{phase}} requires task({{agent}}) but surface {{surface}} cannot spawn that agent.Name a coordinator_surface whose spawn roster includes {{agent}}, or drop {{agent}} from required_agents / if_spawnable.
missing_review_verdict_toolreview_loop phase {{phase}} requires {{tool}} on surface {{surface}} — a terminal verdict is the only leave.Add {{tool}} to the surface tools for phase {{phase}}.
missing_surface_templateSurface template {{path}} does not exist.Point surface_template at an existing agents/… prompt under config/packs/painted-wolf/platform/agents/prompts/.
missing_tool_for_advance_policyCoordinator-controlled advance on phase {{phase}} requires {{tool}} on surface {{surface}}.Add {{tool}} to the surface tools, or set advance.when_gate_met: auto.
obligation_advance_not_hostPhase {{phase}} declares host obligations but its advance policy is coordinator-controlled.Set phases[{{phase}}].advance.when_gate_met: auto (or controls.phase_advance: host) so the host can advance when the obligation settles.
obligation_gate_missingPhase {{phase}} declares obligation kind {{kind}} but does not gate on obligation_settled:{{kind}}.Add obligation_settled:{{kind}} to phases[{{phase}}].gates so the host holds the phase until the obligation settles.
obligation_kind_unknownPhase {{phase}} declares obligation kind {{kind}}, which is not registered on this host.Use a registered obligation kind. Registered kinds: {{kinds}}.
obligation_not_declaredPhase {{phase}} gates on obligation_settled:{{kind}} without declaring that obligation on enter.Add {kind: {{kind}}} to phases[{{phase}}].on_enter.obligations; re-declaring is safe — enqueue adopts existing run-bound work.
obligation_params_invalidPhase {{phase}} obligation {{kind}} has invalid params: {{detail}}.Fix the obligation params to match the kind's declared parameter shape.
obligation_wake_unreachablePhase {{phase}} hands its wait to a host observer, but settling it reaches {{target}}, which never wakes the coordinator.The host parks the session with no timer while a host obligation holds a phase, so the settle must be the wake. Set phases[{{target}}].on_enter.prompt_coordinator: true, mark it terminal, or let it declare its own on_enter.obligations to chain the wait.
overlay_requires_explicit_surfaceCustom overlay phase {{phase}} must set coordinator_surface, surface_template, and mode_refs.Declare all three fields on the phase; do not rely on shipped surface_profile alone.
phase_cyclePhase graph cycles at {{phase}}.Break the next-link cycle.
phase_dangling_nextPhase {{phase}} next {{next}} is not a declared phase.Fix next to a declared phase id or leave it empty for terminal.
phase_surface_cannot_dispatchphase {{phase}} fans out to {{agent}}, but coordinator_surface {{surface}} dispatches the {{lane}} lane and {{agent}} declares {{declared_lanes}}.Add {{lane}} to {{agent}}'s dispatch_lanes, or name a coordinator_surface whose lane {{agent}} already serves.
phase_unreachablePhase {{phase}} is never reachable from the start phase.Wire next links so every phase is reachable, or remove the orphan phase.
prompt_requiredPhase {{phase}}: request_user_feedback.prompt is required.Set on_enter.request_user_feedback.prompt.
report_control_without_report_phasecontrols.report.enabled is set but no phase is gated on topology_report_delivered — the run would never produce the report the PDF is assembled from.Gate the phase that delivers the run's report on topology_report_delivered, or drop controls.report.
report_phase_exit_mismatchPhase {{phase}} waits on topology_report_delivered but binds {{surface}} (exit: {{exit}}) — the report would ship without the closeout grounding contract.Bind phases[{{phase}}].coordinator_surface to a surface declared `exit: report` in coordinator-surfaces.yaml.
required_gate_missingPosture {{posture}} requires gate {{gate}}.Add the required gate to the closeout or terminal phase.
required_phase_missingextends {{extends}} requires phase {{phase}} in the effective manifest.Add the required phase id.
review_agent_not_declaredreview_loop on phase {{phase}} names agent {{agent}}, which is not in this workflow's agents[] list.Add {{agent}} to agents[] or remove it from phases[{{phase}}].review_loop.required_agents / if_spawnable.
review_agent_role_conflictreview_loop on phase {{phase}} lists agent {{agent}} in both required_agents and if_spawnable.Keep {{agent}} on required_agents if it must always run, or if_spawnable if it runs only when the turn roster can spawn it.
review_loop_evidence_unreferencedPhase {{phase}} review_loop evidence_key {{key}} is not in gates as evidence_passed:{{key}}.Add evidence_passed:{{key}} to the phase gates.
review_loop_incompletereview_loop evidence_key {{key}} is missing gate-feedback or verdict fixture closure.Add gate-feedback YAML and a verdict fixture for this evidence key.
rules_path_missingRules path {{path}} was not found under bundled or project config.Fix the rules: entry to a real relative path.
spawn_not_subsetSpawn allowlist agent {{agent}} is not in implement allowed_agents.Add the agent to implement/workflow.yaml allowed_agents or remove it from implement-default-spawn.yaml.
surface_missing_update_progressSurface {{surface}} lists progress-gated tools but omits update_progress.Add update_progress to the surface tools list in coordinator-surfaces.yaml.
terminal_requires_orchestration_completeTerminal phase {{phase}} must use complete_when: orchestration_complete.Set complete_when: orchestration_complete on every terminal: true phase.
topology_bind_unknownPhase {{phase}} binds topology stage {{stage}} which is not in topology {{topology}}.Set bind_topology_stage to a pipeline.stages[].name from the topology YAML.
topology_missingTopology {{topology}} is missing or failed to load.Add a YAML file under config/packs/painted-wolf/platform/workflows/_topologies/ for this topology id.
topology_stage_complete_unboundPhase {{phase}} uses topology_stage_complete without bind_topology_stage.Set bind_topology_stage to a topology stage name.
unknown_agentUnknown agent {{agent}}.Use an agent id from config/catalog/agents.
unknown_evidence_keyUnknown evidence_passed key {{key}}.Use a typed evidence key (verify, test, security, or a known review-family key).
unknown_extends_parentUnknown extends parent {{extends}}.Point extends at a known id@version.
unknown_predicateUnknown or unregistered condition {{leaf}}.Use a registered gate or complete_when leaf from the condition registry.
unknown_readinesshuman_approval.readiness leaf {{leaf}} is not registered.Use a registered condition leaf for readiness.

Extension resolve codes are on the packs page.

Stock packs

The shipped baseline your desired state starts from — all enabled by default:

PackUnitsRequiresWhat it provides
painted-wolf/browser54painted-wolf/platformBrowser tooling capability for agents that drive a real page.
painted-wolf/bugbash1painted-wolf/implement, painted-wolf/plan, painted-wolf/platformThe /bugbash workflow — hunt bugs in parallel, choose what to fix, verify.
painted-wolf/hitl30painted-wolf/platformHuman-in-the-loop consult surfaces — ask, wait, and approval plumbing.
painted-wolf/implement27painted-wolf/platformThe ambient implement workflow that attaches to every session.
painted-wolf/options2painted-wolf/platformThe /options workflow — explore alternatives before committing to one.
painted-wolf/plan37painted-wolf/platformThe /plan workflow — research, blueprint, and approval phases.
painted-wolf/platform1725—The mandatory base — coordinator, core agents, native tool policy, anchors, and surfaces. It cannot be disabled or removed.
painted-wolf/recon-pack7painted-wolf/platformThe /recon workflow — scout the codebase, reconcile findings, and run one targeted follow-up when needed.
painted-wolf/scan-guidance26painted-wolf/platformPolicy, bindings, and guidance around security scanners. Contains no engines — scanners are enabled in Settings.
painted-wolf/security552painted-wolf/platformGuardrail policy and review behavior. Default on; may be disabled.
painted-wolf/security-survey17painted-wolf/platformThe /security-survey workflow — survey a codebase against its threat model.
painted-wolf/themes8—Ports of widely used cross-editor palettes. Themes only — it contributes no commands or behavior, so enabling it can change nothing but how the window looks.
painted-wolf/web-research24painted-wolf/platformWeb research capability — agents, tools, and guidance for sourced external research.

Schemas

You do not need these to author — extensions validate, workflow validate, and rules test check your files against them. Reach for a schema when you’re building tooling of your own. The Open Agent Rules schema is published on the standard’s own site; the rest are host schemas, mirrored in the product repo:

SchemaValidates
oar.schema.jsonOpen Agent Rules documents
anchor-binding.schema.jsonGuidance bindings (and workflow injects:) — anchor ids are catalogued here
selector.schema.jsonThe shared selector vocabulary
workflow_vocabulary.jsonThe full gate/condition leaf catalog
fixtures/Reference documents for each of the above

Host API

The local engine exposes authenticated REST and SSE on 127.0.0.1; the API reference covers routes and bearer-token setup. Read GET /v1/host first and stop if its contract_version has a different major version from the one your client supports. Treat cursors and revisions as opaque, and branch on error code rather than message text. For GET /v1/contributions, send its ETag as If-None-Match and keep the cached frame after 304 Not Modified.

Wire shapes, editor leases, and workflow revision conflicts are in host-contract.md.

Stability contract

SurfacePromise
Code: ids, native tool names, reject shapeStable — a retired code is never reused
Pack host contractDeclared as compatibility.extension_api on extension.yaml — see packs
Prompt and guidance wordingShips with the app; not a separate version stream
{repo}/.paintedwolf/** formatsAdditive-preferred; extensions.yaml is format: 1
OAR documentsPortable spec, oar: '1.0'
Gate kit, unit kinds, diagnostic codesPublished with the v1 documentation from the engine catalogs
Local HTTP APIcontract_version from GET /v1/host; a new major version is breaking — see API reference