Documentation
¶
Overview ¶
Package dojocal is the dojo-RSI loop's PURE proposer + self-scoring rung — the genuinely-safe autonomous slice that MUTATES NOTHING (Phase 1 of docs/fak/dojo-rsi-loop.md, issue #1023). It is the twin of internal/guardrsi pointed at the dojo's calibration journal instead of the guard's verdict journal: where guardrsi folds a verdict journal and proposes the worst-bucket honesty hole, dojocal folds a dojo report's scored episodes (reusing dojo.BoardFromEpisodes) and proposes the worst-calibrated MEASURED, NON-DENY-LISTED, NON-FLOOR (lever, metric) cell to recalibrate.
What makes the loop safe to run unattended is what it does NOT do: it never opens a worktree, never rewrites a claim literal, never touches a file. It PROPOSES a recalibration (swap the claimed number to the corpus mean) and SELF-SCORES it by replaying dojo.FoldCalibrable over the same episodes with the candidate claim swapped in — a pure, deterministic, CI-able re-fold. The KEEP bit is non-forgeable and mirrors guardrsi.RunIteration exactly: KEEP iff measured-rows>0 AND the folded calibrable metric STRICTLY drops AND a per-lever sample threshold is met AND an external `--witness {"ok":true}` is supplied.
The structural honesty guarantee comes from dojo.FoldCalibrable, not from a check here: an INTENTIONAL FLOOR (false_warm_rate must stay 0.0) folds by its breach, not its calib_err, so "recalibrating" a floor up to its empirical rate RAISES the fold and is reverted. The candidate picker refuses to target a floor at all (it is routed, never recalibrated). A small NeverRecalibrate deny-list repeats that safety rule independently of the episode's IntentionalFloor bit, and dojo.FoldCalibrable refuses to fold an UNMEASURED episode — so the ways the loop could optimise itself into dishonesty are closed by construction, the same way guardrsi can only ever repair an honesty hole and never invent a row.
Index ¶
- Constants
- func AppendJournalLine(row JournalRow) (string, error)
- func CheckIteration(it Iteration) []string
- func ClaimChangeLine(src []byte, lever, metric string, newClaimed float64) (lineNo int, before, after string, err error)
- func EffectiveMinSample(lever, metric string, requested int) int
- func MarshalTrendText(tr JournalTrend) string
- func MinSampleOverride(lever, metric string) (int, bool)
- func NeverRecalibrateReason(lever, metric string) (string, bool)
- func NewWorktreeHarness(cfg WorktreeConfig) rsiloop.Harness
- func ProjectionPaths(lever, metric string) []string
- func ReadClaim(src []byte, lever, metric string) (float64, error)
- func RewriteClaim(src []byte, lever, metric string, newClaimed float64) ([]byte, float64, error)
- func Round3(v float64) float64
- func TreeChangedWithin(changed, declared []string) bool
- type Iteration
- type JournalRow
- type JournalTrend
- type ProposePayload
- type Recal
- type RecalKind
- type ScoredCell
- type SelectOptions
- type Wakeup
- type WorktreeCandidate
- type WorktreeConfig
Constants ¶
const ( // ProposeSchema tags the proposal envelope. ProposeSchema = "fak-dojo-rsi.propose/1" // IterationSchema tags one replayed self-scoring iteration. IterationSchema = "fak-dojo-rsi/1" // DefaultMinSample is the per-lever sample floor a RECALIBRATE candidate must // clear before its proposed claim is trusted enough to KEEP: a recalibration // fitted to one or two boundaries is noise, not a corpus central tendency. The // guard's worst-bucket loop has no analogue (a journal row is its own witness); // the dojo's claim is a population estimate, so it owes a sample floor. DefaultMinSample = 3 // SparseLeverMinSample is the stricter floor for sparse, high-variance dojo // levers whose corpus has fewer natural boundaries than resume-posture. SparseLeverMinSample = 6 )
const ( // JournalSchema tags committed dojo-RSI journal rows. JournalSchema = "fak-dojo-rsi-journal/1" // TrendSchema tags a folded KEEP/REVERT trend over the journal. TrendSchema = "fak-dojo-rsi-trend/1" // DefaultJournalRel is the committed dojo-RSI ledger the loop and CI feed read. DefaultJournalRel = "docs/dojo/rsi-journal.jsonl" // DefaultCellRecheckDays is the staleness horizon used by the Phase-3 selector. DefaultCellRecheckDays = 7 )
const ClaimsRelPath = "internal/dojo/claims.go"
ClaimsRelPath is the module-relative path of the dojo claim registry — the one file the RSI loop's RECALIBRATE arm rewrites. Named here so the worktree harness and the rewrite verifier agree on one literal, exactly as rsiloop pins tunableRelPath. Forward slashes; the command layer joins it onto the module dir.
Variables ¶
This section is empty.
Functions ¶
func AppendJournalLine ¶
func AppendJournalLine(row JournalRow) (string, error)
AppendJournalLine renders a journal row as one JSONL line.
func CheckIteration ¶
CheckIteration re-derives the KEEP invariants from the iteration's own fields — the dojo twin of guardrsi.CheckIteration. A kept iteration that cannot defend its keep bit (no rows, no strict drop, too few samples, no witness, or a routed candidate) returns the violations, so a fabricated KEEP is caught at the gate.
func ClaimChangeLine ¶
func ClaimChangeLine(src []byte, lever, metric string, newClaimed float64) (lineNo int, before, after string, err error)
ClaimChangeLine reports the single source line that RewriteClaim would change for (lever, metric): its 1-based line number and the before/after text, for a legible preview diff. It re-runs RewriteClaim internally so the preview is exactly what an apply would write (never a hand-rendered approximation). A rewrite that changes no line (impossible once RewriteClaim succeeds) returns ok=false.
func EffectiveMinSample ¶ added in v0.37.0
EffectiveMinSample returns the sample floor for a candidate cell. The caller's requested floor is a global lower bound; per-cell policy can only raise it.
func MarshalTrendText ¶
func MarshalTrendText(tr JournalTrend) string
MarshalTrendText renders the trend as a compact text card for Slack/CI.
func MinSampleOverride ¶ added in v0.37.0
MinSampleOverride reports a stricter per-(lever,metric) sample floor. Absence means the caller's requested floor (or DefaultMinSample) is sufficient.
func NeverRecalibrateReason ¶ added in v0.37.0
NeverRecalibrateReason reports metrics that must never be converted into a RECALIBRATE claim swap, even if a malformed report omits IntentionalFloor.
func NewWorktreeHarness ¶ added in v0.38.0
func NewWorktreeHarness(cfg WorktreeConfig) rsiloop.Harness
NewWorktreeHarness wires a rsiloop.Harness to the real dojo worktree/probe/ suite/truth impls. It is the dojo twin of rsiloop.NewWorktreeHarness: the baseline measures FoldCalibrable over a two-shard corpus in a pinned-main worktree, the candidate rewrites one claim literal and re-measures, and the keep-bit reads a real go-suite plus the two-shard gate plus treeChangedOnly.
func ProjectionPaths ¶
ProjectionPaths returns the conservative declared path set for an agent REPROJECT candidate. The command layer verifies actual changed paths stay within this set before accepting any re-measurement.
func ReadClaim ¶
ReadClaim returns the literal claim value currently registered for (lever, metric) in `src`, without rewriting anything. The worktree harness uses it to read the pinned-baseline literal a candidate forks from; the preview command uses it to show the before value. Same fail-closed anchoring as RewriteClaim (not-found / ambiguous are errors, never a silent zero).
func RewriteClaim ¶
RewriteClaim re-points the single registered claim literal for (lever, metric) in the claims.go source `src` to newClaimed, returning the rewritten bytes and the OLD value it replaced. It is the pure rewrite the worktree arm applies inside a throwaway worktree before re-measuring FoldCalibrable.
It fails closed on anything that would make the rewrite ambiguous or empty:
- the cell's `claim(`/`floor(` anchor is not found (an unregistered or moved cell — the rewrite contract is broken, never a silent no-op),
- the anchor matches more than once (the registry's keys are unique; a double match means the source is not the registry this targets),
- the new value formats identically to the old (a no-op is not a real recalibration — the same "changed nothing fails closed" rule treeChangedOnly enforces on the worktree side).
Only the float is rewritten; the basis string (which may quote the same number), a floor's trailing bool, and every other cell are byte-identical in the output.
func Round3 ¶
Round3 is re-exported for callers that fold dojocal numbers without importing mathx directly. It rounds to three decimals, matching the dojo's reporting.
func TreeChangedWithin ¶
TreeChangedWithin is the REPROJECT path gate: every changed path must be within the candidate's declared path set. Entries ending in "/" or "/**" are treated as prefixes; other entries are exact files.
Types ¶
type Iteration ¶
type Iteration struct {
Schema string `json:"schema"`
Goal string `json:"goal"`
Candidate Recal `json:"candidate"`
BaselineFold dojo.FoldResult `json:"baseline_fold"`
ReplayedFold dojo.FoldResult `json:"replayed_fold"`
BaselineValue float64 `json:"baseline_value"`
ReplayedValue float64 `json:"replayed_value"`
MeasuredDelta float64 `json:"measured_delta"` // baseline - replayed; positive = the fold dropped
MinSample int `json:"min_sample"`
Witness map[string]any `json:"witness,omitempty"`
Kept bool `json:"kept"`
Reason string `json:"reason"`
KeepRevertRule string `json:"keep_revert_rule"`
}
Iteration is one replayed self-scoring tick — the dojo twin of guardrsi.Iteration. It carries the candidate, the calibrable fold BEFORE and AFTER the proposed claim swap (both DERIVED by re-folding, never asserted), the strict measured delta, and the non-forgeable KEEP bit + reason.
func RunIteration ¶
RunIteration replays dojo.FoldCalibrable over the report's episodes with the candidate's claim SWAPPED IN, and decides KEEP/REVERT — the pure, deterministic, CI-able dojo twin of guardrsi.RunIteration, taking `--witness {"ok":true}` exactly as guard-verdict-rsi does.
The replay re-scores every episode of the candidate's (lever, metric) cell against the proposed NewClaimed (using the same dojo.Score + dojo.DefaultCalibBand the live builders use), leaving every other episode untouched, then re-folds. Nothing on disk changes; the swap is a value substitution in a copy of the episodes. The KEEP bit mirrors guardrsi exactly:
KEEP iff measured-rows>0 AND the folded calibrable Value STRICTLY drops
AND the candidate's per-lever sample >= minSample AND a green witness.
A floor target can never KEEP: FoldCalibrable folds a floor by its breach, so closing the gap RAISES Value (negative delta) and REVERTs — the structural guarantee, surfaced here as a normal no-strict-gain revert. An UNMEASURED-routed or floor-routed candidate is refused before any replay (it carries no swap). minSample<=0 falls back to DefaultMinSample; per-cell policy can only raise it.
type JournalRow ¶
type JournalRow struct {
Schema string `json:"schema"`
Tick int `json:"tick"`
Date string `json:"date"`
GeneratedAt string `json:"generated_at"`
Commit string `json:"commit,omitempty"`
Lever string `json:"lever"`
Metric string `json:"metric,omitempty"`
Kind RecalKind `json:"kind"`
Decision string `json:"decision"`
Kept bool `json:"kept"`
AgentArm bool `json:"agent_arm,omitempty"`
Baseline float64 `json:"baseline_value"`
Replayed float64 `json:"replayed_value"`
MeasuredDelta float64 `json:"measured_delta"`
BreakerCount int `json:"breaker_nonkeeps"`
WakeupAt string `json:"wakeup_at,omitempty"`
Reason string `json:"reason,omitempty"`
}
JournalRow is one durable dojo-RSI tick. It stores the measured keep/revert facts and enough routing context for the CI feed to trend mechanical KEEPs, agent-route REVERTs, and floor ESCALATEs without re-running a corpus.
func NewJournalRow ¶
func NewJournalRow(tick int, it Iteration, decision string, breaker int, now time.Time, commit string, wake Wakeup) JournalRow
NewJournalRow projects one iteration into the durable row schema.
func ParseJournal ¶
func ParseJournal(content string) []JournalRow
ParseJournal parses committed dojo-RSI JSONL, skipping torn or foreign rows.
type JournalTrend ¶
type JournalTrend struct {
Schema string `json:"schema"`
GeneratedAt string `json:"generated_at"`
Rows int `json:"rows"`
Keep int `json:"keep"`
Revert int `json:"revert"`
Escalate int `json:"escalate"`
MechanicalKeep int `json:"mechanical_keep"`
AgentRoutes int `json:"agent_routes"`
ReprojectRoutes int `json:"reproject_routes"`
HarvestRoutes int `json:"harvest_routes"`
FloorEscalates int `json:"floor_escalates"`
Latest *JournalRow `json:"latest,omitempty"`
Summary string `json:"summary"`
}
JournalTrend folds the committed journal into the CI feed payload.
func FoldTrend ¶
func FoldTrend(rows []JournalRow, now time.Time) JournalTrend
FoldTrend summarizes the KEEP/REVERT journal for the CI feed.
type ProposePayload ¶
type ProposePayload struct {
Schema string `json:"schema"`
Baseline dojo.FoldResult `json:"baseline"`
Board dojo.Board `json:"board"`
Candidates []Recal `json:"candidates"`
Worst Recal `json:"worst"`
}
ProposePayload is the proposal envelope: the worst-first candidates, the folded calibrable baseline they were drawn from, and the single worst candidate the loop would act on first (mirroring guardrsi's WorstBucket-led FoldPayload).
func ProposeRecals ¶
func ProposeRecals(r dojo.Report) ProposePayload
ProposeRecals folds a dojo report's scored episodes into the worst-first list of recalibration candidates — the twin of guardrsi.WorstBucket, reusing dojo.BoardFromEpisodes for the cross-lever board the candidates ride alongside.
It is pure and total. For each MEASURED, NON-FLOOR (lever, metric) cell it proposes a RECALIBRATE re-pointing the claim at the cell's measured mean; an explicit NeverRecalibrate entry or INTENTIONAL FLOOR cell is emitted as a ROUTE_FLOOR (never a claim swap — a floor breach is a bug to escalate, the design's structural anti-gaming rule); a lever whose episodes were ALL UNMEASURED contributes a ROUTE_UNMEASURED so the floor of the honesty constraint is visible rather than silently dropped. Candidates are sorted worst-first by calib_err (a measured recalibration always ahead of a routed one), then lever, then metric, for determinism.
type Recal ¶
type Recal struct {
Lever string `json:"lever"`
Metric string `json:"metric"`
Kind RecalKind `json:"kind"`
OldClaimed float64 `json:"old_claimed"`
NewClaimed float64 `json:"new_claimed"`
// MeasuredMean is the mean realized value over the cell's measured episodes —
// the corpus central tendency a RECALIBRATE re-points the claim at.
MeasuredMean float64 `json:"measured_mean"`
// Sample is how many measured episodes stand behind MeasuredMean.
Sample int `json:"sample"`
// MinSample is the effective sample floor this cell must clear to KEEP.
MinSample int `json:"min_sample"`
// Verdict is the worst measured episode's verdict for this cell (OVER_CLAIM /
// UNDER_CLAIM / CALIBRATED), carried for the operator's context.
Verdict string `json:"verdict"`
// CalibErr is the worst measured calib_err for the cell — the size of the gap
// the recalibration would close.
CalibErr float64 `json:"calib_err"`
// IntentionalFloor is mirrored from the episode so a reader can see at a glance
// why a ROUTE_FLOOR candidate is routed rather than recalibrated.
IntentionalFloor bool `json:"intentional_floor"`
// NeverRecalibrate marks a safety-critical deny-list entry whose claim may not
// be swapped even if the input report accidentally drops IntentionalFloor.
NeverRecalibrate bool `json:"never_recalibrate,omitempty"`
// Reason is the one-line rationale (why this cell, why this kind).
Reason string `json:"reason"`
// DeclaredPaths is the path allow-list the agent arm must stay within for a
// REPROJECT candidate. The command layer verifies changed paths against this
// list before any re-measured FoldCalibrable gain can be trusted.
DeclaredPaths []string `json:"declared_paths,omitempty"`
}
Recal is one proposed recalibration targeting exactly one (lever, metric) cell of the worst-first board. It mirrors the design doc's Recal: the loop writes the proposed NewClaimed (the corpus mean) but the KEEP decision is derived by re-folding, never asserted here.
type RecalKind ¶
type RecalKind string
RecalKind names what a proposal asks for, mirroring the design doc's split (docs/fak/dojo-rsi-loop.md "The candidate"). Only RECALIBRATE is self-KEEPable here; the rest are ROUTED to a human/agent arm and never KEPT by this pure loop.
const ( // RecalibrateKind re-points a genuine ESTIMATE claim at its corpus central // tendency — the mechanical, self-keepable win. RecalibrateKind RecalKind = "RECALIBRATE" // ReprojectKind routes a projection-code fix: the claim stays pinned, and an // agent proposes a patch that moves realized behavior toward it. It is never // self-KEPT by the pure recalibration loop. ReprojectKind RecalKind = "REPROJECT" // HarvestKind routes a real under-claimed saving to the issue/harvest arm. A // human decides whether to build it; the pure loop never auto-lands it. HarvestKind RecalKind = "HARVEST" // RouteFloor marks a candidate whose target cell is an INTENTIONAL FLOOR: a // guard the dojo defends, never a recalibration. A breach is a belief-code bug // to escalate, so the loop routes it and never proposes a claim swap. RouteFloor RecalKind = "ROUTE_FLOOR" // RouteUnmeasured marks a candidate the loop cannot score: the worst lever had // no measured episode (UNMEASURED is uncandidatable by construction — the floor // the honesty constraint routes to). RouteUnmeasured RecalKind = "ROUTE_UNMEASURED" )
type ScoredCell ¶
type ScoredCell struct {
Candidate Recal `json:"candidate"`
Score float64 `json:"score"`
LastTouched string `json:"last_touched,omitempty"`
NextEligible string `json:"next_eligible,omitempty"`
AgeDays float64 `json:"age_days"`
Novelty float64 `json:"novelty"`
ValueWeight float64 `json:"value_weight"`
Staleness float64 `json:"staleness"`
Saturated bool `json:"saturated,omitempty"`
Reason string `json:"reason"`
}
ScoredCell is one candidate scored by novelty x value x staleness.
func NextCandidate ¶
func NextCandidate(ranked []ScoredCell) (ScoredCell, bool)
NextCandidate returns the highest-ranked non-saturated cell.
func RankCandidates ¶
func RankCandidates(candidates []Recal, rows []JournalRow, opts SelectOptions) []ScoredCell
RankCandidates reuses nightrun's selector shape for dojo cells: novelty (never touched), value (calibration gap), and staleness (last touch age) are blended into one deterministic priority score. A fresh touched cell is marked Saturated so a loop can stop instead of thrashing the same row.
type SelectOptions ¶
SelectOptions parameterizes candidate ranking.
type Wakeup ¶
type Wakeup struct {
At string `json:"at"`
DelayS int64 `json:"delay_seconds"`
Reason string `json:"reason"`
Pending bool `json:"pending"`
}
Wakeup is the loop's next self-pacing decision. Command layers can translate it to an MCP ScheduleWakeup call; pure code only computes the time.
func ScheduleWakeup ¶
func ScheduleWakeup(ranked []ScoredCell, now time.Time) Wakeup
ScheduleWakeup returns now when there is runnable work, or the earliest next-eligible time when every candidate is saturated.
type WorktreeCandidate ¶ added in v0.38.0
WorktreeCandidate is the one recalibration the worktree arm measures: the (lever, metric) cell to re-point and the corpus-mean claim to swap in. It is the Payload a rsiloop.Candidate carries; the command layer picks the worst RECALIBRATE from `fak dojo-rsi propose` and builds this from it.
type WorktreeConfig ¶ added in v0.38.0
type WorktreeConfig struct {
// Repo is a path inside the working copy (the module root is fine; git finds
// the repo from there).
Repo string
// BaselineRef is the ref the baseline + the candidate fork from ("main"). The
// harness resolves it to a SHA ONCE and pins it, so before/after are measured
// on the identical tree even if main advances mid-run.
BaselineRef string
// Corpus is the directory of .jsonl transcripts `fak dojo run --corpus` scores
// against. It is split into two disjoint shards for the two-shard gate.
Corpus string
// Candidate is the single recalibration to measure.
Candidate WorktreeCandidate
// SuiteCmds is the suite-green gate; ALL must exit 0. Default: build + vet.
SuiteCmds [][]string
// SuitePkgs is the package pattern for the default suite gate ("./...").
SuitePkgs string
// DojoArgs are extra args appended to `fak dojo run` (e.g. --ttl 1h, --lever).
DojoArgs []string
// DojoRun, when non-nil, overrides the real `fak dojo run --json` invocation.
// A test injects a fake that returns a fixed report for a corpus dir; the
// production harness leaves it nil so the real exec runs.
DojoRun func(moduleDir, corpusDir string) (dojo.Report, error)
// ScratchDir is the parent for ephemeral worktrees + shard dirs ("" => os.TempDir).
ScratchDir string
}
WorktreeConfig parameterizes the real dojo worktree harness.