How to run, monitor, and recover the codex-driven bug-hunt orchestrator.
This doc is gitignored (lives under .omx/) — it's a working reference,
not a product doc.
A Python orchestrator that spawns codex exec subagents in parallel to
scan the App codebase for bugs, optionally fixing them in-flight. It
calibrates against a category whitelist (silent failure, anti-fab,
wire-shape, correctness, staleness, slop, etc.) and routes per-task
across multiple codex models based on complexity.
Script: .omx/subagent_orchestrator/scripts/run_bug_hunt.py
Prompts:
- Discovery:
.omx/subagent_orchestrator/prompts/subagent-prompt.md - Fixer:
.omx/subagent_orchestrator/prompts/fixer-prompt.md
The standard fluid-budget concurrent-discovery + concurrent-fix sweep with build verification and spark fallback:
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 200 --max-rounds 80 \
--max-agents 5 --agent-timeout 600 \
--codex-model gpt-5.5 --codex-reasoning-effort high \
--simple-codex-model gpt-5.3-codex-spark --simple-reasoning-effort xhigh \
--simple-codex-fallback-model gpt-5.3-codex \
--simple-codex-fallback-reasoning-effort xhigh \
--commit-fixes --parallel-fix --fix-agents 2 --verify-build \
--fix-timeout 1500What this does:
- 5 discovery scouts (spark/xhigh) + 2 fix workers (gpt-5.5/high) sharing a fluid 7-permit budget
- Discovery and fix run alongside each other from round 1
- Each fix: codex edits → orchestrator validates file-set → commits →
xcodebuild/node --checkverifies the build → reverts on failure - Sticky fallback: if spark hits its weekly quota, all subsequent simple
tasks switch to
gpt-5.3-codex(different quota pool) - Estimated wall time: ~1-6 hours depending on how much fix work the findings imply
When you want findings but not auto-commits.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 200 --max-rounds 80 --max-agents 5 --agent-timeout 600 \
--codex-model gpt-5.5 --codex-reasoning-effort high \
--simple-codex-model gpt-5.3-codex-spark --simple-reasoning-effort xhigh \
--simple-codex-fallback-model gpt-5.3-codex \
--simple-codex-fallback-reasoning-effort xhigh \
--no-fix --commit-fixes--no-fix blocks the fixer entirely; --commit-fixes is still needed
to spawn the cmux monitor (a quirk — see "Monitoring" below).
When discovery already happened and you just want to land fixes from a
previous run's unique_findings.json.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--fix-only-from 20260519T024841Z \
--codex-model gpt-5.5 --codex-reasoning-effort high \
--commit-fixes --parallel-fix --fix-agents 2 --verify-build \
--fix-timeout 1500The --fix-only-from arg accepts either a run-id slug or an absolute
path to a unique_findings.json.
When you don't trust the parallel-fix collision detection or want a known-good fix queue.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 100 --max-rounds 60 --max-agents 5 --agent-timeout 600 \
--codex-model gpt-5.5 --codex-reasoning-effort high \
--commit-fixes --verify-build --fix-timeout 1500No --parallel-fix. Discovery still runs concurrent; fix runs one at a
time after discovery completes.
Use codex's subscription quota for as long as possible, then transparently fall through to pi.dev's pay-per-token pool when the subscription is spent. Long runs survive the day instead of aborting at the spark wall.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 200 --max-rounds 80 --max-agents 5 --agent-timeout 600 \
--simple-codex-model gpt-5.3-codex-spark --simple-reasoning-effort xhigh \
--simple-codex-fallback-model gpt-5.3-codex \
--simple-codex-fallback-reasoning-effort xhigh \
--pi-fallback-model pi:openai-codex/gpt-5.3-codex-spark \
--pi-fallback-reasoning-effort xhigh \
--commit-fixes --parallel-fix --fix-agents 2 --verify-build \
--fix-timeout 1500The chain is sticky — once pi engages, both discovery and fix route
through pi for the rest of the run (unless --fix-model pins fix
elsewhere). Watch the [orchestrator] tier-transition log lines in the
monitor's main pane.
Run all discovery on DeepSeek's flash model (1M context, cheap) and all fixes on DeepSeek's pro model (1M context, more capable). Reverse the two if you want the bigger model finding edge cases and the cheap model mechanically patching them.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 200 --max-rounds 80 --max-agents 5 --agent-timeout 600 \
--discovery-model pi:deepseek/deepseek-v4-flash \
--discovery-reasoning-effort high \
--fix-model pi:deepseek/deepseek-v4-pro \
--fix-reasoning-effort high \
--commit-fixes --parallel-fix --fix-agents 2 --verify-build \
--fix-timeout 1500Phase overrides disable the fallback chain for each phase, so this is
a flat "always-this-model" config. Pay-per-token billing applies for
both phases — keep an eye on [orchestrator] cost lines (pi's JSON
mode emits per-call cost; we don't surface it yet but --codex-panes
lets you watch it live).
Use codex CLI's spark as primary, pi.dev's gateway to the same spark model as the fallback. Two different gateways, two different quota pools, same model fingerprint — useful when you want to keep running without changing the model's behaviour after the quota wall hits.
python3 .omx/subagent_orchestrator/scripts/run_bug_hunt.py \
--target 200 --max-rounds 80 --max-agents 5 --agent-timeout 600 \
--simple-codex-model gpt-5.3-codex-spark --simple-reasoning-effort xhigh \
--pi-fallback-model pi:openai-codex/gpt-5.3-codex-spark \
--pi-fallback-reasoning-effort xhigh \
--commit-fixes --parallel-fix --fix-agents 2 --verify-build(No --simple-codex-fallback-model — chain goes spark-via-codex →
spark-via-pi directly.)
- Up to
--max-agents(default 5) concurrent read-only codex spawns - Each task carries a
complexity: simple|complexlabel set by the focus name (see "Model routing") - Tasks come from
build_tasks(ROOT, rng)which produces:- 15 hand-coded cross-file scopes (
build_cross_file_tasks()) - 1000-line file chunks × 2 focuses per chunk (
build_per_file_tasks()) - Total typically 700-800 tasks
- 15 hand-coded cross-file scopes (
- Each batch is
--max-agentstasks; rounds run sequentially through the task list - Stops on:
--targetfindings hit,--max-roundscap, scope exhaustion, or quota wall (≥half a round hits codex usage-limit)
- Up to
--fix-agents(default 2) concurrent workspace-write codex spawns - Workers run as a
ThreadPoolExecutor, consuming a sharedqueue.Queuethat the discovery loop populates after each batch - Each fix attempt:
- Codex edits files (concurrent across workers — no lock)
_FIX_SERIALIZER_LOCKacquiredgit diff --name-onlycompared to fixer's declaredfiles_touched— mismatch → stash + reject- Risk gate:
risk: high→ stash + reject _orchestrator_commit(declared, subject, body)lands the commit- If
--verify-build: auto-route by file ext (xcodebuildfor.swift/.m/.h,node --checkfor.mjs,py_compilefor.py). Failure →git reset --soft HEAD~1+ stash - Lock released
- Single
threading.BoundedSemaphore(--max-agents + --fix-agents)shared by everyrun_codexspawn - Discovery and fix workers both acquire permits before spawning codex
- When all
--fix-agentspermits are held by fix work, discovery naturally caps attotal - fix_in_flight - When fix is idle, discovery can burst to the full budget
Two orthogonal axes: (a) which tier of the fallback chain handles a task, and (b) which phase (discovery vs fix) the task belongs to.
Model strings are runtime-tagged:
- Plain id → invoked through the codex CLI (subscription quota, per-model: spark and non-spark draw from different pools).
pi:-prefixed id → invoked through the pi.dev CLI (pay-per-token via API keys; separate quota pool from codex CLI even for the same underlying model). Examples:pi:openai-codex/gpt-5.3-codex-spark— pi's gateway to the same sparkpi:openai-codex/gpt-5.3-codex— pi's gateway to the non-spark codexpi:deepseek/deepseek-v4-flash— DeepSeek's fast/cheap model (1M ctx)pi:deepseek/deepseek-v4-pro— DeepSeek's capable model (1M ctx)
Anywhere a model id is accepted (--codex-model, --simple-codex-model,
--simple-codex-fallback-model, --pi-fallback-model, --discovery-model,
--fix-model), a pi: prefix swaps the runtime; reasoning effort uses the
same {minimal,low,medium,high,xhigh} scale for both.
| Tier | Engaged when | Configured by |
|---|---|---|
| 0 — simple-primary | Default for simple-complexity focuses | --simple-codex-model + --simple-reasoning-effort |
| 1 — simple-fallback | Tier 0 returns a quota signal | --simple-codex-fallback-model + --simple-codex-fallback-reasoning-effort |
| 2 — pi-fallback | Tier 1 (or tier 0 when no tier 1) returns a quota signal | --pi-fallback-model + --pi-fallback-reasoning-effort |
Each tier flips a sticky threading.Event (_SIMPLE_QUOTA_EXHAUSTED,
_PI_FALLBACK_ENGAGED) on first quota hit and stays engaged for the rest
of the run. The triggering task is retried in-process against the next
tier; every subsequent task skips the spent tier with zero retry overhead.
Complexity is set per-focus in FOCUS_COMPLEXITY. Simple focuses
(pattern-match heavy): silent failure, @MainActor sync I/O, Pre-launch
creep, Smoke-fixture drift, Slop. Complex focuses (cross-file or
semantic): wire-shape, anti-fab, perf-signature, correctness, staleness.
Complex tasks always use --codex-model (no tier-0 simple-primary) but
DO advance to the pi fallback when the codex tier hits its wall.
--discovery-model and --fix-model override the chain entirely for the
named phase. Use case: cheap broad discovery on pi:deepseek/deepseek-v4-flash
(1M context, fast) and capable fixes on pi:deepseek/deepseek-v4-pro —
or the reverse if you want pro to find subtle edge cases that flash
patches mechanically. When a phase override is set the fallback chain
is disabled for that phase (the override is a single model, not a chain).
Once _PI_FALLBACK_ENGAGED is set during discovery, the fix phase also
switches to the pi fallback by default — otherwise fixes would still try
codex and immediately bounce. An explicit --fix-model takes precedence
over that auto-routing.
| Flag | Purpose |
|---|---|
--target N |
Stop after N unique findings. Default 100 |
--max-rounds N |
Hard cap on scan batches. Default 24 — too low; raise to 80+ for real runs |
--max-agents N |
Concurrent discovery agents. Default 5 |
--agent-timeout SEC |
Per-discovery-agent codex timeout. Default 240; raise to 600 for gpt-5.5/high |
| Flag | Purpose |
|---|---|
--codex-model NAME |
Complex tasks model. Default: codex's own default. Use gpt-5.5 or a pi:-prefixed id |
--codex-reasoning-effort {minimal,low,medium,high,xhigh} |
For complex tasks |
--simple-codex-model NAME |
Simple-tier-0 model. Use gpt-5.3-codex-spark |
--simple-reasoning-effort {minimal,...,xhigh} |
For simple tasks. Use xhigh |
--simple-codex-fallback-model NAME |
Simple-tier-1 fallback when spark quota exhausted. Use gpt-5.3-codex |
--simple-codex-fallback-reasoning-effort |
Reasoning for tier-1 fallback. Use xhigh |
--pi-fallback-model NAME |
Tier-2 fallback (pi.dev, pay-per-token). Must start with pi: — e.g. pi:openai-codex/gpt-5.3-codex-spark, pi:deepseek/deepseek-v4-pro |
--pi-fallback-reasoning-effort |
Reasoning for tier-2 fallback. Use xhigh |
--discovery-model NAME |
Override the model for discovery only. Disables the chain for discovery. Codex or pi:-prefixed |
--discovery-reasoning-effort |
Reasoning when --discovery-model is set |
--fix-model NAME |
Override the model for fix only. Codex or pi:-prefixed |
--fix-reasoning-effort |
Reasoning when --fix-model is set |
| Flag | Purpose |
|---|---|
--commit-fixes |
Allow fix agents to actually edit files + commit. Without this, fix workers are read-only |
--no-fix |
Skip fix phase entirely. Discovery-only |
--parallel-fix |
Concurrent fix workers (default: serial after discovery) |
--fix-agents N |
Concurrent fix workers when --parallel-fix is on. Default 2 |
--fix-timeout SEC |
Per-fix codex timeout. Default 480 — too short for gpt-5.5/high with verify-build, use 1500 |
--max-fixes N |
Hard cap on auto-fix attempts. Default unbounded |
--verify-build |
Run xcodebuild / node --check / py_compile after each commit; revert on failure |
| Flag | Purpose |
|---|---|
--fix-only-from RUN_ID_OR_PATH |
Skip discovery; load unique_findings.json from a prior run and just run the fix phase |
--run-id RUN_ID |
Override the auto-generated timestamp run-id |
| Flag | Purpose |
|---|---|
--codex-panes |
Open a cmux pane per codex spawn tailing its live stdout. High visual churn. Requires --commit-fixes + cmux running |
--no-cmux-monitor |
Skip auto-spawning the 3-pane cmux monitor workspace |
--seed N |
Random seed for task shuffling. Default 42 |
Every run produces a directory at
.omx/subagent_orchestrator/runs/<run-id>/:
runs/<run-id>/
├── raw/ # Per-agent JSON outputs
│ ├── cross-01.json # Cross-file scope agents
│ ├── file-0042-0.json # Per-file chunk, slot 0
│ ├── file-0042-1.json # Per-file chunk, slot 1 (different focus)
│ └── file-NNNN-N.json.stdouterr # Debug companion (codex stdout/stderr capture)
├── merged/ # Deduplicated findings (JSON)
│ └── unique_findings.json # Source of truth for --fix-only-from
└── final/ # Human-readable reports
├── run_log.md # Round-by-round progress + status
├── unique_findings.md # Markdown findings report
├── needs_decision.md # Findings with requires_decision: true
├── simple_findings.md # requires_decision: false (auto-fixable)
├── simple_fixes.jsonl # Fix-attempt receipts (one per line)
└── fixes/ # Per-fix codex receipts
└── fix-NNN.json
A 3-pane cmux workspace auto-spawns when --commit-fixes is set (the
gate is args.commit_fixes and not args.no_cmux_monitor — quirky but
intentional; the monitor was built for fix runs and stuck).
The 3 panes:
tail -f run_log.md— round progress + fix outcomestail -f simple_fixes.jsonl— fix attempt receipts as they landgit log --grep "Codex Auto-Fix"+git status— committed work so far + working tree state
Auto-rotate logic: panes follow the most-recent run dir, so a fresh orchestrator invocation rebinds the existing monitor workspace.
To check progress mid-run from a separate shell:
# Round + finding count
tail -30 .omx/subagent_orchestrator/runs/<run-id>/final/run_log.md
# Fix outcomes so far
ls .omx/subagent_orchestrator/runs/<run-id>/final/fixes/ | wc -l
# Commits landed since the sweep started
git log --oneline ^origin/main main | wc -l
# Real quota errors (vs substring matches)
grep -l -E '"error":"codex-usage-limit"|"error":"codex-rate-limit"' \
.omx/subagent_orchestrator/runs/<run-id>/raw/*.json | wc -l
# Active codex children
pgrep -af "codex exec" | wc -l
# Active orchestrator
pgrep -af "run_bug_hunt.py" | head -3{
"status": "found",
"agent_scope": "...",
"agent_focus": "...",
"finding": {
"file": "ios/App/Core/AppStateStore.swift",
"line": 12345,
"category": "Silent failure",
"issue": "Short noun phrase, under 80 chars",
"evidence": ["LINE 12345: try? fileManager.removeItem(at: url)"],
"why": "Concrete failure mode produced",
"impact": "UX | developer | correctness | security + sentence",
"conditions": "Runtime state that triggers it",
"confidence": "high | medium | low",
"severity": "critical | high | medium | low",
"fix_or_validation": "Minimal fix shape OR probe to confirm",
"recommendation": "Concrete what-to-change",
"minimum_fix_scope": "1 file (call site only)",
"why_tests_do_not_already_cover_this": "...",
"suggested_regression_test": "single-line test seed OR null",
"requires_decision": false
}
}{
"status": "ready_to_commit",
"issue_anchor": "<file>:<line>",
"files_touched": ["path/to/file.swift"],
"commit_subject": "fix(<area>): ...",
"commit_body": "...",
"risk": "low | medium | high",
"risk_reasoning": "one sentence",
"notes": "optional"
}OR:
{
"status": "skipped",
"issue_anchor": "...",
"reason": "already_fixed | unsafe_scope | depends_on_decision | code_moved | other",
"notes": "..."
}status is one of:
committed— landed cleanly, build greenskipped— codex chose skipskipped_but_dirty_stashed— codex skipped but had leftover editsrejected_bad_receipt— codex's output JSON malformedrejected_missing_fields—files_touchedorcommit_subjectemptyrejected_file_set_mismatch— declared file set ≠ actual git diffrejected_risk_too_high— fixer self-flaggedrisk: highreverted_build_failed— committed then build failed, soft-resetcommit_failed— git commit step failed (hook? identity? lock?)
Before fallback (last night's pattern):
Round 49: 3/5 spark agents hit codex-usage-limit → orchestrator aborts
discovery with Aborting discovery: 3/5 agents this round hit a codex usage/rate limit.
With fallback (recommended):
First spark task to hit quota triggers retry on gpt-5.3-codex AND
flips _SIMPLE_QUOTA_EXHAUSTED. All subsequent simple tasks go straight
to fallback. Run continues at slower per-task throughput against a
separate quota pool.
If fallback ALSO hits quota (rare — pools are usually distinct), the 3-of-5 abort guard catches the next round.
In parallel-fix mode, two fixers can edit overlapping files before either commits. The lock-protected file-set validator rejects the loser. With 2 fixers and ~80 findings, expect ~15-30 mismatches per run. The work isn't lost — each mismatch is stashed under a labeled stash for recovery.
To recover stashes after a run, see "Stash recovery" below.
Orchestrator checks _working_tree_dirty() before the fix phase. If
dirty:
- Serial mode: tries one defensive stash; if still dirty, skips
- Parallel mode: the file-set validator inside the lock rejects every fix that touches a file you've already edited
Always run with a clean tree. If you have in-flight work, stash it under a clear label first:
git stash push -m "in-flight-feature-X-pre-sweep" -- <files>--verify-build runs full xcodebuild after every Swift fix. Each
xcodebuild is 60-90s. For a 50-fix sweep, that's an extra 50-75 minutes
just for verification.
Tradeoffs:
- Drop
--verify-build: faster but commits can break the build - Keep
--verify-build: longer run, but tree is always green and bad fixes auto-revert
For overnight runs, keep it. For quick experiments, drop it and
manually xcodebuild once at the end.
Rejected/reverted fixes are stashed with descriptive labels:
bug-hunt-fix-NNN-<file_path_underscored>_<line>-<reason>
Reasons:
file-set-mismatch— concurrent edit collisionrisk-too-high— fixer self-flaggedbuild-failed-reverted— build verify caught a regressionskip-but-dirty— codex chose skip but had leftover editscommit-failed— git commit step failedbad-status/missing-fields/bad-receipt— codex output malformedtree_dirty_pre— tree was dirty entering the attempt (rare with self-heal)
To triage stashes after a sweep:
# List
git stash list
# Inspect
git stash show "stash@{N}" --stat
git stash show "stash@{N}" -p | head -80
# Recover (apply, build, commit, drop)
git stash apply "stash@{N}"
xcodebuild -project ios/App.xcodeproj -scheme App \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-configuration Debug build 2>&1 | grep -E '^(.*error:|\*\* BUILD)'
# If green:
git add <files> && git commit -m "..." && git stash drop "stash@{N}"
# If conflicts or build fail:
git checkout -- <files> # discard the apply
# Leave stash for later or drop with noteThe risk-too-high stashes are usually genuine product decisions — keychain throw refactors, recording-lifecycle changes, signing/manifest edits — read each before applying.
Edit when:
- Adding a new finding category (also update
FOCUSESin the script andFOCUS_COMPLEXITY) - Changing the finding JSON schema (also update
render_markdown) - Tightening calibration anchors
- Adding new cross-file invariants to probe
Placeholders the script fills: {{SCOPE}}, {{FOCUS_HINT}},
{{SKIP_LIST}}.
Edit when:
- Changing the fix output schema (also update
_attempt_one_fixparsing) - Changing risk calibration anchors
- Adding new concurrency constraints
The fixer is told that 1 other fix agent may be concurrent in
parallel-fix mode — re-read files before editing, skip with reason
code_moved if context shifted.
These patterns are known and intentional. The discovery prompt already excludes them; if they come back, downgrade or skip.
'ORG'/APP_HOSTED_PROCESSING_DEMO_KEYWORD— active dev workflowAPP_BACKEND_ALLOW_NON_CLIENT_OWNED=1flag — 11-test refactor deferredtry?in test cleanup / tearDown — acceptable- SSE
event:line parsing — design pinned: JSON envelope is source of truth PROCESSING_MANIFEST_NONCE_MAX_TTL_MSclamping — security guardrail, not a fix targetlegacyKeyparameter onappEnvironmentValue— explicit opt-in
Past sweep findings live at:
.omx/subagent_orchestrator/runs/<run-id>/final/unique_findings.md
Significant runs:
audit-20260517T200007Z— original 8-finding privacy audit (all closed)audit-20260517T222143Z— 23-finding privacy follow-up (22 closed, 1 deferred)20260518T010641Z— first 169-finding broad sweep (56 closed via multi-session triage)20260518T200701Z— 23-finding sweep (all closed via codex handoff)20260519T024841Z— fluid-budget parallel-fix sweep (80 findings, 46 auto-committed + 18 recovered from stashes = 64 closed)
When git log --grep "audit-" to find commits referencing a specific
audit run.
Auto-fix commits follow the existing style:
fix(<area>): <one-line subject under 70 chars>
<one paragraph: failure mode + fix shape>
Finding: <file:line> [<category>]
Co-Authored-By: Codex Auto-Fix <codex-auto-fix@noreply.app>
Manual recovery commits (you, me, deepseek) use:
fix(<area>): <subject>
<paragraph>
Recovered from stash <stash-label> (<reason>).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Area prefixes used so far: fix(privacy), fix(anti-fab),
fix(silent-failures), fix(wire-shape), fix(correctness),
fix(staleness), fix(perf-signature), fix(intelligence),
fix(record), fix(settings), fix(server), fix(backend),
fix(agentic), fix(wiki), fix(tests), fix(auth),
chore(privacy), chore(api-client), chore(tests),
chore(intelligence), chore: bump build number to N.
README.md(same dir) — original orchestrator overviewdocs/PRIVACY.md(repo root) — privacy contract + audit closure table- Memory:
~/.claude/projects/-Users-user-Code-App/memory/— user/project facts that carry across sessions
Codex CLI may be missing or unauthenticated. Test:
echo '{"task": "ping"}' | codex exec --sandbox read-only -C $(pwd) "Return {}"Should return within 30s. If it hangs, codex is the issue, not the orchestrator.
Known issue: codex needs stdin=DEVNULL or it can stall waiting for
input that never comes. The script already passes
stdin=subprocess.DEVNULL, but if you see hung children:
ps -o pid,etime,command -p $(pgrep codex) | head -10If etime exceeds your --agent-timeout and CPU is 0%, send SIGTERM:
pkill -TERM -f "codex exec"--verify-build runs xcodebuild on the FULL project, not just changed
files. If an unrelated file already has a build error (your in-flight
work, a stash residue, etc.), every fix will revert. Solution: make
sure xcodebuild passes on main before kicking off the sweep.
If you have many runs back-to-back without cleaning up, stashes accumulate. Drop them with:
# List all bug-hunt stashes
git stash list | grep "bug-hunt-fix-"
# Drop a specific one
git stash drop "stash@{N}"
# Nuclear: drop all bug-hunt stashes (keeps anything not labelled
# `bug-hunt-fix-`)
git stash list | grep "bug-hunt-fix-" | \
awk -F: '{print $1}' | tac | xargs -I{} git stash drop {}Always triage before dropping en masse — some stashes contain real work.
--fix-only-from <run-id> replays the fix phase against a prior run's
unique_findings.json. The dedup signatures persist across runs so
you can re-run the SAME sweep and skip already-fixed anchors via the
prompt's {{SKIP_LIST}}.