experiment-loop
Skill7 starsChanged 19 days agoMITView on GitHub
Autonomous experimentation pattern for iterative code improvement. Describes the modify-commit-run-evaluate-keep/discard loop generalized from autoresearch. Auto-activates on optimize, experiment, improve iteratively, benchmark, metric-driven, A/B test approaches, autonomous improvement, iterate until better.
3 other projects use a version of this.
Adopted by
Most recently taken up by
- ponycompute/agent-skills-archivetheir own version, first seen Nov 8, 2026
- ramuzes/skillstheir own version, first seen Oct 17, 2026
- qyinm/agent-skills-archivetheir own version, first seen Mar 31, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- ponycompute/agent-skills-archive★ 1same name
- qyinm/agent-skills-archive★ 1same name
- ramuzes/skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- api-contract-testingSkill
- browser-testingSkill
- ci-cd-templatesSkill
- code-review-checklistSkill
- context-escalationSkill
- deep-analysisSkill
- deep-readSkill
- deliverySkill
- deployment-runbookSkill
- divergeSkill
- docker-deploymentSkill
- executeSkill
- git-workflowSkill
- handoffSkill
- infrastructure-as-codeSkill
- investigateSkill
- kubernetes-opsSkill
- library-docsSkill
- multi-agent-orchestrationSkill
- observability-stackSkill
- remote-triggersSkill
- repo-contextSkill
- scheduled-tasksSkill
- security-scanSkill
- skill-creatorSkill
- sre-runbooksSkill
- ui-guidelinesSkill
- verified-handoffSkill
- worktree-workflowSkill
The file
We split this skill into the rules it gives an agent
7 rules. 0 are common, 7 are distinctive, and 0 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
7
Distinctive
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 89
Use the portable two-step capture, which returns 127 in both shells:
- Distinctive: line 123
Every measurement goes through it — never call the bare pipeline:
- Distinctive: line 140
VOID is not zero: Never record it as an improvement, a regression, or a plateau.
- Distinctive: line 163
After every MODIFY, run the project's build and test command before looking at the metric.
- Distinctive: line 175
Before the first iteration, measure the metric at least 5 times with no code change at all and record the spread.
- Distinctive: line 259
Rule: No noise floor, no wall-clock metric (Gate 3).
- Distinctive: line 278
Not setting a maximum iteration count
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Experiment Loop Pattern
Overview
The experiment loop is a systematic approach to iterative code improvement where each change is measured against a baseline metric and kept only if it improves the result. Inspired by karpathy/autoresearch and generalized for any measurable code quality metric.
The Core Pattern
0. VALIDATE → Prove the tool exists and actually RAN. No valid run, no metric.
1. DEFINE → Choose a measurable metric, its noise floor, and constraints
2. BASELINE → Measure current state; pin tool path + version + accepted exit codes
3. MODIFY → Make one targeted change
4. MEASURE → Re-run step 0 FIRST, then re-evaluate the metric
5. GUARD → Run build + tests. If they fail → REVERT, whatever the metric says
6. DECIDE → Keep only if the gain EXCEEDS the noise floor; otherwise revert
7. LOG → Record the change, the metric, and BOTH gate results
8. REPEAT → Go to step 3 (until done or plateau)Steps 0, 5 and 6 are the difference between an improvement loop and a loop that optimizes for breaking the toolchain. They are specified in Mandatory Preconditions below and are not optional.
When to Use This Pattern
Good fit:
- Reducing lint warnings or type errors in a codebase
- Improving test coverage for a module
- Reducing bundle size or build time
- Optimizing database query performance (measurable via EXPLAIN ANALYZE)
- Improving accessibility scores (Lighthouse, axe)
- Reducing code complexity (cyclomatic complexity metrics)
Bad fit:
- Subjective improvements (readability, "cleaner" code)
- Exploratory work (no clear metric)
- Feature development (no single metric captures correctness)
- Security fixes (can't reduce to one number)
Requirements for the Pattern
For the loop to work, you need all five. Missing any one of them makes the loop actively harmful — it will confidently report as "improvements" things that are breakage or noise.
- A metric that can be proved valid — not merely "a command that outputs a number." The tool must be present and must actually have run. A number from a tool that never ran is not a measurement. See Gate 1.
- A known noise floor — the run-to-run spread of the metric on an unchanged tree. Any gain smaller than that spread is noise. Deterministic counts have a noise floor of 0; wall-clock metrics do not. See Gate 3.
- A correctness gate independent of the metric — a build and test command whose pass/fail is authoritative. The metric never overrules it. See Gate 2.
- Controlled experiments — each change is isolated. One modification per iteration, measured independently.
- Automatic evaluation — the keep/discard decision is mechanical once gated: gates pass AND gain exceeds the noise floor? Keep. Otherwise revert. "Mechanical" describes the decision rule; it is not a licence to skip the gates.
Mandatory Preconditions
No valid run, no metric. A number produced without these three gates is void, and a loop that scores void numbers is a loop that rewards destroying the toolchain.
Gate 1 — the tool must exist and must have RUN
Root cause: a shell pipeline reports the exit status of its last command. Every metric
command in the table below ends in wc -l, grep -c, jq, or awk, so the tool's own exit
status is discarded. A missing compiler and a clean compile become indistinguishable.
Executed proof, on a machine with no TypeScript installed:
$ which tsc
tsc not found
$ tsc --noEmit 2>&1 | grep error | wc -l
0 # <- a PERFECT score
$ echo $?
0 # <- and the pipeline "succeeded"Deleting the compiler outscores every genuine optimization. This is a property of the pipeline
shape, not of tsc — the eslint, vitest, du and time rows all have the same hole.
Two tempting guards that do not work:
set -o pipefail—grepexits 1 when it matches nothing, so a clean codebase and a missing tool both yield rc 1. Verified: both cases return 1.${PIPESTATUS[0]}— a bash-ism. Verified: in bash it is127; in zshPIPESTATUS[0]is empty (the array ispipestatus, and it is 1-indexed).
Use the portable two-step capture, which returns 127 in both shells:
# METRIC_OK_RC: space-separated exit codes that mean "the tool RAN".
# Default 0 = fail closed. Record the real set at BASELINE — linters and compilers
# exit non-zero when they FIND problems, and that is a successful run, not a failure.
: "${METRIC_OK_RC:=0}"
metric_run() { # metric_run <tool> <cmd...> -> raw output, or rc 2 = VOID
tool=$1; shift
command -v "$tool" >/dev/null 2>&1 || { echo "VOID: '$tool' not on PATH" >&2; return 2; }
raw=$("$@" 2>&1); rc=$?
case " $METRIC_OK_RC " in
*" $rc "*) ;;
*) printf 'VOID: %s exited %s\n%s\n' "$tool" "$rc" "$raw" >&2; return 2 ;;
esac
printf '%s\n' "$raw"
}
metric_value() { # metric_value <parser...> (reads stdin) -> ONE number, or rc 2 = VOID
v=$("$@")
# $(...) already drops trailing newlines; strip only the surrounding blanks that BSD
# `wc -l` pads with. Whatever survives must be ONE token -- NEVER join what is left.
v=${v#"${v%%[![:blank:]]*}"}; v=${v%"${v##*[![:blank:]]}"}
case "$v" in
'') echo "VOID: parser produced no output" >&2; return 2 ;;
*[[:space:]]*) printf 'VOID: parser produced %s values, not one scalar:\n%s\n' \
"$(printf '%s\n' "$v" | wc -w | tr -d '[:blank:]')" "$v" >&2; return 2 ;;
*[!0-9.]*|.|*.*.*) printf "VOID: '%s' is not a number\n" "$v" >&2; return 2 ;;
esac
printf '%s\n' "$v"
}Every measurement goes through it — never call the bare pipeline:
export METRIC_OK_RC="0 1 2" # pinned at BASELINE
raw=$(metric_run tsc tsc --noEmit) || exit 2 # VOID -> stop
val=$(printf '%s\n' "$raw" | metric_value grep -c 'error TS') || exit 2Verified behaviour of that guard across all four toolchain states:
| Toolchain state | Guard result |
tsc deleted | VOID: 'tsc' not on PATH — no metric |
tsc present, 2 real errors (exit 2) | METRIC = 2 |
tsc present, codebase genuinely clean (exit 0) | METRIC = 0 — a real zero |
tsc present but crashes (exit 139) | VOID: tsc exited 139 — no metric |
VOID is not zero. Never record it as an improvement, a regression, or a plateau. A VOID
measurement stops the loop: fix the toolchain, then resume. Pin the tool at BASELINE
(command -v <tool> plus <tool> --version) and re-assert both every iteration, so a change
that removes, shadows or downgrades the tool fails the gate instead of winning it.
One scalar or nothing. metric_run proves the tool ran; metric_value proves the parser
produced a number rather than several. A parser that emits one line per file — the obvious
first attempt at the eslint . --format json row below — must never be joined into a score:
Parser output (eslint . --format json, true total 8) | Joined | Guard result |
8 — jq '[.[].errorCount] | add' | 8 | METRIC = 8 |
0 — BSD wc -l padding | 0 | METRIC = 0 |
3 4 1 — jq '.[].errorCount', one line per file | 341 | VOID: parser produced 3 values, not one scalar |
3 4 1 — same values on one line | 341 | same VOID |
| (empty) — parser matched nothing | VOID | VOID: parser produced no output |
Joining is not merely a wrong number. With the tree completely unchanged, walking those same
three files in a different order turns 341 into 134 — a phantom -207 "improvement" that a
mechanical keep/revert banks as a win. Fix the parser, never the guard.
Gate 2 — regression gate: tests outrank the metric, always
After every MODIFY, run the project's build and test command before looking at the metric.
- Build or tests fail → REVERT immediately. Do not compare metrics; a broken tree has no score. This holds even when the metric improved — especially then, because "metric improved, tests broke" is the signature of metric gaming.
- The test command is itself subject to Gate 1. A test runner that is absent, crashed, or collected zero tests has not "passed" — that is VOID, not green.
- Record the gate result in the log beside the metric. An iteration logged without a gate result is unverified, and unverified iterations may not be counted as improvements in the final report.
Gate 3 — noise floor: a delta inside the noise is not a result
Before the first iteration, measure the metric at least 5 times with no code change at all and record the spread. Only a delta larger than that spread counts as a change.
Measured here — 10 runs of one identical, completely unmodified workload:
0.0542 0.0609 0.0522 0.0519 0.0553 0.0516 0.0507 0.0535 0.0520 0.0542
n=10 mean=0.0537s stdev=0.0028s (5.2%) spread=0.0102s (19.0%)With zero code change, a mechanical keep/revert would have "KEPT" every run below the mean and "REVERTED" every run above it — manufacturing a 19% swing out of nothing.
| Metric class | Examples | Noise floor | Rule |
| Deterministic counts | lint warnings, type errors, TODO count | 0 | Any non-zero delta is real |
| Deterministic sizes | du -sk dist/ with no content hashing | ~0 | Any delta beyond byte-level jitter is real |
| Wall-clock / sampled | build time, benchmark ms, query latency | must be measured | Keep only if delta > max(2 × stdev, observed spread); otherwise log UNCHANGED and revert |
For wall-clock metrics, re-measure the candidate at least 3 times and compare medians, not single runs. If the baseline and candidate distributions overlap, the verdict is UNCHANGED — not a win. A percentage improvement smaller than the measured spread must never be reported as a result.
Integration with Framework Commands
| Command | Role in the Loop |
/optimize <metric> | Runs the full loop automatically |
/checkpoint | Creates save points to revert to |
/build-fix | Uses the same regression guard pattern |
/tdd | Similar loop structure (RED-GREEN-REFACTOR) |
/quality-gate | Provides metrics (lint, types, tests) |
Metric Selection Guide
Built-In Metrics
These are measure steps, not metrics. Every command below ends in a counter that discards the tool's exit status, so every row returns a perfect score when its tool is missing. Run each one through
metric_run/metric_valuefrom Gate 1; a bare invocation of any row is not a measurement.
| Metric | Command | Direction | Good For |
| Lint warnings | eslint . --format json | Lower is better | Code quality |
| Type errors | tsc --noEmit 2>&1 | grep error | wc -l | Lower is better | Type safety |
| Test coverage | vitest --coverage --reporter=json | Higher is better | Test completeness |
| Bundle size | du -sk dist/ | Lower is better | Frontend performance |
| Build time | time npm run build | Lower is better | Developer experience |
| Complexity | npx eslint . --rule 'complexity: error' | Lower is better | Maintainability |
Custom Metrics
Any command that outputs exactly one number works. metric_value voids anything else, so
reduce to a single scalar inside the parser — never leave several values for it to join:
# Count TODO comments -> one number
grep -r "TODO\|FIXME\|HACK" src/ | wc -l
# Count functions without JSDoc -> two commands = two numbers.
# Subtract them yourself; feed metric_value the difference, not the pair.
fns=$(grep -rP "^(export )?(async )?function" src/ | wc -l)
doc=$(grep -rP "@param|@returns" src/ | wc -l)
echo $((fns - doc))
# Database query time -> a nested plan prints several "Execution Time" rows;
# pick one deliberately rather than letting them all through.
psql -c "EXPLAIN ANALYZE SELECT ..." | grep "Execution Time" | awk 'NR==1{print $3}'Anti-Patterns
Breaking the Toolchain
- Deleting, renaming, downgrading or shadowing the metric tool — the measure command then reports a perfect score
- Narrowing what the tool analyzes (trimming
tsconfig.jsonincludes, adding.eslintignoreentries, pointing the run at a subdirectory) - Letting the test runner collect zero tests and reading that as green
- Rule: Gate 1 every iteration — the tool must be proved present, at the pinned version, run over the same scope as the baseline. A metric that improved because less was measured is a regression wearing a win's clothing.
Trusting Noise
- Keeping a change because one timed run came out 3% faster
- Comparing single runs of a wall-clock metric instead of medians of repeated runs
- Reporting a percentage improvement smaller than the measured run-to-run spread
- Rule: No noise floor, no wall-clock metric (Gate 3). A delta inside the noise is UNCHANGED, so revert it.
Metric Gaming
- Adding
// eslint-disableto "fix" lint warnings - Marking tests as
.skipto "improve" coverage percentages - Moving code to
node_modulesto "reduce" bundle size - Rule: The improvement must be genuine, not cosmetic
Overfitting
- Making changes that improve the metric on this specific codebase but are bad practice generally
- Example: replacing all
anytypes withunknownimproves type error count but may hurt usability - Rule: Each change should make sense in isolation, not just as a metric hack
Complexity Creep
- Adding 50 lines of code to reduce lint warnings by 2
- The cure is worse than the disease
- Rule: The complexity of the change should be proportional to the improvement
Infinite Loops
- Not setting a maximum iteration count
- Continuing when improvements are negligible (diminishing returns)
- Rule: Set a max (default 10), stop on plateau (3 consecutive no-improvement iterations)
Structured Logging
Each iteration should be logged for post-hoc analysis:
## Optimization Log
| # | Timestamp | Change | Gate 1 | Gate 2 | Before | After | Delta | Status |
|---|-----------|--------|--------|--------|--------|-------|-------|--------|
| 1 | 14:30:01 | Remove unused import in auth.ts | ok | pass | 42 | 41 | -1 | KEEP |
| 2 | 14:32:15 | Fix missing return type on getUser | ok | pass | 41 | 40 | -1 | KEEP |
| 3 | 14:34:30 | Add explicit any → unknown in utils | ok | pass | 40 | 40 | 0 | REVERT |
| 4 | 14:36:02 | Narrow tsconfig include globs | ok | pass | 40 | 12 | -28 | REVERT — scope shrank, the errors did not |
| 5 | 14:38:44 | Reorder imports in server.ts | ok | **FAIL** | 40 | 38 | -2 | REVERT — tests outrank the metric |
| 6 | 14:41:10 | Upgrade typescript | **VOID** | — | 40 | — | — | STOP — tool exited 139, no metric |Both gate columns are mandatory. An iteration with a blank Gate 1 or Gate 2 cell was not verified and cannot be counted as an improvement in the final report.
This log enables:
- Understanding which changes had the biggest impact
- Identifying patterns (e.g., "import cleanup" is always effective)
- Avoiding repeating failed approaches in future sessions
- Proving, after the fact, that every kept change passed both gates rather than exploiting one
Prior Art
- karpathy/autoresearch — Autonomous ML experimentation on GPU training. 5-minute time-boxed experiments, single metric (val_bpb), ~100 experiments overnight. The original inspiration for this pattern.
- ARIS — Auto-Research-In-Sleep. Markdown-only, zero dependencies, cross-model review loops.
- Ralph Wiggum Technique — Persistence pattern for autonomous iteration until verification passes.
- Anthropic long-running agents — Initializer agent + coding agent pattern for multi-session chaining.
Example: Reducing Lint Warnings
User: /optimize lint-warnings --iterations 5 --scope src/
Step 0: Validate the toolchain (Gate 1)
→ command -v eslint → /usr/local/bin/eslint [pinned]
→ eslint --version → 9.14.0 [pinned]
→ METRIC_OK_RC="0 1" (eslint exits 1 when it FINDS warnings)
→ Test gate command: `npm test` — exists, currently green, collects 214 tests
Step 1: Baseline + noise floor (Gate 3)
→ 3 unchanged runs all report 47 → deterministic, noise floor = 0
→ Baseline: 47 warnings
Step 2: Iteration 1
→ Analyze: 12 warnings are "no-unused-vars"
→ Fix: Remove 12 unused imports across 8 files
→ Gate 1: eslint 9.14.0 still present, same scope → ok
→ Gate 2: npm test → 214 passed
→ Measure: 35 warnings
→ KEEP (47 → 35, -12; > noise floor 0)
Step 3: Iteration 2
→ Analyze: 8 warnings are "prefer-const"
→ Fix: Change let → const where no reassignment
→ Gate 1: ok → Gate 2: 214 passed
→ Measure: 27 warnings
→ KEEP (35 → 27, -8)
Step 4: Iteration 3
→ Analyze: 5 warnings are "no-explicit-any"
→ Fix: Add types to 5 function parameters
→ Gate 1: ok → Gate 2: npm test → 2 FAILED
→ REVERT (metric said 27 → 22, but tests outrank the metric)
...
Report:
Baseline: 47 → Final: 18 → Improvement: 29 warnings (-62%)
5 iterations, 4 kept, 1 reverted
Gate 1 passed on all 5; Gate 2 failed once (iteration 3, reverted); 0 VOID measurementsDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

