Skip to main content

Development

Model Peer is one Bash script. There is no build step and no runtime dependencies beyond the vendor CLIs.

make test # smoke tests against stub CLIs; runs check-sync first
make lint # bash -n over every script, plus shellcheck when installed
make sync # regenerate install.sh's embedded copy of bin/model-peer

Smoke tests use stub CLIs. They do not contact Anthropic, OpenAI, or Google and do not consume model usage.

The duplication invariant

install.sh must work standalone when piped from curl, so it carries a verbatim copy of bin/model-peer inside a heredoc.

Any change to bin/model-peer must be followed by make sync.

make check-sync runs as a prerequisite of make test and fails the build otherwise, and the smoke tests independently install and cmp the result. This is the single easiest way to break the repository.

Releasing

VERSION is the source of truth, but the string is duplicated in the CLI banner, the installer, the pinned curl URLs in the docs, and a smoke-test assertion:

tools/bump-version.sh <major.minor.patch>

That rewrites every occurrence, regenerates install.sh's embedded copy and then sweeps every tracked file for the old string, failing if one survives. The sweep is deliberately broader than the list of files the script rewrites, so a version pinned in a page nobody remembered to add still fails the bump rather than shipping stale. CHANGELOG.md and the lockfile are excluded on purpose: old releases keep their own numbers, and npm dependency versions are not ours.

Write release examples as <major.minor.patch> rather than a real version, so a documentation snippet is never mistaken for a pin. Date the CHANGELOG.md entry, then tag the merge commit on main.

Portability

Target Bash 3.2 — that is what macOS ships, and CI runs macOS as well as Ubuntu. Avoid:

  • declare -A (associative arrays)
  • ${var^^} / ${var,,} case conversion
  • read -t with fractional seconds
  • bare "${arr[@]}" on possibly-empty arrays — use ${arr[@]+"${arr[@]}"}

Architecture notes

Both consultation paths funnel through run_provider, which is the single chokepoint that enforces the guards, pushes the chain, resolves delegation, and computes the depth budget before delegating to run_claude / run_codex / run_gemini. Put policy in run_provider, not in the per-provider runners — those exist only to translate a prompt plus a delegation decision into one vendor's CLI flags.

Keep depth and delegation separate. Depth is a limit; delegation is a permission resolved from the provider's ability to hold it narrowly. If you add a provider, decide its delegation support explicitly — defaulting to unsupported is the safe answer.

New behavior needs a tests/smoke.sh assertion. The stubs make this nearly free, and they can capture generated artifacts: the Gemini stub copies the generated policy into the log so its deny rules can be asserted at every depth.

Docs site

The site in documentation/ is Docusaurus.

cd documentation
npm install
npm start # local dev server
npm run build # production build; fails on broken links

It deploys to GitHub Pages from main via .github/workflows/docs.yml.