Skip to the content.

The RSI closed loop (rsiloop)

fak’s recursive-self-improvement loop, closed. internal/shipgate is the non-forgeable keep-bit and cmd/rsicycle is a one-shot that takes the witnesses as flags. internal/rsiloop + cmd/rsiloop are the true loop: they derive every witness from a real measurement the loop runs itself, fork-isolated off main, 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 in EXTENDING.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 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.