Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,20 @@ 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 --single-model "Stress test this" # both roles on claude
```

`--single-model [claude|codex]` pins both roles onto one agent (bare two-role
mode by default — empirical winner on dense technical docs per 2026-05-24 A/B).
Use when only one model is available, or to A/B against cross-model runs.

Add `--persona-discipline` to prepend the divergence preface
(`templates/co-evolve/single-model-preface.md`) that asks the model to
deliberately read against its own prior turn. Best paired with compose-then-
bounce of your own draft, where the shared-author bias actually applies.
On `--bounce-only` of external docs the preface tends to suppress the model's
natural investigatory impulse — use bare mode there.

### Agent Bouncer (`agent-bouncer/`)

Legacy standalone script that bounces any markdown document between two agents.
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,32 @@ bash ./co-evolve-bouncer.sh --vanilla "What is the strongest version of this arg
bash ./co-evolve-bouncer.sh --vanilla --chain "Should we ship this migration?"
```

When only one agent is available — or to A/B against a cross-model run — use
`--single-model` to pin both reviewer and composer onto the same agent. By
default this is **bare two-role mode**: same model, two role prompts (composer
and critic), nothing else. An A/B on a dense technical document (2026-05-24)
showed this beats the persona-discipline variant — the model preserves its
natural impulse to investigate the codebase and produces concrete, code-grounded
critique instead of abstract methodological objections.

```bash
bash ./co-evolve-bouncer.sh --vanilla --single-model "Stress test this argument"
bash ./co-evolve-bouncer.sh --vanilla --single-model codex --chain "Ship plan?"
```

If you want the divergence preface — which asks the model to deliberately read
against its own prior turn — enable it with `--persona-discipline`. This pairs
naturally with compose-then-bounce of your own draft, where there *is* a
shared-author bias to fight:

```bash
bash ./co-evolve-bouncer.sh --vanilla --single-model --persona-discipline "..."
```

Expect shallower diversity than cross-model bounces (shared weights share
blindspots), but most single-model runs still surface 1-2 real objections in
the first pass.

### [Agent Bouncer](agent-bouncer/)

A standalone bash script that bounces any markdown document between two agents.
Expand Down
134 changes: 124 additions & 10 deletions co-evolve-bouncer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ CHAIN=false
MAX_BOUNCES=2
AGENT_A="claude"
AGENT_B="codex"
SINGLE_MODEL=false
SINGLE_MODEL_AGENT=""
# Persona-discipline preface — opt-in. Default off because empirical A/B on a
# dense technical document showed the preface suppressed the model's natural
# investigatory impulse (raised abstract objections instead of concrete,
# code-grounded ones). Still useful when compose-then-bouncing your own draft;
# enable with --persona-discipline.
PERSONA_DISCIPLINE=false
# Set true by the bounce loop when an agent returns empty output after retry.
# Causes the script to exit 2 and emit a HINT pointing at --single-model.
BOUNCE_FAILED=false
FAILED_AGENT=""
BOUNCE_ONLY=false
OUTPUT_FILE=""
TASK=""
Expand All @@ -37,11 +49,13 @@ TIMESTAMP=$(date +%Y%m%d-%H%M%S)

TEMPLATE_DIR="$SCRIPT_DIR/templates/co-evolve"
PROTOCOL_TEMPLATE="$SCRIPT_DIR/agent-bouncer/templates/bounce-protocol.md"
SINGLE_MODEL_PREFACE="$TEMPLATE_DIR/single-model-preface.md"

# Validate templates exist
for _tmpl in "$TEMPLATE_DIR/role-reviewer-light.md" "$TEMPLATE_DIR/role-composer-light.md" \
"$TEMPLATE_DIR/chain-critique.md" "$TEMPLATE_DIR/chain-defend.md" \
"$TEMPLATE_DIR/chain-tighten.md" "$PROTOCOL_TEMPLATE"; do
"$TEMPLATE_DIR/chain-tighten.md" "$SINGLE_MODEL_PREFACE" \
"$PROTOCOL_TEMPLATE"; do
[[ -f "$_tmpl" ]] || die "Missing template: $_tmpl"
done

