Review layer for DStack. Add-on for the Claude Code plugin, with a native oh-my-pi port.
Independent critic and oracle for coding agents.
The agent acts. A decision model judges changes, plans, stops, and risky commands. Code changes get a second opinion by default.
- oh-my-pi (omp): a working
judgemodel role (modelRoles.judge). It answers the review questions. - Claude Code plugin (Python):
TYPESAFE_API_KEYin the environment. Python 3.8+, standard library only. The judge calls go to the TypeSafe decision-model API.
The reflector ships in two surfaces; install whichever matches your agent:
- Claude Code plugin: the original single-file Python hook. Needs Python 3.8+ (stdlib only, no pip dependencies).
- oh-my-pi (omp): a native TypeScript port, no Python runtime. Needs the
ompCLI.
A single-file Python hook (scripts/codex-reflector.py, stdlib only) wired through hooks/hooks.json. Requires Python 3.8+ and TYPESAFE_API_KEY in the environment.
claude plugin marketplace add OutlineDriven/odin-reflector; claude plugin install codex-reflector@odin-reflectorPlace this plugin at ~/.claude/codex-reflector/ and restart Claude Code.
~/.claude/codex-reflector/
├── .claude-plugin/plugin.json
├── hooks/hooks.json
├── scripts/codex-reflector.py
├── LICENSE
├── README.md
└── .gitignore
| Event | Trigger | Mode | Purpose |
|---|---|---|---|
| PostToolUse | Write, Edit, Fast-Apply MCP edit | async | Decision-model scorecard review (PASS/FAIL/UNCERTAIN) |
| PostToolUse | ExitPlanMode | async | Decision-model plan review, advisory (PASS/FAIL/UNCERTAIN) |
| Stop | Agent finishing | sync | Fresh judge review; blocks on FAIL only (PASS/UNCERTAIN settle) |
Cursor can run this plugin through its Claude Code third-party hooks compatibility layer.
This repository includes native Cursor plugin metadata at .cursor-plugin/plugin.json.
To install it as a local Cursor plugin, copy or symlink this repository into:
~/.cursor/plugins/local/codex-reflectorThen reload Cursor. The plugin components are auto-discovered from this repository:
hooks/hooks.jsonrules/codex-reflector-usage.mdcskills/codex-reflector/SKILL.mdagents/codex-reflector-reviewer.mdcommands/install-cursor-plugin.md
- Enable Third-party skills in Cursor Settings.
- Install the direct-export Claude settings:
scripts/install-cursor.shThat writes ~/.claude/settings.json with absolute paths to this checkout. To install into a specific project instead, pass the project path:
scripts/install-cursor.sh /path/to/projectWhen this repository itself is opened in Cursor, the checked-in .claude/settings.json also works directly because Cursor provides CLAUDE_PROJECT_DIR.
Cursor compatibility notes:
PostToolUseandStoprun through Cursor's Claude hook compatibility.- Cursor treats Claude
Stopblocks as follow-up messages, so a blocked Stop review causes the agent to continue with the judge's reason instead of hard-stopping the UI. - The direct export mirrors the Claude plugin matcher. Cursor only fires the parts whose tool names are exposed through its Claude hook compatibility layer.
A native TypeScript port of this reflector ships at omp/codex-reflector.ts, with no Python runtime. It registers oh-my-pi extension handlers that mirror the plugin: a scorecard review on write/edit/ast_edit and Fast-Apply MCP edits, an opt-in Stop gate, and an opt-in bash guard.
Skip if you already have it (more options):
curl -fsSL https://omp.sh/install | sh # macOS / Linux
bun install -g @oh-my-pi/pi-coding-agent # any platform, bun >= 1.3.14Install this repository directly as an oh-my-pi plugin:
omp plugin install https://github.com/OutlineDriven/odin-reflectorThe repository's package.json declares omp.extensions: ["./omp/codex-reflector.ts"], so omp installs the GitHub package and loads the native extension from that manifest. This is the canonical install path for normal use: no local clone, symlink, or config-file edit is required.
For a named omp profile, run the same command with that profile selected:
omp --profile <profile> plugin install https://github.com/OutlineDriven/odin-reflectorReads the shared environment variables. The TYPESAFE_* judge variables are Python-only.
Run the unit tests with bun test omp/codex-reflector.test.ts.
Behavioral delta from the Claude plugin: both surfaces are stateless. Scorecard reviews inject their verdict inline every time. On OMP, the pre-execution bash guard (tool_call) and the Stop gate (session_stop) are opt-in and disabled by default; you can toggle them via extension flags (codex-bash-guard, codex-stop-review) in /settings or enable them via env vars (CODEX_REFLECTOR_BASH_GUARD=1, CODEX_REFLECTOR_STOP_REVIEW=1). The Python Stop gate is always on, and Python has no bash guard. When active, the Stop gate runs a fresh judge review on the native session_stop event: only a FAIL blocks (the worst defect probability at 0.9 or above); PASS and UNCERTAIN settle (fail-open, never block on uncertainty). It runs once per stop chain: a FAIL returns { decision: "block", reason } so the agent keeps working with that context, then the harness stop_hook_active flag settles the re-stop; the native 8-continuation cap is the backstop.
| omp event | Trigger | Mode | Purpose |
|---|---|---|---|
tool_result |
write / edit / ast_edit / fast_edit, Fast-Apply MCP edit |
async | Decision-model scorecard review (PASS/FAIL/UNCERTAIN) |
tool_call |
bash, before execution |
sync | Opt-in bash guard; five yes/no answers; blocks at 0.9 |
session_stop |
main agent about to settle | sync | Fresh judge review once per stop chain (stop_hook_active guard); only FAIL blocks (PASS/UNCERTAIN settle); native 8-continuation cap backstop |
Both surfaces read CODEX_REFLECTOR_ENABLED and CODEX_REFLECTOR_DEBUG. The TYPESAFE_* variables are Python-only. The two gate variables are OMP-only: the Python Stop gate is always on, and Python has no bash guard.
| Variable | Default | Description |
|---|---|---|
CODEX_REFLECTOR_ENABLED |
1 |
Set to 0 to disable all hooks (both surfaces) |
CODEX_REFLECTOR_BASH_GUARD |
0 |
OMP only. Set to 1 to enable the opt-in pre-execution bash guard (0 disables). Python has no bash guard |
CODEX_REFLECTOR_STOP_REVIEW |
0 (OMP) |
OMP only. Set to 1 to enable the opt-in Stop gate (0 disables). The Python Stop gate is always on |
CODEX_REFLECTOR_DEBUG |
0 |
Set to 1 for diagnostics: on stderr in the Python plugin, in the agent log on oh-my-pi |
TYPESAFE_API_KEY |
(none) | Python only. Required. Bearer token for the TypeSafe decision-model API |
TYPESAFE_BASE_URL |
https://api.typesafe.ai |
Python only. Judge API base URL |
TYPESAFE_DEFAULT_MODEL |
jev-1.14.0 |
Python only. Judge model id |
Both surfaces keep no FAIL state: reviews inject their verdict and scorecard inline, and the Stop gate runs a fresh review rather than consulting persisted state. Every route asks the decision model. The implementation specifics below (exit-code blocking) describe the Python plugin; the omp port mirrors the behavior with the delta noted above.
| Route | Reviews it | Outcome |
|---|---|---|
| Write review (Write/Edit tool calls) | Decision model | Scorecard with PASS/FAIL/UNCERTAIN, advisory |
| Plan review (Python only) | Decision model | Scorecard, advisory |
| Stop gate (Python always on, OMP opt-in) | Decision model | Scorecard, blocks on FAIL |
| Bash guard (OMP only, opt-in) | Decision model | Five yes/no answers, blocks at 0.9 |
If the judge does not answer, the route proceeds unreviewed and shows review skipped: <cause>. It never blocks.
After Write/Edit tool calls, the decision model judges the change. It answers scored questions on seven dimensions: correctness, completeness against the stated job, complexity, slop, concurrency, performance, and design. UI files (.html, .htm, .css, .scss, .sass, .less, .jsx, .tsx, .vue, .svelte, .astro) add layout, animation, and accessibility. The answers become a scorecard, and the reflector injects it inline so the agent sees it on the next turn.
The verdict is computed in code from the judge's probabilities:
- FAIL when the worst defect probability reaches the route threshold: 0.7 on the advisory routes (write review, plan review).
- PASS when the worst defect probability is below 0.3 and no concern is strictly the most likely level on every dimension that still applies.
- UNCERTAIN otherwise. A dimension that leans to a concrete concern makes the verdict UNCERTAIN, so the agent sees it.
Only the Stop gate and the bash guard can block. The Stop gate blocks on a FAIL, and the bash guard blocks when an answer reaches 0.9. Write and plan reviews are advisory. Both surfaces judge the text of the last assistant message at the Stop gate, and fall back to the transcript only when that message has no text. The Stop gate runs once per stop chain and blocks when the worst defect probability reaches 0.9: in the Python plugin a FAIL blocks via exit 2, then the stop_hook_active guard lets a re-stop settle; in omp a FAIL returns { decision: "block", reason } and the same guard settles the re-stop, with the native 8-continuation cap as backstop.
If the message and the transcript are both empty, the Stop gate settles without a review and without a notice.
A scorecard is the review text. It starts with the verdict and the file, lists one line per dimension that shows a concern, and names the judge model:
Reflector Review ⚠️ FAIL [src/orders.py]:
- correctness: material defect (P(defect)=0.86) It contains a defect that produces a wrong result, a crash, data loss, or a security hole on a realistic input.
- slop: concrete concern (P(defect)=0.41) It contains one or two small leftovers, such as an unused import, a stale comment, or a vague name.
Judged by jev-1.14.0.
When no dimension shows a concern, the body reads No dimension shows a material defect (highest P(defect)=0.04).
A change, a plan, a stop subject, or a command can be over 28,000 bytes after secrets are redacted. The reflector does not cut it. It splits the text into consecutive parts and judges every part with the same questions.
The combined verdict is FAIL when any judged part is FAIL. When every part has a verdict, it is PASS when all parts are PASS, and UNCERTAIN otherwise. When a part shows a concern, its scorecard line ends with the part it came from, for example (part 3 of 9).
Completeness is judged only when the whole subject and the request fit in one part. When the subject alone had to be split, the scorecard holds the line Completeness was not judged: the subject was split into <n> parts. When the subject is one part but the request does not fit with it, the line reads Completeness was not judged: the request does not fit in one part with the subject.
A FAIL from a judged part stands even when other parts were not judged. The scorecard then holds the line Parts not judged: <k> of <n> (<cause>). Without a FAIL, one part without a verdict skips the review with the review skipped: <cause> notice.
The thresholds are fixed in code: FAIL at 0.7 on the advisory routes (write review, plan review), PASS below 0.3, block at 0.9 (Stop gate, bash guard).
A scorecard names the dimension and the criterion. It does not name the line or the fix; to find the line, read the criterion against the change.
The decision model reads text. Text inside the change it judges can steer it. Treat a scorecard as one signal, the same way you treat any review.
Layout, animation, and accessibility are judged from the source text of UI files. The judge never sees a rendered page.
The reflector does not explain a failed command and does not reflect on thinking steps.
A subject over 28,000 bytes is split into parts, and each part gets one judge call. At most 8 calls run at a time. On oh-my-pi all calls must end inside the 25 second handler budget; in the Python plugin inside 90 seconds. Otherwise the review is skipped with the notice.
The 28,000 byte limit is a size estimate, not a guarantee. Text made mostly of rare characters can be over the context of the judge and be rejected. On oh-my-pi the judge role can be a model with a smaller context. A rejected part counts as a failed part; the Python plugin shows judge request failed: HTTP 400.
A part is judged without the text around it. A defect that spans two parts can be missed.
The Stop gate does not see your request, only the last answer of the agent.
- Fail-open: an internal error or a missing judge answer never blocks the agent. The route proceeds unreviewed with the
review skipped: <cause>notice; the Python plugin exits0, the omp hook returns without a block. - Redaction: secrets are redacted before any text leaves the process, on the judge path.
- Loop prevention: both surfaces check the
stop_hook_activeflag to run the Stop review once per stop chain, with oh-my-pi's built-in 8-continuation cap as the omp backstop.
Apache-2.0