diff --git a/CLAUDE.md b/CLAUDE.md index 8670bd1..e8f995b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,16 @@ document, or run staged critique -> defend -> tighten passes. bash ./co-evolve-bouncer.sh --vanilla "What is the strongest version of this argument?" bash ./co-evolve-bouncer.sh --vanilla --bounce-only docs/plan.md bash ./co-evolve-bouncer.sh --vanilla --chain "Should we ship this migration?" +bash ./co-evolve-bouncer.sh --vanilla --adversarial --bounce-only docs/plan.md +bash ./co-evolve-bouncer.sh --vanilla --adversarial --agents claude,claude "Same-model adversarial review" ``` +`--adversarial` swaps the reviewer's 1-line role for a structured falsification +persona (vendored from the compound-engineering adversarial-document-reviewer; +templates/co-evolve/role-reviewer-adversarial.md). Cross-AI by default; with +`--agents claude,claude` it is an internal same-model adversarial review. Pair +with `--chain` or `--bounces 3` on dense documents (marker-flooding risk). + ### Agent Bouncer (`agent-bouncer/`) Legacy runner (still used by tests/experiments) that bounces any markdown diff --git a/README.md b/README.md index 1ec7ddc..b3fca77 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,17 @@ existing document, or run staged critique -> defend -> tighten passes. ```bash bash ./co-evolve-bouncer.sh --vanilla "What is the strongest version of this argument?" bash ./co-evolve-bouncer.sh --vanilla --chain "Should we ship this migration?" +bash ./co-evolve-bouncer.sh --vanilla --adversarial --bounce-only docs/plan.md +bash ./co-evolve-bouncer.sh --vanilla --adversarial --agents claude,claude "Same-model adversarial review" ``` +`--adversarial` swaps the reviewer's 1-line role for a structured falsification +persona (premises, assumptions, decisions, complexity, alternatives). It rides +the normal cross-AI bounce, or runs single-vendor with `--agents claude,claude` +when you want an internal same-model review. An aggressive persona can leave +markers open at the default 2 passes; pair it with `--chain` or `--bounces 3` +on dense documents. + ### [Agent Bouncer](agent-bouncer/) A legacy runner (still used by tests/experiments) that bounces any markdown diff --git a/co-evolve-bouncer.sh b/co-evolve-bouncer.sh index e770329..613492d 100644 --- a/co-evolve-bouncer.sh +++ b/co-evolve-bouncer.sh @@ -11,6 +11,10 @@ EXOCORTEX_QUERY="" CONTEXT_FILE="" AUDIENCE="" LENS="" +# Adversarial reviewer persona (falsification method, vendored from the +# compound-engineering adversarial-document-reviewer). Default off so runs +# without the flag are byte-identical to the pre-persona bouncer. +ADVERSARIAL=false CHAIN=false MAX_BOUNCES=2 AGENT_A="claude" @@ -67,7 +71,9 @@ PROTOCOL_TEMPLATE="$SCRIPT_DIR/agent-bouncer/templates/bounce-protocol.md" # Validate templates exist for _tmpl in "$TEMPLATE_DIR/role-reviewer-light.md" "$TEMPLATE_DIR/role-composer-light.md" \ + "$TEMPLATE_DIR/role-reviewer-adversarial.md" \ "$TEMPLATE_DIR/chain-critique.md" "$TEMPLATE_DIR/chain-defend.md" \ + "$TEMPLATE_DIR/chain-critique-adversarial.md" \ "$TEMPLATE_DIR/chain-tighten.md" "$TEMPLATE_DIR/adjudicate.md" \ "$PROTOCOL_TEMPLATE"; do [[ -f "$_tmpl" ]] || die "Missing template: $_tmpl" @@ -89,6 +95,10 @@ Options: --context FILE Include a file as background context (not bounced; one file, concatenate if needed) --audience WHO Prime agents for a specific reader --lens NAME Use a named adversarial lens (replaces auto-shaped roles) + --adversarial Structured adversarial reviewer persona (falsification + method: premises, assumptions, decisions, complexity, + alternatives). Composes with --lens (lens becomes the + focus) and --chain (swaps the critique stage). --chain Use staged passes: critique -> defend -> tighten --bounces N Max bounce passes (default: 2, ignored with --chain) --agents A,B Agent pair (default: claude,codex) @@ -143,6 +153,7 @@ while [[ $# -gt 0 ]]; do --context) CONTEXT_FILE="$2"; shift 2 ;; --audience) AUDIENCE="$2"; shift 2 ;; --lens) LENS="$2"; shift 2 ;; + --adversarial) ADVERSARIAL=true; shift ;; --chain) CHAIN=true; shift ;; --bounces) MAX_BOUNCES="$2" @@ -460,7 +471,16 @@ fi # improve the composed draft, so that draft is the comparison baseline. BASELINE_FILE="original-input.md" [[ "$RUN_MODE" == "compose" ]] && BASELINE_FILE="compose-output.md" -init_bounce_state "$STATE_FILE" "co-evolve-bouncer.sh" "$RUN_MODE" "$TASK" "$INPUT_TYPE" "$BASELINE_FILE" "working.md" +# Reviewer persona is orthogonal to RUN_MODE (the scorer's baseline logic keys +# off mode, so persona must never become a mode value). Precedence mirrors +# build_reviewer_preamble: adversarial > lens > light. +REVIEWER_PERSONA="light" +if [[ "$ADVERSARIAL" == "true" ]]; then + REVIEWER_PERSONA="adversarial" +elif [[ -n "$LENS" ]]; then + REVIEWER_PERSONA="lens" +fi +init_bounce_state "$STATE_FILE" "co-evolve-bouncer.sh" "$RUN_MODE" "$TASK" "$INPUT_TYPE" "$BASELINE_FILE" "working.md" "$REVIEWER_PERSONA" # Any fatal exit (die, set -e, auth abort) marks the run aborted so the # scorer never issues a quality verdict for a half-finished run. @@ -548,7 +568,22 @@ fi # --- Role Preamble Generation --- build_reviewer_preamble() { - if [[ -n "$LENS" ]]; then + # --adversarial wins over --lens: adversarial is the METHOD, lens the FOCUS, + # so a lens given alongside it composes as a focus line instead of replacing + # the persona. Adversarial-off paths below are byte-identical to pre-persona. + if [[ "$ADVERSARIAL" == "true" ]]; then + local preamble + preamble=$(cat "$TEMPLATE_DIR/role-reviewer-adversarial.md") + if [[ -n "$LENS" ]]; then + preamble="${preamble} +Focus your adversarial review through this lens: ${LENS}." + fi + if [[ "$SKIP_INTERVIEW" != "true" && -n "$AUDIENCE" && "$AUDIENCE" != "general" && "$AUDIENCE" != "auto" ]]; then + preamble="${preamble} +Evaluate this as if you are a ${AUDIENCE} reading it. What would they find unconvincing, unclear, or missing?" + fi + echo "$preamble" + elif [[ -n "$LENS" ]]; then echo "You are the ${LENS} reviewing this work. Be adversarial from that perspective. Every critique must include a concrete alternative." elif [[ "$SKIP_INTERVIEW" == "true" ]]; then cat "$TEMPLATE_DIR/role-reviewer-light.md" @@ -780,7 +815,15 @@ run_bounce_phase() { current_agent="$AGENT_A" if [[ "$CHAIN" == "true" ]]; then case "$pass" in - 1) role_preamble=$(cat "$TEMPLATE_DIR/chain-critique.md") ;; + 1) + # --adversarial swaps only the critique stage; defend/tighten keep + # their templates (the persona is a critique method, not a chain). + if [[ "$ADVERSARIAL" == "true" ]]; then + role_preamble=$(cat "$TEMPLATE_DIR/chain-critique-adversarial.md") + else + role_preamble=$(cat "$TEMPLATE_DIR/chain-critique.md") + fi + ;; 3) role_preamble=$(cat "$TEMPLATE_DIR/chain-tighten.md") ;; esac role="critique" @@ -1099,6 +1142,10 @@ log " Input: $INPUT_TYPE" log " Task: $(echo "$TASK" | head -c 80)" log " Compose: $AGENT_A" log " Bounce: $AGENT_A / $AGENT_B" +log " Persona: $REVIEWER_PERSONA (reviewer)" +if [[ "$AGENT_A" == "$AGENT_B" ]]; then + log " NOTE: same-model bounce ($AGENT_A vs $AGENT_B) — no cross-vendor disagreement; persona and per-role seats are the only independence between passes." +fi # v1.5 Phase 1 (A-4b): resolved per-role seats. Reviewer runs on AGENT_A (odd # passes / critique+tighten); composer runs on AGENT_B (even passes / defend). # The compose PHASE also runs the composer role, but on AGENT_A — its resolved diff --git a/evals/BOUNCE-RUNNER-CONTRACT.md b/evals/BOUNCE-RUNNER-CONTRACT.md index 62ba89d..c198716 100644 --- a/evals/BOUNCE-RUNNER-CONTRACT.md +++ b/evals/BOUNCE-RUNNER-CONTRACT.md @@ -45,6 +45,7 @@ a pass artifact missing. "input_type": "file | string | pipe", "baseline_file": "original-input.md", "final_file": "working.md", + "reviewer_persona": "light | adversarial | lens", "status": "running | complete | aborted", "convergence_status": "converged | adjudicated | stuck | null", "started_at": "2026-06-10T22:00:00Z", @@ -103,6 +104,12 @@ Field rules: fail a gate on it. Only an explicit `stuck` blocks. Only `co-evolve-bouncer.sh` writes this field today; `agent-bouncer.sh` leaves it `null`. +- `reviewer_persona` — which reviewer persona shaped the critique passes: + `light` (default 1-line role), `adversarial` (falsification persona, the + `--adversarial` flag), or `lens` (`--lens` free-text override). Additive to + `1.1`: absent means `light` (pre-persona states, agent-bouncer). Consumers + MUST NOT fail on its absence and MUST NOT branch scoring on it — persona is + orthogonal to `mode` and to the baseline rules above. - `passes[].contested` / `clarify` — marker counts of the **clean** output, counted by `lib/co-evolution.sh::count_markers` (code-fence-aware). The scorer reuses the same function; counts must match. @@ -111,7 +118,7 @@ Field rules: ## Writer helpers (lib/co-evolution.sh) -- `init_bounce_state ` +- `init_bounce_state [reviewer_persona]` — the optional 8th arg defaults to `light` - `append_bounce_pass ` - `set_bounce_convergence_status ` — set the convergence outcome (co-evolve only; call before `finalize_bounce_state`). - `finalize_bounce_state ` diff --git a/lib/co-evolution.sh b/lib/co-evolution.sh index b294c1a..625a1e2 100644 --- a/lib/co-evolution.sh +++ b/lib/co-evolution.sh @@ -1369,6 +1369,10 @@ init_bounce_state() { local input_type="${5:-}" local baseline_file="${6:?baseline file required}" local final_file="${7:?final file required}" + # Optional: which reviewer persona shaped the critique passes + # (light|adversarial|lens). Additive to bounce-state/1.1 — callers that + # omit it (agent-bouncer) default to "light", the pre-persona behavior. + local reviewer_persona="${8:-light}" if ! command -v jq >/dev/null 2>&1; then log "WARNING: jq unavailable — bounce state.json will not be written (scorer falls back to artifact parsing)" @@ -1382,6 +1386,7 @@ init_bounce_state() { --arg input_type "$input_type" \ --arg baseline "$baseline_file" \ --arg final "$final_file" \ + --arg persona "$reviewer_persona" \ --arg now "$(bounce_state_now_utc)" \ '{ schema: "bounce-state/1.1", @@ -1391,6 +1396,7 @@ init_bounce_state() { input_type: $input_type, baseline_file: $baseline, final_file: $final, + reviewer_persona: $persona, status: "running", convergence_status: null, started_at: $now, diff --git a/skills/co-evolution/SKILL.md b/skills/co-evolution/SKILL.md index be92475..dd12832 100644 --- a/skills/co-evolution/SKILL.md +++ b/skills/co-evolution/SKILL.md @@ -6,7 +6,8 @@ description: > using [CONTESTED]/[CLARIFY] markers until it converges. Triggers on "co-evolution", "co-evolve", "co evolve", "bounce", "bounce document", "agent bouncer", "refine with another agent", "cross-AI refinement", - "stress test this", and "have two AIs review this". + "stress test this", "have two AIs review this", "adversarial review", + and "red team this". allowed-tools: Bash, Read, Write, Glob, AskUserQuestion --- @@ -77,6 +78,20 @@ repo=$(resolve_co_evolution_repo) || exit 1 bash -lc 'cd "$1" && bash ./co-evolve-bouncer.sh --vanilla --chain "$2"' bash "$repo" "argument or decision to stress test" ``` +Run an adversarial review (structured falsification persona — premises, +assumptions, decisions, complexity, alternatives). Cross-AI by default; add +`--agents claude,claude` for an internal same-model review when the user wants +no Codex involvement: + +```bash +repo=$(resolve_co_evolution_repo) || exit 1 +bash -lc 'cd "$1" && bash ./co-evolve-bouncer.sh --vanilla --adversarial --bounce-only "$2"' bash "$repo" "path/to/document.md" +bash -lc 'cd "$1" && bash ./co-evolve-bouncer.sh --vanilla --adversarial --agents claude,claude --bounce-only "$2"' bash "$repo" "path/to/document.md" +``` + +On dense documents pair `--adversarial` with `--chain` or `--bounces 3` — an +aggressive critique pass can leave markers open at the default 2 passes. + ## Routing | User intent | Command | @@ -84,6 +99,8 @@ bash -lc 'cd "$1" && bash ./co-evolve-bouncer.sh --vanilla --chain "$2"' bash "$ | General question, idea, strategy, or draft | `bash ./co-evolve-bouncer.sh --vanilla "input"` | | Existing markdown file needs refinement | `bash ./co-evolve-bouncer.sh --vanilla --bounce-only ` | | High-stakes argument or decision needs adversarial passes | `bash ./co-evolve-bouncer.sh --vanilla --chain "input"` | +| Adversarial review of a document (cross-AI bounce) | `bash ./co-evolve-bouncer.sh --vanilla --adversarial --bounce-only ` | +| Internal adversarial review, same model, no Codex | `bash ./co-evolve-bouncer.sh --vanilla --adversarial --agents claude,claude --bounce-only ` | | Real repo change with code execution | `/dev-review` or `dev-review/codex/dev-review.sh` | ## Output diff --git a/templates/co-evolve/chain-critique-adversarial.md b/templates/co-evolve/chain-critique-adversarial.md new file mode 100644 index 0000000..1be4839 --- /dev/null +++ b/templates/co-evolve/chain-critique-adversarial.md @@ -0,0 +1,2 @@ +Your job this pass: CRITIQUE by falsification. Do not fix anything — only identify problems, marking each with [CONTESTED] or [CLARIFY]. Attack on five fronts: (1) premises — is the stated problem the real problem, and would meeting every success criterion actually solve it; (2) unstated assumptions — environment, user behavior, scale, ordering; state what breaks if each is wrong; (3) decisions — for each major choice, construct the conditions under which it becomes the wrong choice, weighing reversal cost against evidence quality; (4) complexity — apply the subtraction test: anything whose removal costs nothing gets challenged; (5) alternatives — for every "we chose X", ask why not Y, including the do-nothing baseline. Every critique must include a concrete alternative. Suppress hunches you could not defend; mark at most the 10 most material findings. + diff --git a/templates/co-evolve/role-reviewer-adversarial.md b/templates/co-evolve/role-reviewer-adversarial.md new file mode 100644 index 0000000..14f232c --- /dev/null +++ b/templates/co-evolve/role-reviewer-adversarial.md @@ -0,0 +1,30 @@ + +Your job: try to falsify this document, not polish it. Where an ordinary reviewer asks whether it is clear and consistent, you ask whether it is *right* — whether the premises hold, the assumptions are warranted, and the decisions would survive contact with reality. Construct counterarguments, not checklists. Every critique must include a concrete alternative. + +## Depth calibration + +Estimate the document's size and stakes before reviewing, then pick a depth: + +- **Quick** (under ~1000 words, no risk signals): run assumption surfacing and decision stress-testing only. Mark at most 3 findings — the most material ones. +- **Standard** (medium size or moderate complexity): add premise challenging and simplification pressure. Mark findings in proportion to the document's decision density, not its word count. +- **Deep** (over ~3000 words, more than 10 requirements, or a high-stakes domain — authentication, payments, data migration, compliance, external APIs, personal data, cryptography): run all five techniques including alternative blindness, and trace assumption chains across sections. + +At any depth, mark at most the 10 most material findings per pass. A flooded document converges on nothing. + +## Techniques + +1. **Premise challenging** — Is the stated problem the real problem? Would meeting every stated success criterion actually solve it, or could all criteria pass while the problem remains? Is the framing artificially narrowing the solution space? +2. **Assumption surfacing** — Find claims that depend on conditions never stated or verified: environment (a service or capability works a certain way), user behavior, scale (what happens at 10x or 0.1x), and ordering/timeline. For each, state the assumed condition and what breaks if it is wrong. +3. **Decision stress-testing** — For each major decision, construct the conditions under which it becomes the wrong choice. What evidence would prove it wrong, and did anyone look? Weigh reversal cost against evidence quality; give the most scrutiny to load-bearing decisions that other decisions depend on. +4. **Simplification pressure** — Apply the subtraction test: for each component or requirement, what happens if it is removed? Challenge abstractions with a single consumer and plans that build the final version before validating the approach. +5. **Alternative blindness** — For every "we chose X", ask why not Y — including existing solutions (build vs. use) and the do-nothing baseline. If no alternative is ever mentioned, the choice may be path-dependent rather than deliberate. + +## Confidence discipline + +Only mark what you can defend. A strong finding quotes the document's own text, constructs a concrete failure scenario or counterargument, and traces the consequence. If confirming a suspicion would need information not in the document, say so in the marker. Suppress hunches you would not bet on — treat these thresholds as directional guidance, not a scoring exercise. + +## Output + +Express every finding as a [CONTESTED] or [CLARIFY] marker per the protocol below — inline in the document, never as JSON or a separate findings list. + diff --git a/tests/adversarial-persona-simulation.sh b/tests/adversarial-persona-simulation.sh new file mode 100644 index 0000000..26fd46f --- /dev/null +++ b/tests/adversarial-persona-simulation.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# tests/adversarial-persona-simulation.sh +# Hermetic gate for the --adversarial reviewer persona in co-evolve-bouncer.sh. +# +# --adversarial swaps the reviewer's 1-line role preamble for the structured +# falsification persona (templates/co-evolve/role-reviewer-adversarial.md); in +# chain mode it swaps only the pass-1 critique stage template +# (chain-critique-adversarial.md). It composes with --lens (lens becomes a +# focus line appended to the persona) and is recorded in state.json as the +# additive reviewer_persona field (adversarial > lens > light). +# +# Coverage: +# 1. Bounce mode: persona text lands in the pass-1 (reviewer) prompt, NOT in +# the pass-2 (composer) prompt; state.json reviewer_persona=adversarial. +# 2. Chain mode: pass-1 prompt carries the adversarial critique variant and +# not the default critique text; defend (pass 2) is untouched. +# 3. --adversarial --lens composes: persona + focus line in the same prompt. +# 4. Parity: without --adversarial the pass-1 prompt carries the light role +# and no persona text; reviewer_persona=light (and =lens with --lens). +# 5. Same-model pair (--agents claude,claude) logs the cross-vendor warning. +# 6. --help documents --adversarial. +# +# Pattern: PATH-injected claude + codex stubs (same discipline as +# doc-pipeline-seats-simulation.sh); assertions grep the run dir's prompt +# files, state.json, and run.log. Both stubs drain stdin via a read-loop (NOT +# `cat`) to avoid SIGPIPE against the runner's stdin producer. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BOUNCER="$REPO_ROOT/co-evolve-bouncer.sh" + +TEST_DIR="$(mktemp -d -t adv-persona-XXXXXX)" +trap 'rm -rf "$TEST_DIR"' EXIT + +TOTAL=0 +FAILURES=0 +pass() { printf "PASS: %s\n" "$1"; } +fail() { printf "FAIL: %s\n" "$1" >&2; FAILURES=$((FAILURES + 1)); } + +# Distinctive tokens per template (quoted from the template files; if a +# template is reworded these greps are the tripwire that docs/tests drifted). +PERSONA_TOKEN='try to falsify this document' +CHAIN_ADV_TOKEN='CRITIQUE by falsification' +CHAIN_DEFAULT_TOKEN='Your job this pass: CRITIQUE\. Find every weakness' +LIGHT_TOKEN='find what is wrong, missing, weak, or unsupported' +LENS_FOCUS_TOKEN='Focus your adversarial review through this lens: security auditor' + +# --- stub CLIs -------------------------------------------------------------- +mkdir -p "$TEST_DIR/bin" + +# claude stub: drain stdin, emit a body that clears the >=10-word gate and one +# [CONTESTED] marker so the loop always advances past the reviewer pass. +cat > "$TEST_DIR/bin/claude" <<'STUB' +#!/usr/bin/env bash +for arg in "$@"; do + case "$arg" in + --version|-v|version) echo "claude 1.0.0 (adv-persona-stub)"; exit 0 ;; + esac +done +while IFS= read -r _line; do :; done # drain stdin, no SIGPIPE +echo "Stub reviewer/composer body with plenty of plain words to clear the bouncer minimum word count and any downstream size or auth check applied to it." +echo "[CONTESTED] one open marker so the loop advances into the next pass." +STUB +chmod +x "$TEST_DIR/bin/claude" + +# codex stub: parse -o FILE, drain stdin, emit a clean body (0 markers) so an +# even codex pass converges the loop. +cat > "$TEST_DIR/bin/codex" <<'STUB' +#!/usr/bin/env bash +for arg in "$@"; do + case "$arg" in + --version|-v|version) echo "codex 0.117.0 (adv-persona-stub)"; exit 0 ;; + esac +done +output_file=""; prev="" +for arg in "$@"; do + case "$prev" in -o) output_file="$arg" ;; esac + prev="$arg" +done +while IFS= read -r _line; do :; done # drain stdin, no SIGPIPE +body="Stub composer body with plenty of plain words to clear the bouncer minimum word count and any downstream size or auth check applied to it." +if [[ -n "$output_file" ]]; then printf '%s\n' "$body" > "$output_file"; else printf '%s\n' "$body"; fi +STUB +chmod +x "$TEST_DIR/bin/codex" + +DOC="$TEST_DIR/doc.md" +cat > "$DOC" <<'DOCEOF' +# Sample Document + +## Claim +This is a sample document with enough plain words that the bouncer treats it as a +real document to review and improve across a couple of passes. + +## Detail +It has multiple sections and sentences so the marker accounting and word counts +have real content to operate on during the bounce loop. +DOCEOF + +# run_bouncer — hermetic invocation; never fails the +# harness (stuck/adjudicated terminals are valid runs for these assertions). +run_bouncer() { + local runs_dir="$1"; shift + ( + unset CLAUDE_MODEL CLAUDE_EFFORT CODEX_MODEL CODEX_REASONING_EFFORT + unset COMPOSER_MODEL COMPOSER_EFFORT REVIEWER_MODEL REVIEWER_EFFORT + export PATH="$TEST_DIR/bin:$PATH" + export CO_EVOLVE_RUNS_DIR="$TEST_DIR/$runs_dir" + bash "$BOUNCER" --vanilla --no-report "$@" "$DOC" + ) >"$TEST_DIR/$runs_dir.out" 2>&1 || true + ls -dt "$TEST_DIR/$runs_dir"/co-evolve-* 2>/dev/null | head -1 +} + +# =========================================================================== +# Scenario 1: bounce mode — persona in the reviewer prompt only; state field. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +run1=$(run_bouncer runs1 --adversarial --bounce-only --bounces 2) +s1_ok=true +if [[ -z "$run1" ]]; then + s1_ok=false +else + grep -Fq "$PERSONA_TOKEN" "$run1/.bounce-pass-1-prompt.md" 2>/dev/null || s1_ok=false + # Composer (pass 2) keeps its own preamble — the persona must not leak in. + if grep -Fq "$PERSONA_TOKEN" "$run1/.bounce-pass-2-prompt.md" 2>/dev/null; then s1_ok=false; fi + # Light role text must be absent from the reviewer prompt (swap, not append). + if grep -Fq "$LIGHT_TOKEN" "$run1/.bounce-pass-1-prompt.md" 2>/dev/null; then s1_ok=false; fi + grep -Eq '"reviewer_persona": *"adversarial"' "$run1/state.json" 2>/dev/null || s1_ok=false +fi +if [[ "$s1_ok" == true ]]; then + pass "bounce mode: persona in pass-1 prompt only; reviewer_persona=adversarial in state.json" +else + fail "bounce-mode persona placement or state field wrong (run dir: ${run1:-missing})" + [[ -n "$run1" ]] && ls "$run1" >&2 +fi + +# =========================================================================== +# Scenario 2: chain mode — adversarial critique variant on pass 1 only. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +run2=$(run_bouncer runs2 --adversarial --chain --bounce-only) +s2_ok=true +if [[ -z "$run2" ]]; then + s2_ok=false +else + grep -Fq "$CHAIN_ADV_TOKEN" "$run2/.bounce-pass-1-prompt.md" 2>/dev/null || s2_ok=false + if grep -Eq "$CHAIN_DEFAULT_TOKEN" "$run2/.bounce-pass-1-prompt.md" 2>/dev/null; then s2_ok=false; fi + # Defend stage (pass 2) must not carry the adversarial critique variant. + if grep -Fq "$CHAIN_ADV_TOKEN" "$run2/.bounce-pass-2-prompt.md" 2>/dev/null; then s2_ok=false; fi +fi +if [[ "$s2_ok" == true ]]; then + pass "chain mode: pass-1 uses chain-critique-adversarial.md; defend untouched" +else + fail "chain-mode adversarial critique swap wrong (run dir: ${run2:-missing})" + [[ -n "$run2" ]] && ls "$run2" >&2 +fi + +# =========================================================================== +# Scenario 3: --adversarial --lens compose — persona + focus line together. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +run3=$(run_bouncer runs3 --adversarial --lens "security auditor" --bounce-only --bounces 2) +s3_ok=true +if [[ -z "$run3" ]]; then + s3_ok=false +else + grep -Fq "$PERSONA_TOKEN" "$run3/.bounce-pass-1-prompt.md" 2>/dev/null || s3_ok=false + grep -Fq "$LENS_FOCUS_TOKEN" "$run3/.bounce-pass-1-prompt.md" 2>/dev/null || s3_ok=false + grep -Eq '"reviewer_persona": *"adversarial"' "$run3/state.json" 2>/dev/null || s3_ok=false +fi +if [[ "$s3_ok" == true ]]; then + pass "--adversarial --lens composes: persona + lens focus line in the reviewer prompt" +else + fail "adversarial+lens composition wrong (run dir: ${run3:-missing})" + [[ -n "$run3" ]] && head -40 "$run3/.bounce-pass-1-prompt.md" >&2 +fi + +# =========================================================================== +# Scenario 4: parity — no --adversarial => light role, no persona text; and +# --lens alone records reviewer_persona=lens with the pre-persona lens text. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +run4=$(run_bouncer runs4 --bounce-only --bounces 2) +run4b=$(run_bouncer runs4b --lens "security auditor" --bounce-only --bounces 2) +s4_ok=true +if [[ -z "$run4" || -z "$run4b" ]]; then + s4_ok=false +else + grep -Fq "$LIGHT_TOKEN" "$run4/.bounce-pass-1-prompt.md" 2>/dev/null || s4_ok=false + if grep -Fq "$PERSONA_TOKEN" "$run4/.bounce-pass-1-prompt.md" 2>/dev/null; then s4_ok=false; fi + grep -Eq '"reviewer_persona": *"light"' "$run4/state.json" 2>/dev/null || s4_ok=false + # Lens-only path: pre-persona lens preamble, no adversarial persona text. + grep -Fq 'You are the security auditor reviewing this work' "$run4b/.bounce-pass-1-prompt.md" 2>/dev/null || s4_ok=false + if grep -Fq "$PERSONA_TOKEN" "$run4b/.bounce-pass-1-prompt.md" 2>/dev/null; then s4_ok=false; fi + grep -Eq '"reviewer_persona": *"lens"' "$run4b/state.json" 2>/dev/null || s4_ok=false +fi +if [[ "$s4_ok" == true ]]; then + pass "parity: default path keeps the light role (persona=light); lens-only path unchanged (persona=lens)" +else + fail "adversarial-off parity broken (run dirs: ${run4:-missing}, ${run4b:-missing})" +fi + +# =========================================================================== +# Scenario 5: same-model pair logs the cross-vendor warning. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +run5=$(run_bouncer runs5 --adversarial --agents claude,claude --bounce-only --bounces 2) +s5_ok=true +if [[ -z "$run5" ]]; then + s5_ok=false +else + grep -Fq 'same-model bounce (claude vs claude)' "$run5/run.log" 2>/dev/null || s5_ok=false +fi +# Cross-vendor default must NOT trip the warning. +if [[ -n "${run1:-}" ]] && grep -Fq 'same-model bounce' "$run1/run.log" 2>/dev/null; then s5_ok=false; fi +if [[ "$s5_ok" == true ]]; then + pass "same-model pair logs the no-cross-vendor-disagreement warning; default pair does not" +else + fail "same-model warning wrong (run dir: ${run5:-missing})" + [[ -n "$run5" ]] && grep -F 'NOTE' "$run5/run.log" >&2 || true +fi + +# =========================================================================== +# Scenario 6: --help documents --adversarial. +# =========================================================================== +TOTAL=$((TOTAL + 1)) +help_out="$(bash "$BOUNCER" --help 2>/dev/null || true)" +if printf '%s' "$help_out" | grep -Eq -- '^[[:space:]]*--adversarial[[:space:]]'; then + pass "--help documents --adversarial" +else + fail "--help does not document --adversarial" +fi + +# --- summary ---------------------------------------------------------------- +passed=$((TOTAL - FAILURES)) +if (( FAILURES == 0 )); then + echo "$passed/$TOTAL scenarios passed" + exit 0 +else + echo "$passed/$TOTAL scenarios passed ($FAILURES failed)" >&2 + exit 1 +fi