test-brainstorm-handoff-e2e.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. #!/usr/bin/env bash
  2. # Test: Brainstorm-to-plan handoff (end-to-end)
  3. #
  4. # Full brainstorming flow that builds enough context distance to reproduce
  5. # the EnterPlanMode failure. Simulates a real brainstorming session with
  6. # multiple turns of Q&A before the "build it" moment.
  7. #
  8. # This test takes 5-10 minutes to run.
  9. #
  10. # PASS: Skill tool invoked with "writing-plans" AND EnterPlanMode NOT invoked
  11. # FAIL: EnterPlanMode invoked OR writing-plans not invoked
  12. #
  13. # Usage:
  14. # ./test-brainstorm-handoff-e2e.sh # With fix (expects PASS)
  15. # ./test-brainstorm-handoff-e2e.sh --without-fix # Strip fix, reproduce failure
  16. # ./test-brainstorm-handoff-e2e.sh --verbose # Show full output
  17. #
  18. set -euo pipefail
  19. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  20. PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
  21. # Parse flags
  22. VERBOSE=false
  23. WITHOUT_FIX=false
  24. while [[ $# -gt 0 ]]; do
  25. case $1 in
  26. --verbose|-v) VERBOSE=true; shift ;;
  27. --without-fix) WITHOUT_FIX=true; shift ;;
  28. *) echo "Unknown flag: $1"; exit 1 ;;
  29. esac
  30. done
  31. TIMESTAMP=$(date +%s)
  32. OUTPUT_DIR="/tmp/superpowers-tests/${TIMESTAMP}/brainstorm-handoff-e2e"
  33. mkdir -p "$OUTPUT_DIR"
  34. echo "=== Brainstorm-to-Plan Handoff E2E Test ==="
  35. echo "Mode: $([ "$WITHOUT_FIX" = true ] && echo "WITHOUT FIX (expect failure)" || echo "WITH FIX (expect pass)")"
  36. echo "Output: $OUTPUT_DIR"
  37. echo "This test takes 5-10 minutes."
  38. echo ""
  39. # --- Project Setup ---
  40. PROJECT_DIR="$OUTPUT_DIR/project"
  41. mkdir -p "$PROJECT_DIR/src" "$PROJECT_DIR/test"
  42. cat > "$PROJECT_DIR/package.json" << 'PROJ_EOF'
  43. {
  44. "name": "my-express-app",
  45. "version": "1.0.0",
  46. "type": "module",
  47. "scripts": {
  48. "start": "node src/index.js",
  49. "test": "vitest run"
  50. },
  51. "dependencies": {
  52. "express": "^4.18.0",
  53. "better-sqlite3": "^9.0.0"
  54. },
  55. "devDependencies": {
  56. "vitest": "^1.0.0",
  57. "supertest": "^6.0.0"
  58. }
  59. }
  60. PROJ_EOF
  61. cat > "$PROJECT_DIR/src/index.js" << 'PROJ_EOF'
  62. import express from 'express';
  63. const app = express();
  64. app.use(express.json());
  65. app.get('/health', (req, res) => res.json({ status: 'ok' }));
  66. const PORT = process.env.PORT || 3000;
  67. if (process.env.NODE_ENV !== 'test') {
  68. app.listen(PORT, () => console.log(`Listening on ${PORT}`));
  69. }
  70. export default app;
  71. PROJ_EOF
  72. cd "$PROJECT_DIR"
  73. git init -q
  74. git add -A
  75. git commit -q -m "Initial commit"
  76. # --- Plugin Setup ---
  77. EFFECTIVE_PLUGIN_DIR="$PLUGIN_DIR"
  78. if [ "$WITHOUT_FIX" = true ]; then
  79. echo "Creating plugin copy without the handoff fix..."
  80. EFFECTIVE_PLUGIN_DIR="$OUTPUT_DIR/plugin-without-fix"
  81. cp -R "$PLUGIN_DIR" "$EFFECTIVE_PLUGIN_DIR"
  82. python3 << PYEOF
  83. import pathlib
  84. # Strip fix from brainstorming SKILL.md
  85. p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/brainstorming/SKILL.md')
  86. content = p.read_text()
  87. content = content.replace(
  88. '**Implementation (if continuing):**\nWhen the user approves the design and wants to build:\n1. **Invoke \`superpowers:writing-plans\` using the Skill tool.** Not EnterPlanMode. Not plan mode. Not direct implementation. The Skill tool.\n2. After the plan is written, use superpowers:using-git-worktrees to create an isolated workspace for implementation.',
  89. '**Implementation (if continuing):**\n- Ask: "Ready to set up for implementation?"\n- Use superpowers:using-git-worktrees to create isolated workspace\n- **REQUIRED:** Use superpowers:writing-plans to create detailed implementation plan'
  90. )
  91. p.write_text(content)
  92. # Strip fix from using-superpowers
  93. p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/using-superpowers/SKILL.md')
  94. lines = p.read_text().splitlines(keepends=True)
  95. lines = [l for l in lines if 'I should use EnterPlanMode' not in l]
  96. p.write_text(''.join(lines))
  97. # Strip fix from writing-plans
  98. p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/writing-plans/SKILL.md')
  99. content = p.read_text()
  100. content = content.replace(
  101. 'description: Use when you have a spec or requirements for a multi-step task, before touching code. After brainstorming, ALWAYS use this — not EnterPlanMode or plan mode.',
  102. 'description: Use when you have a spec or requirements for a multi-step task, before touching code'
  103. )
  104. content = content.replace(
  105. '**Context:** This runs in the main workspace after brainstorming, while context is fresh. The worktree is created afterward for implementation.',
  106. '**Context:** This should be run in a dedicated worktree (created by brainstorming skill).'
  107. )
  108. p.write_text(content)
  109. PYEOF
  110. echo "Plugin copy created."
  111. echo ""
  112. fi
  113. # --- Helper ---
  114. run_turn() {
  115. local turn_num="$1"
  116. local prompt="$2"
  117. local max_turns="$3"
  118. local label="$4"
  119. local continue_flag="${5:-}"
  120. local log_file="$OUTPUT_DIR/turn${turn_num}.json"
  121. echo ">>> Turn $turn_num: $label"
  122. local cmd="timeout 300 claude -p \"$prompt\""
  123. cmd="$cmd --plugin-dir \"$EFFECTIVE_PLUGIN_DIR\""
  124. cmd="$cmd --dangerously-skip-permissions"
  125. cmd="$cmd --max-turns $max_turns"
  126. cmd="$cmd --output-format stream-json"
  127. if [ -n "$continue_flag" ]; then
  128. cmd="$cmd --continue"
  129. fi
  130. eval "$cmd" > "$log_file" 2>&1 || true
  131. echo " Done."
  132. if [ "$VERBOSE" = true ]; then
  133. echo " ---"
  134. grep '"type":"assistant"' "$log_file" 2>/dev/null | tail -1 | \
  135. jq -r '.message.content[0].text // empty' 2>/dev/null | \
  136. head -c 600 || true
  137. echo ""
  138. echo " ---"
  139. fi
  140. echo "$log_file"
  141. }
  142. # --- Run Full Brainstorming Flow ---
  143. cd "$PROJECT_DIR"
  144. # Turn 1: Start brainstorming - this loads the skill and begins Q&A
  145. T1=$(run_turn 1 \
  146. "I want to add URL shortening to this Express app. Help me think through the design." \
  147. 5 "Starting brainstorming")
  148. # Turn 2: Answer first question (whatever it is) generically
  149. T2=$(run_turn 2 \
  150. "Good question. Here is what I want: POST /api/shorten that takes a URL and returns a short code. GET /:code that redirects. GET /api/stats/:code for click tracking. Random 6-char alphanumeric codes. SQLite storage using better-sqlite3 which is already in package.json. No auth needed." \
  151. 5 "Answering first question" --continue)
  152. # Turn 3: Agree with recommendations
  153. T3=$(run_turn 3 \
  154. "Yes, that sounds right. Go with your recommendation." \
  155. 5 "Agreeing with recommendation" --continue)
  156. # Turn 4: Continue agreeing
  157. T4=$(run_turn 4 \
  158. "Looks good. I agree with that approach." \
  159. 5 "Continuing to agree" --continue)
  160. # Turn 5: Push toward completion
  161. T5=$(run_turn 5 \
  162. "Perfect. I am happy with all of that. Please wrap up the design and write the spec." \
  163. 8 "Requesting spec write-up" --continue)
  164. # Turn 6: Approve the spec
  165. T6=$(run_turn 6 \
  166. "The spec looks great. I approve it." \
  167. 5 "Approving spec" --continue)
  168. # Turn 7: THE CRITICAL MOMENT - "build it"
  169. T7=$(run_turn 7 \
  170. "Yes, build it." \
  171. 5 "Critical handoff: build it" --continue)
  172. # Turn 8: Safety net in case turn 7 asked a follow-up
  173. T8=$(run_turn 8 \
  174. "Yes. Go ahead and build it now." \
  175. 5 "Safety net: build it" --continue)
  176. echo ""
  177. # --- Assertions ---
  178. echo "=== Results ==="
  179. echo ""
  180. # Combine all logs
  181. ALL_LOGS="$OUTPUT_DIR/all-turns.json"
  182. cat "$OUTPUT_DIR"/turn*.json > "$ALL_LOGS" 2>/dev/null
  183. # Check handoff turns (6-8, where approval + "build it" happens)
  184. HANDOFF_LOGS="$OUTPUT_DIR/handoff-turns.json"
  185. cat "$OUTPUT_DIR/turn6.json" "$OUTPUT_DIR/turn7.json" "$OUTPUT_DIR/turn8.json" > "$HANDOFF_LOGS" 2>/dev/null
  186. # Detection: writing-plans skill invoked in handoff turns?
  187. HAS_WRITING_PLANS=false
  188. if grep -q '"name":"Skill"' "$HANDOFF_LOGS" 2>/dev/null && grep -q 'writing-plans' "$HANDOFF_LOGS" 2>/dev/null; then
  189. HAS_WRITING_PLANS=true
  190. fi
  191. # Detection: EnterPlanMode invoked in handoff turns?
  192. HAS_ENTER_PLAN_MODE=false
  193. if grep -q '"name":"EnterPlanMode"' "$HANDOFF_LOGS" 2>/dev/null; then
  194. HAS_ENTER_PLAN_MODE=true
  195. fi
  196. # Also check across ALL turns (might happen earlier)
  197. HAS_ENTER_PLAN_MODE_ANYWHERE=false
  198. if grep -q '"name":"EnterPlanMode"' "$ALL_LOGS" 2>/dev/null; then
  199. HAS_ENTER_PLAN_MODE_ANYWHERE=true
  200. fi
  201. # Report
  202. echo "Skills invoked (all turns):"
  203. grep -o '"skill":"[^"]*"' "$ALL_LOGS" 2>/dev/null | sort -u || echo " (none)"
  204. echo ""
  205. echo "Skills invoked (handoff turns 6-8):"
  206. grep -o '"skill":"[^"]*"' "$HANDOFF_LOGS" 2>/dev/null | sort -u || echo " (none)"
  207. echo ""
  208. echo "Tools invoked in handoff turns (6-8):"
  209. grep -o '"name":"[A-Z][^"]*"' "$HANDOFF_LOGS" 2>/dev/null | sort | uniq -c | sort -rn | head -10 || echo " (none)"
  210. echo ""
  211. if [ "$HAS_ENTER_PLAN_MODE_ANYWHERE" = true ]; then
  212. echo "WARNING: EnterPlanMode was invoked somewhere in the conversation."
  213. echo "Turns containing EnterPlanMode:"
  214. for f in "$OUTPUT_DIR"/turn*.json; do
  215. if grep -q '"name":"EnterPlanMode"' "$f" 2>/dev/null; then
  216. echo " $(basename "$f")"
  217. fi
  218. done
  219. echo ""
  220. fi
  221. # Determine result
  222. PASSED=false
  223. if [ "$WITHOUT_FIX" = true ]; then
  224. echo "--- Without-Fix Mode (reproducing failure) ---"
  225. if [ "$HAS_ENTER_PLAN_MODE" = true ] || [ "$HAS_ENTER_PLAN_MODE_ANYWHERE" = true ]; then
  226. echo "REPRODUCED: Claude used EnterPlanMode (the bug we're fixing)"
  227. PASSED=true
  228. elif [ "$HAS_WRITING_PLANS" = true ]; then
  229. echo "NOT REPRODUCED: Claude used writing-plans even without the fix"
  230. echo "(The old guidance was sufficient in this run)"
  231. PASSED=false
  232. else
  233. echo "INCONCLUSIVE: Claude used neither writing-plans nor EnterPlanMode"
  234. echo "The brainstorming flow may not have reached the handoff point."
  235. PASSED=false
  236. fi
  237. else
  238. echo "--- With-Fix Mode (verifying fix) ---"
  239. if [ "$HAS_WRITING_PLANS" = true ] && [ "$HAS_ENTER_PLAN_MODE_ANYWHERE" = false ]; then
  240. echo "PASS: Claude used writing-plans skill (correct handoff)"
  241. PASSED=true
  242. elif [ "$HAS_ENTER_PLAN_MODE_ANYWHERE" = true ]; then
  243. echo "FAIL: Claude used EnterPlanMode instead of writing-plans"
  244. PASSED=false
  245. elif [ "$HAS_WRITING_PLANS" = true ] && [ "$HAS_ENTER_PLAN_MODE_ANYWHERE" = true ]; then
  246. echo "FAIL: Claude used BOTH writing-plans AND EnterPlanMode"
  247. PASSED=false
  248. else
  249. echo "INCONCLUSIVE: Claude used neither writing-plans nor EnterPlanMode"
  250. echo "The brainstorming flow may not have reached the handoff."
  251. echo "Check logs to see where the conversation stopped."
  252. PASSED=false
  253. fi
  254. fi
  255. echo ""
  256. # Show what happened in each turn
  257. echo "Turn-by-turn summary:"
  258. for i in 1 2 3 4 5 6 7 8; do
  259. local_log="$OUTPUT_DIR/turn${i}.json"
  260. if [ -f "$local_log" ]; then
  261. local_skills=$(grep -o '"skill":"[^"]*"' "$local_log" 2>/dev/null | tr '\n' ' ' || true)
  262. local_tools=$(grep -o '"name":"EnterPlanMode\|"name":"Skill"' "$local_log" 2>/dev/null | tr '\n' ' ' || true)
  263. local_size=$(wc -c < "$local_log" | tr -d ' ')
  264. printf " Turn %d: %s bytes" "$i" "$local_size"
  265. [ -n "$local_skills" ] && printf " | skills: %s" "$local_skills"
  266. [ -n "$local_tools" ] && printf " | tools: %s" "$local_tools"
  267. echo ""
  268. fi
  269. done
  270. echo ""
  271. echo "Logs: $OUTPUT_DIR"
  272. echo ""
  273. if [ "$PASSED" = true ]; then
  274. exit 0
  275. else
  276. exit 1
  277. fi