97: two-tier parity, shipped one tier at a time
Where we left off
PR #95 (litellm provider-prefix fix) closed the loop on the local agent actually
working — a real dispatch reached Ornith and got a response instead of silently
no-opping. That fixed correctness. It said nothing about safety posture: local
dispatched with the same flag surface as every other agent, no guardrails scoped to
the fact that this agent runs against a small (9B, 4-bit) self-hosted model with no
vendor-side abuse controls behind it.
What moved the goalpost
Brainstorming surfaced that "parity" for the local agent isn't one config, it's two:
a Starter tier (small model, conservative flags, safe defaults) and a Full tier
(larger model, --architect mode, autonomous lint/test) — gated by which model is
actually pinned in .agents/local.json. The open question was whether to build both
tiers now, or ship Starter immediately and defer Full.
The design spec resolved it by adding an A/B test as a gate: dispatch the same battery
of prompts (docstring add, mechanical rename, small extraction, plus the PR #690
runaway-trigger prompt) against both Ornith-1.0-9B-4bit (currently pinned) and
Qwen3.6-27B-4bit (Full-tier candidate), capture diff-correctness/safety-drift/wall-clock
data directly — not from job status — and let that data decide whether Full-tier is
worth building at all. The last open question, resolved explicitly rather than left
implicit: ship Starter-tier guardrails now, against whichever model is currently
pinned, regardless of what the A/B test eventually shows. Re-pinning to a different
model later is a separate, small change — it shouldn't block shipping the safety
guardrails that apply either way.
What this PR ships
Two independent pieces, sequenced per the plan (docs/superpowers/plans/2026-08-03-local-agent-parity-config.md):
The A/B harness (scripts/local_agent_ab_test.py) — a standalone CLI, not a permanent
part of the dispatch path:
def _build_temp_config(base_config: dict, model_id: str) -> dict:
... # pins model_id, unpins everything else, never mutates the original
def run_ab_case(model_id, label, prompt, dispatch_runner=None) -> dict:
... # swaps .agents/local.json, dispatches, restores config in a finally block
run_ab_case backs up and restores .agents/local.json around every run — including on
a raised exception from the dispatch itself — so a crashed A/B run can never leave the
repo's real agent config pointed at the wrong model. Results append as JSONL to
project-docs/decisions/2026-08-03-local-agent-ab-test-results.jsonl, one row per
model/prompt pair, for Claude/PM to read and decide from — the harness itself makes no
judgment call about which model wins.
Starter-tier guardrails (synlynk/local_agent.py), shipped against the currently-pinned
Ornith-1.0-9B-4bit per the resolved open question above:
_STARTER_TIER_GUARDRAIL_FLAGS = [
"--no-auto-lint", "--no-auto-test", "--map-tokens", "0",
]
appended unconditionally to every local dispatch's flags — no --architect, no
autonomous lint/test execution, no repo-map context leak. Full-tier flags are explicitly
not in this codepath; the function's docstring says so directly, so the boundary
survives the next person who edits this file without having read the spec.
Built via four sequential synlynk dispatch codex tasks for the harness (config-transform
→ config I/O/result-row → run_ab_case orchestration → CLI entrypoint) plus one for the
guardrail flags, each reviewed directly — diff read, tests re-run independently, never
trusting job self-report — before fast-forward merging into the branch. Codex caught two
real gaps in the plan itself without a round-trip: a missing import json in a test
snippet, and two pre-existing exact-match flag-list assertions (in both
tests/test_local_agent.py and tests/test_dispatch_local_agent.py) that needed updating
for the new flags — both anticipated in the plan's own regression-risk note, so this was
the plan working as intended, not a surprise.
Where this leaves the local-agent track
Full project suite: 1622 passed, 2 skipped (baseline 1575 + ~15 new tests from this plan +
existing growth, 0 failures). Starter-tier guardrails are live against the currently-pinned
model regardless of what the A/B test later shows. Full-tier (--architect,
autonomous lint/test) remains explicitly out of scope for this plan — a future,
separately-gated change once the A/B data actually justifies re-pinning to a larger model.
Next goalpost
Run the A/B harness for real against both models and read the results. If Qwen3.6-27B-4bit
wins on the data, re-pin .agents/local.json and build the Full-tier flag surface as its
own small, separately-reviewed change — the guardrail boundary this PR drew makes that a
config change plus a new flag set, not a rewrite.