agent-crucible 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. agent_crucible-0.4.2/.gitattributes +1 -0
  2. agent_crucible-0.4.2/.github/workflows/ci.yml +27 -0
  3. agent_crucible-0.4.2/.github/workflows/release.yml +25 -0
  4. agent_crucible-0.4.2/.gitignore +25 -0
  5. agent_crucible-0.4.2/BENCHMARK.md +125 -0
  6. agent_crucible-0.4.2/LICENSE +21 -0
  7. agent_crucible-0.4.2/PKG-INFO +409 -0
  8. agent_crucible-0.4.2/README.md +397 -0
  9. agent_crucible-0.4.2/SECURITY.md +40 -0
  10. agent_crucible-0.4.2/examples/ci/skill-eval.yml +73 -0
  11. agent_crucible-0.4.2/examples/error-handling/.crucible/history.jsonl +3 -0
  12. agent_crucible-0.4.2/examples/error-handling/.crucible/runs/20260831T034109Z-5729f88cabdd1eca.json +443 -0
  13. agent_crucible-0.4.2/examples/error-handling/.crucible/runs/20260831T045243Z-5729f88cabdd1eca.json +753 -0
  14. agent_crucible-0.4.2/examples/error-handling/SKILL.md +44 -0
  15. agent_crucible-0.4.2/examples/error-handling/evals/evals.yaml +13 -0
  16. agent_crucible-0.4.2/examples/error-handling/evals/fixtures/task-001/src/calc.py +9 -0
  17. agent_crucible-0.4.2/examples/error-handling/evals/fixtures/task-001/tests/test_calc.py +22 -0
  18. agent_crucible-0.4.2/examples/error-handling/evals/task-001.yaml +27 -0
  19. agent_crucible-0.4.2/examples/error-handling/evals/triggers.yaml +19 -0
  20. agent_crucible-0.4.2/examples/negative-control/.crucible/history.jsonl +1 -0
  21. agent_crucible-0.4.2/examples/negative-control/.crucible/runs/20260831T035920Z-8bf8dd00b9e0a4f6.json +393 -0
  22. agent_crucible-0.4.2/examples/negative-control/README.md +16 -0
  23. agent_crucible-0.4.2/examples/negative-control/SKILL.md +12 -0
  24. agent_crucible-0.4.2/examples/negative-control/evals/evals.yaml +9 -0
  25. agent_crucible-0.4.2/examples/negative-control/evals/fixtures/task-001/src/seq.py +3 -0
  26. agent_crucible-0.4.2/examples/negative-control/evals/fixtures/task-001/tests/test_seq.py +10 -0
  27. agent_crucible-0.4.2/examples/negative-control/evals/task-001.yaml +16 -0
  28. agent_crucible-0.4.2/examples/sql-parameterization/.crucible/history.jsonl +1 -0
  29. agent_crucible-0.4.2/examples/sql-parameterization/.crucible/runs/20260831T034530Z-451e5bfcf15706cd.json +493 -0
  30. agent_crucible-0.4.2/examples/sql-parameterization/SKILL.md +38 -0
  31. agent_crucible-0.4.2/examples/sql-parameterization/evals/evals.yaml +14 -0
  32. agent_crucible-0.4.2/examples/sql-parameterization/evals/fixtures/task-001/src/db.py +18 -0
  33. agent_crucible-0.4.2/examples/sql-parameterization/evals/fixtures/task-001/tests/test_db.py +26 -0
  34. agent_crucible-0.4.2/examples/sql-parameterization/evals/task-001.yaml +32 -0
  35. agent_crucible-0.4.2/examples/sql-parameterization/evals/triggers.yaml +12 -0
  36. agent_crucible-0.4.2/examples/type-hints/.crucible/history.jsonl +2 -0
  37. agent_crucible-0.4.2/examples/type-hints/.crucible/runs/20260831T035159Z-d3a2310433f5c630.json +493 -0
  38. agent_crucible-0.4.2/examples/type-hints/.crucible/runs/20260831T083738Z-d3a2310433f5c630.json +853 -0
  39. agent_crucible-0.4.2/examples/type-hints/SKILL.md +42 -0
  40. agent_crucible-0.4.2/examples/type-hints/evals/evals.yaml +12 -0
  41. agent_crucible-0.4.2/examples/type-hints/evals/fixtures/task-001/src/report.py +5 -0
  42. agent_crucible-0.4.2/examples/type-hints/evals/fixtures/task-001/tests/test_report.py +14 -0
  43. agent_crucible-0.4.2/examples/type-hints/evals/task-001.yaml +31 -0
  44. agent_crucible-0.4.2/examples/type-hints/evals/triggers.yaml +12 -0
  45. agent_crucible-0.4.2/pyproject.toml +24 -0
  46. agent_crucible-0.4.2/src/agent_crucible/__init__.py +11 -0
  47. agent_crucible-0.4.2/src/agent_crucible/assertions.py +168 -0
  48. agent_crucible-0.4.2/src/agent_crucible/cli.py +307 -0
  49. agent_crucible-0.4.2/src/agent_crucible/config.py +275 -0
  50. agent_crucible-0.4.2/src/agent_crucible/environment.py +45 -0
  51. agent_crucible-0.4.2/src/agent_crucible/regression.py +180 -0
  52. agent_crucible-0.4.2/src/agent_crucible/reporter.py +200 -0
  53. agent_crucible-0.4.2/src/agent_crucible/runner.py +436 -0
  54. agent_crucible-0.4.2/src/agent_crucible/scaffold.py +121 -0
  55. agent_crucible-0.4.2/src/agent_crucible/scoring.py +253 -0
  56. agent_crucible-0.4.2/src/agent_crucible/storage.py +103 -0
  57. agent_crucible-0.4.2/src/agent_crucible/trigger.py +191 -0
  58. agent_crucible-0.4.2/src/agent_crucible/validator.py +215 -0
  59. agent_crucible-0.4.2/tests/fixtures/stream_skill_invoked.jsonl +2 -0
  60. agent_crucible-0.4.2/tests/test_assertions.py +106 -0
  61. agent_crucible-0.4.2/tests/test_environment.py +51 -0
  62. agent_crucible-0.4.2/tests/test_integration.py +192 -0
  63. agent_crucible-0.4.2/tests/test_regression.py +96 -0
  64. agent_crucible-0.4.2/tests/test_regressions.py +129 -0
  65. agent_crucible-0.4.2/tests/test_runner.py +95 -0
  66. agent_crucible-0.4.2/tests/test_scoring.py +104 -0
  67. agent_crucible-0.4.2/tests/test_trigger.py +101 -0
  68. agent_crucible-0.4.2/tests/test_validator.py +71 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest, macos-latest]
