E4 · Three cycles at no cost
10 minRun the rehearsal.
Done when: tail -n +2 $E/sis/results.tsv | cut -f5 | sort | uniq -c → 1 keep and 2 discard; git -C $E/sis log --oneline | wc -l → 2.
Lesson 2 · 20 min
The cleanest design for a loop that improves on its own is short: the agent changes one thing, a command gives the score, if it improved it stays with a commit, if it didn't it goes back with git reset, and everything becomes one line in a file. In this lesson you run this cycle with fake agents and then with the real Claude Code.
Start the lesson
Goal: run an autoresearch-style experiment: the agent changes, a single metric decides, git commit if it improved and git reset if not.
The autoresearch design, as described in secondary sources. Click each block.
In autoresearch the system is a training script and the metric is validation loss; here, a triage classifier with 40 synthetic cases.
Four ideas, one per cycle. One of them breaks on the first run (the agent is left without an answer). Run it, interrupt it, and resume.
Browser simulation. The real rehearsal is right below.
The kit creates a triage repo (v1) and runs three cycles: an agent that improves, one that makes things worse, and one that does not change the score. No model is called.
K=~/projetos/loop-r-eng-aprende-v72/kit
E=${TMPDIR:-/tmp}/ensaio-aprende/a2
bash $K/novo-sistema.sh $E/sis
for m in sinonimos estraga comenta; do
AGENTE="bash $K/agente-falso.sh $m" bash $K/experimento.sh $E/sis "$m"; echo "saída: $?"
done
echo "## results.tsv"; cut -f1,3- $E/sis/results.tsv
echo "## git log"; git -C $E/sis log --format=%ssistema criado em /tmp/ensaio-aprende/a2/sis (acerto: 0.700 (28/40)) ciclo 1 · keep · 0.700 → 0.800 · melhorou saída: 0 ciclo 2 · discard · 0.800 → 0.700 · não melhorou saída: 1 ciclo 3 · discard · 0.800 → 0.800 · não melhorou saída: 1 ## results.tsv ciclo base novo status descricao 1 0.700 0.800 keep sinonimos 2 0.800 0.700 discard estraga 3 0.800 0.800 discard comenta ## git log keep: sinonimos (0.700 → 0.800) v1: regras de base
Commit hash column omitted (it changes on every run). The exit code of experimento.sh tells you what happened: 0 keep, 1 discard, 2 error.
Treating an agent with no answer as "did not improve". When your subscription hits its limit, the agent returns empty: the cycle must come out as an error (code 2), otherwise the queue turns into a series of false discards and nobody notices that nothing ran. agente-claude.sh exits with 1 when the answer has no rules, and experimento.sh records erro.
One call through your subscription (uses your subscription quota): claude -p with no tools receives the rules and the wrong cases and returns a new regras.txt. The measurement decides, not the model.
K=~/projetos/loop-r-eng-aprende-v72/kit
E=${TMPDIR:-/tmp}/ensaio-aprende/a2 # the rehearsal repo above, already with the synonyms keep
AGENTE="bash $K/agente-claude.sh" bash $K/experimento.sh $E/sis "claude: uma mudança"; echo "saída: $?"
echo "status da última linha: $(tail -1 $E/sis/results.tsv | cut -f5)"#!/usr/bin/env bash
# agente-claude.sh — the REAL agent of the experiment: Claude Code (no tools) proposes a new regras.txt.
# Runs in the repo folder, called by experimento.sh. Uses your subscription quota (one call).
# Empty answer (session limit, error) = ERROR, exit 1: it never becomes "nothing changed".
set -uo pipefail
K=$(cd "$(dirname "$0")" && pwd)
PEDIDO="Você melhora as regras de um classificador de mensagens de suporte. Formato de cada linha: 'categoria: palavra, palavra'; a primeira categoria cuja palavra aparece no texto vence; sem nenhuma, 'outros'.
Regras atuais:
$(cat regras.txt)
Casos que o classificador errou:
$(python3 "$K/medir.py" . --erros | tail -n +2)
Proponha UMA mudança que generalize (palavras que pessoas reais usariam), sem copiar frases inteiras dos casos.
Responda SÓ com o conteúdo completo do novo regras.txt, sem explicação e sem cercas de código."
J=$(mktemp)
echo "$PEDIDO" | claude -p --tools "" --output-format json > "$J" 2>/dev/null
python3 - "$J" <<'PY'
import json, os, sys
try:
d = json.load(open(sys.argv[1], encoding="utf-8"))
except Exception:
print("ERRO: resposta do claude não é JSON"); sys.exit(1)
r = (d.get("result") or "").strip()
linhas = [l for l in r.splitlines() if not l.startswith("```")]
if d.get("is_error") or not any(":" in l for l in linhas):
print("ERRO: sem regras na resposta (" + (r[:80] or "vazia") + ")"); sys.exit(1)
u = d.get("usage") or {}
ent = u.get("input_tokens", 0) + u.get("cache_read_input_tokens", 0) + u.get("cache_creation_input_tokens", 0)
if os.environ.get("TOKENS_ARQ"):
open(os.environ["TOKENS_ARQ"], "w").write(f"{ent} {u.get('output_tokens', 0)}\n")
open("regras.txt", "w", encoding="utf-8").write("\n".join(linhas).strip() + "\n")
print(f"regras.txt reescrito ({len(linhas)} linhas)")
PY
rc=$?; rm -f "$J"; exit $rcciclo 4 · keep · 0.800 → 0.900 · melhorou saída: 0 status da última linha: keep
One run. The model added delivery words (pacote, encomenda, transportadora, frete…) and the visible score went from 0.800 to 0.900. Your run may give keep or discard: the "done when" only checks that the line was recorded. In lesson 6, this same change is rejected by the hidden cases.
The kit's experimento.sh measures the triage classifier. In your repo, two variables swap the measurer; results.tsv is created on its own and stays out of git. Cost-free rehearsal in a practice repo.
K=~/projetos/loop-r-eng-aprende-v72/kit
E=${TMPDIR:-/tmp}/ensaio-aprende/a2-adaptar; rm -rf $E; mkdir -p $E/repo; cd $E/repo
# um repo "seu": sem results.tsv, sem .gitignore, com o próprio medidor
printf 'def dobro(x):\n return x + 2\n' > calc.py
printf '1 2\n2 4\n5 10\n0 0\n' > casos.txt # entrada e esperado
cat > medir.sh <<'FIM'
# imprime "<acerto> <ok> <total>" (o experimento.sh lê o 1º número)
python3 -c "
import calc
c = [l.split() for l in open('casos.txt') if l.strip()]
ok = sum(calc.dobro(int(a)) == int(b) for a, b in c)
print(f'{ok / len(c):.3f} {ok} {len(c)}')"
FIM
git init -q -b main; git add -A; git -c user.name=treino -c user.email=treino@exemplo.invalid commit -qm "v1"
export MEDIR="bash medir.sh" MEDIDOR="casos.txt medir.sh" # as duas variáveis da adaptação
bash $K/experimento.sh --congelar . $E/medidor.sha256; export GUARDA=$E/medidor.sha256
AGENTE="echo '# revisado' >> calc.py" bash $K/experimento.sh . "comenta"; echo "saída: $?"
AGENTE="printf 'def dobro(x):\n return x * 2\n' > calc.py" bash $K/experimento.sh . "corrige"; echo "saída: $?"
AGENTE="echo '3 7' >> casos.txt" bash $K/experimento.sh . "mexe nos casos"; echo "saída: $?"
echo "## results.tsv (criado pelo experimento.sh)"; cut -f1,3- results.tsv
echo "## git status (vazio = limpo)"; git status --shortmedidor congelado em /tmp/ensaio-aprende/a2-adaptar/medidor.sha256 ciclo 1 · discard · 0.250 → 0.250 · não melhorou saída: 1 ciclo 2 · keep · 0.250 → 1.000 · melhorou saída: 0 ciclo 3 · discard · 1.000 → 1.000 · DESCARTADO: mexeu no medidor (casos.txt medir.sh) saída: 1 ## results.tsv (criado pelo experimento.sh) ciclo base novo status descricao 1 0.250 0.250 discard comenta 2 0.250 1.000 keep corrige 3 1.000 1.000 discard mexe nos casos ## git status (vazio = limpo)
Coluna do hash do commit omitida. O ciclo 3 mostra a guarda da aula 6 valendo também para o medidor seu.
1. The measure. MEDIR="[your command]" runs in the repo folder and prints the number first, on one line. It receives --valor and, if you use ESCONDIDOS (lesson 6), also --casos [file]: your script must measure that file instead of the usual cases.
2. What to freeze. MEDIDOR="[files]" lists what the measure reads (cases and the script itself). That is what --congelar writes to the manifest.
3. The record. If it is missing, experimento.sh creates results.tsv with a header and puts it in .git/info/exclude, without touching your .gitignore. Trace and manifest stay outside the repo; a new, non-ignored file inside it makes the next cycle exit with error 2 (dirty repo).
Rehearsal, real run and your own system.
Run the rehearsal.
Done when: tail -n +2 $E/sis/results.tsv | cut -f5 | sort | uniq -c → 1 keep and 2 discard; git -C $E/sis log --oneline | wc -l → 2.
Run the real block right after the rehearsal.
Done when: wc -l < $E/sis/results.tsv → 5 (header + 4 cycles) and the last status column is keep, discard or erro, never empty.
In your system, write the command that prints the metric of your hypothesis (lesson 1) as a number at the start of the line, in the format of medir.py --valor. Plug it into experimento.sh through the MEDIR and MEDIDOR variables, as in the "Adapt it to your repo" block above.
Done when: your measuring command, run twice in a row with no change, prints the same number; and AGENTE="echo '# revisado' >> [a file in the repo]" bash $K/experimento.sh . "comenta"; echo "saída: $?", with MEDIR and MEDIDOR exported, ends in discard and saída: 1.
Question 1 of 4
The change gave exactly the same score as the base. What does the cycle do?skill
Question 2 of 4
Why does the discard also go into results.tsv?
Question 3 of 4
The agent gave no answer (session limit). How should the cycle end?skill
Question 4 of 4
About the autoresearch numbers (experiments per hour, training gain), what does this course say?
Choose an answer. The explanation appears after you try.