subagent-driven-development
OBobra/superpowers299k近 7 天新增 2,675 个 star
Compare MCP tool behavior between target and baseline versions using pre-built and custom stories with diff-based triage.
You are the evaluator. Run the steps in order; each one uses the output of the step before it.
From $ARGUMENTS, extract:
--baseline: REQUIRED. Git tag/branch of the released version (e.g., v6.6.1).--agents: Agent list (default: gemini). Comma-separated.--stories: Force specific pre-built story IDs (e.g., s01,s02). Overrides triage selection.--all-stories: Skip triage, run ALL pre-built stories.--keep-container: Keep HA containers alive after run for manual inspection.--model: Model for Claude agent (e.g., haiku, sonnet).If $ARGUMENTS is --help or missing --baseline, show usage and stop:
/bat-story-eval --baseline v6.6.1
/bat-story-eval --baseline v6.6.1 --agents gemini,claude
/bat-story-eval --baseline v6.6.1 --stories s01,s02
/bat-story-eval --baseline v6.6.1 --all-stories --agents claude --model haiku
cd "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/worktree/uat-stories"
git diff <baseline>..HEAD -- src/ha_mcp/ --stat
git diff <baseline>..HEAD -- src/ha_mcp/ --name-only
Classify changed files:
tools/tools_*.py): specific tool implementations changedclient/, server.py, errors.py, tools/util_helpers.py): affects all toolsutils/, resources/): may affect all toolsSkip if --stories or --all-stories was passed.
tests/uat/stories/catalog/s*.yaml (title, description, prompt, setup)client/, server.py, errors.py) -> all stories selectedRead the diff carefully. Your job is to catch regressions. For each changed code path NOT covered by selected pre-built stories, ask: "could this break something a user would notice?" If yes, design a custom story to test that hypothesis.
Guidelines: Always create at least 1 custom story. Each must test a distinct regression hypothesis — don't create stories that overlap. Stop when you've covered the risky gaps.
Write each as /tmp/custom_c<NN>.yaml using the standard story format:
id: c01
title: "Short description of what is being tested"
category: custom
weight: 5
description: >
Rationale: [what changed in the diff and why this scenario tests it]
setup:
- tool: ha_config_set_helper
args:
helper_type: "input_boolean"
name: "Test Entity Name"
prompt: >
[Natural language request a real user would make that exercises the changed code]
teardown: []
verify:
questions:
- "Did the agent achieve the expected outcome?"
- "Did it use the expected tools?"
expected:
tools_should_use:
- ha_search
description: >
[What a correct agent should do]
Design principles:
For EACH agent, run all stories against the baseline version. One container per agent, reused across all stories.
cd "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/worktree/uat-stories"
uv run python tests/uat/stories/run_story.py \
tests/uat/stories/catalog/<first_story>.yaml \
--agents <agent> --keep-container \
--branch <baseline> \
--results-file local/uat-results.jsonl
CAPTURE from stderr: HA URL (e.g., http://localhost:32771), token, session file path.
After each story, verify via ha_query.py using the story's verify.questions:
uv run python tests/uat/stories/scripts/ha_query.py \
--ha-url http://localhost:PORT --ha-token TOKEN \
--agent <agent> \
"Does an automation with alias 'Sunset Porch Light' exist?"
Record each answer as confirmed / denied / unclear. A non-zero exit
from ha_query.py means the query itself failed (the output carries an
[exit N] marker; [exit 124] is a timeout) — that is not one of the three
outcomes; re-run it, and if it keeps failing record the story as unverified
(Step 5) rather than scoring it. See references/evaluation-protocol.md.
Run remaining pre-built stories on the same container:
uv run python tests/uat/stories/run_story.py \
tests/uat/stories/catalog/<next_story>.yaml \
--agents <agent> --ha-url http://localhost:PORT --ha-token TOKEN \
--branch <baseline> \
--results-file local/uat-results.jsonl
Verify each immediately after running.
uv run python tests/uat/stories/run_story.py \
/tmp/custom_c01.yaml \
--agents <agent> --ha-url http://localhost:PORT --ha-token TOKEN \
--branch <baseline> \
--results-file local/uat-results.jsonl
Verify each via ha_query.py using the custom story's verify.questions.
Stop only the container kept in step 1a. PORT is the host port in its Container kept alive: http://localhost:PORT line:
docker stop $(docker ps -q --filter "publish=PORT")
Repeat Step 1 for the target (local code). Same stories, same order, fresh container.
The only difference: omit --branch so run_story.py uses local code.
uv run python tests/uat/stories/run_story.py \
tests/uat/stories/catalog/<first_story>.yaml \
--agents <agent> --keep-container \
--results-file local/uat-results.jsonl
Same container reuse for remaining stories (--ha-url). Same verification after each.
For each story on each version, read the session file captured during the run.
Gemini sessions (JSON):
python3 -c "
import json, sys
data = json.load(open(sys.argv[1]))
for msg in data.get('messages', []):
for tc in msg.get('toolCalls', []):
print(f\" {tc['name']} ({tc.get('status', '?')})\")
" /path/to/session.json
Claude sessions (JSONL):
python3 -c "
import json, sys
for line in open(sys.argv[1]):
entry = json.loads(line)
if entry.get('type') == 'assistant':
for b in entry.get('message', {}).get('content', []):
if b.get('type') == 'tool_use':
print(f\" {b['name']}\")
" /path/to/session.jsonl
Compare against expected.tools_should_use:
| Black-Box | White-Box | Score |
|---|---|---|
| Entity correct + right structure | Right tools | pass |
| Entity correct + right structure | Wrong tools or recovered errors | pass (with notes) |
| Entity correct + wrong structure | Any | partial |
| Entity not created | Any | fail |
Primary metrics (decide pass/fail on these):
Secondary metrics (report but don't decide on these alone):
# Gemini: input includes cached, so subtract
billable = (input - cached) + output + thoughts
# Claude: input_tokens is already non-cached
billable = input + output
For each story+agent:
Append eval results as NEW lines (never modify existing):
record["eval_score"] = "pass" # or "partial", "fail", or "unverified"
record["eval_notes"] = "Entity created, triggers verified"
record["eval_trend"] = "stable" # or "new", "improved", "decreased"
# "unverified" is for a story whose verification query itself failed — it is
# not a result, so it carries no trend and is not compared to the baseline.
Diff: <baseline>..HEAD — N files changed in src/ha_mcp/
Selected pre-built: s01, s03, s05 (3 stories — tools_automation.py, tools_search.py changed)
Custom stories: c01, c02 (2 stories — covering error handling, fuzzy search threshold)
Skipped: s02, s04, s06-s12 (tools unchanged)
Also read the model and quantization fields from each JSONL record (written
by run_story.py) and show them as columns. Results vary by model, and the same
base model at different quants behaves very differently, so a report naming only
the agent is ambiguous after the fact. (Quant is - for cloud backends that
don't expose it.)
| Story | Agent | Model | Quant | Baseline | Target | Trend | Baseline Tokens | Target Tokens | Delta |
|-------|--------|------------------|-------|----------|--------|--------|-----------------|---------------|-------|
| s01 | claude | claude-sonnet-4-6| - | pass | pass | stable | 36,262 | 34,100 | -6% |
| s03 | claude | claude-sonnet-4-6| - | pass | pass | stable | 42,000 | 41,500 | -1% |
For EACH custom story, output a full section:
#### c01: [Title]
**Rationale**: [What changed in the diff and why this tests it]
**Setup**:
- Created input_boolean "Sophisticated Kitchen Sensor" via FastMCP
**Test prompt**: "[The exact prompt sent to the agent]"
**Verification**:
| Question | Baseline | Target |
|----------|----------|--------|
| Found the entity? | confirmed | confirmed |
| Used ha_search? | confirmed | confirmed |
**Score**: baseline=pass, target=pass, trend=stable
**Tokens**: baseline=28,500, target=27,200 (-5%)
If any trend = decreased:
git diff <baseline>..HEADWhen a story has >30% more billable tokens vs baseline, check for KV-cache misses:
for i, msg in enumerate(data["messages"]):
tok = msg.get("tokens", {})
cached = tok.get("cached", 0)
total = tok.get("input", 0)
print(f"Turn {i+1}: input={total:,} cached={cached:,} non-cached={total-cached:,}")
A turn with cached=0 after a non-cold-start turn = KV-cache miss (provider-side, not a code regression).
Compare tool description sizes between versions:
uv run python tests/uat/stories/scripts/measure_tools.py \
--output local/tool-sizes-target.json
uv run python tests/uat/stories/scripts/measure_tools.py \
--output local/tool-sizes-baseline.json --branch <baseline>
Flag >5% total size increase (directly impacts token cost per turn).
| File | Purpose |
|---|---|
tests/uat/stories/run_story.py | Story runner (container, setup, agent CLI) |
tests/uat/stories/scripts/ha_query.py | Query live HA via agent+MCP for verification |
tests/uat/stories/catalog/s*.yaml | Pre-built story definitions |
local/uat-results.jsonl | Historical results (gitignored) |
references/evaluation-protocol.md | Scoring rules, verification questions, cross-agent checks |
references/regression-protocol.md | Regression classification and flaky handling |
--baseline is required: it's both the diff source and the control group--keep-container), rest use --ha-url/tmp/ (ephemeral); full details reported in Step 6worktree/uat-stories worktree root (where pyproject.toml lives) for uv runOBobra/superpowers299k近 7 天新增 2,675 个 star
MAmattpocock/skills290k近 7 天新增 10,235 个 star
MAmattpocock/skills290k近 7 天新增 10,235 个 star
MAmattpocock/skills290k近 7 天新增 10,235 个 star
AFaffaan-m/ECC279k近 7 天新增 4,056 个 star
AFaffaan-m/ECC279k近 7 天新增 4,056 个 star