* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
4.3 KiB
Eval model rules: .gstack/eval-models.json
A repository that has rules about which models its evals may use (for example
"Fable in smoke tests only", "no gpt-5.4-mini", "the newest model of each
family") writes them as data in .gstack/eval-models.json. gstack-eval-plan
checks every arm against them: arms, dry-run, pilot start, and every
call row of price-gate. gstack never reads these rules from prose in
CLAUDE.md or any other document. Plan: field list 2, W9
(docs/designs/field-list-2-2026-10-16/PLAN.md).
The file
{
"schema_version": 1,
"allow": ["claude-*", "gpt-*"],
"deny": ["gpt-5.4-mini"],
"scopes": { "full": { "deny": ["claude-fable-*"] } },
"families": {
"claude-opus": ["claude-opus-5-5", "claude-opus-5", "claude-opus-4-8"],
"claude-fable": ["claude-fable-5-1", "claude-fable-5"],
"gpt-sol": ["gpt-6.1-sol", "gpt-6-sol"]
}
}
| Key | Required | Meaning |
|---|---|---|
schema_version |
yes | 1 |
allow |
yes | Model ids or globs an arm may use. An id that no entry matches fails, so a misspelled or new model is never admitted by default. Use ["*"] to allow everything not denied. |
deny |
no | Ids or globs no arm may use. Checked first. |
scopes |
no | Per eval scope (smoke or full), an extra allow and/or deny list. "Fable in smoke only" is scopes.full.deny: ["claude-fable-*"]. |
families |
no | Family name to literal ids, newest first. With this key present, an arm must be the newest id of its family (EVAL_MODEL_OLD_GENERATION otherwise), and an arm in no family fails EVAL_MODEL_GENERATION_UNRESOLVED instead of passing unchecked. |
Globs use * (any run of characters) and ? (one character); every other
character is literal. A rule matches the id as written, the id without a
provider prefix (openai/gpt-5.4-mini matches gpt-5.4-mini), and a Claude id
without its date suffix (claude-opus-5-5-20261001 matches claude-opus-5-5).
An unknown key, an unknown scope, a glob inside families, or an id listed in
two families makes the whole file invalid (EVAL_MODELS_INVALID), and then no
arm passes.
The rules apply whatever gstack's own model catalog (lib/model-catalog.ts)
holds: an uncatalogued model is allowed or denied by the file alone. The
catalog only checks that families is current. If a family lists the
catalog's current frontier or smart model after another id, the file is stale
and invalid.
Order of checks
For each arm at its scope, the first rule that refuses decides:
deny→EVAL_MODEL_DENIEDscopes.<scope>.deny→EVAL_MODEL_DENIED- no
allowentry matches →EVAL_MODEL_DENIED(unknown ids fail closed) scopes.<scope>.allowis set and does not match →EVAL_MODEL_DENIEDfamiliesis set: in no family →EVAL_MODEL_GENERATION_UNRESOLVED; not the family's first id →EVAL_MODEL_OLD_GENERATION
Scope
price-gate checks each call row at that row's scope (GSTACK_EVAL_CALLS v1,
docs/eval-calls.md). arms, dry-run and pilot start take
--scope smoke|full, which defaults to full. So a smoke-only model fails
unless the command says it is a smoke run.
Which copy is read
By default the reviewed copy is read: git show origin/<base>:.gstack/eval-models.json
(--base, default origin/HEAD's branch). A branch cannot loosen the rules it
is checked against. When the working-tree copy differs, an
EVAL_MODELS_NOTE: line says so. --models-from head lints with the working-tree copy
instead. Use it for the PR that adds or changes the file; the source label
says head (unreviewed). When neither copy exists, nothing is checked and
the line says EVAL_MODELS: none .... When the base cannot be resolved but a
working-tree copy exists, the command refuses (EVAL_MODELS_INVALID) rather
than skipping the rules.
Output
EVAL_MODEL: claude-fable-5-1 scope=full denied (scopes.full.deny claude-fable-*) where=dry-run --arms
EVAL_MODEL: claude-opus-5-5 scope=full ok (allow claude-*; newest in families.claude-opus) where=dry-run --arms
EVAL_MODELS: fail refused=1/2 source=origin/main@0123456789ab:.gstack/eval-models.json
Each refusal also prints a result line on stderr ending in its code. dry-run
records the check as checks.model_rules in dry-run.json and fails
DRY_RUN_RESULT. pilot start reserves nothing. price-gate writes no
receipt.