- csv-export-delimiter's grader adds a fourth check: the data rows are
the exported values, with an optional header (the hidden intent says a
header is fine either way). Reference scores 1; both decoys 0.75.
- graderIsSound grades every decoy-* overlay and is sound only when each
scores below 1; --check-graders and the pre-billing check print the
decoy scores through formatGraderScores.
- isValidTrial and completedTrialKeys move to lib.js; coverage.js and
run-intent.js use them from there.
- run.js resumes with completedTrialKeys, so a provider-error or timed-out
key is retried, and writes one transcript per attempt
(<key>__<attemptId>.jsonl, recorded on the row) so a retry never
overwrites the failed attempt's record.
- armSummary computes trials, passes and means over valid rows and counts
the failed attempts apart as providerErrors.
Docs: question-effectiveness.md states the decoy rule and that a
csv-export-delimiter pass in the reported runs reflects the dialect only;
its Measures section and the harness README describe retries, per-attempt
transcripts and the hookless-trial stop.
- questionIdsFor and condensedHintFor take an optional
{ externalContract: true }; without it a code change that handles data
asks data-schema only, and an unknown profile gets the original four
questions and condensed hint.
- The hook reads GATEGUARD_EXTERNAL_CONTRACT_QUESTION with the same
spellings as GATEGUARD_METRICS and passes it to the full denial, the
condensed denial and the metrics record, so all three agree.
- The effectiveness harness's arm patch forwards the option, so an arm
never drops it silently.
Docs: change-profile.md gains an External-contract question section and
qualifies "byte-identical to the fixed four questions"; design-notes.md,
SKILL.md (switch table, data rule, word-for-word set) and CHANGELOG say
the fifth question is opt-in; question-effectiveness.md and the harness
README note that the runner clears GATEGUARD_* variables, so a gate arm
from a current tree measures the gate without the question.
skill-triggers@1.json: registryDigest refreshed for the SKILL.md body
change; the description and triggers are unchanged.
The existing harness cannot measure what it set out to measure. Every trap
hides its deciding fact in the repository, which agents reliably discover, and
trials run one-shot under "do not ask clarifying questions", so a fact-forcing
gate is evaluated with no user to answer it. Its scenarios also declare the
question the gate ought to raise, chosen by reading the gate's own taxonomy, so
the suite passes by construction and can never find a missing question.
Add a second runner that measures effectiveness instead of coverage:
- scenarios-intent/ keeps the deciding fact in intent.json, as a choice between
plausible conventions the repository cannot settle, so an agent that does not
ask can only guess and the ungated arm cannot sit at the ceiling.
- user-sim.js plays that user with a separate cheap model: it answers the
question in front of it, never volunteers, and reports which fact ids it
disclosed, so asking something and asking the deciding thing are told apart.
An unparseable judge is a recorded failure, not a silent stonewall.
- session.js runs multi-turn sessions joined by --session-id and --resume, and
tells every arm the user is reachable, so the comparison is symmetric and the
gate's contribution is whether it makes the agent ask the deciding question.
- coverage.js classifies each trial from behaviour alone into working, lucky, or
a follow-through, targeting, silence or coverage hole, and reports the
questions the gate actually raised rather than asserting declared ones.
- run-intent.js refuses minus-target, which needs targets these scenarios
deliberately lack, and checks every grader separates the start, the trap and
the reference before a session is billed.
A scenario counts as carrying no information only when nothing varies across
arms. Trap strength is reported separately: the ungated arm failing is what a
working trap looks like, so it must not make a scenario unusable.