Expand All @@ -64,6 +78,19 @@ Options:
--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)
--single-model[=AGENT]
Force both roles onto the same agent (default: claude).
Bare two-role mode by default (no preface) — empirical A/B
showed this preserves the model's natural investigatory
impulse on dense technical docs. Overrides --agents.
Add --persona-discipline to enable the divergence preface.
--persona-discipline
Prepend a persona-discipline preface that asks the model
to deliberately diverge from its own prior turn. Useful
when compose-then-bouncing your own draft (where there
IS a shared-author bias to fight). May hurt on bounce-only
of external docs — see notesforhumans.md for the A/B data.
Can be used with or without --single-model.
--dev-review Add execute + verify phases after bounce
--bounce-only Skip compose, bounce a file directly
--output FILE Write final output to a file instead of stdout
Expand Down Expand Up @@ -107,6 +134,40 @@ while [[ $# -gt 0 ]]; do
[[ -z "$AGENT_A" || -z "$AGENT_B" ]] && die "--agents requires exactly two agents separated by comma (e.g., claude,codex)"
shift 2
;;
--single-model)
SINGLE_MODEL=true
# Optional positional agent: --single-model codex. If next arg looks like
# a flag or is absent, fall back to the default ("claude").
if [[ $# -ge 2 && -n "${2:-}" && "${2:0:1}" != "-" ]]; then
SINGLE_MODEL_AGENT="$2"
Comment on lines +141 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not consume the task as the optional single-model agent

When users follow the documented bare form, e.g. --single-model "Stress test this argument", this branch treats the positional task as the optional agent name and then fails because it is not claude or codex. Since the parser cannot distinguish a bare task from an optional positional agent, the advertised default-agent syntax is unusable unless the caller adds --, pipes input, or uses --single-model=claude; the bare flag should not consume the next non-flag argument.

Useful? React with 👍 / 👎.

shift 2
else
SINGLE_MODEL_AGENT="claude"
shift
fi
case "$SINGLE_MODEL_AGENT" in
claude|codex) ;;
*) die "--single-model agent must be claude or codex (got: $SINGLE_MODEL_AGENT)" ;;
esac
AGENT_A="$SINGLE_MODEL_AGENT"
AGENT_B="$SINGLE_MODEL_AGENT"
;;
--single-model=*)
SINGLE_MODEL=true
SINGLE_MODEL_AGENT="${1#--single-model=}"
[[ -n "$SINGLE_MODEL_AGENT" ]] || die "--single-model= requires an agent name"
case "$SINGLE_MODEL_AGENT" in
claude|codex) ;;
*) die "--single-model agent must be claude or codex (got: $SINGLE_MODEL_AGENT)" ;;
esac
AGENT_A="$SINGLE_MODEL_AGENT"
AGENT_B="$SINGLE_MODEL_AGENT"
shift
;;
--persona-discipline)
PERSONA_DISCIPLINE=true
shift
;;
--dev-review) die "--dev-review is not yet implemented. Use dev-review/codex/dev-review.sh directly." ;;
--bounce-only) BOUNCE_ONLY=true; shift ;;
--output) OUTPUT_FILE="$2"; shift 2 ;;
Expand Down Expand Up @@ -322,34 +383,49 @@ $(cat "$CONTEXT_FILE")
fi

# --- Role Preamble Generation ---
# Prepends the persona-discipline preface when --persona-discipline is set.
# Decoupled from --single-model after a 2026-05-24 A/B test on a dense
# technical document showed the preface SUPPRESSED concrete code-grounded
# critique in favor of abstract methodological objections — opposite of what
# was intended. Kept available as opt-in for compose-then-bounce-own-draft
# flows, where the prior-turn divergence framing actually applies.
maybe_prepend_single_model_preface() {
local body="$1"
if [[ "$PERSONA_DISCIPLINE" == "true" ]]; then
printf '%s\n%s' "$(cat "$SINGLE_MODEL_PREFACE")" "$body"
else
printf '%s' "$body"
fi
}

