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.yamlmanifest_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.yamlid: 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
initial_posture: spec
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
coordinator_surface: orchestrate_plan
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: {}
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_completeThree things matter:
- Every phase names its surface.
coordinator_surface,surface_template, andmode_refsare 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: autoadvances 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’sguidance/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.yamloar: '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-lycaon-emit: rule:triage_lite
x-lycaon-message: Triage Lite is a read-only workflow.
x-lycaon-scenarios:
- id: default
vars:
tool: write
path: src/main.go
profile: coordinator
expect_contains:
- 'Rejected:'
- 'Code: TRIAGE_LITE_WRITE_FORBIDDEN'
- BranchThe filename is the rule id and the Code: an agent receives when it fires. when: is CEL over the closed fact catalogue; unknown facts fail at load. copy: supplies the rejection guidance, and x-lycaon-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-lycaon-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-lycaon-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 are pongo2 — Django-style tags, markdown body:
guidance/triage-lite-plan.mdTriage 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 --device
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 rather than copying it, so the pack you keep editing is the pack the app loads. Drop --device to install for the current project instead of the whole machine.
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 actually shipped
A template that fails to parse is an error. A template with a typo’d variable is not — it renders empty, and the only way to see that is to look:
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 want | Read |
|---|---|
| Surfaces, gates, and the generated manifest field list | Custom workflows |
| The fact catalogue and anchor list your rules select on | Open Agent Rules |
| Overlay one prompt file and add a phase inject | Prompts |
Shipping this to a team, and the provide − disabled ⊕ own algebra | Extension packs |
| Diagnostic codes, CLI reference, stability promises | Reference |