A run on 2026-09-10 burned most of a session on three failures, all
mechanical and all preventable by one check at the start.
The expensive one was not the wrong branch. 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} the whole time.
So preflight.sh prints the stash stack UNCONDITIONALLY and first, pass or
fail. An agent that sees the stack cannot invent the tooling theory. The
checks are ordered by how much time each failure actually cost, not by
severity:
0. stash stack, always printed
1. not the shared primary checkout (git-dir vs git-common-dir)
2. on a branch that contains origin/<base>
3. clean working tree
4. not behind origin/<base>
Check 2 is deliberately not "must equal main". A feature branch cut from
an up-to-date base is fine; what caused the incident was a LEFTOVER
branch from a merged PR (docs/admin-screenshots), which fails the
ancestor test while a fresh feature branch passes. Verified against the
real commit: `git merge-base --is-ancestor origin/main 5b7ae6d` fails.
AGENTS.md gains the rules the incident earned, the first of which is the
one that matters:
- If the tree looks unexpectedly clean, run `git stash list` BEFORE
concluding anything about your tooling.
- Never bare `git stash` / `pop`. ~20 worktrees on this machine share
ONE stack; another session pushing an entry shifts every index, and
pop deletes on success, so a wrong index is unrecoverable.
- Work in a worktree, not the shared checkout the operator is using.
- The plan is the contract. If an approved item looks wrong, STOP and
say so rather than implementing the alternative -- the same run
re-derived a design that had been explicitly reviewed and rejected,
because the rejected version sounded more reasonable in isolation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
121 lines
5.1 KiB
Markdown
121 lines
5.1 KiB
Markdown
# 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/<base>` 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 '<worktree>/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 <body.md> "<title>" [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`.
|
|
|
|
## 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.
|