| name | concept-eval |
| description | Stage-2 held-out evaluation as one slash command. The orchestrator (researcher session) chains freeze_concepts → instantiate_concepts → run_heldout_eval → aggregate_heldout. Reports headline ASR (broken / |held-out|) and per-concept ranking on the cell named in RUN_HINT.md. |
| argument-hint | run_code [--parallelism P] — e.g. first_run01 |
Stage 2 — Concept Evaluation
This is the second half of the autoresearch loop. The Stage-1 discovery
skill (/autoresearch-redteam-discovery) accumulates COUNTED concepts
in attacks/<run>/vcg.md. Stage 2 freezes those concepts, has
instantiate_concepts.py (a single-shot, sandboxed Claude Code
instantiator) materialise
one attack per held-out instance, runs them all against the victim cell
named in RUN_HINT.md, and prints the headline ASR.
The whole pipeline is four host-side commands; this skill chains them.
Constants
| Constant | Default | Notes |
|---|
DEFAULT_PARALLELISM | 4 | Concurrent held-out attacks. --parallelism overrides. |
STAGE2_OUTPUT_ROOT | held_out_eval/<run>/ | freeze + leaderboard outputs live here. |
HELDOUT_ATTACK_ROOT | attacks/heldout_<run>/ | One attack.json per held-out instance. |
Step 0 — Sanity check + endpoint preflight
[ -f RUN_HINT.md ] || { echo "RUN_HINT.md missing — are you in a worktree?" >&2; exit 1; }
[ -f attacks/$ARGUMENTS[0]/vcg.md ] || { echo "vcg.md missing — Stage 1 hasn't run yet" >&2; exit 1; }
VICTIM=$(grep '^# Victim:' RUN_HINT.md | awk '{print $3}')
SCENARIO=$(grep '^# Scenario:' RUN_HINT.md | awk '{print $3}')
MODEL=$(grep '^# Model:' RUN_HINT.md | awk '{print $3}')
Endpoint preflight — Stage 2 uses three independent model-facing calls:
| Slot | Used in step | Required env vars |
|---|
| Claude Code instantiator | Step 2 (instantiate_concepts.py) | none; optional INSTANTIATOR_CLI_MODEL only |
| Victim | Step 3 (run_heldout_eval.sh runs the attacker × victim cell) | ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY |
| Judge | Step 3 (LLM-prompt judges in run_attack's judge call) | JUDGE_BASE_URL, JUDGE_API_KEY |
The Step-2 Claude Code instantiator is single-shot and sandboxed: exactly one
generation call per held-out instance, no victim/judge test-time loop.
With claude_cli it shells out to claude -p with all file/exec tools
disallowed, so the design call is a pure prompt→JSON transform that
cannot read judge_data.json, the discovery train payloads, or
AGENT_LOG.md — leakage is prevented by construction. See
docs/shared-references/instantiate-protocol.md for the full
white/blacklist.
For each slot, if its env vars are unset OR the user hasn't
confirmed the provider this session, run an AskUserQuestion
per docs/ENVIRONMENT.md (valid providers per slot). Picker:
OpenRouter / OpenAI direct / Anthropic direct (UI catch-all
handles custom endpoints like DeepSeek-anthropic-compat). After
the user picks, export the corresponding env vars for the
session. If a chosen slot ends up missing its key, stop with a
hard error instead of letting the run fail mid-batch.
Step 1 — Freeze COUNTED concepts
uv run python scripts/freeze_concepts.py $ARGUMENTS[0]
If 0 COUNTED concepts found, stop early — Stage 2 has nothing to evaluate. Print a clear message and exit.
Step 2 — Single-shot sandboxed CLAUDE instantiator picks one concept per held-out instance
uv run python scripts/instantiate_concepts.py $ARGUMENTS[0] --scenario $SCENARIO \
[--threat-model indirect|direct|all] [--only-indirect] [--workers 3]
The Claude Code instantiator is intentionally a different role from the victim (avoids
same-model self-attack bias) and runs single-shot — one generation
call per instance, no victim/judge test-time loop. For each held-out
instance it:
- Reads
frozen_concepts.json — but ONLY the abstract concept fields
(mechanism / enabling_condition / transfer_prediction / the
attack_template-as-one-example); the per-version observations /
provenance discovery details are NEVER put in the prompt.
- Sources the per-instance context from the CLEAN instance
(
bench.load_instance), never judge_data.json.
- Picks the single COUNTED concept whose mechanism most plausibly
transfers, and instantiates it into one concrete attack.json (per the
scenario's
attack_schema).
Sandbox (anti-cheat). Step 2 is claude -p with
Bash,Edit,Write,Read,Glob,Grep,WebSearch,WebFetch,...
all disallowed, so the instantiator physically cannot read the answer keys
even if the prompt named them. See the white/blacklist in
docs/shared-references/instantiate-protocol.md.
Mode-A interceptor targeting (dtagent). The scenario tool_catalog
now names the channel's READ-tool family inferred from the
task_instruction + the instance's servers, and the instantiator attaches
the interceptor to that family (multiple specs, or tool: "*") rather
than a single guessed method the victim may never call.
Instance filter. --only-indirect (alias --threat-model indirect)
regenerates only indirect instances and leaves direct attack.json
untouched; a threat-model filter implies --overwrite (use
--no-overwrite to resume instead). The static validate_attack
retry/repair still runs — that is a structural delivery check, not a
victim-feedback loop.
Step 3 — Run all held-out attacks in parallel
PARALLELISM=${PARALLELISM:-4}
bash scripts/run_heldout_eval.sh $ARGUMENTS[0] $PARALLELISM $VICTIM $SCENARIO $MODEL
One attempt per held-out instance. No retries. Containers get the same 500K input / 50K output cap as Stage 1.
Step 4 — Aggregate ASR
uv run python scripts/aggregate_heldout.py $ARGUMENTS[0]
Step 5 — Per-concept ranking (optional but printed)
uv run python -m autoresearch_redteam.concept_rank \
--eval-matrix held_out_eval/$ARGUMENTS[0]/eval_matrix.json 2>/dev/null \
|| echo "(per-concept ranking skipped — eval_matrix.json not produced; expected when only one cell evaluated)"
Step 6 — Done
Print summary block:
Stage 2 done for <run>.
cell: <VICTIM> × <SCENARIO> × <MODEL>
COUNTED concepts evaluated: <N>
held-out instances: <M>
headline ASR: <broken>/<M> = <pct>%
leaderboard: held_out_eval/<run>/leaderboard.json
heldout attacks/results: attacks/heldout_<run>/
When to invoke
- After Stage 1 stops (monitor STOP, outer cap, or manual
touch attacks/<run>/STOP).
- Stage 2 is single-shot — do not
/loop it. Run once per (Stage-1 run × OUTER_CAP=100) pair.
Failure modes
| Symptom | Likely cause | Fix |
|---|
| Step 1 finds 0 COUNTED | Stage 1 ended before any concept promoted | Re-run Stage 1 longer, OR lower promotion gate, OR accept ASR = 0 |
Step 2 fails with "claude CLI not on PATH" | Not logged into Claude Code in this shell | log in / which claude |
| Step 3 hangs on first attack | Docker daemon down, or victim endpoint 401 | Probe with the QUICKSTART troubleshooting block |
| Step 4 shows ASR > 1 / 1 | Scenario's heldout.json not loadable | Check Read(plugins/scenarios/**/heldout.json) settings deny — this script is host-side so it should bypass the deny via subprocess |