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:

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/
  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:

ClassProject may add unitsProject may disabled / ownTypical use
Sharedyesyes — any unit idWorkflows, agents, guidance the whole team should share
Additiveyes (effect-checked for policy and approval rules)only units its own packs provideRules, playbooks, contributions that can tighten, not loosen, the floor
Device-onlynonoTools, 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:

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/<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/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/<stem>AdditiveyesA 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>AdditiveyesWhere 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>AdditiveyesThe 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>AdditiveyesA 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>AdditiveyesA 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>AdditiveyesOne 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>AdditiveyesAn 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

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

FileScope
{repo}/.paintedwolf/extensions.yamlProject desired state (format: 1) — committed
{repo}/.paintedwolf/extensions.lock.yamlProject exact resolution (lock_format: 1) — committed
~/.config/paintedwolf/extensions.yamlDevice desired state
~/.config/paintedwolf/extensions.lock.yamlDevice 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
CodeMeaning
extension_api_incompatibleThe pack’s compatibility.extension_api range does not cover this app’s extension contract. The pack stays installed and contributes nothing.
requires_unmetA pack listed under requires is missing or not contributing, so this pack is blocked.
requires_scanners_unmetThe pack declares requires_scanners and the named scanner is not enabled and runnable.
pack_disabledThe pack is set enabled false in desired state; none of its units are provided.
pack_invalidThe pack has an error and contributes nothing. Other packs are unchanged.
pack_integrityInstalled files do not match the lock. The pack contributes nothing.
pack_needs_reloadA linked folder changed on disk. The pack still contributes; Reload from disk records the new files.
unit_disabledThe unit id is listed under disabled in desired state and was omitted from the effective catalog.
conflictMore than one enabled pack provides this unit id and no own entry names a winner — the unit is not loaded (fail closed).
ownedA multi-provider collision was resolved by an own entry; the named pack’s body is effective.
own_unknown_packAn own entry names a pack that is not installed or enabled.
platform_missingThe mandatory painted-wolf/platform pack is not contributing — session boot fails closed.
mcp_binding_invalidAn mcp_bindings unit failed to load — bad schema, missing fields, or an unsupported transform.
mcp_binding_duplicate_keyTwo binding units declare the same fact key; bindings fail closed until the collision is resolved.
tool_schema_invalidA tools/schemas/<name> unit failed to load — a bad tool name stem or a schema document the loader rejects.
tool_schema_missingA 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_invalidAn 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_missingA pack installed from a linked folder no longer has a readable source directory — restore the folder and Reload.
skill_name_invalidThe skill’s name breaks the Agent Skills spec (empty, or over 64 characters).
skill_name_mismatchThe name in the frontmatter does not match the folder the SKILL.md sits in.
skill_frontmatter_invalidThe SKILL.md frontmatter could not be parsed, so the skill produced nothing.
skill_field_missingA required frontmatter field is absent — a skill needs a description.
skill_field_invalidA frontmatter field is present but outside its limit, such as a description over 1024 characters.
skill_compatibility_invalidThe skill’s compatibility text is over the 500-character limit.
skill_too_largeThe SKILL.md body exceeds the 64 KiB host limit.
skill_catalog_fullThe 128-skill catalog cap was reached and the remaining project skills were set aside. Pack skills always keep their place.
skill_shadowedThis project skill lost its id to an earlier project copy or to a device-resolved skill of the same name.
skill_host_resources_invalidThe skill’s metadata.host_resources list is malformed or exceeds the 32-entry bound.
skill_host_resources_unmetThe skill declares host resources that are not observed or not permitted here, so it stays out of the prompt index.
project_scope_refusedA 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_refusedA project named an own winner for a unit its own packs do not provide — subtraction of device-resolved units stays device scope.
project_effect_forbiddenA project-added policy/ unit declared an OAR effect outside block, warn, nudge. A repo may add asks, never auto-allow.
desired_state_rejectedThe 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_refusedDesired 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_invalidA detection pack unit failed to load — a bad manifest, rule, or fixtures document — so that pack does not contribute matches.
detection_pack_foreign_unitA pack contributed a detection unit whose pack id belongs to another provider. Detection content is single-provider; the foreign unit is refused.
detection_rehearsal_failedA 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_regularThe file behind this unit id is not a regular file (symlink, device, FIFO), so its bytes were not published.
member_missingA suite lists a member pack that is not installed, so the suite can never be complete until it is.
member_disabledA suite’s member pack is present but disabled in desired state — the suite reports partial rather than complete.
suite_conflictTwo suites that declare each other under conflicts_with both have enabled members; disable one side.
meta_compatibilityA suite’s compatibility range does not cover this app’s extension contract, so the suite is ignored.
meta_invalidA meta.yaml failed to parse or validate — unknown fields, missing id, or an unresolvable extends parent.
meta_duplicate_idTwo discovered suites claim the same id; neither is trusted until the collision is resolved.
extends_unresolvedA suite’s extends parent was never discovered, so the inherited membership cannot be computed.
extends_co_resolveA 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 ownown a single unit id to your fork; the rest of the stock pack stays effective.