run-all.sh 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env bash
  2. # With/without A/B (and optional interactive) eval for a codegraph version on a
  3. # repo. Codegraph is the ONLY variable: both arms launch claude with
  4. # --strict-mcp-config — with = codegraph-only MCP (pointed at $CG_BIN),
  5. # without = empty MCP. Built-in Read/Grep/Bash stay available in both arms.
  6. #
  7. # Usage: run-all.sh <repo-path> "<question>" [headless|tmux|all]
  8. #
  9. # Each headless arm reports the three feedback metrics (parse-run.mjs prints all
  10. # three under every run, and compare-arms.mjs puts the arms side by side at the
  11. # end when both ran):
  12. # residual context occupancy (CG-7) window still held by the arm's retrieval
  13. # explore sufficiency (CG-8) was the response ENOUGH — the agent's next act
  14. # allocation efficiency (CG-9) share of returned bytes the answer cited
  15. # docs/benchmarks/agent-eval-feedback-metrics.md is the entry point; the three
  16. # per-metric docs it links carry the caveats.
  17. #
  18. # MULTI-TURN: separate questions with "||" to run them as ONE session —
  19. # run-all.sh <repo> "How does X work?||Where is Y handled in that path?"
  20. # Turn 1 runs normally; every later turn `--resume`s the same session, so the
  21. # earlier turns' tool output is still in the window (that is the whole point:
  22. # residual context occupancy, the cost a single-question run cannot see).
  23. # Segments land in run-<label>.jsonl, run-<label>.t2.jsonl, … and parse-run.mjs
  24. # stitches them back into one session.
  25. #
  26. # Env: CG_BIN codegraph binary (default: command -v codegraph)
  27. # AGENT_EVAL_OUT output dir (default: /tmp/agent-eval)
  28. # MODEL / EFFORT claude model/effort (default: sonnet / high — the
  29. # standing A/B policy; see CLAUDE.md, don't raise)
  30. set -uo pipefail
  31. REPO="${1:?usage: run-all.sh <repo-path> \"<question>\" [headless|tmux|all]}"
  32. Q="${2:?question required}"
  33. MODE="${3:-headless}"
  34. # Split "Q1||Q2||Q3" into turns (kept bash-3.2-safe: macOS ships 3.2).
  35. TURNS=()
  36. rest="$Q"
  37. while [ "$rest" != "${rest#*||}" ]; do
  38. TURNS+=("${rest%%||*}")
  39. rest="${rest#*||}"
  40. done
  41. TURNS+=("$rest")
  42. CG_BIN="${CG_BIN:-$(command -v codegraph)}"
  43. OUT="${AGENT_EVAL_OUT:-/tmp/agent-eval}"
  44. HARNESS="$(cd "$(dirname "$0")" && pwd)"
  45. mkdir -p "$OUT"
  46. # Neutralize any ambient CodeGraph prompt-hook (~/.claude) in BOTH arms:
  47. # the hook injects codegraph context into every prompt, which contaminates
  48. # the without-arm (free structural context) and double-counts the with-arm.
  49. # The A/B's only variable must be the MCP server wired below.
  50. export CODEGRAPH_NO_PROMPT_HOOK=1
  51. # Hide the codegraph CLI from BOTH arms, so the only way to reach codegraph is
  52. # the MCP server wired below — which is what makes it the A/B's single variable.
  53. # Two layers (sanitized PATH + a PreToolUse hook that blocks absolute-path
  54. # invocations), both in no-cli-shim.sh, which ab-new-vs-baseline.sh shares; the
  55. # reasoning and the incident that forced each layer are documented there.
  56. # Sets $ARM_PATH and $ARM_SETTINGS, and aborts if either layer fails its probe.
  57. . "$HARNESS/no-cli-shim.sh"
  58. cg_no_cli_setup "$OUT" || exit 1
  59. [ -n "$CG_BIN" ] || { echo "no codegraph binary on PATH (set CG_BIN)"; exit 1; }
  60. [ -d "$REPO/.codegraph" ] || { echo "no .codegraph index at $REPO — index it first"; exit 1; }
  61. case "$MODE" in headless|tmux|all) ;; *) echo "mode must be headless|tmux|all (got '$MODE')"; exit 1;; esac
  62. # MCP config files (path form avoids inline-JSON quoting through tmux).
  63. cat > "$OUT/mcp-codegraph.json" <<JSON
  64. {"mcpServers":{"codegraph":{"command":"$CG_BIN","args":["serve","--mcp","--path","$REPO"]}}}
  65. JSON
  66. echo '{"mcpServers":{}}' > "$OUT/mcp-empty.json"
  67. echo "###### codegraph: $CG_BIN"
  68. echo "###### repo: $REPO"
  69. echo "###### turns: ${#TURNS[@]}"
  70. for t in "${TURNS[@]}"; do echo "###### - $t"; done
  71. echo
  72. # Pull the session id out of a segment's result event so the next turn can
  73. # --resume it (rather than minting a --session-id, which needs a valid uuid).
  74. session_id_of() {
  75. node -e '
  76. const fs=require("fs");
  77. for (const l of fs.readFileSync(process.argv[1],"utf8").split("\n").reverse()) {
  78. if (!l) continue; let e; try { e=JSON.parse(l) } catch { continue }
  79. if (e.session_id) { console.log(e.session_id); break }
  80. }' "$1" 2>/dev/null
  81. }
  82. # Headless arm: claude -p with stream-json -> exact tool sequence + tokens/cost
  83. # + residual context occupancy. One session, one segment file per turn.
  84. headless() {
  85. local label="$1" cfg="$2"
  86. echo "############################## HEADLESS [$label] ##############################"
  87. local sid="" seg=0 out="" files=()
  88. : > "$OUT/run-$label.err"
  89. for q in "${TURNS[@]}"; do
  90. seg=$((seg + 1))
  91. out="$OUT/run-$label.jsonl"
  92. [ "$seg" -gt 1 ] && out="$OUT/run-$label.t$seg.jsonl"
  93. local resume=()
  94. [ -n "$sid" ] && resume=(--resume "$sid")
  95. ( cd "$REPO" && PATH="$ARM_PATH" claude -p "$q" \
  96. --output-format stream-json --verbose \
  97. --permission-mode bypassPermissions \
  98. --model "${MODEL:-sonnet}" --effort "${EFFORT:-high}" \
  99. --max-budget-usd 4 \
  100. --strict-mcp-config --mcp-config "$cfg" \
  101. --settings "$ARM_SETTINGS" \
  102. ${resume[@]+"${resume[@]}"} \
  103. </dev/null > "$out" 2>>"$OUT/run-$label.err" )
  104. echo "exit $? -> $out ($(wc -l < "$out" | tr -d ' ') lines) [turn $seg/${#TURNS[@]}]"
  105. files+=("$out")
  106. sid="$(session_id_of "$out")"
  107. if [ -z "$sid" ] && [ "$seg" -lt "${#TURNS[@]}" ]; then
  108. echo " WARN: no session_id in $out — later turns would start a FRESH context; stopping this arm"
  109. break
  110. fi
  111. done
  112. tail -2 "$OUT/run-$label.err" 2>/dev/null
  113. node "$HARNESS/parse-run.mjs" "${files[@]}" 2>&1 || true
  114. echo
  115. }
  116. # CG_ARMS=with|without|both — re-run one arm without redoing the other.
  117. ARMS="${CG_ARMS:-both}"
  118. if [ "$MODE" = headless ] || [ "$MODE" = all ]; then
  119. case "$ARMS" in both|with) headless "headless-with" "$OUT/mcp-codegraph.json";; esac
  120. case "$ARMS" in both|without) headless "headless-without" "$OUT/mcp-empty.json";; esac
  121. # Both arms' three metrics on one screen. The per-arm blocks above say WHY a
  122. # number moved (which query fell short, which file was never cited); this says
  123. # whether it moved at all. CG_ARMS=with|without leaves one arm's logs from an
  124. # earlier invocation in $OUT, and comparing against those is the point of the
  125. # split — so this runs whichever arms have logs, not only a fresh pair.
  126. node "$HARNESS/compare-arms.mjs" "$OUT" headless-with headless-without 2>&1 || true
  127. fi
  128. if [ "$MODE" = tmux ] || [ "$MODE" = all ]; then
  129. echo "############################## INTERACTIVE [with] ##############################"
  130. CLAUDE_EXTRA_ARGS="--model ${MODEL:-sonnet} --effort ${EFFORT:-high} --strict-mcp-config --mcp-config $OUT/mcp-codegraph.json" \
  131. bash "$HARNESS/itrun.sh" "$REPO" "int-with" "${TURNS[0]}" 2>&1 || echo "[itrun WITH failed]"
  132. echo
  133. echo "############################## INTERACTIVE [without] ##############################"
  134. CLAUDE_EXTRA_ARGS="--model ${MODEL:-sonnet} --effort ${EFFORT:-high} --strict-mcp-config --mcp-config $OUT/mcp-empty.json" \
  135. bash "$HARNESS/itrun.sh" "$REPO" "int-without" "${TURNS[0]}" 2>&1 || echo "[itrun WITHOUT failed]"
  136. echo
  137. fi
  138. echo "############################## RUN-ALL COMPLETE ##############################"