14
+ python: ["3.10", "3.11", "3.12"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Test
24
+ # The suite is hermetic: it stubs the agent, so no ANTHROPIC_API_KEY or
25
+ # claude CLI is needed. This is the real cross-platform proof, Linux
26
+ # included, that the runner, assertions, and stats work everywhere.
27
+ run: python -m pytest -q
@@ -0,0 +1,25 @@
1
+ name: Release to PyPI
2
+
3
+ # Publishes on a version tag (e.g. v0.1.0). Uses PyPI Trusted Publishing
4
+ # (OIDC) — configure the publisher at pypi.org first; no token secret needed.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build-and-publish:
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write # for trusted publishing
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.11"
20
+ - name: Build
21
+ run: |
22
+ pip install build
23
+ python -m build
24
+ - name: Publish
25
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
9
+
10
+ # Benchmark output lives inside each skill directory.
11
+ **/.agent-skill/runs/
12
+
13
+ # Third-party agent skills installed via `npx skills add` (e.g. strix).
14
+ .agents/
15
+ .claude/
16
+
17
+ # strix lockfile from npx skills add
18
+ skills-lock.json
19
+
20
+ # build artifacts
21
+ dist/
22
+
23
+ # local benchmark scratch (raw run records are committed under examples/)
24
+ bench_logs/
25
+ run_*.sh
@@ -0,0 +1,125 @@
1
+ # Benchmark results
2
+
3
+ Real runs against a live Claude Code agent. These are the actual numbers this
4
+ tool produced — including the ones that show **no effect**. A benchmark you can
5
+ only pass is not a benchmark.
6
+
7
+ ## How to reproduce
8
+
9
+ ```bash
10
+ pip install agent-crucible
11
+ git clone https://github.com/ayah-spec/agent-crucible
12
+ cd agent-crucible
13
+ crucible test examples/error-handling -n 10
14
+ crucible test examples/negative-control -n 5
15
+ ```
16
+
17
+ Each run copies the task fixture into a throwaway git repo, runs the agent once
18
+ without the skill and once with it installed to `.claude/skills/`, then checks
19
+ the result with deterministic assertions. Raw per-run records for every number
20
+ below are committed under each skill's `.crucible/runs/`.
21
+
22
+ ## Conditions
23
+
24
+ | | |
25
+ |---|---|
26
+ | Agent / harness | Claude Code CLI, version 2.1.251 |
27
+ | Model | `claude-sonnet-4-5-20250929` (pinned) |
28
+ | OS | Windows |
29
+ | Python | 3.11.9 |
30
+ | Repeats (N) | per-condition, stated per row |
31
+ | Success test | Fisher exact on per-run outcomes, conclusive at p < 0.05 |
32
+ | Date | 2026-08-31 |
33
+
34
+ Numbers are model-, harness-, task-, and environment-dependent. The correct
35
+ reading of a row is *"on this eval suite, under this model and this Claude Code
36
+ version, the skill did X"* — not *"this skill makes the AI X% better"*.
37
+
38
+ ## Task success
39
+
40
+ | Skill | N | Without | With | Δ | p-value | Verdict |
41
+ |---|---|---|---|---|---|---|
42
+ | error-handling | 5 | 0% | 40% | +40pp | 0.444 | inconclusive |
43
+ | **error-handling** | **10** | **0%** | **50%** | **+50pp** | **0.033** | **improvement** |
44
+ | sql-parameterization | 5 | 100% | 100% | 0pp | 1.000 | no effect |
45
+ | type-hints | 5 | 40% | 80% | +40pp | 0.524 | inconclusive |
46
+ | type-hints | 10 | 20% | 60% | +40pp | 0.170 | inconclusive |
47
+ | negative-control | 5 | 100% | 100% | 0pp | 1.000 | no effect (expected) |
48
+
49
+ ## What these results actually say
50
+
51
+ **error-handling is the headline — and it is a lesson about sample size, not
52
+ just a win.** At N=5 the skill looked like a +40pp improvement, but Fisher's
53
+ exact test returned p=0.444: with five runs, 0/5 vs 2/5 is well within chance.
54
+ Only at N=10 (0/10 vs 5/10, p=0.033) did the effect cross into conclusive
55
+ territory. A tool that reported the N=5 point estimate as fact would have
56
+ published a "+40%" number that its own statistics did not support. This is the
57
+ single clearest demonstration of why the tool repeats runs and tests
58
+ significance instead of subtracting two scores.
59
+
60
+ **sql-parameterization shows no effect, honestly.** The baseline agent already
61
+ wrote parameterized queries and passed every assertion (1.00 → 1.00), so there
62
+ was nothing for the skill to add on this task. That is a true negative, not a
63
+ tuning failure — reported as-is.
64
+
65
+ **negative-control behaves exactly as a control should.** Its skill only changes
66
+ docstring wording, so it cannot affect whether the tests pass, and it didn't
67
+ (100% → 100%, p=1.000). Notably it *raised* token use (184k → 207k) and wall
68
+ time (61s → 92s): an irrelevant skill is not free, it is overhead. An instrument
69
+ that reported an improvement here would be untrustworthy everywhere.
70
+
71
+ **type-hints stays unproven even at N=10.** At N=5 it looked like +40pp
72
+ (p=0.524); at N=10 it was still +40pp but p=0.170 — and the baseline itself moved
73
+ (40% → 20%), which is exactly the run-to-run wobble that makes a single small
74
+ sample untrustworthy. Doubling the runs did not manufacture significance, and the
75
+ tool did not pretend otherwise. An honest "we cannot tell yet, spend more runs or
76
+ accept the uncertainty" is the correct scientific output here.
77
+
78
+ ## Efficiency (with vs without, N shown above)
79
+
80
+ | Skill | Tokens | Wall time | Cost/run |
81
+ |---|---|---|---|
82
+ | error-handling (N=10) | 178k → 193k | 35s → 38s | $0.145 → $0.154 |
83
+ | sql-parameterization | 179k → 193k | 38s → 42s | $0.145 → $0.151 |
84
+ | type-hints | 260k → 258k | 70s → 61s | $0.185 → $0.178 |
85
+ | negative-control | 184k → 207k | 61s → 92s | $0.147 → $0.159 |
86
+
87
+ None of the token/time deltas here are statistically conclusive at these sample
88
+ sizes; they are reported for transparency, not as claims. (Token counts are
89
+ dominated by cheap cache reads, so per-run cost is the more meaningful figure.)
90
+
91
+ ## Trigger evaluation — and an honest surprise
92
+
93
+ `crucible trigger` detects whether the agent invoked the **`Skill` tool** for
94
+ a skill. Run against these three convention skills, the result was the same each
95
+ time:
96
+
97
+ | Skill | Should-fire cases | Fired (Skill tool) | Quiet cases correct |
98
+ |---|---|---|---|
99
+ | error-handling | 2 | 0 | 2 / 2 |
100
+ | sql-parameterization | 2 | 0 | 2 / 2 |
101
+ | type-hints | 2 | 0 | 2 / 2 |
102
+
103
+ Taken alone that reads like "the skills never trigger". But the A/B section
104
+ proves error-handling **does** change behaviour (N=10, p=0.033). Both are true,
105
+ and reconciling them is the real finding:
106
+
107
+ **Capturing one such run's event stream shows why.** Given "Add error handling to
108
+ src/calc.py", sonnet-4-5 in headless mode used `Read` and `Edit` and finished the
109
+ task directly — it never called the `Skill` tool. The skill still shaped the
110
+ result (the with-skill condition followed the `code=` convention more often),
111
+ because Claude Code surfaces a simple convention skill by putting it in context,
112
+ not by making the model formally invoke it.
113
+
114
+ So explicit `Skill`-tool invocation is a **lower bound on influence, not a measure
115
+ of it.** Trigger evaluation is meaningful for skills *designed to be invoked* (a
116
+ multi-step procedure the model calls on demand — a canary skill with a unique
117
+ output was detected correctly in testing), and near-zero for convention skills
118
+ that work by being in context — even when, as here, they measurably help. The
119
+ true-negative side still holds: none of the skills fired on unrelated prompts
120
+ (math, docstring questions), so there were no false positives.
121
+
122
+ This is the kind of thing a benchmark exists to surface. The honest headline is
123
+ not "trigger recall is 0%" — it is "for these skills, influence and explicit
124
+ invocation are different things, and only the A/B test measures influence."
125
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elfan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,409 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-crucible
3
+ Version: 0.4.2
4
+ Summary: Reproducible testing, benchmarking, and regression testing for Agent Skills.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: pyyaml>=6.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Agent Crucible
14
+
15
+ Reproducible A/B testing for Agent Skills.
16
+
17
+ A `SKILL.md` is a prompt you ship to other people. This tool answers the only
18
+ question that matters about one: **does it actually make the agent better, or
19
+ does it just feel like it does?**
20
+
21
+ It runs the same task twice — once with a baseline agent, once with the skill
22
+ installed — repeats that N times, checks the results with deterministic
23
+ assertions, and reports the difference with a confidence interval attached.
24
+
25
+ **Real results are in [BENCHMARK.md](BENCHMARK.md)** — including a skill that only
26
+ becomes conclusive at N=10, two skills with no measurable effect, and an honest
27
+ surprise about what trigger detection can and cannot see.
28
+
29
+ ```
30
+ Without With
31
+ Task success 50% 100%
32
+ Assertion score 0.83 1.00
33
+ Tokens 8,700 7,100
34
+ Time 55s 41s
35
+
36
+ IMPACT (95% bootstrap CI on the delta)
37
+
38
+ Task success +50.0pp [+20.0, +80.0] p=0.033
39
+ ~ Tokens -23.0% [-41.2%, +2.1%]
40
+
41
+ CONFIDENCE MEDIUM (N=5 per condition)
42
+ ~ marks a delta not distinguishable from run-to-run noise.
43
+
44
+ PER TASK (success rate)
45
+
46
+ task-001 0% 100%
47
+ task-002 100% 100%
48
+ ```
49
+
50
+ The per-task breakdown matters: an aggregate `+50pp` driven entirely by one
51
+ task is a different finding from one spread across all of them.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install agent-crucible
57
+ ```
58
+
59
+ Or from a clone: `pip install -e .`
60
+
61
+ Requires Python 3.10+, `git`, and an authenticated [Claude Code](https://claude.com/claude-code)
62
+ CLI on `PATH`. Tested on Linux, macOS, and Windows (CI runs the suite on all
63
+ three across Python 3.10–3.12). Verify the agent side works before benchmarking
64
+ anything:
65
+
66
+ ```bash
67
+ claude -p "reply with OK" --output-format json
68
+ ```
69
+
70
+ If that returns `"Failed to authenticate"`, log in first — every run in this tool
71
+ shells out to that same command.
72
+
73
+ ## Use
74
+
75
+ ```bash
76
+ crucible init my-skill # scaffold SKILL.md + evals/
77
+ crucible validate my-skill # static checks, no tokens spent
78
+ crucible test my-skill # the A/B benchmark
79
+ crucible report my-skill # re-render the last run
80
+ crucible regression my-skill # did the latest run drop below the baseline?
81
+ ```
82
+
83
+ ### 1. Validate
84
+
85
+ Runs before every benchmark, and refuses to spend tokens on a broken suite.
86
+
87
+ ```
88
+ [ok] SKILL.md exists
89
+ [ok] valid YAML frontmatter
90
+ [ok] frontmatter has 'name'
91
+ [ok] frontmatter has 'description'
92
+ [ok] referenced files exist (2/2)
93
+ [ok] [task-001] prompt is customised, not the scaffold placeholder
94
+ [warn] model 'sonnet' is a pinned version (aliases drift between releases)
95
+ ```
96
+
97
+ ### 2. Write eval tasks
98
+
99
+ ```yaml
100
+ id: task-001
101
+ name: Make calc errors typed
102
+
103
+ prompt: |
104
+ The test suite in tests/ fails. Fix src/calc.py so every test passes.
105
+ Do not modify anything under tests/.
106
+
107
+ fixture: fixtures/task-001
108
+
109
+ assertions:
110
+ - type: command # did it solve the task?
111
+ run: "python -m pytest -q"
112
+ expect_exit: 0
113
+
114
+ - type: file_contains # did it follow the convention the skill teaches?
115
+ path: src/calc.py
116
+ pattern: "raise CalcError\\([^)]*code="
117
+
118
+ - type: file_unchanged # anti-gaming: editing the tests does not count
119
+ path: tests/test_calc.py
120
+ ```
121
+
122
+ Assertion types: `command`, `file_exists`, `file_absent`, `file_contains`,
123
+ `file_changed`, `file_unchanged`.
124
+
125
+ Before burning tokens, check two things — neither spends a cent:
126
+
127
+ ```bash
128
+ crucible test my-skill --dry-run # can the suite even measure work?
129
+ crucible test my-skill --estimate # what will a real run cost?
130
+ ```
131
+
132
+ `--dry-run` runs the assertions against the untouched fixture. Anything that
133
+ passes there is flagged `[TRIVIAL]` — it would pass even if the agent did
134
+ nothing. (Assertions that check for the *absence* of something — `file_absent`,
135
+ `file_unchanged`, or a `file_contains` with `expect: false` — are guards, and
136
+ correctly pass on the pristine fixture.)
137
+
138
+ `--estimate` projects cost and wall time as a range. With no history it is a
139
+ coarse prior; after one real run it uses your own recorded per-run cost. A real
140
+ `test` prints the estimate first, then runs.
141
+
142
+ ### 3. Benchmark
143
+
144
+ ```bash
145
+ crucible test my-skill -n 10
146
+ ```
147
+
148
+ ## How it works
149
+
150
+ Per run:
151
+
152
+ 1. The task fixture is copied into a throwaway temp directory.
153
+ 2. That directory is `git init`-ed and committed, which is what makes
154
+ `file_changed` / `file_unchanged` assertions possible.
155
+ 3. In the **with-skill** condition only, the skill is copied to
156
+ `.claude/skills/<name>/` inside that workspace. The `evals/` directory is
157
+ excluded — leaking it would hand the agent its own grading criteria.
158
+ 4. `claude -p "<prompt>" --output-format json` runs with the workspace as cwd,
159
+ with `--setting-sources project`. (Note: in practice this does not fully hide
160
+ your personal `~/.claude` skills — see "Known gap" above. They appear in both
161
+ conditions, so the delta stays attributable to the skill under test.)
162
+ 5. Assertions run against the resulting workspace; tokens, cost and wall time
163
+ come from the agent's own JSON result.
164
+ 6. The workspace is deleted (`--keep-workspace` to inspect it).
165
+
166
+ Results land in `<skill>/.crucible/runs/*.json`, with one summary line per
167
+ invocation appended to `<skill>/.crucible/history.jsonl`.
168
+
169
+ ## Design decisions worth knowing
170
+
171
+ **No LLM judge.** Scoring is deterministic: assertions pass or they don't. An
172
+ LLM grader adds a second noisy component on top of an already noisy agent, and
173
+ you can no longer tell which one moved. Quality rubrics are a later addition,
174
+ not the foundation.
175
+
176
+ **Every delta carries an uncertainty estimate.** Agent runs are
177
+ non-deterministic; the same prompt gives different token counts and sometimes
178
+ different outcomes. A single number invites false regression alarms. Anything
179
+ not separable from noise is printed with `~` and called inconclusive.
180
+
181
+ **Task success is judged by Fisher's exact test, not by the bootstrap.** This
182
+ was not the original design — it was forced by testing the tool against a
183
+ deliberately noisy stub agent with a weak 60%-vs-50% effect. The percentile
184
+ bootstrap called that a *conclusive regression* on the first attempt: five
185
+ Bernoulli samples per condition is exactly the regime where it is
186
+ anti-conservative. Fisher's exact test on the 2×2 outcome table has no such
187
+ problem at small N, and still detects a real effect at N=5 (0/5 vs 5/5 gives
188
+ p=0.008). Continuous metrics keep the bootstrap interval, at 95% to match the
189
+ same 0.05 threshold.
190
+
191
+ **Secondary metrics are not corrected for multiple comparisons.** Five metrics
192
+ per report at a 5% threshold means roughly one report in seven contains a
193
+ secondary claim that is not real. The report says so out loud when it makes one
194
+ below N=10, rather than letting you believe all five numbers equally.
195
+
196
+ **Confidence is labelled by sample size.** N<5 is `LOW` and says so in the
197
+ verdict. The tool will not pretend five runs are evidence — at N=1 it reports
198
+ the observed delta and then explicitly refuses to conclude from it.
199
+
200
+ **Pin your model.** `model: sonnet` is an alias that moves between releases. A
201
+ comparison against a run from last month is meaningless if the model changed
202
+ underneath it. `validate` warns about this.
203
+
204
+ **The baseline carries Claude Code's bundled skills.** They appear in both
205
+ conditions, so the delta stays attributable to the skill under test, and they
206
+ are identical across installs of the same version. `isolation: bare` removes
207
+ them for a clean-room baseline (needs `ANTHROPIC_API_KEY`).
208
+
209
+ **This is decision support, not an oracle.** `--strict` exists for CI, but
210
+ consider making the CI job a warning rather than a hard block, and gate it on
211
+ `SKILL.md` actually having changed — every invocation spends real tokens.
212
+ `N tasks × N repeats × 2 conditions` agent runs is not free the way a unit test
213
+ is free.
214
+
215
+ ## Examples
216
+
217
+ Complete, runnable skills live in `examples/`, each mixing a "did it work"
218
+ command assertion with a "did it the right way" content assertion, and each
219
+ carrying trigger cases:
220
+
221
+ | Skill | Domain | The convention it checks |
222
+ |---|---|---|
223
+ | `error-handling` | code style | raises carry a machine-readable `code=` |
224
+ | `sql-parameterization` | security | queries use placeholders, never f-strings |
225
+ | `type-hints` | typing | functions get annotated params + return type |
226
+ | `negative-control` | — | **deliberately irrelevant** to task success |
227
+
228
+ ```bash
229
+ crucible validate examples/sql-parameterization
230
+ crucible test examples/sql-parameterization --dry-run
231
+ crucible test examples/sql-parameterization --estimate
232
+ crucible test examples/sql-parameterization -n 5
233
+ ```
234
+
235
+ Each positive example is built so the difference is visible: a baseline agent
236
+ usually makes the tests pass, but only the skilled agent reliably follows the
237
+ convention. "Tests pass" and "did it the right way" are different questions —
238
+ which is exactly why a single assertion is not enough.
239
+
240
+ `negative-control` is the opposite on purpose: its skill only changes docstring
241
+ wording, so it *cannot* affect whether the tests pass. Running it should report
242
+ **no conclusive effect** — a measurement instrument you can trust reads zero when
243
+ the effect is zero.
244
+
245
+ ## CI
246
+
247
+ `examples/ci/skill-eval.yml` is a GitHub Actions template. It triggers only when
248
+ `SKILL.md` or `evals/` changed, and posts the report as a PR comment instead of
249
+ failing the build.
250
+
251
+ ## Validated against a live agent
252
+
253
+ Run end-to-end against the real `claude` CLI, not just a stub. Two things the
254
+ example run demonstrated, both worth internalising before you trust any number:
255
+
256
+ - **A lucky small sample lies, and the tool catches it.** At N=3 the example
257
+ skill scored a clean 3/3 vs 0/3 — an apparent +100pp. At N=5 the same skill,
258
+ same prompt, scored 1/5 (+20pp, p=1.000). The effect is real but weak and
259
+ inconsistent; three runs happened to catch a good streak. A tool that reports
260
+ one number would have published "+100% success". This one reported "not
261
+ distinguishable from noise" and asked for more runs. That is the whole point.
262
+
263
+ - **Availability is not invocation.** A skill in `.claude/skills/` is *discovered*
264
+ by the agent but only *used* when its description matches the task. The prompt
265
+ "make the tests pass" did not trigger an error-handling skill; "add proper
266
+ error handling following this project's conventions" did. Your eval prompt is
267
+ a real variable — the same skill looks useless or useful depending on it.
268
+
269
+ Baseline isolation, found in the same run: the headless agent always sees the
270
+ skills that ship with Claude Code (design, code-review, …). They are **not** your
271
+ personal `~/.claude` skills — they are bundled with the install, so they are the
272
+ same for anyone on the same Claude Code version, and they appear in *both*
273
+ conditions, cancelling out of the delta. For a true clean-room baseline set
274
+ `isolation: bare` in `evals.yaml` (runs the agent with `--bare`); it needs
275
+ `ANTHROPIC_API_KEY`, since bare mode never reads your OAuth login. The default
276
+ keeps the bundled skills and works with OAuth.
277
+
278
+ ### 4. Regression
279
+
280
+ Each `test` writes a run record. `regression` compares the newest run against an
281
+ earlier baseline — by default the most recent run whose skill content differs, so
282
+ it answers "did my last edit make the skill worse?"
283
+
284
+ ```bash
285
+ crucible regression my-skill
286
+ ```
287
+
288
+ ```
289
+ With-skill success: 86% -> 62% (-24pp, p=0.021)
290
+ --------------------------------------------------------
291
+ x REGRESSION — with-skill success dropped beyond run-to-run noise.
292
+ ```
293
+
294
+ Crucially, it is judged by the same Fisher exact test as the benchmark, not by
295
+ subtracting two scores. A drop from 100% to 80% on five runs prints
296
+ `~ PASS (inconclusive)` with `p=1.000`, because one flipped run is exactly what
297
+ noise produces — a naive point-vs-point check would have raised a false alarm.
298
+ It exits `1` **only** on a conclusive drop, so a CI gate blocks a genuine
299
+ regression without failing on the dice:
300
+
301
+ ```yaml
302
+ - run: crucible test ./my-skill -n 10
303
+ - run: crucible regression ./my-skill # exit 1 blocks the merge
304
+ ```
305
+
306
+ ### 5. Trigger evaluation
307
+
308
+ A skill can be excellent when used and still be worthless because the agent never
309
+ reaches for it — or noisy because it fires on unrelated prompts. `trigger`
310
+ measures that directly. In `evals/triggers.yaml` you label prompts:
311
+
312
+ ```yaml
313
+ cases:
314
+ - prompt: "Add error handling to src/calc.py."
315
+ should_trigger: true
316
+ - prompt: "What is 27 times 34?"
317
+ should_trigger: false
318
+ ```
319
+
320
+ ```bash
321
+ crucible trigger my-skill
322
+ ```
323
+
324
+ ```
325
+ Precision (when it fired, was it right): 100% FP=0
326
+ Recall (of prompts that should fire it): 75% FN=1
327
+ ! 1 prompt that should have used the skill did not — available but not reached for.
328
+ ```
329
+
330
+ Detection is deterministic, not an LLM judging "was it probably used": the run
331
+ uses `--output-format stream-json`, and a trigger is a `Skill` tool-use event for
332
+ this skill in the stream.
333
+
334
+ **Scope, learned from real runs (see [BENCHMARK.md](BENCHMARK.md)):** this
335
+ measures *explicit `Skill`-tool invocation*. That is the right signal for skills
336
+ designed to be invoked as a procedure. But Claude Code activates a simple
337
+ *convention* skill by putting it in context, not by making the model call the
338
+ Skill tool — so such a skill can measurably change behaviour (proven by the A/B
339
+ test) while its explicit-invocation rate is zero. Read trigger recall as a lower
340
+ bound on influence, not as "the skill did nothing"; the A/B test is what measures
341
+ influence. The false-positive side is still meaningful: a skill firing on
342
+ unrelated prompts is a real defect this catches.
343
+
344
+ ## Real results
345
+
346
+ Four skills were run against a live agent (Claude Code, `claude-sonnet-4-5`,
347
+ N=5). The full write-up with methodology and raw data is in
348
+ [BENCHMARK.md](BENCHMARK.md); the short version:
349
+
350
+ - **No skill reached statistical significance on task success at N=5.** Two
351
+ showed promising but inconclusive swings (0→40%, 40→80%, both p≈0.5); the tool
352
+ reported them as inconclusive rather than publishing the point estimate.
353
+ - **The negative control read exactly zero** (0.00 delta, p=1.000) — the
354
+ instrument does not manufacture improvements.
355
+ - **The only conclusive deltas were token *costs*** — including a skill whose
356
+ advice the baseline agent already followed, so it added tokens and nothing else.
357
+ - **Trigger recall was 0–50%**: the skills often were not invoked even when
358
+ clearly relevant, which likely explains the weak outcome effects. Availability
359
+ is not invocation.
360
+
361
+ This is the intended behaviour: run it on your own skill and it will tell you the
362
+ truth, including "no effect" or "costs more than it helps."
363
+
364
+ ## Reproducibility
365
+
366
+ Every `test` writes a full run record to `.crucible/runs/*.json` — not just
367
+ the summary, but every per-run outcome, and an `environment` block capturing what
368
+ the numbers depend on: tool version, agent (`claude`) version, model, OS,
369
+ Python version, and separate fingerprints for the **skill** and the **eval
370
+ suite**. The suite fingerprint exists because a result can flip when you reword a
371
+ prompt without touching `SKILL.md`; comparing across a changed suite fingerprint
372
+ is comparing two different experiments. `crucible report` prints the
373
+ environment block so a reader knows the conditions.
374
+
375
+ ## Limitations
376
+
377
+ Read these before quoting any number from this tool.
378
+
379
+ - **Results are model-, harness-, task-, and environment-dependent.** A correct
380
+ claim is *"on this eval suite, under this model and Claude Code version, the
381
+ skill raised task success by N percentage points"* — **not** *"this skill makes
382
+ the AI N% better"*. The run record stores exactly those conditions so the
383
+ scoped claim is the easy one to make.
384
+ - **A benchmark is only as good as its eval suite.** The tool measures whether
385
+ the agent satisfied *your* assertions on *your* tasks. A weak or unrepresentative
386
+ suite produces a confident, meaningless number. `--dry-run` guards against
387
+ assertions that pass on the untouched fixture; nothing guards against tasks that
388
+ don't represent real use.
389
+ - **Cost.** Each run is a real agent session spending real tokens. `N tasks × N
390
+ repeats × 2 conditions` is not free the way a unit test is; `--estimate` prints
391
+ the projected spend before you commit.
392
+ - **Small N is a smoke test, not evidence.** Below ~5 runs per condition the tool
393
+ labels the result `LOW` confidence and refuses to call effects conclusive, on
394
+ purpose.
395
+
396
+ ## Status
397
+
398
+ Claude Code only: `init`, `validate`, `test` (with `--dry-run`, `--estimate`,
399
+ `--strict`), `report`, `regression`, `trigger`. Tested on Linux/macOS/Windows
400
+ across Python 3.10–3.12; the test suite stubs the agent, so CI needs no API key.
401
+
402
+ Deliberately not built yet: LLM-as-judge quality scoring, cross-agent
403
+ `benchmark` / `compare`, and `audit` for skill security. These are held back on
404
+ purpose — the differentiator is deterministic, statistically honest A/B testing,
405
+ not breadth.
406
+
407
+ ## License
408
+
409
+ MIT