build_reviewer_preamble() {
local preamble
if [[ -n "$LENS" ]]; then
echo "You are the ${LENS} reviewing this work. Be adversarial from that perspective. Every critique must include a concrete alternative."
preamble="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"
preamble=$(cat "$TEMPLATE_DIR/role-reviewer-light.md")
else
local preamble
preamble=$(cat "$TEMPLATE_DIR/role-reviewer-light.md")
if [[ -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"
fi
maybe_prepend_single_model_preface "$preamble"
}

build_composer_preamble() {
local preamble
if [[ -n "$LENS" ]]; then
echo "Resolve all critiques from the ${LENS} perspective. Strengthen weak points. Make it bulletproof."
preamble="Resolve all critiques from the ${LENS} perspective. Strengthen weak points. Make it bulletproof."
elif [[ "$SKIP_INTERVIEW" == "true" ]]; then
cat "$TEMPLATE_DIR/role-composer-light.md"
preamble=$(cat "$TEMPLATE_DIR/role-composer-light.md")
else
local preamble
preamble=$(cat "$TEMPLATE_DIR/role-composer-light.md")
if [[ -n "${OUTPUT_TYPE:-}" && "$OUTPUT_TYPE" != "auto" ]]; then
preamble="${preamble}The output should be a ${OUTPUT_TYPE}. Shape it accordingly."
fi
echo "$preamble"
fi
maybe_prepend_single_model_preface "$preamble"
}

# --- Agent Invocation Helper ---
Expand Down Expand Up @@ -446,6 +522,7 @@ run_bounce_phase() {
esac
role="critique"
[[ "$pass" == "3" ]] && role="tighten"
role_preamble=$(maybe_prepend_single_model_preface "$role_preamble")
else
role_preamble=$(build_reviewer_preamble)
role="reviewer"
Expand All @@ -455,6 +532,7 @@ run_bounce_phase() {
if [[ "$CHAIN" == "true" ]]; then
role_preamble=$(cat "$TEMPLATE_DIR/chain-defend.md")
role="defend"
role_preamble=$(maybe_prepend_single_model_preface "$role_preamble")
else
role_preamble=$(build_composer_preamble)
role="composer"
Expand Down Expand Up @@ -499,6 +577,8 @@ $(cat "$PROTOCOL_TEMPLATE")"

if [[ ! -s "$output_file" ]]; then
log " ERROR: ${current_agent} returned empty output on retry. Stopping."
BOUNCE_FAILED=true
FAILED_AGENT="$current_agent"
break
fi

Expand Down Expand Up @@ -554,6 +634,12 @@ log " Input: $INPUT_TYPE"
log " Task: $(echo "$TASK" | head -c 80)"
log " Compose: $AGENT_A"
log " Bounce: $AGENT_A / $AGENT_B"
if [[ "$SINGLE_MODEL" == "true" ]]; then
log " Single: yes (both roles on $SINGLE_MODEL_AGENT, bare two-role mode)"
fi
if [[ "$PERSONA_DISCIPLINE" == "true" ]]; then
log " Persona: discipline preface enabled (opt-in)"
fi
if [[ "$CHAIN" == "true" ]]; then
log " Mode: chain (critique -> defend -> tighten)"
else
Expand Down Expand Up @@ -589,14 +675,42 @@ if [[ -n "$OUTPUT_FILE" ]]; then
fi

log "============================================"
log " CO-EVOLVE COMPLETE"
if [[ "$BOUNCE_FAILED" == "true" ]]; then
log " CO-EVOLVE INCOMPLETE (bounce aborted)"
else
log " CO-EVOLVE COMPLETE"
fi
log "============================================"
log " Task: $(echo "$TASK" | head -c 80)"
log " Run dir: $RUN_DIR"
log " Final: $FINAL_FILE"
log "============================================"

# Cross-vendor failure UX: emit a loud WARNING + actionable HINT pointing at
# --single-model with whichever agent did NOT fail. Suppress the hint when the
# user is already in single-model mode (no useful escape hatch to suggest).
if [[ "$BOUNCE_FAILED" == "true" ]]; then
log ""
log "WARNING: bounce ended early because ${FAILED_AGENT} returned empty output."
log " The output may contain unresolved [CONTESTED]/[CLARIFY] markers."
if [[ "$SINGLE_MODEL" == "false" ]]; then
if [[ "$FAILED_AGENT" == "$AGENT_A" ]]; then
working_agent="$AGENT_B"
else
working_agent="$AGENT_A"
fi
log ""
log "HINT: if ${FAILED_AGENT} isn't installed or available, retry with:"
log " --single-model ${working_agent}"
log " (uses ${working_agent} for both roles; cross-vendor diversity reduced)"
fi
fi

# Print clean result to stdout unless output was redirected to file
if [[ -z "$OUTPUT_FILE" ]]; then
cat "$FINAL_FILE"
fi

if [[ "$BOUNCE_FAILED" == "true" ]]; then
exit 2
fi
104 changes: 104 additions & 0 deletions lib/codex-access.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# codex-access — single source of truth for invoking the Codex CLI across
# environments. Historically this logic was duplicated in 6+ places across
# the codebase; the lib/ helper centralizes the launch matrix so callers
# don't reimplement it.
#
# Launch matrix (checked in order):
# 1. WSL with Windows-side codex install:
# `cmd.exe /c codex exec ...` — bridges from WSL to the Windows binary
# because auth state is per-side (installing in both WSL AND Windows
# doubles the OPENAI billing).
# 2. Native Linux/macOS with codex on PATH:
# `codex exec ...` — direct invocation.
# 3. No codex reachable:
# `codex_available` returns 1; `codex_invoke` writes the install hint
# to stderr and produces empty output (matches the existing empty-output
# contract so callers don't need a new branch).
#
# See skills/codex-access/SKILL.md for the user-facing description.
#
# This module is intentionally side-effect free at source time — it only
# defines functions. Source it from any script that needs codex access.

# codex_available → exit 0 iff codex is reachable via any supported path.
# WSL+cmd.exe path is checked first because that's the upstream-recommended
# install on Windows (auth lives on the Windows side).
codex_available() {
if [[ -n "${WSL_DISTRO_NAME:-}" ]] \
&& command -v cmd.exe >/dev/null 2>&1 \
&& command -v wslpath >/dev/null 2>&1; then
cmd.exe /c codex --version >/dev/null 2>&1 && return 0
fi
command -v codex >/dev/null 2>&1
}

# codex_install_hint → prints a multi-line, actionable install/availability
# message to stdout. Callers redirect to stderr when used inside an error path.
codex_install_hint() {
cat <<'HINT'
codex CLI is not reachable from this environment.

To enable codex-backed runs:
1. Install: npm install -g @openai/codex
2. Authenticate: export OPENAI_API_KEY=sk-...
3. Verify: codex --version

WSL users: install codex on the Windows side, NOT inside WSL.
codex-access bridges via cmd.exe automatically. Auth state is per-side,
so installing twice would double your OpenAI billing.

If codex is genuinely unavailable in this environment (e.g. a remote
container with no API key), use:
bash co-evolve-bouncer.sh --single-model claude ...

to run same-model co-evolution. This trades cross-vendor diversity for
the ability to actually run at all.
HINT
}

# codex_invoke "$prompt_file" "$output_file" "$stderr_file"
# Drop-in equivalent of lib/co-evolution.sh's invoke_codex, but routed through
# the launch matrix above so the WSL/native/missing cases all live in one
# place. Returns 0 always — errors flow through empty output_file + populated
# stderr_file, matching the existing invoke_* contract (callers detect failure
# by checking [[ ! -s "$output_file" ]]).
codex_invoke() {
local prompt_file="$1"
local output_file="$2"
local stderr_file="$3"
local workdir="${WORKDIR:-$PWD}"
local -a cmd
local windows_workdir=""
local windows_output=""

if [[ -n "${WSL_DISTRO_NAME:-}" ]] \
&& command -v cmd.exe >/dev/null 2>&1 \
&& command -v wslpath >/dev/null 2>&1; then
windows_workdir=$(wslpath -w "$workdir")
windows_output=$(wslpath -w "$output_file")
cmd=(cmd.exe /c codex exec --full-auto --skip-git-repo-check -C "$windows_workdir")
elif command -v codex >/dev/null 2>&1; then
cmd=(codex exec --full-auto --skip-git-repo-check -C "$workdir")
else
{
echo "ERROR: codex CLI not reachable in this environment."
echo ""
codex_install_hint
} > "$stderr_file"
: > "$output_file"
return 0
fi

if [[ -n "${CODEX_MODEL:-}" ]]; then
cmd+=(-c "model=${CODEX_MODEL}")
fi

if [[ -n "$windows_output" ]]; then
cmd+=(-o "$windows_output")
else
cmd+=(-o "$output_file")
fi

"${cmd[@]}" < "$prompt_file" > /dev/null 2>"$stderr_file" || true
}
Loading