Prior Work Deep-Dive · grounded in source code

SkillOpt — an optimizer that trains a skill document like a neural network

Paper: “SkillOpt: Executive Strategy for Self-Evolving Agent Skills” (arXiv:2605.23904, microsoft/SkillOpt). Every signature and quote on this page is taken verbatim from the cloned implementation at /home/v-seungplee/SkillOpt (skillopt/engine/trainer.py, skillopt/evaluation/gate.py, skillopt/optimizer/*, skillopt/prompts/*, skillopt_sleep/*). The function cards and pipeline stages are collapsed toggles — click a title to unfold the original code and input/output examples.

01

The system in one line

A system that freezes the model weights and optimizes one skill document (.md) as the “trainable state,” under deep-learning training discipline: epochs · minibatches · a learning rate · a validation gate. A separate optimizer LLM turns scored rollouts into bounded add / delete / replace edits, and a candidate edit is accepted only when it strictly raises the held-out validation score. README self-description (verbatim): “SkillOpt treats the skill document as the trainable state of a frozen agent … A textual learning-rate budget, a rejected-edit buffer, and an epoch-wise slow / meta update make skill training stable while adding zero inference-time model calls at deployment.”

The deployment artifact is a compressed best_skill.md (typically 300–2,000 tokens) that attaches as-is to the unchanged target model — zero extra model calls at inference time. The cloned repo carries two copies of the loop: the paper’s main skillopt package with ReflACTTrainer (the benchmark training loop — internally named ReflACT), and skillopt_sleep, the same machinery moved to nightly offline self-evolution (harvest → mine → replay → consolidate(gate) → stage → adopt).

The paper’s four phases (Rollout → Reflect → Edit → Gate) are implemented as six stages in code: Edit unfolds into Aggregate + Select + Update, and Gate into the Evaluate stage. The flowchart below folds the six code stages back onto the four paper phases.

02

Pipeline flowchart — click a stage to unfold the detail

skillopt/engine/trainer.py — module docstring, verbatim

"""ReflACT Trainer — the main training loop.
Orchestrates the 6-stage ReflACT pipeline:
  1. Rollout   — execute episodes with current skill
  2. Reflect   — analyze trajectories, generate patches
  3. Aggregate — hierarchical merge of patches
  4. Select    — rank and select top edits
  5. Update    — apply edits to skill document
  6. Evaluate  — validate candidate skill, accept/reject
"""
① ROLLOUT — run the training batch with the current skill (the paper’s Rollout) Each step runs accumulation batches: adapter.rollout(train_env, current_skill, rollout_dir, use_eval_feedback=True). Each result dict carries a hard (exact-match 0/1) and soft (F1/partial) score, a fail_reason, and the full trajectory. Batches shuffle each epoch from a fixed seed pool (base_seeds — the data-loader counterpart of neural training).
↓ rollout_results (hard/soft scored)
② REFLECT — failure/success-split minibatch analysis (the paper’s Reflect) run_minibatch_reflect groups failure and success trajectories into separate minibatches (default 8) and calls the analyst prompts (analyst_error.md / analyst_success.md) in parallel. Each minibatch returns a patch (a list of proposed edits), and _normalise_patches tags each with source_type: failure|success and a support_count (how many trajectories back it). Earlier steps’ failure patterns + rejected-edit summaries (step_buffer_context) are injected alongside.
↓ failure_patches + success_patches
③④⑤ AGGREGATE → SELECT → UPDATE (the paper’s Edit) Aggregate: merge_patches fuses the patches with a failure-first hierarchical merge. Select: fix the edit budget (= the learning rate) — with lr_control_mode=fixed a scheduler decides (edit_budget max, min_edit_budget min, constant/cosine, …); with autonomous the optimizer LLM picks the count itself (the textual learning rate). Then rank_and_select keeps only the top L by importance. Update: build the candidate skill document with apply_patch_with_report (patch mode) or rewrite_skill_from_suggestions (rewrite mode).
↓ candidate_skill (the candidate skill document)
⑥ EVALUATE — the held-out validation gate (the paper’s Gate) The candidate is rolled out and scored on the whole selection set (the valid_seen split; a hash cache sel_cache prevents duplicate evaluation), and the pure decision function evaluate_gate rules accept_new_best | accept | reject. Acceptance is a strict inequality: anything short of cand_score > current_score is REJECT and the skill rolls back to its previous state. On rejection, every attempted edit and the score drop are loaded into the step buffer as “negative feedback” (the rejected-edit buffer).
↻ next step: the step buffer is re-injected into the Reflect/Select prompts — blocking retries of the same ineffective edits
epoch end — SLOW UPDATE + META SKILL (the slow / meta updates) Slow update: the same 20 training tasks are re-rolled with both the previous-epoch and current skills to build longitudinal comparison pairs in four categories — improved / regressed / persistent_fail / stable_success — and a strategy-advisor prompt (slow_update.md) writes epoch-level guidance into the skill document’s protected region (between the SLOW_UPDATE markers). The prompt has a built-in step of reflecting on whether the guidance it wrote last epoch actually helped. Meta skill: run_meta_skill distills optimizer-side meta-knowledge from the same pairs and injects it as context into every Reflect/Select call of the next epoch (the optimizer learns, not the target skill).
↓ at every step/epoch end
exporting best_skill.md — the deployment artifact The validation-best skill is overwritten to out_root/best_skill.md every step, and runtime_state.json records current/best scores · origin · paths to support resume. Force-accept-mode slow-update guidance is injected only into current_skill; best_skill is protected as “a faithful snapshot of the val-best step” (source comment, verbatim: “best_skill must remain a faithful snapshot of the val-best step”).

The nightly edition of the same machine — skillopt_sleep

run_sleep_cycle() docstring, verbatim: “harvest -> mine -> replay -> consolidate(gate) -> stage (-> optional adopt)”. Instead of benchmarks, it mines recurring tasks from the user’s past Claude Code / Codex session transcripts as training data, and stages only what passes the gate.

harvest → mine — mining reproducible tasks from past sessions harvest_for_config normalizes transcripts into SessionDigests (only since the last harvest; first run uses lookback_hours=72), and mine extracts scoreable TaskRecords (intent + judging criterion, train/val split at holdout_fraction=0.34). An LLM miner on a real backend, heuristics otherwise.
↓ TaskRecord list (train/val split tags)
replay → consolidate(gate) — one night = one SkillOpt epoch consolidate() docstring, verbatim: “Run one consolidation epoch: reflect -> bounded edit -> gate.” Train tasks drive reflect; val tasks drive the gate. Optional dream rollouts (rolling the same task K times for good-vs-bad contrast) and associative recall (summoning K similar past tasks from the archive) augment only the train side — val is never contaminated.
↓ ConsolidationResult (accepted, applied/rejected edits, holdout scores)
stage → adopt — live files are never touched directly write_staging writes proposed_SKILL.md / proposed_CLAUDE.md / report.md / diagnostics.json into .skillopt-sleep/staging/<ts>/ (secrets redacted), and only a separate, explicit adopt step copies over the live files — after taking a backup. Edits happen only inside the <!-- SKILLOPT-SLEEP:LEARNED START/END --> protected region, never trespassing on the user’s hand-written content.
03

Key function · module cards — signatures verbatim from source (click to unfold)

run_minibatch_reflect(…) → list[dict | None]

reflect

skillopt/gradient/reflect.py — signature, verbatim

def run_minibatch_reflect(
    results: list[dict], skill_content: str,
    prediction_dir: str, patches_dir: str,
    workers: int, failure_only: bool,
    minibatch_size: int = 8, edit_budget: int = 4,
    random_seed: int | None = None, *,
    error_system: str | None = None, success_system: str | None = None,
    rejection_context: str = "", trajectory_memory_context: str = "",
    step_buffer_context: str = "", meta_skill_context: str = "",
    update_mode: str = "patch",
    skill_aware_reflection: bool | None = None,
    skill_aware_appendix_source: str | None = None,
) -> list[dict | None]:
    """Full minibatch reflect stage: group → parallel optimizer calls → patches.
    Separates failure and success trajectories, splits each into minibatches
    of size M, runs all minibatches in parallel, and saves patch files."""

Minibatch-analyzes trajectories split into failures and successes, each under a different analyst prompt — failures yield what to fix, successes what to preserve/generalize. Each returned element is a RawPatch.

input → output example (the shape of one RawPatch)

run_minibatch_reflect(rollout_results, current_skill, pred_dir, patches_dir, workers=16, failure_only=False)
# -> [ {"patch": {"edits": [
#        {"op": "add", "content": "When the sheet has merged cells, unmerge before ...",
#         "source_type": "failure", "support_count": 3}, ...]},
#      "source_type": "failure", "batch_size": 8, "failure_summary": [...]}, ... ]

merge_patches(skill_content, failure_patches, success_patches, …) → dict

aggregate

skillopt/gradient/aggregate.py — signature + docstring, verbatim

def merge_patches(
    skill_content: str,
    failure_patches: list[dict], success_patches: list[dict],
    batch_size: int = 8, verbose: bool = True, workers: int = 16,
    update_mode: str = "patch", meta_skill_context: str = "",
) -> dict:
    """Failure-first hierarchical merge with support count tracking.
    1. Merge failure patches independently (parallel)
    2. Merge success patches independently (parallel)
    3. Final merge: combine both groups with failure priority"""

The counterpart of gradient accumulation. Hierarchically merges edit proposals from multiple minibatches, giving failure-derived edits priority and preserving how many trajectories back each edit (support_count) — the ranking evidence for the Select stage.

decide_autonomous_learning_rate(…) → dict — textual learning rate

select / lr

skillopt/optimizer/lr_autonomous.py + skillopt/prompts/lr_autonomous.md

def decide_autonomous_learning_rate(*,
    skill_content: str, merged_patch: dict, update_mode: str,
    rollout_hard: float, rollout_soft: float, rollout_n: int,
    step_buffer_context: str = "", meta_skill_context: str = "",
) -> dict:
    """Ask the optimizer to choose the number of update items for this step.
    The prompt intentionally avoids default budgets, candidate budget lists, or
    scheduler history. The only hard post-processing is validity: the returned
    integer is clamped to the available item count."""

The “learning rate” is not a real-valued step size but the number of edits applied this step. In fixed mode a scheduler decides: build_scheduler(mode=cfg["lr_scheduler"], max_lr=cfg["edit_budget"], min_lr=cfg.get("min_edit_budget", 2), total_steps=total_steps), then edit_budget = scheduler.step() every step. In autonomous mode the optimizer LLM picks the count from evidence alone. The prompt’s response contract, verbatim:

Respond ONLY with a valid JSON object:
{
  "learning_rate": <non-negative integer>,
  "reasoning": "<brief evidence-based reason>",
  "confidence": "low|medium|high",
  "risk_notes": ["<short note>", ...]
}

input → output example (the record saved to lr_decision.json)

decide_autonomous_learning_rate(skill_content=skill, merged_patch=merged, update_mode="patch",
                                rollout_hard=0.4375, rollout_soft=0.5211, rollout_n=16)
# -> {"learning_rate": 3, "raw_learning_rate": 3, "available_update_items": 9,
#     "clamped": false, "fallback": false, "confidence": "medium",
#     "reasoning": "...", "risk_notes": [...], "raw_response": "..."}
# on parse failure learning_rate=0 (fallback=true) — this step safely falls back to zero edits

rank_and_select(skill_content, patch, max_edits, …) → dict

select

skillopt/optimizer/clip.py — signature + docstring, verbatim

def rank_and_select(
    skill_content: str, patch: dict, max_edits: int,
    meta_skill_context: str = "", update_mode: str = "patch",
) -> dict:
    """Use a optimizer LLM to rank edits by importance, then keep top-L.
    If the edit pool is within budget, returns the patch unchanged.
    Otherwise, calls the optimizer to rank and select the most impactful edits."""

The counterpart of gradient clipping. When the merged edit pool exceeds the budget (the learning rate of the card above), the optimizer ranks by importance and passes only the top L. The result persists as the step directory’s ranked_edits.json.

evaluate_gate(…) → GateResult — the held-out validation gate

gate

skillopt/evaluation/gate.py — dataclass + decision logic, verbatim

@dataclass(frozen=True)
class GateResult:
    action: GateAction          # "accept_new_best" | "accept" | "reject"
    current_skill: str;  current_score: float
    best_skill: str;  best_score: float;  best_step: int

def evaluate_gate(candidate_skill, cand_hard, current_skill, current_score,
                  best_skill, best_score, best_step, global_step, *,
                  cand_soft=0.0, metric="hard", mixed_weight=0.5) -> GateResult:
    cand_score = select_gate_score(cand_hard, cand_soft, metric, mixed_weight)
    if cand_score > current_score:          # strict inequality — even a tie is rejected
        if cand_score > best_score:
            return GateResult(action="accept_new_best", ...)
        return GateResult(action="accept", ...)
    return GateResult(action="reject", ...)   # current/best unchanged = rollback

Module docstring, verbatim: “Analogous to validation-based early stopping and model selection in neural network training … This module is the pure decision function.” All side effects (cache lookups, rollouts, state mutation) belong to the trainer; the gate is a pure function. metric is one of hard | soft | mixed (weighted mean (1-w)·hard + w·soft).

input → output example

evaluate_gate(candidate_skill=cand, cand_hard=0.6250, current_skill=cur, current_score=0.5625,
              best_skill=best, best_score=0.6000, best_step=7, global_step=12)
# -> GateResult(action="accept_new_best", current_score=0.625, best_score=0.625, best_step=12)
# had cand_hard been 0.5625 -> GateResult(action="reject", ...)  # ties do not pass

step buffer — what the rejected-edit buffer really is (_format_step_buffer)

negative feedback

skillopt/engine/trainer.py — buf_entry construction (excerpt) + _format_step_buffer

# right after EVALUATE, every step (trainer.py):
buf_entry = {"step": global_step, "action": action,
             "n_total": n_total, "n_fail": n_fail,
             "failure_patterns": failure_patterns}
if "reject" in action and ranked_patch:      # only rejected steps attach their edit details
    buf_entry["score_before"] = current_score
    buf_entry["score_after"] = cand_gate_score
    buf_entry["rejected_edits"] = rejected_edits
step_buffer.append(buf_entry)

A buffer accumulating every earlier step’s failure patterns plus, for rejected steps, the “attempted edits + score drop.” _format_step_buffer renders it into one context block injected into all of the next step’s Reflect · LR-decision · Rewrite prompts. The rendered preamble, verbatim: “Below is a summary of previous steps in this epoch. Use it to avoid repeating ineffective edits and to prioritise failure patterns that remain unsolved.”

rendered output example (format exactly as in source strings)

### Step 4 — REJECT (5/16 failed)
  - "wrong_tool: used pandas where openpyxl is required" (×3, tasks: t12, t18, t27)
  Rejected edits (score 0.5625 → 0.5):
    1. [add] "Always coerce date columns with pd.to_datetime before comparison"
    2. [replace] target="..." → "..."

run_slow_update(…) + build_comparison_pairs(…) — the end-of-epoch slow update

slow update

skillopt/optimizer/slow_update.py + skillopt/prompts/slow_update.md

def run_slow_update(
    skill_content: str, results_prev: list[dict], results_curr: list[dict],
    items: list[dict], *, prev_skill: str = "",
    prev_slow_update_content: str = "",
    prev_rollout_dir: str = "", curr_rollout_dir: str = "",
    comparison_pairs: list[dict] | None = None,
    system_prompt: str | None = None,
) -> dict | None: ...

def build_comparison_pairs(results_prev, results_curr, items, ...) -> list[dict]:
    # category ∈ {improved, regressed, persistent_fail, stable_success}

At epoch end, the same 20 tasks (slow_update_samples) are rolled with both the previous and current skills to build longitudinal comparison pairs, targeting the systemic drift that step-level edits cannot catch. Prompt, verbatim: “The per-step analyst sees individual trajectories and proposes local patches. YOU see how the skill has evolved across an entire epoch … identify systemic drift, regressions, and persistent blind spots that step-level edits cannot catch.” The output guidance goes into the skill document’s SLOW_UPDATE protected region, with mandatory self-reflection on whether last epoch’s guidance helped. Two acceptance modes: by default it is force-injected into current_skill only (best_skill inviolate); with slow_update_gate_with_selection=true it must pass the same validation gate as a step.

run_meta_skill(prev_skill, curr_skill, comparison_pairs, …) — the optimizer’s meta-learning

meta update

skillopt/optimizer/meta_skill.py — signature, verbatim

def run_meta_skill(
    prev_skill: str, curr_skill: str, comparison_pairs: list[dict], *,
    prev_meta_skill_content: str = "",
    system_prompt: str | None = None,
) -> dict | None:
    """Produce updated optimizer-side meta skill from adjacent epochs."""

Consumes the same longitudinal pairs as slow update, but points the other way: it is the optimizer itself, not the target skill, that learns “which edits work in this environment.” The result is saved to meta_skill/epoch_NN/meta_skill_result.json and injected as context into every Reflect · Aggregate · Select · LR-decision call of the next epoch (active_meta_skill = _load_meta_skill_content(out_root, epoch - 1)).

exporting best_skill.md + runtime_state.json — deployment and resume

export

skillopt/engine/trainer.py — per-step save paths (excerpt)

_save_skill(out_root, global_step, current_skill)      # skills/skill_v0012.md — the full trajectory is preserved
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
    f.write(best_skill)                                 # the validation-best skill = the deployment artifact
_persist_runtime_state(global_step)                     # runtime_state.json: current/best scores·origin·paths

Every step’s skill version survives as skills/skill_v<NNNN>.md, while the gate-best copy keeps overwriting best_skill.md. Per the README this file is the final deployment artifact of “typically 300–2,000 tokens,” reporting best or tied-best in all 52 cells of 6 benchmarks × 7 target models × 3 harnesses, and on GPT-5.5 gains over no-skill of +23.5 (direct chat) / +24.8 (Codex loop) / +19.1 (Claude Code). runtime_state.json records last_completed_step and current_origin/best_origin (e.g. step_0012, slow_update_epoch_02) to support interrupt-resume.

run_sleep_cycle(cfg, *, seed_tasks, dry_run, clock) → CycleOutcome — the nightly cycle

skillopt_sleep

skillopt_sleep/cycle.py + consolidate.py + dream.py — signatures, verbatim

def run_sleep_cycle(cfg=None, *, seed_tasks=None, dry_run=False, clock=None) -> CycleOutcome
# CycleOutcome: (report: SleepReport, staging_dir: str, adopted: bool, adopted_paths: list[str])

def consolidate(backend, tasks, skill, memory, *,
    edit_budget: int = 4, gate_metric: str = "mixed", gate_mixed_weight: float = 0.5,
    gate_mode: str = "on", rollouts_k: int = 1, ...) -> ConsolidationResult
    """Run one consolidation epoch: reflect -> bounded edit -> gate."""

def dream_consolidate(backend, tasks, skill, memory, *,
    history_tasks=None, recall_k=0, dream_rollouts=1, dream_factor=0, ...) -> ConsolidationResult

One SkillOpt epoch that runs once a night. ConsolidationResult comes back holding applied_edits / rejected_edits (EditRecord lists) plus holdout baseline/candidate scores, and the report surfaces rejections as-is under a “Rejected by gate (kept as negative feedback)” section. dream (K synthetic variants) / recall (summoning similar past tasks) default to OFF and augment only the train split.

output example (report.md rendering, exactly the _render_report_md format)

# SkillOpt-Sleep — night 7 report
- held-out score: 0.500 -> 0.667
- gate: **accept_new_best** (accepted=True)
## Accepted edits
- [skill/add] Use uv, not pip, in this repo  _why: 3 sessions re-derived this_
## Rejected by gate (kept as negative feedback)
- [memory/add] Always run the full test suite before every commit

apply_edits(doc, edits) + the LEARNED protected region — what a bounded edit really is

skillopt_sleep

skillopt_sleep/memory.py + types.py — verbatim

LEARNED_START = "<!-- SKILLOPT-SLEEP:LEARNED START -->"
LEARNED_END   = "<!-- SKILLOPT-SLEEP:LEARNED END -->"

@dataclass
class EditRecord:
    """One bounded edit proposed/applied to skill or memory."""
    target: str        # "skill" | "memory"
    op: str            # add | delete | replace
    content: str = ""
    anchor: str = ""   # for replace/delete: text being changed
    rationale: str = ""

def apply_edits(doc: str, edits: List[EditRecord]) -> Tuple[str, List[EditRecord]]:
    """Apply add/delete/replace edits to the protected learned region.
    Returns (new_doc, applied_edits). Dedups: an `add` whose content already
    exists (normalized) is skipped."""

The edit operations are only add / delete / replace, and they work only inside the marker-fenced protected region (module docstring, verbatim: “the sleep cycle never clobbers the user's hand-written content”). A normalized-duplicate add is silently skipped; delete/replace match by normalized anchor substring. The main trainer’s patch mode uses the same three-operation contract.

input → output example

apply_edits(claude_md, [EditRecord(target="memory", op="add",
                                   content="Use uv, not pip, in this repo")])
# -> (a new doc with a "- Use uv, not pip, in this repo" bullet added in the LEARNED region, [the applied EditRecord])

SleepState + scheduler — the persistent state and cron that bridge nights

skillopt_sleep

skillopt_sleep/state.py + scheduler.py — verbatim

DEFAULT_STATE = {
    "version": 1,
    "night": 0,
    "last_harvest": {},   # project -> iso timestamp of last harvested record
    "slow_memory": "",    # cross-night consolidated lessons (meta-skill analogue)
    "history": [],        # list of per-night summaries
    "task_archive": [],   # capped(300) past mined tasks (for associative recall)
}

state.json lives in ~/.skillopt-sleep and bridges nightly episodes into long-term capability (docstring, verbatim: “the ‘long-term’ store that turns nightly episodes into durable competence”). The source itself comments that the slow_memory field is the counterpart of the main loop’s meta-skill. The scheduler plants a managed block in the crontab, firing at 03:17 by default — the comment explains why it avoids the top of the hour: “so many users don't all hit the API at the same instant.” Cron only goes as far as staging: “it only STAGES a proposal — adopt is still manual.”

04

Correspondence with us — the same optimization grammar, a different substrate

SkillOpt optimizes a textual skill document; we optimize an executable world model + a probability-carrying skill library. Yet the grammar of the loop overlaps exactly — each row below is a 1:1 correspondence.

SkillOpt (code-grounded)our enginethe correspondence
Rolloutadapter.rollout(train_env, current_skill, …) runs the training batch with the current skill, harvesting trajectories + scores wake OBSERVED log For both, “the record of acting on current knowledge” is the sole raw material of learning. For us that batch is the wake-phase observation log (action → frame change).
Reflectrun_minibatch_reflect splits failure and success trajectories and analyzes each under a different analyst prompt passing / failing split The dichotomy of extracting repair signals from failures and preservation signals from successes. Our sleep’s observation split (rounds where a skill held / rounds where it broke) is isomorphic.
Editedit_budget (a scheduler) or decide_autonomous_learning_rate bounds the number of edits this step EDIT MAGNITUDE In textual learning, step size = permitted edit volume. Our prompt’s EDIT MAGNITUDE directive (edit breadth proportional to the residual) is the same lever.
Gateevaluate_gate: accept only when cand_score > current_score on the held-out selection set, else roll back advantage + unseen confirm “Never trust the generator; judge on held-out data.” For us that gate is the judge’s advantage scoring plus confirmation on unseen states (unseen confirm).
rejected-edit buffer — every rejected step’s edits and score drop re-enter the next prompt via the step buffer hypothesis ledger REFUTED Failed attempts are not discarded but recirculated as explicit “do not retry this” context. Our hypothesis ledger’s REFUTED entries play the same role.
slow / meta updaterun_slow_update/run_meta_skill distill systemic lessons from end-of-epoch comparisons reasoning_log distillation A slower-timescale layer of summary learning on top of per-step edits. We periodically distill the reasoning_log, producing the same two-tier timescale.
best_skill.md — one validation-best skill document is the deployment artifact (300–2,000 tokens) living note Learning condenses into “a single continuously-updated document.” Our living note is that deployment surface.
05

What we added — three extensions on the SkillOpt grammar

SkillOpt’s validation signal is ultimately one scalar score (hard/soft/mixed); its learning target is non-executable text; its data is a fixed benchmark batch. We extended each of the three.

➕ a probability layer

Instead of the gate’s binary accept/reject, every skill carries a Beta-style posterior (confirm_n, falsify_n) that evidence updates cumulatively. In SkillOpt, rejected knowledge survives only as text in a buffer; with us, a refuted skill survives quarantined together with its posterior, so even “how wrong it was” carries forward quantitatively.

➕ an executable WM

SkillOpt’s skill document is text that takes effect only when the target model reads and interprets it. Our world model is executed: miner-authored predict_next code emits predicted changed cells that are checked cell-by-cell against the engine’s own logged transitions (Game.fork sealed, fork_dependence = 0) — the gate directly asks “was the prediction right?”, not “did the score go up?”. A far cheaper and more local signal (down to which cell was wrong) than re-rollout evaluation.

➕ EIG / VoI

SkillOpt never decides what to roll next — the dataset hands it the batch. We choose the next action itself through expected-information-gain (EIG) and value-of-information (VoI) gates: which probe splits the competing hypotheses hardest, and is that observation worth enough to change the current plan? Turning passive data consumption into active experiment design is the third extension.

A fair note in the other direction: SkillOpt is more refined than us in places — the learning rate’s autonomous mode (the optimizer chooses its own step size), the failure-first hierarchical merge’s support counts, and the slow update’s “did my last guidance actually help” self-reflection protocol are disciplines our sleep loop does not yet have.