| name | pi-behavior-monitors |
| description | Behavior monitors that watch agent activity and steer corrections when issues are detected. Monitors are JSON files (.monitor.json) in .pi/monitors/ with classify, patterns, actions, and scope blocks. Patterns and instructions are JSON arrays. Use when creating, editing, debugging, or understanding behavior monitors.
|
<tools_reference>
List all behavior monitors with their current state.
List all behavior monitors with their current state
Inspect a monitor — config, state, pattern count, rule count.
Inspect a monitor — config, state, pattern count, rule count
| Parameter | Type | Required | Description |
|---|
monitor | string | yes | Monitor name |
Control monitors — enable, disable, dismiss, or reset.
Control monitors — enable, disable, dismiss, or reset
| Parameter | Type | Required | Description |
|---|
action | string | yes | |
monitor | string | no | Monitor name (required for dismiss/reset) |
Manage monitor rules — list, add, remove, or replace calibration rules.
Manage monitor rules — list, add, remove, or replace calibration rules
| Parameter | Type | Required | Description |
|---|
monitor | string | yes | Monitor name |
action | string | yes | |
text | string | no | Rule text (for add/replace) |
index | number | no | Rule index, 1-based (for remove/replace) |
List patterns for a behavior monitor.
List patterns for a behavior monitor
| Parameter | Type | Required | Description |
|---|
monitor | string | yes | Monitor name |
</tools_reference>
<commands_reference>
On-demand work quality analysis
Manage behavior monitors
Subcommands: on, off, help, commit-hygiene, fragility, hedge, unauthorized-action, work-quality
</commands_reference>
`session_start`, `agent_end`, `turn_start`, `tool_call`
<bundled_resources>
5 agents, 5 schemas, 21 examples bundled.
See references/bundled-resources.md for full inventory.
</bundled_resources>
<monitor_vocabulary>
Context Collectors:
| Collector | Placeholder | Description | Limits |
|---|
user_text | {user_text} / {{ user_text }} | Most recent user message text | — |
assistant_text | {assistant_text} / {{ assistant_text }} | Most recent assistant message text | — |
tool_results | {tool_results} / {{ tool_results }} | Tool results with tool name and error status | Last 5, truncated 2000 chars |
tool_calls | {tool_calls} / {{ tool_calls }} | Tool calls and results interleaved | Last 20, truncated 2000 chars |
custom_messages | {custom_messages} / {{ custom_messages }} | Custom extension messages since last user message | — |
project_vision | {project_vision} / {{ project_vision }} | /project.json vision, core_value, name | — |
project_conventions | {project_conventions} / {{ project_conventions }} | /conformance-reference.json principle names | — |
git_status | {git_status} / {{ git_status }} | Output of git status --porcelain | 5s timeout |
conversation_history | {conversation_history} / {{ conversation_history }} | Prior turn summaries (user request + actions + assistant response) | 1-3 turns adaptive, 2000 char max |
Any string is accepted in classify.context. Unknown collector names produce empty string.
Built-in placeholders (always available, not in classify.context):
{{ patterns }} — patterns JSON as numbered list
{{ instructions }} — instructions JSON as bulleted list with "follow strictly" preamble
{{ iteration }} — consecutive steer count (0-indexed)
When Conditions:
always — Fire every time the event occurs
has_tool_results — Fire only if tool results present since last user message
has_file_writes — Fire only if write or edit tool called since last user message
has_bash — Fire only if bash tool called since last user message
every(N) — Fire every Nth activation (counter resets when user text changes)
tool(name) — Fire only if specific named tool called since last user message
Events: message_end, turn_end, agent_end, command, tool_call
Verdict Types: clean, flag, new, error
Scope Targets: main, subagent, all, workflow
</monitor_vocabulary>
Monitors are autonomous watchdogs that observe agent activity, classify it against a
JSON pattern library using a side-channel LLM call, and either steer corrections or
write structured findings to JSON files for downstream consumption.
<monitor_locations>
Monitors are discovered from three tiers, checked in precedence order; the first match
by monitor.name wins:
- Project:
.pi/monitors/*.monitor.json (walks up from cwd to find .pi/, stops at the .git boundary)
- Global:
~/.pi/agent/monitors/*.monitor.json (via getAgentDir())
- Bundled:
<package>/examples/*.monitor.json (the five built-in defaults that ship with the extension)
When a higher-precedence tier shadows a same-name monitor in a lower tier, the override
is logged once at session_start so drift is visible. The extension silently exits if
zero monitors are discovered across all three tiers.
</monitor_locations>
To customize a bundled monitor, create `.pi/monitors/.monitor.json` (and its
`.patterns.json` and `.instructions.json` sidecars) in your project. The override fully
replaces the bundled version by name. Per-monitor template subdirectories (e.g.,
`fragility/classify.md`) follow the same multi-tier search via
`createMonitorAgentTemplateEnv`, so an override directory tree mirrors the bundled
layout.
Delete the override to revert to bundled behavior — bundled updates are picked up
automatically on the next session because tier 3 reads the package examples directly,
not a copied snapshot. This replaces the prior copy-on-first-run pattern that left
seeded .pi/monitors/ files frozen at the package version they were seeded from.
<file_structure>
Each monitor is a set of files sharing a name prefix:
.pi/monitors/
├── fragility.monitor.json # Monitor definition (classify + patterns + actions + scope)
├── fragility.patterns.json # Known patterns (JSON array, grows automatically)
├── fragility.instructions.json # User corrections (JSON array, optional)
├── fragility/
│ └── classify.md # Nunjucks classify template, referenced by the classifier agent spec
The instructions file is optional. If omitted, the extension defaults the path to
${name}.instructions.json and treats a missing file as an empty array.
The classify prompt is not declared in the monitor definition. classify.agent names an
.agent.yaml spec — resolved from .pi/agents/ (project), ~/.pi/agent/agents/ (user),
then the package agents/ directory — and that spec's prompt.task.template names the
Nunjucks classify template (e.g. fragility/classify.md). Templates are resolved through
a three-tier search: .pi/monitors/ (project), ~/.pi/agent/monitors/ (user), then the
package examples/ directory. A user overrides a bundled template by placing a file at
the same relative path in .pi/monitors/.
</file_structure>
<monitor_definition>
A .monitor.json file conforms to schemas/monitor.schema.json:
{
"name": "my-monitor",
"description": "What this monitor watches for",
"event": "message_end",
"when": "has_tool_results",
"scope": {
"target": "main",
"filter": { "agent_type": ["audit-fixer"] }
},
"classify": {
"agent": "my-monitor-classifier",
"context": ["tool_results", "assistant_text"],
"excludes": ["other-monitor"]
},
"patterns": {
"path": "my-monitor.patterns.json",
"learn": true
},
"instructions": {
"path": "my-monitor.instructions.json"
},
"actions": {
"on_flag": {
"steer": "Fix the issue.",
"write": {
"block": "issues",
"merge": "append",
"array_field": "issues",
"template": {
"id": "monitor-{finding_id}",
"description": "{description}",
"status": "open",
"category": "monitor",
"source": "monitor"
}
}
},
"on_new": {
"steer": "Fix the issue.",
"learn_pattern": true,
"write": { "...": "same as on_flag" }
},
"on_clean": null
},
"ceiling": 5,
"escalate": "ask"
}
</monitor_definition>
Top-level fields:
| Field | Default | Description |
|---|
name | (required) | Monitor identifier. Must be unique across project and global. |
description | "" | Human-readable description. Also used as command description for event: command monitors. |
event | message_end | When to fire: message_end, turn_end, agent_end, command, or tool_call. tool_call fires pre-execution and can block the tool. |
when | always | Activation condition (see below). |
ceiling | 5 | Max consecutive steers before escalation. |
escalate | ask | At ceiling: ask (confirm with user) or dismiss (silence for session). |
Scope block:
| Field | Default | Description |
|---|
scope.target | main | What to observe: main, subagent, all, workflow. |
scope.filter.agent_type | — | Only monitor agents with these names. |
scope.filter.step_name | — | Glob pattern for workflow step names. |
scope.filter.workflow | — | Glob pattern for workflow names. |
Steering (injecting messages into the conversation) only fires for main scope.
Non-main scopes can still write findings to JSON files.
Classify block:
| Field | Default | Description |
|---|
classify.agent | (required) | Name of an .agent.yaml spec that runs the classification. A model: pinned in the agent spec wins; when the spec omits model, classification runs on the session's currently selected model. |
classify.context | (required) | Context collector names. Any string accepted — unknown collectors produce empty string. |
classify.excludes | [] | Monitor names — skip activation if any of these already steered this turn. |
The classification prompt is not a monitor-definition field: the referenced agent spec's
prompt.task.template names the Nunjucks classify template (see the prompt templates
section below).
Actions block — per verdict (on_flag, on_new, on_clean):
| Field | Description |
|---|
steer | Message to inject into conversation. null = no steering. Only effective for scope.target: "main". |
write.block | Block name in the active substrate to write findings to (e.g. "issues"). block-api derives the path <substrate-dir>/<block>.json and the schema <substrate-dir>/schemas/<block>.schema.json. |
write.merge | append (add to array) or upsert (update by matching id field). |
write.array_field | Which field in target JSON holds the array (e.g. "issues", "findings"). |
write.template | Template mapping with {finding_id}, {description}, {severity}, {monitor_name}, {timestamp}. |
learn_pattern | If true, add new pattern to patterns file on new verdict. |
on_clean can be configured with a write action to log clean verdicts. Setting it to
null means no action on clean (the default behavior).
<patterns_file>
JSON array conforming to schemas/monitor-pattern.schema.json:
[
{
"id": "empty-catch",
"description": "Silently catching exceptions with empty catch blocks",
"severity": "error",
"category": "error-handling",
"examples": ["try { ... } catch {}"],
"source": "bundled"
},
{
"id": "learned-pattern-abc",
"description": "Learned pattern from runtime detection",
"severity": "warning",
"source": "learned",
"learned_at": "2026-03-15T02:30:00.000Z"
}
]
| Field | Required | Description |
|---|
id | yes | Stable identifier for dedup. Auto-generated for learned patterns: lowercased, non-alphanumeric replaced with hyphens, truncated to 60 chars. |
description | yes | What this pattern detects. Used for dedup (exact match) when learning. |
severity | no | "error", "warning", or "info". Defaults to "warning" in prompt formatting. |
category | no | Grouping key (e.g. "error-handling", "avoidance", "deferral"). |
examples | no | Example manifestations. Stored but not surfaced in classification prompts. |
source | no | "bundled", "learned", or "user". Learned patterns are tagged "learned". |
learned_at | no | ISO timestamp for learned patterns. |
Patterns grow automatically when learn_pattern: true and a NEW: verdict is returned.
Dedup is by exact description match — duplicates are silently skipped.
Critical: If the patterns array is empty (file missing, empty array, or unparseable),
classification is skipped entirely for that activation. A monitor with no patterns does nothing.
</patterns_file>
<instructions_file>
JSON array of user rules (called "instructions" on disk, "rules" in the command surface):
[
{ "text": "grep exit code 1 is not an error", "added_at": "2026-03-15T02:30:00.000Z" },
{ "text": "catch-and-log in event handlers is correct for non-critical extensions", "added_at": "2026-03-15T03:00:00.000Z" }
]
Manage via /monitors <name> rules (list), /monitors <name> rules add <text> (add),
/monitors <name> rules remove <n> (remove by number), /monitors <name> rules replace <n> <text>
(replace by number). The LLM can also edit the .instructions.json file directly.
Rules are injected into the classification prompt under a preamble
"Operating instructions from the user (follow these strictly):" — only if the array is
non-empty. An empty array or missing file produces no rules block in the prompt.
</instructions_file>
<prompt_templates>
The classification prompt is a Nunjucks .md template owned by the classifier agent spec:
classify.agent names an .agent.yaml, and that spec's prompt.task.template names the
template file (e.g. fragility/classify.md). The monitor definition itself carries no
prompt or template field. Full Nunjucks syntax is available: conditionals ({% if %}),
loops ({% for %}), includes ({% include %}), filters.
Template variables use {{ name }} syntax. Available: {{ patterns }}, {{ instructions }},
{{ iteration }}, plus any collectors listed in classify.context. tool_call monitors
additionally receive {{ tool_call_context }} (the pending tool name + arguments).
Iteration-aware acknowledgment pattern — templates should include this block to support
monitor-agent dialogue (the agent acknowledging a steer and stating a plan). The bundled
templates pull it in as a shared partial via
{% if iteration > 0 %}{% include "_shared/iteration-grace.md" %}{% endif %}; its content:
NOTE: You have steered {{ iteration }} time(s) already this session.
The agent's latest response is below. If the agent explicitly acknowledged
the issue and stated a concrete plan to address it (not just "noted" but
a specific action), reply CLEAN to allow the agent to follow through.
Re-flag only if the agent ignored or deflected the steer.
Agent response:
{{ assistant_text }}
This requires assistant_text in the classify.context array. When the classifier sees
genuine acknowledgment, it replies CLEAN, which resets whileCount to 0 and gives the agent
a fresh turn without re-flagging.
Template search order (first match wins):
.pi/monitors/<template-path> — project-level override
~/.pi/agent/monitors/<template-path> — user-level
- Package
examples/<template-path> — builtin
(Agent template directories .pi/templates/ and ~/.pi/agent/templates/ are appended to
the search path for shared macros.)
Agent spec search order (first match wins):
.pi/agents/<name>.agent.yaml — project-level
~/.pi/agent/agents/<name>.agent.yaml — user-level
- Package
agents/<name>.agent.yaml — builtin
All five bundled monitors ship with Nunjucks templates in examples/<name>/classify.md,
referenced by their classifier agent specs in agents/<name>-classifier.agent.yaml.
</prompt_templates>
<verdict_format>
The classifier agent spec declares output.format: json with output.schema pointing at
schemas/verdict.schema.json, and the classify call forces a structured verdict (phantom
verdict tool). The classification LLM responds with a JSON object:
{"verdict": "CLEAN"} — no issue detected. Resets consecutive steer counter to 0.
{"verdict": "FLAG", "description": "..."} — known pattern matched. Triggers on_flag action.
{"verdict": "NEW", "description": "...", "newPattern": "..."} — novel issue. newPattern becomes the learned pattern description; description becomes the finding description. Triggers on_new action.
An optional "severity" field (info/warning/critical) is accepted on FLAG/NEW verdicts.
Agent specs that declare no output schema fall back to legacy free-text parsing:
CLEAN, FLAG:<description>, or NEW:<pattern>|<description> (no | → the full text
is used for both). A response matching neither form yields an error verdict — no action fires.
Classification calls use maxTokens: 1024.
</verdict_format>
<runtime_behavior>
Dedup: A monitor will not re-classify the same user text. Once a user message has been
classified, the monitor skips until the user text changes. This prevents redundant
side-channel LLM calls within the same user turn.
Ceiling and escalation: After ceiling consecutive steers (flag/new verdicts without
an intervening clean), the monitor escalates. With escalate: "ask", the user is prompted
to continue or dismiss. With escalate: "dismiss", the monitor is silently dismissed for
the session. A CLEAN verdict resets the consecutive steer counter.
Turn exclusion: The excludes array prevents double-steering. If monitor A steers in
a turn, and monitor B has "excludes": ["A"], monitor B skips that turn. Exclusion tracking
resets at turn_start.
Buffered steer delivery: Monitors on message_end or turn_end buffer their steer
messages and deliver them at agent_end. This is because pi's async event queue processes
extension handlers after the agent loop has already checked for steering messages. The
buffer is drained at agent_end — only the first buffered steer fires per agent run; the
corrected response re-triggers monitors naturally for any remaining issues. Monitors on
agent_end or command events deliver steers immediately (they already run post-loop).
Abort: Classification calls are aborted when the agent ends (via agent_end event).
Aborted classifications produce no verdict and no action.
Pre-execution blocking (tool_call): Monitors with event: "tool_call" fire before a
tool executes. The handler receives the pending tool name and arguments, classifies them,
and can return { block: true, reason } to prevent execution. This is fundamentally
different from post-hoc steering — the tool never runs. Classification failure is fail-open:
if the side-channel LLM call errors, the tool is allowed to proceed. The pending tool call
context (name + arguments) is available as {{ tool_call_context }} in classify templates.
Dedup is not applied to tool_call monitors since each tool call is a distinct event.
Write action: write.block names a block in the active substrate; the write goes
through block-api (append or upsert), which derives the target path, validates the entry
against the block's schema, and stamps author fields with monitor/<name> when the
destination schema declares them. The upsert merge strategy matches on the id field of
array entries. A failed write (schema mismatch, missing block) is logged and does not crash
the host turn.
</runtime_behavior>
All monitor management is through the `/monitors` command. Subcommands are
discoverable via pi's TUI autocomplete — typing `/monitors ` shows available
monitor names and global commands; selecting a monitor shows its verbs.
| Command | Description |
|---|
/monitors | List all monitors with global on/off state and per-monitor status |
/monitors on | Enable all monitoring (session default) |
/monitors off | Pause all monitoring for this session |
/monitors <name> | Inspect a monitor: description, event, state, rule count, pattern count |
/monitors <name> rules | List current rules (numbered) |
/monitors <name> rules add <text> | Add a rule to calibrate the classifier |
/monitors <name> rules remove <n> | Remove a rule by number |
/monitors <name> rules replace <n> <text> | Replace a rule by number |
/monitors <name> patterns | List current patterns (numbered, with severity and source) |
/monitors <name> dismiss | Dismiss a monitor for this session |
/monitors <name> reset | Reset a monitor's state and un-dismiss it |
Monitors with event: "command" also register /<name> as a programmatic trigger
for other extensions or workflows to invoke classification directly.
<bundled_monitors>
Five example monitors ship in examples/ and load directly from the package as the
third discovery tier. Each has a
Nunjucks classify template in examples/<name>/classify.md with iteration-aware
acknowledgment support:
fragility (agent_end, when: has_tool_results)
Watches for unaddressed fragilities after tool use — errors, warnings, or broken state the
agent noticed but chose not to fix. Steers with "Fix the issue you left behind." Writes
findings to the issues block under category: "fragility". Excludes: none. Ceiling: 5.
12 bundled patterns across categories: avoidance (dismiss-preexisting, not-my-change,
blame-environment, workaround-over-root-cause, elaborate-workaround-for-fixable),
error-handling (empty-catch, happy-path-only, early-return-on-unexpected,
undocumented-delegation, silent-fallback), deferral (todo-instead-of-fix,
prose-without-action).
hedge (agent_end, when: always)
Detects when the assistant deviates from what the user actually said — substituting
questions, projecting intent, or deflecting instead of answering. Steers with "Address
what the user actually said." Does not write to files (steer-only). Excludes: none.
Ceiling: 3.
8 bundled patterns across categories: substitution (rephrase-question, reinterpret-words),
projection (assume-intent, attribute-position), augmentation (add-questions),
deflection (ask-permission, qualify-yesno, counter-question).
work-quality (command, when: always)
On-demand work quality analysis invoked via /work-quality. Analyzes user request, tool
calls, and assistant response for quality issues. Writes findings to the issues block
under category: "work-quality". Ceiling: 3.
11 bundled patterns across categories: methodology (trial-and-error, symptom-fix,
double-edit, edit-without-read, insanity-retry, no-plan), verification (no-verify),
scope (excessive-changes, wrong-problem), quality (copy-paste), cleanup (debug-artifacts).
commit-hygiene (agent_end, when: has_file_writes)
Fires when the agent finishes a turn that included file writes. Checks tool call history
for git commit commands. If no commit occurred, steers to commit. If committed with a
generic or certainty-language message, steers to improve. Does not write findings — commits
are their own artifact. Ceiling: 3.
6 bundled patterns across categories: missing-commit (no-commit), message-quality
(generic-message, certainty-language, no-context), commit-safety (amend-not-new, force-push).
unauthorized-action (tool_call, when: has_tool_results)
Pre-execution blocker that classifies each pending tool call against user-directed scope.
If the agent is about to execute a tool the user did not direct, the monitor blocks the
tool call before execution rather than steering correction after the fact. Uses tool_call
event with { block: true, reason } return to prevent execution. Ceiling: 3.
</bundled_monitors>
<disabling_monitors>
Session-level (temporary):
/monitors off — pauses all monitoring for the current session
/monitors <name> dismiss — silences a single monitor for the session
/monitors <name> reset — un-dismisses and resets a monitor's state
Permanent:
- Delete its
.monitor.json file (and optionally its .patterns.json and .instructions.json)
- Or empty its patterns array — a monitor with zero patterns skips classification entirely
- To disable all monitoring: remove all
.monitor.json files from .pi/monitors/ and
~/.pi/agent/monitors/. The extension exits silently when zero monitors are discovered.
Monitors also auto-silence at their ceiling. With escalate: "ask", the user is prompted
to continue or dismiss. With escalate: "dismiss", the monitor silences automatically.
</disabling_monitors>
<creating_monitors>
When the user asks to create a monitor — either from a described behavior ("flag responses
that end with questions") or from a discovered need during conversation ("that response
did X wrong, make a monitor for it") — follow this workflow:
Step 1: Determine the detection target. What specific behavior in the assistant's output
should trigger a flag? Translate the user's description into concrete, observable patterns.
Step 2: Choose event and when. Match the detection target to the right trigger:
- Response content issues (trailing questions, lazy options, tone) →
turn_end, when: always
- Tool use issues (no commit, no test, bad edits) →
agent_end, when: has_file_writes or has_tool_results
- Post-action fragility (ignoring errors) →
message_end, when: has_tool_results
- Pre-execution blocking (unauthorized actions, dangerous commands) →
tool_call, when: has_tool_results or always
- On-demand analysis →
command, when: always
Step 3: Choose context collectors. What data does the classifier need to see?
- Checking the assistant's final response text →
assistant_text
- Checking what the user asked (to compare against response) →
user_text
- Checking what tools were called →
tool_calls
- Checking tool outputs for errors/warnings →
tool_results
- Checking git state →
git_status
- Include
assistant_text if you want iteration-aware acknowledgment (recommended).
Step 4: Write the patterns file. Each pattern is a specific, observable anti-pattern.
Write descriptions that a classifier LLM can match against the collected context. Start with
3-8 seed patterns. Set learn: true so the monitor grows its pattern library from NEW:
verdicts at runtime.
Step 5: Write the classify template. A Nunjucks .md file. The template must:
- Present the collected context to the classifier
- List the patterns to check against
- Include the JSON verdict format instructions (CLEAN/FLAG/NEW)
- Include the iteration-aware acknowledgment block if
assistant_text is collected
Step 6: Write the classifier agent spec. An .agent.yaml in .pi/agents/ whose name
matches classify.agent. It declares output.format: json, output.schema: ../schemas/verdict.schema.json (relative schema paths resolve against the package agents/
directory), and prompt.task.template pointing at the classify template.
Step 7: Write the monitor definition. Wire everything together in the .monitor.json.
Step 8: Create empty instructions file. Write [] so the user can add calibration
rules via /monitors <name> rules add <text>.
Step 9: Activate. After creating the files, tell the user to run /reload 3 to
reload extensions and activate the new monitor without restarting the session.
Example: response-mandates monitor
User says: "create a monitor that flags responses ending with questions and responses
that present lazy deferral options."
Files to create:
.pi/monitors/response-mandates.monitor.json:
{
"name": "response-mandates",
"description": "Flags responses that violate communication mandates: trailing questions, lazy deferral options, non-optimal solutions",
"event": "turn_end",
"when": "always",
"scope": { "target": "main" },
"classify": {
"agent": "response-mandates-classifier",
"context": ["assistant_text", "user_text"],
"excludes": ["fragility"]
},
"patterns": { "path": "response-mandates.patterns.json", "learn": true },
"instructions": { "path": "response-mandates.instructions.json" },
"actions": {
"on_flag": { "steer": "Rewrite your response: report findings and state actions — do not end with a question or present options that defer proper work." },
"on_new": { "steer": "Rewrite your response: report findings and state actions — do not end with a question or present options that defer proper work.", "learn_pattern": true },
"on_clean": null
},
"ceiling": 3,
"escalate": "ask"
}
.pi/agents/response-mandates-classifier.agent.yaml:
name: response-mandates-classifier
role: sensor
description: Classifies responses against communication mandates
output:
format: json
schema: ../schemas/verdict.schema.json
prompt:
task:
template: response-mandates/classify.md
.pi/monitors/response-mandates/classify.md:
The user said:
"{{ user_text }}"
The assistant's response:
"{{ assistant_text }}"
{{ instructions }}
Check the assistant's response against these anti-patterns:
{{ patterns }}
Specifically check:
1. Does the response end with a question to the user? The final sentence or paragraph
should not be a question unless the user explicitly asked to be consulted. Rhetorical
questions, permission-seeking ("shall I...?", "would you like...?"), and steering
questions ("what do you think?") are all violations.
2. Does the response present options where one or more options leave known issues
unaddressed? If a problem has been identified, every option presented must address it.
Options that defer proper work to a vague future ("we could address this later",
"for now we can...") are violations.
3. Does the response propose a non-durable solution when a durable one is known? Workarounds,
temporary fixes, and partial solutions when the root cause is understood are violations.
{% if iteration > 0 %}
NOTE: You have steered {{ iteration }} time(s) already this session.
If the agent explicitly acknowledged the mandate violation and rewrote its response
without the violation, reply CLEAN. Re-flag only if the violation persists.
Agent response:
{{ assistant_text }}
{% endif %}
Respond with a JSON object:
- {"verdict": "CLEAN"} if the response follows all mandates.
- {"verdict": "FLAG", "description": "one sentence describing the violation"} if a known pattern was matched.
- {"verdict": "NEW", "description": "one sentence describing the violation", "newPattern": "pattern description"} if a violation not covered by existing patterns was detected.
.pi/monitors/response-mandates.patterns.json:
[
{ "id": "trailing-question", "description": "Response ends with a question to the user instead of reporting and acting", "severity": "error", "category": "communication", "source": "bundled" },
{ "id": "permission-seeking", "description": "Asks permission before acting when the user has already given direction", "severity": "warning", "category": "communication", "source": "bundled" },
{ "id": "steering-question", "description": "Ends with 'what do you think?', 'does that sound right?', or similar steering questions", "severity": "error", "category": "communication", "source": "bundled" },
{ "id": "lazy-deferral", "description": "Presents options that defer known issues to a vague future ('we can address later', 'for now')", "severity": "error", "category": "anti-laziness", "source": "bundled" },
{ "id": "fragility-tolerant-option", "description": "Offers an option that leaves identified fragility unaddressed", "severity": "error", "category": "anti-laziness", "source": "bundled" },
{ "id": "workaround-over-fix", "description": "Proposes workaround when root cause is understood and fixable", "severity": "warning", "category": "anti-laziness", "source": "bundled" }
]
.pi/monitors/response-mandates.instructions.json:
[]
After creating all files, tell the user: "Monitor created. Run /reload 3 to activate
it in this session."
</creating_monitors>
<modifying_monitors>
Adding patterns — When the user identifies a new anti-pattern during conversation
("that kind of response should also be flagged"), add it to the patterns JSON file.
Each pattern needs id, description, severity, and source: "user".
Adding rules — Use the monitors-rules tool or /monitors <name> rules add <text>
to add calibration rules. Rules fine-tune the classifier without changing patterns.
Example: "responses that end with 'let me know' are not questions."
Changing the classify prompt — Edit the Nunjucks .md template named by the classifier
agent spec's prompt.task.template. To customize a bundled monitor's prompt, place an
override template at the same relative path in .pi/monitors/ (e.g.
.pi/monitors/fragility/classify.md) — it shadows the bundled one via the template search
order. To change the classifier itself (output schema, template path, model pin), override
the .agent.yaml in .pi/agents/.
Adjusting sensitivity — Lower the ceiling to escalate sooner if the monitor is
over-firing. Raise it to give the agent more chances. Set escalate: "dismiss" to
auto-silence without prompting.
After any file changes, tell the user to run /reload 3 to pick up the changes.
</modifying_monitors>
<success_criteria>
- Monitor
.monitor.json validates against schemas/monitor.schema.json
- Patterns
.patterns.json validates against schemas/monitor-pattern.schema.json
- Patterns array is non-empty (empty patterns = monitor does nothing)
classify.agent resolves to an .agent.yaml in the agent search path, and that spec's prompt.task.template file exists in one of the template search directories
- Classify template includes
{{ patterns }} and the JSON verdict format instructions (CLEAN/FLAG/NEW)
assistant_text is in classify.context when the template uses iteration-aware acknowledgment
- Actions specify
steer for scope.target: "main" monitors, write for findings output
write.block names a block in the active substrate whose schema accepts the templated entry
excludes lists monitors that should not double-steer in the same turn
- Instructions file exists (even if empty
[]) to enable /monitors <name> rules add <text> calibration
- After creating or modifying monitor files, remind user to run
/reload 3
</success_criteria>
Generated from source by scripts/generate-skills.js — do not edit by hand.