Lane-adoption playbook
Sibling of
rubric.md, not a replacement for it. The rubric decides whether to adopt a community TOOL (a skill, hook, MCP server). This playbook is for a different object: a model LANE — an entry inscripts/lanes/lanes.jsonan operator or the dispatch guard can route implementation work to (GLM, claudex, a future frontier model). A lane is not evaluated once and filed away like a tool ADR; it is profiled and tailored per axis, then measured into readiness. Reference the rubric for the general trust-posture/measurement vocabulary (§2, §4) — it isn’t repeated here.
Why
Every lane this project has onboarded arrived with the same failure shape: the harness inherited some OTHER lane’s tuning — a timeout sized for Codex’s latency, a context-compaction default sized for a 200k-token model, an identity file that hardcodes a different provider’s name — and the new lane either timed out spuriously, compacted early, or ran under an under-specified persona, before anyone measured what the new lane actually needed. The GLM onboarding (the GLM lane is now dormant, HIMMEL-1967) is the worked example this playbook generalizes: each axis below is a knob GLM’s rollout tripped over, with the fix now checked into the repo as evidence of what “profile it, then tailor it” looks like in practice.
The rule, once per axis: PROFILE the new lane’s real behavior before you TAILOR any config to it. Never inherit another lane’s number.
How to use this
Run the seven axes below against the new lane, in order, before it earns any default routing. Each axis names what to measure on the new lane and what config/code knob the measurement feeds. Where the axis has GLM evidence already in the repo, it’s cited by path/line so you can see the pattern before repeating it. Where the brief for this ticket named a ticket number this worktree could not find checked into the repo, that’s flagged as a discrepancy rather than guessed at — treat those as open follow-up tickets, not yet-landed evidence.
1. Latency budget
Profile: the new lane’s real wall-clock response time and payload size under a representative task — not a number carried over from another lane.
Tailor: CRITIC_TIMEOUT_SECS in scripts/cr/critic-panel.sh (default
240, set at line ~214: CRITIC_TIMEOUT_SECS="${CRITIC_TIMEOUT_SECS:-240}")
is the per-critic-member wall-clock timeout applied to every model in the CR
panel, regardless of which backend answers. It also supports a per-row
override (_resolve_member_timeout, line ~316) — use that to give one lane
its own number instead of bumping the shared default for everyone.
GLM evidence: glm-5.2[1m] measured ~205s / 23KB response on a real
panel run — close enough to the 240s ceiling that real-world variance tips it
into a spurious timeout. The 240s constant itself predates
GLM (CHANGELOG.md: “raise CRITIC_TIMEOUT_SECS default 150
to 240s”) — it was sized around the panel’s existing (Codex-class) members,
then a new lane inherited it without being measured against it.
Action: before wiring a new lane into any dispatcher with a hardcoded wall-clock bound, measure its typical response latency and size, then set a per-lane override rather than trusting the shared default to fit.
Discrepancy flagged: the spurious-timeout finding is not cited anywhere in-repo (no
comment, no CHANGELOG line) — the 240s constant and its origin are
real and grounded (scripts/cr/critic-panel.sh), but the GLM
timeout-measurement finding itself is evidence carried in the onboarding brief,
not yet landed as a code comment or test. Treat it as the motivating
observation, not as something you can grep and confirm today.
2. Context window
Profile: the lane’s real backend context ceiling AND its cost-optimal
operating window — these are not the same number. scripts/lanes/lanes.json’s
$context-note states the distinction explicitly: context.windowTokens is “the
lane’s cost-optimal default operating window… don’t route work that won’t
fit” — codex lanes default to 272k even though the real backend window is
~372k, because 272k is the 2x-pricing cliff, not a hard ceiling.
Tailor: two separate config points, not one — there’s no automatic projection from one to the other:
lanes.json’s per-lanecontext.windowTokensfield (glm: 1000000,claudex: 900000 — operator ruling, 2026-08-17, launcher default unchanged — see the registry,haiku: 200000) feeds the lane resolver (scripts/lanes/resolve.mjs), which uses it for the/lanes[ctx: N]display annotation only — no dispatch guard reads it; the dispatch-side window preflight is configured separately (spawn-claudex.tshardcodes its own 272000 constant) — and it does not set any env var itself.CLAUDE_CODE_AUTO_COMPACT_WINDOW(the Claude Code env var that actually controls when the harness starts compacting) is set by the launcher from its own env knob: the GLM launcher’sGLM_CONTEXT_WINDOWandspawn-glm --context big|smallpreset:big=glm-5.2[1m]+CLAUDE_CODE_AUTO_COMPACT_WINDOW=1000000;small=glm-5.2+ 200000; the codex lane’s twin,CODEX_CONTEXT_WINDOW(docs/tooling-catalog.mdclaude-codexsection).
Configure both for a new lane — the registry value and the launcher env knob — since neither derives the other; setting only one leaves the other silently stale.
GLM evidence: without an explicit window, Claude Code assumes its ~200k
default for an unrecognized model slug and compacts/rejects early — the
documented “prompt too long” deaths. Setting
CLAUDE_CODE_AUTO_COMPACT_WINDOW=1000000 for the [1m] GLM variant fixed it.
Action: never let a new lane’s model slug silently fall through to Claude
Code’s built-in default window. Look up (or benchmark) the backend’s real
ceiling, decide the cost-optimal operating point, and set the
auto-compact-window knob explicitly — same pattern as glm/claudex above.
3. SOUL/persona
Profile: does the new lane get its own identity file matched to its actual capability tier, or does it inherit an identity written for a different provider, or default to a persona sized for a weaker role?
Tailor: two SOUL assets already exist in scripts/hermes/assets/, and
neither is a drop-in fit for a new frontier lane:
himmel-agent.SOUL.md— the senior identity, installed onto thehimmel_agenthermes profile byscripts/hermes/install-himmel-profile.sh. Its opening line hardcodes the provider: “running as the main tier on Codex (GPT-5.5)” — and the installer’s own profile description literally reads “himmel’s main-tier orchestrator (Codex/GPT-5.5)” (install-himmel-profile.shline 97). This SOUL is Codex-provider-bound by construction, not generic.free-tier.SOUL.md— the junior, read-only identity for free critics (qwen3-coder-plusanchor): terse, ~32K context budget, JSON-only output contract, explicitly “You do not write code to disk, run git, open PRs.”
GLM evidence: a frontier lane dispatched under himmel-agent.SOUL.md
verbatim inherits a false provider claim; dispatched under
free-tier.SOUL.md it inherits a read-only ceiling that under-drives a
full-capability model. Neither fits — the fix is a per-lane senior
profile: clone the senior SOUL’s structure (identity, working principles,
precedence ladder, hard limits) but swap the provider-specific line for the
new lane’s actual backend.
Discrepancy flagged: the nearest in-repo precedent
(scripts/agents-md/debrand.json) is a different,
related problem: it’s the CLAUDE.md → AGENTS.md debranding pass that
neutralizes Fable-specific identity language (“the Fable main thread” →
“the main thread”) so a non-Claude driver reading the shared CLAUDE.md
doesn’t misidentify itself. That’s about a Codex/GPT session correctly
NOT reading Claude-branded prose as its own identity — the opposite move
from authoring a dedicated senior SOUL for a new provider. Both are
persona-correctness work; they are not the same change.
4. Prompt family
Profile: which scaffolding family the new lane’s model name resolves to.
Tailor: family_for_model() in scripts/cr/critic-first-pass.sh
(lines 88–101) — a case statement mapping a lowercased model-name substring
to gpt | open | claude, each with different prompt scaffolding (GPT/codex:
explicit non-contradiction + spec tags; open: rigid format-obedience because
open models “drift from the contract and over-report”; Claude: XML +
IMPORTANT markers). Order matters — gpt-oss/gptoss must match before the
real-GPT branch or they’d be misfiled.
GLM evidence: *glm* falls into the open family bucket (alongside
qwen/kimi/deepseek/mistral/llama) — rigid format-obedience scaffolding, the
same bucket the unknown-model fallback (*) echo open ;;) defaults to as
“the safest default.”
Action: before wiring a new lane’s model name into critic-first-pass.sh
or any prompt-family-aware caller, check which branch its name resolves to.
An unmatched name silently lands in open — verify that’s actually the right
scaffolding for the new model’s failure modes, don’t assume the fallback is
correct just because it doesn’t error.
5. Harness choice
Profile: does the task need a fire-and-forget one-shot dispatch, or a full himmel-harness session (worktree, hooks, guardrails, durable transcript)? Measure the actual requirement — isolation, durability, per-dispatch model control — don’t default to whichever is easiest to wire.
Tailor: two structurally different spawn shapes exist:
- Hermes one-shot (
scripts/hermes/invoke.sh/dispatch-trusted.sh) — no worktree;invoke.shwall-clock timeboxes + Nth-identical-deny aborts it (a parity_guard DENY is not terminal to hermes upstream),-zauto-approval with atodo-only default toolset. Cheapest to dispatch, weakest isolation. - Full-session
cc-<lane>(scripts/claude-glm,scripts/claude-codex) — runs the complete Claude Code harness (skills, hooks, guardrails, worktrees) over the lane’s backend. Dedicated worktree + branch, durable session artifacts, native Claude Code hook coverage.
Evidence: docs/internals/worker-spawn-matrix.md is the
grounded comparison — a full matrix of worktree isolation, guard/hook
coverage, per-dispatch model selection, session artifacts, push/PR authority,
permission handling, and supervision signal across all five spawn paths
(native Hermes subagent, Hermes one-shot, external Claude Code worker,
GLM worker, Codex worker). Its verdict: “the Hermes one-shot lane has the
clearest model/toolset selector… the GLM worker has the clearest worker
artifact contract” — the two shapes trade off differently, and the matrix
names which cells are UNVERIFIED per path rather than assuming parity.
Discrepancy flagged: the ticket the brief cites for this axis has no
reference anywhere in this repo (clean grep across code and docs). The
comparison this axis needs is fully grounded instead by
worker-spawn-matrix.md — cite that as the evidence base.
6. Trust/egress posture
Profile: is the new lane’s provider declared in the egress matrix at all? If not, every corpus defaults to deny for it.
Tailor: scripts/guardrails/egress-matrix.json is keyed by provider ×
corpus × purpose — there is no lane axis. What to do depends on whether the
new lane’s backend is already a declared provider:
- Provider already declared (e.g. another lane already runs on
zai-glm): add nothing toproviders. Reusing it means the new lane automatically inherits every existingrulesrow for that provider, across every corpus — review those rows before onboarding, since a rule scoped to the existing lane’s use case now also covers the new one. If the new lane needs different clearance, give it a distinct provider identity instead of reusing the shared one. - Provider genuinely new: add a
providersentry (region + one-line note — see thezai-glm/openai-codexrows for the shape), then explicitrulesrows for each corpus the lane needs (corpusxproviderxpurpose→allow/allow+log/conditional/deny). - Nothing at all, if the lane should stay denied everywhere for now — the
file’s own
"default": "deny"already covers that.
GLM evidence — a full ratify-then-reverse cycle. zai-glm was declared
("region": "CN") with narrow, dated, ticketed rules — luna-personal ×
zai-glm × extraction was allow+log, ratified 2026-07-17
with a measured comparison against the prior incumbent recorded directly in
the why field, and handover-state × zai-glm × inference was
conditional on “brief-scoped… never bulk corpus runs.” When the lane was
dropped (Coding Plan lapsed 2026-08-17) all four cells were
reversed to explicit deny with the reversal recorded in each why,
and the providers note was annotated DE-LISTED — the same
shape used earlier for DeepSeek/Alibaba. This is the pattern to copy
when a lane you onboarded goes away: reverse the rows, never delete them. A
deleted row falls through to "default": "deny" and reads identically to a
provider that was never permitted, destroying the audit trail of what was
once open and why. PHI (salus corpus) is hard-denied for every provider with
no override, structurally — this doesn’t change per lane.
Action: before a new lane touches any vault or handover corpus, check
whether its provider is already declared. An undeclared provider is
default-DENY ("default": "deny", scripts/guardrails/egress-matrix.json
line 176) — add its provider row and the specific per-corpus rules it needs.
A declared provider is different: reusing it is not free, since the
matrix has no lane axis and every existing rule for that provider now also
covers the new lane — review those rows for fit rather than assuming they
were written with this lane’s use case in mind, and register a distinct
provider identity if they don’t fit.
7. Measure before trust
Profile: run the rubric’s measurement protocol against the new lane on real work, and clear the structural readiness gate before it earns default routing — readiness is measured, never asserted.
Tailor:
- The measurement method:
docs/tool-adoption/rubric.md§4 (baseline vs treatment on outcome-per-session signals, not token count) — the same protocol this playbook’s sibling doc uses for tools, reused here for lanes. - The structural gate:
scripts/lanes/lane-readiness.mjs+readiness.passesRequiredinscripts/lanes/lanes.json. Readiness is computed from trailing consecutive PASS rows in the verify-return ledger (scripts/lanes/verify-return.mjs) — a single FAILED row resets the streak to zero. A lane below its gate readsdown, and the dispatch guard (scripts/hooks/guard-implementor-dispatch.sh) treatsdownas skip-toward-another-lane, never as a hard refusal.
GLM evidence: lanes.json’s $readiness-note records the live operator
ruling (2026-08-07): glm gated at 10 consecutive passes (down since the
startup-hang class was found), claudex on probation at 5
consecutive passes to graduate. A lane can be fully funded (quota
available) while still not ready (an operator ruling holds it down) —
funding and readiness are deliberately different questions
(lane-readiness.mjs header comment).
Action: don’t route default work to a new lane on the strength of a
pitch or a single successful dispatch. Register it in lanes.json with a
readiness.passesRequired gate, let the verify-return ledger accumulate real
passes, and only drop the gate (or raise passesRequired back up on a
regression) once the trailing-pass evidence supports it. Treat this as the
dispatch gate, not the whole of “measure before trust” — clearing it only
shows the lane’s dispatches keep passing their own verify-return checks; it
says nothing about outcome-per-session on real work. Run the rubric §4
measurement independently and don’t let a clean pass streak substitute for it.
Discrepancy flagged: the ticket the brief cites for the benchmark step
has no reference anywhere in this repo. The grounded artifacts for “measure
before trust” are the rubric’s own protocol
(docs/tool-adoption/rubric.md §4) and the concrete structural
gate that now backs it (scripts/lanes/lane-readiness.mjs) —
cite those.
Summary checklist
Run in order; each row’s “tailor” artifact is where the fix lands.
| # | Axis | Profile (measure) | Tailor (config/code) |
|---|---|---|---|
| 1 | Latency budget | Real wall-clock + payload size on representative work | CRITIC_TIMEOUT_SECS / per-row override in scripts/cr/critic-panel.sh |
| 2 | Context window | Real backend ceiling vs cost-optimal operating point | CLAUDE_CODE_AUTO_COMPACT_WINDOW, lanes.json context.windowTokens, lane-specific *_CONTEXT_WINDOW env |
| 3 | SOUL/persona | Capability tier the lane actually has | A per-lane senior SOUL cloned from himmel-agent.SOUL.md’s structure, provider line swapped |
| 4 | Prompt family | Which family bucket the model name resolves to | family_for_model() in scripts/cr/critic-first-pass.sh |
| 5 | Harness choice | Isolation/durability the task actually needs | Hermes one-shot vs full-session cc-<lane> launcher — see docs/internals/worker-spawn-matrix.md |
| 6 | Trust/egress posture | Which corpora the lane is cleared against | scripts/guardrails/egress-matrix.json — provider entry + explicit rules |
| 7 | Measure before trust | Outcome-per-session over real work, sustained (rubric §4 — independent of the dispatch gate below) | Structural dispatch gate: readiness.passesRequired in scripts/lanes/lanes.json + scripts/lanes/lane-readiness.mjs (a trailing verify-return PASS streak, not outcome evidence) |
Ground every number on the new lane itself. An inherited default is exactly the failure this playbook exists to catch.