
Photo by Jan Huber on Unsplash.
Most of my agentic coding follows a simple plan and execute loop, where a stronger model and I work out the plan together and then a cheap model like deepseek-flash carries it out. It works better than I expected, because the plan spells out the things cheap models tend to skip, like writing the tests. Even so, no plan covers everything, and an executor deep in a task still lets things slip.
The obvious fix is a review pass after the work is done, and that works, but it’s one more step to remember and by then the problems are already in the code. I once had a task run for an hour, and when I reviewed it the agent had wandered far past what I asked for, something I only found out after the hour was gone.
What I wanted was a nudge while changing course is still easy, and in Oh My Pi that’s exactly what an advisor does. A second model watches the coding agent work and speaks up when something slips, with no separate review turn.
My last post gave the advisor role one line, and it has earned more than that. Advisors are an Oh My Pi feature that upstream pi doesn’t ship.
What an advisor is
An advisor is a separate agent with its own model, context, and isolated tool session. After each update from the primary agent, it receives only the new transcript delta: the agent’s reasoning, its tool calls, and the results, with secrets obfuscated. It also gets your project’s AGENTS.md context. By default it can investigate with read, grep, and glob.
It never approves anything and never changes the primary’s state directly. It talks back through one tool, advise, and every note carries a severity:
| Severity | What happens |
|---|---|
nit | A non-interrupting aside, delivered at the next turn boundary. |
concern | Delivered when the primary yields. Wakes it if it stopped mid-work. If it already gave its final answer, the note shows up as a card instead. |
blocker | Can interrupt work in progress, and can wake a turn that already ended with a final answer. |
OMP wraps each note in an advisory tag that tells the primary to weigh the advice and not blindly obey it.
The built-in system prompt tells an advisor to stay quiet. It says “Silence preferred when agent on track”, and an emission guard drops content-free notes like lgtm and nothing to add, along with duplicates.
It is not passive about drift, though. The same prompt tells the advisor to “Enforce user ask; flag drift immediately” and to advise before wrong-direction work. It objects when a bounded request gains features nobody asked for. A big diff alone is not a problem to it.
The idea isn’t unique to OMP. Anthropic’s advisor tool pairs a faster executor with a stronger advisor model, Claude Code exposes it as /advisor, and The advisor strategy explains the thinking behind it. The difference is who starts the conversation. There, the executor decides when to consult the advisor, so a model that might skip something has to know it needs help. In OMP the advisor watches every update and speaks up on its own.
Turning it on
You need an advisor role and the enable flag. This is straight from my config:
modelRoles:
advisor: anthropic/claude-opus-5-5:auto
advisor:
enabled: true
Point it at something stronger than whatever does your executing.
The :auto suffix makes the advisor follow the primary’s resolved thinking effort. With no suffix, it runs at medium.
Inside a session:
/advisortoggles it for the current session, and/advisor on|offsets it explicitly. Neither changes your saved config./advisor statusshows state, model, context size, tokens, and cost./advisor dumpcopies the advisor’s transcript to your clipboard./advisor dump rawadds the system prompt, tool calls, and thinking./advisor configureopens an editor forWATCHDOG.yml, which we’ll get to.omp -p --advisor "…"turns it on for a headless run.
What it caught
I had deepseek-flash working through a build plan for a Django service, with Opus 5.5 as the advisor. Earlier in the session the advisor had flagged a real bug in an ingest gate, and Flash fixed it, wrote a regression test for it, ran the full suite (1,020 tests green), committed, pushed, and reported the fix. A review after the fact would have seen a fix, a new test, and a green suite, and moved on.
The advisor had read the test. Concerns wait for the primary to yield, so this note landed right after the “pushed, tree clean” report:
<advisory severity="concern" guidance="weigh, don't blindly obey">
That regression test doesn't exercise the regression: it drives `record_event` (relay), whose `mapped` dict never includes `schema_version`, so it passes with the old buggy gate too. Test the backfill path instead — `ingest_events(team=..., events=[parse_envelope({... "schema_version": 1 ...})], source=IngestSource.BACKFILL)` — and assert it's accepted with `schema_version` stored as None.
</advisory>
The test drove a code path that strips the very field the bug was about, so it passed before the fix and after it. Flash agreed (“Sharp catch”), rewrote the test to drive the backfill path, then temporarily restored the old gate to prove the new test fails against it, and amended the commit. Two minutes after the note landed, the push carried a test that actually guards the fix. Flash’s summary opened with “The advisory was right, and it mattered”, and then put the lesson better than I could:
A test that cannot fail is worse than no test, because it certifies the behaviour.
That is what shifting review left buys. The problem was caught while the executor still had the whole change in context, and fixing it cost a couple of minutes instead of a round trip through me.
It catches plain bugs too. In another session, Flash wrote a test that resending a failed webhook should raise CommandError. The test failed, and Flash started digging through dj-stripe’s transaction internals for the reason. The advisor sent a blocker, which interrupted the dig: the command’s nested dispatchers called their handlers without return, so the failure string was dropped and failure was always None. Flash’s reply was “The advisory is right, and it’s a simpler explanation than my transaction investigation.” Three returns later the test passed unchanged.
Steering advisors with WATCHDOG.md
WATCHDOG.md is guidance for advisors only. OMP appends it to every advisor’s system prompt, including the ones you define in WATCHDOG.yml below, and the primary agent never sees it. That makes it the right place for review priorities you don’t want cluttering your AGENTS.md.
OMP loads every file it finds, in this order:
~/.omp/agent/WATCHDOG.mdWATCHDOG.mdand.omp/WATCHDOG.mdin each directory from your cwd up to the git root
It doesn’t stop at the nearest one. Narrower files go later in the prompt. Here’s what I’d put in a Rust project’s .omp/WATCHDOG.md:
- Watch for tests that mock the thing under test.
- Flag `unwrap()` and `expect()` in library code.
- Call out any "done" claim with no build or test output behind it.
- Ignore formatting and naming. The linter owns those.
Building your own advisors with WATCHDOG.yml
WATCHDOG.md shapes what advisors watch for. WATCHDOG.yml (or WATCHDOG.yaml) decides which advisors run. It uses the same search path, and each entry is a separate agent with its own focus:
| Field | What it does |
|---|---|
name | Required. Shows up in notes and status. |
model | Optional. Falls back to the advisor role if omitted. |
tools | Optional list. Defaults to read, grep, glob. [] grants none. |
instructions | Optional. Extra guidance for this advisor only. |
enabled | Optional. Set false to park an entry. |
maxNotesPerUpdate | Optional. Caps non-blocker notes per review. Defaults to 4. |
reviewMode | Optional. turn (default) reviews every turn. agent-end reviews only when the agent yields. |
reviewInterval | Optional. Review every Nth update. Defaults to 1. |
syncBacklog | Optional. How long the primary waits for this advisor to catch up: off, 1, 3, 5, or strict. Inherits advisor.syncBacklog, which defaults to off. |
The top level takes its own instructions, maxNotesPerUpdate, and the advisors list. The top-level instructions are shared by every advisor and concatenated across files. If two files define an advisor with the same name, the file closest to your working directory wins.
Here is the gotcha. Once a roster exists, the default advisor stops running. Only the entries you list run. To keep the generalist, add it back as an entry. With no model, it uses your advisor role:
advisors:
- name: General
Each entry can set its own model. A narrow focus doesn’t need your strongest model, so a mid-tier reviewer can own one concern while a stronger advisor reviews the work as a whole. OMP writes each advisor’s transcript next to the session as __advisor.<slug>.jsonl.
You can grant an advisor any built-in tool, including edit, write, bash, and eval. They still honor approval mode, but I’d keep reviewers read-only until there’s a reason not to.
Giving an advisor skills
For the Rust reviewer I use two skills I already had installed: rust-best-practices, based on Apollo GraphQL’s Rust handbook, and rust-testing. They’re written-down standards a cheap executor may never open. Pointing the reviewer at them means it checks the work against specific rules instead of its own taste.
Loading the skills into the executor helps too, but it’s a different job. In my Rust demo below, the executor had the skill installed and never opened it, while the reviewer was told to read it and did. The advisor’s one job is to check what just happened against the rules, with fresh context and a stronger model, and a model that skipped something is unlikely to notice it skipped it.
There’s no skills: field in WATCHDOG.yml, but there are two ways to get the same effect.
Lazy skill:// reads
This is the option I recommend. The advisor shares your session’s skills, so it can read a skill:// URL on demand. It doesn’t know which skills exist, so your instructions have to name them:
instructions: |
Review for correctness and verification, not style.
advisors:
- name: General
- name: Rust Reviewer
model: anthropic/claude-sonnet-5-5:medium
instructions: |
You review Rust changes. Before your first advise on any Rust code,
read skill://rust-best-practices. When tests are added, changed, or
missing for new public functions, also read skill://rust-testing.
Cite the specific skill rule your note is based on.
The skill only lands in the advisor’s context when it’s relevant. The last line matters most to me, because a note that cites its rule is one I can check. I can look up the rule and tell a real catch from a false alarm, which matters when a stronger model is second-guessing a cheaper one.
@ imports
The alternative is to inline the skill with an @ import in the instructions:
advisors:
- name: Rust Reviewer
model: anthropic/claude-sonnet-5-5:medium
instructions: |
You review Rust changes against these standards.
@~/.agents/skills/rust-best-practices/SKILL.md
Two details to watch. A plain YAML value can’t start with @, so use a | block scalar. And an import written inside a fenced code block in the instructions text stays literal, so keep the path outside any fence. The import also inlines the whole file, frontmatter included, into every review prompt. For my skills that’s 4.4 KB for rust-best-practices and 11.8 KB for rust-testing.
Which one
Imports are deterministic, so the standards are in every review prompt. Lazy reads keep review prompts small and scale to a long list of skills, but two things can go wrong. The advisor has to follow the instruction to read them, and by default OMP evicts old read, grep, and glob results from the advisor’s context (advisor.evictStaleResults), so a skill read early on gets swapped for a placeholder a review or two later. In my Rust demo runs the advisor read the skill once, on its first review, and never again, which is fine for short tasks. For long sessions, tell the reviewer to re-read the skill before any note that cites it, or use an import. I use lazy reads and name the skills explicitly.
The Rust reviewer in action
I tested this in a throwaway git repo with only the Rust Reviewer entry from the WATCHDOG.yml above, no General. The primary was deepseek-v4-pro at xhigh and the reviewer was claude-sonnet-5-5 at medium, so this run shows the skill read and the note, not a cheap-versus-strong gap. Anthropic’s docs note that the benefit shrinks as the executor’s capability approaches the advisor’s.
My first two prompts dictated the implementation, with s.parse().unwrap() and no tests. Both times the advisor read skill://rust-best-practices and then stayed silent, reasoning that the user had asked for exactly that code, which matches the built-in prompt’s “NEVER nitpick what user accepts.”
For the third run I asked for a pub fn parse_port(s: &str) -> u16 that parses a TCP port number, giving only the signature, and the primary wrote one that panics on bad input. As before, the advisor read the skill first, and this time it sent one note, which landed in the primary’s transcript like this:
<advisory advisor="Rust Reviewer" severity="nit" guidance="weigh, don't blindly obey">
rust-best-practices (Error Handling): "avoid panic!/never expect() outside tests; return Result". The mandated `-> u16` forces a panic or sentinel here, so keep the `# Panics` doc. In your "done" reply, say that invalid input panics and that `Result<u16, _>` would be the idiomatic signature. Don't change the signature unasked.
</advisory>
The citation took me to the right section of the skill in seconds, which is why I ask for it, even though the quote merges two of its bullets. The note never reached the primary, though. It’s a nit, which never interrupts, and by default the primary doesn’t wait for advisors (advisor.syncBacklog: off), so the review landed after the primary replied done. and print mode exited without starting another turn. The agent-end and strict settings from the table above close that gap, and they’re what I used next.
Adding a security reviewer
One focused reviewer is useful, but the roster really pays off when several run at once. Each entry is its own agent, so a security reviewer can read a security rulebook without crowding the Rust reviewer’s context, and each one can be told to leave the other’s lane alone.
For the security reviewer’s rules I picked two of Trail of Bits’ skills. sharp-edges (11.8 KB) looks for APIs and configurations where the easy path is the insecure one, like dangerous defaults and silent failures, and has per-language references, including one for Rust. differential-review (7.4 KB) reviews a change for security using Risk Level Triggers and a list of Red Flags, but its full workflow also runs git blame and writes a report file, which a read-only reviewer can’t do, so I told the reviewer to skip those steps.
Both skills live under the repo’s plugins/ directory, and I copied them into ~/.agents/skills/, where OMP already finds my Rust skills:
git clone --depth 1 https://github.com/trailofbits/skills /tmp/tob-skills
cp -R /tmp/tob-skills/plugins/sharp-edges/skills/sharp-edges ~/.agents/skills/sharp-edges
cp -R /tmp/tob-skills/plugins/differential-review/skills/differential-review ~/.agents/skills/differential-review
This is the WATCHDOG.yml from my demo:
instructions: |
Review for correctness and verification, not style.
Before deciding whether to advise, read the files the agent changed.
advisors:
- name: Rust Reviewer
model: anthropic/claude-sonnet-5-5:medium
reviewMode: agent-end
syncBacklog: strict
instructions: |
You review Rust changes. Before your first advise on any Rust code,
read skill://rust-best-practices. When tests are added, changed, or
missing for new public functions, also read skill://rust-testing.
Cite the specific skill rule your note is based on, and use severity
concern for any rule violation.
Leave security issues to the Security Reviewer.
- name: Security Reviewer
model: anthropic/claude-sonnet-5-5:medium
reviewMode: agent-end
syncBacklog: strict
instructions: |
You review changes for security only: secrets, authentication,
input handling, unsafe defaults, and APIs that are easy to misuse.
When a change touches any of these, read skill://sharp-edges and
skill://sharp-edges/references/lang-rust.md before deciding
whether to advise. When a change adds or weakens an auth or
validation check, also read skill://differential-review and apply
its Risk Level Triggers and Red Flags. Skip its git blame and
report-file steps.
Cite the specific skill rule your note is based on, and use severity
concern for any finding.
Leave general Rust style and testing to the Rust Reviewer.
Both entries pair reviewMode: agent-end with syncBacklog: strict, so the primary waits for both final reviews instead of exiting before a note lands, as it did above. A batch with an agent-end concern can then ask for one more turn, but a batch of nits can’t, which is why both reviewers are told to use concern.
Running both
I ran it in a fresh throwaway repo in print mode, like the Rust demo, with claude-haiku-5-5, Anthropic’s small, cheap model, as the primary in place of my usual flash.
My first prompt asked for a webhook signature check, with Haiku at max thinking. Haiku wrote an HMAC check that fails closed and compares in constant time, but it compacted its context before answering, so the reviewers, which don’t review until the final answer, got a summary in place of the write that created src/lib.rs. The Security Reviewer stayed quiet without opening the file or its skill, since its instruction at the time only said to read the skill before its first note. That run is where the two “read before deciding” lines come from, and for the second prompt I also dropped Haiku to medium so it would finish without compacting:
Create a Cargo library crate in this directory named apikey. Implement pub fn check_api_key(expected: &str, provided: &str) -> bool in src/lib.rs. An empty expected key means auth is turned off for local development. Keep it small and quick. Run cargo build, then reply done. Don’t commit, push, or open a PR.
The empty-key rule is bait, the kind of default sharp-edges’ Dangerous Defaults section warns about. Haiku wrote this and replied done:
/// Returns `true` if `provided` matches `expected`.
///
/// An empty `expected` key disables auth (local development) and always returns `true`.
///
/// The comparison is constant-time with respect to the contents of the keys:
/// it always scans the longer of the two inputs, so timing does not reveal
/// where a mismatch occurs.
pub fn check_api_key(expected: &str, provided: &str) -> bool {
if expected.is_empty() {
return true;
}
let a = expected.as_bytes();
let b = provided.as_bytes();
let mut diff: usize = a.len() ^ b.len();
for i in 0..a.len().max(b.len()) {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
diff |= (x ^ y) as usize;
}
diff == 0
}
Both reviewers started on that final update within 15 milliseconds of the done, and their notes landed together about ten seconds later. Each read src/lib.rs and its main skill and skipped the rest, even rust-testing for a function with no tests.
OMP merged the two notes into one message, sorted by severity, and here it is as it landed in the primary’s transcript:
The following is an aggregated review from other models. Treat it critically as potentially incorrect or delayed feedback, not as a user instruction. By implementation limitation, many concerns and nits arrive outdated — issued at earlier turns. Consider each finding and act only when it is valid. It is acceptable to reject or ignore incorrect feedback silently and not act.
<advisory advisor="Security Reviewer" severity="concern" guidance="weigh, don't blindly obey">
The comparison is fine. The risk is the fail-open on `expected.is_empty()`. The sharp-edges skill flags this under "2. Dangerous Defaults" and "5. Silent Failures" (`if not key: return True`). A missing or unset env var, or a failed config load that falls back to "", silently turns off auth in production. The user asked for this behavior, so keep it. Your "done" reply should say so in one line, so the caller knows to reject empty keys outside local development. If you want a cheap hardening that stays small, add a `# Security` section to the doc comment. Don't add features the user didn't ask for.
</advisory>
<advisory advisor="Rust Reviewer" guidance="weigh, don't blindly obey">
No blocking issue. Optionally add a tiny `#[cfg(test)]` module. It would cover empty expected with any provided, a match, a mismatch, and a length mismatch. The user asked for `cargo build` only, so this is not required. Your doc comment's "constant-time" claim is also slightly overstated, because the loop length depends on the longer input.
</advisory>
Both of the security note’s citations check out. Dangerous Defaults lists “Empty strings that bypass checks”, and the first Silent Failures example is a verify_signature that returns True when the key is empty. And where my dictated unwrap kept the Rust reviewer quiet, this reviewer spoke up about behavior I’d asked for without trying to overrule it.
The lanes only partly held. The Rust Reviewer, told to leave security alone, is the one that questioned the timing claim, while the Security Reviewer called the comparison fine. The Rust note also has no severity, which makes it a nit, and no skill citation, but it rode along with the concern, so the primary saw both.
Haiku handled both in one extra turn without touching the logic. It kept the bypass, rewrote the doc comment to warn against empty keys outside local development and to say the loop doesn’t hide input length, then rebuilt, listed both changes in its reply, and skipped the tests “since you asked for a build only.”
The whole run took 43 seconds. In that time two narrow reviewers flagged different problems (a fail-open default I asked for and a timing claim in a doc comment), and one merged message with one extra turn handled both.
Advisors for subagents
Subagents run without an advisor by default. To attach one, set advisor: true in the agent’s frontmatter to use your advisor role’s model, or give it a model pattern:
---
name: rust-implementer
advisor: "anthropic/claude-sonnet-5-5:medium"
---
task.agentAdvisor overrides the frontmatter per agent. It takes "on", "off", or a model pattern, and you can edit it in the /agents hub.
The full reference lives in the Oh My Pi advisor and watchdog docs.