# scripts/ — shared agent mechanics Small scripts for the things every agent working on this repo ends up doing by hand, with the reasons attached. Usable from Claude Code, from opencode, or from a terminal — they take no agent-specific input and print plain text. Each of these encodes something that cost real time to work out at least once and is not guessable from the code. ## `preflight.sh` — refuse a start that will waste an hour ``` scripts/preflight.sh [--allow-shared] [--base main] ``` Run it before any multi-step work. Exit 0 means safe to proceed; exit 1 prints the reason and the fix. It exists because of one run on 2026-09-10 that burned most of a session, and its four checks are ordered by how much time each failure actually cost — not by severity. **It prints the stash stack unconditionally, first, even when it passes.** That is the whole point. In that run an executor stashed its own partial work with a descriptive message, came back to a clean tree, concluded "subagent edits are not persisting", and re-fired subagents twice against that theory. The work was in `stash@{0}` throughout. An unexpectedly clean tree is far more often a stash than a broken editor, and an agent that sees the stack cannot invent the tooling theory in the first place. The stack is also **shared across every worktree of this repo on this machine** — about twenty of them. Another session pushing an entry shifts every index, and `git stash pop` deletes on success, so a wrong index is destructive. Hence the printed reminder to `apply` by SHA. **Wrong-branch and shared-checkout checks.** The same run executed on `docs/admin-screenshots` — a merged PR's leftover branch — so every edit landed somewhere nobody was looking, against a tree missing two merged PRs, and the operator's `git pull origin main` failed against it. The branch check is deliberately not "must equal main": a feature branch cut from an up-to-date base is fine, so it tests whether `origin/` is an ancestor. A leftover branch from a merged PR fails that; a fresh feature branch passes. `--allow-shared` skips the worktree requirement for the case where working in the primary checkout is genuinely intended. ## `sandbox.sh` — throwaway router on 8081 ``` scripts/sandbox.sh start [worktree] # defaults to $PWD; waits for /health scripts/sandbox.sh stop scripts/sandbox.sh status ``` Runs a second router instance so the admin portal can be exercised against real data without touching production. **8080 is production, always** — it is baked into `opencode.json`, the systemd unit, every curl example in `CLAUDE.md`, and the admin frontend's own fetches. A throwaway binds **8081**. This script cannot bind 8080. **It only kills a pid it recorded itself.** Never `pkill uvicorn` or kill-by-port here: `Restart=always` will fight you, and on this machine that process is the operator's model access. **Why it passes both a cwd and a PYTHONPATH.** `cwd` decides `config/config.yaml` and the relative `router.db`; `PYTHONPATH` decides which `src/` runs and which `admin/frontend/*.html` is served, because `admin.py` resolves `_REPO_ROOT` from `Path(__file__).parent.parent`. Pointing both at a worktree keeps every write — including admin writes to `config.local.yaml` — inside that worktree. To exercise the portal against the **real catalog and decision history**, clone the production DB into the worktree first: ``` sqlite3 router.db "VACUUM INTO '/router.db'" ``` `VACUUM INTO` rather than `cp`: the live DB is WAL-mode with an active writer, so a plain copy can be torn. ## `mkpr.sh` — cut a gitea PR with a long body ``` scripts/mkpr.sh "" [head] [base] ``` `tea pr create` has no `--description-file`, only `--description`, so a long body has to arrive through `$(cat ...)`. This wraps that, and refuses to open a PR whose head branch is unpushed, has diverged from its remote, or is not actually ahead of the base — `tea` will cheerfully do all three and leave you with an empty or misleading PR. Set `MKPR_REPO` to target a repo other than `alee/6krrt`. ## `verify_commit.py` — check a commit is valid, clean, and tested ``` python3 scripts/verify_commit.py [--repo DIR] [--require-clean] SHA ``` Checks that a commit exists (PASS), working tree is clean (PASS/FAIL with --require-clean), and pytest passes on the files the commit touched (against the committed tree, via git archive). Output is one PASS|FAIL|SKIP line per check, at most 40 lines. Used as the default `plan_tick_gate` command: the opencode guardrails plugin refuses to tick a todo while this script reports FAIL on HEAD. ## `oc_dispatch_audit.py` — audit orchestrator dispatch calls ``` python3 scripts/oc_dispatch_audit.py <session_id> [--worktree <path>] ``` Opens an orchestrator session's transcript and flags every task() or call_omo_agent call that is DEAD (no category, subagent_type, AND task_id), BANNED (subagent_type starts with oh-my-claudecode:), NOWT (missing WORKTREE: line when worktree is given), or IDLE (child session with 0 tool calls). An orchestrator runs this before reporting a wave done. ## Things these scripts deliberately do not do - **Merge to `main`.** A merge needs `main` checked out, which conflicts with any agent working in a git worktree. Merges stay a human step. - **Run `Seed Energy` or `Restart Service`.** The first spends real provider credit (`?samples=5` is 65+ billed completions); the second restarts the *production* systemd unit whichever instance is asked. Read them in source rather than invoking them to find out. - **Touch `config/config.local.yaml`.** It is gitignored, never committed, and holds the live classifier settings — there is no copy in git to restore from. Back it up and `md5sum` before and after if a task must write to it. ## Tests ``` PYTHONPATH=src .venv/bin/python -m pytest tests/ -q ``` `tests/conftest.py` sets `ROUTER_IGNORE_LOCAL_CONFIG`, so the suite ignores whichever `config.local.yaml` the machine has. Before that, a deployment with a configured `classifier.mode` failed nine tests that had nothing to do with its changes. A test that genuinely wants overlay merging must pass `include_overlay=True` to `load_config` explicitly.