Extension packs
A pack is a directory of units—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:
| Rule | Detail |
|---|---|
| Conflict | Multiple providers and no own → the unit is not loaded |
| Order | Not an input — same desired state and content always produce the same catalog |
No remove: | A pack can never tombstone another pack’s units |
| Platform | painted-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/
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
shared/ # archetypes/, partials/ — include-target trees
mcp_bindings/ # typed MCP result projections (device scope)
contributions/ # commands, menus, keybindings, editor-actions,
# themes, configuration, mcp-requirements
profiles/ # named enable/disable/own sets (not units)
Unit kind properties
When a project enables a pack, each kind falls into one of three classes:
| Class | Project may add units | Project may disabled / own | Typical use |
|---|---|---|---|
| Shared | yes | yes — any unit id | Workflows, agents, guidance the whole team should share |
| Additive | yes (effect-checked for policy and approval rules) | only units its own packs provide | Rules, playbooks, contributions that can tighten, not loosen, the floor |
| Device-only | no | no | Tools, pack skills, bindings, notices, MCP bindings, detection packs |
own: picks a winner when two packs ship the same unit id. Detection packs refuse own: at every scope. Project policy may only use OAR effects block, warn, or nudge. Project approval rules may only use ask or deny. Project skills are not pack units: put them under .paintedwolf/skills/ or .agents/skills/.
The generated table is authoritative for ids, class, and whether own: is allowed:
| Directory | Unit id | Project scope | own: | What it ships |
|---|---|---|---|---|
policy/ | policy/<CODE> | Additive | yes | Open Agent Rules — one flat YAML unit per rule, filename equal to its SCREAMING_SNAKE id. |
guidance/ | guidance/<stem> | Shared | yes | Kick and inject templates — the markdown the host renders into agent prompts at lifecycle anchors. Nested stems allowed. |
workflows/ | workflows/<id> | Shared | yes | Workflow manifests (<id>/workflow.yaml), plus workflows/_templates/<name> and workflows/_topologies/<name> for parameterized recipes and fan-out patterns. |
agents/ | agents/<id> | Shared | yes | Agent persona YAML plus their prompt markdown under agents/prompts/<name>. |
tools/ | tools/<path> | Device-only | yes | Tool catalogs and tool profiles — which tools an agent may use, sticky or deferred. |
approvals/ | approvals/<path> | Additive | yes | Approval catalogs — the human checkpoint definitions workflows reference. |
playbooks/ | playbooks/<path> | Additive | yes | Worker checklists delegated agents follow. |
skills/ | skills/<name> | Device-only | yes | Task 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-only | yes | Anchor 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/<id>/{pack,fixtures,rules/<slug>} | Device-only | never | Sigma 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/user-notices/ | host/user-notices/<code> | Device-only | yes | User-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> | Shared | yes | Shared partials, prompt archetypes, and diagnostics copy other units include. |
mcp_bindings/ | mcp_bindings/<id> | Device-only | yes | Typed projections of MCP tool results into rule facts — a JSON Schema plus field paths, no Go required. |
contributions/commands/ | contributions/commands/<stem> | Additive | yes | A 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. Declaring it is what puts it in Search or run and in Settings → General → Keyboard. |
contributions/menus/ | contributions/menus/<stem> | Additive | yes | Where an existing command appears — the Search or run list, 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/<stem> | Additive | yes | The 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/<stem> | Additive | yes | A 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/<stem> | Additive | yes | A 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/<stem> | Additive | yes | One 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/<stem> | Additive | yes | An MCP server 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
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.
Packs never ship processes. MCP server 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
| Surface | How it moves |
|---|---|
| Host contract (unit kinds, resolve rules, Binding/Anchor envelope, load shapes) | Declared as compatibility.extension_api |
| Prompt and guidance wording in stock packs | Ships 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 ids | Independent 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 has exactly category (tool, command, mcp, path, host, write_root, host_resource), pattern, and effect (ask or deny only). 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.
The description is the only part under a budget: one line of what and when. Write the body as steps a small model can execute. Pack bodies render through pongo2; project skill bodies are never templated. Skills use the open Agent Skills format.
Host content
host/bindings/, host/user-notices/, and host/detection-packs/ are resolve units (device-only). Other host/ paths are fixed-path operator configuration — surfaces, postures, anchors, budgets — and can affect every session. Prefer a workflow or rule when either is sufficient. The full path map is in extend.md.
Shared trees
shared/archetypes/ and shared/partials/ hold reusable include targets. See Prompts.
Desired state
Desired state and the exact lock travel together. Missing desired files mean all stock packs enabled.
| File | Scope |
|---|---|
{repo}/.paintedwolf/extensions.yaml | Project desired state (format: 1) — committed |
{repo}/.paintedwolf/extensions.lock.yaml | Project exact resolution (lock_format: 1) — committed |
~/.config/paintedwolf/extensions.yaml | Device desired state |
~/.config/paintedwolf/extensions.lock.yaml | Device exact resolution |
format: 1
packs:
- id: acme/triage
source: https://example.com/acme-triage.git
version: "^1.0.0"
enabled: true
disabled: []
own: {}
A sourced pack row needs exactly one of version, ref, or development: true. Merge: a duplicate pack id keeps the device package’s identity; the project row is an enablement selector. Project wins the own value. disabled is the union of both files.
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. 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 manages the project. The local API is /v1/extensions* — see the Host API. The full CLI is in the Reference.
Resolve diagnostics
Validation and the Extensions UI report stable codes — branch on the code, never the message:
Show all extension diagnostic codes
| Code | Meaning |
|---|---|
extension_api_incompatible | The pack’s compatibility.extension_api range does not cover this app’s extension contract. The pack stays installed and contributes nothing. |
requires_unmet | A pack listed under requires is missing or not contributing, so this pack is blocked. |
requires_scanners_unmet | The pack declares requires_scanners and the named scanner is not enabled and runnable. |
pack_disabled | The pack is set enabled false in desired state; none of its units are provided. |
pack_invalid | The pack has an error and contributes nothing. Other packs are unchanged. |
pack_integrity | Installed files do not match the lock. The pack contributes nothing. |
pack_needs_reload | A linked folder changed on disk. The pack still contributes; Reload from disk records the new files. |
unit_disabled | The unit id is listed under disabled in desired state and was omitted from the effective catalog. |
conflict | More than one enabled pack provides this unit id and no own entry names a winner — the unit is not loaded (fail closed). |
owned | A multi-provider collision was resolved by an own entry; the named pack’s body is effective. |
own_unknown_pack | An own entry names a pack that is not installed or enabled. |
platform_missing | The mandatory painted-wolf/platform pack is not contributing — session boot fails closed. |
mcp_binding_invalid | An mcp_bindings unit failed to load — bad schema, missing fields, or an unsupported transform. |
mcp_binding_duplicate_key | Two binding units declare the same fact key; bindings fail closed until the collision is resolved. |
tool_schema_invalid | A tools/schemas/<name> unit failed to load — a bad tool name stem or a schema document the loader rejects. |
tool_schema_missing | A 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_invalid | An 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_missing | A pack installed from a linked folder no longer has a readable source directory — restore the folder and Reload. |
skill_name_invalid | The skill’s name breaks the Agent Skills spec (empty, or over 64 characters). |
skill_name_mismatch | The name in the frontmatter does not match the folder the SKILL.md sits in. |
skill_frontmatter_invalid | The SKILL.md frontmatter could not be parsed, so the skill produced nothing. |
skill_field_missing | A required frontmatter field is absent — a skill needs a description. |
skill_field_invalid | A frontmatter field is present but outside its limit, such as a description over 1024 characters. |
skill_compatibility_invalid | The skill’s compatibility text is over the 500-character limit. |
skill_too_large | The SKILL.md body exceeds the 64 KiB host limit. |
skill_catalog_full | The 128-skill catalog cap was reached and the remaining project skills were set aside. Pack skills always keep their place. |
skill_shadowed | This project skill lost its id to an earlier project copy or to a device-resolved skill of the same name. |
skill_host_resources_invalid | The skill’s metadata.host_resources list is malformed or exceeds the 32-entry bound. |
skill_host_resources_unmet | The skill declares host resources that are not observed or not permitted here, so it stays out of the prompt index. |
project_scope_refused | A project tried to contribute a device-only kind (tools/**, skills, bindings, user notices, MCP bindings, detection packs). Project scope is additive-only against enforcement, so the unit is refused by id. |
project_own_refused | A project named an own winner for a unit its own packs do not provide — subtraction of device-resolved units stays device scope. |
project_effect_forbidden | A project-added policy/ unit declared an OAR effect outside block, warn, nudge. A repo may add asks, never auto-allow. |
desired_state_rejected | The 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_refused | Desired state named an own winner for a kind that refuses ownership at every scope — today that is detection packs, because replacing another pack’s rule is never the intended answer. |
detection_pack_invalid | A detection pack unit failed to load — a bad manifest, rule, or fixtures document — so that pack does not contribute matches. |
detection_pack_foreign_unit | A pack contributed a detection unit whose pack id belongs to another provider. Detection content is single-provider; the foreign unit is refused. |
detection_rehearsal_failed | A detection pack’s fixtures did not rehearse cleanly through the approval or connection adapter, so the pack is not trusted to match live events. |
unit_not_regular | The file behind this unit id is not a regular file (symlink, device, FIFO), so its bytes were not published. |
member_missing | A suite lists a member pack that is not installed, so the suite can never be complete until it is. |
member_disabled | A suite’s member pack is present but disabled in desired state — the suite reports partial rather than complete. |
suite_conflict | Two suites that declare each other under conflicts_with both have enabled members; disable one side. |
meta_compatibility | A suite’s compatibility range does not cover this app’s extension contract, so the suite is ignored. |
meta_invalid | A meta.yaml failed to parse or validate — unknown fields, missing id, or an unresolvable extends parent. |
meta_duplicate_id | Two discovered suites claim the same id; neither is trusted until the collision is resolved. |
extends_unresolved | A suite’s extends parent was never discovered, so the inherited membership cannot be computed. |
extends_co_resolve | A suite and the parent it extends provide the same unit id — resolving them together leaves the unit in conflict. |
Recipes
- Additive — ship new unit ids only; install, enable, validate. Stock stays fully effective.
- Swap — fork one thin pack plus a profile that disables the stock leaf; every other stock pack still updates with the app.
- Surgical own —
owna single unit id to your fork; the rest of the stock pack stays effective.