The RSI closed loop (rsiloop)
fak’s recursive-self-improvement loop, closed.
internal/shipgateis the non-forgeable keep-bit andcmd/rsicycleis a one-shot that takes the witnesses as flags.internal/rsiloop+cmd/rsiloopare the true loop: they derive every witness from a real measurement the loop runs itself, fork-isolated offmain, so the loop author cannot forge the numbers that drive a KEEP. This is the runnable assembly of the four-part process the repo already names inEXTENDING.md.
This loop is the top rung of fak’s loops-all-the-way-down picture. See Engineering is building loops for how the RSI loop nests with the inner tool-call, turn, session, and fleet loops, and the orthogonal threads (trust, cost, memory, observability, governance) that cut across them.
The gap this closes
rsicycle is honest about being hand-fed:
# the one-shot: YOU supply before/after/suite-green/truth-clean as flags
go run ./cmd/rsicycle -metric hit_rate -before 0.07 -after 0.16 -suite-green -truth-clean
The keep-bit (shipgate.Evaluate) is non-forgeable in code — only Evaluate
sets the improvedBit. But its inputs are author-supplied flags. A true loop
has to measure them. That is the whole job of rsiloop.
The four modular parts → four seams
| Part of the cycle | Seam (rsiloop.Harness) |
What the real impl does (worktree.go) |
|---|---|---|
| 1. Propose | Candidates() |
yields candidate DefaultCacheSize values |
| 2. Verify-correct | Measure().SuiteGreen |
runs a real go build+go vet in the worktree |
| 3. Measure-faster | Measure().Metric + .TruthClean |
runs cmd/kpiprobe in the worktree; checks the worktree’s git status |
| 4. Keep-or-revert | shipgate.Evaluate + shipgate.Gate |
the keep-bit + the escalation breaker |
Each candidate is applied to a fresh detached git worktree off main, so main
is never touched while a candidate is adjudicated (the same isolation
shipgate.ApplyInWorktree gives the one-shot). A KEEP advances the running
baseline in memory — the next candidate competes against the improved metric (the
recursion). The loop never auto-lands to main; surfacing the kept patch for a
human/gated step is the separate “Land it” stage in EXTENDING.md.
The metric is a legal witness (deterministic)
The demo KPI is an LRU cache hit-rate over a fixed reference trace
(internal/rsiloop/kpi.go). It is wall-clock-free and RNG-free, so it reproduces
bit-for-bit on any platform — the rule for an RSI witness in
docs/proofs/00-METHOD.md. The hit-rate is monotonically
non-decreasing in the cache size and strictly rises over the candidate range, so the
loop has a real gain to find. The measured curve (go run ./cmd/kpiprobe -dump):
size 4 KPI=0.068182 <- DefaultCacheSize on main (the baseline)
size 6 KPI=0.157197
size 8 KPI=0.284091
size 10 KPI=0.467803
size 12 KPI=0.706439
With the default candidates 6,8,8,10 the loop produces KEEP, KEEP, REVERT, KEEP:
each strict gain is kept (advancing the baseline), and the no-op 8 (no gain over the
already-kept 8) is reverted — driven by the measurement, not a flag.
Structured scores for multi-axis controls
The keep-bit still reads exactly three candidate witnesses: scalar Metric,
SuiteGreen, and TruthClean. Some controls need more operator-visible shape than a
single float, though. Context signal/noise is the canonical case: the scalar fitness
must travel with mean S/N ratio, fault pressure, token split, and a grade so a loop can
tell “lean” from “starving” instead of overfitting one number.
rsiloop.Measurement.Score is that additive side-channel. It is copied into the JSONL
row as score, observed by any RunObserved consumer, and deliberately ignored by
shipgate.Evaluate. A scorecard can explain a metric; it cannot make a REVERT into a
KEEP. The shipped harnesses now use it consistently: cmd/attnsnrsi journals
attention_sn fitness with mean_ratio, mean_fault_ratio, and token totals;
rulesynth journals caught/regressed/cluster/self-modify axes; sessionobs journals
loop-index and debt axes; and the LRU worktree demo journals cache-size and trace-shape
axes. cmd/rsiloop prints a compact score summary in its per-cycle trace and includes
the same summary in -dos-observe narration, so external loop ledgers can see the score
without re-gating the decision.
The same rule now applies to the standalone RSI-like controls that do not run through
internal/rsiloop: fak dojo-rsi emits a dojo_calibration scorecard for its
FoldCalibrable replay, sample floor, selector priority, floor-breach pressure, and
witness bits; internal/docfreshrsi carries a doc_freshness_debt score on every
verdict, separating the debt drop from the cleanliness and link-resolution truth
signals. These scorecards are evidence surfaces, not extra authority.
Inside internal/rsiloop, the same scoring shape also covers the meta controls:
loopvariant archive rows carry spec_oracle_points scorecards with pass counts,
point deltas, task counts, and DOS-evidence cleanliness; metarsi apply records carry
keep_rate_truth_clean scorecards with before/after rates, row counts, and the
suite/truth bits that fenced the proposed gate retune.
Lower-level keep-bit helpers now expose score evidence at the point where they prove
it, too. internal/rulesynth.Validate returns a near_misses_caught scorecard on
the direct verdict, so callers outside the generic harness still see caught,
regressed, cluster, support, and self-modify axes. internal/supportmaturity
keeps the compact Promote API, but PromoteWithRecord returns a
support_maturity_promotion scorecard with rung movement, witness-kind binding,
metric delta, and suite/truth evidence for lifecycle promotion gates.
The one-shot cmd/rsicycle adapter has no journal, but its stdout now includes the
same compact score line (score=rsicycle ... metric_delta=...) before the unchanged
DECISION line and exit-code verdict.
S0 as the objective: session outcomes
The same engine now has a session-observability harness for the dev-ex learning loop:
go run ./cmd/rsiloop -mode improve -harness sessionobs
That harness uses the full loop_index score as S0, with the Learn stage derived
from internal/sessionobs.Score and LowerBetter=false. The first candidate is a
no-op sessionobs toolchain proposal and REVERTs because S0 does not move. The second
links value and waste outcomes, marks the scrubbed corpus consumed by the loop, and
KEEPs only after the S0 loop-index rises to 100 with a clean sessionobs report. This
closes the session->outcome->toolchain loop for issue #1161 without adding a
separate keep/revert path; it reuses shipgate.Evaluate.
Run it
# the closed improvement loop: propose, measure, keep-or-revert, recurse
go run ./cmd/rsiloop -mode improve -repo . -baseline-ref main \
-candidates 6,8,8,10 -journal /tmp/rsi.jsonl
# the ongoing benchmark against latest main (append one point; alert on regression)
go run ./cmd/rsiloop -mode track -repo . -baseline-ref main -journal /tmp/rsi.jsonl
Exit codes: 0 = normal (run completed without escalation), 1 = error, 3 = ESCALATE (the breaker
tripped after K consecutive non-keeps — hand to a human) or, in track mode, a
detected regression on main (alert).
Witnessed run (the loop, run for real against main)
go run ./cmd/rsiloop -mode improve -candidates 6,8,8,10 — every suite= /
truth= / cand= field below was DERIVED from a real worktree run, not supplied:
baseline lru_hit_rate@5459aa1c4e65 = 0.068182
cycle 1 DefaultCacheSize=6 base=0.068182 cand=0.157197 improved=true suite=true truth=true -> KEEP (kept=true, breaker=0)
cycle 2 DefaultCacheSize=8 base=0.157197 cand=0.284091 improved=true suite=true truth=true -> KEEP (kept=true, breaker=0)
cycle 3 DefaultCacheSize=8 base=0.284091 cand=0.284091 improved=false suite=true truth=true -> REVERT (kept=false, breaker=1)
cycle 4 DefaultCacheSize=10 base=0.284091 cand=0.467803 improved=true suite=true truth=true -> KEEP (kept=true, breaker=0)
SUMMARY cycles=4 kept=3 final=KEEP final_baseline=0.467803 escalated=false
The baseline was measured at main@5459aa1c — a SHA that landed after the loop’s
own commit, because main advanced under the run. The loop re-derived its baseline
from latest main with no prompting: that is the “benchmark against latest main”
property, observed live. Cycle 3 is the load-bearing case — a candidate with a green
suite AND a clean tree is still REVERTED because the metric did not strictly improve;
no amount of “looks fine” buys a KEEP without a measured gain.
Ongoing benchmark-against-main
-mode track measures the KPI on main and appends one row to the JSONL journal,
tagged with the main SHA it was measured at. Run on a cadence (a cron / /loop),
the journal becomes a time series of main’s KPI — and each run compares to the
last recorded point, exiting 3 on a regression. Because the improve baseline is
also re-derived from main every run, a kept gain is always a gain over latest
main, never a number that drifted from ground truth. (A regression caused by main
getting faster at the arm a number depends on — the F1 tombstone case in
BENCHMARK-AUTHORITY.md — is exactly what the series
surfaces.)
Enforced in CI (not just a cadence). .github/workflows/ci.yml
runs rsiloop -mode track on every push, re-measuring the deterministic main KPI in
a worktree off HEAD and comparing it against a committed baseline floor
(internal/rsiloop/testdata/main-kpi-baseline.jsonl),
failing the build (exit 3) on a strict drop. The track verdict mirrors
dos improve’s REVERT (a non-improving candidate); wired this way it stops being
inert telemetry and becomes a hard gate — a regression on the loop’s own KPI now
blocks the trunk. Because the KPI is wall-clock-free and RNG-free (a single integer
hits/total division), the floor is bit-identical on the runner, so the gate fires
only on a real drop (e.g. DefaultCacheSize lowered), never on platform noise.
Extending it to a real subsystem
The demo wires one tunable. A real optimization (a cache-eviction policy, a quant
kernel, an admission rung) plugs in by supplying its own Harness: a Candidates()
that proposes real changes, and a Measure() that applies each in a worktree and
returns the measured KPI + suite-green + truth-clean. The keep-bit, the breaker, the
journal, and the vs-main discipline are reused unchanged — the loop is the harness,
your subsystem is the payload.