Compare commits

...

137 Commits

Author SHA1 Message Date
Yifan Yang b860a5cf88 Merge pull request #150 from Yif-Yang/test/assert-fractional-degradation-score
test(sleep): assert fractional candidate score
2026-07-19 01:28:47 +08:00
Yif-Yang 7933f37bf0 test(sleep): assert fractional candidate score 2026-07-18 17:28:22 +00:00
Yifan Yang 67b67acf5f Merge pull request #149 from harshitnagar22/test/fractional-degradation-gate
test(sleep): add fractional degradation gate test for partial held-out regression
2026-07-19 01:26:25 +08:00
Harshit Nagar 4a4d597527 test(sleep): add fractional degradation gate test for partial held-out regression 2026-07-18 22:24:30 +05:30
Yifan Yang 8a50db3312 Merge pull request #147 from detailobsessed/feat/devin-handoff-backend
feat(devin): expose handoff backend in MCP server enum
2026-07-18 01:01:59 +08:00
Ismar 195d95e00c feat(devin): expose handoff backend in MCP server enum (#3)
The Devin MCP server's backend enum only listed mock/claude/codex/copilot,
excluding the handoff backend that was merged to the engine in #125. This
made the subscription-friendly, no-API-key path unavailable to Devin users
while Claude Code had a dedicated /skillopt-sleep-handoff command for it.

Add "handoff" to the backend enum in _TOOL_SCHEMA so sleep_run accepts
backend: "handoff". The engine already handles the prompt/answer file loop
(exit code 3 + .skillopt-sleep-handoff/); the MCP server needs no special
handling — it passes through the engine output showing pending prompts.

Update the README, rules snippet, and test_backends_in_enum accordingly.
2026-07-16 15:20:50 +02:00
Yifan Yang d2670205d1 Merge pull request #146 from Yif-Yang/fix/codex-token-tracker-isolation
fix(model): prevent Codex token double counting
2026-07-16 19:03:02 +09:00
Yif-Yang 74ff263608 fix(model): isolate Codex token tracking 2026-07-16 10:02:21 +00:00
Yifan Yang 3e3df8895e Merge pull request #112 from TheGameVIX/agent/codex-exec-optimizer-main
fix(codex): support exec optimizer backend
2026-07-16 18:57:47 +09:00
Yifan Yang 0d8839778c Merge pull request #101 from SparshGarg999/fix/46-windows-support-codex
feat(codex): add Windows support and compatibility for Codex plugin
2026-07-16 18:57:15 +09:00
TheGameVIX 0b94f890a0 fix(codex): support exec optimizer backend 2026-07-15 12:06:46 -04:00
Sparsh :) 7c7fdfcd6c fix(windows): resolve control flow and path quoting in scheduler task execution 2026-07-15 14:54:27 +05:30
Yifan Yang 57333f3406 Merge pull request #143 from Yif-Yang/draft/technical-blog-sar-review
docs: clarify blog structure and Skill-Aware Reflection
2026-07-15 04:12:07 +09:00
Yif-Yang a44566aeb8 docs: clarify technical blog structure and SAR 2026-07-14 19:10:47 +00:00
Yifan Yang 9babe37944 Merge pull request #140 from Yif-Yang/docs/technical-blog-gating-reflection
docs: launch SkillOpt Technical Blog
2026-07-15 03:14:20 +09:00
Yif-Yang a12072241d docs: launch SkillOpt Technical Blog 2026-07-14 18:12:58 +00:00
Yifan Yang a49d0eb1b9 Merge pull request #139 from Yif-Yang/docs/sync-post-v0.2
docs: sync documentation with post-v0.2 changes
2026-07-15 02:16:18 +09:00
Yif-Yang f31bf8c06b docs: sync documentation with post-v0.2 changes 2026-07-14 17:11:40 +00:00
Yifan Yang efb30b4bcc Merge pull request #138 from Yif-Yang/fix/sleep-openai-compat-safety
fix(sleep): harden OpenAI compatibility boundaries
2026-07-14 18:17:58 +09:00
Yif-Yang 8b39394c88 fix(sleep): harden OpenAI compatibility boundaries 2026-07-14 09:17:18 +00:00
Yifan Yang be2184660a Merge pull request #129 from Alphaxalchemy/feat/openai-compatible-endpoints
feat(sleep): support OpenAI-compatible endpoints (DeepSeek, vLLM) in azure_openai backend
2026-07-14 18:10:17 +09:00
Yifan Yang 940c3cd4ec Merge pull request #137 from Yif-Yang/fix/claude-tempdir-cleanup
fix(claude): use cleanup-tolerant temporary directory
2026-07-14 17:50:10 +09:00
Yifan Yang 42270f7fa4 Merge pull request #136 from Yif-Yang/fix/json-array-scan-linear
perf(json): keep array scan linear on malformed output
2026-07-14 17:50:07 +09:00
Yif-Yang 6a193ffd78 fix(claude): use cleanup-tolerant temporary directory 2026-07-14 08:46:54 +00:00
Yif-Yang 48e4c66827 perf(json): keep array scan linear on malformed output 2026-07-14 08:46:54 +00:00
Yifan Yang 1b64b000fb Merge pull request #135 from nankingjing/fix/issue-117-wheel-md-and-tempdir
Fix #117: include .md prompt files in wheel + handle Windows temp-dir cleanup
2026-07-14 17:41:55 +09:00
Yifan Yang 7191f1cb1f Merge pull request #103 from zixuanguo786-ctrl/codex/robust-json-array-extraction
[codex] Harden JSON array extraction
2026-07-14 17:41:52 +09:00
Yifan Yang 7bf9ecbec6 Merge pull request #115 from nankingjing/contrib/add-openai-compatible-backend
feat(model): add generic OpenAI-compatible LLM backend
2026-07-14 17:41:49 +09:00
Yifan Yang 2ffa73410e Merge pull request #105 from zixuanguo786-ctrl/codex/reject-duplicate-searchqa-manifest-ids
[codex] Reject duplicate SearchQA manifest ids
2026-07-14 17:41:45 +09:00
Yifan Yang c2992b96a6 Merge pull request #104 from zixuanguo786-ctrl/codex/preserve-fractional-hard-score
[codex] Preserve fractional rollout hard scores
2026-07-14 17:41:42 +09:00
Alphaxalchemy 4ff77b71ff fix(sleep): address review — CLI choice, auth guard, provider-neutral kwargs, tests
Addresses maintainer review on the OpenAI-compatible endpoints PR:

1. CLI: accept --backend azure_openai in skillopt_sleep/__main__.py (the
   documented command was rejected by the argparse choices).
2. Example runner: exit with the child's return code so watchdog/supervisors
   see a failed sleep run as a failure.
3. Error state: clear last_call_error when a retry recovers; set an explicit
   "empty response on all N attempts" diagnostic when every attempt returns
   empty text.
4. Security guard: the managed-identity path now refuses to send an Azure AD
   bearer token to any endpoint outside *.openai.azure.com /
   *.cognitiveservices.azure.com — a custom endpoint requires explicit
   AZURE_OPENAI_AUTH_MODE=openai_compatible + API key.
5. Provider-neutral requests: compat mode sends only the standard contract
   (max_tokens, default 8192 via SKILLOPT_SLEEP_COMPAT_MAX_TOKENS);
   provider-specific body fields are opt-in via SKILLOPT_SLEEP_CHAT_EXTRA_BODY
   (JSON) — the deepseek model-name inference is removed.
6. Docs: removed the unimplemented OPTIMIZER_*/TARGET_* env-var claim; added a
   configuration reference matching the implementation exactly.
7. Tests: tests/test_azure_openai_compat.py — 17 deterministic no-network
   unittest cases covering CLI acceptance, compat-vs-Azure client selection,
   endpoint resolution, the credential guard, request kwargs (opt-in extra
   body / token cap), retry-success error clearing, empty-response
   diagnostics, and runner exit-code propagation.

Re-verified live against DeepSeek (deepseek-v4-pro, openai_compatible mode)
after the rework: client type OpenAI, completion returned, no error state.
2026-07-14 01:22:19 -06:00
nankingjing df5bf20f09 Fix #117: include .md prompt files in wheel + handle Windows temp-dir cleanup
Fix 1: Add [tool.setuptools.package-data] to pyproject.toml so that
all .md prompt files (generic prompts under skillopt/prompts/ and
env-specific prompts under skillopt/envs/*/prompts/) are included
in the wheel. Previously pip install from PyPI would fail with
FileNotFoundError on first prompt load.

Fix 2: Replace TemporaryDirectory context manager with mkdtemp +
shutil.rmtree(ignore_errors=True) in claude_backend._run_claude_print.
On Windows, the spawned claude/node subprocess still holds a handle
on the temp directory when the context manager tries to clean up,
causing WinError 32. Manual rmtree with ignore_errors=True works
on all Python versions >= 3.10.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 11:17:19 +08:00
Ziiii ddc35b339d Fix array extraction after unmatched brace 2026-07-14 09:35:42 +08:00
nankingjing 45ff88a20c test(model): cover OpenAI-compatible backend integration
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 09:21:11 +08:00
黄云龙 e452761231 docs(new-backend): document the built-in openai_compatible backend 2026-07-14 09:17:43 +08:00
黄云龙 608757a73f feat(model): route optimizer/target calls to the openai_compatible backend 2026-07-14 09:16:39 +08:00
黄云龙 9dc43fccc4 feat(model): accept openai_compatible as optimizer/target backend 2026-07-14 09:16:09 +08:00
黄云龙 3656f1cb54 feat(model): register openai_compatible backend (aliases + default model) 2026-07-14 09:16:09 +08:00
黄云龙 84ea713a71 feat(model): add OpenAI-compatible generic backend for any OpenAI-compatible API 2026-07-14 09:16:09 +08:00
Yifan Yang 50fed2958b Merge pull request #133 from Yif-Yang/fix/scheduler-preserve-annealing-contract
fix(scheduler): preserve existing annealing contract after #123
2026-07-14 02:01:48 +09:00
Yif-Yang a0b6db59f8 fix(scheduler): preserve existing annealing contract 2026-07-13 16:57:07 +00:00
Yifan Yang e7665f8f5e Merge pull request #123 from nankingjing/test-scheduler-core
test: add unit tests for skillopt.optimizer.scheduler (edit-budget schedulers)
2026-07-14 01:54:24 +09:00
Yifan Yang ed0770dd38 Merge pull request #131 from AKhozya/feat/preferences-cli-wiring
feat: wire documented --preferences flag through CLI/config into the backend
2026-07-14 01:54:05 +09:00
黄云龙 daf3035429 test(scheduler): update sequence expectation to match consistent endpoint formula
The LinearScheduler.known_decay_sequence test now reflects the new
t = (step-1)/(total_steps-1) contract where step 1 returns max_lr.
Updated docstrings for midpoint and early-step tests to match.

All 43 tests now pass against the updated formula.
2026-07-14 00:45:22 +08:00
黄云龙 9f9f87c064 fix(scheduler): use consistent (step-1)/(total_steps-1) endpoint formula
LinearScheduler and CosineScheduler now guarantee step() returns max_lr
on the first call and min_lr on the total_steps-th call, using the
standard endpoint parameterisation t = (step-1)/(total_steps-1).

This resolves the inconsistent first-step semantics reported in review.
2026-07-14 00:45:12 +08:00
AKhozya 3b0af4668c feat: wire preferences through run cycle into reflect prior 2026-07-13 17:07:16 +01:00
Sparsh :) 79d6117f6f fix: address PR reviews, add scheduler Windows tasks and Pester coverage 2026-07-13 21:20:30 +05:30
Sparsh :) 3c45db3a90 feat(codex): add Windows support and compatibility for Codex plugin 2026-07-13 21:13:15 +05:30
Yifan Yang 25b0f39405 Merge pull request #107 from ichoosetoaccept/fix/plugin-pip-fallback
fix(plugin): fall back to pip/uv-installed skillopt-sleep in run-sleep.sh
2026-07-14 00:36:17 +09:00
Alphaxalchemy af33f6c338 feat(sleep): support OpenAI-compatible endpoints in azure_openai backend
Let AzureOpenAIBackend drive any OpenAI-compatible chat-completions server
(DeepSeek, self-hosted vLLM/Ollama, ...) alongside native Azure deployments.

- __init__ resolves the endpoint as: explicit arg > AZURE_OPENAI_ENDPOINT env
  > the built-in _AZURE_ENDPOINTS table (previously a non-Azure endpoint could
  not be supplied at all).
- _get_client() builds a plain openai.OpenAI(base_url=...) client when
  AZURE_OPENAI_AUTH_MODE=openai_compatible, matching the auth mode already
  supported by the sibling skillopt/model/azure_openai.py. This avoids the
  AzureOpenAI SDK rewriting request URLs with Azure-only ?api-version= query
  params and deployment path segments, which non-Azure servers reject with 404.
- _call() sends max_tokens + extra_body thinking flag for deepseek* models and
  records the last exception in self.last_call_error so a failed night is
  diagnosable instead of collapsing to a silent empty->0 score.

Adds docs/sleep/openai-compatible-endpoints.md and sanitized example
runner/watchdog scripts documenting an Antigravity + DeepSeek integration.
The default managed-identity Azure path is unchanged.
2026-07-12 21:42:55 -06:00
Sparsh :) 49a5b617c0 fix(gate): resolve issue #100 (#102) 2026-07-13 01:27:13 +09:00
codeL1985 da99301210 fix(harvest): exclude sub-agent transcripts and plugin noise from task mining (#99)
Three filters to stop machine-generated prompts polluting the mined task
pool (they dominated ~80% of tasks on this machine):

- _is_meta_prompt: drop expanded slash-command bodies (<command-message>
  tags or '# /' headers) — plugin self-invocations are not user intents
- _AGENT_SESSION_MARKERS + _is_agent_session: skip sessions whose first
  prompt is another tool's agent brief (claude-mem observers, CLAUDE.md
  critic sub-agents, SkillOpt-Sleep's own command body)
- load walk: skip <session>/subagents/ dirs and agent-*.jsonl files —
  Agent-tool sidechain transcripts are Claude-authored, not user tasks

Verified: harvest went from 120 sessions / mostly-noise tasks to 55
sessions / 38 real user tasks.

Co-authored-by: codeL1985 <l@cypherlab.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:27:05 +09:00
Jay C 19d98ea01c fix(model): route chat_optimizer through minimax_chat (not just chat_target) (#116)
Two missing pieces in the minimax_chat dispatch chain:

1. minimax_backend.py did not define chat_optimizer or chat_optimizer_messages;
   only chat_target / chat_target_messages existed. Any caller using
   optimizer_backend='minimax_chat' would hit AttributeError or fall through
   to _openai (Azure).

2. skillopt/model/__init__.py's chat_optimizer and chat_optimizer_messages
   dispatchers checked claude_chat and qwen_chat but not minimax_chat,
   so minimax_chat callers would silently fall through to _openai.chat_optimizer
   (the Azure path), which fails with 'Azure OpenAI endpoint is not configured'
   on any setup without AZURE_OPENAI_* env vars.

Adds chat_optimizer to minimax_backend.py (mirrors chat_target via
_chat_messages_impl) and minimax_chat branches to both
chat_optimizer / chat_optimizer_messages dispatchers.

Verified locally: 1-epoch training on a 4-item SearchQA-format dataset
went from '[skip] no usable patches — skill unchanged' (baseline fallback)
to a successful accept_new_best with success_patches=1 per step.

Co-authored-by: Mavis (MiniMax) <Mavis@MiniMax.local>
Co-authored-by: jc <jc@users.noreply.github.com>
2026-07-13 01:25:55 +09:00
Chirag Singhal 46e8e800bc fix(qwen): support reasoning-model params (max_completion_tokens, omit temperature) (#128)
The qwen_chat backend (the generic OpenAI-compatible client) hardcoded
max_tokens and always sent temperature, so reasoning models behind
OpenAI-compatible gateways (GPT-5.x, Claude Opus 4.8 via Azure/LiteLLM)
would 400.

- Add opt-in QWEN_CHAT_USE_MAX_COMPLETION_TOKENS (+ role variants) that
  swaps the payload key max_tokens -> max_completion_tokens.
- Treat an explicit empty / none / off temperature as "omit" instead of
  collapsing to the 0.7 default (via _resolve_temperature).
- Thread both through configure_qwen_chat / _update_config.
- Defaults unchanged; fully backward compatible. Adds 6 tests.

Fixes #127

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
2026-07-13 01:24:40 +09:00
dimitarvdenev b309723baa feat(sleep): add handoff backend — session-executed model calls, no API subprocess (#125)
Adds --backend handoff: the engine runs all deterministic stages and
outsources attempt/judge/reflect to prompt/answer files an interactive
agent session fills between runs (exit 3 = pending batch, re-run to
resume). Deterministic replay + the prompt-hash answer cache make resume
stateless; sentinel detection aborts any call built from unanswered
output so placeholders never reach scores or staging. Session digests
and mined tasks are pinned per night (secret-redacted) so the sessions
answering prompts cannot shift the task set, and LLM mining is routed
through the same handoff files. Ships a /skillopt-sleep-handoff Claude
Code command that answers each prompt in a fresh-context subagent to
protect the held-out gate.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:23:53 +09:00
ClumsyLucid 7df49656d1 fix(skillopt-sleep): surface Claude CLI spawn failures instead of silent zero scores (#126)
* fix(skillopt-sleep): surface Claude CLI spawn failures instead of silent zero scores

In _call and attempt_with_tools, the bare 'except Exception: return ""'
swallows FileNotFoundError (e.g. bare 'claude' on Windows with npm .cmd
shim) and any other spawn failure, returning an empty string that the
trainer treats as a legitimate model response that scores 0.0 everywhere.

Now the exception is caught explicitly: last_call_error is set, a
warning is logged, and the empty-string return is preserved for
backward compatibility of the control flow. This mirrors the pattern
from #92 (codex backend) which fixed the same class of 'dead CLI
masquerades as nothing to learn' bug.

Issue: #121

* test(skillopt-sleep): add tests verifying Claude CLI spawn failures are surfaced

Add two tests to TestClaudeCliBackendBare:
- test_spawn_failure_sets_last_call_error: _call sets last_call_error
  and returns '' when subprocess.run raises FileNotFoundError.
- test_attempt_tools_spawn_failure_sets_last_call_error: same for
  attempt_with_tools.

These prove the fix from the parent commit (surface spawn failures
instead of silently scoring 0) and guard against regressions.
2026-07-13 01:23:10 +09:00
codeL1985 df94a91ddc fix: make ClaudeCliBackend work on Windows (.cmd shim resolution + argv length) (#98)
* fix(backend): make ClaudeCliBackend work on Windows (.cmd shim + argv limit)

Every claude call failed with WinError 2 and was swallowed by the bare
except -> return '', so real-backend cycles silently scored 0.000/0.000
and produced zero edits.

- resolve claude_path via shutil.which: the npm-installed claude is a
  .cmd shim that CreateProcess cannot resolve by bare name
- pass the prompt via stdin instead of argv: the .cmd shim routes
  through cmd.exe whose command line caps at ~8K chars, which
  reflect/judge prompts exceed

Verified: reflect() on a synthetic max_chars failure now returns a
concrete bounded edit via the real CLI (was [] before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(backend): suppress console window flashes on Windows (CREATE_NO_WINDOW)

Every CLI subprocess (claude/codex/copilot) spawned from a console-less
parent allocates a visible console window on Windows. A cycle making
hundreds of calls strobes cmd windows and steals focus, making the
machine unusable while the engine runs. Pass CREATE_NO_WINDOW on all
CLI call sites; no-op on POSIX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: codeL1985 <l@cypherlab.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:22:14 +09:00
Tanmay Garg cd8034c2db test(sleep): assert scores and gate_action in verifier tests (closes #94) (#96)
Add explicit assertions for held-out scores and gate actions to the verifier discipline test suite to strengthen its guarantees.

- Assert the concrete held-out baseline and candidate scores in test_gate_rejects_reward_hacking_edit.
- Add test_gate_accepts_beneficial_edit using MockBeneficialBackend to provide a paired case where an edit genuinely improves the held-out slice, expecting accepted=True and gate_action='accept_new_best'.
2026-07-13 01:20:30 +09:00
黄云龙 352cbc3445 fix(config): read YAML config files as UTF-8 (#124)
_load_yaml opened config files with the platform default (locale)
encoding instead of UTF-8. The shipped configs (e.g.
configs/_base_/default.yaml) contain UTF-8 non-ASCII characters
(em-dash, arrows, box-drawing), so load_config() raises
UnicodeDecodeError on any non-UTF-8 locale (e.g. Windows cp936/cp1252),
breaking scripts/train.py and scripts/eval_only.py before startup.

YAML is UTF-8 by spec, and the rest of the codebase already opens text
files with encoding="utf-8". Pass encoding="utf-8" here for parity.
2026-07-13 01:19:59 +09:00
黄云龙 8687566792 test(gate): add unit tests for evaluation gate decision function (#122) 2026-07-13 01:19:56 +09:00
黄云龙 3c565a7f84 test: add unit tests for skillopt.optimizer.scheduler 2026-07-11 15:06:11 +08:00
Ismar ce55241a89 fix(plugin): fall back to pip/uv-installed skillopt-sleep in run-sleep.sh
The Claude Code and Codex plugin shells' runner (run-sleep.sh) only
resolved the engine by searching for a skillopt_sleep/ source directory
on disk. After v0.2.0 shipped the engine on PyPI (skillopt-sleep CLI),
the plugins still errored for users who installed via pip/uv without
cloning the repo — even though the error message said "pip install
skillopt" would work.

Add two fallbacks before the error:
  1. skillopt-sleep CLI on PATH (covers uv tool install, pipx, pip install)
  2. python -m skillopt_sleep when importable (covers pip install into
     the active Python)

Fallback 1 is checked before fallback 2 because uv tool install / pipx
isolate the package from the system Python's import path, so the import
check would fail even though the CLI is available.

The existing source-checkout resolution path is unchanged — repo-clone
users keep working as before. The fix lands in both plugins/run-sleep.sh
(shared, used by Codex) and plugins/claude-code/scripts/run-sleep.sh
(bundled copy, byte-identical, used by the Claude Code marketplace
install which fetches only the plugins/claude-code/ subtree).
2026-07-06 15:25:38 +02:00
Ziiii 08916747ef Reject overlapping SearchQA manifest ids
Runnable SearchQA splits should remain disjoint. A duplicate manifest id across train, val, or test previously collapsed into the wanted-id set and reused the same row in multiple output splits without warning.

Constraint: Preserve manifest order and output schema for valid manifests.

Rejected: Deduplicate automatically | hiding split overlap would make evaluation contamination harder to notice.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Fail fast on split-manifest integrity problems instead of repairing them silently.

Tested: uv run --with pytest pytest tests/test_materialize_searchqa.py -q

Tested: uv run --with ruff ruff check scripts/materialize_searchqa.py tests/test_materialize_searchqa.py

Not-tested: Loading the live Hugging Face dataset.
2026-07-06 15:42:15 +08:00
Ziiii 0c30b778fb Preserve continuous rollout hard scores
Rollout hard scores can be continuous when smoothed rewards are used. Converting the field through int() turned values like 0.75 into 0, which loses signal before scoring and serialization.

Constraint: Keep the existing RolloutResult dictionary shape unchanged.

Rejected: Clamp hard scores to 0 or 1 | contradicts existing continuous-score support in compute_score and sleep replay types.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Treat hard as numeric reward data, not only a binary label.

Tested: uv run --with pytest pytest tests/test_types.py tests/test_scoring.py -q

Tested: uv run --with ruff ruff check skillopt/types.py tests/test_types.py

Not-tested: Full benchmark rollouts.
2026-07-06 15:42:07 +08:00
Ziiii bf85781ffb Avoid ambiguous JSON array extraction
LLM responses can include bracketed prose before the actual JSON array. The old greedy regex spanned from the first '[' to the last ']', causing valid single-array answers to be dropped and making multiple arrays indistinguishable.

Constraint: Keep object extraction behavior unchanged.

Rejected: Return first parseable array | silently guesses when a response has multiple valid arrays.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep array extraction conservative when more than one valid top-level array is present.

Tested: uv run --with pytest pytest tests/test_json_utils.py -q

Tested: uv run --with ruff ruff check skillopt/utils/json_utils.py tests/test_json_utils.py

Not-tested: Full benchmark rollouts.
2026-07-06 15:42:01 +08:00
CharlesYang030 e4ea6a6771 chore(release): v0.2.0
Highlights since v0.1.0:
- feat: SkillOpt-Sleep engine — nightly offline self-evolution
  (harvest -> mine -> replay -> consolidate behind a validation gate),
  with multi-objective reward, experience replay + dream rollouts,
  slow-update long-term memory, and secret redaction in cycle diagnostics.
  Shipped as the `skillopt-sleep` CLI.
- feat: cross-tool backends & plugin shells — Claude, Codex (+Desktop
  harvest), Copilot, Devin, and OpenClaw.
- feat: SearchQA split materialization + rollout fail-fast.
- fix: Windows robustness for claude/codex backends, hardened JSON
  fallback, Qwen timeout/thinking gating, Codex failure surfacing.

Packaging:
- Bump pyproject / skillopt / skillopt_sleep to 0.2.0.
- Restore skillopt_webui to the packaged wheel.

See CHANGELOG.md for the full changelog and contributor acknowledgements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:11:10 +08:00
Yif Yang 5487e2c426 fix(skillopt-sleep): redact secrets before persisting cycle diagnostics
PR #92 added a per-cycle diagnostics.json that surfaces backend stderr,
optimizer replies, and task responses so a 0.0 night is self-diagnosing.
Those free-text fields can carry credentials (e.g. a codex 401 stderr dump
containing an auth token), so persisting them verbatim was a new on-disk
leak surface.

- Add a shared redact_secrets() in staging.py and route diagnostics.json's
  call_error / reflect_raw_head / holdout_detail through it before writing.
- Redact the codex and Claude auth-error log lines too (a secondary sink
  when a file log handler is attached); last_call_error stays raw in memory
  so _AUTH_MARKERS matching is unaffected.
- Centralize _SECRET_PATTERNS in staging.py (harvest_codex now reuses them)
  and extend coverage to AWS / GitHub / Slack / Google / JWT token shapes.
- Tests: secret-shape coverage, private-key blocks, recursive/scalar
  passthrough, no over-redaction of plain prose, fail-fast auth-error log
  redaction, and an end-to-end check that diagnostics.json has no secret.

Observability-only; the gate and learning algorithm are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-30 19:47:36 +00:00
Yifan Yang b9142bad24 fix(skillopt-sleep): surface codex auth/model/version failures instead of silently scoring 0 (#92)
Splits CodexCliBackend._call into _call_once + a retry wrapper so transient empties/timeouts are retried instead of silently scored 0, and fails fast on fatal auth/model/version errors (401, refresh_token_reused, token_expired, ChatGPT-account-unsupported, newer-Codex-required). On non-zero exit the CLI error text is surfaced via last_call_error instead of being returned as a model response. Adds per-cycle diagnostics.json (observability only; gate and learning algorithm unchanged) so a 0.0 night self-explains.
2026-07-01 03:20:08 +08:00
Yifan Yang 95a9e959fe test(sleep): add verifier-discipline stress test for the validation gate (#87)
Adds a reward-hacking stress test ensuring the consolidation gate rejects skill edits that game train/replay tasks while degrading held-out behavior. Also wires the minimax_chat backend into scripts/eval_only.py (coexisting with the qwen wiring from #85). Closes #67.
2026-07-01 02:40:24 +08:00
Tanmay9223 680dd28f5a fix(tests): move TestVerifierDiscipline above main block
(Addresses PR review feedback by ensuring python file-run execution discovers the test class)
2026-06-30 13:05:01 +05:30
Tanmay9223 fccc21f3f6 test(sleep): add verifier-discipline stress test (closes #67)
Add a regression test to ensure the validation gate correctly rejects
reward-hacking skill edits. It has been observed that optimizers
sometimes propose shortcuts that improve train/replay metrics but fail
to improve held-out behavior. This test codifies that the gate blocks
such artifacts.

Add TestVerifierDiscipline to the test_sleep_engine.py suite:
- Create MockRewardHackingBackend that simulates a reward-hacking rule
  which passes the train set but degrades the held-out tasks.
- Assert that the proposed edit is rejected by the gate.
2026-06-30 13:04:22 +05:30
Yifan Yang 6849e609a3 feat(eval): add missing minimax backend configuration
Add missing configuration setup in scripts/eval_only.py to properly
support the minimax_chat backend, which was entirely omitted.

Fix the following coverage gaps in eval_only.py:
- Add minimax CLI arguments
- Include the minimax config mappings in _MAP
- Update the backend parsing logic
- Call configure_minimax_chat
2026-06-30 13:04:22 +05:30
Daniel Martinez 9fa0716c72 fix(skillopt-sleep): also surface codex failures on the tool-call rollout path
Follow-up from a fresh-context review of the prior commit: CodexCliBackend.attempt_with_tools
(the rollout path for tool-requiring tasks) ran codex exec inline, swallowed all exceptions,
and never set last_call_error — so an auth/model/version failure on the tool path still
produced a silent empty->0 with no diagnostic signal, the exact failure class the prior commit
fixed for the _call path. Now it surfaces timeout/exception/non-zero-exit via last_call_error
(response stays empty; never leaks the CLI error text), so a failed tool rollout shows up in
diagnostics.json. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:56:11 -05:00
Daniel Martinez 9fcf5868c3 fix(skillopt-sleep): surface codex auth/model/version failures instead of silently scoring 0
A nightly sleep cycle could run for weeks emitting held-out 0.0 -> 0.0 (gate reject, zero
edits), indistinguishable from "nothing to learn", when the real cause was the codex backend
returning an error (expired auth / model unsupported on the account / outdated CLI) that got
scored as a failed rollout.

backend (CodexCliBackend):
- split _call into _call_once + a retry wrapper: transient empties/timeouts are retried
  instead of silently returning "" (mirrors AzureOpenAIBackend's guard);
- on a non-zero exit, surface the reason via last_call_error and return "" rather than
  leaking the CLI error text as if it were a model response;
- fail fast (no retries) on fatal auth/model/version errors (401, refresh_token_reused,
  token_expired, "not supported when using Codex with a ChatGPT account",
  "requires a newer version of Codex").
backend (CliBackend.reflect): retain last_reflect_raw so a no-edits night is diagnosable.
consolidate: ConsolidationResult now carries per-task held-out detail (response, hard/soft,
  fail_reason) + reflect_raw + call_error.
cycle: write diagnostics.json per cycle so a 0.0 night self-explains instead of being a black box.
tests: 4 new (retry-not-silent-zero, auth-error-surfaced-not-scored, holdout-detail, reflect-raw).

Also gitignore the .skillopt-sleep/ runtime dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:26:20 -05:00
Yifan Yang 9969a8f393 Add Devin plugin (plugins/devin): MCP server + ATIF-v1.7 harvest (#88)
Wires skillopt_sleep into Devin via a stdlib-only MCP server and an ATIF-v1.7 harvester, following the plugins/copilot thin-shell pattern. Includes path-expansion fix, tests + ATIF fixture, schema/tool parity with copilot, and a harvest fix so single-turn sessions aren't dropped by the <3s replay filter.
2026-06-26 11:04:23 +08:00
Yif Yang 26e5338def Update citation from @misc to @article format
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 02:54:46 +00:00
khashayar 1a70e4c9cd devin harvest: space turns >=5s so single-turn sessions aren't dropped
A harvested single-turn Devin session spanned only 1s (reply written 1000ms
after the prompt), which the engine's harvest filter conservatively classifies
as a <3s headless replay (skillopt_sleep Issue #62) and skips — so a real
single-turn session mined 0 tasks. Widen the prompt->reply gap to 5s. With this,
an end-to-end dry-run mines the task: "night 1: 1 sessions -> 1 tasks".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:03:15 +02:00
khashayar 9799c41461 devin plugin: full schema/tool parity with plugins/copilot
Mirror the copilot MCP server: same rich _TOOL_SCHEMA (source, model,
tasks_file, target_skill_path, max_sessions, max_tasks, lookback_hours,
auto_adopt, json, edit_budget, hour, minute) and generic flag forwarding, plus
sleep_schedule / sleep_unschedule. Devin specifics retained: the ATIF-v1.7
harvest step (run before data-reading actions, engine pointed at it via
--claude-home, default --source claude) and post-adopt sync into .devin/skills/.
Tests + README + rules snippet updated for the 7-tool interface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:56:42 +02:00
khashayar e51eb7c4be devin plugin: expand ~ in CLAUDE_HOME from env + add tests & ATIF fixture
Review fixes:
- Path bug: SKILLOPT_DEVIN_CLAUDE_HOME (and SKILLOPT_SLEEP_REPO) read from the
  env are now wrapped in os.path.expanduser, so the documented "~/..." config
  no longer passes a literal ~ to --claude-home (which yielded zero mined
  sessions). expanduser on an absolute default is a no-op.
- tests/test_devin_plugin.py: tool-schema completeness, action→subcommand map,
  backend enum, the CLAUDE_HOME expansion regression, and an ATIF-v1.7 harvest
  shape test against a bundled fixture.
- plugins/devin/fixtures/devin_sample.json: sample ATIF-v1.7 transcript.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:49:21 +02:00
Yifan Yang 99ccb93945 fix(eval-only): configure qwen_chat/minimax backends so local LLM endpoints work (#85)
Replicates the trainer's backend setup in scripts/eval_only.py so eval-only no longer silently falls back to an unconfigured local endpoint. Closes #84.
2026-06-26 02:55:18 +08:00
Yifan Yang 9de9220214 docs(sleep): add cross-model scaling results (nano +11.9) and hyperparam ablation (#89)
Update RESULTS.md with:
- §2: GPT-5.4-nano target yields +11.9 pt (0.560→0.679) on SearchQA —
  2× the GPT-5.5 gain, demonstrating bigger benefit where headroom exists
- §4: Hyperparameter sweep confirms shipped defaults are optimal

Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
2026-06-26 01:40:58 +08:00
khashayar bec23ed020 Add Devin plugin (plugins/devin): MCP server + ATIF-v1.7 harvest
Wires the skillopt_sleep engine into Devin (Cognition) via an MCP server,
following the same thin-shell pattern as plugins/copilot.

- mcp_server.py: stdlib-only stdio MCP server exposing the standard sleep_*
  tools (status, dry-run, run, adopt, harvest). REPO_ROOT defaults to ../.. so
  it finds skillopt_sleep automatically when run from plugins/devin/.
- harvest_devin.py: converts Devin ATIF-v1.7 transcripts, agentmemory, and
  .devin/skills/*/SKILL.md into the Claude Code-compatible JSONL the engine
  consumes; enriches with taskKey + outcome envelopes (hard test/build signal
  or judge rubric). Workspace auto-detection; cross-platform paths.
- judge.py, mcp-config.example.json, devin-rules.snippet.md, README.md.
- plugins/README.md: add Devin to the platform + install tables.

No changes to skillopt_sleep; shells out to `python -m skillopt_sleep` like the
other plugins. Pure stdlib; default backend mock (no API spend).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:42:52 +02:00
Gergely Imreh 8559308361 fix(eval-only): call configure_qwen_chat so itslocal LLM endpoints can be used
The eval-only tool skipped configuring some of the backend types, that
the training did configure. Because of this, the eval is silently
fell back to a local endpoint that wasn't actually configured, and
all evaluations runs failed.

Replicate the backend setup based on the trainer's code, and eval-only
can run with the qwen_chat backends.

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
2026-06-24 15:31:19 +08:00
Yifan Yang 2d7e37a395 fix(json_utils): reject prose pseudo-JSON in single quotes/backticks (#82)
Follow-up to the string-aware brace scan: that change only skipped
double-quoted prose, so brace-shaped text in single quotes, backticks, or
bare prose (e.g. `{op: delete}`, '{x: 1}') still reached json_repair and was
fabricated into a bogus dict — strictly worse than None, since extract_json
feeds the optimizer's skill edits.

Add a _looks_json_like() guard before repair: a genuine JSON object's first
non-space char after `{` is `"` (a key) or `}` (empty). Prose pseudo-objects
start with a bare word and are rejected, while legitimate repair targets
(trailing commas, unescaped quotes inside string values) all begin with `"`
and pass — including objects whose string VALUES contain single quotes or
backticks, which must not be rejected.

Found by an independent GPT-5.5 re-review of the merged #79 code. Adds
regression tests for single-quoted / backticked / bare prose (-> None) and
for legitimate objects with quote/backtick string values (still repaired).
Tests: 30 pass (+3 skip) without json_repair, 33 pass with it, both clean
under -W error::RuntimeWarning.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 20:31:39 +08:00
Yifan Yang baad64a3b9 docs(readme): remove Acknowledgements section (#81)
The contributor is already credited via the Co-authored-by trailer carried
into main by #79; a dedicated README section is unnecessary.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 19:13:16 +08:00
Yifan Yang c2e47c50fb docs(readme): acknowledge community contributor @samuelgoofus-boop (#80)
Add an Acknowledgements section crediting @samuelgoofus-boop for the
Windows-robustness work on the Claude/Codex backends (originally #77,
merged via #79).

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 19:03:30 +08:00
Yifan Yang 14c045f04f Windows robustness for claude/codex backends (+ hardened JSON fallback) (#79)
* Robustness for the claude/codex backends on Windows: argv overflow, subprocess encoding, tolerant JSON, test-eval dirs

Fixes surfaced running SkillOpt end-to-end on the bundled `claude` backend
(local Claude CLI) on Windows. None changes the OpenAI/GPT happy path.

1. skillopt/engine/trainer.py — the final test-eval directory
   (test_eval_final/) is written to before being created; add
   os.makedirs(..., exist_ok=True), matching the two sibling test-eval dirs.
   Without it, summary.json raises FileNotFoundError when a rollout yields
   zero predictions.

2. skillopt/model/claude_backend.py
   a. Pass the prompt via stdin (not argv): on Windows the whole command line
      is capped at ~32 KB and a large optimizer prompt (the success-analyst
      minibatch carrying several report trajectories) overflows it with
      [WinError 206], killing the run after retries.
   b. Pass the system prompt via --append-system-prompt-file (a temp file),
      not argv. The system prompt here is the skill being optimized, which
      SkillOpt grows over training; since the ~32 KB cap applies to the SUM of
      all argv, a grown skill would re-hit [WinError 206] even with the prompt
      on stdin.
   c. Pin the subprocess encoding to utf-8 (errors="replace"). With text=True
      and no encoding=, stdin is encoded with the system codepage; on a zh-CN
      box (cp936/GBK) a prompt containing an emoji or some Latin-1 characters
      raises UnicodeEncodeError before the CLI even starts, failing every retry.

3. skillopt/model/codex_backend.py — the same utf-8 encoding pin on its
   subprocess.run(input=...) call (identical unpinned-encoding pattern).

4. skillopt/utils/json_utils.py — extract_json() returned None for valid-
   looking JSON that strict json.loads rejects (unescaped ASCII quotes inside
   CJK string values, trailing commas), silently dropping the analyst's edits
   on non-schema backends (Claude/Qwen): reflect produces N edits, 0 applied.
   Add a json_repair fallback, but only on a single unambiguous object — a
   balanced-brace extractor plus a refuse-on-multiple-objects guard — so a
   chain-of-thought "scratch + final" response can't make repair silently
   return the wrong (discarded) object, which would be worse than None (None is
   detectable and retryable; a wrong-but-valid edit is applied blind). Declare
   json_repair in requirements.txt and the claude/qwen optional extras so the
   fallback is actually present (it otherwise no-ops, dropping edits silently).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit dca74a683e)

* fix(json_utils): harden tolerant JSON fallback from PR #77

Follow-up fixes on top of the cherry-picked Windows-robustness change:

1. Make _top_level_brace_objects() fully string-aware in its OUTER scan, not
   just inside an object. A '{' inside quoted prose (e.g. '"set it to {x}"')
   no longer starts a candidate object, so extract_json() returns None for
   prose pseudo-JSON instead of repairing it into a bogus dict — which would
   be strictly worse than dropping the edit, since extract_json feeds the
   optimizer's skill edits.

2. Pick the repair candidate BEFORE importing json_repair, so the missing-
   dependency RuntimeWarning only fires when there is genuinely a single
   malformed object that could have been repaired. Ordinary no-JSON / prose
   replies (the common case) now return None silently instead of warning on
   every call.

3. Resolve dependency-metadata inconsistency: json_repair is optional, so add
   it to the `all` extra (it was already in `claude`/`qwen`) and demote it
   from a hard requirement to an optional/commented entry in requirements.txt,
   matching the project's convention for backend-specific deps.

Adds regression tests for prose-with-braces (-> None), no-warning-on-plain-
text, single-object repair, and multi-object ambiguity. Existing 22 json
tests still pass with and without json_repair installed.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: samuelgoofus-boop <260247789+samuelgoofus-boop@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:00:23 +08:00
carpedkm 2841f82428 Fix ALFWorld gamefile paths relative to ALFWORLD_DATA 2026-06-23 10:32:38 +00:00
Yifan Yang 64c6dda105 Merge pull request #78 from Yif-Yang/main
docs(readme): add Trendshift daily/weekly badges
2026-06-23 16:52:42 +08:00
Yifan Yang c98eac18c7 docs(readme): add Trendshift daily/weekly badges (#1)
Add the microsoft/SkillOpt Trendshift badges (daily + weekly) side by
side in the README header.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 16:50:47 +08:00
Yifan Yang fc1f827f07 Merge pull request #74 from Yif-Yang/fix/python-path-and-lookback
fix: SKILLOPT_SLEEP_PYTHON override + lookback_hours first-run fallback
2026-06-20 22:26:43 +08:00
carpedkm 01b3e01804 fix: use None default for --lookback-hours to distinguish omitted vs 0
Codex round 3: argparse default=0 made every CLI invocation without
--lookback-hours clobber the config's 72h default. Now default=None;
only explicit --lookback-hours N (including 0) overrides config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 14:23:17 +00:00
carpedkm 01075c90d3 fix: address codex round 2 — revert harvest break + allow lookback 0
- harvest.py: revert break to continue — mtime ordering can diverge
  from embedded ended_at timestamps (copy/touch), so we must check all
  files rather than early-exiting on the first old one
- cycle.py: use `is not None and > 0` so lookback_hours=0 means
  "scan full history" (opt-out of the cutoff)
- __main__.py: propagate --lookback-hours 0 to config as explicit 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 14:21:18 +00:00
carpedkm 6cc1cd2e95 fix: address codex review — use clock for cutoff + early-exit harvest
- cycle.py: use supplied `clock` parameter (not wall time) for the
  lookback cutoff, so deterministic tests/experiments get reproducible
  harvest windows
- harvest.py: break (not continue) when a file is older than since_iso,
  since files are sorted newest-first by mtime — avoids scanning the
  entire transcript directory for quiet projects with large histories

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 14:11:58 +00:00
carpedkm 889238b234 fix: add SKILLOPT_SLEEP_PYTHON override + lookback_hours first-run fallback
Two fixes from issue #57 feedback:

1. run-sleep.sh: support SKILLOPT_SLEEP_PYTHON env var to explicitly set
   the Python interpreter. Useful on macOS where system Python is 3.9 but
   a newer Python is available elsewhere (e.g. Codex Desktop's bundled
   Python 3.12). Applied to both the shared runner and the bundled
   Claude Code plugin copy.

2. cycle.py: on first run (no prior harvest recorded), apply the
   lookback_hours config (default 72h) as a time cutoff. Previously,
   first run scanned the entire transcript history, which could trigger
   massive LLM mining on users with months of session data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 14:07:50 +00:00
Yifan Yang b5a1c2b317 Merge pull request #73 from Yif-Yang/fix/bare-subscription-auth
fix(sleep): make --bare conditional on ANTHROPIC_API_KEY (#68)
2026-06-20 21:46:09 +08:00
carpedkm 552ddefd74 fix: narrow CLI error markers to avoid false positives
Address codex review: "API key" was too generic — a model response
about configuring API keys would trigger a false auth warning. Now:
- Use specific phrases ("Invalid API key", "Unauthorized: invalid x-api-key")
- Only check short stdout (<300 chars) to skip real model responses
- Still check stderr unconditionally

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 13:32:43 +00:00
carpedkm bfa53bc46d fix(sleep): make --bare conditional on ANTHROPIC_API_KEY (#68)
ClaudeCliBackend._call() and attempt_with_tools() hardcoded --bare,
which skips Claude CLI's credential resolution. This broke subscription-
token auth: every model call silently returned "Not logged in" and
scored 0 — the user saw "baseline 0.0 → candidate 0.0, gate reject"
with no indication of an auth failure.

Fix: only pass --bare when ANTHROPIC_API_KEY is set. The remaining
isolation flags (--disable-slash-commands, --disallowedTools,
--exclude-dynamic-system-prompt-sections, clean temp cwd) already
provide the needed isolation without --bare.

Also adds _detect_cli_error() to log a warning when CLI output matches
known auth error patterns, so auth failures surface loudly instead of
deflating every score to 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 13:28:34 +00:00
Yifan Yang 24b5a25ba8 Merge pull request #72 from Yif-Yang/feat/plugin-feature-sync
feat: sync all 4 runtime plugins with full engine surface + fix #52 #58 #62
2026-06-20 20:42:24 +08:00
carpedkm 0d648b2580 fix: address codex+gpt-5.5 review findings
- harvest: tighten sub-3s filter to also require prompt < 200 chars,
  avoiding false positives on fast real one-shot questions
- openclaw schedule_cmd: add docstring clarifying it schedules the
  shared engine, not the OpenClaw-native runner

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 12:40:34 +00:00
carpedkm 7d36b1d592 fix: address review findings in plugin sync PR
- OpenClaw schedule_cmd: pass project as required positional arg
- OpenClaw schedule_cmd/unschedule_cmd: unpack Tuple[bool, str] return
- OpenClaw schedule_cmd: propagate failure status (return 1 on not ok)
- OpenClaw unschedule_cmd: pass project to avoid silent no-op
- OpenClaw --minute default: 17 (consistent with engine and MCP)
- harvest.py: move datetime import to module level

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 12:04:07 +00:00
carpedkm 0be780052a feat: sync all 4 runtime plugins with full engine surface + fix #52 #58 #62
Bug fixes:
- #52: bundle run-sleep.sh in Claude Code plugin + 4-level fallback
- #58: add skillopt-sleep console script entry point in pyproject.toml
- #62: filter headless claude -p replay sessions from harvest

Plugin sync (Claude Code / Codex / Copilot / OpenClaw):
- Document all 22 CLI flags, 7 actions, 4 backends across all SKILL.md files
- Document config keys (preferences, gate_mode, dream_rollouts, etc.)
- Document memory consolidation (evolve_memory / evolve_skill)
- Add schedule/unschedule to all plugins
- Copilot MCP: expand schema from 3 → 16 params + schedule tools
- OpenClaw: add schedule/unschedule subcommands via shared scheduler

Tests:
- Cross-plugin parity test (prevents future feature drift)
- MCP schema completeness test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-20 11:31:09 +00:00
carpedkm 0b5b9a4296 Merge pull request #60 from Kirchberg/codex/reviewed-task-files-cwd
Add reviewed task-file flow for Codex sleep runs
2026-06-20 08:59:02 +00:00
Kirill Kostarev 05cdc26beb Add reviewed task-file flow for Codex sleep runs 2026-06-20 08:58:48 +00:00
Yifan Yang 382811ddcc Merge pull request #50 from Dongbumlee/Dongbumlee/copilot-sleep-backend
Add Copilot as a SkillOpt-Sleep model backend (CopilotCliBackend) + research-engine MCP plugin
2026-06-20 16:57:53 +08:00
DB Lee d367ae1eea docs(plugins): list copilot in the cross-tool backend overview
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:38:10 -07:00
DB Lee 2c0980bda3 docs(copilot): correct backend hint in research MCP plugin (openai -> azure_openai)
The advertised backend choices in scripts/train.py use 'azure_openai',
not 'openai'; align the inputSchema description hint accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:25:50 -07:00
DB Lee 5799695951 feat(copilot): implement attempt_with_tools with cross-platform tool shims
Adds honest tool-call detection for CopilotCliBackend, mirroring the
Claude/Codex backends. Writes per-tool executable shims into the work dir
and detects real invocations from a calllog (not self-reported markers).
The Copilot backend is Windows-validated, so shims are cross-platform:
a .cmd batch shim on Windows and a chmod'd bash shim on POSIX, with an
OS-specific tool hint. Mirrors _call's flags/env (isolated COPILOT_HOME,
--allow-all-tools, MCP/instruction disabling) and the UTF-8 subprocess fix.

Adds test_attempt_with_tools_honest_detection: a CI-friendly, OS-aware
stub stands in for the CLI, runs the shim, and asserts both JSONL parsing
and log-based detection. Validated live on Windows (real Copilot call) and
on Linux/WSL (POSIX path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:25:50 -07:00
DB Lee 013a7cd83a test: add unit tests for CopilotCliBackend (parsing + alias + isolated home)
Covers _parse_jsonl_response (multi-message concat, junk-line skipping,
empty/non-assistant events), get_backend alias resolution, and the
isolated-COPILOT_HOME / full-env opt-out behavior. Pure logic, no CLI required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:25:50 -07:00
DB Lee 21f93c16c7 Add GitHub Copilot backend to SkillOpt-Sleep
Add CopilotCliBackend that drives the GitHub Copilot CLI in
non-interactive mode (copilot -p ... --output-format json) and parses the
JSONL event stream for assistant.message content. Registered as the
'copilot' backend (with aliases) and wired through the CLI, config,
experiment harness, and the Copilot MCP server's backend enum.

- Force UTF-8 decoding of CLI output (fixes cp1252 UnicodeDecodeError on
  Windows when responses contain non-cp1252 bytes).
- Minimise per-call startup: isolated COPILOT_HOME with built-in MCPs and
  custom instructions disabled, so user MCP servers are not spawned per
  call (~5x faster: 36s -> 7.4s). Override via SKILLOPT_SLEEP_COPILOT_HOME
  / SKILLOPT_SLEEP_COPILOT_MODEL / SKILLOPT_SLEEP_COPILOT_FULL_ENV.

Validated end-to-end on real held-out tasks (researcher persona:
0.42 -> 1.00 lift; gate correctly rejects non-improving edits).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:25:50 -07:00
DB Lee 5dc894715f Add SkillOpt research-engine MCP server plugin for Copilot
Exposes scripts/train.py and scripts/eval_only.py as Copilot MCP tools
(skillopt_list_configs, skillopt_train, skillopt_eval) via a stdlib-only
stdio server, mirroring the existing SkillOpt-Sleep plugin layout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 17:24:00 -07:00
Yifan Yang 6940e46f4e Merge pull request #65 from summerview1997/codex/searchqa-materialize-splits
Add SearchQA split materialization helper
2026-06-17 23:50:38 +08:00
Yifan Yang 0e962219f5 Merge pull request #64 from summerview1997/codex/searchqa-rollout-failfast
Fail fast on systemic SearchQA rollout failures
2026-06-17 23:49:55 +08:00
Yifan Yang fc42e6bf72 Merge pull request #63 from summerview1997/codex/webui-env-backend-preflight
Add WebUI env loading and backend preflight
2026-06-17 23:49:50 +08:00
summerview1997 c755792049 Add SearchQA materialization tests 2026-06-16 09:27:09 +08:00
summerview1997 e591a28242 Add SearchQA split materialization helper 2026-06-16 09:26:56 +08:00
summerview1997 c04467a428 Add SearchQA materialization dependency extra 2026-06-16 09:26:46 +08:00
summerview1997 d5ae8c8e66 Document SearchQA split materialization 2026-06-16 09:26:35 +08:00
summerview1997 923becb00f Add SearchQA rollout fail-fast tests 2026-06-16 09:21:08 +08:00
summerview1997 da799620ba Fail fast on systemic SearchQA rollout failures 2026-06-16 09:20:57 +08:00
summerview1997 30cc8a3ed3 Add WebUI env preflight tests 2026-06-16 09:04:30 +08:00
summerview1997 d05851bd7f Add WebUI env loading and backend preflight 2026-06-16 09:04:19 +08:00
Yifan Yang 46b3207b96 docs(sleep): trim RESULTS to the headline results (remove the full grid)
Remove the per-cell full deployment grid section; keep the gate-safety stress
test, experience-replay scaling + night-by-night climb, the dream-diversity
ablation, the gbrain end-to-end result, and the scope/limitations. Renumber
sections; update the README pointer accordingly.
2026-06-15 17:08:51 +00:00
Yifan Yang d43e8dba1a docs(sleep): expand the grid into per-benchmark night-by-night tables
Replace the compact baseline->after grid with three grouped per-benchmark tables
(SearchQA / LiveMath / SpreadsheetBench), each showing all 3 targets x both modes
across every night (N0..N5) + Δ. Makes the trajectory visible — gains reach a
level and hold rather than being single lucky readings — and presents the full
18-cell evidence in a more solid, readable form. Footnotes LiveMath's 4-night run
(train split <50 tasks). Numbers unchanged; just richer presentation.
2026-06-15 16:54:01 +00:00
Yifan Yang d02098ffc4 docs(sleep): add full Results & Analysis (RESULTS.md); link from README
Adds docs/sleep/RESULTS.md — the complete deployment-scale study behind
SkillOpt-Sleep, presented rigorously (named benchmarks, test sizes, metrics,
baseline->after, single shared protocol):
  1. Gate-safety stress test: ungated nano SearchQA collapses 0.554->0.026
     (-52.8); the gated twin holds 0.570 — the core argument for the design.
  2. Full 18-cell deployment grid (3 benchmarks x 3 targets x gate/free),
     shipped config: mean +0.5, range [-2.4, +5.1], nothing hidden.
  3. Experience-replay scaling (recall_k 10->20->full: +3.1->+4.5->+5.6) and
     the night-by-night climb (0.798->...->0.858, gate accepts as late as N5).
  4. Dream-diversity fix as defense-in-depth: 3-config grid comparison
     (-2.66/-52.8 -> +0.24/-4.0 -> +0.53/-2.4); the -52.8 cell becomes +2.7
     from the dream fix alone.
  5. gbrain end-to-end 0.00->1.00 on real Claude + Codex.
  6. Honest scope: where it helps vs flat-in-noise, single-seed caveat with a
     seed-robustness spot check, keep-the-gate-on.
README Results section now links prominently to it. Docs only; numbers are
self-contained with reproduce commands (no raw run dumps committed).
2026-06-15 16:49:13 +00:00
Yifan Yang ea4ff459d7 docs(sleep): make the results section rigorous (named benchmarks, baseline→after)
Label each result with its benchmark, test size, metric, target model, and gate
mode; show absolute baseline→after (not just Δ); state the single shared protocol
once. SearchQA recall-scaling table (1400-item test, SQuAD-EM, GPT-5.5, gated) +
SpreadsheetBench confirmation (280-item, cell-value compare, nano, gate-free) +
the gbrain end-to-end line. Keeps the single-seed / flat-on-noisy caveats.
2026-06-15 16:42:43 +00:00
Yifan Yang de3be75bac docs(sleep): add a SkillOpt-Sleep module readme + News mention
Adds docs/sleep/README.md — a concise intro to the SkillOpt-Sleep plugin (what
it is, how to use it across the three agents, the opt-in experience-replay /
dream-rollout knobs, and headline results), linking to the full guide section.
Adds a News bullet pointing to it. No code changes.
2026-06-15 16:31:15 +00:00
Yifan Yang b701d9b6d9 docs: move SkillOpt-Sleep into the guide; clean docs/sleep; fix guide link
Per maintainer request:
- Remove the internal/scratch docs/sleep/ tree (reports, raw logs, blog run
  JSON, sweep.jsonl) — 23 files — and the root PUBLISHING.md. These were
  working notes, not reference docs.
- Take the dedicated SkillOpt-Sleep content out of the main README (News bullet
  + section) and host it in the rendered guide instead: new section 9 in
  docs/guideline.html (deployment companion, the three plugins, opt-in
  experience replay / dream rollouts) with a sidebar entry.
- Fix the README's opening reference so "Documentation & Reproduction Guide"
  links directly to the rendered GitHub Pages page, not the raw .html source.
- Repoint the now-removed docs/sleep links in the plugin READMEs to the guide
  section.

The plugin code (plugins/, skillopt_sleep/) is unchanged; only docs move.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-06-15 16:20:50 +00:00
Yifan Yang 722ce646d4 feat(sleep): experience replay + dream rollouts in the cycle (opt-in)
Wires two consolidation mechanisms into the shipped nightly cycle, both default
OFF so existing behavior is unchanged:
  - dream_rollouts (>1): multi-rollout contrastive reflection per task
  - recall_k (>0): associative recall of the K most-similar past tasks (from a
    capped task_archive persisted in state.json) into tonight's dream
  - dream_factor (>0): synthetic task variants

New shared engine module skillopt_sleep/dream.py (recall_similar, dream_augment,
dream_consolidate) is called by both the plugin cycle and the experiment harness,
so reported numbers exercise the exact shipped code. Built on the existing
rollouts_k/sample_id support already in consolidate.py/rollout.py.

Validated (5 nights x 10 real tasks/night, full held-out test, GPT-5.5, gated):
the gain scales with recall depth on a clean signal —
SearchQA recall_k=10 +3.1, recall_k=20 +4.5, full-history reference +5.6;
SpreadsheetBench (nano, gate-free) +3.6. Flat within noise on saturated/noisy
cells. See docs/sleep/EXPERIENCE_REPLAY.md (+ raw runs under blog_runs/v2_port/).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-06-15 15:58:27 +00:00
Yifan Yang 576f2f8bad Merge pull request #59 from Elzlxx/feat/openclaw-skillopt-sleep
feat(plugins): add OpenClaw shell for SkillOpt-Sleep
2026-06-15 18:26:12 +08:00
carpedkm 00d07bc59a Merge pull request #48 from Kirchberg/codex/codex-desktop-harvest
Add Codex Desktop transcript harvesting
2026-06-15 10:23:18 +00:00
Kirill Kostarev 31715a8b43 Add Codex Desktop transcript harvesting 2026-06-15 10:23:08 +00:00
carpedkm e8c3e10b30 Merge pull request #49 from Kirchberg/codex/codex-skill-first-upstream
Make Codex integration skill-first
2026-06-15 10:21:43 +00:00
Kirill Kostarev d31e9d9407 Back up legacy Codex prompt during install 2026-06-15 10:21:30 +00:00
Kirill Kostarev 1953484822 Make Codex integration skill-first 2026-06-15 10:21:30 +00:00
carpedkm 1b2652c6f8 Merge pull request #44 from imshunsuke/refactor/reflect-default-base
refactor: make EnvAdapter.reflect a shared default (fixes dropped reflect kwargs)
2026-06-15 09:06:38 +00:00
elzlxx 553446575a feat(plugins): add OpenClaw shell for SkillOpt-Sleep
Adds a thin OpenClaw shell wrapping the SkillOpt-Sleep engine. Enables
nightly validation-gated skill improvement cycles for OpenClaw agents.

Components:
- skillopt_sleep_openclaw.py: DeepSeek V4 Pro + Ollama nomic-embed-text
  backend, mirroring the Claude/Codex/Copilot backend pattern.
- run_sleep.py: CLI entry point supporting dry-run and pre-built task files.
- run_sleep_cron.sh: bash wrapper for nightly cron invocation.
- slash_sleep.py: /sleep command (status / run / adopt / reject / cost).
- config.json: engine config tuned for our stack.
- SKILL.md: OpenClaw skill manifest.
- tests/: 14 held-out tasks across 3 categories (research-cron, devops, wiki).

OpenClaw is the 4th ecosystem in which SkillOpt-Sleep can be deployed,
joining Claude Code, Codex, and Copilot. The shell follows the same
single-engine / thin-shell pattern as the existing three plugins.

End-to-end tested: pipeline runs against real OpenClaw session transcripts,
gate correctly rejects non-improvements, staging artifacts land in
~/.skillopt-sleep/staging/<night>/. Cost: ~$0.02/night on DeepSeek V4 Pro.
2026-06-14 23:27:54 +08:00
156 changed files with 16131 additions and 3250 deletions
+30 -4
View File
@@ -8,6 +8,7 @@ export AZURE_OPENAI_API_VERSION=2024-12-01-preview
# Authentication: choose one method
# Option 1: API Key
export AZURE_OPENAI_API_KEY=
export AZURE_OPENAI_AUTH_MODE=api_key
# Option 2: Azure CLI (no API key needed, recommended on Azure VMs)
# export AZURE_OPENAI_AUTH_MODE=azure_cli
# Option 3: Managed Identity
@@ -15,20 +16,45 @@ export AZURE_OPENAI_API_KEY=
# export AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID=your-client-id
# ── OpenAI-compatible endpoints ──────────────────────────────────────
# Set AUTH_MODE to openai_compatible and reuse AZURE_OPENAI_ENDPOINT / _API_KEY.
# The plain OpenAI client is used; no Azure auth, no api-version header.
# Path 1: generic research backend. Select openai_compatible explicitly as
# model.optimizer_backend and/or model.target_backend.
# export OPENAI_COMPATIBLE_BASE_URL=https://api.deepseek.com/v1
# export OPENAI_COMPATIBLE_API_KEY=sk-...
# export OPENAI_COMPATIBLE_MODEL=deepseek-chat
# Per-role overrides use OPTIMIZER_OPENAI_COMPATIBLE_* and
# TARGET_OPENAI_COMPATIBLE_* (BASE_URL, API_KEY, MODEL, TEMPERATURE,
# MAX_TOKENS, TIMEOUT_SECONDS).
# For scripts/train.py and scripts/eval_only.py, also set model.optimizer and
# model.target in YAML (or via --cfg-options). Those role model values are
# applied after backend initialization and override *_MODEL environment values.
# Path 2: research openai_chat compatibility mode. This reuses the Azure-family
# variables but creates a plain OpenAI client (no Azure auth or api-version).
# export AZURE_OPENAI_ENDPOINT=https://api.openai.com/v1
# export AZURE_OPENAI_API_KEY=sk-...
# export AZURE_OPENAI_AUTH_MODE=openai_compatible
# ── Anthropic / Claude (for claude_chat backend) ─────────────────────
# Path 3: SkillOpt-Sleep. `skillopt-sleep run --backend azure_openai` uses the
# same three AZURE_* variables from path 2. Optional Sleep-only controls:
# export SKILLOPT_SLEEP_COMPAT_MAX_TOKENS=8192
# export SKILLOPT_SLEEP_CHAT_EXTRA_BODY='{"provider_option": true}'
# ── Claude Code CLI (for claude_chat backend) ─────────────────────────
# Install and authenticate the `claude` CLI before use. For a non-default path:
# export CLAUDE_CLI_BIN=/path/to/claude
# ANTHROPIC_API_KEY is one authentication option understood by the CLI; SkillOpt
# does not create a direct Anthropic API client for this backend.
# export ANTHROPIC_API_KEY=sk-ant-...
# ── Qwen Local Model (for qwen_chat backend) ────────────────────────
# export QWEN_CHAT_BASE_URL=http://localhost:8000/v1
# export QWEN_CHAT_MODEL=Qwen/Qwen3.5-4B
# The train/eval entry points likewise override this model with
# model.optimizer/model.target for the selected Qwen roles.
# ── MiniMax (for minimax_chat backend) ──────────────────────────────
# export MINIMAX_BASE_URL=https://api.minimax.io/v1
# export MINIMAX_API_KEY=...
# export MINIMAX_MODEL=MiniMax-M2.7
# When MiniMax is the target, set model.minimax_model in YAML. The current
# adapter shares one deployment across MiniMax roles; mixed-backend runs cannot
# independently select a MiniMax optimizer model and a different target model.
+5
View File
@@ -22,6 +22,11 @@ data/*
outputs/
logs/
external/
# SkillOpt-Sleep runtime state (staging proposals, config, diagnostics, cron logs)
.skillopt-sleep/
# SkillOpt-Sleep handoff-backend round data (prompts/answers derived from transcripts)
.skillopt-sleep-handoff/
.skillopt-sleep-handoff.night*.done/
/BabyVision/
/MMRB/
+171
View File
@@ -0,0 +1,171 @@
# Changelog
All notable changes to SkillOpt are documented here. This project adheres to
[Semantic Versioning](https://semver.org/) and the format is based on
[Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **Handoff backend** (`--backend handoff`) for SkillOpt-Sleep — runs the
sleep cycle with no model subprocess or API key: the engine writes each
pending model call to `PROMPTS.md`/`pending.json` (exit code 3) and the
user's own agent session answers into `answers/<id>.md`; re-running the
same command resumes statelessly from the answers (typically 36 rounds
per night). Mined tasks are pinned per night so answering sessions cannot
shift the task set. Ships a `/skillopt-sleep-handoff` Claude Code command
that automates the loop with fresh-context subagents to protect the
held-out gate (thanks @dimitarvdenev, #125).
- **Generic OpenAI-compatible research backend** for optimizer and target
calls, with configurable base URL, API key, model, and timeout (thanks
@nankingjing, #115).
- **OpenAI-compatible SkillOpt-Sleep endpoint support** for providers such as
DeepSeek and self-hosted vLLM servers (thanks @Alphaxalchemy, #129; hardened
in #138).
- End-to-end wiring for the documented reflection `--preferences` option
(thanks @AKhozya, #131).
### Changed
- Claude Code's Sleep plugin can now use a `pip`/`uv`-installed
`skillopt-sleep` when no repository checkout is present (thanks
@ichoosetoaccept, #107).
- Qwen reasoning-model requests now use `max_completion_tokens` and omit
unsupported temperature parameters (thanks @chirag127, #128).
- Configuration files are read explicitly as UTF-8 (thanks @nankingjing,
#124).
### Fixed
- Preserve fractional rollout hard scores instead of coercing them to binary
values (thanks @zixuanguo786-ctrl, #104).
- Reject duplicate and overlapping IDs while materializing SearchQA manifests
(thanks @zixuanguo786-ctrl, #105).
- Make JSON-array extraction robust to unmatched braces and keep malformed
scans linear-time (thanks @zixuanguo786-ctrl, #103; follow-up #136).
- Package Markdown prompt assets in wheels and tolerate Windows temporary-file
cleanup failures (thanks @nankingjing, #135; follow-up #137).
- Exclude sub-agent transcripts and plugin-generated sessions from Sleep task
mining (thanks @codeL1985, #99).
- Normalize validation-gate density against the proposed edits and handle
zero-edit candidates safely (thanks @SparshGarg999, #102).
- Route optimizer-role MiniMax calls through the MiniMax backend (thanks
@jcforever1, #116).
- Surface Claude CLI spawn failures instead of silently turning them into zero
scores (thanks @Phoenix0531-sudo, #126).
- Improve Claude CLI behavior on Windows, including `.cmd` resolution and
long-prompt handling (thanks @codeL1985, #98).
- Preserve the scheduler's established annealing contract while expanding its
endpoint and sequence coverage (thanks @nankingjing, #123; follow-up #133).
### Security
- Prevent managed-identity credentials from being sent to non-Azure or
non-HTTPS endpoints, and isolate compatible-provider request extensions
from native Azure mode in SkillOpt-Sleep (#138, following
@Alphaxalchemy's #129).
### Tests
- Strengthen SkillOpt-Sleep verifier-discipline assertions, including recorded
scores and gate actions (thanks @Tanmay9223, #96).
- Add focused coverage for the validation-gate decision core and edit-budget
schedulers (thanks @nankingjing, #122, #123).
### Acknowledgements 🙏
Thank you to the contributors behind this unreleased work:
@AKhozya, @Alphaxalchemy, @Phoenix0531-sudo, @SparshGarg999,
@Tanmay9223, @chirag127, @codeL1985, @dimitarvdenev,
@ichoosetoaccept, @jcforever1, @nankingjing, and
@zixuanguo786-ctrl.
## [0.2.0] — 2026-07-02
The headline of this release is **SkillOpt-Sleep**: a nightly offline
self-evolution engine that harvests a coding agent's real session
transcripts, mines recurring tasks, replays them offline, and consolidates
short-term experience into long-term memory and skills — all behind the same
held-out validation gate that keeps SkillOpt training honest. It ships as a
decoupled top-level package (`skillopt_sleep/`, zero dependency on the
research code) and as the new `skillopt-sleep` CLI.
### Added
- **SkillOpt-Sleep engine** — nightly offline self-evolution cycle
(harvest → mine → replay → consolidate) behind a validation gate, exposed
as the `skillopt-sleep` console script and `python -m skillopt_sleep`.
- Multi-objective reward (accuracy / tokens / latency) with user preferences.
- Multi-rollout contrastive reflection under a token/time budget.
- Experience replay + controllable dream rollouts (opt-in).
- Slow-update long-term memory field (runs even with the gate off).
- 3-way train/val/test split with `gate_mode on|off`.
- Verifier-discipline validation gate, with a stress-test suite
(thanks @Tanmay9223, #87).
- **Cross-tool backends & plugin shells** for Claude Code, Codex, Copilot,
Devin, and OpenClaw:
- Codex Desktop transcript harvesting, skill-first Codex integration, and a
reviewed task-file flow (thanks @Kirchberg, #48, #49, #60).
- GitHub Copilot backend (`CopilotCliBackend`) + research-engine MCP plugin
(thanks @Dongbumlee, #50).
- Devin plugin: MCP server + ATIF-v1.7 harvest (thanks @xerxes-y, #88).
- OpenClaw shell for SkillOpt-Sleep (thanks @Elzlxx, #59).
- **SearchQA** split materialization helper and fail-fast on systemic rollout
failures, with a `searchqa` install extra (thanks @summerview1997,
#63, #64, #65).
- WebUI environment loading and backend preflight (thanks @summerview1997, #63).
### Changed
- Decoupled the Sleep engine into a standalone top-level `skillopt_sleep/`
package with zero dependency on the research code.
- Made `EnvAdapter.reflect` a shared default so reflect kwargs are no longer
dropped (thanks @imshunsuke, #44).
- English-only pass across the engine, plugins, and docs.
### Fixed
- Windows robustness for the Claude/Codex backends, plus a hardened JSON
fallback path (thanks @Yif-Yang, #79).
- Reject prose pseudo-JSON wrapped in single quotes/backticks (#82).
- Surface Codex auth/model/version failures instead of silently scoring 0
(thanks @dmmdea, #92).
- Redact secrets before persisting cycle diagnostics.
- Configure the `qwen_chat`/`minimax` backends so local LLM endpoints work
(thanks @imrehg, #85).
- Forward the Qwen target timeout and gate `enable_thinking` for vLLM targets
(thanks @mvanhorn, #40).
- Make `--bare` conditional on `ANTHROPIC_API_KEY` (#68), add a
`SKILLOPT_SLEEP_PYTHON` override with a lookback-hours first-run fallback
(#74), and fix ALFWorld gamefile paths relative to `ALFWORLD_DATA`.
### Packaging
- Bump `skillopt`, `skillopt.__version__`, and `skillopt_sleep.__version__`
to `0.2.0`.
- Restore `skillopt_webui` to the built wheel (it was dropped when the
`packages.find` include list was made explicit).
- Add the `searchqa` extra and include `json_repair` in the `claude`, `qwen`,
and `all` extras.
### Acknowledgements 🙏
v0.2.0 landed thanks to our community contributors — thank you!
- @Kirchberg — Codex Desktop harvesting, skill-first Codex integration,
reviewed task-file flow (#48, #49, #60)
- @Dongbumlee — GitHub Copilot backend + research-engine MCP plugin (#50)
- @summerview1997 — SearchQA materialization, rollout fail-fast, WebUI
preflight (#63, #64, #65)
- @xerxes-y — Devin plugin: MCP server + ATIF-v1.7 harvest (#88)
- @Elzlxx — OpenClaw shell for SkillOpt-Sleep (#59)
- @imshunsuke — shared `EnvAdapter.reflect` default + docs fixes (#43, #44)
- @mvanhorn — Qwen timeout forwarding + `enable_thinking` gating (#40)
- @dmmdea — surface Codex auth/model/version failures (#92)
- @Tanmay9223 — verifier-discipline stress test (#87)
- @imrehg`configure_qwen_chat` for local LLM endpoints (#85)
- @samuelgoofus-boop — community contributions
Special thanks to @Yif-Yang for driving the SkillOpt-Sleep engine.
**Full changelog:** https://github.com/microsoft/SkillOpt/compare/v0.1.0...v0.2.0
## [0.1.0] — 2026-06-02
Initial public release: the full training loop (rollout → reflect →
aggregate → select → update → evaluate), multi-backend support
(OpenAI / Azure / Claude / Qwen / MiniMax), six built-in benchmarks, and the
WebUI dashboard.
[0.2.0]: https://github.com/microsoft/SkillOpt/releases/tag/v0.2.0
[0.1.0]: https://github.com/microsoft/SkillOpt/releases/tag/v0.1.0
+12 -7
View File
@@ -7,7 +7,7 @@ Thank you for your interest in contributing! SkillOpt welcomes contributions of
```bash
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
pip install -e ".[dev]"
python -m pip install -e ".[dev,docs]"
```
## How to Contribute
@@ -16,23 +16,28 @@ pip install -e ".[dev]"
Open a GitHub issue with reproduction steps, expected/actual behavior, and your config file (remove API keys).
### 🔧 Add a Benchmark
See the [guide](docs/guide/new-benchmark.md) and use the scaffold at `skillopt/envs/_template/`.
See the [guide](docs/guide/new-benchmark.md) and use the scaffold at
`skillopt/envs/_template/`. Register the adapter lazily in both
`scripts/train.py` and `scripts/eval_only.py`, and add focused tests.
### 🤖 Add a Model Backend
See the [guide](docs/guide/new-backend.md).
First check whether the built-in `openai_compatible` backend covers the
provider. Otherwise follow the function-based backend contract in the
[backend guide](docs/guide/new-backend.md), including routing, configuration,
token accounting, and no-network tests.
### 📝 Improve Documentation
```bash
pip install -e ".[docs]"
mkdocs serve # Preview at http://localhost:8000
python -m mkdocs serve # Preview at http://localhost:8000
```
## Pull Request Process
1. Fork the repo and create a feature branch
2. Make changes and test with an existing benchmark
2. Make changes and run focused tests plus `python -m pytest -q`
3. Submit a PR with a clear description
4. Ensure CI passes
4. For documentation changes, run `python -m mkdocs build --strict`
5. Ensure CI passes
## Code Style
- Follow existing patterns in the codebase
-81
View File
@@ -1,81 +0,0 @@
# Publishing SkillOpt-Sleep — how people install and use it
This is the open-source SkillOpt-Sleep tool: a nightly offline "sleep cycle" for
local coding agents, shipped as plugins for **Claude Code**, **Codex**, and
**Copilot**. One engine ([`skillopt_sleep/`](skillopt_sleep)), three thin shells
([`plugins/`](plugins)), decoupled from the research code.
## How end users install it
### Claude Code
The Claude Code plugin ships a marketplace manifest at
`plugins/claude-code/.claude-plugin/marketplace.json`.
```text
# inside Claude Code:
/plugin marketplace add microsoft/SkillOpt
/plugin install skillopt-sleep
/sleep status
```
(`/plugin marketplace add <owner>/<repo>` reads the marketplace manifest from the
repo; the entry points at `plugins/claude-code`.)
### Codex
```bash
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
bash plugins/codex/install.sh # installs /sleep prompt + skill
export SKILLOPT_SLEEP_REPO="$(pwd)" # so the runner is found anywhere
# then, in Codex: /sleep status
```
### Copilot
```bash
git clone https://github.com/microsoft/SkillOpt.git
# register the MCP server with your Copilot config (see plugins/copilot/README.md
# and plugins/copilot/mcp-config.example.json), pointing SKILLOPT_SLEEP_REPO at
# the clone. Then ask Copilot to "run the sleep cycle".
```
Requirements for all three: Python ≥ 3.10, and the corresponding agent CLI on
PATH. The default backend is `mock` (no API spend); `--backend claude|codex`
uses the user's own budget.
## Wider distribution (optional, maintainer steps)
1. **GitHub Release.** Tag the milestone so users can pin a version:
```bash
gh release create sleep-v0.1.0 --title "SkillOpt-Sleep v0.1.0" \
--notes "Nightly offline self-evolution plugins for Claude Code, Codex, Copilot."
```
2. **Official Claude Code plugin marketplace.** To appear in the public
directory, open a PR adding a `marketplace.json` entry to
[`anthropics/claude-code` / the official marketplace repo], pointing at
`microsoft/SkillOpt` subdir `plugins/claude-code`. Users could then
`/plugin install skillopt-sleep@<official-marketplace>`.
3. **PyPI (optional).** `skillopt_sleep` is a standalone package
(`pyproject.toml` lists it). A `pip install skillopt-sleep` distribution would
let users run `python -m skillopt_sleep ...` without cloning. Build with
`python -m build` and publish with `twine`.
4. **README News.** The main [`README.md`](README.md) already announces the
release and links to [`plugins/`](plugins) and
[`docs/sleep/FINAL_REPORT.md`](docs/sleep/FINAL_REPORT.md).
## Verifying a release works
```bash
# deterministic, no API key:
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
# the unit suite:
python -m unittest tests.test_sleep_engine
# the MCP server (Copilot):
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| SKILLOPT_SLEEP_REPO="$(pwd)" python3 plugins/copilot/mcp_server.py
```
+25 -64
View File
@@ -4,12 +4,18 @@
[![Project Page](https://img.shields.io/badge/Project%20Page-SkillOpt-8dbb3c)](https://microsoft.github.io/SkillOpt/) [![Paper](https://img.shields.io/badge/Paper-arXiv-b31b1b)](https://arxiv.org/abs/2605.23904) [![Project Video](https://img.shields.io/badge/Project%20Video-Watch%20Demo-ff0000)](https://youtu.be/JUBMDTCiM0M) [![PyPI](https://img.shields.io/badge/PyPI-skillopt-green.svg)](https://pypi.org/project/skillopt/) [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
> 📖 **For installation, data preparation, training/eval commands, the full configuration reference, and framework internals, see the [Documentation & Reproduction Guide](docs/guideline.html)** — view it [rendered online](https://htmlpreview.github.io/?https://github.com/microsoft/SkillOpt/blob/main/docs/guideline.html) or via [GitHub Pages](https://microsoft.github.io/SkillOpt/docs/guideline.html).
<p align="center">
<a href="https://trendshift.io/repositories/38498?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-38498" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/38498/daily?language=Python" alt="microsoft%2FSkillOpt | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/38498?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-38498" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/38498/weekly?language=Python" alt="microsoft%2FSkillOpt | Trendshift" width="250" height="55"/></a>
</p>
> 📖 **For installation, data preparation, training/eval commands, configuration, and framework internals, start with the versioned [SkillOpt documentation](https://github.com/microsoft/SkillOpt/blob/main/docs/index.md). A concise rendered overview is available in the [Documentation & Reproduction Guide](https://microsoft.github.io/SkillOpt/docs/guideline.html), and longer-form engineering analysis appears on the [Technical Blog](https://microsoft.github.io/SkillOpt/blog/). We also maintain a [Changelog](CHANGELOG.md) for released and unreleased changes.**
---
## News 🔥🔥🔥
- **[2026-06-14]** 😴 **SkillOpt-Sleep (preview).** A nightly *sleep cycle* for local coding agents (Claude Code / Codex / Copilot): review past sessions offline, replay recurring tasks, and consolidate validated skills behind a held-out gate. This is an early **preview** — open-source and decoupled from the paper code — that we'll keep iterating on. See [`plugins/`](plugins/) and the [section below](#-skillopt-sleep--the-deployment-time-companion).
- **[2026-07-02]** 🚀 **SkillOpt [v0.2.0](https://github.com/microsoft/SkillOpt/releases/tag/v0.2.0) is out on [PyPI](https://pypi.org/project/skillopt/)!** Headline feature: **SkillOpt-Sleep**, a nightly offline self-evolution engine (harvest → mine → replay → consolidate behind a held-out validation gate), now shipped as the `skillopt-sleep` CLI. It also includes experimental multi-objective, replay, and dream-rollout controls; the main CLI keeps conservative defaults and does not expose every experiment-harness control as a flag. The release source adds integration shells for **Claude Code, Codex, Copilot, and Devin**, plus an **OpenClaw reference adaptation**; these plugin/MCP files live in the repository rather than the PyPI wheel. It also adds SearchQA split materialization, Windows robustness, and hardened JSON parsing. See the [release notes](https://github.com/microsoft/SkillOpt/releases/tag/v0.2.0) for full release details and contributor acknowledgements.
- **[2026-06-15]** 😴 **SkillOpt-Sleep (preview)** — a nightly offline self-evolution companion for local coding agents (Claude Code / Codex / Copilot): review past sessions, replay recurring tasks, and consolidate validated skills behind a held-out gate. See **[`docs/sleep/README.md`](docs/sleep/README.md)** for what it is, how to use it, and results.
- **[2026-06-03]** 🎉 **[gbrain](https://github.com/garrytan/gbrain), [gbrain-evals](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-06-03-skillopt.md), and [darwin-skill](https://github.com/alchaincyf/darwin-skill) have all integrated SkillOpt.**
- **[2026-06-02]** 🎉 **SkillOpt [v0.1.0](https://github.com/microsoft/SkillOpt/releases/tag/v0.1.0) is now available on [PyPI](https://pypi.org/project/skillopt/)!** Install with `pip install skillopt`. This initial release includes the full training loop (rollout → reflect → aggregate → select → update → evaluate), multi-backend support (OpenAI / Azure / Claude / Qwen / MiniMax), six built-in benchmarks, and WebUI dashboard.
@@ -25,9 +31,9 @@ which reliably improves over its starting point under feedback.
**SkillOpt treats the skill document as the trainable state of a frozen
agent**, and trains it with the discipline that makes weight-space
optimization reproducible. A separate optimizer model turns scored rollouts
into bounded add / delete / replace edits on a single skill document; a
candidate edit is accepted only when it strictly improves a held-out
validation score. A textual learning-rate budget, a rejected-edit buffer,
into bounded add / delete / replace edits on a single skill document; in the
default paper-style path, a candidate edit is accepted only when it strictly
improves a held-out validation score. 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.
@@ -53,60 +59,14 @@ https://github.com/user-attachments/assets/eb12d3bc-371c-467f-904d-91b61f339ed7
---
## 😴 SkillOpt-Sleep — the deployment-time companion
> **Preview.** SkillOpt-Sleep is an early preview that we are actively iterating
> on; interfaces and defaults may change. Feedback and issues are welcome.
SkillOpt (above) trains a skill offline on a benchmark. **SkillOpt-Sleep**
applies the same discipline to *your own daily usage*: it gives a local coding
agent a nightly **sleep cycle** that reviews your past sessions, replays your
recurring tasks on your own API budget, and consolidates what it learns into
**validated** long-term memory and skills — behind a held-out gate, staged for
your review. The agent gets better the more you use it, with no weight training.
It synthesizes **SkillOpt** (validation-gated bounded text edits), **Claude
Dreams** (offline consolidation; review-then-adopt), and the **agent sleep**
idea (short-term experience → long-term competence). One "night":
```
harvest session transcripts → mine recurring tasks → replay offline
→ consolidate (reflect → bounded edit → GATE on real held-out tasks)
→ stage proposal → (you) adopt
```
**Plugins for three agents** (one engine, three thin shells — see [`plugins/`](plugins/)):
| Platform | Folder | Install |
|---|---|---|
| **Claude Code** | [`plugins/claude-code`](plugins/claude-code) | `/plugin marketplace add ./plugins/claude-code``/skillopt-sleep` |
| **Codex** | [`plugins/codex`](plugins/codex) | `bash plugins/codex/install.sh``/skillopt-sleep` |
| **Copilot** | [`plugins/copilot`](plugins/copilot) | register `plugins/copilot/mcp_server.py` as an MCP server |
**Validated on real models.** On the public
[gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1` benchmark,
deficient skills go **0.00 → 1.00** on held-out sets with **both Claude and
Codex** (all 4 seeds, including a real tool-use loop), cross-model transfer is
positive, and the gate blocks regressions
([full results](docs/sleep/FINAL_REPORT.md)).
> **Open-source tool, decoupled from the research.** The engine lives in the
> top-level [`skillopt_sleep/`](skillopt_sleep) package with **zero dependency**
> on the paper's `skillopt/` experiment code (the validation gate is vendored).
> Controls — optional gate, multi-rollout contrastive reflection, token/time
> budget, multi-objective reward, user preferences, optimizer/target split — are
> documented in [`docs/sleep/CONTROLLABLE_DREAMING.md`](docs/sleep/CONTROLLABLE_DREAMING.md).
Deterministic proof (no API key): `python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves`.
---
## Extensibility & WebUI
### Adding a new backend
A backend = a chat / exec target (e.g. `openai_chat`, `claude_chat`,
`qwen_chat`, `minimax_chat`, `codex_exec`, `claude_code_exec`). See
`qwen_chat`, `minimax_chat`, `openai_compatible`, `codex_exec`,
`claude_code_exec`). If a provider implements the OpenAI Chat Completions
protocol, try the built-in `openai_compatible` backend before adding code. See
[`docs/guide/new-backend.md`](docs/guide/new-backend.md) for the full
contract; in short you add a `skillopt/model/<name>_backend.py` module,
register it in `skillopt/model/common.py` + `backend_config.py`, and wire
@@ -115,8 +75,9 @@ and `minimax_backend.py` are good templates.
### Adding a new benchmark
A benchmark = a `skillopt/envs/<name>/` package with a `dataloader.py`, a
`rollout.py`, and an `initial.md` seed skill. See
A benchmark = a `skillopt/envs/<name>/` package with an adapter, a data loader,
a scored rollout helper, a YAML config, and optionally an initial seed skill.
See
[`docs/guide/new-benchmark.md`](docs/guide/new-benchmark.md) for the full
contract; the simplest reference is `skillopt/envs/searchqa/`.
@@ -135,18 +96,18 @@ python -m skillopt_webui.app
| `--host` | `0.0.0.0` | Bind address |
| `--share` | off | Create a public Gradio share link |
The default host listens on every network interface. Use
`--host 127.0.0.1` for local-only access.
---
## Citation
```bibtex
@misc{yang2026skilloptexecutivestrategyselfevolving,
title={SkillOpt: Executive Strategy for Self-Evolving Agent Skills},
author={Yifan Yang and Ziyang Gong and Weiquan Huang and Qihao Yang and Ziwei Zhou and Zisu Huang and Yan Li and Xuemei Gao and Qi Dai and Bei Liu and Kai Qiu and Yuqing Yang and Dongdong Chen and Xue Yang and Chong Luo},
year={2026},
eprint={2605.23904},
archivePrefix={arXiv},
primaryClass={cs.AI},
url={https://arxiv.org/abs/2605.23904}
@article{yang2026skillopt,
title={Skillopt: Executive strategy for self-evolving agent skills},
author={Yang, Yifan and Gong, Ziyang and Huang, Weiquan and Yang, Qihao and Zhou, Ziwei and Huang, Zisu and Li, Yan and Gao, Xuemei and Dai, Qi and Liu, Bei and others},
journal={arXiv preprint arXiv:2605.23904},
year={2026}
}
```
File diff suppressed because it is too large Load Diff
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SkillOpt Technical Blog</title>
<meta name="description" content="Technical notes from the SkillOpt team on skill optimization, evaluation, safety, and agent learning systems.">
<link rel="canonical" href="https://microsoft.github.io/SkillOpt/blog/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="SkillOpt">
<meta property="og:title" content="SkillOpt Technical Blog">
<meta property="og:description" content="Technical notes on skill optimization, evaluation, safety, and agent learning systems.">
<meta property="og:url" content="https://microsoft.github.io/SkillOpt/blog/">
<meta property="og:image" content="https://microsoft.github.io/SkillOpt/skillopt-assets/teaser-1.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="SkillOpt Technical Blog">
<meta name="twitter:description" content="Technical notes on skill optimization, evaluation, safety, and agent learning systems.">
<meta name="twitter:image" content="https://microsoft.github.io/SkillOpt/skillopt-assets/teaser-1.png">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='16' fill='%23245fc7'/%3E%3Ctext x='50' y='68' text-anchor='middle' font-size='58' font-family='Arial' font-weight='700' fill='white'%3ES%3C/text%3E%3C/svg%3E">
<style>
:root {
--ink: #172033;
--muted: #596579;
--line: #d9deea;
--paper: #fff;
--wash: #f7f9fc;
--blue: #245fc7;
--blue-soft: #eaf1ff;
--shadow: 0 14px 44px rgba(23, 32, 51, .08);
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--ink);
background: var(--wash);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.6;
}
a { color: var(--blue); }
.skip-link {
position: fixed;
left: 16px;
top: -80px;
z-index: 100;
padding: 10px 14px;
border-radius: 8px;
color: #fff;
background: var(--blue);
}
.skip-link:focus { top: 12px; }
header {
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, .95);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
max-width: 1040px;
margin: 0 auto;
padding: 16px 24px;
}
.brand {
color: var(--ink);
font-weight: 750;
text-decoration: none;
}
nav { display: flex; flex-wrap: wrap; gap: 16px; font-size: 14px; }
nav a { color: var(--muted); text-decoration: none; }
nav a:hover, nav a:focus-visible { color: var(--blue); text-decoration: underline; }
main { max-width: 1040px; margin: 0 auto; padding: 72px 24px 96px; }
.eyebrow {
color: var(--blue);
font-size: 13px;
font-weight: 750;
letter-spacing: .08em;
text-transform: uppercase;
}
h1 { margin: 10px 0 16px; font-size: clamp(40px, 7vw, 68px); line-height: 1.02; }
.intro { max-width: 760px; color: var(--muted); font-size: 20px; }
.post-list { margin-top: 52px; }
.post-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 30px;
padding: 34px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--paper);
box-shadow: var(--shadow);
}
.post-card h2 { margin: 8px 0 12px; font-size: clamp(25px, 4vw, 36px); line-height: 1.15; }
.post-card h2 a { color: var(--ink); text-decoration: none; }
.post-card h2 a:hover, .post-card h2 a:focus-visible { color: var(--blue); text-decoration: underline; }
.meta { color: var(--muted); font-size: 14px; }
.summary { max-width: 720px; margin: 16px 0 0; color: #344054; }
.read-link { align-self: center; white-space: nowrap; font-weight: 700; }
footer {
max-width: 1040px;
margin: 0 auto;
padding: 26px 24px 42px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 14px;
}
@media (max-width: 720px) {
.header-inner { align-items: flex-start; flex-direction: column; }
main { padding-top: 48px; }
.post-card { grid-template-columns: 1fr; padding: 24px; }
.read-link { justify-self: start; }
}
</style>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
<header>
<div class="header-inner">
<a class="brand" href="../">SkillOpt</a>
<nav aria-label="Site navigation">
<a href="../">Project</a>
<a href="../docs/guideline.html">Documentation</a>
<a href="https://arxiv.org/abs/2605.23904">Paper</a>
<a href="https://github.com/microsoft/SkillOpt">Code</a>
</nav>
</div>
</header>
<main id="main-content">
<span class="eyebrow">Microsoft Research · SkillOpt</span>
<h1>Technical Blog</h1>
<p class="intro">Scoped experiments and engineering notes on optimizing agent skills, evaluating text-space updates, and deploying self-improving systems responsibly.</p>
<section class="post-list" aria-labelledby="latest-posts">
<h2 id="latest-posts">Latest posts</h2>
<article class="post-card">
<div>
<div class="meta"><time datetime="2026-07-14">July 14, 2026</time> · Ziwei Zhou, Ziyang Gong, and Yifan Yang</div>
<h2><a href="gating-reflection-safe-updates/">Expanded SkillOpt Ablations, Skill-Aware Reflection, and SkillOpt-Sleep</a></h2>
<p class="summary">A three-part report with expanded SkillOpt ablations, a skill-aware reflection design with memory consolidation, and a controlled study of the SkillOpt-Sleep plugin.</p>
</div>
<a class="read-link" href="gating-reflection-safe-updates/" aria-label="Read Expanded SkillOpt Ablations, Skill-Aware Reflection, and SkillOpt-Sleep">Read article →</a>
</article>
</section>
</main>
<footer>SkillOpt Technical Blog · <a href="https://github.com/microsoft/SkillOpt">github.com/microsoft/SkillOpt</a></footer>
</body>
</html>
+22 -14
View File
@@ -10,9 +10,10 @@ provided skills on a given split without re-running the training loop.
> skills as portable artifacts. If you want to *train* your own skill,
> use `scripts/train.py` per the top-level README.
>
> This is the first artifact batch. We plan to continue uploading the
> remaining optimized skills and benchmark split manifests as they are
> cleaned and verified.
> This is the first optimized-skill artifact batch. We plan to continue
> uploading remaining paper artifacts as they are cleaned and verified. All
> six lightweight ID/path split manifests are already checked in under
> `data/`; most still require materializing their upstream benchmark payload.
## What's here
@@ -35,12 +36,17 @@ longitudinal guidance — that's expected, not a formatting issue.
invoking the optimizer. Example for SearchQA against the test split:
```bash
# The checked-in SearchQA split is ID-only; materialize full examples first.
python -m pip install -e ".[searchqa]"
python scripts/materialize_searchqa.py
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill ckpt/searchqa/gpt5.5_skill.md \
--split valid_unseen \
--split_dir data/searchqa_id_split \
--split_dir data/searchqa_split \
--azure_openai_endpoint https://your-resource.openai.azure.com/ \
--azure_openai_auth_mode api_key \
--target_model gpt-5.5
```
@@ -52,13 +58,16 @@ is the selection / validation split, `train` is the training split, and
## On comparing to the paper numbers
To compare against the paper-reported cells, use the same dataset split and
scorer. SearchQA's split is checked in at `data/searchqa_id_split/` (400
train / 200 selection / 1400 test). For the other benchmarks, point
`--split_dir` at your own materialized split; the loader is deterministic
from `split_seed` (default `42`) + `split_ratio` (default `2:1:7`) when
`split_mode: ratio` is used, so a given `data_path` + seed reproduces
across machines. Explicit per-benchmark split manifests are being prepared
for upload — see issues #14 and #21.
scorer. SearchQA's ID manifest is checked in at `data/searchqa_id_split/` (400
train / 200 selection / 1400 test); the materializer writes the runnable
payload to `data/searchqa_split/`. All six lightweight split manifests are
checked in under `data/`. ALFWorld's manifest records game-file paths; the
other ID manifests still require you to materialize the corresponding
upstream benchmark payload into the documented `split_dir`. See
[`data/README.md`](../data/README.md) for the exact status of each benchmark.
When using `split_mode: ratio` instead, the loader is deterministic from
`split_seed` (default `42`) + `split_ratio` (default `2:1:7`), so a given
`data_path` + seed reproduces across machines.
## Why force-accept vs. gated slow-update matters
@@ -74,6 +83,5 @@ Current `main` defaults to `false` (force-accept mode), a newer
post-submission behavior where the slow-update guidance is written into
`current_skill` and `best_skill` unconditionally at the epoch boundary. If
you re-train with the current default, you may produce a *different*
`best_skill.md` than the one checked in here. Both modes are supported;
see the top-level README's "Configuration -> Slow-update acceptance mode"
section.
`best_skill.md` than the one checked in here. Both modes are supported; see
the [configuration reference](../docs/reference/config.md).
+14
View File
@@ -138,6 +138,20 @@ ALFWorld:
`searchqa_id_split/` is an ID-only manifest. Each released `id` exactly matches
the `key` field in `lucadiliello/searchqa`.
To materialize the runnable SearchQA split used by
`configs/searchqa/default.yaml`, install the optional dependency and run:
```bash
python -m pip install 'skillopt[searchqa]'
python scripts/materialize_searchqa.py
```
This writes full examples to:
```text
data/searchqa_split
```
Materialized examples must include the fields consumed by the SearchQA
environment, including:
+19 -10
View File
@@ -15,6 +15,7 @@ pip install -e ".[dev]"
### 🐛 Bug Reports
Open an issue with:
- Steps to reproduce
- Expected vs actual behavior
- Config file used (sanitize API keys)
@@ -25,21 +26,29 @@ Open an issue with:
See [Add a New Benchmark](guide/new-benchmark.md) for the implementation guide.
**Checklist:**
- [ ] Data loader in `skillopt/envs/<benchmark>/loader.py`
- [ ] Environment adapter in `skillopt/envs/<benchmark>/env.py`
- [ ] Data loader in `skillopt/envs/<benchmark>/dataloader.py`
- [ ] Scored rollout implementation in `skillopt/envs/<benchmark>/rollout.py`
- [ ] Per-item `predictions/<id>/conversation.json` artifacts for shared reflection
- [ ] Environment adapter in `skillopt/envs/<benchmark>/adapter.py`
- [ ] Config file in `configs/<benchmark>/default.yaml`
- [ ] Registration in `skillopt/envs/__init__.py`
- [ ] Documentation page in `docs/`
- [ ] Lazy registration in `scripts/train.py` and `scripts/eval_only.py`
- [ ] Focused tests and an optional seed skill referenced by `env.skill_init`
- [ ] Documentation update
### 🤖 New Model Backend
See [Add a New Model Backend](guide/new-backend.md) for the implementation guide.
**Checklist:**
- [ ] Backend in `skillopt/model/<backend>.py`
- [ ] Registration in `skillopt/model/__init__.py`
- [ ] API key entry in `.env.example`
- [ ] Documentation update
- [ ] Function-based backend module in `skillopt/model/<name>_backend.py`
- [ ] Alias and default model in `skillopt/model/common.py`
- [ ] Optimizer/target whitelist entries in `skillopt/model/backend_config.py`
- [ ] Dispatch, token tracking, and setter forwarding in `skillopt/model/__init__.py`
- [ ] YAML/CLI wiring when the backend exposes structured config fields
- [ ] Focused routing, configuration, tool-call, and token-accounting tests
- [ ] `.env.example` and backend/configuration reference updates
### 📝 Documentation
@@ -59,9 +68,9 @@ mkdocs serve # Preview at http://localhost:8000
## Pull Request Process
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-benchmark`
2. Create a feature branch: `git switch -c feature/my-benchmark`
3. Make your changes
4. Test with an existing benchmark config
4. Run focused tests, the full test suite, and `mkdocs build --strict` when docs change
5. Submit a PR with a clear description
## License
+85 -12
View File
@@ -20,15 +20,53 @@ Benchmark configs inherit from `_base_/default.yaml` and override specific value
## Key Parameters
### Model
### Model Backends
`optimizer_backend` controls reflection and skill editing;
`target_backend` controls task rollout. The legacy `backend` field remains for
backward compatibility, but explicit role fields are the clearest configuration.
```yaml
model:
backend: azure_openai # azure_openai | openai_chat | claude_code_exec | qwen
optimizer: gpt-5.5 # Optimizer model (for reflection)
target: gpt-5.5 # Target model (for rollout)
backend: azure_openai # High-level compatibility label
optimizer_backend: openai_chat
target_backend: openai_chat
optimizer: gpt-5.5 # Optimizer deployment/model
target: gpt-5.5 # Target deployment/model
azure_openai_auth_mode: api_key
```
| Backend | Optimizer | Target | Configuration |
|---|:---:|:---:|---|
| `openai_chat` | ✓ | ✓ | Azure OpenAI, or its explicit compatibility auth mode |
| `openai_compatible` | ✓ | ✓ | Generic OpenAI Chat Completions endpoint |
| `claude_chat` | ✓ | ✓ | Claude Code CLI (`claude -p`) |
| `qwen_chat` | ✓ | ✓ | Qwen served through an OpenAI-compatible local endpoint |
| `minimax_chat` | ✓ | ✓ | MiniMax API |
| `codex_exec` | — | ✓ | Codex CLI execution harness |
| `claude_code_exec` | — | ✓ | Claude Code CLI execution harness |
The current MiniMax adapter has one shared deployment. Set
`model.minimax_model` when MiniMax is the target; a mixed-backend run cannot
independently select a MiniMax optimizer model and a different target model.
For a generic compatible provider, select the role backends explicitly rather
than relying on a high-level shorthand:
```yaml
model:
optimizer_backend: openai_compatible
target_backend: openai_compatible
optimizer: deepseek-chat
target: deepseek-chat
```
The train/eval entry points apply `model.optimizer` and `model.target` after
backend initialization. For the selected roles, these YAML values override
`OPENAI_COMPATIBLE_MODEL`, `QWEN_CHAT_MODEL`, and their per-role environment
forms. The environment model variables mainly seed direct library use; always
set the role models in a training or evaluation config.
### Training
```yaml
@@ -96,9 +134,16 @@ Notes:
```yaml
evaluation:
use_gate: true # Validation gating (accept/reject updates)
gate_metric: hard # hard | soft | mixed
gate_mixed_weight: 0.5 # Soft-score weight when metric=mixed
use_semantic_density: false # Optional instruction-density bonus
eval_test: true # Run test evaluation after training
```
The default and paper-style setting is `use_gate: true`. Setting it to `false`
still records selection scores but force-accepts every candidate, so it changes
the optimization semantics and should be reported explicitly.
### Environment (Data)
```yaml
@@ -117,9 +162,10 @@ Override any config value from the command line:
```bash
python scripts/train.py \
--config configs/searchqa/default.yaml \
optimizer.learning_rate=16 \
optimizer.lr_scheduler=linear \
gradient.analyst_workers=8
--cfg-options \
optimizer.learning_rate=16 \
optimizer.lr_scheduler=linear \
gradient.analyst_workers=8
```
## Environment Variables
@@ -128,11 +174,38 @@ Model credentials are loaded from environment variables:
| Variable | Backend | Description |
|---|---|---|
| `AZURE_OPENAI_ENDPOINT` | azure_openai | Azure resource endpoint |
| `AZURE_OPENAI_API_KEY` | azure_openai | Azure API key |
| `OPENAI_API_KEY` | openai | OpenAI API key |
| `ANTHROPIC_API_KEY` | claude | Anthropic API key |
| `QWEN_API_BASE` | qwen | Local Qwen vLLM endpoint |
| `AZURE_OPENAI_ENDPOINT` | `openai_chat` | Azure resource URL, or compatibility-mode base URL |
| `AZURE_OPENAI_API_VERSION` | `openai_chat` | Azure API version |
| `AZURE_OPENAI_AUTH_MODE` | `openai_chat` | `api_key`, `azure_cli`, `managed_identity`, or `openai_compatible` |
| `AZURE_OPENAI_API_KEY` | `openai_chat` | Required when auth mode is `api_key` or `openai_compatible` |
| `OPENAI_COMPATIBLE_BASE_URL` | `openai_compatible` | Generic Chat Completions base URL |
| `OPENAI_COMPATIBLE_API_KEY` | `openai_compatible` | Provider API key; optional for local servers |
| `OPENAI_COMPATIBLE_MODEL` | `openai_compatible` | Shared provider model ID for direct library use; train/eval YAML role models take precedence |
| `CLAUDE_CLI_BIN` | `claude_chat` | Optional path to the `claude` executable; defaults to `claude` |
| `ANTHROPIC_API_KEY` | `claude_chat` | Optional authentication method understood by the Claude CLI, not a direct SkillOpt API client |
| `QWEN_CHAT_BASE_URL` | `qwen_chat` | Local Qwen/vLLM endpoint |
| `QWEN_CHAT_MODEL` | `qwen_chat` | Served model name for direct library use; train/eval YAML role models take precedence |
| `MINIMAX_BASE_URL` | `minimax_chat` | MiniMax-compatible base URL |
| `MINIMAX_API_KEY` | `minimax_chat` | MiniMax API key |
`OPTIMIZER_` and `TARGET_` prefixes provide per-role overrides for the
Azure, OpenAI-compatible, and Qwen variable families. See the
[Configuration Reference](../reference/config.md) for exact names.
`claude_chat` launches the installed Claude Code CLI with `claude -p`; install
and authenticate that CLI before use. Setting `ANTHROPIC_API_KEY` is one way
the CLI may authenticate, but SkillOpt does not call the Anthropic API
directly through this backend.
### Three OpenAI-compatible paths
- Research, generic provider: select `openai_compatible` and use
`OPENAI_COMPATIBLE_*`.
- Research, Azure-family compatibility mode: keep `openai_chat`, set
`AZURE_OPENAI_AUTH_MODE=openai_compatible`, and use `AZURE_OPENAI_*`.
- SkillOpt-Sleep: run with `--backend azure_openai` and use the same
compatibility-mode `AZURE_OPENAI_*` variables. Sleep does not read the
research backend's role-specific variables.
## Full Reference
+5 -3
View File
@@ -14,10 +14,9 @@ SkillOpt is designed around a core insight: **optimizing natural-language prompt
| **Gradient aggregation** | Patch aggregation | Merge similar edits |
| **Gradient clipping** | Edit selection | Cap max edits per step |
| **Learning rate** | `learning_rate` | Max number of edits applied per step |
| **LR scheduler** | `lr_scheduler` | Decay schedule: cosine, linear, constant |
| **LR scheduler** | `lr_scheduler` | Edit-budget schedule: cosine, linear, constant, or autonomous |
| **SGD step** | Skill update | Apply selected patches to document |
| **Validation set** | Selection split | Gate checks improvement before accepting |
| **Early stopping** | Gate patience | Reject updates that don't improve |
| **Training step** | Step | One rollout → reflect → update cycle |
| **Epoch** | Epoch | Full pass with slow update + meta memory |
| **Momentum** | Slow update | Longitudinal comparison at epoch boundary |
@@ -34,7 +33,10 @@ SkillOpt is designed around a core insight: **optimizing natural-language prompt
1. **Familiar mental model**: ML practitioners immediately understand how to tune SkillOpt
2. **Principled hyperparameter search**: Grid search over `learning_rate` × `lr_scheduler` works just like in DL
3. **Proven mechanisms**: Gating validation-based selection, patience ≈ early stopping, slow update momentum — all with strong theoretical motivation
3. **Reusable mechanisms**: Gating provides validation-based model selection, while slow update plays a momentum-like role across epochs
The gate is a per-candidate accept/reject decision. SkillOpt does not implement
a gate-patience counter or stop training after a run of rejected candidates.
## Hyperparameter Transfer Rules
+68 -41
View File
@@ -4,17 +4,43 @@ This guide walks through running a complete SkillOpt training on SearchQA.
## 1. Choose a Benchmark
SkillOpt includes ready-to-use configs for several benchmarks:
SkillOpt includes ready-to-use configs for several benchmarks. End-to-end
runtime depends on the chosen models, provider latency, worker limits, and
dataset size, so the project does not promise fixed wall-clock estimates.
| Benchmark | Difficulty | Typical Runtime |
| Benchmark | Modality | Additional setup |
|---|---|---|
| SearchQA | ⭐ Easy | ~30 min |
| DocVQA | ⭐⭐ Medium | ~2 hours |
| ALFWorld | ⭐⭐⭐ Hard | ~3 hours |
| SearchQA | Text QA | Materialize the released ID manifest |
| DocVQA | Document/image QA | Obtain and materialize images and examples |
| ALFWorld | Embodied agent | Install ALFWorld and download its assets |
We'll use **SearchQA** as it's the fastest to complete.
We'll use **SearchQA** because it is the simplest text-only walkthrough.
## 2. Configure
## 2. Install and Materialize SearchQA
The repository contains a stable SearchQA ID manifest, not the full runnable
examples. From a source checkout, install the data extra and materialize the
split once:
```bash
python -m pip install -e ".[searchqa]"
python scripts/materialize_searchqa.py
```
By default, the materializer reads `data/searchqa_id_split/` and writes the
train/validation/test payloads expected by the config to
`data/searchqa_split/`; both paths have command-line overrides.
## 3. Configure
Configure and export one model backend as described in
[Installation](installation.md#environment-variables). For example:
```bash
cp .env.example .env
# Edit .env, choose one authentication mode, then export it:
set -a; source .env; set +a
```
Review the config file:
@@ -42,56 +68,55 @@ evaluation:
use_gate: true # (validation gating)
```
## 3. Train
## 4. Train
```bash
python scripts/train.py --config configs/searchqa/default.yaml
python scripts/train.py \
--config configs/searchqa/default.yaml \
--out_root outputs/searchqa_first_run
```
You'll see output like:
The command prints the resolved backend/data configuration, per-step rollout
and gate progress, and the generated output directory.
## 5. Monitor
The explicit `--out_root` above creates this run directory:
```
[Step 1/8] Rollout: 20 items, 4 workers...
[Step 1/8] Score: 0.65 → Reflect...
[Step 1/8] 6 edit patches generated
[Step 1/8] Selected 4 edits (lr=8, cosine → 7.7)
[Step 1/8] Gate: val score 0.68 > 0.65 ✓ ACCEPT
[Step 2/8] ...
```
## 4. Monitor
Training outputs are saved to `outputs/<benchmark>/<run_id>/`:
```
outputs/searchqa/2024-01-15_10-30-00/
├── steps/
│ ├── step_0001/
│ │ ├── candidate_skill.md
│ │ ├── step_record.json
│ │ └── trajectory_digest.json
│ └── step_0002/
├── slow_update/
│ └── epoch_02/
├── meta_skill/
│ └── epoch_02/
├── skills/
│ └── step_0001.md
├── best_skill.md
outputs/searchqa_first_run/
├── config.json
├── runtime_state.json
├── history.json
── config.yaml
── best_skill.md
├── skills/
│ └── skill_vXXXX.md
├── steps/
│ └── step_XXXX/
│ ├── candidate_skill.md
│ ├── step_record.json
│ └── trajectory_digest.json
├── slow_update/
│ └── epoch_XX/
└── meta_skill/
└── epoch_XX/
```
## 5. Evaluate
## 6. Evaluate
Evaluate the best skill on the test split:
```bash
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill outputs/searchqa/<run_id>/skills/best_skill.md
--skill outputs/searchqa_first_run/best_skill.md \
--split valid_unseen
```
The `--skill` path above is the training artifact. Evaluation writes
`eval_summary.json` to its own timestamped `outputs/eval_.../` directory unless
you pass an explicit `--out_root`; it does not overwrite the training run.
## WebUI
Prefer a graphical interface? Launch the WebUI:
@@ -101,7 +126,9 @@ pip install -e ".[webui]"
python -m skillopt_webui.app
```
Then open `http://localhost:7860` in your browser to configure parameters and launch training.
Then open `http://localhost:7860` in your browser to configure parameters and
launch training. The default host is `0.0.0.0`; pass `--host 127.0.0.1` for a
local-only dashboard.
## Next Steps
+98 -23
View File
@@ -3,16 +3,44 @@
## Requirements
- Python ≥ 3.10
- At least one model API key (Azure OpenAI, OpenAI, Anthropic, or local Qwen)
- For research training/evaluation, access to at least one configured model
backend (hosted API, local server, or an installed execution CLI)
- The SkillOpt-Sleep `mock` backend needs no credentials
## Quick Install
## Choose an Install
### PyPI
Use PyPI for the Python packages and installed commands:
```bash
python -m pip install skillopt
skillopt-sleep --help
```
This installs `skillopt-train`, `skillopt-eval`, and `skillopt-sleep`. The wheel
does not include the repository's benchmark configs, data materializers,
agent-integration shells/MCP servers, or development tests; use a source
checkout for those files.
!!! important "PyPI versus `main`"
These docs track the latest `main`. The current PyPI release is `0.2.0`.
The generic research `openai_compatible` backend, SkillOpt-Sleep handoff,
Sleep support for non-Azure OpenAI-compatible endpoints, and the Sleep
`--preferences` flag landed after that release and require a source install
from `main` until the next release.
### Source checkout
```bash
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
pip install -e .
python -m pip install -e .
```
Use the source checkout for paper reproduction, built-in benchmark configs,
and contributions.
## Optional Dependencies
Install extras for specific benchmarks or backends:
@@ -20,68 +48,115 @@ Install extras for specific benchmarks or backends:
=== "ALFWorld"
```bash
pip install -e ".[alfworld]"
python -m pip install -e ".[alfworld]"
```
=== "Claude Backend"
=== "Claude agent SDK (optional)"
```bash
pip install -e ".[claude]"
python -m pip install -e ".[claude]"
```
This extra does not install the `claude` executable. The research
`claude_chat` backend launches `claude -p`, so install and authenticate the
Claude Code CLI separately. The SDK extra is only needed when selecting an
SDK-backed Claude Code exec path.
=== "Qwen (Local)"
```bash
pip install -e ".[qwen]"
python -m pip install -e ".[qwen]"
```
=== "SearchQA data"
```bash
python -m pip install -e ".[searchqa]"
```
=== "WebUI"
```bash
pip install -e ".[webui]"
python -m pip install -e ".[webui]"
```
=== "Development"
```bash
pip install -e ".[dev]"
python -m pip install -e ".[dev]"
```
=== "All"
```bash
pip install -e ".[alfworld,claude,qwen,webui,dev]"
python -m pip install -e ".[alfworld,claude,qwen,searchqa,webui,docs,dev]"
```
## Environment Variables
Copy the example `.env` file and fill in your credentials:
From a source checkout, copy the template and fill in only the backend you
will use:
```bash
cp .env.example .env
```
Edit `.env` with your API keys:
SkillOpt does not automatically load `.env`; export it into the current shell
before running commands:
```ini
# Azure OpenAI (default backend)
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-key
# Or use OpenAI directly
OPENAI_API_KEY=sk-...
# Or Anthropic Claude
ANTHROPIC_API_KEY=sk-ant-...
```bash
set -a
source .env
set +a
```
For Azure OpenAI with API-key authentication, the minimum settings are:
```ini
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_VERSION=2024-12-01-preview
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_AUTH_MODE=api_key
```
Use `AZURE_OPENAI_AUTH_MODE=azure_cli` for Azure CLI credentials, or
`managed_identity` with an optional
`AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID`.
The research `claude_chat` backend is a Claude Code CLI adapter, not a direct
Anthropic API client. Install and authenticate `claude`, and set
`CLAUDE_CLI_BIN` only if the executable is not available as `claude` on
`PATH`. `ANTHROPIC_API_KEY` is one authentication option the CLI may consume.
OpenAI-compatible servers have three distinct entry points:
1. The research engine's generic `openai_compatible` backend uses
`OPENAI_COMPATIBLE_BASE_URL`, `OPENAI_COMPATIBLE_API_KEY`, and
`OPENAI_COMPATIBLE_MODEL`.
2. The research `openai_chat` backend can use
`AZURE_OPENAI_AUTH_MODE=openai_compatible` with
`AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_API_KEY`.
3. SkillOpt-Sleep uses the same Azure-family variables as item 2 with
`skillopt-sleep run --backend azure_openai`.
For research train/eval commands, `model.optimizer` and `model.target` in the
YAML config are applied after backend initialization. They override model-name
environment variables such as `OPENAI_COMPATIBLE_MODEL` and
`QWEN_CHAT_MODEL`; set both role models explicitly when selecting those
backends.
!!! tip
You only need credentials for the backend you plan to use. Azure OpenAI is the default.
You only need to configure the backend you plan to use. See
[Configuration](configuration.md#model-backends) for exact backend names
and role-specific overrides.
## Verify Installation
```bash
python -c "import skillopt; print('SkillOpt ready!')"
skillopt-train --help
skillopt-eval --help
skillopt-sleep --help
```
## Next Steps
+15 -9
View File
@@ -35,9 +35,14 @@ Use the split names your adapter maps to SkillOpt phases:
- `val` or `valid_seen` for selection/gating
- `test` or `valid_unseen` for final evaluation
## 2. Support an offline mock mode
## 2. Support a genuinely offline mock mode
Add a configuration flag such as `mock: true` to your adapter. In mock mode, `rollout()` should return deterministic responses without calling external model APIs.
Add a configuration flag such as `mock: true` to your adapter. In mock mode,
`rollout()` should return deterministic responses without calling external
model APIs. The inherited `EnvAdapter.reflect()` does call the configured
optimizer backend, so a no-credential smoke test must also override
`reflect()` in mock mode to return a small, schema-valid deterministic patch
(and delegate to `super().reflect(...)` otherwise).
This lets you verify the SkillOpt loop with a fast command such as:
@@ -46,13 +51,14 @@ python scripts/train.py \
--config configs/myenv/tiny_mock.yaml
```
Mock mode should still write the same artifacts as a real run, for example:
Mock mode should still exercise the trainer's normal artifact paths, including:
- `responses.json`
- `rollout_results.json`
- `ranked_edits.json`
- `candidate_skill.md`
- `summary.json`
- `config.json`, `runtime_state.json`, and `history.json`
- `skills/skill_vXXXX.md`
- `steps/step_XXXX/ranked_edits.json`
- `steps/step_XXXX/candidate_skill.md`
- `steps/step_XXXX/step_record.json`
- the final `summary.json`
## 3. Keep the smoke config tiny
@@ -127,7 +133,7 @@ For the real tiny run, verify that:
- the run completes
- `summary.json` is written
- `ranked_edits.json` contains the expected ranking metadata
- the step directory's `ranked_edits.json` contains the expected ranking metadata
- any optimizer bridge log marks the response schema as valid
- no generated files are written outside `out_root`
+171 -101
View File
@@ -1,130 +1,200 @@
# Add a New Model Backend
SkillOpt supports multiple LLM backends. This guide shows how to add your own.
SkillOpt's model layer is function-based: each chat backend is a Python module
that exposes the call, token-tracking, and deployment-setting functions used by
`skillopt.model`. There is no backend base class or registry object to subclass.
## Backend Architecture
## Built-in: the generic OpenAI-compatible backend
```
skillopt/model/
├── base.py # Abstract base class
├── azure_openai.py # Azure OpenAI backend
├── openai_model.py # Direct OpenAI backend
├── claude.py # Anthropic Claude backend
├── qwen.py # Local Qwen (vLLM) backend
└── your_backend.py # Your new backend
```
!!! note "Version requirement"
This backend landed after v0.2.0. Install from the latest `main` until it is
included in the next release.
## Step 1: Create the Backend
Before writing a new backend, check whether your provider already speaks the
OpenAI Chat Completions protocol. Most do, in which case you can use the
built-in **`openai_compatible`** backend
(`skillopt/model/openai_compatible_backend.py`) with no code changes.
Create `skillopt/model/your_backend.py`:
A single `base_url` + `api_key` pair lets you point SkillOpt at, for example:
| Provider | `base_url` | Example model |
|---|---|---|
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` |
| Together AI | `https://api.together.xyz/v1` | `meta-llama/Llama-3.3-70B-Instruct-Turbo` |
| Ollama (local) | `http://localhost:11434/v1` | `qwen2.5:7b` |
| vLLM / SGLang / TGI | `http://localhost:8000/v1` | your served model |
| LiteLLM proxy | `http://localhost:4000` | any proxied model |
| OpenRouter / Fireworks / xAI / … | provider base URL | provider model id |
### Python API
Select and configure the backend directly when embedding SkillOpt as a Python
library:
```python
from skillopt.model.base import ModelBackend, ModelResponse
import skillopt.model as model
class YourBackend(ModelBackend):
"""Your custom model backend."""
def __init__(self, cfg: dict):
super().__init__(cfg)
self.model_name = cfg.get('model_name', 'your-default-model')
self.api_key = os.environ.get('YOUR_API_KEY', '')
self.client = self._init_client()
def _init_client(self):
"""Initialize API client."""
# TODO: Set up your API client
pass
async def generate(
self,
messages: list[dict],
temperature: float = 0.7,
max_tokens: int = 4096,
**kwargs
) -> ModelResponse:
"""
Generate a completion.
Args:
messages: Chat messages [{"role": "...", "content": "..."}]
temperature: Sampling temperature
max_tokens: Maximum tokens in response
Returns:
ModelResponse with content, usage, and metadata
"""
response = await self.client.chat(
model=self.model_name,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return ModelResponse(
content=response.text,
usage={
'prompt_tokens': response.usage.input,
'completion_tokens': response.usage.output,
},
model=self.model_name,
)
async def generate_with_tools(
self,
messages: list[dict],
tools: list[dict],
**kwargs
) -> ModelResponse:
"""Generate with tool/function calling support."""
# Optional: implement if your model supports tool use
raise NotImplementedError("Tool use not supported")
# Use the generic backend for both optimizer and target calls.
model.set_backend("openai_compatible")
model.configure_openai_compatible(
base_url="https://api.deepseek.com/v1",
api_key="sk-...",
model="deepseek-chat",
)
```
## Step 2: Register the Backend
`configure_openai_compatible()` also accepts `optimizer_*` and `target_*`
arguments when the two roles use different endpoints or models.
Add to `skillopt/model/__init__.py`:
### Environment variables
```python
from .your_backend import YourBackend
The shared variables below configure both roles. Role-specific
`OPTIMIZER_OPENAI_COMPATIBLE_*` and `TARGET_OPENAI_COMPATIBLE_*` variables take
precedence:
BACKEND_REGISTRY = {
# ... existing backends ...
'your_backend': YourBackend,
}
```bash
export OPENAI_COMPATIBLE_BASE_URL="https://api.groq.com/openai/v1"
export OPENAI_COMPATIBLE_API_KEY="gsk_..."
export OPENAI_COMPATIBLE_MODEL="llama-3.3-70b-versatile"
# Optional: OPENAI_COMPATIBLE_TEMPERATURE, _MAX_TOKENS, _TIMEOUT_SECONDS
```
## Step 3: Configure
Use your backend in any config:
For direct library use, `OPTIMIZER_BACKEND=openai_compatible` and/or
`TARGET_BACKEND=openai_compatible` select the role. The training and evaluation
scripts resolve backend selection from their config, so set the split fields
explicitly there:
```yaml
model:
backend: your_backend
model_name: your-model-id
temperature: 0.7
max_tokens: 4096
optimizer_backend: openai_compatible
target_backend: openai_compatible
optimizer: llama-3.3-70b-versatile
target: llama-3.3-70b-versatile
```
Set credentials via environment variable:
Equivalently, override those fields on the command line:
```bash
export YOUR_API_KEY="your-key"
python scripts/train.py --config configs/searchqa/default.yaml \
--cfg-options \
model.optimizer_backend=openai_compatible \
model.target_backend=openai_compatible \
model.optimizer=llama-3.3-70b-versatile \
model.target=llama-3.3-70b-versatile
```
## Required Interface
Do not rely on the legacy high-level `model.backend` label to replace the two
role-specific fields in a structured config.
Your backend must implement these methods:
The generic backend uses the official `openai` SDK and the Chat Completions
API. It records token usage through the shared tracker, supports provider tool
calling through `chat_*_messages(..., tools=...)`, and exposes `count_tokens()`
(tiktoken when available, with a character-based fallback). Provider-specific
Responses API features are outside this backend's contract.
| Method | Required | Description |
|---|---|---|
| `generate()` | ✅ | Basic text generation |
| `generate_with_tools()` | Optional | Tool/function calling |
| `count_tokens()` | Optional | Token counting for context management |
Only write a new backend when the provider is not compatible with this surface
or requires behavior that cannot be expressed by its configuration.
## Tips
## Backend architecture
!!! tip
- Test your backend with `python -c "from skillopt.model.your_backend import YourBackend"` first
- Use `async` methods for all API calls — SkillOpt uses asyncio throughout
- Implement retry logic with exponential backoff for production use
- Add your API key to `.env.example` when submitting a PR
The active split optimizer/target dispatcher is the public
`skillopt/model/__init__.py` module:
```text
skillopt/model/
├── common.py # aliases, default models, token/response helpers
├── backend_config.py # optimizer/target whitelists and runtime selection
├── __init__.py # public API and split-role dispatch
├── openai_compatible_backend.py # generic Chat Completions example
├── qwen_backend.py # raw-HTTP chat example with per-role config
├── minimax_backend.py # compact raw-HTTP chat example
├── codex_harness.py # target-only exec harnesses
└── router.py # legacy single-backend compatibility surface
```
`router.py` is not the dispatcher used by the current training loop. Update it
only if the new backend must also be exposed through that legacy single-backend
API.
## Step 1: implement the module contract
Create a module such as `skillopt/model/your_backend.py`. Copy the signatures
from `openai_compatible_backend.py` or `qwen_backend.py`; model calls in the
current framework are synchronous.
For a chat backend that supports both roles, the public module surface is:
| Function | Purpose |
|---|---|
| `chat_optimizer(...)` | Optimizer system/user call; returns `(text, usage)` |
| `chat_target(...)` | Target system/user call; returns `(text, usage)` |
| `chat_optimizer_messages(...)` | Optimizer message-list call, including optional tools |
| `chat_target_messages(...)` | Target message-list call, including optional tools |
| `get_token_summary()` | Return per-stage counters plus `_total` |
| `reset_token_tracker()` | Clear this backend's counters |
| `set_optimizer_deployment(name)` | Change the optimizer model at runtime |
| `set_target_deployment(name)` | Change the target model at runtime |
| `set_reasoning_effort(effort)` | Apply or safely ignore the shared reasoning setting |
Every call returns a usage dict with `prompt_tokens`, `completion_tokens`, and
`total_tokens`. Use `TokenTracker` from `skillopt.model.common` and record each
call exactly once. Message-list calls that accept tools should return the
compatibility message objects from `common.py` when `return_message=True`.
Provider-specific configuration helpers and `count_tokens()` are optional, but
their state must be safe to update while calls may run concurrently. Keep
credentials out of logs and persisted artifacts.
Exec-style targets do not implement this chat contract. They are target-only
and are integrated through `codex_harness.py` plus environment-specific rollout
code.
## Step 2: register and route the backend
A new backend normally requires all of the following:
1. Add its canonical name, aliases, and default model to
`skillopt/model/common.py`.
2. Add the canonical name to the appropriate optimizer and/or target whitelist
in `skillopt/model/backend_config.py`. Do not advertise a role the module
cannot execute.
3. Import the module in `skillopt/model/__init__.py` and add dispatch branches
for every supported call surface.
4. Include its counters in `get_token_summary()` / `reset_token_tracker()` and
forward the shared deployment/reasoning setters where applicable.
5. If it has YAML settings, add structured-to-flat mappings in
`skillopt/config.py`, wire them through `scripts/train.py` and
`scripts/eval_only.py`, and document their precedence over environment
variables.
6. Update `router.py` only when legacy single-backend compatibility is part of
the intended feature.
Backend selection in `scripts/train.py` must use
`model.optimizer_backend` and `model.target_backend`. A high-level
`model.backend` alias alone is not a substitute for this explicit split.
## Step 3: test the integration
Add focused tests under `tests/` that do not call a live provider. At minimum,
cover:
- optimizer and target whitelist validation;
- routing for text and message-list calls;
- role-specific configuration precedence;
- tool-call compatibility, if supported;
- deployment/reasoning setters;
- token accounting, including a single correct `_total`;
- actionable errors for missing credentials or invalid responses.
Then run the focused test, the full suite, and the documentation build:
```bash
python -m pytest tests/test_your_backend.py -q
python -m pytest tests/ -q
mkdocs build --strict
```
Also update `.env.example`, the configuration reference, and the backend table
in the API reference. Add an optional dependency extra only when the backend
requires a package that is not already a core dependency.
+60 -23
View File
@@ -14,16 +14,18 @@ To add a benchmark you implement four things:
1. **A `SplitDataLoader` subclass** — knows how to load train / val / test
item dicts from disk.
2. **A rollout helper** — runs the target model on a batch of items
under the current skill and scores each prediction.
2. **A rollout helper** — runs the target model on a batch of items, scores
each prediction, and persists the per-item conversation consumed by the
shared reflection stage.
3. **An `EnvAdapter` subclass** — wires the loader + rollout helper into
SkillOpt's lifecycle (`build_*_env`, `rollout`, `reflect`,
`get_task_types`).
SkillOpt's lifecycle (`build_*_env`, `rollout`, and `get_task_types`).
The shared `reflect()` implementation is inherited unless the benchmark
needs custom reflection logic.
4. **A YAML config** — references your env name plus the standard
train / optimizer / gradient knobs.
Then one line in `scripts/train.py`'s `_register_builtins()` makes it
discoverable.
Then lazy registration in the training and evaluation scripts makes it
discoverable without importing optional dependencies at startup.
---
@@ -99,8 +101,8 @@ def _score(prediction: str, ground_truth: str) -> tuple[int, float]:
return hard, soft
def _rollout_one(item: dict, skill_content: str,
*, max_completion_tokens: int) -> dict:
def _rollout_one(item: dict, skill_content: str, *, prediction_dir: Path,
max_completion_tokens: int) -> dict:
system = skill_content
user = (
f"Question: {item['question']}\n\n"
@@ -113,14 +115,33 @@ def _rollout_one(item: dict, skill_content: str,
max_completion_tokens=max_completion_tokens,
)
hard, soft = _score(prediction, item.get("ground_truth", ""))
# EnvAdapter.reflect() reads this exact trajectory path. Keep item IDs
# unique and filesystem-safe.
task_dir = prediction_dir / str(item["id"])
task_dir.mkdir(parents=True, exist_ok=True)
conversation = [
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "assistant", "content": prediction},
]
(task_dir / "conversation.json").write_text(
json.dumps(conversation, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return {
"id": str(item["id"]),
"hard": hard,
"soft": soft,
"predicted_answer": prediction,
"task_description": item.get("question", ""),
"question": item.get("question", ""),
"reference_text": item.get("reference_text", ""),
"task_type": item.get("task_type", "docfaithful"),
"target_system_prompt": system,
"target_user_prompt": user,
"n_turns": 1,
}
@@ -128,15 +149,18 @@ def run_batch(*, items: list[dict], skill_content: str, out_root: str,
workers: int = 4, max_completion_tokens: int = 4096) -> list[dict]:
"""Run a batch of episodes sequentially or with a thread pool."""
os.makedirs(out_root, exist_ok=True)
prediction_dir = Path(out_root, "predictions")
# For brevity we go sequentially — swap in concurrent.futures.ThreadPoolExecutor
# when network / model latency dominates.
results = [
_rollout_one(item, skill_content,
prediction_dir=prediction_dir,
max_completion_tokens=max_completion_tokens)
for item in items
]
Path(out_root, "rollouts.json").write_text(
json.dumps(results, ensure_ascii=False, indent=2)
json.dumps(results, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return results
```
@@ -150,9 +174,17 @@ Two design points worth flagging:
- **Use `skillopt.model.chat_target`**, not raw OpenAI/Claude calls.
That routes through whichever **chat** target backend the user
configured (`openai_chat` / `claude_chat` / `qwen_chat` /
`minimax_chat`) without your adapter caring. Exec-style backends
(`codex_exec`, `claude_code_exec`) need env-specific rollout code —
see `skillopt/envs/swebench/` for an example.
`minimax_chat` / `openai_compatible`) without your adapter caring.
Exec-style backends (`codex_exec`, `claude_code_exec`) need
environment-specific rollout code —
see `skillopt/model/codex_harness.py` together with the rollout modules in
`skillopt/envs/searchqa/`, `skillopt/envs/docvqa/`, or
`skillopt/envs/officeqa/` for working examples.
- **Persist a conversation for reflection.** The shared `EnvAdapter.reflect()`
looks under `<rollout_dir>/predictions/<result-id>/conversation.json` and
skips results whose trajectory is absent or empty. Returning `hard`/`soft`
scores alone is sufficient for evaluation, but it cannot produce learning
patches.
## Step 4 — Implement the environment adapter
@@ -277,9 +309,11 @@ the answer against `item["ground_truth"]`, and returns a list of dicts:
]
```
The trainer only requires `id`, `hard`, `soft`. The rest is preserved on
`RolloutResult.extras` (see `skillopt/types.py`) and is what your
`reflect()` consumes via `run_minibatch_reflect`.
The trainer requires `id`, `hard`, and `soft` for scoring. The remaining fields
are preserved on `RolloutResult.extras` (see `skillopt/types.py`). The shared
reflection implementation combines those fields with each persisted
`predictions/<id>/conversation.json`; without that file the result is omitted
from reflection.
## Step 5 — Register the adapter
@@ -294,9 +328,11 @@ and add to `_register_builtins()`:
pass # docfaithful deps not installed — skip
```
There is **no `BENCHMARK_REGISTRY` dict in `skillopt/envs/__init__.py`**
the registry lives in `scripts/train.py` and is populated lazily so that
optional deps don't break `--help`.
Mirror the same lazy registration in
[`scripts/eval_only.py`](https://github.com/microsoft/SkillOpt/blob/main/scripts/eval_only.py)
so standalone evaluation can resolve the environment too. There is **no
`BENCHMARK_REGISTRY` dict in `skillopt/envs/__init__.py`**; both entry points
keep a small lazy registry so optional dependencies do not break `--help`.
## Step 6 — Create the YAML config
@@ -322,9 +358,7 @@ optimizer:
env:
name: docfaithful
# Optional: a seed skill document. Create this file (or any markdown
# file) yourself before the first run, or omit the key to let SkillOpt
# start from an empty skill.
# Point to an existing Markdown file. Use an empty file to start blank.
skill_init: skillopt/envs/docfaithful/skills/initial.md
split_mode: split_dir
split_dir: data/docfaithful_split
@@ -341,7 +375,7 @@ env:
## Step 7 — Run
```bash
# If you set skill_init above, create the seed skill first:
# Create the file referenced by env.skill_init before the first run:
# mkdir -p skillopt/envs/docfaithful/skills
# echo "# DocFaithful initial skill" > skillopt/envs/docfaithful/skills/initial.md
@@ -364,8 +398,11 @@ you forgot to implement one of the four abstract methods on `EnvAdapter`:
`hard` / `soft`.
- Noisy scoring kills the optimizer. Spend time on `run_batch`'s scoring
before you spend time on prompts.
- If training repeatedly reports `skip_no_patches`, first verify that every
rollout result has a non-empty
`rollout/predictions/<id>/conversation.json` using the same `id` string.
- If your benchmark needs heavy optional deps (selenium, vllm, ...),
wrap the registration block with `try / except ImportError` (Step 5)
wrap both registration blocks with `try / except ImportError` (Step 5)
so people without those deps can still `--help`.
- Copy `skillopt/envs/_template/` as a starting skeleton — it now
implements the real abstract methods.
+30 -7
View File
@@ -38,23 +38,45 @@ During training, the skill document is modified by **edit patches**:
2. **Modifications**: Refining existing rules that are partially correct
3. **Deletions**: Removing rules that consistently lead to errors
Each edit is validated through the **gate** mechanism before being permanently accepted.
Selected edits are applied together to produce a candidate skill. With the
validation gate enabled, that candidate replaces the current skill only when
its score on the selection split strictly improves.
SkillOpt may maintain two protected, machine-managed regions:
```markdown
<!-- SLOW_UPDATE_START -->
... epoch-level longitudinal guidance ...
<!-- SLOW_UPDATE_END -->
<!-- APPENDIX_START -->
... skill-aware execution reminders ...
<!-- APPENDIX_END -->
```
Normal edit patches cannot modify either region. Slow update owns the first;
optional skill-aware reflection owns the second. Preserve these markers when
copying or manually inspecting a trained skill.
## Initial Skill
You can start training with:
- **Empty skill**: The system learns everything from scratch
- **Empty skill**: Point `env.skill_init` to an empty Markdown file
- **Seed skill**: Provide initial instructions to bootstrap training
- **Pre-trained skill**: Transfer a skill from a related benchmark
Configure the initial skill in your YAML:
```yaml
train:
init_skill: "path/to/initial_skill.md" # or omit for empty
env:
skill_init: path/to/initial_skill.md
```
To start from scratch, create an empty Markdown file and use its path. A missing
path currently also starts blank, so using an explicit file avoids silently
treating a typo as an empty skill.
## Skill Quality Metrics
Track your skill's evolution through:
@@ -62,15 +84,16 @@ Track your skill's evolution through:
- **Validation score**: Primary metric on the selection split
- **Test score**: Final metric on held-out test data
- **Skill length**: Total tokens in the document
- **Edit acceptance rate**: Fraction of proposed edits that pass gating
- **Candidate acceptance rate**: Fraction of candidate skill updates that pass
gating; multiple proposed edits can be combined into one candidate
## Best Practices
!!! tip "Tips for better skills"
1. **Start with a seed skill** (`env.skill_init`) if you have domain knowledge — it converges faster
2. **Use cosine LR schedule** — aggressive early exploration + careful late refinement
3. **Enable slow update** (`use_slow_update: true`) to prevent forgetting across epochs
4. **Enable meta skill** (`use_meta_skill: true`) so the optimizer accumulates strategy memory
3. **Enable slow update** (`optimizer.use_slow_update: true`) to counter forgetting across epochs
4. **Enable meta skill** (`optimizer.use_meta_skill: true`) so the optimizer accumulates strategy memory
## Next Steps
+22 -9
View File
@@ -37,12 +37,11 @@ scores = evaluate(predictions, ground_truth)
### 2. Reflect (Backward Pass)
The **optimizer** model analyzes failed trajectories and produces **edit patches** — structured suggestions for improving the skill document.
Two modes:
- **Shallow**: Analyze each trajectory independently
- **Deep**: Cross-reference multiple failures to find systemic issues
The **optimizer** model analyzes trajectory minibatches and produces **edit
patches** — structured suggestions for improving the skill document. Failure
minibatches are always eligible for analysis; successful trajectories are also
analyzed unless `gradient.failure_only` is enabled. Independent minibatches can
run concurrently according to `gradient.analyst_workers`.
```python
# Analogy: computing gradients
@@ -74,17 +73,31 @@ Selected edits are applied to the skill document, producing a new version.
### 6. Gate (Validation)
The updated skill is evaluated on a **selection split** (analogous to a validation set). The update is only accepted if performance improves.
The updated skill is evaluated on a **selection split** (analogous to a
validation set). With the gate enabled, the candidate is accepted only when its
configured gate score (`hard`, `soft`, or `mixed`) is strictly higher than the
current skill's score. With `evaluation.use_gate: false`, validation is still
recorded but candidates are force-accepted.
## Epoch Boundary Mechanisms
### Slow Update
At the end of each epoch (starting from epoch 2), the system performs a **longitudinal comparison**: it rolls out both the previous epoch's skill and the current skill on the same samples, categorizes items as improved/regressed/persistent_fail/stable_success, then generates high-level **guidance** that is injected into the skill document. This prevents catastrophic forgetting of earlier improvements.
At the end of each epoch (starting from epoch 2), the system performs a
**longitudinal comparison**: it rolls out both the previous epoch's skill and
the current skill on the same samples, categorizes items as
improved/regressed/persistent-fail/stable-success, then generates high-level
**guidance** for the skill document. Depending on
`optimizer.slow_update_gate_with_selection`, that guidance is either checked on
the selection split or applied unconditionally. Its purpose is to counter
cross-epoch forgetting.
### Meta Skill
A **meta-skill memory** accumulates high-level strategy notes across the entire training run. At the end of each epoch, the optimizer reflects on what changed between epochs and produces a compact memory that is provided as additional context during future reflection steps.
A **meta-skill memory** accumulates high-level strategy notes across the training
run. Starting at the end of epoch 2, the optimizer compares the previous and
current epoch, writes a compact memory, and provides the prior epoch's memory as
additional context during later reflection and update stages.
## Next Steps
+472 -907
View File
File diff suppressed because it is too large Load Diff
+56 -11
View File
@@ -18,6 +18,19 @@ hide:
---
## Two Complementary Workflows
| Workflow | Package / command | Use it for |
|---|---|---|
| **Research engine** | `skillopt`, `skillopt-train`, `skillopt-eval` | Train and evaluate skill documents on explicit benchmark splits. |
| **SkillOpt-Sleep (preview)** | `skillopt_sleep`, `skillopt-sleep` | Review supported coding-agent sessions and stage proposed memory/skill updates for human adoption. |
They share the idea of bounded text updates and validation, but they are
separate entry points with different configs and safety boundaries. Start with
the [SkillOpt-Sleep overview](sleep/README.md) before using real session data.
---
## How It Works
<div class="pipeline-container" markdown>
@@ -106,29 +119,52 @@ SkillOpt brings the familiar deep-learning training paradigm to agentic prompt o
| **ALFWorld** | Embodied AI | `configs/alfworld/` |
| **OfficeQA** | Enterprise QA | `configs/officeqa/` |
| **SearchQA** | Open-domain QA | `configs/searchqa/` |
| **LiveMathBench** | Math reasoning | `configs/livemathematicianbench/` |
| **SWEBench** | Software Engineering | `configs/swebench/` |
| + 5 more | Various | See [docs](guide/first-experiment.md) |
| **LiveMathematicianBench** | Math reasoning | `configs/livemathematicianbench/` |
| **SpreadsheetBench** | Spreadsheet editing | `configs/spreadsheetbench/` |
---
## Model Backends
Optimizer and target roles are configured separately. Chat backends include
Azure OpenAI (`openai_chat`), the provider-neutral
`openai_compatible` backend, the Claude Code CLI (`claude_chat`), Qwen, and
MiniMax. Codex and Claude Code exec harnesses are target-only and require
adapter support. Despite its name, `claude_chat` launches `claude -p`; it is
not a direct Anthropic API client.
If a provider implements OpenAI Chat Completions, begin with the
[built-in compatible backend](guide/new-backend.md#built-in-the-generic-openai-compatible-backend)
instead of adding a new integration. See [Configuration](guide/configuration.md)
for authentication and per-role overrides.
---
## Quick Example
```bash
# Install
pip install -e .
# Clone and install the research checkout plus the SearchQA data extra
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
python -m pip install -e ".[searchqa]"
# Configure credentials
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_API_KEY="your-key"
# Configure credentials (choose one auth mode in .env)
cp .env.example .env
set -a; source .env; set +a
# Train on SearchQA
python scripts/train.py --config configs/searchqa/default.yaml
# Materialize the runnable split from the checked-in ID manifest
python scripts/materialize_searchqa.py
# Train on SearchQA into a predictable output directory
python scripts/train.py \
--config configs/searchqa/default.yaml \
--out_root outputs/searchqa_quickstart
# Evaluate best skill
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill outputs/best_skill.md
--skill outputs/searchqa_quickstart/best_skill.md \
--split valid_unseen
```
---
@@ -167,4 +203,13 @@ python scripts/eval_only.py \
[:octicons-arrow-right-24: WebUI Guide](guide/first-experiment.md#webui)
- :material-weather-night:{ .lg .middle } **SkillOpt-Sleep**
---
Test the deployment companion with the no-provider mock path, then review
its data boundary before selecting a real backend.
[:octicons-arrow-right-24: Sleep Overview](sleep/README.md)
</div>
+21 -18
View File
@@ -17,7 +17,9 @@ browse [`skillopt/envs/`](https://github.com/microsoft/SkillOpt/tree/main/skillo
`skillopt/envs/base.py` — abstract adapter that connects the SkillOpt
trainer to an environment (benchmark, simulator, REST API, ...).
Subclasses **must** implement the five abstract methods below.
Subclasses **must** implement the four abstract methods below. Reflection has a
shared default implementation and only needs to be overridden for
environment-specific behavior.
```python
from abc import ABC, abstractmethod
@@ -30,6 +32,10 @@ class EnvAdapter(ABC):
def setup(self, cfg: dict) -> None: ...
def get_dataloader(self) -> BaseDataLoader | None: ...
def requires_ray(self) -> bool: ... # default False
def reflect(self, results: list[dict], skill_content: str,
out_dir: str, **kwargs) -> list[dict | None]:
"""Delegate to the shared minibatch reflection pipeline."""
...
# ── Abstract methods (subclasses MUST implement) ────────────────────
@@ -53,26 +59,21 @@ class EnvAdapter(ABC):
It MAY contain env-specific extra keys (parsed into RolloutResult.extras).
"""
@abstractmethod
def reflect(self, results: list[dict], skill_content: str,
out_dir: str, **kwargs) -> list[dict | None]:
"""Turn rollout results into a list of raw patch dicts.
Each dict (or None to drop the slot) MUST contain:
- "patch": {"edits": [...]} a Patch.to_dict() payload
- "source_type": "failure" | "success"
"""
@abstractmethod
def get_task_types(self) -> list[str]:
"""Distinct task-type strings used for stratified sampling."""
```
The trainer also calls a few default-implemented helpers on every adapter:
The default `reflect()` delegates to `run_minibatch_reflect` and returns raw
patch dicts with a `patch` payload plus a `failure` or `success` source type.
It expects each rollout to persist a non-empty trajectory at
`<rollout_dir>/predictions/<result-id>/conversation.json`; results without that
file can be scored but are skipped during reflection.
The trainer also calls several default-implemented helpers on every adapter:
`build_reference_text`, `get_reference_metadata`, `attach_reference_context`,
`select_representative_items`, and `build_env_from_batch`. Read the docstrings
in `skillopt/envs/base.py` if you need to override any of these — most
benchmarks don't.
benchmarks do not.
### `BaseDataLoader` / `SplitDataLoader`
@@ -161,16 +162,17 @@ into `RolloutResult.extras`.
### `GateResult` / `GateAction`
`skillopt/evaluation/gate.py` — the validation-gate decision types
returned each epoch.
returned for each candidate optimization step, and optionally for a separate
epoch-end slow-update candidate.
---
## Registering an environment
Environments are not registered via decorators or a `BENCHMARK_REGISTRY`
dict. The trainer keeps a lazy registry inside `scripts/train.py`
`_ENV_REGISTRY` populated by `_register_builtins()`. To add a new env
you append a `try / except ImportError` block there. See
dict. The training and standalone-evaluation entry points each keep a lazy
`_ENV_REGISTRY`, populated by `_register_builtins()` in `scripts/train.py` and
`scripts/eval_only.py`. Add the environment to both entry points. See
[Add a New Benchmark](../guide/new-benchmark.md) for the full step-by-step.
---
@@ -187,7 +189,8 @@ not via a base class subclass. Supported values (as of this writing):
| `claude_chat` | ✓ | ✓ |
| `qwen_chat` | ✓ | ✓ |
| `minimax_chat` | ✓ | ✓ |
| `codex_exec` | | ✓ |
| `openai_compatible` | | ✓ |
| `codex_exec` | ✓ | ✓ |
| `claude_code_exec` | — | ✓ |
See `skillopt/model/backend_config.py` for the live whitelist and
+72 -6
View File
@@ -1,9 +1,17 @@
# CLI Reference
> **Version note.** This reference tracks `main`. PyPI 0.2.0 does not yet
> include the generic research `openai_compatible` backend, Sleep handoff,
> Sleep support for non-Azure OpenAI-compatible endpoints, or the Sleep
> `--preferences` flag; use a source install from `main` for those features
> until the next release.
## Training
```bash
python scripts/train.py --config <config.yaml> [overrides...]
# Installed equivalent:
skillopt-train --config <config.yaml> [overrides...]
```
### Arguments
@@ -11,13 +19,15 @@ python scripts/train.py --config <config.yaml> [overrides...]
| Argument | Description |
|---|---|
| `--config` | Path to YAML config file (required) |
| `key=value` | Override any config parameter |
| `--cfg-options key=value [...]` | Override structured config parameters |
### Examples
```bash
# Basic training
python scripts/train.py --config configs/searchqa/default.yaml
python scripts/train.py \
--config configs/searchqa/default.yaml \
--out_root outputs/searchqa_run
# With overrides
python scripts/train.py \
@@ -34,6 +44,8 @@ python scripts/train.py \
```bash
python scripts/eval_only.py --config <config.yaml> --skill <skill.md>
# Installed equivalent:
skillopt-eval --config <config.yaml> --skill <skill.md>
```
### Arguments
@@ -42,7 +54,8 @@ python scripts/eval_only.py --config <config.yaml> --skill <skill.md>
|---|---|
| `--config` | Path to YAML config file (required) |
| `--skill` | Path to skill document to evaluate (required) |
| `--split` | Evaluation split: `test` (default), `valid`, `train` |
| `--split` | `train`, `valid_seen`, `valid_unseen`, or `all` (default) |
| `--cfg-options` | One or more `section.key=value` overrides |
### Examples
@@ -50,15 +63,64 @@ python scripts/eval_only.py --config <config.yaml> --skill <skill.md>
# Evaluate best skill on test set
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill outputs/searchqa/run_001/skills/best_skill.md
--skill outputs/searchqa_run/best_skill.md \
--split valid_unseen
# Evaluate on validation set
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill outputs/searchqa/run_001/skills/best_skill.md \
--split valid
--skill outputs/searchqa_run/best_skill.md \
--split valid_seen
```
`--skill` consumes the artifact produced by training. Unless `--out_root` is
set for evaluation, `eval_only.py` creates a separate timestamped
`outputs/eval_<env>_<model>_<timestamp>/` directory and writes
`eval_summary.json` there; it does not modify the training run directory.
For the generic OpenAI-compatible research backend, select the role backends
explicitly:
```bash
python scripts/train.py \
--config configs/searchqa/default.yaml \
--cfg-options \
model.optimizer_backend=openai_compatible \
model.target_backend=openai_compatible \
model.optimizer=deepseek-chat \
model.target=deepseek-chat
```
## SkillOpt-Sleep
```bash
skillopt-sleep <action> [options]
# Equivalent from a source checkout:
python -m skillopt_sleep <action> [options]
```
Actions are `run`, `dry-run`, `status`, `adopt`, `harvest`, `schedule`, and
`unschedule`. Common options include:
| Argument | Description |
|---|---|
| `--project PATH` | Project to evolve (default: current directory) |
| `--scope invoked\|all` | Harvest this project or all projects |
| `--source claude\|codex\|auto` | Transcript source |
| `--backend mock\|claude\|codex\|copilot\|handoff\|azure_openai` | Replay/optimizer backend |
| `--model NAME` | Backend-specific model override |
| `--preferences TEXT` | House rules supplied to reflection |
| `--lookback-hours N` | Initial transcript lookback; `0` scans all history |
| `--max-sessions N` / `--max-tasks N` | Bound the harvested workload |
| `--target-skill-path PATH` | Explicit skill document to stage/adopt |
| `--tasks-file PATH` | Replay a reviewed task JSON file instead of harvesting |
| `--edit-budget N` | Maximum bounded edits for the night |
| `--progress` / `--json` | Progress or machine-readable output |
| `--auto-adopt` | Apply an accepted staged proposal automatically |
Backend-specific setup for compatible endpoints is documented in
[OpenAI-compatible endpoints for SkillOpt-Sleep](../sleep/openai-compatible-endpoints.md).
## WebUI
```bash
@@ -68,4 +130,8 @@ python -m skillopt_webui.app [--port PORT] [--share]
| Argument | Default | Description |
|---|---|---|
| `--port` | 7860 | Port number |
| `--host` | `0.0.0.0` | Server bind address |
| `--share` | false | Create public Gradio link |
The default host binds every network interface. Use `--host 127.0.0.1` when
the dashboard should be reachable only from the local machine.
+142 -53
View File
@@ -1,85 +1,174 @@
# Configuration Reference
Complete reference for all SkillOpt configuration parameters.
SkillOpt loads structured YAML, resolves `_base_` inheritance, and flattens
the result for the trainer. Shipped defaults live in
`configs/_base_/default.yaml`; benchmark configs override them.
## Model
## Model and Backend Selection
Use explicit optimizer and target backends when the two roles differ or when
selecting the generic OpenAI-compatible backend.
| Backend | Optimizer | Target |
|---|:---:|:---:|
| `openai_chat` | ✓ | ✓ |
| `openai_compatible` | ✓ | ✓ |
| `claude_chat` | ✓ | ✓ |
| `qwen_chat` | ✓ | ✓ |
| `minimax_chat` | ✓ | ✓ |
| `codex_exec` | ✓ | ✓ |
| `claude_code_exec` | — | ✓ |
MiniMax currently has one shared deployment. `model.minimax_model` is applied
when MiniMax is the target; mixed-backend runs cannot independently choose a
MiniMax optimizer model and a different target model.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `model.backend` | str | `azure_openai` | Backend: `azure_openai` / `openai_chat` / `claude_code_exec` / `qwen` |
| `model.optimizer` | str | `gpt-5.5` | Optimizer model (for reflection & slow update) |
| `model.target` | str | `gpt-5.5` | Target model (for rollout execution) |
| `model.reasoning_effort` | str | `medium` | Reasoning effort level |
| `model.optimizer_backend` | str | `openai_chat` | Optimizer backend: `openai_chat` / `claude_chat` / `qwen_chat` / `minimax_chat` |
| `model.target_backend` | str | `openai_chat` | Target backend: chat backends plus execution harnesses |
| `model.qwen_chat_base_url` | str | `http://localhost:8000/v1` | Shared Qwen/vLLM OpenAI-compatible endpoint |
| `model.qwen_chat_enable_thinking` | bool | `false` | Shared Qwen thinking flag |
| `model.optimizer_qwen_chat_base_url` | str | — | Optimizer-specific Qwen/vLLM endpoint; overrides shared `qwen_chat_base_url` |
| `model.target_qwen_chat_base_url` | str | — | Target-specific Qwen/vLLM endpoint; overrides shared `qwen_chat_base_url` |
| `model.backend` | str | `azure_openai` | Backward-compatible high-level run label |
| `model.optimizer` | str | `gpt-5.5` | Optimizer deployment/model |
| `model.target` | str | `gpt-5.5` | Target deployment/model |
| `model.optimizer_backend` | str | `openai_chat` | Optimizer client path; chat backends plus `codex_exec` |
| `model.target_backend` | str | `openai_chat` | Target client path; chat or exec backend |
| `model.reasoning_effort` | str | `medium` | Shared reasoning effort |
| `model.rewrite_reasoning_effort` | str | empty | Optional full-rewrite effort override |
| `model.rewrite_max_completion_tokens` | int | `64000` | Full-rewrite output cap |
### Azure/OpenAI `openai_chat`
| Parameter | Default | Description |
|---|---|---|
| `model.azure_openai_endpoint` | empty | Shared Azure resource URL or compatibility-mode base URL |
| `model.azure_openai_api_version` | `2024-12-01-preview` | Azure API version |
| `model.azure_openai_api_key` | empty | Key for `api_key` or compatibility auth |
| `model.azure_openai_auth_mode` | empty | Config value; empty falls back to env, whose default is `azure_cli` |
| `model.azure_openai_ad_scope` | Azure Cognitive Services scope | AAD token scope |
| `model.azure_openai_managed_identity_client_id` | empty | Optional user-assigned identity client ID |
Every shared key also has an `optimizer_azure_openai_*` and
`target_azure_openai_*` form.
### Claude `claude_chat`
`claude_chat` launches an installed, authenticated Claude Code CLI with
`claude -p`; it does not instantiate an Anthropic API client. The executable
defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.
`ANTHROPIC_API_KEY` is one authentication option understood by the CLI.
### Qwen, MiniMax, and Exec Backends
| Parameter family | Description |
|---|---|
| `model.qwen_chat_*` | Shared `base_url`, `api_key`, `temperature`, `timeout_seconds`, `max_tokens`, and `enable_thinking` |
| `model.optimizer_qwen_chat_*` / `model.target_qwen_chat_*` | Per-role Qwen overrides |
| `model.minimax_*` | MiniMax `base_url`, `api_key`, shared `minimax_model`, `temperature`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target |
| `model.codex_exec_*` | Codex path, sandbox, profile, SDK mode, reasoning, network/search, and approval policy |
| `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap |
## Training (`train`)
| Parameter | Type | Default | DL Analogy | Description |
|---|---|---|---|---|
| `train.num_epochs` | int | 4 | Epochs | Number of training epochs |
| `train.batch_size` | int | 40 | Batch size | Tasks sampled per step |
| `train.accumulation` | int | 1 | Gradient accumulation | Accumulation rounds per step |
| `train.seed` | int | 42 | Random seed | Reproducibility seed |
| Parameter | Type | Default | Description |
|---|---|---|---|
| `train.num_epochs` | int | `4` | Training epochs |
| `train.train_size` | int | `0` | `0` derives the size from the dataset split |
| `train.steps_per_epoch` | int | derived | Runtime field recomputed from train size, batch size, and accumulation; configured values are overwritten |
| `train.batch_size` | int | `40` | Tasks sampled per step |
| `train.accumulation` | int | `1` | Accumulation rounds per step |
| `train.seed` | int | `42` | Random seed |
## Gradient / Reflection (`gradient`)
| Parameter | Type | Default | Description |
|---|---|---|---|
| `gradient.minibatch_size` | int | 8 | Reflect minibatch size |
| `gradient.merge_batch_size` | int | 8 | Patch merge batch size |
| `gradient.analyst_workers` | int | 16 | Parallel reflection workers |
| `gradient.max_analyst_rounds` | int | 3 | Max rounds of analyst reflection |
| `gradient.failure_only` | bool | `false` | Only reflect on failures |
| `gradient.minibatch_size` | int | `8` | Reflect minibatch size |
| `gradient.merge_batch_size` | int | `8` | Patch merge batch size |
| `gradient.analyst_workers` | int | `16` | Parallel reflection workers |
| `gradient.max_analyst_rounds` | int | `3` | Maximum analyst rounds |
| `gradient.failure_only` | bool | `false` | Reflect only on failures |
## Optimizer (`optimizer`)
| Parameter | Type | Default | DL Analogy | Description |
|---|---|---|---|---|
| `optimizer.learning_rate` | int | 4 | Learning rate | Max edit patches per step (edit budget) |
| `optimizer.min_learning_rate` | int | 2 | Min LR | Min edits for decay schedulers |
| `optimizer.lr_scheduler` | str | `cosine` | LR schedule | `constant` / `linear` / `cosine` / `autonomous` |
| `optimizer.skill_update_mode` | str | `patch` | — | `patch` / `rewrite_from_suggestions` / `full_rewrite_minibatch` |
| `optimizer.use_slow_update` | bool | `true` | Momentum | Epoch-boundary longitudinal comparison & guidance |
| `optimizer.slow_update_samples` | int | 20 | — | Samples for slow update evaluation |
| `optimizer.use_meta_skill` | bool | `true` | Meta-learning | Cross-epoch optimizer-side strategy memory |
| `optimizer.longitudinal_pair_policy` | str | `mixed` | — | `mixed` / `changed` / `unchanged` |
| Parameter | Type | Default | Description |
|---|---|---|---|
| `optimizer.learning_rate` | int | `4` | Maximum edit patches per step |
| `optimizer.min_learning_rate` | int | `2` | Floor for decaying schedules |
| `optimizer.lr_scheduler` | str | `cosine` | `constant`, `linear`, `cosine`, or `autonomous` |
| `optimizer.lr_control_mode` | str | `fixed` | `fixed`, `autonomous`, or `none` |
| `optimizer.skill_update_mode` | str | `patch` | `patch`, `rewrite_from_suggestions`, or `full_rewrite_minibatch` |
| `optimizer.use_slow_update` | bool | `true` | Epoch-boundary longitudinal update |
| `optimizer.slow_update_samples` | int | `20` | Longitudinal evaluation samples |
| `optimizer.slow_update_gate_with_selection` | bool | `false` | Gate slow-update guidance on the selection split |
| `optimizer.longitudinal_pair_policy` | str | `mixed` | `mixed`, `changed`, or `unchanged` |
| `optimizer.use_meta_skill` | bool | `true` | Cross-epoch optimizer memory |
| `optimizer.use_skill_aware_reflection` | bool | `false` | Enable skill-defect vs execution-lapse routing |
| `optimizer.skill_aware_appendix_source` | str | `both` | `both` or `failure_only` |
| `optimizer.skill_aware_consolidate_threshold` | int | `0` | Appendix compaction threshold; `0` disables it |
## Evaluation (`evaluation`)
| Parameter | Type | Default | Description |
|---|---|---|---|
| `evaluation.use_gate` | bool | `true` | Enable validation gating (accept/reject updates) |
| `evaluation.eval_test` | bool | `true` | Run test evaluation after training |
| `evaluation.use_gate` | bool | `true` | Accept only improvements when enabled; `false` records validation but force-accepts each candidate |
| `evaluation.gate_metric` | str | `hard` | `hard`, `soft`, or `mixed` |
| `evaluation.gate_mixed_weight` | float | `0.5` | Soft-score weight for `mixed` |
| `evaluation.use_semantic_density` | bool | `false` | Add the optional instruction-density bonus |
| `evaluation.semantic_density_weight` | float | `0.05` | Density bonus weight |
| `evaluation.leading_words` | list/str | built in | Optional custom high-influence words |
| `evaluation.sel_env_num` | int | `0` | Selection size; `0` uses the full split |
| `evaluation.test_env_num` | int | `0` | Test size; `0` uses the full split |
| `evaluation.eval_test` | bool | `true` | Run final test evaluation |
## Environment (`env`)
| Parameter | Type | Default | Description |
|---|---|---|---|
| `env.name` | str | | Benchmark name (e.g., `searchqa`, `docvqa`) |
| `env.data_path` | str | — | Path to dataset |
| `env.skill_init` | str | — | Path to initial seed skill (optional) |
| `env.name` | str | empty | Benchmark name |
| `env.skill_init` | str | empty | Initial skill document |
| `env.split_mode` | str | `ratio` | `ratio` or `split_dir` |
| `env.split_ratio` | str | `2:1:7` | Train:val:test ratio |
| `env.exec_timeout` | int | 120 | Per-task timeout in seconds |
| `env.out_root` | str | — | Output directory |
| `env.split_ratio` | str | benchmark/default | Train:validation:test ratio |
| `env.split_seed` | int | `42` | Deterministic split seed |
| `env.split_dir` | str | empty | Materialized train/val/test directory |
| `env.data_path` | str | empty | Raw data path for ratio mode |
| `env.split_output_dir` | str | empty | Optional materialized split output |
| `env.exec_timeout` | int | `120` | Per-task timeout in seconds |
| `env.out_root` | str | generated by the train/eval CLIs | Output directory |
## Azure OpenAI Credentials
Benchmark-specific `env` keys are passed through to the adapter.
## Credential Environment Variables
### Azure-family backend
| Variable | Description |
|---|---|
| `AZURE_OPENAI_ENDPOINT` / `model.azure_openai_endpoint` | Azure resource endpoint |
| `AZURE_OPENAI_API_KEY` / `model.azure_openai_api_key` | Azure API key |
| `OPENAI_API_KEY` | OpenAI API key (for `openai_chat` backend) |
| `ANTHROPIC_API_KEY` | Anthropic API key (for `claude_code_exec` backend) |
| `QWEN_CHAT_BASE_URL` | Shared local vLLM endpoint for `qwen_chat` |
| `QWEN_CHAT_MODEL` | Shared served model name for `qwen_chat` |
| `QWEN_CHAT_API_KEY` | Optional API key for the shared Qwen endpoint |
| `OPTIMIZER_QWEN_CHAT_BASE_URL` | Optimizer-specific local vLLM endpoint |
| `OPTIMIZER_QWEN_CHAT_MODEL` | Optimizer-specific served model name |
| `TARGET_QWEN_CHAT_BASE_URL` | Target-specific local vLLM endpoint |
| `TARGET_QWEN_CHAT_MODEL` | Target-specific served model name |
| `AZURE_OPENAI_ENDPOINT` | Shared Azure endpoint or compatibility base URL |
| `AZURE_OPENAI_API_VERSION` | Azure API version |
| `AZURE_OPENAI_AUTH_MODE` | `api_key`, `azure_cli`, `managed_identity`, or `openai_compatible` |
| `AZURE_OPENAI_API_KEY` | Key for `api_key` or `openai_compatible` mode |
| `AZURE_OPENAI_AD_SCOPE` | Optional AAD scope |
| `AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID` | Optional managed-identity client ID |
Use `OPTIMIZER_AZURE_OPENAI_*` and `TARGET_AZURE_OPENAI_*` for role-specific
overrides.
### Generic OpenAI-compatible backend
| Variable suffix | Shared / per-role forms |
|---|---|
| `BASE_URL` | `OPENAI_COMPATIBLE_BASE_URL`, `OPTIMIZER_OPENAI_COMPATIBLE_BASE_URL`, `TARGET_OPENAI_COMPATIBLE_BASE_URL` |
| `API_KEY` | Corresponding shared/optimizer/target `*_API_KEY` names |
| `MODEL` | Corresponding shared/optimizer/target `*_MODEL` names |
| `TEMPERATURE` | Corresponding shared/optimizer/target `*_TEMPERATURE` names |
| `MAX_TOKENS` | Corresponding shared/optimizer/target `*_MAX_TOKENS` names |
| `TIMEOUT_SECONDS` | Corresponding shared/optimizer/target `*_TIMEOUT_SECONDS` names |
The train/eval entry points set deployments from YAML `model.optimizer` and
`model.target` after backend initialization. For selected OpenAI-compatible or
Qwen roles, those values override the corresponding `*_MODEL` environment
variables; the environment model names mainly seed direct library use.
Other backend families use the authenticated Claude CLI (`CLAUDE_CLI_BIN`;
optionally `ANTHROPIC_API_KEY`), `QWEN_CHAT_*`, and `MINIMAX_*`.
SkillOpt-Sleep's compatible endpoint uses `AZURE_OPENAI_*`, not the research
backend's `OPENAI_COMPATIBLE_*`; see
[the Sleep endpoint guide](../sleep/openai-compatible-endpoints.md).
-117
View File
@@ -1,117 +0,0 @@
# SkillOpt-Sleep — controllable dreaming architecture
The sleep engine is no longer a single fixed pipeline. It is a controllable
offline "dream / imagination" loop the user steers. This documents the knobs
added in the four-stage refactor and how they map to the user's design.
## The mental model
> Sleep = an offline imagination rollout. Re-run the user's real
> tasks (and dream-augmented variants) many times, look at what went well vs
> badly, distil durable rules, and keep only what survives a real-task check —
> unless the user opts out of that check.
## 1. Data splits — train (dream) / val (real) / test (real)
The anti-overfitting foundation:
| Split | Source | Role |
|---|---|---|
| **train** | real tasks **+ dream-augmented** variants | drives reflection (the imagination pool — over-dreaming is fine) |
| **val** | **real only**, disjoint from test | gates updates (prevents overfitting) |
| **test** | **real only**, disjoint from val | the final held-out measure, kept close to real usage |
Hard guarantee (unit-tested): a task with `origin='dream'` **never** lands in
val or test. `assign_splits(val_fraction, test_fraction)` does the deterministic
3-way split; gbrain's own held-out maps to our `test`.
## 2. The validation gate is optional
`--gate on` (default): an edit is accepted only if it strictly improves the
**val** score — the SkillOpt discipline that blocks regressions and reward
hacking.
`--gate off`: greedy. Edits are kept without the hard val-improvement
requirement (the user decides they don't want hard filtering), but val/test
movement is still reported (`greedy_improved` / `greedy_regressed` /
`greedy_flat`) so nothing is hidden.
## 3. Slow-update — long-term memory, gate-independent
Even with the gate off, the engine runs a **slow-update** at the end of the
nights: it compares behaviour under the first-night vs final skill across the
val tasks and distils durable longitudinal guidance into a **protected field**
(`<!-- SLOW_UPDATE_START --> … <!-- SLOW_UPDATE_END -->`, the same markers as
the main SkillOpt repo). Step-level edits never touch this field. This is the
"short-term experience → long-term memory" consolidation; turning the gate off
does not cost you long-term memory.
## 4. Budget — the user picks the spend
`--budget-tokens N` / `--budget-minutes M`: the engine auto-plans depth
(`nights × rollouts_per_task`) to fit the budget (`plan_depth`). Stops cleanly
when exhausted and logs what it skipped — no silent truncation. The whole thing
is offline imagination on the user's own quota.
## 5. Multi-rollout contrastive reflection — the imagination core
`--rollouts-k K` (K>1): each train task is rolled out K times. The optimizer is
shown the **high-scoring vs low-scoring** attempts of the same task and asked
what the good ones did that the bad ones didn't, distilling a general rule. This
is a far stronger signal than a single failure, and it is exactly the user's
"run it many times, learn from the contrast" idea. Tasks with the highest score
*spread* (some passed, some failed) are the most informative and are prioritised.
## 6. Multi-objective reward — accuracy ↑, tokens ↓, latency ↓
Every rollout records its `tokens` and `latency_ms`.
`multi_objective_reward(w_acc, w_tokens, w_latency)` is a weighted reward so a
skill can be optimised to be **cheaper and faster**, not only more accurate
(cost terms normalised against a reference; default weights = accuracy-only, so
existing behaviour is unchanged). This turns "gets better the more you use it"
into "more accurate, cheaper, and faster the more you use it".
## 7. User preferences as a prior
`--preferences "<free text>"`: injected into the optimizer's reflect prompt as a
prior (set on the optimizer model for dual backends), so the user's stated
preferences steer what rules get written.
## How the knobs compose (one command)
```bash
python -m skillopt.sleep.experiments.run_gbrain \
--optimizer-backend claude --optimizer-model sonnet \ # strong optimizer
--target-backend claude --target-model haiku \ # cheap target (transfer)
--seeds thorough-analyst \
--gate on \ # or off for greedy
--rollouts-k 2 \ # contrastive imagination
--budget-tokens 60000 \ # auto-plan depth
--preferences "Prefer concise, British English." \ # prior
--nights 3
```
All of this is exercised by the deterministic test suite (29 tests) and
validated on real Claude + Codex (see `real_api_results.md` / `FINAL_REPORT.md`).
## Real cross-validation of the new features (Claude ⟷ Codex)
Three live runs exercised the new code paths on both runtimes (raw logs under
`docs/sleep/raw/crosscheck_*.txt`):
| # | Config | What it proves | Result |
|---|---|---|---|
| **A** | Claude Sonnet→Haiku, **gate=off**, **rollouts_k=2** | greedy mode + multi-rollout + 3-way split (val & test both reported) | brief-writer **test 0→1.00**, action `greedy_improved`, val=1.0 test=1.0 |
| **B** | **Codex**, gate=on, **rollouts_k=2** | new paths on the other runtime | brief-writer **test 0→1.00**, 2-night `accept_new_best`, val+test reported |
| **C** | Claude Sonnet→Haiku, thorough-analyst, 3 nights | **slow-update** long-term memory fires | test 0→0.33 (val gate holds nights 23) and the slow-update distilled a durable meta-rule |
The slow-update guidance C produced is the kind of cross-night lesson the field
is for — note it is general, not task-specific:
> *"On character-constrained tasks (≤1200 chars), plan structure before writing:
> allocate space per point explicitly and cut until the outline fits, then fill —
> never draft freely and trim after."*
Takeaways confirmed live: the **gate-off greedy path**, the **3-way val/test
split**, **multi-rollout** on both runtimes, and the **gate-independent
slow-update** all work with real models on both Claude and Codex.
-160
View File
@@ -1,160 +0,0 @@
# SkillOpt-Sleep — final validation report
> **What this is:** the consolidated, presented results for the SkillOpt-Sleep
> Claude Code plugin — a tool that lets a local agent improve itself overnight by
> reviewing past sessions, replaying tasks, and consolidating validated memory +
> skills behind a held-out gate. Every real-model result here was run on **both
> Claude and Codex**, including the honest failures and the bugs they exposed.
**Date:** 2026-06-07 · **Branch:** `feat/claude-code-sleep-plugin`
**Benchmark:** [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1`
(the same public suite gbrain scores its own optimizer against).
**Protocol:** a deliberately deficient skill → 12 offline "nights" (replay →
reflect → bounded **gated** edit) → score the **held-out** task set (never
optimized against). Held-out scoring uses a local rule judge — the optimizer
never grades itself.
---
## 1. Headline — clean, all green (full gbrain parity)
**Strong optimizer (Claude Sonnet 4.6) → weak target (Claude Haiku 4.5)**, fully
isolated calls, 3 held-out tasks/seed. All **4** gbrain `skillopt-v1` seeds —
matching gbrain's own scorecard coverage:
| Optimizer → Target | Seed | Flaw | Held-out before → after | Nights |
|---|---|---|---|---|
| Sonnet → Haiku | brief-writer | missing structure | **0.00 → 1.00** | 1 |
| Sonnet → Haiku | advisor | no verdict | **0.00 → 1.00** | 1 |
| Sonnet → Haiku | thorough-analyst | no length discipline | **0.00 → 1.00** | 2 |
| Sonnet → Haiku | quick-answerer | never uses tools | **0.00 → 1.00** | 1 |
| Codex → Codex (gpt-5.5) | brief-writer | missing structure | **0.00 → 1.00** | 2 |
| Codex → Codex (gpt-5.5) | advisor | no verdict | **0.00 → 1.00** | 2 |
**4/4 Claude seeds reach a perfect held-out score** (gbrain's headline is the same
4/4 0→1.00), plus Codex on the text seeds. Every change is gated and staged.
The `quick-answerer` seed is judged by **real tool use** (`tool_called: search`):
the deficient skill says *"never look anything up — answer from memory"*; the
optimizer wrote an OVERRIDE rule, and the Haiku target **genuinely invoked a
`./search` shell tool** (detected from the tool's own log, not self-reported) →
held-out 1.00. The thorough-analyst run shows textbook **2-night convergence**
(0.33 → 1.00).
---
## 2. The finding that matters most: the optimizer model is decisive
This is the direct answer to "let me specify the optimizer and target separately,
and watch the skill." It matters a lot:
| Optimizer | Target | brief-writer | advisor | thorough-analyst |
|---|---|---|---|---|
| **Haiku** (weak) | Haiku | 1.00 *or* 0.00 (flaky) | 1.00 | 0.33 |
| **Sonnet** (strong) | Haiku | **1.00** | **1.00** | **1.00** |
A weak self-optimizing model (Haiku proposing its own edits) is **unreliable**
it intermittently emits non-JSON and wastes a night, so the same seed scores 1.00
on one run and 0.00 on another. A **strong optimizer** (Sonnet) reliably produces
clean, concrete edit rules and lifts every seed to 1.00. This is exactly the
SkillOpt design (strong optimizer, frozen target) and the reason the
optimizer/target split is a first-class feature here.
**Practical guidance baked into the plugin:** default to a strong optimizer; the
sweep's `direct` plan now uses Sonnet→Haiku.
---
## 3. Two real bugs we found by running against live models
Per gbrain's own lesson ("the bugs that matter only show up when the whole thing
actually runs"), the first live runs surfaced two real defects. Both are fixed.
1. **Ambient-context leak (Claude).** `claude -p` was injecting the user's
*global* skills + project `CLAUDE.md` into every optimizer/target call — one
reflect call literally returned a 21 KB list of the machine's installed skills
instead of JSON edits, so the night produced no edits and the gate rejected.
Some early Claude "successes" were partly leak-assisted. **Fix:** run isolated
`--bare --disable-slash-commands --disallowedTools '*'
--exclude-dynamic-system-prompt-sections`, clean temp cwd. (Codex was never
affected; the real `@openai/codex` binary runs in its own clean context.)
2. **Wasted nights on transient non-JSON.** A single malformed reply zeroed a
night. **Fix:** `reflect()` retries once with a firmer "JSON only" instruction.
We report these because a tool people build on has to be honest about where it was
weak and what changed.
---
## 4. Cross-model transfer (the price-difference value prop)
> *Optimize cheap overnight, deploy anywhere.* A skill is just text, so a good
> rewrite should help a model it was never optimized on.
Optimize on SOURCE, **freeze** the learned skill, evaluate held-out on TARGET with
no further optimization. All four pairs are positive — including **across
runtimes** (Codex ↔ Claude):
| Source (optimizer) | Target (deploy) | Seed | Target baseline → transferred | Gain |
|---|---|---|---|---|
| Claude Haiku (cheap) | Claude Sonnet (expensive) | brief-writer | 0.00 → **1.00** | +1.00 |
| Claude Sonnet | Claude Haiku | brief-writer | 0.00 → **1.00** | +1.00 |
| **Codex** | **Claude Haiku** | brief-writer | 0.00 → **1.00** | +1.00 |
| **Claude Haiku** | **Codex** | brief-writer | 0.00 → **1.00** | +1.00 |
**4/4 transfers positive.** A skill optimized on a cheap model deploys for free on
an expensive one, and skills move between Codex and Claude — the Sleep-setting
analogue of SkillOpt's cross-model and cross-harness transfer tables. This is the
quantified answer to "optimize cheap overnight, deploy anywhere."
Full machine-generated scorecard: [`benchmark_report.md`](benchmark_report.md)
(source data `sweep.jsonl`).
---
## 5. Reproduce everything
```bash
git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals
cd <repo>/SkillOpt-sleep
# the clean headline result (strong optimizer -> weak target)
python3.12 -m skillopt.sleep.experiments.run_gbrain \
--optimizer-backend claude --optimizer-model sonnet \
--target-backend claude --target-model haiku \
--seeds brief-writer,advisor,thorough-analyst \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --nights 2 --limit-replay 3 --limit-holdout 3
# Codex self-optimized
python3.12 -m skillopt.sleep.experiments.run_gbrain --backend codex --seeds brief-writer \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --nights 2 --limit-replay 3 --limit-holdout 3
# cross-model transfer
python3.12 -m skillopt.sleep.experiments.run_transfer \
--source-backend claude --source-model haiku --target-backend claude --target-model sonnet \
--seeds brief-writer
# the whole sweep + report
python3.12 -m skillopt.sleep.experiments.sweep --plan full \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --out docs/sleep/sweep.jsonl
python3.12 -m skillopt.sleep.experiments.report --in docs/sleep/sweep.jsonl --out docs/sleep/benchmark_report.md
# deterministic, no API (CI anchor)
python3.12 -m skillopt.sleep.experiments.run_experiment --persona researcher --assert-improves
```
Raw run logs are under `docs/sleep/raw/`.
---
## 6. Honest limitations
- **Latency:** each CLI call is ~1415 s startup-dominated, so runs are capped at
a few tasks/nights. Fine for nightly cron; we note it plainly.
- **Weak optimizers are flaky:** use a strong optimizer model (§2).
- **Tool-use seed covered honestly:** `quick-answerer` (`tool_called: search`)
runs a real tool loop — a callable `./search` shim, detected from its log.
Deeper multi-tool / multi-turn workflows are future work.
- **Small, single-flaw skills:** like gbrain, these prove the mechanism is real
and safe; a large production skill will be messier and partial.
-53
View File
@@ -1,53 +0,0 @@
TITLE:
Add SkillOpt-Sleep: nightly offline self-evolution plugins (Claude Code, Codex, Copilot)
BODY:
## Summary
Adds **SkillOpt-Sleep** — a nightly offline "sleep cycle" that gives a local
coding agent the deployment-time analogue of training: it reviews past sessions,
replays recurring tasks on the user's own API budget, and consolidates what it
learns into **validated** long-term memory and skills behind a held-out gate.
Synthesizes SkillOpt (validation-gated bounded text edits), Claude Dreams
(offline consolidation; review-then-adopt), and the agent-sleep idea
(short-term experience -> long-term competence).
Shipped as plugins for **three agents**, one engine + three thin shells:
- **Claude Code** — `.claude-plugin` + `/sleep` command + skill + hooks
- **Codex** — `~/.codex/prompts/sleep.md` + `~/.agents/skills` + `install.sh`
- **Copilot** — a stdlib-only MCP server exposing `sleep_*` tools
## Design notes
- **Open-source tool, decoupled from the research code.** The engine lives in the
new top-level `skillopt_sleep/` package with **zero dependency** on the paper's
`skillopt/` experiment package (the validation gate is vendored).
- Controllable: optional gate (`--gate on|off`), train(dream)/val(real)/test(real)
splits, slow-update long-term memory, token/time budget, multi-rollout
contrastive reflection, multi-objective reward (accuracy/tokens/latency), user
preferences, and separate optimizer/target models.
## Validation (real models)
On the public [gbrain-evals](https://github.com/garrytan/gbrain-evals)
`skillopt-v1` benchmark, deficient skills go **0.00 -> 1.00** on held-out sets
with **both Claude and Codex** (all 4 seeds, including a real tool-use loop);
cross-model transfer is positive; the gate blocks regressions. Independently
load-tested on a fresh non-benchmark persona ("SQL must always include LIMIT"):
held-out test **0.00 -> 1.00** on both backends. See `docs/sleep/FINAL_REPORT.md`
and `docs/sleep/plugin_load_test.md`.
## Tests
- 29 deterministic unit tests (`tests/test_sleep_engine.py`), no API key required.
- `python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves`
proves held-out lift and that the gate blocks a harmful edit.
## Test plan
- [ ] `python -m unittest tests.test_sleep_engine` (29 pass)
- [ ] `python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves`
- [ ] Claude Code: `/plugin marketplace add ./plugins/claude-code` -> `/sleep status`
- [ ] Codex: `bash plugins/codex/install.sh`
- [ ] Copilot: MCP server `tools/list` returns the `sleep_*` tools
+132
View File
@@ -0,0 +1,132 @@
# SkillOpt-Sleep 😴 — deployment-time companion (preview)
**SkillOpt-Sleep** applies SkillOpt's discipline to your *own daily usage*. It gives a
local coding agent a nightly **sleep cycle** that reviews your past sessions, replays
your recurring tasks on your own API budget, and consolidates what it learns into
**validated** long-term memory and skills — behind a held-out gate, staged for your
review. It requires **no weight training** and adds no separate optimization loop to
normal agent requests.
> **Preview.** This is an early preview we are actively iterating on; interfaces and
> defaults may change. The engine lives in the top-level [`skillopt_sleep/`](https://github.com/microsoft/SkillOpt/tree/main/skillopt_sleep)
> package with **zero dependency** on the paper's `skillopt/` code (the validation gate
> is vendored).
## How it works
One "night":
```
harvest Claude Code / Codex transcripts → mine recurring tasks → replay offline
→ consolidate (reflect → bounded edit → GATE on real held-out tasks)
→ stage proposal → (you) adopt
```
It synthesizes **SkillOpt** (validation-gated bounded text edits), **Claude Dreams**
(offline consolidation; review-then-adopt), and the **agent-sleep** idea (short-term
experience → long-term competence).
> **Data boundary.** Harvesting is local and read-only. The `mock` backend makes no
> provider calls. A real backend, however, sends truncated excerpts from harvested
> sessions and derived tasks to the provider you select for mining, replay, judging,
> and reflection. Outbound prompts are not currently guaranteed to be secret-free;
> review your transcript source and provider policy before running on sensitive
> projects. For a reviewable workflow, harvest to a task file, inspect/redact it, mark
> it `"reviewed": true`, and then replay that file with the real backend.
## How to use it
### Quickest path: the `skillopt-sleep` CLI (pip)
```bash
pip install skillopt # installs the engine + the `skillopt-sleep` command
skillopt-sleep dry-run # harvest + mine + replay, report only; stages nothing
skillopt-sleep run # a full nightly cycle; the proposal is staged for review
skillopt-sleep status # show state + the latest staged proposal
skillopt-sleep adopt # apply the latest staged proposal
skillopt-sleep schedule # install a nightly cron entry for this project
```
> **Version note.** This page tracks `main`. PyPI 0.2.0 provides the base
> commands above. Sleep handoff, non-Azure OpenAI-compatible endpoints, and
> `--preferences` landed later and require a source install from `main` until
> the next release.
The per-agent integrations below still come from the repo; the CLI above is the
standalone, pip-only way to run a cycle. Claude Code, Codex, Copilot, and Devin wrap
the shared engine. OpenClaw is a separate reference adaptation and has its own setup.
One engine, thin per-agent shells (see [`plugins/`](https://github.com/microsoft/SkillOpt/tree/main/plugins)):
| Platform | Folder | Install |
|---|---|---|
| **Claude Code** | [`plugins/claude-code`](https://github.com/microsoft/SkillOpt/tree/main/plugins/claude-code) | `/plugin marketplace add ./plugins/claude-code``/skillopt-sleep` |
| **Codex** | [`plugins/codex`](https://github.com/microsoft/SkillOpt/tree/main/plugins/codex) | `bash plugins/codex/install.sh``skillopt-sleep` skill |
| **Copilot** | [`plugins/copilot`](https://github.com/microsoft/SkillOpt/tree/main/plugins/copilot) | register `plugins/copilot/mcp_server.py` as an MCP server |
| **Devin** | [`plugins/devin`](https://github.com/microsoft/SkillOpt/tree/main/plugins/devin) | register `plugins/devin/mcp_server.py` as an MCP server |
| **OpenClaw** | [`plugins/openclaw`](https://github.com/microsoft/SkillOpt/tree/main/plugins/openclaw) | adapt the reference wrapper and paths for your installation |
To use DeepSeek, vLLM, Ollama, or another Chat Completions server, see
**[OpenAI-compatible endpoints](openai-compatible-endpoints.md)**. That guide also
documents the separate HTTPS-only boundary for Azure managed-identity credentials.
Deterministic proof (no API key):
`python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves`.
### Opt-in: experience replay & dream rollouts
Two consolidation mechanisms, both default **off** (behavior is unchanged unless you
enable them). They strengthen the nightly update when your tasks have a clean
correctness signal; the validation gate still governs what ships.
| Config knob | Default | Effect |
|---|---|---|
| `dream_rollouts` | `1` | Run each task K times → learn from the good-vs-bad contrast (contrastive reflection). |
| `recall_k` | `0` | Associative recall — pull the K most-similar past tasks (from a persisted archive) into tonight's dream. |
| `dream_factor` | `0` | Add N lightweight synthetic variants of each task. |
## Results
> 📊 **More results & analysis — the gate-safety stress test, experience-replay
> scaling, and the dream-diversity ablation — are in
> [`docs/sleep/RESULTS.md`](RESULTS.md).** The highlights:
**Controlled experiment recipe (not the shipping CLI defaults).** 5 nights × 10 new
real "today" tasks per night; the full held-out **test** split is scored before night
1 (baseline) and after night 5 (after); optimizer = GPT-5.5; single seed (42). The
experiments use the shipped consolidation and gate components, while the nightly CLI
and benchmark harnesses remain separate entry points. Numbers are absolute held-out
accuracy; **Δ** = `after baseline` in percentage points.
**(a) End-to-end on real agents — [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1`.**
Deficient seed skills go **0.00 → 1.00** on the held-out set with **both Claude Code
and Codex** as the target agent (all 4 seeds, including a real tool-use loop).
**(b) Experience replay scales the gain — SearchQA** (1,400-item held-out test,
SQuAD exact-match; target = GPT-5.5; **validation-gated**):
| Replay config (`dream_rollouts=5`) | Baseline → After | Δ (pts) |
|---|---|---|
| `recall_k=10` | 0.802 → 0.834 | +3.1 |
| `recall_k=20` | 0.803 → 0.848 | **+4.5** |
| full-history replay *(reference, not a shipping default)* | 0.796 → 0.851 | +5.6 |
| `recall_k=10`, `dream_rollouts=8` *(more dreaming, same recall)* | 0.798 → 0.835 | +3.7 |
The gain rises monotonically with how much relevant past experience is recalled. The
same SearchQA cell **without** the gate (`recall_k=10`) is 0.808 → 0.839 (+3.1).
**(c) Second benchmark — SpreadsheetBench** (280-item held-out test; the agent's
generated openpyxl code is executed and compared cell-by-cell to a golden workbook;
target = GPT-5.4-nano; gate-free + the output-contract guardrail): 0.279 → 0.314 (**+3.6**).
**(d) Honest scope.** These gains hold where tasks recur and have a checkable
correctness signal. On saturated or noisy benchmarks (e.g. a strong model already
near ceiling) the effect is **flat within run-to-run noise** — single-seed baseline
variance here is ±12 pts, so treat sub-~1.5 pt differences as noise. The validation
gate keeps the worst case bounded; keep it **on** by default.
## Learn more
See the [SkillOpt documentation index](../index.md), the
[CLI reference](../reference/cli.md), and the integration-specific READMEs under
[`plugins/`](https://github.com/microsoft/SkillOpt/tree/main/plugins).
+191
View File
@@ -0,0 +1,191 @@
# SkillOpt-Sleep — results & analysis
This is the evidence behind SkillOpt-Sleep: does a nightly, offline sleep cycle
actually make a *deployed* agent better, and is it safe to run unattended? We
answer with a controlled deployment-scale study built from the same shipped
consolidation and gate components. Its multi-night benchmark recipe is an
experiment configuration, not the default configuration of the nightly CLI.
## Setup
**Protocol (identical for every cell unless stated).** 5 nights; each night adds
**10 new real "today" tasks**; the skill carries over and is refined night to
night. The full held-out **test** split is scored before night 1 (*baseline*) and
after night 5 (*after*); **Δ = after baseline** in percentage points. Optimizer
model = **GPT-5.5**; single seed (42). The measurements use the shipped replay,
consolidation, and gate implementations. The nightly CLI and the checked-in
benchmark convenience harnesses are separate entry points and do not all call one
shared wrapper function.
**Benchmarks** (real evaluators, not format heuristics):
| Benchmark | Held-out test | Scoring |
|---|---|---|
| SearchQA | 1,400 items | SQuAD exact-match vs gold |
| LiveMathematicianBench | 124 items | multiple-choice label (choices shuffled per item) |
| SpreadsheetBench | 280 items | the agent's generated openpyxl code is **executed**, output workbook compared cell-by-cell to a golden file |
**Targets:** GPT-5.5, GPT-5.4-mini, GPT-5.4-nano. **Modes:** validation-gated
(default) and gate-free.
---
## 1. The headline — the validation gate is what makes nightly self-evolution *safe*
Self-evolution is easy to build and easy to ruin: an optimizer that accepts its
own "lessons" unconditionally can adopt a plausible-but-wrong rule and an obedient
model will follow it off a cliff. We reproduced exactly that failure, then showed
the gate prevents it.
Stress case — **GPT-5.4-nano on SearchQA**, weak model on a single-sample (degraded)
reflection signal, same nights, same candidate edits, gate **off** vs **on**:
| | Night 0 → Night 5 | Δ |
|---|---|---|
| **no gate** | 0.554 → **0.026** | **52.8** |
| **with gate (default)** | 0.570 → 0.570 | 0.0 |
Ungated, the optimizer learned "answer with the document-title string, verbatim";
the model complied and accuracy collapsed night after night
(0.554 → 0.490 → 0.325 → 0.031 → 0.034 → 0.026). The gated twin **rejected every one
of those edits** and never lost a point. This single experiment is the core
argument for SkillOpt-Sleep's design, and why the gate ships **on by default**.
---
## 2. Cross-model scaling — bigger gains where there's headroom
The same protocol on a weaker target model (**GPT-5.4-nano**, optimizer = GPT-5.5)
produces substantially larger gains — because the weaker model has more room to
learn. This is the realistic "cheap deployed agent, strong overnight optimizer"
scenario:
| Config (SearchQA, nano, gated) | Baseline → After | Δ | Night-by-night |
|---|---|---|---|
| **cumulative replay, nights=5** | 0.560 → **0.679** | **+11.9** | 0.560 → 0.626 → 0.665 → 0.665 → 0.665 → 0.679 |
| recall_k=20, nights=5 | 0.566 → 0.681 | +11.5 | 0.566 → 0.659 → 0.685 → 0.685 → 0.681 → 0.681 |
| cumulative, nights=8 | 0.562 → 0.657 | +9.5 | saturates after night 5 |
Both replay strategies (cumulative and recall) agree within 0.4 pt — the gain is
robust across configurations.
**Compared to GPT-5.5 on the same benchmark (SearchQA, gated):**
| Target model | Best Δ | Baseline | Headroom |
|---|---|---|---|
| GPT-5.4-nano | **+11.9** | 0.560 | 44 pt |
| GPT-5.5 | +6.0 | 0.798 | 20 pt |
The story: **SkillOpt-Sleep helps most where there's the most to learn** — weaker
deployed models benefit ~2× as much from the same nightly optimization. This is
also the economical deployment pattern (cheap inference model + one strong
overnight optimizer call).
---
## 3. Experience replay turns a one-time bump into a climb
The plugin's two opt-in knobs (`recall_k`, `dream_rollouts`) are what produce the
gains. On **SearchQA, GPT-5.5, gated** — the gain rises monotonically with how
much relevant past experience is recalled:
| Replay (`dream_rollouts=5`) | Baseline → After | Δ |
|---|---|---|
| `recall_k=10` | 0.802 → 0.834 | +3.1 |
| `recall_k=20` | 0.803 → 0.848 | **+4.5** |
| full-history (reference, not a default) | 0.796 → 0.851 | +5.6 |
And the curve genuinely **climbs across nights** rather than jumping once and
plateauing — full-history replay, gated, night by night:
```
0.798 → 0.814 → 0.854 → 0.854 → 0.854 → 0.858
```
The gate accepts a new, better skill as late as **night 5** (0.854 → 0.858).
Replay-policy ablation (SearchQA, GPT-5.5):
| Replay policy | Gate-free Δ | Gated Δ |
|---|---|---|
| none (tonight's tasks only) | +3.9 | +2.0 |
| **recall k=10 (opt-in experiment)** | +5.1 | +4.4 |
| cumulative (full history) | +4.8 | +6.0 |
Recall captures most of cumulative's benefit at a fraction of the per-night cost.
---
## 4. Sensitivity around the experiment recipe
We swept `dream_factor`, `rollouts`, `per_night`, and `nights` on the nano cell
(SearchQA, gated) around the study recipe: `dream_factor=2`, `rollouts=5`,
`per_night=10`, and `nights=5`. These are **experiment values**, not the shipping
defaults (`dream_factor=0`, `dream_rollouts=1`, and `recall_k=0`):
| Variant | Δ | vs experiment baseline (+11.9) |
|---|---|---|
| dream_factor=4 (baseline 2) | +8.8 | 3.1 |
| rollouts=10 (baseline 5) | +9.5 | 2.4 |
| per_night=15 (baseline 10) | +2.7 | 9.2 |
| nights=8 (baseline 5) | +9.5 | 2.4 |
Every tested direction away from that baseline reduced the measured gain in this
cell. The result supports that particular study recipe; it does not establish a
universal optimum. Shipping stays conservative, and users must opt in to additional
dream rollouts or recall after considering task quality and provider cost.
---
## 5. Why these gains exist — the dream-diversity fix (and a rigor note)
Reflection learns from the **contrast** between good and bad rollouts of the same
task, which requires the K dream rollouts to be *independent samples*. An early
version of the engine collapsed them to one cached sample, so contrastive
reflection never fired. Fixing that, then adding recall, is what produces the
gains in Sections 12. Measured across an 18-cell deployment sweep (3 benchmarks ×
3 targets × 2 modes), under three engine configurations:
| Engine configuration | mean Δ | worst-cell Δ | cells > +0.5 | cells < 0.5 |
|---|---|---|---|---|
| single-sample reflection (degraded) | 2.66 | **52.8** | 7 / 18 | 5 / 18 |
| diverse rollouts (K=5), no recall | +0.24 | 4.0 | 6 / 18 | 7 / 18 |
| **diverse rollouts + recall (experiment recipe)** | **+0.53** | **2.4** | 7 / 18 | 7 / 18 |
The catastrophic 52.8 is removed **at its source** by diverse rollouts: the same
gate-free nano-SearchQA cell goes 0.554 → **0.586 (+2.7)** with no gate at all once
the dream is fixed. Recall then lifts the grid mean and tightens the worst case.
This is **defense in depth, each layer measured**: diverse rollouts propose better
edits, recall remembers relevant experience, and the gate catches whatever still
slips through.
---
## 6. End-to-end on real agents
On the public [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1`
benchmark — designed for exactly this learnable-gap setting — deficient seed skills
go **0.00 → 1.00** on the held-out set with **both Claude Code and Codex** as the
target agent (all 4 seeds, including a real tool-use loop), and the two agents
cross-verify each other's consolidated skills.
---
## 7. Honest scope & limitations
- **Where it helps:** recurring tasks with a checkable correctness signal and real
headroom. That is the plugin's actual use case (your repeated daily tasks and
house rules the agent keeps missing).
- **Where it's flat:** saturated tasks on strong models, or noisy tasks with a weak
learning signal — within run-to-run noise.
- **Single seed.** Cells aggregate one seed per config; treat sub-~1.5 pt
differences as noise. Spot seed-robustness check on the one flagged cell
(nano SearchQA gated): seeds 42/43/44 give 1.9 / +3.6 / +4.7 (3-seed mean
**+2.1**), i.e. the tabled 1.9 is a pessimistic draw, not the typical outcome.
- **Keep the gate on.** It is the difference between bounded downside (2.4) and a
52.8 collapse. Gate-free mode is for users who cannot hold out a validation set
and is additionally protected by the output-contract guardrail.
---
Back to the module overview: [`docs/sleep/README.md`](README.md) ·
documentation index: [SkillOpt documentation](../index.md).
-41
View File
@@ -1,41 +0,0 @@
# SkillOpt-Sleep — benchmark report
Auto-generated from `sweep.jsonl`. Benchmark: [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1` (deficient skills, train/held-out split, local rule judge — no judge-API).
Held-out scores are computed by the harness, not the optimizer.
## Direct improvement (optimize, then deploy)
| Optimizer → Target | Seed | Held-out before | Held-out after | Nights | Tokens |
|---|---|---|---|---|---|
| claude:sonnet → claude:haiku | brief-writer | 0.00 | **1.00** | 2 | 6657 |
| claude:sonnet → claude:haiku | advisor | 0.00 | **1.00** | 2 | 7891 |
| claude:sonnet → claude:haiku | thorough-analyst | 0.00 | **1.00** | 2 | 17960 |
| codex:default → codex:default | brief-writer | 0.00 | **1.00** | 2 | 9969 |
| codex:default → codex:default | advisor | 0.00 | **1.00** | 2 | 6210 |
| claude:sonnet → claude:haiku | quick-answerer | 0.00 | **1.00** | 2 | 10988 |
| codex:default → codex:default | quick-answerer | 0.00 | **1.00** | 2 | 7347 |
**7/7 configurations improved on held-out.**
## Cross-model transfer (optimize on SOURCE, deploy frozen on TARGET)
The price-difference story: spend cheap tokens optimizing overnight, then deploy the frozen skill on any model with no further optimization.
| Source (optimizer) | Target (deploy) | Seed | Target baseline | Transferred | Gain |
|---|---|---|---|---|---|
| claude:haiku | claude:sonnet | brief-writer | 0.00 | **1.00** | +1.00 |
| claude:sonnet | claude:haiku | brief-writer | 0.00 | **1.00** | +1.00 |
| codex:default | claude:haiku | brief-writer | 0.00 | **1.00** | +1.00 |
| claude:haiku | codex:default | brief-writer | 0.00 | **1.00** | +1.00 |
**4/4 transfers were positive** (frozen skill helped a different model than it was optimized on).
## How to reproduce
```bash
git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals
python -m skillopt.sleep.experiments.sweep --plan full \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --out docs/sleep/sweep.jsonl
python -m skillopt.sleep.experiments.report \
--in docs/sleep/sweep.jsonl --out docs/sleep/benchmark_report.md
```
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Reference launcher for running SkillOpt-Sleep against an OpenAI-compatible
endpoint (DeepSeek shown here), plus an Antigravity `session-end` hook.
This is a *sanitized example*, not a supported entry point. Adapt the paths and
provider details to your environment. No API keys are hardcoded the key is read
from an .env file or the process environment.
Usage:
python runner.py run # run a full sleep cycle against DeepSeek
python runner.py dry-run # harvest + replay, report only
python runner.py session-end # Antigravity Stop-hook: append rollout evidence
"""
import os
import re
import sys
import json
import subprocess
import datetime
from pathlib import Path
# --- Configure these for your environment -----------------------------------
# Path to a file containing your provider key as `sk-...` (kept out of source).
PROVIDER_ENV_FILE = Path(os.environ.get("SKILLOPT_PROVIDER_ENV_FILE", "provider.env"))
# Endpoint + model for the OpenAI-compatible provider.
PROVIDER_ENDPOINT = os.environ.get("SKILLOPT_PROVIDER_ENDPOINT", "https://api.deepseek.com")
PROVIDER_MODEL = os.environ.get("SKILLOPT_PROVIDER_MODEL", "deepseek-v4-pro")
# Project whose SKILL.md files the sleep cycle should evolve.
PROJECT_DIR = os.environ.get("SKILLOPT_PROJECT_DIR", os.getcwd())
# Where the session-end hook appends rollout evidence.
ROLLOUT_LOG = Path(os.environ.get("SKILLOPT_ROLLOUT_LOG", "brain/rollout-evidence.jsonl"))
# ----------------------------------------------------------------------------
def load_provider_key(env: dict) -> None:
"""Ensure DEEPSEEK_API_KEY is set, reading it from PROVIDER_ENV_FILE if needed."""
if env.get("DEEPSEEK_API_KEY"):
return
try:
text = PROVIDER_ENV_FILE.read_text(encoding="utf-8")
except OSError:
return
m = re.search(r"sk-[A-Za-z0-9]+", text)
if m:
env["DEEPSEEK_API_KEY"] = m.group(0)
def main() -> None:
if len(sys.argv) < 2:
print("Usage: runner.py [dry-run|run|status|adopt|session-end]")
sys.exit(1)
command = sys.argv[1]
# Antigravity Stop-hook: enrich future nights with task-outcome metadata.
if command == "session-end":
ROLLOUT_LOG.parent.mkdir(parents=True, exist_ok=True)
outcome = {
"timestamp": datetime.datetime.now().isoformat(),
"event": "SessionEnd",
"metadata": "Appended task outcome metadata",
}
with open(ROLLOUT_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(outcome) + "\n")
print("Rollout evidence metadata appended.")
return
env = os.environ.copy()
load_provider_key(env)
if env.get("DEEPSEEK_API_KEY"):
# OpenAI-compatible path — see docs/sleep/openai-compatible-endpoints.md
backend = "azure_openai"
env["PYTHONIOENCODING"] = "utf-8"
env["AZURE_OPENAI_AUTH_MODE"] = "openai_compatible"
env["AZURE_OPENAI_ENDPOINT"] = PROVIDER_ENDPOINT
env["AZURE_OPENAI_API_KEY"] = env["DEEPSEEK_API_KEY"]
# Provider-specific request fields are opt-in, never inferred from the
# model name. For DeepSeek reasoning models, enable the thinking channel:
env.setdefault("SKILLOPT_SLEEP_CHAT_EXTRA_BODY",
json.dumps({"thinking": {"type": "enabled"}}))
env.setdefault("SKILLOPT_SLEEP_COMPAT_MAX_TOKENS", "8192")
else:
# OPTIONAL, UNVERIFIED fallback: route the `claude` CLI backend through a
# local Anthropic-compatible proxy (e.g. LiteLLM) to reach Gemini. There
# is no native Gemini backend; this path was not validated. See the doc.
backend = "claude"
if "ANTHROPIC_API_KEY" not in env and "GEMINI_API_KEY" in env:
env["ANTHROPIC_API_KEY"] = env["GEMINI_API_KEY"]
env.setdefault("ANTHROPIC_BASE_URL", "http://127.0.0.1:4000")
args = ["skillopt-sleep", command]
if command in ("run", "dry-run"):
args = ["skillopt-sleep", command, "--backend", backend,
"--model", PROVIDER_MODEL, "--project", PROJECT_DIR]
print(f"Running: {' '.join(args)}")
# Propagate the child's exit code so supervisors (watchdog.py, systemd,
# Task Scheduler) see a failed sleep run as a failure, not a success.
proc = subprocess.run(args, env=env, check=False)
sys.exit(proc.returncode)
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Minimal supervisor that runs the SkillOpt-Sleep cycle on a fixed interval.
Sanitized example (see docs/sleep/openai-compatible-endpoints.md). On Windows,
register this under a Scheduled Task so it survives logout; on Linux/macOS a
systemd timer or cron entry serves the same purpose and is usually preferable to
a long-lived process.
"""
import os
import sys
import time
import subprocess
import datetime
import traceback
INTERVAL_SECONDS = int(os.environ.get("SKILLOPT_WATCHDOG_INTERVAL", str(4 * 3600)))
RUNNER = os.environ.get("SKILLOPT_RUNNER", os.path.join(os.path.dirname(__file__), "runner.py"))
LOG_FILE = os.environ.get("SKILLOPT_WATCHDOG_LOG", "brain/watchdog.log")
def log(msg: str) -> None:
os.makedirs(os.path.dirname(LOG_FILE) or ".", exist_ok=True)
line = f"[{datetime.datetime.now().isoformat()}] {msg}"
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
print(line)
def run_once() -> None:
log("Invoking skillopt-sleep run via runner.py...")
try:
result = subprocess.run([sys.executable, RUNNER, "run"],
capture_output=True, text=True)
if result.returncode == 0:
log("Successfully completed run.")
else:
log(f"Run failed (exit {result.returncode}).")
log(f"STDERR: {result.stderr}")
except Exception as e:
log(f"Exception while running skillopt: {e}")
log(traceback.format_exc())
def main() -> None:
log(f"Watchdog started. Interval: {INTERVAL_SECONDS}s.")
while True:
try:
run_once()
except Exception as e:
log(f"Unexpected error in watchdog loop: {e}")
log(f"Sleeping for {INTERVAL_SECONDS}s...")
time.sleep(INTERVAL_SECONDS)
if __name__ == "__main__":
main()
-73
View File
@@ -1,73 +0,0 @@
# SkillOpt-Sleep — validation experiment results
Generated: 2026-06-07 (autonomous offline session)
Backend: mock (deterministic, no API). Reproducible via the commands below.
```
$ python3.12 -m skillopt.sleep.experiments.run_experiment --persona researcher --nights 4 --json
{
"persona": "researcher",
"backend": "mock",
"nights_run": 1,
"baseline_holdout": 0.3333,
"after_holdout": 1.0,
"lift": 0.6667,
"improved": true,
"gate_blocks_harmful": true,
"final_skill_excerpt": "T -->\n## Learned preferences & procedures\n\n_This block is maintained by SkillOpt-Sleep. Edits here are proposed offline, validated against your past tasks, and adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- Always wrap the final answer in <answer>...</answer> tags.\n- Report arXiv ids in the exact form arXiv:XXXX.XXXXX.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n",
"trace": [
{
"night": 0,
"holdout_score": 0.3333,
"action": "baseline",
"n_edits": 0
},
{
"night": 1,
"holdout_score": 1.0,
"action": "accept_new_best",
"accepted": true,
"n_edits": 2,
"edits": [
"Always wrap the final answer in <answer>...</answer> tags.",
"Report arXiv ids in the exact form arXiv:XXXX.XXXXX."
],
"n_rejected": 0
}
]
}
```
```
$ python3.12 -m skillopt.sleep.experiments.run_experiment --persona programmer --nights 4 --json
{
"persona": "programmer",
"backend": "mock",
"nights_run": 1,
"baseline_holdout": 0.3194,
"after_holdout": 1.0,
"lift": 0.6806,
"improved": true,
"gate_blocks_harmful": true,
"final_skill_excerpt": "laude Code sessions.\n\n<!-- SKILLOPT-SLEEP:LEARNED START -->\n## Learned preferences & procedures\n\n_This block is maintained by SkillOpt-Sleep. Edits here are proposed offline, validated against your past tasks, and adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- Write git commit subjects in imperative mood, max 50 chars.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n",
"trace": [
{
"night": 0,
"holdout_score": 0.3194,
"action": "baseline",
"n_edits": 0
},
{
"night": 1,
"holdout_score": 1.0,
"action": "accept_new_best",
"accepted": true,
"n_edits": 1,
"edits": [
"Write git commit subjects in imperative mood, max 50 chars."
],
"n_rejected": 0
}
]
}
```
+173
View File
@@ -0,0 +1,173 @@
# OpenAI-compatible endpoints for SkillOpt-Sleep (DeepSeek, local vLLM, …)
This document describes the `azure_openai` backend in
`skillopt_sleep/backend.py`, which can drive servers that implement the expected
OpenAI-compatible Chat Completions request shape — for example DeepSeek's hosted
API or a self-hosted vLLM/Ollama server — in addition to native Azure OpenAI
deployments. The included runner is a sanitized unattended-launch example that
was originally used alongside Antigravity; it is not an Antigravity transcript
integration.
> **Version requirement.** This capability landed after v0.2.0. Until the next
> release, install SkillOpt from the latest `main`; the current PyPI 0.2.0
> package does not provide this compatible-endpoint path.
## What changed
All changes are backward-compatible — the default managed-identity Azure path
is unchanged:
1. **CLI acceptance.** `skillopt-sleep run --backend azure_openai` is now an
accepted choice in `skillopt_sleep/__main__.py` (it was previously rejected
by argparse even though `get_backend()` understood the name).
2. **Endpoint resolution honors `AZURE_OPENAI_ENDPOINT`.**
`AzureOpenAIBackend.__init__` resolves the endpoint as `explicit arg`
`AZURE_OPENAI_ENDPOINT` env → the built-in `_AZURE_ENDPOINTS` table.
Previously a non-Azure endpoint could not be supplied at all.
3. **`openai_compatible` auth mode.** When
`AZURE_OPENAI_AUTH_MODE=openai_compatible` (also accepts `compat`/`openai`),
`_get_client()` builds a plain `openai.OpenAI(base_url=…)` client with
`AZURE_OPENAI_API_KEY` instead of an `AzureOpenAI` client. This mirrors the
auth mode already supported by the sibling `skillopt/model/azure_openai.py`
module. (The `AzureOpenAI` client rewrites request URLs with Azure-only
`?api-version=…` query params and deployment path segments, which non-Azure
servers reject with `404 Resource not found` — the sleep cycle then scores
every rollout `0.0` with no diagnostic.)
4. **Managed-identity credential guard.** The managed-identity path attaches an
Azure AD bearer token to every request. It therefore accepts only an **HTTPS**
endpoint whose hostname ends in `*.openai.azure.com` or
`*.cognitiveservices.azure.com`. An HTTP endpoint — even one with an
Azure-looking hostname — and any host outside those suffixes are rejected
before a credential-bearing client is created.
5. **Provider-neutral request shape.** In compat mode the backend sends only the
standard OpenAI-compatible contract (`model`, `messages`, `max_tokens`).
Provider-specific request fields are **opt-in** via environment variables
(below) and are attached only in compat mode — nothing is inferred from
model-name substrings, and the native Azure request remains unchanged.
6. **Reliable error state.** `_call()` records the last exception in
`self.last_call_error` (surfaced in `diagnostics.json`), clears it when a
retry recovers, and sets an explicit `"empty response on all N attempts"`
diagnostic when every attempt returns empty text.
## Configuration reference
SkillOpt-Sleep's `azure_openai` backend reads these environment variables
(unprefixed only — the `OPTIMIZER_*`/`TARGET_*` dual-role variables belong to
the separate `skillopt.model.azure_openai` module and are **not** used by the
sleep cycle):
| Variable | Meaning |
|---|---|
| `AZURE_OPENAI_AUTH_MODE` | `openai_compatible` (or `compat`/`openai`) selects the plain OpenAI client. Unset/other = Azure managed identity (default). |
| `AZURE_OPENAI_ENDPOINT` | Base URL of the server, e.g. `https://api.deepseek.com`. Azure managed identity requires HTTPS plus an approved Azure hostname. |
| `AZURE_OPENAI_API_KEY` | API key sent by the compat client to the configured base URL. |
| `SKILLOPT_SLEEP_COMPAT_MAX_TOKENS` | Optional int (default `8192`): `max_tokens` sent in compat mode. |
| `SKILLOPT_SLEEP_CHAT_EXTRA_BODY` | Optional JSON object passed as `extra_body` for provider-specific fields in compat mode only. It is ignored in native Azure mode. |
## Data and transport boundaries
- Harvesting reads local transcripts without modifying them, and the `mock`
backend makes no provider calls. A real backend sends **truncated transcript
excerpts and derived task content** to the selected provider for mining,
replay, judging, and reflection.
- Outbound prompts are not currently guaranteed to be free of secrets. Review
the provider's data policy and avoid a third-party endpoint for sensitive
transcripts unless you have first inspected and redacted the task material.
One reviewable path is `skillopt-sleep harvest --output tasks.json`, followed
by a reviewed `--tasks-file` run.
- Use HTTPS for every remote compatible provider. Plain HTTP is appropriate only
for an explicitly trusted loopback development server such as
`http://127.0.0.1:8000/v1`; the compat client sends its API key to the configured
URL.
- Azure managed-identity credentials have the stricter invariant described
above: HTTPS **and** an approved Azure hostname are both mandatory.
## How to use it
```bash
export AZURE_OPENAI_AUTH_MODE=openai_compatible
export AZURE_OPENAI_ENDPOINT=https://api.deepseek.com # DeepSeek base URL
export AZURE_OPENAI_API_KEY=sk-... # your provider key
# DeepSeek reasoning models: enable the thinking channel (opt-in, not inferred)
export SKILLOPT_SLEEP_CHAT_EXTRA_BODY='{"thinking": {"type": "enabled"}}'
export SKILLOPT_SLEEP_COMPAT_MAX_TOKENS=8192
skillopt-sleep run \
--backend azure_openai \
--model deepseek-v4-pro \
--project /path/to/your/project
```
The same pattern works for a server that implements this Chat Completions
contract: point `AZURE_OPENAI_ENDPOINT` at the provider-specific base URL, set a
matching `--model`, and omit `SKILLOPT_SLEEP_CHAT_EXTRA_BODY` unless the provider
needs extra request fields. Self-hosted vLLM and Ollama commonly use a `/v1` base
path, for example `http://127.0.0.1:8000/v1` or
`http://127.0.0.1:11434/v1`.
`--project` selects the project/transcript scope and the project `CLAUDE.md`; it
does **not** by itself select an arbitrary project `SKILL.md`. Pass
`--target-skill-path path/to/SKILL.md` when a specific skill is the optimization
target. Without that flag, SkillOpt-Sleep uses its configured managed skill.
## Unattended runner example (originally used with Antigravity)
The [`examples/`](https://github.com/microsoft/SkillOpt/tree/main/docs/sleep/examples) directory contains a sanitized reference for running
the compatible backend unattended:
- **`examples/runner.py`** — a thin launcher that loads a provider key from an
`.env` file, exports the variables above, invokes `skillopt-sleep run` with
the DeepSeek backend, and **exits with the child's return code** so
supervisors see failures as failures. Its `session-end` action writes a small
local rollout-evidence event as an example hook target.
- **`examples/watchdog.py`** — a minimal supervisor loop that invokes the runner
on a fixed interval (e.g. every 4 hours) and logs non-zero exits as failures.
On Windows this is registered as a Scheduled Task so it survives logout; on
Linux/macOS a `systemd` timer or cron entry serves the same role.
The current engine does **not** read `brain/rollout-evidence.jsonl`, and it does
not harvest Antigravity transcripts. That hook output is illustrative metadata,
not additional training evidence. A real run must use a supported Claude
Code/Codex transcript source or a reviewed task file converted by the operator.
### Contributor-reported validation
The contributor reported the following results from a private Windows 11 setup
driving the cycle against `deepseek-v4-pro` in `openai_compatible` mode. They are
useful integration evidence, but the private session set is not a reproducible
benchmark bundled with this repository:
- A direct backend smoke test returns a live completion (no `404`,
`last_call_error` empty, client type `OpenAI`).
- A full nightly cycle using the configured session source moved the held-out
validation gate from `0.250 → 1.000`, **accepting** a DeepSeek-authored
skill edit (`accept_new_best`). `diagnostics.json` for that night reports
`"backend": "azure_openai"` with a non-empty token count and an empty
`call_error` — i.e. a genuine optimization night, versus the prior all-`0.0`
nights that the endpoint bug produced.
- A subsequent unattended night triggered by the watchdog completed the full
chain (watchdog → runner → `skillopt-sleep` → DeepSeek) and the gate correctly
**rejected** a non-improving proposal (`0.3 → 0.3`), confirming the validation
gate behaves normally on the new backend.
Deterministic no-network coverage for the new behavior lives in
`tests/test_azure_openai_compat.py` (CLI acceptance, client selection,
endpoint/auth guard, request kwargs, retry error-state, empty-response
diagnostics, and runner exit-code propagation).
## Unsupported Gemini proxy branch in the example
`examples/runner.py` still contains an illustrative branch that routes the
**`claude` CLI backend** through a loopback Anthropic-compatible proxy such as
[LiteLLM](https://github.com/BerriAI/litellm). It is not a native Gemini backend,
has no validated model mapping in this example, and is not part of the supported
path documented here. The sample currently enters that branch whenever no
DeepSeek key is found, so a production adaptation should remove it or replace it
with an explicit opt-in, a separately configured model, and a trusted isolated
loopback proxy. Do not treat this branch as tested Gemini support.
-76
View File
@@ -1,76 +0,0 @@
# SkillOpt-Sleep — plugin load-test (fresh examples)
This records an actual end-to-end load-test of all three plugin shells on a
**brand-new example** (not the gbrain benchmark seeds), run on 2026-06-08.
## The fresh persona
A data analyst whose SQL queries must always include a `LIMIT` clause — built
from scratch for this test. Two forms were used:
1. **Real transcripts** — crafted Claude Code session JSONL where the analyst
asks for SQL, the agent forgets `LIMIT`, and the user complains ("you forgot
a LIMIT again", "always cap results"). This exercises the real
harvest → mine pipeline.
2. **Checkable tasks** — the same intent with a rule judge
(`regex: (?i)LIMIT\s+100`), so the optimizer can be scored on whether future
SQL follows the house rule.
## Results
### Shell plumbing (all three drive the engine)
| Shell | What was run | Result |
|---|---|---|
| **Claude Code** (`scripts/sleep.sh`) | `harvest`, full `run`, `adopt` | harvest found 2 sessions → 2 tasks; `run` staged a proposal; `adopt` honored the safety contract (no live change when nothing was accepted) |
| **Codex** (`install.sh` + shared runner) | `install.sh` into a temp HOME | placed `~/.codex/prompts/sleep.md` and `~/.agents/skills/skillopt-sleep/SKILL.md` correctly |
| **Copilot** (`mcp_server.py`) | `initialize``tools/list``tools/call sleep_harvest` | 5 tools listed; `sleep_harvest` returned real engine output (2 sessions → 2 tasks) |
### Genuine improvement (real model, fresh persona)
Optimizer **Claude Sonnet 4.6** → target **Claude Haiku 4.5**, 3-way split
(5 train / 2 val / 5 test), scored on the held-out **test** queries; and the same
fresh persona self-optimized on **Codex**:
| Backend | Held-out **test** (fraction of SQL with `LIMIT 100`) before → after |
|---|---|
| Claude (Sonnet → Haiku) | **0.00 → 1.00** |
| Codex | **0.00 → 1.00** |
In one night each optimizer wrote, into the protected learned block, a rule like:
> *"OVERRIDE: Every SQL query you generate MUST include `LIMIT 100` …"* (Claude)
> *"Hard requirement: every SQL query response must include …"* (Codex)
and the target then applied it to the **unseen** test queries. This is the whole
claim on a task family the engine had never seen: it learned the user's house
rule from their failures and generalized it — confirmed on both backends.
## An honest finding from load-testing
The **first** attempt used `val_fraction=0.34, test_fraction=0.34`, which left
only **1 train task** for an 8-task set — too little signal — so reflect produced
nothing and the night was a no-op (val already 0.75). Re-balancing the split to a
real train pool (5 train) fixed it and produced the 0 → 1.00 result above. This
is exactly the kind of issue that only surfaces when you actually run the thing,
and it motivates a future guardrail: warn when the train pool is too small for
the chosen split fractions.
## Reproduce
The checkable persona run (real Claude):
```python
# see the snippet in docs/sleep/plugin_load_test.md history, or run:
python -m skillopt_sleep.experiments.run_experiment --persona programmer --assert-improves # deterministic
```
Shell checks:
```bash
# Copilot MCP server
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| SKILLOPT_SLEEP_REPO="$(pwd)" python3 plugins/copilot/mcp_server.py
# Codex installer (into a throwaway HOME)
HOME=$(mktemp -d) bash plugins/codex/install.sh
```
-45
View File
@@ -1,45 +0,0 @@
=== gbrain brief-writer CODEX, improved prompt, 2 nights, 3+3 tasks ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "codex",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 9990,
"results": [
{
"seed": "brief-writer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 2,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 0.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"Every brief must include a clearly labeled section exactly titled `Key Risks`.",
"Every brief must include a line beginning `Confidence:` followed by a concise confidence level or rationale."
]
},
{
"night": 2,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"- Preserve required sections even when keeping the brief short; shorten the analysis before omitting `## Key Risks` or `Confidence:`."
]
}
],
"final_skill_tail": "tside this block are never touched._\n\n- Every brief must include a clearly labeled section exactly titled `Key Risks`.\n- Every brief must include a line beginning `Confidence:` followed by a concise confidence level or rationale.\n- Preserve required sections even when keeping the brief short; shorten the analysis before omitting `## Key Risks` or `Confidence:`.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
@@ -1,38 +0,0 @@
=== REAL cross-check A: Sonnet->Haiku, gate=OFF, rollouts_k=2, brief-writer (exercises new paths) ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "target=claude/optimizer=claude",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 11271,
"results": [
{
"seed": "brief-writer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"test_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"val_hard": 1.0,
"test_hard": 1.0,
"action": "greedy_improved",
"accepted": true,
"edits": [
"Every brief MUST include a section with the exact heading '## Key Risks' that lists the primary risks relevant to the recommendation. This section is required in every output regardless of topic.",
"Every brief MUST include a 'Confidence:' label (satisfying /[Cc]onfidence\\s*[:=]/) that states the confidence level in the recommendation (e.g., 'Confidence: Medium'). Place it near the answer/recommendation line or at the end of the brief."
]
}
],
"slow_update": null,
"final_skill_tail": "at lists the primary risks relevant to the recommendation. This section is required in every output regardless of topic.\n- Every brief MUST include a 'Confidence:' label (satisfying /[Cc]onfidence\\s*[:=]/) that states the confidence level in the recommendation (e.g., 'Confidence: Medium'). Place it near the answer/recommendation line or at the end of the brief.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
@@ -1,48 +0,0 @@
=== REAL cross-check B: Codex, gate=ON (default), rollouts_k=2, brief-writer ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "codex",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 17251,
"results": [
{
"seed": "brief-writer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 2,
"trace": [
{
"night": 0,
"test_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"val_hard": 0.667,
"test_hard": 0.333,
"action": "accept_new_best",
"accepted": true,
"edits": [
"Every brief must include a section/heading titled exactly 'Key Risks'.",
"Every brief must include a confidence line labeled exactly 'Confidence:' so the response matches /[Cc]onfidence\\s*[:=]/."
]
},
{
"night": 2,
"val_hard": 1.0,
"test_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE any brevity guidance: every brief must include a standalone Markdown heading line exactly '## Key Risks' to satisfy section_present=Key Risks, even when the brief is very short."
]
}
],
"slow_update": null,
"final_skill_tail": "clude a section/heading titled exactly 'Key Risks'.\n- Every brief must include a confidence line labeled exactly 'Confidence:' so the response matches /[Cc]onfidence\\s*[:=]/.\n- OVERRIDE any brevity guidance: every brief must include a standalone Markdown heading line exactly '## Key Risks' to satisfy section_present=Key Risks, even when the brief is very short.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
@@ -1,54 +0,0 @@
=== cross-check C: Sonnet->Haiku thorough-analyst (2 nights, slow-update should fire) ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "target=claude/optimizer=claude",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 26010,
"results": [
{
"seed": "thorough-analyst",
"held_out_before": 0.0,
"held_out_after": 0.333,
"improved": true,
"nights": 3,
"trace": [
{
"night": 0,
"test_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"val_hard": 0.667,
"test_hard": 0.667,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE (supersedes 'be exhaustive and detailed', 'Explore every angle', 'consider many scenarios', and 'Write multiple paragraphs'): the ENTIRE response must be at most 1200 characters long, counting every character including spaces, newlines, and punctuation. This hard character limit takes priority over all instructions to be thorough, exhaustive, or multi-paragraph.",
"To stay within 1200 characters while still being useful: lead with the single most critical trade-off, then list 2-3 key considerations as tight bullet points. Omit headers, preamble, and restating the question."
]
},
{
"night": 2,
"val_hard": 0.667,
"test_hard": 0.667,
"action": "reject",
"accepted": false,
"edits": []
},
{
"night": 3,
"val_hard": 0.667,
"test_hard": 0.667,
"action": "reject",
"accepted": false,
"edits": []
}
],
"slow_update": "• On character-constrained tasks (≤1200 chars), plan structure before writing: allocate space per point explicitly and cut until the outline fits, then fill — never draft freely and trim after.\n• Multi-variable business/strategy analyses are high-risk for overrun; default to covering only the 23 most decisive factors rather than attempting exhaustive coverage.\n• Lead with the conclusion or recommendation first; eliminate all introductory restatement of the question, hedging preamble, and transitional filler under tight limits.\n• Persistent failures on the same task signal a structural habit, not a one-off error — treat repeated length violations as a signal to change the drafting approach entirely, not just edit more aggressively.",
"final_skill_tail": "ead with the conclusion or recommendation first; eliminate all introductory restatement of the question, hedging preamble, and transitional filler under tight limits.\n• Persistent failures on the same task signal a structural habit, not a one-off error — treat repeated length violations as a signal to change the drafting approach entirely, not just edit more aggressively.\n<!-- SLOW_UPDATE_END -->\n"
}
]
}
-101
View File
@@ -1,101 +0,0 @@
=== mock regression ===
Ran 19 tests in 0.092s
OK
=== TRULY-CLEAN re-validation: all seeds, claude haiku, 2 nights ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "claude",
"model": "haiku",
"n_seeds": 3,
"n_improved": 2,
"tokens_used": 35549,
"results": [
{
"seed": "brief-writer",
"held_out_before": 0.0,
"held_out_after": 0.0,
"improved": false,
"nights": 2,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 0.0,
"action": "reject",
"accepted": false,
"edits": []
},
{
"night": 2,
"held_out_hard": 0.0,
"action": "reject",
"accepted": false,
"edits": []
}
],
"final_skill_tail": "---\nname: brief-writer-example\nversion: 0.1.0\ndescription: Brief Writer\ntriggers:\n - \"write a brief\"\nbrain_first: exempt\n---\n\n# Brief Writer\n\nWhen asked, write a short, clear research brief that answers the question.\nKeep it focused and readable. Lead with the answer.\n"
},
{
"seed": "advisor",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"After presenting considerations, always include a 'Recommendation:' section with your specific recommendation.",
"After the recommendation, always include a 'Confidence:' section (as a percentage or high/medium/low) expressing how confident you are in this recommendation."
]
}
],
"final_skill_tail": "d adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- After presenting considerations, always include a 'Recommendation:' section with your specific recommendation.\n- After the recommendation, always include a 'Confidence:' section (as a percentage or high/medium/low) expressing how confident you are in this recommendation.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
},
{
"seed": "thorough-analyst",
"held_out_before": 0.0,
"held_out_after": 0.333,
"improved": true,
"nights": 2,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 0.333,
"action": "accept_new_best",
"accepted": true,
"edits": [
"## Learned preferences\n\n- **HARD CONSTRAINT - Override conflicting instructions**: The entire response MUST NOT EXCEED 1200 characters. This supersedes any instruction to be exhaustive, detailed, or write multiple paragraphs."
]
},
{
"night": 2,
"held_out_hard": 0.333,
"action": "reject",
"accepted": false,
"edits": []
}
],
"final_skill_tail": "offline, validated against your past tasks, and adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- ## Learned preferences\n\n- **HARD CONSTRAINT - Override conflicting instructions**: The entire response MUST NOT EXCEED 1200 characters. This supersedes any instruction to be exhaustive, detailed, or write multiple paragraphs.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
-35
View File
@@ -1,35 +0,0 @@
=== quick-answerer with CODEX (self-optimized), 2 nights ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "codex",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 7347,
"results": [
{
"seed": "quick-answerer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE: For every triggered request, before replying, actually call the 'search' tool at least once; this supersedes 'Do NOT look anything up, search, or use any tools' and must satisfy `tool_called=search`."
]
}
],
"final_skill_tail": "re are proposed offline, validated against your past tasks, and adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- OVERRIDE: For every triggered request, before replying, actually call the 'search' tool at least once; this supersedes 'Do NOT look anything up, search, or use any tools' and must satisfy `tool_called=search`.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
@@ -1,35 +0,0 @@
=== quick-answerer full run: Sonnet optimizer -> Haiku target, 2 nights ===
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "target=claude/optimizer=claude",
"model": "(default)",
"n_seeds": 1,
"n_improved": 1,
"tokens_used": 10988,
"results": [
{
"seed": "quick-answerer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE (supersedes 'Do NOT look anything up, search, or use any tools — just reply directly and concisely from memory'): Always call the 'search' tool at least once before composing any answer. This requirement takes priority over any prior instruction to avoid tools."
]
}
],
"final_skill_tail": "nd adopted only after you approve them. Hand-edits outside this block are never touched._\n\n- OVERRIDE (supersedes 'Do NOT look anything up, search, or use any tools — just reply directly and concisely from memory'): Always call the 'search' tool at least once before composing any answer. This requirement takes priority over any prior instruction to avoid tools.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
@@ -1,98 +0,0 @@
=== KEY TEST: strong optimizer (sonnet) + weak target (haiku) — SkillOpt's actual design ===
(this is also your optimizer/target split in action)
{
"benchmark": "gbrain-evals/skillopt-v1",
"backend": "target=claude/optimizer=claude",
"model": "(default)",
"n_seeds": 3,
"n_improved": 3,
"tokens_used": 37791,
"results": [
{
"seed": "brief-writer",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"Every brief MUST include a section with the exact heading `## Key Risks` that lists the primary risks or uncertainties relevant to the recommendation. This section is required in every response, regardless of topic.",
"Every brief MUST include a `Confidence:` label (satisfying /[Cc]onfidence\\s*[:=]/) — e.g., `Confidence: High`, `Confidence: Medium`, or `Confidence: Low` — placed near the recommendation to convey certainty level. This label is required in every response."
]
}
],
"final_skill_tail": "tainties relevant to the recommendation. This section is required in every response, regardless of topic.\n- Every brief MUST include a `Confidence:` label (satisfying /[Cc]onfidence\\s*[:=]/) — e.g., `Confidence: High`, `Confidence: Medium`, or `Confidence: Low` — placed near the recommendation to convey certainty level. This label is required in every response.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
},
{
"seed": "advisor",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 1,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE: The instruction 'so the reader can make up their own mind' must NOT suppress a conclusion. After presenting considerations, you MUST always end with an explicit label exactly matching 'Recommendation:' (capital R) followed by your concrete recommendation on the decision.",
"Always include a 'Confidence:' label (e.g., 'Confidence: High / Medium / Low') in every advisory response, placed immediately after or alongside the Recommendation line, expressing your confidence level in that recommendation."
]
}
],
"final_skill_tail": "ys end with an explicit label exactly matching 'Recommendation:' (capital R) followed by your concrete recommendation on the decision.\n- Always include a 'Confidence:' label (e.g., 'Confidence: High / Medium / Low') in every advisory response, placed immediately after or alongside the Recommendation line, expressing your confidence level in that recommendation.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
},
{
"seed": "thorough-analyst",
"held_out_before": 0.0,
"held_out_after": 1.0,
"improved": true,
"nights": 2,
"trace": [
{
"night": 0,
"held_out_hard": 0.0,
"action": "baseline"
},
{
"night": 1,
"held_out_hard": 0.333,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE — supersedes all instructions to be 'exhaustive and detailed' or 'write multiple paragraphs': The ENTIRE response must be at most 1200 characters long (every character, including spaces, headers, and punctuation, counts toward this limit). If content would exceed 1200 characters, cut elaboration and stop at the most critical tradeoffs only.",
"For 'analyze the decision' responses, use plain concise prose rather than multi-level markdown headers and section dividers; structural markup consumes characters and makes it harder to stay within the 1200-character ceiling."
]
},
{
"night": 2,
"held_out_hard": 1.0,
"action": "accept_new_best",
"accepted": true,
"edits": [
"OVERRIDE — supersedes all instructions to be 'exhaustive and detailed' or 'write multiple paragraphs': The ENTIRE response must be at most 1200 characters long (every character counts). Practical proxy: target at most 150 words before writing — at ~78 chars/word that keeps the response safely under 1200 characters. Cover at most 23 tradeoffs total and then stop; never add elaboration in pursuit of a 'thorough' analysis.",
"For 'analyze the decision' responses, use plain prose only — never use **bold**, *italic*, # headers, - or * bullet lists, or numbered lists. Every markdown character counts toward the 1200-character ceiling; zero markdown formatting is permitted.",
"Limit every 'analyze the decision' response to at most 5 sentences total. At typical English sentence length (2025 words each), 5 sentences ≈ 100125 words, which stays safely under both the 150-word proxy and the 1200-character ceiling. Stop after the 5th sentence regardless of how much more could be said."
]
}
],
"final_skill_tail": "ter ceiling; zero markdown formatting is permitted.\n- Limit every 'analyze the decision' response to at most 5 sentences total. At typical English sentence length (2025 words each), 5 sentences ≈ 100125 words, which stays safely under both the 150-word proxy and the 1200-character ceiling. Stop after the 5th sentence regardless of how much more could be said.\n<!-- SKILLOPT-SLEEP:LEARNED END -->\n"
}
]
}
-114
View File
@@ -1,114 +0,0 @@
# SkillOpt-Sleep — REAL API results (Claude + Codex)
**Date:** 2026-06-07 (autonomous offline session)
**Benchmark:** [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1`
the same public suite gbrain publishes its own SkillOpt scorecard against
([docs/benchmarks/2026-06-03-skillopt.md](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-06-03-skillopt.md)).
These are **real model runs**, not the deterministic mock. The agent's
`attempt` (and the optimizer's `reflect`) call live models via the `claude`
and `codex` CLIs. Held-out scoring is done **locally** by the rule judge
(`skillopt/sleep/judges.py`), so no judge-API spend and no way for the
optimizer to grade its own homework.
## Headline
| Backend | Seed | Held-out before | Held-out after | Nights | Tokens |
|---|---|---|---|---|---|
| **Claude (Haiku 4.5)** | brief-writer | **0.00** | **1.00** | 1 | ~6.7k |
| **Codex (default)** | brief-writer | **0.00** | **0.67** | 1 | ~5.1k |
| **Codex (directive prompt)** | brief-writer | **0.00** | **1.00** | 2 | ~10k |
Both backends took a **deliberately deficient** skill (a brief-writer with no
risks section and no confidence level) and, within 12 sleep nights, proposed
gated edits that lifted the held-out score to perfect. The edits went into the
protected `SKILLOPT-SLEEP:LEARNED` block; nothing else in the skill was touched.
This reproduces gbrain's published `0 → 1.00` headline with **our** engine and
shows it works across **two different agent runtimes** — the core of the
"Claude now, Codex next" plan.
### The multi-night convergence (Codex, why it matters)
The 2-night Codex run is the most informative trace in this whole exercise:
- **Night 1** — added two precise rules (a `Key Risks` section, a `Confidence:`
line). Held-out still **0.00**: the rules were right but the agent, told to
keep briefs short, was *dropping* them under length pressure.
- **Night 2** — the optimizer diagnosed its own residual failure and added a
meta-rule: *"Preserve required sections even when keeping the brief short;
shorten the analysis before omitting Key Risks or Confidence."* Held-out → **1.00**.
That second edit is not pattern-matching a checklist — it is reasoning about
*why the previous night underperformed*. This is exactly the iterative,
slow-update behavior SkillOpt's design predicts, and it is the strongest
argument for the sleep **loop** over a one-shot rewrite.
## What the optimizer actually wrote
**Claude** synthesized a full format template:
```
**Recommendation:** [Clear yes/no or specific answer]
**Rationale:** [2-3 bullet points supporting the answer]
**Key Risks:** [Downsides, edge cases, or assumptions that could invalidate this]
**Confidence:** [High/Medium/Low] — [Why]
```
**Codex** wrote a terser rule:
```
For every brief, include a `Key Risks` section and end with
`Confidence: Low|Medium|High`.
```
Both are correct, general, reusable rules (not task-specific answers). Claude's
fuller template made the agent satisfy the checks on **3/3** held-out items;
Codex's terser rule landed **2/3** — the missing item is a consistency miss the
agent would likely fix with one more night (see "Honest notes").
## How to reproduce
```bash
# clone the benchmark data
git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals
cd <repo>/SkillOpt-sleep # this worktree
# Claude backend
python3.12 -m skillopt.sleep.experiments.run_gbrain \
--backend claude --model haiku --seeds brief-writer \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
--nights 1 --limit-replay 3 --limit-holdout 3 --json
# Codex backend (auto-detects the real @openai/codex binary, not the wrapper)
python3.12 -m skillopt.sleep.experiments.run_gbrain \
--backend codex --seeds brief-writer \
--data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
--nights 1 --limit-replay 3 --limit-holdout 3 --json
```
## Honest notes (in the spirit of gbrain's own scorecard)
- **Latency:** each CLI call is ~1415 s of startup-dominated wall time, so runs
were capped at 3 train + 3 held-out tasks and 1 night to keep them ~2.5 min.
The response cache makes re-scoring an unchanged (skill, memory) free.
- **Codex 0.67, not 1.00:** a single terse edit + single night under-shoots on
one held-out item. Two improvements (below) are expected to close it. We report
the 0.67, we don't dress it up.
- **3 of gbrain's 4 seeds are scored with zero API beyond `attempt`:**
`section_present`, `regex`, `max_chars` are pure-text checks. Only the
`quick-answerer` seed (`tool_called: search`) needs a real tool loop, which is
Phase-3 `fresh` replay.
- **The gate is real:** every accepted edit had to beat the held-out score; a
no-op night is rejected and the skill is left unchanged.
## Improvements this run motivated (applied + verified)
1. **A more directive `reflect` prompt** that aggregates the *exact* failing
judge criteria and tells the optimizer to satisfy every one (gbrain's lesson:
"the optimizer was never told what the scorer rewards"). Applied in
`skillopt/sleep/backend.py`. **Verified**: lifted Codex from 0.67 → 1.00.
2. **Multi-night convergence** — a terse first edit gets a sharper second pass;
the night-2 trace above shows the optimizer self-correcting. Recommend
`nights >= 2` for real backends.
-11
View File
@@ -1,11 +0,0 @@
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 6657, "cfg": {"kind": "dual", "optimizer_backend": "claude", "optimizer_model": "sonnet", "target_backend": "claude", "target_model": "haiku", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"kind\": \"dual\", \"nights\": 2, \"optimizer_backend\": \"claude\", \"optimizer_model\": \"sonnet\", \"seed\": \"brief-writer\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}", "elapsed_s": 71.5}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 7891, "cfg": {"kind": "dual", "optimizer_backend": "claude", "optimizer_model": "sonnet", "target_backend": "claude", "target_model": "haiku", "seed": "advisor", "nights": 2}, "cfg_key": "{\"kind\": \"dual\", \"nights\": 2, \"optimizer_backend\": \"claude\", \"optimizer_model\": \"sonnet\", \"seed\": \"advisor\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}", "elapsed_s": 79.3}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 17960, "cfg": {"kind": "dual", "optimizer_backend": "claude", "optimizer_model": "sonnet", "target_backend": "claude", "target_model": "haiku", "seed": "thorough-analyst", "nights": 2}, "cfg_key": "{\"kind\": \"dual\", \"nights\": 2, \"optimizer_backend\": \"claude\", \"optimizer_model\": \"sonnet\", \"seed\": \"thorough-analyst\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}", "elapsed_s": 319.3}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 9969, "cfg": {"kind": "direct", "backend": "codex", "model": "", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"backend\": \"codex\", \"kind\": \"direct\", \"model\": \"\", \"nights\": 2, \"seed\": \"brief-writer\"}", "elapsed_s": 187.6}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 6210, "cfg": {"kind": "direct", "backend": "codex", "model": "", "seed": "advisor", "nights": 2}, "cfg_key": "{\"backend\": \"codex\", \"kind\": \"direct\", \"model\": \"\", \"nights\": 2, \"seed\": \"advisor\"}", "elapsed_s": 114.1}
{"baseline_target": 0.0, "transferred": 1.0, "transfer_gain": 1.0, "tokens": 13673, "cfg": {"kind": "transfer", "source_backend": "claude", "source_model": "haiku", "target_backend": "claude", "target_model": "sonnet", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"kind\": \"transfer\", \"nights\": 2, \"seed\": \"brief-writer\", \"source_backend\": \"claude\", \"source_model\": \"haiku\", \"target_backend\": \"claude\", \"target_model\": \"sonnet\"}", "elapsed_s": 180.3}
{"baseline_target": 0.0, "transferred": 1.0, "transfer_gain": 1.0, "tokens": 11668, "cfg": {"kind": "transfer", "source_backend": "claude", "source_model": "sonnet", "target_backend": "claude", "target_model": "haiku", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"kind\": \"transfer\", \"nights\": 2, \"seed\": \"brief-writer\", \"source_backend\": \"claude\", \"source_model\": \"sonnet\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}", "elapsed_s": 173.9}
{"baseline_target": 0.0, "transferred": 1.0, "transfer_gain": 1.0, "tokens": 13707, "cfg": {"kind": "transfer", "source_backend": "codex", "source_model": "", "target_backend": "claude", "target_model": "haiku", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"kind\": \"transfer\", \"nights\": 2, \"seed\": \"brief-writer\", \"source_backend\": \"codex\", \"source_model\": \"\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}", "elapsed_s": 215.7}
{"baseline_target": 0.0, "transferred": 1.0, "transfer_gain": 1.0, "tokens": 11284, "cfg": {"kind": "transfer", "source_backend": "claude", "source_model": "haiku", "target_backend": "codex", "target_model": "", "seed": "brief-writer", "nights": 2}, "cfg_key": "{\"kind\": \"transfer\", \"nights\": 2, \"seed\": \"brief-writer\", \"source_backend\": \"claude\", \"source_model\": \"haiku\", \"target_backend\": \"codex\", \"target_model\": \"\"}", "elapsed_s": 145.5}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 10988, "cfg": {"kind": "dual", "optimizer_backend": "claude", "optimizer_model": "sonnet", "target_backend": "claude", "target_model": "haiku", "seed": "quick-answerer", "nights": 2}, "elapsed_s": null, "note": "real tool loop", "cfg_key": "{\"kind\": \"dual\", \"nights\": 2, \"optimizer_backend\": \"claude\", \"optimizer_model\": \"sonnet\", \"seed\": \"quick-answerer\", \"target_backend\": \"claude\", \"target_model\": \"haiku\"}"}
{"baseline": 0.0, "after": 1.0, "improved": true, "tokens": 7347, "cfg": {"kind": "direct", "backend": "codex", "model": "", "seed": "quick-answerer", "nights": 2}, "elapsed_s": null, "note": "real tool loop", "cfg_key": "{\"backend\": \"codex\", \"kind\": \"direct\", \"model\": \"\", \"nights\": 2, \"seed\": \"quick-answerer\"}"}
@@ -1,5 +1,13 @@
# SkillOpt Sleep — Claude Code self-evolving plugin (design)
> **Historical design proposal.** This document records the June 2026 design
> target and includes planned controls that are not part of the current nightly
> CLI. It is not an installation or configuration reference. For implemented
> behavior, flags, defaults, and data boundaries, use
> [`docs/sleep/README.md`](../../sleep/README.md),
> [`docs/reference/cli.md`](../../reference/cli.md), and
> [`plugins/README.md`](https://github.com/microsoft/SkillOpt/blob/main/plugins/README.md).
**Status:** approved-for-build (autonomous offline session, 2026-06-07)
**Author:** generated for Yifan Yang, executed autonomously while user is asleep
**Branch:** `feat/claude-code-sleep-plugin` (worktree `my_repo/SkillOpt-sleep`)
@@ -234,4 +242,3 @@ This session targets **Phase 0 + Phase 1 fully**, **Phase 2 scaffolded**, and th
3. **Real-API demo:** want me to spend live `ANTHROPIC_API_KEY` budget on the persona demo, or keep everything mock until you say go?
4. **Skill target:** evolve a *new* dedicated `skillopt-sleep`-managed skill, or also edit your existing hand-written skills in `~/.claude/skills`?
5. **Paper:** should this become a section/figure in the SkillOpt arXiv (Dream+Sleep framing as "deployment-time continual skill optimization")?
```
+9 -10
View File
@@ -1778,6 +1778,8 @@
<a href="#evolution">Evolution</a>
<a href="#transfer">Transfer</a>
<a href="#citation">Citation</a>
<a href="https://microsoft.github.io/SkillOpt/blog/">Blog</a>
<a href="https://github.com/microsoft/SkillOpt/blob/main/docs/index.md" target="_blank" rel="noopener">Docs</a>
<a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a>
</nav>
</header>
@@ -1915,7 +1917,7 @@
<h3>A skill is external state for an agent.</h3>
<p>
Instead of fine-tuning a model or hand-maintaining prompts, SkillOpt runs
the frozen agent on scored batches, asks a separate optimizer model to
the frozen agent on scored batches, asks an optimizer model to
propose structured edits, and accepts a candidate only when validation
performance improves.
</p>
@@ -2416,21 +2418,18 @@
<div class="bibtex-box">
<button class="copy-btn" type="button" onclick="copyBibtex(this)">Copy</button>
<pre><code>@misc{yang2026skilloptexecutivestrategyselfevolving,
title={SkillOpt: Executive Strategy for Self-Evolving Agent Skills},
author={Yifan Yang and Ziyang Gong and Weiquan Huang and Qihao Yang and Ziwei Zhou and Zisu Huang and Yan Li and Xuemei Gao and Qi Dai and Bei Liu and Kai Qiu and Yuqing Yang and Dongdong Chen and Xue Yang and Chong Luo},
year={2026},
eprint={2605.23904},
archivePrefix={arXiv},
primaryClass={cs.AI},
url={https://arxiv.org/abs/2605.23904},
<pre><code>@article{yang2026skillopt,
title={Skillopt: Executive strategy for self-evolving agent skills},
author={Yang, Yifan and Gong, Ziyang and Huang, Weiquan and Yang, Qihao and Zhou, Ziwei and Huang, Zisu and Li, Yan and Gao, Xuemei and Dai, Qi and Liu, Bei and others},
journal={arXiv preprint arXiv:2605.23904},
year={2026}
}</code></pre>
</div>
</section>
<footer class="footer">
<span>SkillOpt: Executive Strategy for Self-Evolving Agent Skills</span>
<span><a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a> / <a href="#citation">Citation</a></span>
<span><a href="https://microsoft.github.io/SkillOpt/blog/">Blog</a> / <a href="https://github.com/microsoft/SkillOpt/blob/main/docs/index.md" target="_blank" rel="noopener">Docs</a> / <a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a> / <a href="#citation">Citation</a></span>
</footer>
</main>
<script>
+5
View File
@@ -39,6 +39,7 @@ theme:
nav:
- Home: index.md
- Technical Blog: https://microsoft.github.io/SkillOpt/blog/
- Getting Started:
- Installation: guide/installation.md
- First Experiment: guide/first-experiment.md
@@ -47,6 +48,10 @@ nav:
- Training Loop: guide/training-loop.md
- Skill Document: guide/skill-document.md
- Deep Learning Analogy: guide/dl-analogy.md
- SkillOpt-Sleep:
- Overview: sleep/README.md
- OpenAI-compatible Endpoints: sleep/openai-compatible-endpoints.md
- Results: sleep/RESULTS.md
- Extension Guides:
- Add a New Benchmark: guide/new-benchmark.md
- Local Environment Smoke Tests: guide/local-env-smoke.md
+144 -179
View File
@@ -1,213 +1,178 @@
# SkillOpt-Sleep — plugins for Claude Code, Codex, and Copilot
# SkillOpt-Sleep integrations
**Your coding agent forgets everything between sessions. SkillOpt-Sleep fixes
that.** While you sleep, it reviews what you did today, notices the rules you
keep repeating ("always add a LIMIT", "answers in `\boxed{}`", "cite the
source"), and writes them into your agent's long-term memory and skills — but
only the rules that actually make it score better on *your own* past tasks. You
wake up to an agent that's better at *your* work, and you approve every change
before it sticks.
**SkillOpt-Sleep** reviews recent agent sessions, mines recurring tasks, replays
them, and proposes bounded updates to memory and skills. A held-out validation
gate decides whether a proposal is worth staging, and nothing live changes until
the user explicitly adopts it.
One engine, three thin shells. It synthesizes **SkillOpt** (validation-gated
bounded text optimization — the research in this repo), **Claude Dreams**
(offline consolidation; input never mutated; review-then-adopt), and the **agent
sleep** idea (short-term experience → long-term competence).
The shared engine lives in [`skillopt_sleep/`](../skillopt_sleep) and has no
runtime dependency on the paper's `skillopt/` experiment package.
> **Open-source tool, decoupled from the research.** The engine lives in the
> top-level [`skillopt_sleep/`](../skillopt_sleep) package with **zero
> dependency** on the paper's `skillopt/` experiment code (the validation gate is
> vendored). Use it without the research stack.
## Available integrations
---
Four integrations wrap the shared `skillopt_sleep` CLI. OpenClaw is a separate
reference adaptation with its own backend and setup assumptions.
## Install (pick your agent)
| Platform | Folder | Mechanism | Status |
|---|---|---|---|
| **Claude Code** | [`claude-code/`](claude-code) | marketplace plugin, commands, skill, and hooks | installable shared-engine integration |
| **Codex** | [`codex/`](codex) | user-level skill and shared runner | installable shared-engine integration |
| **GitHub Copilot** | [`copilot/`](copilot) | MCP server exposing seven `sleep_*` tools | shared-engine MCP integration |
| **Devin** | [`devin/`](devin) | MCP server plus Devin transcript conversion | shared-engine MCP integration |
| **OpenClaw** | [`openclaw/`](openclaw) | custom DeepSeek/Ollama wrapper | independent reference adaptation; review and adapt before use |
## Install
Clone the repository first unless an installed `skillopt-sleep` CLI is sufficient
for your workflow.
| Platform | Install | Then |
|---|---|---|
| **Claude Code** | `/plugin marketplace add microsoft/SkillOpt` `/plugin install skillopt-sleep` | `/skillopt-sleep status` |
| **Codex** | `git clone``bash plugins/codex/install.sh` | `/skillopt-sleep status` |
| **Copilot** | `git clone` register `plugins/copilot/mcp_server.py` as an MCP server | ask "run the sleep cycle" |
| **Claude Code** | from the repository root, `/plugin marketplace add ./plugins/claude-code`, then `/plugin install skillopt-sleep@skillopt-sleep` | `/skillopt-sleep status` |
| **Codex** | `bash plugins/codex/install.sh` | ask Codex to use the `skillopt-sleep` skill |
| **Copilot** | register `plugins/copilot/mcp_server.py` using its example MCP config | ask Copilot to run `sleep_status` |
| **Devin** | register `plugins/devin/mcp_server.py` using its example MCP config | ask Devin to run `sleep_status` |
| **OpenClaw** | follow and adapt [`openclaw/README.md`](openclaw/README.md) | validate paths, credentials, and tasks locally |
Requirements: Python 3.10 and the agent's CLI on PATH. All three call the same
[`run-sleep.sh`](run-sleep.sh) → `python -m skillopt_sleep`, so behaviour is
identical everywhere. Default backend is `mock` (no API spend); `--backend
claude|codex` uses your own budget.
Python 3.10 or newer is required. Real CLI backends also require the selected
agent CLI to be installed and authenticated.
---
The shared [`run-sleep.sh`](run-sleep.sh) supports both source checkouts and
installed packages. If it cannot find the repository, it tries the
`skillopt-sleep` executable on `PATH` (including `uv tool`/`pipx` installs), then
an importable `skillopt_sleep` module. Install with `uv tool install skillopt` or
`pip install skillopt` when using that fallback.
## How it works: one "night", in plain terms
> **Version note.** This integration reference tracks `main`. PyPI 0.2.0
> supports the base Sleep CLI, while handoff, Sleep support for non-Azure
> OpenAI-compatible endpoints, and `--preferences` require a source checkout
> from `main` until the next release.
```
harvest your past sessions → mine the tasks you keep doing → replay them offline
→ reflect on failures → propose a few rule edits → KEEP only edits that raise
your held-out score → stage a proposal → (you) review & adopt
## One sleep cycle
```text
harvest supported local sessions → mine recurring tasks → replay tasks
→ reflect and propose bounded edits → validate on held-out real tasks
→ stage proposal → (you) review and adopt
```
Nothing live changes until you `adopt`; every adopt backs up the prior file.
The default backend is `mock`: it makes no provider calls and is useful for
checking plumbing. A real backend is required for model-driven mining and genuine
optimization.
### The split that keeps it honest: dream-train / real-val / real-test
## Data boundary
This is the heart of the design, borrowed from the SkillOpt paper's
train/selection/test protocol:
- Harvesting is local and read-only. The `mock` backend has no model-provider
data path and no API spend.
- A real backend sends truncated transcript excerpts and derived task content to
the provider selected for mining, replay, judging, and reflection.
- Outbound prompts are not currently guaranteed to be free of secrets. Do not
use a third-party provider on sensitive transcripts without reviewing the data
source and the provider's retention policy.
- For a reviewable workflow, export tasks first, inspect and redact the JSON, set
its top-level `"reviewed"` field to `true`, and then use the task file with a
real backend:
| Split | Where it comes from | What it's for |
|---|---|---|
| **train** | your real tasks **+ optional "dreamed" variants** | what the optimizer *learns from*. Over-dreaming here is fine — it's imagination. |
| **val** (selection) | **your real tasks only**, held out | the **gate**: an edit is kept only if it raises this score. Stops overfitting. |
| **test** | **your real tasks only**, held out, never seen during optimization | the **final score** we report. Kept as close to your real usage as possible. |
```bash
python -m skillopt_sleep harvest --project "$(pwd)" --output reviewed-tasks.json
python -m skillopt_sleep dry-run --project "$(pwd)" --backend codex \
--tasks-file reviewed-tasks.json --progress
```
So you can **dream up extra training examples** to learn a rule robustly, while
the rule is still **judged on real, unseen tasks**. A `dream` task can *never*
land in val or test — that invariant is unit-tested.
Real backends reject task files that are still marked unreviewed.
---
For the separate API-key and Azure managed-identity transport boundaries, see
[OpenAI-compatible endpoints](../docs/sleep/openai-compatible-endpoints.md).
## What each feature does **for you** (with examples)
## Supported CLI surface
Every control below works on all three platforms (pass it after the action,
e.g. `/skillopt-sleep run --rollouts-k 3`).
Actions:
### `--preferences "..."` — tell it your house rules
The single most useful knob. Free text that steers what the optimizer writes,
as a prior. Use it to encode the conventions you're tired of repeating.
```bash
# A backend engineer:
/skillopt-sleep run --preferences "Always use async/await, never callbacks. \
Prefer pytest over unittest. Commit subjects in imperative mood under 50 chars."
# A data analyst:
/skillopt-sleep run --preferences "Every SQL query must end with LIMIT 1000 unless \
I say otherwise. Money in USD with 2 decimals. Prefer CTEs over nested subqueries."
# A researcher:
/skillopt-sleep run --preferences "Cite sources as [Author, Year]. Math answers in \
\\boxed{}. Keep explanations under 150 words unless I ask for depth."
```
*What it does for you:* the next morning your agent already follows these
without you re-typing them, and the rules are validated against your real tasks
(if a "preference" actually hurts your held-out score, the gate drops it).
### `--gate on|off` — strict vs. greedy
- `on` (default): an edit is kept **only if it raises your held-out score**.
Safe — blocks plausible-but-wrong rules and reward-hacking.
- `off`: greedy — keep edits without the strict check (still reports whether
quality moved).
*What it does for you:* leave it `on` for trust. Flip it `off` when you're
exploring and want to see everything the optimizer proposes.
### `--rollouts-k K` — learn from contrast, not just failure
Re-runs each task `K` times and learns from the difference between the **good**
and **bad** attempts, not just a single failure.
```bash
/skillopt-sleep run --rollouts-k 3
```
*What it does for you:* a much stronger signal. If your agent gets a task right 1
time in 3, the optimizer figures out *what the winning attempt did* and makes it
reliable.
### `--optimizer-model` / `--target-model` — optimize cheap, deploy anywhere
Use a strong model to *write* the rules and a cheap model to *run* your tasks.
The learned skill then helps the cheap model — or any model.
```bash
/skillopt-sleep run --optimizer-model sonnet --target-model haiku
```
*What it does for you:* spend a little on a smart optimizer overnight; your
everyday cheap/fast agent inherits the upgrade. (Verified: a skill optimized on
one model lifts a different one — cross-model and even cross-runtime
Codex↔Claude.)
### `--budget-tokens N` / `--budget-minutes M` — cap the spend
You decide how much the nightly "dreaming" costs; it auto-plans how many nights
× how many rollouts fit.
```bash
/skillopt-sleep run --backend claude --budget-tokens 60000
```
*What it does for you:* predictable cost. It stops cleanly when the budget is hit
and tells you what it skipped.
### multi-objective (accuracy ↑, tokens ↓, latency ↓)
The reward can weight not just correctness but **cost and speed**, so a skill can
learn to be cheaper and faster, not only more accurate. *What it does for you:*
"answer directly instead of opening five files" becomes a learned habit.
### `schedule` / `unschedule` — set it and forget it
Built-in nightly scheduling (no manual cron):
```bash
/skillopt-sleep schedule --hour 3 --minute 17 # runs every night for this project
/skillopt-sleep unschedule # stop it
```
*What it does for you:* it just gets better while you sleep. The nightly run only
*stages* a proposal — adopting is still your call (or add `--auto-adopt` when you
schedule, if you trust it).
---
## Full action / flag reference
| Action | Does |
| Action | Behavior |
|---|---|
| `status` | nights so far + the latest staged proposal (read-only) |
| `dry-run` | harvestminereplayreport; **stages nothing** |
| `run` | full cycle; **stages** a proposal; nothing live changes |
| `adopt` | apply the staged proposal to `CLAUDE.md`/`SKILL.md` (backs up first) |
| `harvest` | debug: print the recurring tasks it mined |
| `schedule` / `unschedule` | install/remove the nightly cron entry |
| `status` | show state and the latest staged proposal |
| `dry-run` | harvest, mine, replay, and report; stage nothing |
| `run` | run the full cycle and stage a proposal |
| `adopt` | apply the latest staged proposal, with backups |
| `harvest` | inspect or export mined tasks |
| `schedule` / `unschedule` | install or remove the managed nightly cron entry |
| Flag | Default | Meaning |
Common implemented flags include:
| Flag | Default | Purpose |
|---|---|---|
| `--backend mock\|claude\|codex` | `mock` | who runs/optimizes (mock = free) |
| `--preferences "..."` | | your house rules, as a prior |
| `--gate on\|off` | `on` | strict held-out gate vs. greedy |
| `--rollouts-k K` | `1` | multi-rollout contrastive reflection |
| `--optimizer-model` / `--target-model` | | split the optimizer from the target |
| `--budget-tokens` / `--budget-minutes` | | cap the nightly spend |
| `--scope invoked\|all` | `invoked` | this project only, or all projects |
| `--auto-adopt` | off | apply without manual review (power users) |
| `--backend mock\|claude\|codex\|copilot\|handoff\|azure_openai` | `mock` | select who performs model calls |
| `--model NAME` | backend default | select a backend-specific model |
| `--source claude\|codex\|auto` | `claude` | select the transcript source |
| `--project PATH` | current directory | select the project and invoked harvest scope |
| `--scope invoked\|all` | `invoked` | limit transcript harvesting |
| `--target-skill-path PATH` | managed skill | select a specific `SKILL.md` to stage/adopt |
| `--tasks-file PATH` | none | replay a reviewed task file instead of harvesting |
| `--max-sessions N` / `--max-tasks N` | unset → `3 × tasks` / `40` tasks | bound harvested work; these are not hard token or wall-clock budgets |
| `--edit-budget N` | `4` | cap bounded edits per cycle |
| `--preferences "..."` | empty | add house rules to the reflection prior |
| `--progress` | off | print phase progress to stderr |
| `--auto-adopt` | off | adopt an accepted proposal without a separate command |
| `--json` | off | emit machine-readable output where supported |
Deep dive: [`../docs/sleep/CONTROLLABLE_DREAMING.md`](../docs/sleep/CONTROLLABLE_DREAMING.md).
The nightly CLI does **not** currently expose `--gate`, `--rollouts-k`,
`--optimizer-model`, `--target-model`, `--budget-tokens`, or `--budget-minutes`.
Do not pass experiment-harness flags to the main CLI.
---
### Preferences
## Does it actually work?
`--preferences` is the main user-facing steering knob:
Yes — measured with **real models on both Claude and Codex**, scored on held-out
tasks the optimizer never trained on:
- **gbrain-evals `skillopt-v1`** (the public suite gbrain scores SkillOpt on):
deficient skills go **0.00 → 1.00** on all 4 seeds, including a real tool-use
loop; cross-model transfer is positive; the gate blocks regressions.
→ [`../docs/sleep/FINAL_REPORT.md`](../docs/sleep/FINAL_REPORT.md)
- **Academic daily-cases** (math / spreadsheet / search-QA, the paper's 4:1:5
split with dream-augmented train): see
[`../docs/sleep/daily_cases_results.md`](../docs/sleep/daily_cases_results.md).
- **Fresh load-test** (a "SQL must always include LIMIT" analyst, built from
scratch): held-out **0.00 → 1.00** on both backends.
→ [`../docs/sleep/plugin_load_test.md`](../docs/sleep/plugin_load_test.md)
Try the deterministic proof yourself (no API key, no spend):
```bash
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
python -m skillopt_sleep run --backend codex --project "$(pwd)" \
--preferences "Prefer pytest. Keep commit subjects imperative and concise."
```
It prints the held-out score rising to 1.0 as the gate accepts the right rules,
and confirms the gate **rejects** an injected harmful edit.
---
Preferences guide reflection but remain subject to the validation gate.
## Safety
### Advanced config
- **Read-only** harvest of your sessions. `mock` replay has no side effects.
- Proposals are **staged**, never auto-applied (unless you opt in with `--auto-adopt`).
- Every adopt writes a backup. Per-night token/time budget caps. Secrets redacted.
The JSON/YAML config under `~/.skillopt-sleep/` supports additional engine keys,
including `gate_mode`, `gate_metric`, `dream_rollouts`, `dream_factor`, `recall_k`,
`evolve_memory`, and `evolve_skill`. These are config keys, not aliases for the
unsupported CLI flags listed above. Shipping defaults are conservative:
`gate_mode="on"`, `dream_rollouts=1`, `dream_factor=0`, and `recall_k=0`.
### Handoff backend
`--backend handoff` keeps model subprocesses out of the engine. It writes pending
model calls to `.skillopt-sleep-handoff/PROMPTS.md` and `pending.json`, exits with
code 3, and resumes after answers are placed in `answers/<id>.md`:
```bash
python -m skillopt_sleep run --backend handoff --project "$(pwd)"
# answer each prompt in a fresh context, then run the same command again
```
Answering held-out prompts from a context that has already seen their references
contaminates the validation gate. Claude Code's `/skillopt-sleep-handoff` command
automates the loop with isolated fresh-context subagents.
## Validation
The deterministic no-provider check exercises consolidation and the gate:
```bash
python -m skillopt_sleep.experiments.run_experiment \
--persona researcher --assert-improves
```
Real-model benchmark results and their limitations are documented in
[`docs/sleep/RESULTS.md`](../docs/sleep/RESULTS.md). The benchmark recipes are not
the shipping CLI defaults.
## Safety summary
- Session harvesting is read-only.
- `mock` replay makes no provider calls.
- `run` stages proposals; `adopt` is the normal live-change boundary.
- Adoption backs up existing target files.
- `--max-sessions` and `--max-tasks` bound work, but the main CLI does not yet
enforce a hard token or elapsed-time budget.
- Treat real-backend transcript excerpts as data shared with the selected
provider.
+67 -18
View File
@@ -10,7 +10,7 @@ SkillOpt-Sleep is the **deployment-time** companion to
[SkillOpt](https://github.com/microsoft/SkillOpt). SkillOpt trains a skill
offline on a benchmark; SkillOpt-Sleep applies the same discipline to *your own
daily usage*: bounded text edits, accepted only through a held-out validation
gate, with rejected edits kept as negative feedback.
gate, with rejected candidates recorded in the cycle report for review.
It synthesizes three ideas:
@@ -32,7 +32,8 @@ then adopt or discard" contract). Every adopt backs up the prior file first.
## Install
**Requirements:** Python ≥ 3.10, and the `claude` CLI (and/or `codex` CLI) on PATH.
**Requirements:** Python ≥ 3.10. A real CLI backend additionally requires its
corresponding `claude` or `codex` executable on `PATH` and authenticated.
```bash
# 1) get the code (the plugin ships inside the SkillOpt repo)
@@ -40,7 +41,7 @@ git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
# 2) add the plugin to Claude Code as a local marketplace
/plugin marketplace add ./skillopt-sleep-plugin
/plugin marketplace add ./plugins/claude-code
/plugin install skillopt-sleep@skillopt-sleep
# 3) verify
@@ -48,18 +49,27 @@ cd SkillOpt
```
The plugin's bundled runner (`scripts/sleep.sh`) auto-selects a Python ≥ 3.10
interpreter and calls the `skillopt_sleep` engine in the repo. No `pip install`
is required for the default `mock` backend or for `claude`/`codex` backends —
they shell out to the CLIs you already have.
interpreter and calls the `skillopt_sleep` engine. A source checkout needs no
`pip install`. If the marketplace cache does not contain a usable source tree,
the shared runner falls back first to a `skillopt-sleep` executable on `PATH`
(including `uv tool`/`pipx` installs), then to an importable Python module. Use
`uv tool install skillopt` or `pip install skillopt` for that fallback.
> **Version note.** This page tracks `main`. PyPI 0.2.0 provides the base Sleep
> CLI, but handoff mode and `--preferences` require a source checkout from
> `main` until the next release.
## Quick start
```bash
# from inside any project you use with Claude Code:
/skillopt-sleep dry-run # safe preview: what it would learn, no changes staged
/skillopt-sleep dry-run # preview what it would learn; no changes staged
/skillopt-sleep run # full cycle: stages a reviewed proposal (still no live edits)
/skillopt-sleep status # see history + the latest staged proposal
/skillopt-sleep adopt # apply the staged proposal to CLAUDE.md / SKILL.md (with backup)
/skillopt-sleep-handoff run # same cycle, but THIS session answers the model calls
# (no claude -p subprocess, no API key — subscription-friendly)
```
Or call the engine directly (Python ≥ 3.10):
@@ -71,8 +81,39 @@ python -m skillopt_sleep run --project "$(pwd)" --backend codex # real lift v
```
Default backend is **`mock`** — deterministic, no API spend — so you can try the
plumbing for free. Switch to `--backend claude` or `--backend codex` for genuine
improvement on your own budget.
plumbing for free. Switch to `--backend claude` or `--backend codex` for
model-driven mining and optimization on your own budget; an accepted gain is
task- and model-dependent, not guaranteed.
### Data boundary for real backends
Harvesting `~/.claude` is local and read-only, and the `mock` backend makes no
provider calls. A real backend sends truncated transcript excerpts and derived
tasks to the selected provider for mining, replay, judging, and reflection.
Outbound prompts are not currently guaranteed to be secret-free. Review your
session data and provider policy before using a real backend on a sensitive
project; the [shared integration guide](../README.md#data-boundary) describes a
reviewed task-file workflow.
### Handoff mode (session answers the model calls)
`--backend handoff` runs the cycle without any model subprocess: the engine
executes the deterministic stages and writes every model call it needs to
`.skillopt-sleep-handoff/PROMPTS.md` + `pending.json` (exit code 3). You (or
the `/skillopt-sleep-handoff` command, which automates the loop with isolated
fresh-context subagents) write each raw answer to `answers/<id>.md` and re-run
the same command; it resumes from the answers and either finishes or stages
the next batch. Typically 36 rounds per night.
```bash
python -m skillopt_sleep run --backend handoff --project "$(pwd)"
# ... answer .skillopt-sleep-handoff/PROMPTS.md into answers/<id>.md ...
python -m skillopt_sleep run --backend handoff --project "$(pwd)" # resume
```
Answer every prompt in a **fresh context** — a session that has already seen
the mined tasks and their references would contaminate the held-out gate.
Details: [the plugins README](../README.md#handoff-backend).
## Does it actually improve? (real models, public benchmark)
@@ -91,8 +132,8 @@ Both took a brief-writer with no risks section / no confidence level and, within
12 nights, proposed gated edits that lifted the held-out score to perfect —
into the protected `LEARNED` block, nothing else touched. The Codex 2-night
trace even shows the optimizer **diagnosing its own residual failure** and
adding a meta-rule to fix it. Full writeup + reproduction:
[`docs/sleep/real_api_results.md`](../docs/sleep/real_api_results.md).
adding a meta-rule to fix it. See the recorded results and limitations in
[`docs/sleep/RESULTS.md`](../../docs/sleep/RESULTS.md).
Reproduce:
@@ -115,24 +156,32 @@ python -m skillopt_sleep.experiments.run_experiment --persona programmer --asse
Each prints the held-out score rising from baseline toward 1.0 as the gate
accepts the general rules your tasks need, and confirms the gate **rejects** an
injected harmful edit. Recorded output: [`docs/sleep/experiment_results.md`](../docs/sleep/experiment_results.md).
injected harmful edit. Context for the measured experiments is in
[`docs/sleep/RESULTS.md`](../../docs/sleep/RESULTS.md).
## Schedule it nightly
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/install-cron.sh" "$(pwd)" # prints a crontab line; installs nothing
/skillopt-sleep schedule --hour 3 --minute 17
/skillopt-sleep unschedule
```
The built-in scheduler creates a managed cron entry and logs under the project.
The scheduled run stages proposals unless `--auto-adopt` is explicitly selected.
## Safety
- **Read-only** harvest of `~/.claude`. `mock` replay has no side effects.
- Proposals are **staged**, never auto-applied (unless you opt in with `--auto-adopt`).
- Every adopt writes a backup under the staging dir's `backup/`.
- Per-night **token/task budget caps**; secrets redacted from prompts.
- `fresh` replay (Phase 3) runs only in throwaway git worktrees.
- `--max-sessions` and `--max-tasks` bound work, but the main CLI does not enforce
a hard token or wall-clock budget.
- Real backends share truncated session/task content with the selected provider;
do not assume outbound prompts have been fully redacted.
## Status
Phase 1 (engine + deterministic experiment + plugin surface) is complete.
Phase 3 adds the real-API miner/judge and `fresh` worktree replay. See
[`docs/superpowers/specs/2026-06-07-skillopt-sleep-claude-code-plugin-design.md`](../docs/superpowers/specs/2026-06-07-skillopt-sleep-claude-code-plugin-design.md).
The engine, deterministic experiment, Claude/Codex CLI backends, handoff mode,
and staged adoption flow are implemented. Advanced experiment-harness flags are
not automatically available on the nightly CLI; see the
[shared integration reference](../README.md#supported-cli-surface).
@@ -0,0 +1,74 @@
---
description: Run the SkillOpt-Sleep cycle with the handoff backend — no API subprocess; this session answers the engine's model calls via prompt/answer files, in isolated fresh-context subagents
argument-hint: "[run | dry-run] [--preferences \"...\"] (default: run)"
allowed-tools: Bash, Read, Write, Task
---
# /skillopt-sleep-handoff — session-executed sleep cycle
You are driving **SkillOpt-Sleep in handoff mode**: the Python engine runs
every deterministic stage (harvest → mine → replay scoring → gate → stage)
and outsources each model call (attempt / judge / reflect) to YOU via
prompt files. No `claude -p` subprocess, no API key — the model work runs
on this session's budget, but each prompt MUST be answered in a fresh,
isolated context so the validation gate stays honest.
## Requested action: $ARGUMENTS
(If `$ARGUMENTS` is empty, treat it as `run`.)
## The loop
Repeat until the engine exits 0 (done) — at most 8 rounds:
1. **Run the engine** via the bundled runner. Split `$ARGUMENTS` into the action
and remaining options, and preserve those options on every resumed round:
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" <action> --backend handoff --project "$(pwd)" --scope invoked <remaining options>
```
- exit 0 → the night is complete; go to "Finish" below.
- exit 3 → pending model calls; continue with step 2.
- anything else → stop and show the user the error output.
2. **Read the batch**: `Read` `.skillopt-sleep-handoff/pending.json` in the
project. Each entry has `id`, `prompt`, `max_tokens`, `answer_file`.
3. **Answer each prompt in ISOLATION** — this is the integrity rule:
- For each entry, launch a subagent (Task tool) whose ENTIRE input is
the `prompt` text verbatim. Add nothing: no summary of this session,
no mention of SkillOpt, no other prompts from the batch.
- Take the subagent's reply and `Write` the raw answer text (no
commentary, no code fences) to the entry's `answer_file`.
- NEVER answer from this session's own context — you have seen the
mined tasks and their references, so inline answers would contaminate
the held-out gate and fake the improvement score.
4. **Re-run the same engine command** — it resumes from the answers
directory and either finishes or stages the next batch.
## Finish
- For `run`, if the engine prints a staging directory, `Read` its `report.md`
and show the user: held-out baseline → candidate score, the gate decision,
the proposed edits, and where the proposal is staged. If an accepted proposal
was staged, tell the user nothing live changed and offer
`/skillopt-sleep adopt`.
- For `dry-run`, no staging directory or `report.md` is created; summarize the
final stdout instead.
- The engine archives `.skillopt-sleep-handoff/` on a completed real run;
do not delete it yourself.
## Safety reminders
- **Never** edit `CLAUDE.md` or `SKILL.md` yourself — only `adopt` does
that, with a backup.
- Mined tasks are pinned to `.skillopt-sleep-handoff/tasks.json` on round
one, so sessions created while answering prompts cannot shift the task
set. Do not edit that file.
- If a batch looks like it contains secrets or content the user would not
want re-processed, stop and ask before answering.
- Handoff files apply pattern-based secret redaction, but that is not a
guarantee that prompts are free of sensitive data. Treat the pending batch as
private user data and do not copy it into chat, logs, or commits.
+40 -24
View File
@@ -1,5 +1,5 @@
---
description: Run or manage the SkillOpt-Sleep self-evolution cycle (review past sessions, replay tasks offline, consolidate validated memory + skills; can also schedule nightly runs)
description: Run or manage the SkillOpt-Sleep self-evolution cycle (review past sessions, replay tasks through a selected backend, consolidate validated memory + skills, or schedule nightly runs)
argument-hint: "[run | dry-run | status | adopt | harvest | schedule | unschedule] (default: status)"
allowed-tools: Bash, Read
---
@@ -7,10 +7,11 @@ allowed-tools: Bash, Read
# /skillopt-sleep — SkillOpt-Sleep nightly self-evolution
You are driving **SkillOpt-Sleep**: a tool that lets this user's Claude agent
improve offline by reviewing past sessions, replaying recurring tasks, and
improve from past usage by reviewing sessions, replaying recurring tasks, and
consolidating what it learns into **validated** memory (`CLAUDE.md`) and skills
(`SKILL.md`). It is gated like SkillOpt: a change is kept only if it improves a
held-out replay score, and nothing live is modified until the user adopts it.
(`SKILL.md`). With the default gate enabled, a change is kept only if it improves
a held-out replay score. Nothing live is modified until adoption unless the
user explicitly requests `--auto-adopt`.
## Requested action: $ARGUMENTS
@@ -18,11 +19,14 @@ held-out replay score, and nothing live is modified until the user adopts it.
## How to run it
The engine is the `skillopt_sleep` Python package in this repo. Use the
**plugin's bundled runner** so the right interpreter and repo are on the path:
The engine is the `skillopt_sleep` Python package in this repo. Split
`$ARGUMENTS` into the first action token and its remaining options, then use the
**plugin's bundled runner** so the right interpreter and repo are on the path.
Preserve the user's remaining options (for example `--preferences`, `--backend`,
or `--target-skill-path`) instead of silently dropping them:
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" <action> --project "$(pwd)" --scope invoked
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" <action> --project "$(pwd)" --scope invoked <remaining options>
```
`<action>` is one of:
@@ -30,37 +34,49 @@ The engine is the `skillopt_sleep` Python package in this repo. Use the
| action | what it does |
|--------------|--------------|
| `status` | show how many nights have run + the latest staged proposal (READ-ONLY) |
| `dry-run` | harvest → mine → replay → report, but **stage nothing** (safe preview) |
| `run` | full cycle: also **stage** a reviewed proposal (still does NOT touch live files) |
| `dry-run` | harvest → mine → replay → report, but **stage nothing** (no-staging preview) |
| `run` | full cycle: **stage** a validation report and any accepted proposal; only explicit `--auto-adopt` may also update live files |
| `adopt` | apply the latest staged proposal to live `CLAUDE.md` / `SKILL.md` (backs up first) |
| `harvest` | debug: print the recurring tasks mined from recent sessions |
| `schedule` | install a nightly cron entry for this project (`--hour --minute`, off-:00 by default) |
| `unschedule` | remove the nightly cron entry (`--all` to remove every managed entry) |
Default backend is `mock` (deterministic, no API spend). To use real budget for
genuine improvement, add `--backend claude` or `--backend codex`. To steer what
the optimizer writes, add `--preferences "<your house rules>"`.
model-driven optimization, add `--backend claude` or `--backend codex`. An
accepted gain is evidence on this run's held-out tasks, not a guarantee of
general improvement; results depend on the tasks, model, and checks. To steer
what the optimizer writes, add `--preferences "<your house rules>"`.
## Steps to follow
1. **Run the requested action** via the bundled runner above. Capture stdout.
2. **For `run` / `dry-run`:** after it completes, `Read` the generated
`report.md` in the staging dir it prints, and show the user:
- held-out score: baseline → candidate (the proof it helped)
1. **Run the requested action** via the bundled runner above. Capture stdout and
stderr.
2. **For `run`:** if it prints a staging directory, `Read` its `report.md` and
show the user:
- held-out score: baseline → candidate (evidence on this run's held-out tasks)
- the gate decision (accept/reject) and the exact edits it proposes
- where the proposal is staged
3. **For `run` that produced an accepted proposal:** tell the user the diff is
staged and that **nothing live changed yet**. Offer to run `/skillopt-sleep adopt`.
4. **For `adopt`:** confirm which live files were updated and that backups were
3. **For `dry-run`:** no staging directory or `report.md` is created. Summarize
the score, gate decision, and edits from stdout (or request `--json` when
machine-readable output is useful).
4. **For `run` that produced an accepted proposal:** inspect whether stdout says
it was auto-adopted. If not, tell the user nothing live changed and offer
`/skillopt-sleep adopt`; if it was, report the updated paths explicitly.
5. **For `adopt`:** confirm which live files were updated and that backups were
written under the staging dir's `backup/`.
5. **Never** edit `CLAUDE.md` or `SKILL.md` yourself — only the `adopt` action
does that, with a backup. Respect the review gate.
6. **Never** edit `CLAUDE.md` or `SKILL.md` yourself — let the engine's explicit
`adopt` or user-requested `--auto-adopt` path apply its manifest and backup
behavior. Respect the review gate.
## Safety reminders
- Harvest is **read-only** over `~/.claude`. Replay in `mock` mode runs no
shell side effects.
- The cycle stages proposals; the user is in control of adoption.
- If the user asks to schedule this nightly, point them at
`${CLAUDE_PLUGIN_ROOT}/scripts/install-cron.sh` (prints a crontab line; does
not install anything without confirmation).
- The cycle stages proposals by default; auto-adoption requires explicit opt-in.
- A real backend sends truncated transcript excerpts and derived tasks to its
provider for mining, replay, judging, and reflection. Pattern-based redaction
is not a guarantee that outbound prompts are secret-free. For sensitive data,
use `mock` or first run `harvest --output <file>`, review/redact the file, set
`"reviewed": true`, and then pass it with `--tasks-file`.
- `schedule` manages a cron entry when `crontab` is available; otherwise it
prints a line for manual installation.
+2 -1
View File
@@ -24,6 +24,7 @@ cat <<EOF
${MIN} ${HOUR} * * * "${RUNNER}" run --project "${PROJECT}" --scope invoked --backend ${BACKEND} >> "${PROJECT}/.skillopt-sleep/cron.log" 2>&1
#
# For fully-autonomous adoption (power users), append: --auto-adopt
# To spend real API budget for genuine lift, set BACKEND=anthropic above.
# To use the authenticated Claude CLI for model-driven optimization, set
# BACKEND=claude above.
# ────────────────────────────────────────────────────────────────────────────
EOF
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# SkillOpt-Sleep shared runner — used by all platform plugins (Claude Code,
# Codex, Copilot). Resolves the repo root (which contains the skillopt_sleep
# package), picks a Python >= 3.10, and execs the engine CLI.
#
# Usage: run-sleep.sh <run|dry-run|status|adopt|harvest|...> [args...]
set -euo pipefail
# This script lives at <repo>/plugins/run-sleep.sh, so the repo root (which
# holds skillopt_sleep/) is one level up. CLAUDE_PLUGIN_ROOT (if set by Claude
# Code) points at the plugin dir; the engine is then two levels above it.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -d "$SCRIPT_DIR/../skillopt_sleep" ]; then
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -d "$CLAUDE_PLUGIN_ROOT/../../skillopt_sleep" ]; then
REPO_ROOT="$(cd "$CLAUDE_PLUGIN_ROOT/../.." && pwd)"
elif [ -n "${SKILLOPT_SLEEP_REPO:-}" ] && [ -d "$SKILLOPT_SLEEP_REPO/skillopt_sleep" ]; then
REPO_ROOT="$SKILLOPT_SLEEP_REPO"
else
# last resort: search upward from CWD
d="$PWD"
while [ "$d" != "/" ]; do
[ -d "$d/skillopt_sleep" ] && { REPO_ROOT="$d"; break; }
d="$(dirname "$d")"
done
fi
if [ "$#" -eq 0 ]; then set -- status; fi
if [ -n "${REPO_ROOT:-}" ]; then
# Source checkout: run from repo root so skillopt_sleep/ is importable.
PY=""
# Allow explicit Python override (useful on macOS with old system Python).
if [ -n "${SKILLOPT_SLEEP_PYTHON:-}" ]; then
PY="$SKILLOPT_SLEEP_PYTHON"
else
for cand in python3.12 python3.11 python3.10 python3; do
if command -v "$cand" >/dev/null 2>&1; then
ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)"
if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi
fi
done
fi
if [ -z "$PY" ]; then
echo "[sleep] ERROR: need Python >= 3.10 (found none)." >&2
exit 1
fi
cd "$REPO_ROOT"
exec "$PY" -m skillopt_sleep "$@"
fi
# No source checkout found — fall back to an installed engine.
# Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
# Checked before the import fallback because uv tool install / pipx isolate the
# package from the system Python's import path, so `python -c "import
# skillopt_sleep"` would fail even though the CLI is available.
if command -v skillopt-sleep >/dev/null 2>&1; then
exec skillopt-sleep "$@"
fi
# Fallback 2: importable as a module (pip install into the active Python).
# Pick a Python >= 3.10 and check importability.
PY=""
for cand in python3.12 python3.11 python3.10 python3; do
if command -v "$cand" >/dev/null 2>&1; then
ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)"
if [ "${ver:-0}" -ge 310 ] && "$cand" -c "import skillopt_sleep" >/dev/null 2>&1; then
PY="$cand"; break
fi
fi
done
if [ -n "$PY" ]; then
exec "$PY" -m skillopt_sleep "$@"
fi
echo "[sleep] ERROR: could not locate the skillopt_sleep package." >&2
echo "[sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt'," >&2
echo "[sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo." >&2
exit 1
+25 -6
View File
@@ -1,11 +1,30 @@
#!/usr/bin/env bash
# Claude Code plugin runner — thin wrapper over the shared runner so all three
# platform plugins share one engine launcher. The shared runner lives at
# <repo>/plugins/run-sleep.sh and handles repo-root + interpreter resolution.
# Claude Code plugin runner — thin wrapper over the shared runner so all
# platform plugins share one engine launcher.
#
# After marketplace install the plugin is isolated in a cache directory and
# the repo-relative path no longer works. We try four locations:
# 1. Co-located run-sleep.sh (bundled copy — works in marketplace cache)
# 2. Repo-relative ../../run-sleep.sh (dev checkout)
# 3. CLAUDE_PLUGIN_ROOT/../run-sleep.sh (plugin env variable)
# 4. SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh (explicit env)
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # <repo>/plugins/claude-code/scripts
SHARED="$(cd "$HERE/../.." && pwd)/run-sleep.sh" # <repo>/plugins/run-sleep.sh
if [ ! -f "$SHARED" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SHARED=""
if [ -f "$HERE/run-sleep.sh" ]; then
SHARED="$HERE/run-sleep.sh"
elif [ -f "$(cd "$HERE/../.." 2>/dev/null && pwd)/run-sleep.sh" ]; then
SHARED="$(cd "$HERE/../.." && pwd)/run-sleep.sh"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$(cd "$CLAUDE_PLUGIN_ROOT/.." 2>/dev/null && pwd)/run-sleep.sh" ]; then
SHARED="$(cd "$CLAUDE_PLUGIN_ROOT/.." && pwd)/run-sleep.sh"
elif [ -n "${SKILLOPT_SLEEP_REPO:-}" ] && [ -f "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" ]; then
SHARED="$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh"
fi
if [ -z "$SHARED" ]; then
echo "[sleep] ERROR: cannot locate run-sleep.sh." >&2
echo "[sleep] Set SKILLOPT_SLEEP_REPO to the SkillOpt repo root, or pip install skillopt." >&2
exit 1
fi
exec bash "$SHARED" "$@"
@@ -1,25 +1,29 @@
---
name: skillopt-sleep
description: "Use when the user wants their Claude agent to self-improve from past usage, asks about a nightly/offline 'sleep' or 'dream' cycle, memory/skill consolidation, or says things like 'make my agent better the more I use it', 'review my past sessions', 'learn my preferences', 'consolidate what you learned', 'run the sleep cycle', or wants to schedule offline self-optimization. Drives the skillopt_sleep engine: harvest past sessions -> mine recurring tasks -> replay offline -> consolidate validated CLAUDE.md/SKILL.md behind a held-out gate."
description: "Use when the user wants their Claude agent to self-improve from past usage, asks about a nightly/offline 'sleep' or 'dream' cycle, memory/skill consolidation, or says things like 'make my agent better the more I use it', 'review my past sessions', 'learn my preferences', 'consolidate what you learned', 'run the sleep cycle', or wants to schedule background self-optimization. Drives the skillopt_sleep engine: harvest past sessions -> mine recurring tasks -> replay through a selected backend -> consolidate validated CLAUDE.md/SKILL.md behind a held-out gate."
---
# SkillOpt-Sleep: offline self-evolution for a local Claude agent
# SkillOpt-Sleep: usage-driven self-evolution for a local Claude agent
SkillOpt-Sleep gives the user's agent a **sleep cycle**. While the user is
offline (e.g. nightly), it reviews their real past Claude Code sessions,
re-runs recurring tasks on their own API budget, and consolidates what it
learns into **memory** (`CLAUDE.md`) and **skills** (`SKILL.md`) — but only
keeps changes that pass a held-out validation gate, and only after the user
adopts them. The agent gets measurably better at *this* user's recurring work,
SkillOpt-Sleep gives the user's agent a **sleep cycle**. On demand or on a
nightly schedule, it reviews real past Claude Code sessions, re-runs recurring
tasks through the selected backend, and consolidates what it
learns into **memory** (`CLAUDE.md`) and **skills** (`SKILL.md`). With the
default validation gate enabled, it keeps only changes that improve a held-out
score. Live files change only through explicit adoption or a user-requested
`--auto-adopt`. It aims to improve this user's recurring work, while making
each accepted proposal measurable on the run's held-out tasks,
with no model-weight training. It is the deployment-time analogue of training:
short-term experience → long-term competence.
It synthesizes three ideas:
- **SkillOpt** — the skill/memory doc is trainable text; bounded add/delete/replace
edits; accepted only through a held-out gate; rejected edits become negative feedback.
- **Claude Dreams** — offline consolidation that reads past sessions and rebuilds
memory (dedup/merge/resolve); the input is never mutated; output is reviewed then adopted.
- **Agent sleep** — periodic offline replay turns episodes into durable skill.
edits; accepted only through a held-out gate; rejected edits are recorded in
the run report for review.
- **Claude Dreams** — consolidation that reads past sessions and proposes changes
inside protected learned blocks; the input is never mutated, and output is
reviewed before adoption.
- **Agent sleep** — periodic background replay turns episodes into durable skill.
## When to use this skill
@@ -34,9 +38,14 @@ Trigger when the user wants any of:
1. **Harvest** — read `~/.claude/projects/*/<session>.jsonl` + `~/.claude/history.jsonl` (READ-ONLY) → session digests.
2. **Mine** — digests → `TaskRecord`s (recurring intents + outcome labels + checkable refs where possible).
3. **Replay** — re-run tasks offline under the *current* skill+memory → (hard, soft) scores.
4. **Consolidate** — reflect on failures → propose bounded edits → **gate** on a held-out slice; accept only if it strictly improves.
5. **Stage** — write `proposed_CLAUDE.md`, `proposed_SKILL.md`, a diff, and `report.md` into `<project>/.skillopt-sleep/staging/<date>/`. **Nothing live changes.**
3. **Replay** — re-run tasks through the selected backend under the *current*
skill+memory → (hard, soft) scores.
4. **Consolidate** — reflect on failures → propose bounded edits → **gate** on a held-out slice; with the default gate enabled, accept only if it strictly improves.
5. **Stage** — write the accepted `proposed_CLAUDE.md` and/or
`proposed_SKILL.md`, plus `report.md`, `report.json`, `manifest.json`, and
`diagnostics.json` into `<project>/.skillopt-sleep/staging/<timestamp>/`.
**Nothing live changes.** A rejected run still has a report but no proposed
live-file replacement.
6. **Adopt** — explicit (or opt-in auto): copy staged files over live ones, backing up first.
## How to drive it
@@ -45,35 +54,102 @@ Prefer the `/skillopt-sleep` command. Under the hood it calls the bundled runner
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" status # what's happened
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" dry-run --project "$(pwd)" # safe preview
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" dry-run --project "$(pwd)" # no-staging preview
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" run --project "$(pwd)" # full cycle, stages a proposal
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" adopt --project "$(pwd)" # apply staged proposal (with backup)
```
- Default backend is `mock` (deterministic, **no API spend**) — good for trying the plumbing.
- Add `--backend claude` or `--backend codex` to spend the user's real budget for genuine improvement.
- Scope defaults to the invoked project; `--scope all` harvests every project.
- Add `--backend claude` or `--backend codex` to spend the user's real budget
for model-driven optimization. A held-out gain is run-specific evidence, not
a guarantee of broader improvement; results depend on the tasks, model, and
checks.
- Scope defaults to the invoked project; `--scope all` harvests every Claude
project into the current run's configured targets.
- A real backend sends truncated transcript/task content to its provider. See
the data-boundary rules below before using one with sensitive sessions.
### Scheduling
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" schedule --project "$(pwd)" --hour 3 --minute 17
"${CLAUDE_PLUGIN_ROOT}/scripts/sleep.sh" unschedule --project "$(pwd)"
```
Installs a nightly cron entry. `unschedule --all` removes every managed entry.
## Common CLI flags
| Flag | Default | Description |
|------|---------|-------------|
| `--project PATH` | cwd | Project directory to evolve |
| `--scope all\|invoked` | invoked | Harvest scope |
| `--backend mock\|claude\|codex\|copilot\|handoff\|azure_openai` | mock | Backend (mock = no provider calls) |
| `--model NAME` | backend default | Override the model used for replay |
| `--source claude\|codex\|auto` | claude | Transcript source |
| `--lookback-hours N` | 72 | Harvest window |
| `--max-sessions N` | derived | Cap harvested sessions; defaults to 3 × max tasks (120 with current defaults) |
| `--max-tasks N` | 40 | Cap mined tasks |
| `--target-skill-path PATH` | `~/.claude/skills/skillopt-sleep-learned/SKILL.md` | Explicit SKILL.md to evolve |
| `--tasks-file PATH` | — | Reviewed TaskRecord JSON (skip harvest) |
| `--progress` | off | Print phase progress to stderr |
| `--auto-adopt` | off | Auto-adopt if gate passes |
| `--edit-budget N` | 4 | Max bounded edits per night |
| `--preferences TEXT` | empty | Add house rules to the optimizer's reflection prior |
| `--json` | off | Machine-readable JSON output |
The CLI also has source/runtime path overrides (`--claude-home`, `--codex-home`,
and `--codex-path`) and action-specific flags. Use
`python -m skillopt_sleep <action> --help` as the authoritative surface.
## Config keys (`~/.skillopt-sleep/config.json`)
Beyond the CLI flags, advanced behavior is controlled via config:
- **`preferences`** — free-text house rules injected into the optimizer's reflect step (e.g. "Always use async/await", "Answers in `\boxed{}`").
- **`gate_mode`** — `on` (default, validation-gated) or `off` (greedy, accept all edits).
- **`gate_metric`** — `hard`, `soft`, or `mixed` (default). Controls how the held-out gate scores.
- **`dream_rollouts`** — >1 enables multi-rollout contrastive reflection per task.
- **`recall_k`** — >0 recalls K similar past tasks into the dream (long-term memory).
- **`evolve_memory`** / **`evolve_skill`** — independently toggle CLAUDE.md vs SKILL.md consolidation.
## Memory consolidation
The sleep cycle can consolidate both:
- **SKILL.md** — the managed skill file (bounded edits: add/delete/replace)
- **CLAUDE.md** — the project memory (same bounded edits)
With the default gate enabled, both are evaluated by the same held-out score.
Set `evolve_memory: false` to consolidate only skills, or `evolve_skill: false`
for only memory.
## Hard rules
- **Never** hand-edit the user's `CLAUDE.md` / `SKILL.md` as part of this skill.
Only the `adopt` action changes live files, and it backs them up first.
Let the engine's explicit `adopt` or user-requested `--auto-adopt` path apply
the staging manifest and back up existing live files first.
- Harvest is read-only. `mock` replay has no side effects.
- Real backends send truncated transcript excerpts and derived tasks to the
selected provider for mining, replay, judging, and reflection. The Claude
transcript path is not guaranteed to remove every secret before those calls.
Review provider policy and session contents first. For sensitive data, use
`mock` or run `harvest --output <file>`, inspect/redact the JSON, set
`"reviewed": true`, and replay it with `--tasks-file`; real backends refuse an
unreviewed task file.
- Always show the user the **held-out baseline → candidate** score and the
exact proposed edits before suggesting adoption. Evidence before adoption.
- If asked whether it really helps, run
- If asked to demonstrate the mechanism without provider calls, run
`python -m skillopt_sleep.experiments.run_experiment --persona researcher --json`
— a deterministic demo that proves held-out lift and that the gate blocks
harmful edits.
— a deterministic synthetic demo of held-out lift and gate rejection. It
validates the mechanism, not effectiveness on the user's own tasks.
## Validate / demo
```bash
# deterministic proof (no API): held-out score rises, gate blocks regressions
# deterministic synthetic demo (no API): score rises and the gate blocks a regression
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
python -m skillopt_sleep.experiments.run_experiment --persona programmer --assert-improves
```
See `docs/sleep/experiment_results.md` for recorded output and
`docs/superpowers/specs/2026-06-07-skillopt-sleep-claude-code-plugin-design.md`
for the full design.
See the [SkillOpt-Sleep documentation](https://github.com/microsoft/SkillOpt/tree/main/docs/sleep)
for recorded results, limitations, and the supported integration surface.
+76 -20
View File
@@ -9,51 +9,107 @@ as the Claude Code plugin (`skillopt_sleep`), wrapped for Codex.
> [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1`
> benchmark, a deliberately deficient skill goes **0.00 → 1.00** on a held-out
> set with the Codex backend (incl. the tool-use seed via a real tool loop).
> See [`../../docs/sleep/FINAL_REPORT.md`](../../docs/sleep/FINAL_REPORT.md).
> See the recorded results and limitations in
> [`docs/sleep/RESULTS.md`](../../docs/sleep/RESULTS.md).
## What Codex supports (and what we use)
Codex (`@openai/codex`) extends via **`AGENTS.md`** instructions, **skills** at
`~/.agents/skills/<name>/SKILL.md`, and **custom prompts** at
`~/.codex/prompts/<name>.md` (invoked as `/<name>`). This integration ships all
three, plus a shared runner.
`~/.agents/skills/<name>/SKILL.md`, and plugins that can distribute skills.
Custom prompts are deprecated in Codex, so this integration is skill-first: the
installed `skillopt-sleep` skill contains the launch commands and operating
rules. The shared runner remains a plain shell entrypoint that the skill calls.
## Install
On Linux/macOS:
```bash
git clone <repo-url> SkillOpt-Sleep
cd SkillOpt-Sleep
bash plugins/codex/install.sh # installs the /skillopt-sleep prompt + skill
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
bash plugins/codex/install.sh # installs the skill
export SKILLOPT_SLEEP_REPO="$(pwd)" # so the runner is found from anywhere
```
On Windows (PowerShell):
```powershell
git clone <repo-url> SkillOpt-Sleep
cd SkillOpt-Sleep
powershell -File plugins/codex/install.ps1
[System.Environment]::SetEnvironmentVariable("SKILLOPT_SLEEP_REPO", "$(pwd)", "User")
```
If a previous install created `~/.codex/prompts/sleep.md`, the installer moves
that deprecated prompt aside with a `.skillopt-legacy*.bak` suffix.
Requires Python ≥ 3.10 and the `codex` CLI on PATH.
## Use
Mention `$skillopt-sleep` where Codex supports explicit skill mentions, or ask
Codex in natural language:
```text
/skillopt-sleep status # what's happened
/skillopt-sleep dry-run # safe preview, stages nothing
/skillopt-sleep run # full cycle, stages a reviewed proposal (no live edits)
/skillopt-sleep adopt # apply the staged proposal (with backup)
Use the skillopt-sleep skill to run status for this project.
Use the skillopt-sleep skill to run a dry-run for this project.
Use the skillopt-sleep skill to run the full cycle for this project with the Codex backend.
Use the skillopt-sleep skill to adopt the latest staged proposal.
```
Or call the engine directly:
```bash
python -m skillopt_sleep run --project "$(pwd)" --backend codex
python -m skillopt_sleep dry-run --project "$(pwd)" --source codex --backend mock
python -m skillopt_sleep run --project "$(pwd)" --source codex --backend codex \
--max-sessions 5 --max-tasks 3 --progress
python -m skillopt_sleep run --project "$(pwd)" --source codex --backend codex \
--target-skill-path .agents/skills/example/SKILL.md \
--max-sessions 5 --max-tasks 3 --progress
```
Default backend is `mock` (no API spend). `--backend codex` uses your Codex
budget for real improvement. All the controllable knobs (`--gate on|off`,
`--rollouts-k`, `--budget-tokens`, `--preferences`, optimizer/target split) work
identically — see [`../../docs/sleep/CONTROLLABLE_DREAMING.md`](../../docs/sleep/CONTROLLABLE_DREAMING.md).
`--source codex` reads Codex Desktop archived sessions from
`~/.codex/archived_sessions`. Use `--codex-home /path/to/.codex` to point at a
different Codex home, or `--source auto` to try Codex archives first and fall
back to Claude Code transcripts. Default backend is `mock` (no API spend).
`--backend codex` uses your Codex budget for model-driven optimization; an
accepted gain is task-dependent, not guaranteed. Bound live runs
with `--max-sessions` and `--max-tasks`; add `--progress` because Codex-backed
mining, replay, and reflection can be slow and otherwise quiet. Use
`--target-skill-path` to stage/adopt into a repo-scoped Codex skill such as
`.agents/skills/<name>/SKILL.md`; target runs over-sample mined tasks and
prefer tasks that match the target skill's path, headings, and content. The
implemented main-CLI flags work the same across the shared integrations, and
`--preferences "..."` is available for house rules. Advanced keys such as
`gate_mode`, `dream_rollouts`, and `recall_k` belong in the Sleep config; the
nightly CLI does not expose `--gate`, `--rollouts-k`, token/time-budget, or
optimizer/target-split flags. See the
[shared CLI reference](../README.md#supported-cli-surface).
For privacy-sensitive projects, split the run into reviewable steps:
```bash
python -m skillopt_sleep harvest --project "$(pwd)" --source codex \
--target-skill-path .agents/skills/example/SKILL.md \
--max-sessions 5 --max-tasks 3 \
--output reviewed-tasks.json
python -m skillopt_sleep dry-run --project "$(pwd)" --backend codex \
--tasks-file reviewed-tasks.json --progress --json
```
Inspect/redact the JSON and set `"reviewed": true` before using a real backend.
`--tasks-file` skips archive harvest/mining and replays only the reviewed JSON
tasks; real backends refuse task files still marked `"reviewed": false`.
This review step matters even though the Codex transcript converter removes
known secret-shaped strings: pattern-based redaction is not a guarantee. A real
backend sends truncated transcript/task content to the selected provider, while
`--backend mock` makes no provider calls.
## Notes / status
- Codex's `exec` runs shell, so the real-tool-loop replay (e.g. the
`tool_called: search` benchmark seed) works natively.
- Codex's standalone *plugin-package manifest* format is not yet a stable public
spec; this integration uses the documented `AGENTS.md` + skills + prompts
mechanisms, which are stable. If/when a `codex plugin` package format ships,
we'll add a one-file manifest.
- This integration no longer installs a `.codex/prompts` slash command. Skills
are the reusable Codex workflow surface; mention `skillopt-sleep` explicitly
or ask for a sleep/dream/offline self-improvement run and Codex can load the
skill.
+47
View File
@@ -0,0 +1,47 @@
# Install the SkillOpt-Sleep Codex integration as a user-level Codex skill on Windows.
# Idempotent; prints what it does.
$ErrorActionPreference = "Stop"
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE ".codex" }
$AgentsSkills = Join-Path $env:USERPROFILE ".agents\skills"
$LegacyPrompt = Join-Path $CodexHome "prompts\sleep.md"
Write-Output "[install] repo: $RepoRoot"
# 1) user-level skill
$SkillDir = Join-Path $AgentsSkills "skillopt-sleep"
if (-not (Test-Path $SkillDir)) {
New-Item -ItemType Directory -Path $SkillDir -Force | Out-Null
}
Copy-Item (Join-Path $RepoRoot "plugins\codex\skills\skillopt-sleep\SKILL.md") (Join-Path $SkillDir "SKILL.md") -Force
Write-Output "[install] skill -> $(Join-Path $SkillDir 'SKILL.md')"
# 2) retire the old custom prompt entrypoint from previous installs
if (Test-Path $LegacyPrompt) {
$Backup = "${LegacyPrompt}.skillopt-legacy.bak"
if (Test-Path $Backup) {
$DateStr = Get-Date -Format "yyyyMMddHHmmss"
$Backup = "${LegacyPrompt}.skillopt-legacy.${DateStr}.bak"
}
Move-Item $LegacyPrompt $Backup -Force
Write-Output "[install] legacy prompt -> $Backup"
}
# 3) record the repo location so the runner is found from anywhere
Write-Output "[install] add to your environment variables:"
Write-Output " [System.Environment]::SetEnvironmentVariable('SKILLOPT_SLEEP_REPO', '$RepoRoot', 'User')"
Write-Output " Or set it via System Properties."
# 4) optional: append an AGENTS.md hint (only if the user opts in)
Write-Output ""
Write-Output "[install] Optional — add this to ~/.codex/AGENTS.md so Codex always knows the tool:"
Write-Output ""
Write-Output " ## SkillOpt-Sleep"
Write-Output " Use the skillopt-sleep skill when I ask to run a sleep/dream/offline"
Write-Output " self-improvement cycle. The runner is:"
Write-Output " \`powershell -File `"$RepoRoot\plugins\run-sleep.ps1`" status --project `"\$(pwd)\`"\`."
Write-Output ""
Write-Output "Done. Try asking Codex:"
Write-Output " Use the skillopt-sleep skill to run status for this project."
+19 -11
View File
@@ -1,24 +1,30 @@
#!/usr/bin/env bash
# Install the SkillOpt-Sleep Codex integration into the user's ~/.codex and
# ~/.agents directories. Idempotent; prints what it does.
# Install the SkillOpt-Sleep Codex integration as a user-level Codex skill.
# Idempotent; prints what it does.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
AGENTS_SKILLS="${HOME}/.agents/skills"
LEGACY_PROMPT="$CODEX_HOME/prompts/sleep.md"
echo "[install] repo: $REPO_ROOT"
# 1) custom /skillopt-sleep prompt
mkdir -p "$CODEX_HOME/prompts"
cp "$REPO_ROOT/plugins/codex/prompts/skillopt-sleep.md" "$CODEX_HOME/prompts/skillopt-sleep.md"
echo "[install] /skillopt-sleep prompt -> $CODEX_HOME/prompts/skillopt-sleep.md"
# 2) user-level skill
# 1) user-level skill
mkdir -p "$AGENTS_SKILLS/skillopt-sleep"
cp "$REPO_ROOT/plugins/codex/skills/skillopt-sleep/SKILL.md" "$AGENTS_SKILLS/skillopt-sleep/SKILL.md"
echo "[install] skill -> $AGENTS_SKILLS/skillopt-sleep/SKILL.md"
# 2) retire the old custom prompt entrypoint from previous installs
if [ -f "$LEGACY_PROMPT" ]; then
backup="${LEGACY_PROMPT}.skillopt-legacy.bak"
if [ -e "$backup" ]; then
backup="${LEGACY_PROMPT}.skillopt-legacy.$(date +%Y%m%d%H%M%S).bak"
fi
mv "$LEGACY_PROMPT" "$backup"
echo "[install] legacy prompt -> $backup"
fi
# 3) record the repo location so the runner is found from anywhere
echo "[install] add to your shell profile:"
echo " export SKILLOPT_SLEEP_REPO=\"$REPO_ROOT\""
@@ -29,8 +35,10 @@ cat <<EOF
[install] Optional — add this to ~/.codex/AGENTS.md so Codex always knows the tool:
## SkillOpt-Sleep
An offline self-improvement cycle is available. To run it:
\`bash "$REPO_ROOT/plugins/run-sleep.sh" status\`. Use \`/skillopt-sleep\` for the guided flow.
Use the skillopt-sleep skill when I ask to run a sleep/dream/offline
self-improvement cycle. The runner is:
\`bash "$REPO_ROOT/plugins/run-sleep.sh" status --project "\$(pwd)"\`.
Done. Try: /skillopt-sleep status
Done. Try asking Codex:
Use the skillopt-sleep skill to run status for this project.
EOF
-21
View File
@@ -1,21 +0,0 @@
# /skillopt-sleep — SkillOpt-Sleep for Codex
#
# Custom prompt: copy this file to ~/.codex/prompts/skillopt-sleep.md and invoke with
# `/skillopt-sleep` in the Codex CLI. ($ARGUMENTS is the text after /skillopt-sleep.)
Run the SkillOpt-Sleep offline self-evolution cycle. Action: $ARGUMENTS
(empty → "status").
Use the bundled runner via shell:
bash "${SKILLOPT_SLEEP_REPO:?set SKILLOPT_SLEEP_REPO to the repo root}/plugins/run-sleep.sh" $ARGUMENTS --project "$(pwd)"
Then:
- For `run`/`dry-run`: read the staged `report.md` and show the held-out
baseline → candidate score and the proposed edits. `run` only stages a
proposal; nothing live changes until `adopt`.
- For `adopt`: confirm which files were updated and that a backup was written.
- Never edit the user's AGENTS.md / skills yourself; only `adopt` does that.
Default backend is `mock` (no API spend). Add `--backend codex` for real
improvement on the user's Codex budget.
+157 -21
View File
@@ -1,49 +1,185 @@
---
name: skillopt-sleep
description: Nightly offline self-evolution for a Codex agent. Reviews past sessions, replays recurring tasks, and consolidates validated memory + skills behind a held-out gate. Use when the user wants Codex to learn from past usage, run a "sleep"/"dream" cycle, or schedule offline self-optimization.
description: "Use when the user wants Codex to self-improve from past usage, asks about a nightly/offline 'sleep' or 'dream' cycle, wants Codex to review past sessions, learn preferences, consolidate memory/skills, run dry-run/run/adopt/status for SkillOpt-Sleep, or schedule background self-optimization. Drives the skillopt_sleep engine: harvest past sessions -> mine recurring tasks -> replay through a selected backend -> consolidate validated memory + skills behind a held-out gate."
---
# SkillOpt-Sleep (Codex skill)
# SkillOpt-Sleep: usage-driven self-evolution for a local Codex agent
This skill drives the `skillopt_sleep` engine — an offline "sleep cycle" that
makes a Codex agent better at the user's recurring work without retraining.
SkillOpt-Sleep gives the user's Codex agent a sleep cycle. On demand or on a
nightly schedule, it reviews past local sessions, re-runs recurring tasks
through the selected backend, and proposes changes to a configured skill and to
the project's `CLAUDE.md`. With the default validation gate enabled, it keeps
only changes that improve a held-out score. Live files change only through
explicit adoption or a user-requested `--auto-adopt`. There is no model-weight
training.
The current shared engine does **not** write `AGENTS.md`. For a Codex-visible
result, always select a Codex skill explicitly with `--target-skill-path` (for
example `.agents/skills/<name>/SKILL.md`). If project `CLAUDE.md` is not a
desired secondary target, set `"evolve_memory": false` in
`~/.skillopt-sleep/config.json` before running.
## When to use
Trigger when the user wants to: review past sessions, learn their preferences,
consolidate feedback into long-term memory/skills, run a nightly/offline
self-improvement cycle, or adopt a staged proposal.
Trigger when the user wants any of:
## How to run it
- Codex to learn from past sessions or get better the more they use it;
- a nightly/scheduled or on-demand sleep/dream/offline self-improvement run;
- to review past sessions and distill recurring tasks;
- to consolidate feedback into memory or managed skills;
- to run `status`, `harvest`, `dry-run`, `run`, or `adopt` for SkillOpt-Sleep.
## The cycle
1. **Harvest** - read local session transcripts according to the engine
configuration and normalize them into session digests.
2. **Mine** - turn digests into recurring `TaskRecord`s with outcomes and
checkable references where possible.
3. **Replay** - re-run mined tasks through the selected backend under the
current skill and memory.
4. **Consolidate** - reflect on failures and propose bounded edits.
5. **Gate** - with the default gate enabled, accept edits only when the held-out
validation score improves.
6. **Stage** - write the proposal under
`<project>/.skillopt-sleep/staging/<date>/`; nothing live changes.
7. **Adopt** - explicitly, or through user-requested auto-adopt, copy staged
files over live files with backups for existing targets.
## How to drive it
Invoke the bundled runner via shell (Codex `exec` has shell access). The runner
finds the engine and a Python 3.10 automatically:
finds the engine and a Python >= 3.10 automatically.
```bash
# point at the repo if it isn't auto-detected from CWD:
export SKILLOPT_SLEEP_REPO=/path/to/SkillOpt-Sleep
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" <action> --project "$(pwd)"
export SKILLOPT_SLEEP_REPO=/path/to/SkillOpt
TARGET_SKILL=.agents/skills/example/SKILL.md
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" status --project "$(pwd)"
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" harvest --project "$(pwd)" \
--source codex --target-skill-path "$TARGET_SKILL"
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" dry-run --project "$(pwd)" \
--source codex --target-skill-path "$TARGET_SKILL" --backend mock
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" run --project "$(pwd)" \
--source codex --target-skill-path "$TARGET_SKILL" --backend codex \
--max-sessions 5 --max-tasks 3 --progress
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" adopt --project "$(pwd)"
```
`<action>``status | dry-run | run | adopt | harvest`. Use `--backend codex`
for real improvement on the user's own Codex budget (default `mock` = no spend).
On Windows (CMD / PowerShell):
```cmd
:: CMD
set SKILLOPT_SLEEP_REPO=C:\path\to\SkillOpt-Sleep
"%SKILLOPT_SLEEP_REPO%\plugins\run-sleep.cmd" status --project "%CD%"
```
```powershell
# PowerShell
$env:SKILLOPT_SLEEP_REPO = "C:\path\to\SkillOpt-Sleep"
powershell -File "$env:SKILLOPT_SLEEP_REPO\plugins\run-sleep.ps1" status --project "$(pwd)"
```
Actions are `status`, `harvest`, `dry-run`, `run`, `adopt`, `schedule`, and `unschedule`.
- Default backend is `mock`, which is deterministic and spends no API budget.
- `--backend codex` uses the user's Codex budget for model-driven optimization.
An accepted held-out gain is run-specific evidence, not a guarantee of
broader improvement; results depend on the tasks, model, and checks.
- `--source codex` reads Codex Desktop archived sessions from `~/.codex/archived_sessions`;
use `--codex-home /path/to/.codex` if the archive lives elsewhere.
- `--target-skill-path` is required for a Codex skill target. Without it, the
shared default is a Claude-managed skill under `~/.claude/skills/`, not an
`.agents` skill.
- Keep `dry-run --backend mock` as the first smoke check unless the user
explicitly asked for a real optimization run.
### Scheduling
```bash
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" schedule --project "$(pwd)" \
--backend codex --hour 3 --minute 17
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" unschedule --project "$(pwd)"
```
The scheduler persists the project, backend, time, and optional auto-adopt flag;
it does not persist `--source` or `--target-skill-path` from this command. Before
scheduling a Codex-targeted run, set `"transcript_source": "codex"` and an
absolute `"target_skill_path"` in `~/.skillopt-sleep/config.json`. On systems
without `crontab`, `schedule` prints a line for manual installation.
`unschedule --all` removes every managed entry.
### All backends
- `--backend mock` — deterministic, no API spend (default)
- `--backend claude` — uses the Claude CLI
- `--backend codex` — uses the Codex CLI
- `--backend copilot` — uses the GitHub Copilot CLI
- `--backend handoff` — emits prompt/answer files for an interactive session
- `--backend azure_openai` — uses the configured Azure OpenAI endpoint
### Additional flags
| Flag | Description |
|------|-------------|
| `--auto-adopt` | Auto-adopt if the gate passes (default: stage only) |
| `--edit-budget N` | Max bounded edits per night (default: 4) |
| `--lookback-hours N` | Harvest window in hours (default: 72) |
| `--json` | Machine-readable JSON output |
### Config keys (`~/.skillopt-sleep/config.json`)
- **`preferences`** — free-text house rules for the optimizer
- **`gate_mode`** — `on` (validation-gated, default) or `off` (greedy)
- **`gate_metric`** — `hard` | `soft` | `mixed` (default)
- **`dream_rollouts`** — >1 for multi-rollout contrastive reflection
- **`recall_k`** — >0 recalls similar past tasks from the archive
### Memory consolidation
The shared sleep cycle consolidates project **memory** (`CLAUDE.md`) and the
selected **skill** (`SKILL.md`) by default. It does not update `AGENTS.md`.
Each target is independently toggleable through `evolve_memory` /
`evolve_skill`, and both are gated by the same held-out validation score.
## Steps
1. Run the requested action; capture stdout.
2. For `run`/`dry-run`: read the staged `report.md` it prints and show the user
the held-out baseline → candidate score and the exact proposed edits.
3. `run` only **stages** a proposal under `<project>/.skillopt-sleep/staging/`;
nothing live changes until `adopt`. Offer `/skillopt-sleep adopt`.
4. Never hand-edit the user's `AGENTS.md` / skills yourself — only `adopt` does,
and it backs up first.
2. For `dry-run` and `run`, report the held-out baseline -> candidate score,
gate action, task count, session count, and exact proposed edits.
3. If a staging directory is printed, read `report.md` before summarizing.
4. `run` stages by default; if `--auto-adopt` was explicitly supplied, report
the paths it updated instead of claiming nothing changed.
5. Offer adoption only after the user has reviewed a still-staged proposal.
6. Never hand-edit the configured `CLAUDE.md` or target skill as a substitute
for the engine's adopt path; adoption is the safety boundary and backs up
existing targets first.
## Hard rules
- Harvest is read-only. Do not edit archived sessions or raw transcripts.
- Codex transcript harvesting removes known secret-shaped strings, developer
instructions, and raw tool payloads, but pattern-based redaction is not a
guarantee. A real backend still sends truncated transcript/task content to
its provider. Review sensitive sessions and provider policy first; prefer a
reviewed `--tasks-file` workflow when the data boundary matters.
- Keep raw secrets, credentials, private user data, and transcript contents out
of messages, logs, generated artifacts, and commits.
- Show validation evidence before recommending adoption.
- Treat generated edits as proposals, not as source of truth.
- Do not rely on deprecated custom prompts or `/sleep` slash commands for this
Codex integration. This skill is the entrypoint.
## Validate
```bash
python -m skillopt_sleep dry-run --project "$(pwd)" --source codex \
--target-skill-path .agents/skills/example/SKILL.md --backend mock --json
python -m skillopt_sleep.experiments.run_gbrain --backend codex \
--seeds brief-writer --data-root /path/to/gbrain-evals/eval/data/skillopt-v1 \
--nights 2 --limit-replay 3 --limit-holdout 3
```
A deficient skill goes 0.00 → 1.00 on a held-out set; the optimizer's edits are
gated on real-task performance.
In the recorded `brief-writer` gbrain run, the deliberately deficient fixture
went 0.00 -> 1.00 on that run's held-out set. Treat this as reproducible
benchmark evidence for that configuration, not a guarantee for other skills,
tasks, or models; see the
[recorded results](https://github.com/microsoft/SkillOpt/blob/main/docs/sleep/RESULTS.md)
for context and limitations.
+28 -11
View File
@@ -27,8 +27,8 @@ Requires Python ≥ 3.10. No third-party packages — the server is pure stdlib.
"mcpServers": {
"skillopt-sleep": {
"command": "python3",
"args": ["/abs/path/SkillOpt-Sleep/plugins/copilot/mcp_server.py"],
"env": { "SKILLOPT_SLEEP_REPO": "/abs/path/SkillOpt-Sleep" }
"args": ["/abs/path/SkillOpt/plugins/copilot/mcp_server.py"],
"env": { "SKILLOPT_SLEEP_REPO": "/abs/path/SkillOpt" }
}
}
}
@@ -42,11 +42,27 @@ Requires Python ≥ 3.10. No third-party packages — the server is pure stdlib.
## Use
Ask Copilot things like *"run the sleep cycle"*, *"what did the last sleep
propose?"*, *"adopt the staged sleep proposal"*. Copilot calls the MCP tools:
`sleep_status`, `sleep_dry_run`, `sleep_run`, `sleep_adopt`, `sleep_harvest`.
propose?"*, *"adopt the staged sleep proposal"*. The server exposes seven MCP
tools: `sleep_status`, `sleep_dry_run`, `sleep_run`, `sleep_adopt`,
`sleep_harvest`, `sleep_schedule`, and `sleep_unschedule`.
Each tool takes optional `project`, `backend` (`mock`/`claude`/`codex`), and
`scope` arguments. Default backend is `mock` (no API spend).
Each tool takes optional `project`, `backend` (`mock`/`claude`/`codex`/`copilot`), and
`scope` arguments. Default backend is `mock` (no API spend). The `copilot`
backend drives the GitHub Copilot CLI (`copilot -p ... --output-format json`)
and requires the `copilot` CLI to be installed and authenticated.
Harvesting is local and read-only, and the default `mock` backend makes no
provider calls. A real backend sends truncated transcript excerpts and derived
tasks to the selected provider. Outbound prompts are not currently guaranteed
to be secret-free; review sensitive data and provider policy first. See the
[shared data-boundary guidance](../README.md#data-boundary).
For speed, the `copilot` backend runs each call against an isolated
`COPILOT_HOME` with built-in MCP servers and custom instructions disabled, so
your user MCP servers (including this project's own) are not spawned per call
(~5x faster). Override with `SKILLOPT_SLEEP_COPILOT_HOME=<dir>`, pick a model
with `SKILLOPT_SLEEP_COPILOT_MODEL`, or set `SKILLOPT_SLEEP_COPILOT_FULL_ENV=1`
to use your real Copilot environment instead.
## Verify the server directly (no Copilot needed)
@@ -56,12 +72,13 @@ printf '%s\n' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| SKILLOPT_SLEEP_REPO="$(pwd)" python3 plugins/copilot/mcp_server.py
```
You should see the server info and the five `sleep_*` tools.
You should see the server info and all seven `sleep_*` tools.
## Notes / status
- MCP is the stable, official Copilot extension surface, so this is the most
portable of the three integrations (one server → CLI + IDE).
- The engine and all its controls (gate on/off, multi-rollout, budget,
preferences, optimizer/target split) are identical across platforms — see
[`../../docs/sleep/CONTROLLABLE_DREAMING.md`](../../docs/sleep/CONTROLLABLE_DREAMING.md).
portable shared-engine integration (one server → CLI + IDE).
- The MCP schema exposes the main CLI's implemented controls, including task and
session caps, target-skill selection, scheduling, and staged adoption. It does
not add experiment-only gate, rollout, token/time-budget, or optimizer/target
split flags. See the [shared CLI reference](../README.md#supported-cli-surface).
@@ -8,18 +8,49 @@ automatically as ambient guidance.)
This project has SkillOpt-Sleep available via an MCP server (`skillopt-sleep`).
It gives the agent a nightly "sleep cycle": it reviews past sessions, replays
recurring tasks offline, and consolidates validated memory + skills behind a
held-out gate.
recurring tasks through a selected backend, and stages validation-gated changes
to project `CLAUDE.md` and a configured `SKILL.md`.
When the user asks to "run the sleep cycle", "review my past sessions", "learn
my preferences", or "make the agent improve from past usage", use the MCP tools:
- `sleep_status` — what's happened + the latest staged proposal
- `sleep_dry_run`safe preview, stages nothing
- `sleep_run` — full cycle, stages a reviewed proposal (nothing live changes)
- `sleep_adopt` — apply the staged proposal (backs up first)
- `sleep_dry_run`no-staging preview; a real backend still makes provider calls
- `sleep_run` — full cycle, stages a validation-gated proposal by default;
explicit `auto_adopt` may update live files
- `sleep_adopt` — apply the staged proposal (backs up an existing live file first)
- `sleep_harvest` — list mined recurring tasks
- `sleep_schedule` — install a nightly cron entry (set `hour`/`minute`)
- `sleep_unschedule` — remove the nightly cron entry
### Key parameters (pass as MCP tool arguments)
- `backend``mock` (default, no provider calls), `claude`, `codex`, or `copilot`
- `source``claude`, `codex`, or `auto` (where to read transcripts)
- `target_skill_path` — explicit SKILL.md to evolve; use this for a skill that
the current agent actually loads
- `tasks_file` — reviewed TaskRecord JSON (skip harvest); real backends require
its metadata to contain `"reviewed": true`
- `max_tasks` / `max_sessions` — cap workload
- `auto_adopt` — auto-adopt if the gate passes
- `json` — machine-readable output for programmatic use
### Advanced config (`~/.skillopt-sleep/config.json`)
- `preferences` — free-text house rules for the optimizer
- `gate_mode``on` (default) or `off`; `dream_rollouts` — >1 for more signal
- `evolve_memory` / `evolve_skill` — toggle which docs consolidate
Always show the user the held-out baseline → candidate score and the proposed
edits before suggesting `sleep_adopt`. Never hand-edit the user's memory/skill
files; only `sleep_adopt` does that, with a backup.
files; use `sleep_adopt` (or an explicitly requested `auto_adopt`) so the engine
applies its staging manifest and backup behavior.
Harvesting is local and read-only, and `backend: "mock"` makes no provider
calls. A real backend sends truncated transcript excerpts and derived tasks to
the selected provider; outbound prompts are not guaranteed to be secret-free.
Review sensitive data and provider policy before selecting a real backend.
`sleep_schedule` persists only the project, backend, time, and optional
auto-adopt setting. Put a non-default transcript source or target skill in
`~/.skillopt-sleep/config.json` before scheduling it.
+63 -11
View File
@@ -38,16 +38,48 @@ TOOLS = [
"description": "Apply the latest staged proposal to CLAUDE.md/SKILL.md (backs up first)."},
{"name": "sleep_harvest", "action": "harvest",
"description": "Debug: list the recurring tasks mined from recent sessions."},
{"name": "sleep_schedule", "action": "schedule",
"description": "Install a nightly cron entry to run the sleep cycle automatically."},
{"name": "sleep_unschedule", "action": "unschedule",
"description": "Remove the nightly cron entry for a project."},
]
_BY_NAME = {t["name"]: t for t in TOOLS}
_TOOL_SCHEMA = {
"type": "object",
"properties": {
"project": {"type": "string", "description": "Project dir to evolve (default: cwd)."},
"backend": {"type": "string", "enum": ["mock", "claude", "codex"],
"description": "mock = no API spend (default); claude/codex = real."},
"scope": {"type": "string", "enum": ["invoked", "all"]},
"project": {"type": "string",
"description": "Project dir to evolve (default: cwd)."},
"backend": {"type": "string", "enum": ["mock", "claude", "codex", "copilot"],
"description": "mock = no API spend (default); claude/codex/copilot = real."},
"scope": {"type": "string", "enum": ["invoked", "all"],
"description": "Harvest scope (default: invoked project only)."},
"source": {"type": "string", "enum": ["claude", "codex", "auto"],
"description": "Transcript source (default: claude)."},
"model": {"type": "string",
"description": "Backend-specific model override."},
"tasks_file": {"type": "string",
"description": "Path to reviewed TaskRecord JSON (skips harvest)."},
"target_skill_path": {"type": "string",
"description": "Explicit SKILL.md path to evolve/stage/adopt."},
"progress": {"type": "boolean",
"description": "Print phase progress to stderr."},
"max_sessions": {"type": "integer",
"description": "Cap harvested sessions per run."},
"max_tasks": {"type": "integer",
"description": "Cap mined tasks per run."},
"lookback_hours": {"type": "integer",
"description": "Harvest window in hours (default: 72)."},
"auto_adopt": {"type": "boolean",
"description": "Auto-adopt if gate passes (default: false)."},
"json": {"type": "boolean",
"description": "Return machine-readable JSON output."},
"edit_budget": {"type": "integer",
"description": "Max bounded edits per night (default: 4)."},
"hour": {"type": "integer",
"description": "Hour for schedule (0-23, default: 3)."},
"minute": {"type": "integer",
"description": "Minute for schedule (0-59, default: 17)."},
},
"additionalProperties": False,
}
@@ -56,15 +88,35 @@ _TOOL_SCHEMA = {
def _run_engine(action: str, args: dict) -> str:
py = sys.executable or "python3"
cmd = [py, "-m", "skillopt_sleep", action]
if args.get("project"):
cmd += ["--project", str(args["project"])]
if args.get("backend"):
cmd += ["--backend", str(args["backend"])]
if args.get("scope"):
cmd += ["--scope", str(args["scope"])]
# String-valued flags
for flag, key in [
("--project", "project"), ("--backend", "backend"),
("--scope", "scope"), ("--source", "source"),
("--model", "model"), ("--tasks-file", "tasks_file"),
("--target-skill-path", "target_skill_path"),
]:
val = args.get(key)
if val:
cmd += [flag, str(val)]
# Integer-valued flags
for flag, key in [
("--max-sessions", "max_sessions"), ("--max-tasks", "max_tasks"),
("--lookback-hours", "lookback_hours"), ("--edit-budget", "edit_budget"),
("--hour", "hour"), ("--minute", "minute"),
]:
val = args.get(key)
if val is not None:
cmd += [flag, str(int(val))]
# Boolean flags
for flag, key in [
("--progress", "progress"), ("--auto-adopt", "auto_adopt"),
("--json", "json"),
]:
if args.get(key):
cmd.append(flag)
try:
proc = subprocess.run(cmd, cwd=REPO_ROOT, capture_output=True, text=True, timeout=3600)
except Exception as e: # noqa: BLE001
except Exception as e:
return f"[error] failed to run engine: {e}"
out = (proc.stdout or "").strip()
err = (proc.stderr or "").strip()
+107
View File
@@ -0,0 +1,107 @@
# SkillOpt — GitHub Copilot integration
Give **Copilot** (CLI or VS Code) direct access to the **SkillOpt** research
engine via a tiny **MCP server**. MCP is GitHub's supported way to extend
Copilot, so this works across Copilot CLI, VS Code, and other MCP clients with
the same server.
SkillOpt is **validation-gated, text-space skill optimization**: it reflects on
rollouts, makes bounded edits to a skill, and keeps a change only if it improves
a held-out validation set. This plugin exposes the repo's training and eval
entry points (`scripts/train.py`, `scripts/eval_only.py`) as Copilot tools.
> This is the companion to the **SkillOpt-Sleep** plugin (`../mcp_server.py`,
> `sleep_*` tools). Sleep evolves a *local coding agent* from your past
> sessions; this server drives the *research* training/eval loops on the
> benchmark configs in [`../../../configs`](../../../configs).
## What's here
| File | Purpose |
|---|---|
| `mcp_server.py` | stdlib-only MCP (stdio) server exposing `skillopt_*` tools |
| `mcp-config.example.json` | drop-in MCP server config |
| `copilot-instructions.snippet.md` | paste into `.github/copilot-instructions.md` |
## Install
Requires Python ≥ 3.10. The MCP server itself is pure stdlib, but the tools it
launches need SkillOpt's runtime deps — install the package first:
```bash
pip install -e . # or: pip install -r requirements.txt
```
1. **Register the MCP server.** Add the server to your Copilot MCP config
(Copilot CLI: `~/.copilot/mcp-config.json`; VS Code: your MCP settings).
Use `mcp-config.example.json` as a template — set `SKILLOPT_REPO` to this
repo's path:
```json
{
"mcpServers": {
"skillopt": {
"command": "python3",
"args": ["/abs/path/SkillOpt/plugins/copilot/skillopt/mcp_server.py"],
"env": { "SKILLOPT_REPO": "/abs/path/SkillOpt" }
}
}
}
```
2. **(Optional) Tell Copilot about it.** Append
`copilot-instructions.snippet.md` to your repo's
`.github/copilot-instructions.md` so Copilot reaches for the tools when the
user asks to "optimize a skill" or "train on a benchmark".
## Use
Ask Copilot things like *"what configs can I run?"*, *"optimize the searchqa
skill"*, or *"evaluate this skill on the dataset"*. Copilot calls the MCP tools:
`skillopt_list_configs`, `skillopt_train`, `skillopt_eval`.
| Tool | Required args | Notes |
|---|---|---|
| `skillopt_list_configs` | — | Lists `configs/**/*.yaml` you can pass as `config`. |
| `skillopt_train` | `config` | Runs a reflective optimization loop. Long-running; spends budget. |
| `skillopt_eval` | `config`, `skill` | Evaluates one skill markdown file; no training. |
Common optional args (both train and eval): `env`, `backend`,
`optimizer_model`, `target_model`, `out_root`, `cfg_options` (space-separated
`section.key=value` YAML overrides), and `extra_args` (raw passthrough flags
for the underlying script). `skillopt_train` also accepts `num_epochs`,
`batch_size`, `seed`, and `use_gate`. `use_gate` defaults to `true`; setting it
to `false` still records validation scores but force-accepts every candidate,
which changes the optimization semantics.
The MCP schema's `backend` argument follows the underlying script's
`--backend` choices. Role-specific and generic OpenAI-compatible backends can
be selected through config or `extra_args` (for example,
`--optimizer_backend openai_compatible --target_backend openai_compatible`);
see the repository's [backend guide](../../../docs/guide/new-backend.md) for
the required environment variables.
Runs can be very long. The server's subprocess timeout defaults to 6 hours;
override it with the `SKILLOPT_RUN_TIMEOUT` environment variable (seconds).
## Verify the server directly (no Copilot needed)
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"skillopt_list_configs","arguments":{}}}' \
| SKILLOPT_REPO="$(pwd)" python3 plugins/copilot/skillopt/mcp_server.py
```
You should see the server info, the three `skillopt_*` tools, and the list of
benchmark configs.
## Notes / status
- MCP is the stable, official Copilot extension surface, so this is portable
across Copilot CLI and IDE from one server.
- `skillopt_list_configs` is filesystem-only and safe to call anytime;
`skillopt_train` / `skillopt_eval` shell out to the repo scripts and require
the SkillOpt runtime deps (and, for real backends, model credentials — see
[`../../../.env.example`](../../../.env.example)).
@@ -0,0 +1,34 @@
<!--
Copy this block into your repo's .github/copilot-instructions.md so Copilot
knows the SkillOpt research-engine tools exist. (Copilot reads
copilot-instructions.md automatically as ambient guidance.)
-->
## SkillOpt (research skill-optimization engine)
This repo exposes the core **SkillOpt** training/eval engine via an MCP server
(`skillopt`). SkillOpt is validation-gated, text-space skill optimization: it
reflects on rollouts, makes bounded edits to a skill, and keeps a change only
if it improves a held-out validation set.
When the user asks to "optimize a skill", "train on <benchmark>", "run
SkillOpt", "evaluate this skill", or "what configs can I run", use the MCP
tools:
- `skillopt_list_configs` — list the benchmark YAML configs you can pass as `config`
- `skillopt_train` — run a reflective skill-optimization loop on a config (long-running; spends API/compute budget)
- `skillopt_eval` — evaluate a single skill markdown file on a dataset (no training)
Guidance:
- Always run `skillopt_list_configs` first if you don't already know a valid `config` path.
- `skillopt_train` and `skillopt_eval` are long-running and consume the user's
model backend/budget — confirm the `config`, `backend`, and model choices
with the user before launching. Surface the held-out gate result for training,
or the evaluation score and output directory for eval-only runs.
- For one-off YAML overrides use dotted `cfg_options` for structured configs
(e.g. `train.seed=123 train.batch_size=40`);
for any other underlying flag use `extra_args`.
This is distinct from the **SkillOpt-Sleep** MCP server (`skillopt-sleep`,
`sleep_*` tools), which evolves a local coding agent from past sessions rather
than running the research benchmarks.
@@ -0,0 +1,11 @@
{
"mcpServers": {
"skillopt": {
"command": "python3",
"args": ["plugins/copilot/skillopt/mcp_server.py"],
"env": {
"SKILLOPT_REPO": "${workspaceFolder}"
}
}
}
}
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""SkillOpt (research engine) — minimal MCP server (stdio, stdlib-only).
Exposes the core SkillOpt skill-optimization engine as MCP tools so any
MCP-capable client (GitHub Copilot CLI / VS Code, Claude Desktop, etc.) can
drive it. No third-party deps: speaks JSON-RPC 2.0 over stdio with just the
handful of MCP methods clients need.
This is the companion to the SkillOpt-Sleep MCP server (``../mcp_server.py``).
Where Sleep evolves a *local agent* from past sessions, this server drives the
*research* training/eval loops from this repo (``scripts/train.py`` /
``scripts/eval_only.py``) against the benchmark configs in ``configs/``.
Tools exposed:
- skillopt_list_configs : discover the benchmark YAML configs you can use
- skillopt_train : run a reflective skill-optimization (training) loop
- skillopt_eval : evaluate a single skill on a dataset (no training)
``skillopt_train`` and ``skillopt_eval`` shell out to the repo's entry-point
scripts and stream back their stdout/stderr. Configure your client to launch:
python plugins/copilot/skillopt/mcp_server.py
"""
from __future__ import annotations
import glob
import json
import os
import subprocess
import sys
# Repo root: three levels up from plugins/copilot/skillopt/mcp_server.py
REPO_ROOT = os.environ.get("SKILLOPT_REPO") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..")
)
PROTOCOL_VERSION = "2024-11-05"
# Training/eval runs are long; give the engine plenty of headroom.
RUN_TIMEOUT_SECONDS = int(os.environ.get("SKILLOPT_RUN_TIMEOUT", "21600")) # 6h
def _list_configs() -> str:
"""List the benchmark configs available under configs/ (filesystem only)."""
pattern = os.path.join(REPO_ROOT, "configs", "**", "*.yaml")
paths = sorted(glob.glob(pattern, recursive=True))
if not paths:
return f"[no configs found under {os.path.join(REPO_ROOT, 'configs')}]"
rels = [os.path.relpath(p, REPO_ROOT).replace(os.sep, "/") for p in paths]
lines = ["Available SkillOpt configs (pass as `config`):", ""]
lines += [f" - {r}" for r in rels]
return "\n".join(lines)
def _run_script(script_rel: str, args: dict, *, required: tuple[str, ...] = ()) -> str:
"""Shell out to a repo entry-point script, mapping args -> --flags."""
for key in required:
if not args.get(key):
return f"[error] missing required argument: {key}"
py = sys.executable or "python3"
cmd = [py, os.path.join("scripts", script_rel)]
# Ordered flags that the train/eval scripts accept directly.
flag_args = (
"config", "skill", "split", "env", "backend",
"optimizer_model", "target_model", "out_root",
"num_epochs", "batch_size", "seed", "use_gate",
)
for key in flag_args:
val = args.get(key)
if val is None or val == "":
continue
cmd += [f"--{key}", str(val)]
# cfg-options: arbitrary KEY=VALUE YAML overrides (nargs="+").
cfg_options = args.get("cfg_options")
if cfg_options:
if isinstance(cfg_options, str):
cfg_options = cfg_options.split()
cmd += ["--cfg-options", *[str(x) for x in cfg_options]]
# extra_args: raw passthrough for any other train/eval flag.
extra = args.get("extra_args")
if extra:
if isinstance(extra, str):
extra = extra.split()
cmd += [str(x) for x in extra]
try:
proc = subprocess.run(
cmd, cwd=REPO_ROOT, capture_output=True, text=True,
timeout=RUN_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
return f"[error] run exceeded {RUN_TIMEOUT_SECONDS}s timeout: {' '.join(cmd)}"
except Exception as e: # noqa: BLE001
return f"[error] failed to run script: {e}"
out = (proc.stdout or "").strip()
err = (proc.stderr or "").strip()
body = out + (("\n[stderr]\n" + err) if err else "")
return body or f"[done] exit code {proc.returncode}, no output"
TOOLS = [
{
"name": "skillopt_list_configs",
"description": "List the benchmark YAML configs under configs/ that can be passed as `config` to train/eval.",
},
{
"name": "skillopt_train",
"description": "Run a SkillOpt reflective skill-optimization (training) loop on a benchmark config. Long-running; uses your model backend/budget.",
},
{
"name": "skillopt_eval",
"description": "Evaluate a single skill markdown file on a dataset without training (scripts/eval_only.py).",
},
]
_BY_NAME = {t["name"]: t for t in TOOLS}
_NO_ARGS_SCHEMA = {"type": "object", "properties": {}, "additionalProperties": False}
_COMMON_PROPS = {
"config": {"type": "string",
"description": "Path to a benchmark YAML config (e.g. configs/searchqa/default.yaml). See skillopt_list_configs."},
"env": {"type": "string", "description": "Override the environment/adapter name (e.g. searchqa, alfworld)."},
"backend": {"type": "string", "description": "Model backend (e.g. azure_openai, claude, codex, qwen, minimax)."},
"optimizer_model": {"type": "string", "description": "Model used for reflection/skill rewriting (the optimizer)."},
"target_model": {"type": "string", "description": "Model used to execute tasks (the target)."},
"out_root": {"type": "string", "description": "Output directory root for run artifacts."},
"cfg_options": {"type": "string", "description": "Space-separated YAML overrides, e.g. 'seed=123 batch_size=40'."},
"extra_args": {"type": "string", "description": "Raw passthrough flags for the underlying script, e.g. '--workers 8 --max_turns 30'."},
}
_TRAIN_SCHEMA = {
"type": "object",
"properties": {
**_COMMON_PROPS,
"num_epochs": {"type": "integer", "description": "Number of optimization epochs."},
"batch_size": {"type": "integer", "description": "Tasks per optimization step."},
"seed": {"type": "integer", "description": "Random seed."},
"use_gate": {"type": "string", "enum": ["true", "false"],
"description": "Whether to keep the held-out validation gate on (default on)."},
},
"required": ["config"],
"additionalProperties": False,
}
_EVAL_SCHEMA = {
"type": "object",
"properties": {
**_COMMON_PROPS,
"skill": {"type": "string", "description": "Path to the skill markdown file to evaluate."},
"split": {"type": "string", "description": "Dataset split to evaluate (default: all)."},
},
"required": ["config", "skill"],
"additionalProperties": False,
}
_SCHEMA_BY_NAME = {
"skillopt_list_configs": _NO_ARGS_SCHEMA,
"skillopt_train": _TRAIN_SCHEMA,
"skillopt_eval": _EVAL_SCHEMA,
}
def _result(id_, result):
return {"jsonrpc": "2.0", "id": id_, "result": result}
def _error(id_, code, message):
return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": message}}
def _dispatch(name: str, args: dict) -> str:
if name == "skillopt_list_configs":
return _list_configs()
if name == "skillopt_train":
return _run_script("train.py", args, required=("config",))
if name == "skillopt_eval":
return _run_script("eval_only.py", args, required=("config", "skill"))
return f"[error] unknown tool: {name}"
def handle(req: dict):
method = req.get("method")
id_ = req.get("id")
if method == "initialize":
return _result(id_, {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {"tools": {}},
"serverInfo": {"name": "skillopt", "version": "0.1.0"},
})
if method in ("notifications/initialized", "initialized"):
return None # notification, no response
if method == "tools/list":
return _result(id_, {"tools": [
{"name": t["name"], "description": t["description"],
"inputSchema": _SCHEMA_BY_NAME[t["name"]]}
for t in TOOLS
]})
if method == "tools/call":
params = req.get("params") or {}
name = params.get("name")
if name not in _BY_NAME:
return _error(id_, -32602, f"unknown tool: {name}")
text = _dispatch(name, params.get("arguments") or {})
return _result(id_, {"content": [{"type": "text", "text": text}]})
if method == "ping":
return _result(id_, {})
return _error(id_, -32601, f"method not found: {method}")
def main() -> int:
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except Exception:
continue
resp = handle(req)
if resp is not None:
sys.stdout.write(json.dumps(resp) + "\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+81
View File
@@ -0,0 +1,81 @@
# SkillOpt-Sleep — Devin integration
Give **Devin** (Cognition) a nightly **sleep cycle** via a tiny **MCP server**
that exposes the `skillopt_sleep` engine as tools. MCP is Devin's supported way
to add custom tooling, so this works in Devin's CLI and IDE.
Devin doesn't write transcripts in the format the engine consumes, so this
plugin adds a **Devin-specific harvester** that converts every locally available
source into the Claude Code-compatible JSONL the engine reads.
## What's here
| File | Purpose |
|---|---|
| `mcp_server.py` | stdlib-only MCP (stdio) server exposing `sleep_*` tools |
| `harvest_devin.py` | converts Devin ATIF-v1.7 transcripts + agentmemory + `.devin/skills` into JSONL, with `taskKey` + outcome envelopes |
| `judge.py` | reference judge for the deferred/judge branch of the validation gate |
| `mcp-config.example.json` | drop-in MCP server config |
| `devin-rules.snippet.md` | paste into `.devin/rules/skillopt-sleep.md` |
## What it harvests
| Source | Where |
|---|---|
| Devin transcripts (ATIF-v1.7) | `~/.local/share/devin/cli/transcripts/*.json` |
| agentmemory | `~/.agentmemory/standalone.json` |
| Skill files | `.devin/skills/*/SKILL.md` |
Workspaces are auto-detected from `~/.config/Devin/User/workspaceStorage/*/workspace.json`.
After `sleep_adopt`, the evolved skill is synced to `.devin/skills/skillopt-sleep-learned/SKILL.md`.
## Install
Requires Python ≥ 3.10. No third-party packages — the server is pure stdlib.
1. **Register the MCP server.** Use `mcp-config.example.json` as a template; set
`args` to the absolute path of this `mcp_server.py`. The engine is found
automatically (this plugin lives inside the SkillOpt repo). Or via the Devin
CLI:
```bash
devin mcp add skillopt-sleep \
--env "SKILLOPT_DEVIN_CLAUDE_HOME=$HOME/.skillopt-sleep-devin" \
-- python3 /abs/path/to/SkillOpt/plugins/devin/mcp_server.py
```
2. **(Optional)** copy `devin-rules.snippet.md` to `.devin/rules/skillopt-sleep.md`
so Devin proactively offers the tools.
3. Ask Devin: *"run the sleep cycle"*, *"what did the last sleep propose?"*, *"adopt it"*.
## Tools
| Tool | What it does |
|---|---|
| `sleep_status` | nights run so far + latest staged proposal |
| `sleep_dry_run` | preview cycle — no staging; a real backend still makes provider calls |
| `sleep_run` | full cycle; stages a proposal for review |
| `sleep_adopt` | apply the staged proposal; syncs skill to the workspace |
| `sleep_harvest` | debug: list the recurring tasks mined |
| `sleep_schedule` | install a nightly cron entry (`--hour` / `--minute`) |
| `sleep_unschedule` | remove the nightly cron entry |
Default backend is `mock` (no API spend); the `claude`, `codex`, and `copilot`
backends use the corresponding authenticated CLI and budget. The `handoff`
backend runs the cycle with no model subprocess or API key — the engine writes
pending model calls to `.skillopt-sleep-handoff/PROMPTS.md` + `pending.json`
(exit code 3) and resumes after answers are placed in `answers/<id>.md`; re-run
`sleep_run` with the same arguments to resume. The seven tools call the same
`python -m skillopt_sleep` actions as the other shared-engine integrations.
## Data boundary
The Devin harvester reads local ATIF transcripts, agentmemory, and skill files
and converts them into the engine's session format. The `mock` backend keeps
that workflow local. A real backend sends truncated excerpts and derived tasks
to the selected provider for mining, replay, judging, and reflection. The
conversion step is not a guarantee that outbound prompts contain no secrets;
review sensitive sources and provider policy before enabling a real backend.
See the [shared data-boundary guidance](../README.md#data-boundary) and
[implemented CLI reference](../README.md#supported-cli-surface).
+41
View File
@@ -0,0 +1,41 @@
# SkillOpt-Sleep (Devin)
You have access to a nightly self-evolution cycle via the `skillopt-sleep` MCP
server. Use these tools to improve your long-term skills over time:
- **`sleep_status`** — refresh the converted local cache, then show how many
nights have run and the latest staged proposal
- **`sleep_dry_run`** — refresh the converted local cache and preview a cycle
without engine staging/adoption; a real backend still makes provider calls
- **`sleep_run`** — run a full cycle; stages a proposal by default, while an
explicit `auto_adopt` may also update live files
- **`sleep_adopt`** — apply the staged proposal, then sync the managed skill to
`.devin/skills/skillopt-sleep-learned/SKILL.md` when `project` is the Devin
workspace and that workspace already contains a `.devin/` directory
- **`sleep_harvest`** — debug: list the recurring tasks mined from recent sessions
- **`sleep_schedule`** / **`sleep_unschedule`** — low-level shared-engine cron
controls; the current scheduled command does not run Devin's conversion step,
so do not use it as an unattended Devin-harvest workflow
When a user asks about the sleep cycle or skill evolution, prefer calling these
tools over explaining the concept.
Always pass the absolute Devin workspace as `project`, especially for
`sleep_adopt`. Default backend is `mock` (no provider calls). The `claude`,
`codex`, and `copilot` backend values use the corresponding installed and
authenticated CLI; they do not require this plugin to implement a separate
API-key flow. The `handoff` backend runs the cycle with no model subprocess
or API key — the engine writes pending model calls to
`.skillopt-sleep-handoff/` and exits; answer each prompt in a fresh context
and re-run `sleep_run` to resume (typically 36 rounds).
The Devin conversion and mock workflow stay local. A real backend sends
truncated transcript excerpts and derived tasks to the selected provider for
mining, replay, judging, and reflection; conversion is not a guarantee that
outbound prompts contain no secrets. Review local sources and provider policy
before selecting a real backend.
For a reviewed task file, pass `tasks_file`; before using it with a real backend,
inspect/redact it and ensure its metadata contains `"reviewed": true`.
Place this file at `.devin/rules/skillopt-sleep.md` in your workspace.
+21
View File
@@ -0,0 +1,21 @@
{
"schema_version": "ATIF-v1.7",
"session_id": "demo-001",
"steps": [
{
"source": "user",
"message": "Fix the failing NullPointerException in OrderService.persist() in the dutch-kis project",
"timestamp": "2026-06-20T10:00:00Z"
},
{
"source": "agent",
"message": "The repository call returns an Optional that is being unwrapped with .get(). I'll switch to orElseThrow(NotFoundException::new) so the missing-row case is handled.",
"timestamp": "2026-06-20T10:00:05Z"
},
{
"source": "agent",
"message": "Applied the fix and ran the suite: rtk mvn test -Dtest=OrderServiceTest -> BUILD SUCCESS, 142 passed, 0 failed.",
"timestamp": "2026-06-20T10:01:00Z"
}
]
}
+533
View File
@@ -0,0 +1,533 @@
#!/usr/bin/env python3
"""Convert Devin IDE local data into Claude Code-format JSONL transcripts.
Devin (Cognition) does not persist agent conversation transcripts to disk in a
format the sleep engine understands. This script bridges that gap by synthesising
JSONL files from every locally available source:
1. **Devin transcripts** (~/.local/share/devin/cli/transcripts/*.json)
Native ATIF-v1.7 format source:"user" / source:"agent" messages
converted directly to user/assistant JSONL turns.
2. **agentmemory** (~/.agentmemory/standalone.json)
Memories saved by the `agentmemory` MCP server each memory's title
becomes a synthetic user prompt; its content becomes the assistant reply.
3. **Skill files** (.devin/skills/*/SKILL.md)
Each skill description is converted to a session where the user asked
"use the <skill> skill" and the assistant described how to apply it.
Output layout (mirrors ~/.claude/projects/<slug>/<sessionId>.jsonl):
<out_dir>/projects/<slug>/<session_id>.jsonl
Workspace auto-detection order:
1. ``SKILLOPT_DEVIN_WORKSPACES`` env var colon-separated abs paths
2. Devin registry: ``~/.config/Devin/User/workspaceStorage/*/workspace.json``
4. Working directory fallback
Usage (standalone):
python harvest_devin.py [--out-dir PATH] [--workspaces PATH ...]
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import unquote, urlparse
# ── cross-platform path resolution (Linux + Windows + macOS) ──────────────────
#
# Devin is a VS Code-family app, so its user-data dir moves with the OS:
# Linux ~/.config/<App>, Windows %APPDATA%\<App>, macOS
# ~/Library/Application Support/<App>. Resolve all candidates and let callers
# keep whichever actually exists.
def _app_data_roots(app: str) -> List[str]:
"""User-data dir candidates for a VS Code-family app, current OS first."""
home = os.path.expanduser("~")
roots: List[str] = []
if os.name == "nt":
appdata = os.environ.get("APPDATA") or os.path.join(home, "AppData", "Roaming")
roots.append(os.path.join(appdata, app))
elif sys.platform == "darwin":
roots.append(os.path.join(home, "Library", "Application Support", app))
# XDG / Linux (also a sensible fallback everywhere)
xdg = os.environ.get("XDG_CONFIG_HOME") or os.path.join(home, ".config")
roots.append(os.path.join(xdg, app))
# de-dupe, preserve order
return list(dict.fromkeys(roots))
def _devin_transcript_candidates() -> List[str]:
"""Where the Devin CLI may store ATIF transcripts, per OS."""
home = os.path.expanduser("~")
cands: List[str] = []
if os.name == "nt":
for base in (os.environ.get("LOCALAPPDATA"), os.environ.get("APPDATA")):
if base:
cands.append(os.path.join(base, "devin", "cli", "transcripts"))
elif sys.platform == "darwin":
cands.append(os.path.join(home, "Library", "Application Support",
"devin", "cli", "transcripts"))
cands.append(os.path.join(home, ".local", "share", "devin", "cli", "transcripts"))
return list(dict.fromkeys(cands))
def _first_existing(paths: List[str]) -> str:
"""First path that exists, else the first candidate (for nice messaging)."""
for p in paths:
if os.path.exists(p):
return p
return paths[0] if paths else ""
def _uri_to_path(folder: str) -> str:
"""Convert a VS Code ``file://`` workspace URI to a local path, cross-platform.
Linux: file:///home/u/proj -> /home/u/proj
Windows: file:///c%3A/Users/u/p -> c:/Users/u/p
"""
if not folder.startswith("file://"):
return folder
path = unquote(urlparse(folder).path)
# Windows drive paths come through as '/C:/...' — strip the leading slash.
if os.name == "nt" and re.match(r"^/[A-Za-z]:", path):
path = path[1:]
return path
# ── workspace auto-detection ─────────────────────────────────────────────────
def _workspaces_from_registry(storage_root: str) -> List[tuple]:
"""Read VS Code-style workspaceStorage to get (mtime, path) pairs."""
results: List[tuple] = []
if not os.path.isdir(storage_root):
return results
for entry in os.scandir(storage_root):
ws_json = os.path.join(entry.path, "workspace.json")
if not os.path.isfile(ws_json):
continue
try:
with open(ws_json, encoding="utf-8") as f:
data = json.load(f)
folder = _uri_to_path(data.get("folder", ""))
if folder and os.path.isdir(folder):
results.append((os.path.getmtime(ws_json), folder))
except Exception:
continue
return results
def _detect_workspaces() -> List[str]:
"""Return known workspace paths (Devin registry), newest first."""
env_val = os.environ.get("SKILLOPT_DEVIN_WORKSPACES", "")
if env_val:
# os.pathsep so Windows 'C:\a;C:\b' splits correctly (not on the drive colon)
return [p for p in env_val.split(os.pathsep) if p and os.path.isdir(p)]
registries: List[str] = [
os.path.join(r, "User", "workspaceStorage")
for r in _app_data_roots("Devin")
]
seen: set = set()
results: List[tuple] = []
for registry in registries:
for mtime, folder in _workspaces_from_registry(registry):
if folder not in seen:
seen.add(folder)
results.append((mtime, folder))
results.sort(reverse=True)
paths = [p for _, p in results]
return paths if paths else [os.getcwd()]
# ── helpers ───────────────────────────────────────────────────────────────────
def _slug(path: str) -> str:
"""SHA-256 of abs-path, first 16 hex chars — matches Claude Code's scheme."""
return hashlib.sha256(os.path.abspath(path).encode()).hexdigest()[:16]
def _iso(epoch_ms: Optional[float] = None) -> str:
dt = (datetime.fromtimestamp(epoch_ms / 1000.0, tz=timezone.utc)
if epoch_ms is not None else datetime.now(tz=timezone.utc))
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
def _write_session(
out_dir: str, project: str, session_id: str,
user_prompts: List[str], assistant_replies: List[str],
timestamp_base_ms: float,
task_key: Optional[str] = None,
) -> None:
slug = _slug(project)
session_dir = os.path.join(out_dir, "projects", slug)
os.makedirs(session_dir, exist_ok=True)
out_path = os.path.join(session_dir, f"{session_id}.jsonl")
ts = timestamp_base_ms
with open(out_path, "w", encoding="utf-8") as f:
for user_text, asst_text in zip(user_prompts, assistant_replies):
user_rec = {
"type": "user",
"message": {"role": "user", "content": user_text},
"cwd": project,
"timestamp": _iso(ts),
"sessionId": session_id,
"version": "1.0",
}
if task_key:
# grouping key so the miner can collapse repeats into one recurring task
user_rec["taskKey"] = task_key
f.write(json.dumps(user_rec, ensure_ascii=False) + "\n")
# space the reply >=5s after the prompt so a single-turn session
# isn't misclassified as a <3s headless replay and dropped by the
# engine's harvest filter (skillopt_sleep Issue #62).
ts += 5000
f.write(json.dumps({
"type": "assistant",
"message": {"role": "assistant", "content": asst_text},
"timestamp": _iso(ts),
"sessionId": session_id,
"version": "1.0",
}, ensure_ascii=False) + "\n")
ts += 2000
def _append_history(out_dir: str, display: str, project: str, timestamp_ms: float) -> None:
record = {"display": display, "timestamp": timestamp_ms, "project": project}
with open(os.path.join(out_dir, "history.jsonl"), "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def _infer_project(text: str, workspaces: List[str]) -> str:
for ws in workspaces:
if os.path.basename(ws.rstrip("/")).lower() in text.lower():
return ws
return workspaces[0] if workspaces else os.getcwd()
# ── task identity + outcome extraction (fuel for the validation gate) ─────────
#
# SkillOpt's gate only works "where tasks recur and have a checkable correctness
# signal." These helpers add the two things a raw transcript lacks:
# * a stable taskKey so repeats collapse into one recurring task, and
# * an outcome envelope (success + verifier + re-runnable reference) so the
# held-out replay has something to score against.
_LANG_HINTS = [
("java", r"(java|spring|maven|\bmvn\b|gradle|\.java\b|lombok)"),
("python", r"(python|pytest|\bpip\b|\.py\b|django|flask)"),
("ts", r"(typescript|\.tsx?\b|\bnpm\b|jest|node)"),
("js", r"(javascript|\.jsx?\b)"),
("sql", r"(\bsql\b|select\s|mariadb|mysql|postgres|\.sql\b)"),
("go", r"(golang|\bgo test\b|\.go\b)"),
("rust", r"(rust|cargo|\.rs\b)"),
]
_INTENT_HINTS = [
("fix", r"(fix|bug|error|fail|npe|exception|broken|crash)"),
("implement", r"(implement|add|create|build|introduce|support)"),
("refactor", r"(refactor|clean ?up|rename|extract|simplify)"),
("test", r"(test|coverage|assert)"),
("review", r"(review|audit|inspect)"),
("optimize", r"(optimi[sz]e|perf|speed up|slow)"),
("explain", r"(explain|understand|what does|how does)"),
]
_STOPWORDS = {"please", "this", "that", "with", "from", "into", "should",
"would", "code", "using", "the", "have"}
def _normalize_task_key(text: str, project: str) -> str:
"""Stable '<lang>:<intent>:<target>' grouping key for a task."""
low = text.lower()
lang = next((n for n, pat in _LANG_HINTS if re.search(pat, low)), "general")
intent = next((n for n, pat in _INTENT_HINTS if re.search(pat, low)), "task")
# target: prefer a CamelCase identifier, then a filename, then first real word
m = re.search(r"\b([A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+)\b", text) # CamelCase
if not m:
m = re.search(r"\b([\w-]+\.\w+)\b", text) # filename.ext
if m:
target = m.group(1)
else:
# first content word that isn't a stopword or an intent verb (e.g. "implement")
target = next((w for w in re.findall(r"[a-zA-Z]{4,}", low)
if w not in _STOPWORDS
and not any(re.search(pat, w) for _, pat in _INTENT_HINTS)),
"general")
target = re.sub(r"[^a-zA-Z0-9]+", "-", target).strip("-").lower()[:40] or "general"
return f"{lang}:{intent}:{target}"
_PASS_PAT = re.compile(
r"(build success|all tests? pass(?:ed)?|\b\d+ passed\b|\b0 failed\b|"
r"tests? pass(?:ed)?|✓|no errors)", re.IGNORECASE)
_FAIL_PAT = re.compile(
r"(build failure|tests? failed|\b[1-9]\d* failed\b|error:|traceback|"
r"assertion ?error)", re.IGNORECASE) # note: "0 failed" must NOT match
_CMD_PAT = re.compile(
r"((?:rtk\s+)?(?:mvn|gradle|pytest|npm(?:\s+run)?\s+test|yarn\s+test|"
r"go\s+test|cargo\s+test)[^\n`]*)", re.IGNORECASE)
def _detect_outcome(messages: List[str]) -> Optional[Dict[str, Any]]:
"""Best-effort checkable signal from agent messages. None ⇒ no hard signal."""
blob = "\n".join(m for m in messages if m)
pass_hit, fail_hit = _PASS_PAT.search(blob), _FAIL_PAT.search(blob)
if not pass_hit and not fail_hit:
return None
verifier = "tests" if re.search(r"test|pytest", blob, re.IGNORECASE) else "build"
out: Dict[str, Any] = {
"success": bool(pass_hit) and not fail_hit,
"verifier": verifier,
"evidence": (pass_hit or fail_hit).group(0).strip(),
}
cmd = _CMD_PAT.search(blob)
if cmd:
# keep only the command itself, dropping any "-> result" / ": output" tail
repro = re.split(r"\s*(?:->|→|:|,)\s*", cmd.group(1))[0].strip()
out["reference"] = {"repro": repro}
return out
def _build_rubric(user_prompt: str) -> List[str]:
"""Derive checkable criteria from the task so a judge has something to score."""
crit: List[str] = []
ids = re.findall(r"\b([A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+|[\w-]+\.\w+)\b", user_prompt)
for i in dict.fromkeys(ids): # dedupe, preserve order
crit.append(f"Addresses {i}")
intent = _normalize_task_key(user_prompt, "").split(":")[1]
crit.append({
"fix": "Resolves the reported defect without introducing new errors",
"implement": "Implements the requested behavior end to end",
"refactor": "Preserves behavior while improving structure",
"test": "Adds or fixes tests that actually exercise the change",
"optimize": "Improves performance without changing results",
}.get(intent, "Satisfies the user's stated request"))
crit.append("Response is concrete and actionable, not a restatement of the task")
return crit[:5]
def _judge_rubric_fallback(user_prompt: str) -> Dict[str, Any]:
"""When no hard signal exists, attach a rubric and mark the task for judge
scoring. success=None tells the gate to defer/judge rather than trust it.
The actual scoring is done by judge.py (or the engine) at replay time."""
return {
"success": None,
"verifier": "judge",
"rubric": _build_rubric(user_prompt or ""),
}
def _write_outcome(out_dir: str, session_id: str, task_key: str, project: str,
ts_ms: float, outcome: Dict[str, Any]) -> None:
rec = {"type": "outcome", "sessionId": session_id, "taskKey": task_key,
"project": project, "timestamp": _iso(ts_ms), **outcome}
with open(os.path.join(out_dir, "outcomes.jsonl"), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
# ── source 1: Devin ATIF-v1.7 transcripts ────────────────────────────────────
def harvest_devin_transcripts(
transcripts_dir: str, out_dir: str, workspaces: List[str]
) -> int:
"""Convert Devin CLI ATIF-v1.7 transcripts to Claude Code JSONL."""
if not os.path.isdir(transcripts_dir):
return 0
written = 0
for entry in os.scandir(transcripts_dir):
if not entry.name.endswith(".json"):
continue
try:
with open(entry.path, encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
if data.get("schema_version", "").startswith("ATIF"):
pass # Devin native format
else:
continue
session_id = data.get("session_id") or entry.name[:-5]
steps = data.get("steps") or []
user_prompts: List[str] = []
agent_replies: List[str] = []
project = ""
ts_base: Optional[float] = None
for step in steps:
src = step.get("source", "")
msg = str(step.get("message") or "").strip()
if not msg or src == "system":
continue
if src == "user":
user_prompts.append(msg)
if not project:
project = _infer_project(msg, workspaces)
elif src == "agent":
agent_replies.append(msg)
if ts_base is None:
raw_ts = step.get("timestamp", "")
if raw_ts:
try:
from datetime import datetime as _dt
ts_base = _dt.fromisoformat(
raw_ts.replace("Z", "+00:00")
).timestamp() * 1000
except Exception:
pass
if not user_prompts:
continue
if not project:
project = workspaces[0] if workspaces else os.getcwd()
if ts_base is None:
ts_base = datetime.now(tz=timezone.utc).timestamp() * 1000
# Identity + outcome: what makes this trajectory replayable & gradeable.
task_key = _normalize_task_key(user_prompts[0], project)
outcome = _detect_outcome(agent_replies) or _judge_rubric_fallback(user_prompts[0])
# Pair turns; pad shorter list
n = max(len(user_prompts), len(agent_replies))
user_prompts += [""] * (n - len(user_prompts))
agent_replies += [""] * (n - len(agent_replies))
sid = f"devin_{session_id}"
_write_session(
out_dir, project, sid,
user_prompts=[p for p in user_prompts if p],
assistant_replies=[r if r else "[no reply recorded]" for r, p in
zip(agent_replies, user_prompts) if p],
timestamp_base_ms=ts_base,
task_key=task_key,
)
_write_outcome(out_dir, sid, task_key, project, ts_base, outcome)
_append_history(
out_dir,
display=(user_prompts[0] or session_id)[:120],
project=project,
timestamp_ms=ts_base,
)
written += 1
return written
# ── source 2: agentmemory ─────────────────────────────────────────────────────
def harvest_agentmemory(agentmemory_path: str, out_dir: str,
workspaces: List[str]) -> int:
if not os.path.isfile(agentmemory_path):
return 0
with open(agentmemory_path, encoding="utf-8") as f:
data = json.load(f)
memories: Dict[str, Any] = data.get("mem:memories", {})
written = 0
base_ts = datetime.now(tz=timezone.utc).timestamp() * 1000 - len(memories) * 60_000
for i, (mem_id, mem) in enumerate(memories.items()):
title = str(mem.get("title", "")).strip()
content = str(mem.get("content", "")).strip()
if not title or not content:
continue
project = _infer_project(title + " " + content, workspaces)
ts = base_ts + i * 60_000
_write_session(out_dir, project, mem_id,
user_prompts=[title],
assistant_replies=[content],
timestamp_base_ms=ts)
_append_history(out_dir, display=title[:120], project=project, timestamp_ms=ts)
written += 1
return written
# ── source 3: skill files (.devin/skills) ─────────────────────────────────────
def harvest_skills(workspaces: List[str], out_dir: str) -> int:
written = 0
seen_ids: set = set()
for ws in workspaces:
skills_root = os.path.join(ws, ".devin", "skills")
if not os.path.isdir(skills_root):
continue
for skill_dir in os.scandir(skills_root):
if not skill_dir.is_dir():
continue
skill_md = os.path.join(skill_dir.path, "SKILL.md")
if not os.path.isfile(skill_md):
continue
sid = f"skill_{skill_dir.name}"
if sid in seen_ids:
continue
seen_ids.add(sid)
with open(skill_md, encoding="utf-8") as f:
raw = f.read()
body = re.sub(r"^---.*?---\s*", "", raw, flags=re.DOTALL).strip()
if not body:
continue
first_line = body.split("\n")[0].lstrip("# ").strip()
user_ask = f"Please use the {skill_dir.name} skill: {first_line}"
ts = datetime.now(tz=timezone.utc).timestamp() * 1000 - 3_600_000
_write_session(out_dir, ws, sid,
user_prompts=[user_ask],
assistant_replies=[body[:1200]],
timestamp_base_ms=ts)
_append_history(out_dir, display=user_ask[:120], project=ws, timestamp_ms=ts)
written += 1
return written
# ── main ─────────────────────────────────────────────────────────────────────
def main(argv=None) -> int:
parser = argparse.ArgumentParser(
description="Generate SkillOpt-Sleep transcripts from Devin local data"
)
parser.add_argument(
"--out-dir",
default=os.path.expanduser("~/.skillopt-sleep-devin"),
help="Output claude_home dir (default: ~/.skillopt-sleep-devin)",
)
parser.add_argument(
"--agentmemory",
default=os.path.expanduser("~/.agentmemory/standalone.json"),
help="Path to agentmemory standalone.json",
)
parser.add_argument(
"--devin-transcripts",
default=_first_existing(_devin_transcript_candidates()),
help="Devin CLI ATIF transcripts directory (default: per-OS auto-detect)",
)
parser.add_argument(
"--workspaces", nargs="*",
help="Workspace paths (default: auto-detect from Devin registry)",
)
parser.add_argument("--quiet", action="store_true")
args = parser.parse_args(argv)
out_dir = os.path.expanduser(args.out_dir)
os.makedirs(out_dir, exist_ok=True)
os.makedirs(os.path.join(out_dir, "projects"), exist_ok=True)
workspaces = args.workspaces or _detect_workspaces()
workspaces = [ws for ws in workspaces if os.path.isdir(ws)]
if not workspaces:
workspaces = [os.getcwd()]
total = 0
devin_transcripts = os.path.expanduser(args.devin_transcripts)
n = harvest_devin_transcripts(devin_transcripts, out_dir, workspaces)
if not args.quiet:
print(f"[harvest_devin] devin : {n} sessions")
total += n
n = harvest_agentmemory(args.agentmemory, out_dir, workspaces)
if not args.quiet:
print(f"[harvest_devin] agentmemory : {n} sessions")
total += n
n = harvest_skills(workspaces, out_dir)
if not args.quiet:
print(f"[harvest_devin] skill files : {n} sessions")
total += n
if not args.quiet:
print(f"[harvest_devin] total : {total} synthetic sessions → {out_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Reference judge for SkillOpt-Sleep — score a candidate reply against a rubric.
Tasks harvested without a hard test/build signal get ``verifier: "judge"`` and a
``rubric`` (see ``_build_rubric`` in harvest_devin.py). This module is the
scorer the validation gate calls for those tasks: given the rubric and a
candidate reply produced during replay, it returns a score in ``[0, 1]``. The
gate accepts a skill edit only if the *new* skill scores strictly higher on the
held-out tasks.
It is self-contained on purpose in a full deployment the SkillOpt engine owns
replay+scoring, but having a runnable reference here lets you sanity-check the
judge path without the engine.
Backends (select via ``SKILLOPT_JUDGE``):
* ``heuristic`` (default) keyword-coverage, offline, no API key, deterministic.
* ``claude`` LLM judge via the Anthropic API (needs ANTHROPIC_API_KEY).
Usage:
python judge.py --rubric rubric.json --reply reply.txt
echo "<reply>" | python judge.py --rubric-inline '["Addresses OrderService", ...]'
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from typing import List
_STOPWORDS = {"addresses", "resolves", "implements", "without", "introducing",
"behavior", "request", "response", "concrete", "actionable", "not",
"the", "and", "that", "with", "stated", "reported", "actually",
"preserves", "improving", "structure", "requested", "satisfies"}
# Cheap, fast model is the right default for a judge.
_JUDGE_MODEL = os.environ.get("SKILLOPT_JUDGE_MODEL", "claude-haiku-4-5-20251001")
def _content_words(text: str) -> List[str]:
return [w for w in re.findall(r"[A-Za-z][A-Za-z0-9_.\-]{3,}", text.lower())
if w not in _STOPWORDS]
def heuristic_score(reply: str, rubric: List[str]) -> float:
"""Fraction of rubric criteria whose key content words appear in the reply.
Crude but deterministic: each criterion is 'met' if at least one of its
content words shows up in the candidate reply. Good enough to smoke-test the
gate wiring; swap in the claude backend for real judging.
"""
if not rubric:
return 0.0
low = reply.lower()
met = 0
for criterion in rubric:
words = _content_words(criterion)
if not words: # nothing to check → treat as met
met += 1
continue
if any(w in low for w in words):
met += 1
return round(met / len(rubric), 3)
def claude_score(reply: str, rubric: List[str]) -> float:
"""LLM judge via the Anthropic API. Returns a 0..1 score.
Stdlib-only (urllib) so this file stays dependency-free. Falls back to the
heuristic if the key is missing or the call fails, so the gate never hard-errors.
"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
print("[judge] ANTHROPIC_API_KEY unset — using heuristic", file=sys.stderr)
return heuristic_score(reply, rubric)
import urllib.request
rubric_block = "\n".join(f"- {c}" for c in rubric)
prompt = (
"You are scoring an AI agent's reply against a rubric. For each criterion, "
"decide if the reply satisfies it. Respond with ONLY a number between 0 and "
"1 — the fraction of criteria satisfied.\n\n"
f"Rubric:\n{rubric_block}\n\nReply:\n{reply}\n\nScore:"
)
body = json.dumps({
"model": _JUDGE_MODEL,
"max_tokens": 8,
"messages": [{"role": "user", "content": prompt}],
}).encode()
req = urllib.request.Request(
"https://api.anthropic.com/v1/messages", data=body,
headers={"content-type": "application/json", "x-api-key": api_key,
"anthropic-version": "2023-06-01"},
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.load(resp)
text = "".join(b.get("text", "") for b in data.get("content", []))
m = re.search(r"[01](?:\.\d+)?", text)
return max(0.0, min(1.0, float(m.group(0)))) if m else heuristic_score(reply, rubric)
except Exception as exc: # network/auth/parse — degrade gracefully
print(f"[judge] claude backend failed ({exc}) — using heuristic", file=sys.stderr)
return heuristic_score(reply, rubric)
def score(reply: str, rubric: List[str]) -> float:
backend = os.environ.get("SKILLOPT_JUDGE", "heuristic")
return claude_score(reply, rubric) if backend == "claude" else heuristic_score(reply, rubric)
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="Score a reply against a rubric (0..1)")
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--rubric", help="Path to a JSON file containing a list of criteria")
g.add_argument("--rubric-inline", help="Inline JSON list of criteria")
p.add_argument("--reply", help="Path to the reply text (default: stdin)")
args = p.parse_args(argv)
rubric = (json.load(open(args.rubric, encoding="utf-8")) if args.rubric
else json.loads(args.rubric_inline))
reply = (open(args.reply, encoding="utf-8").read() if args.reply
else sys.stdin.read())
print(score(reply, rubric))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"skillopt-sleep": {
"command": "python3",
"args": ["/abs/path/to/SkillOpt/plugins/devin/mcp_server.py"],
"env": {
"SKILLOPT_DEVIN_CLAUDE_HOME": "~/.skillopt-sleep-devin"
}
}
}
}
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""SkillOpt-Sleep — Devin MCP server (stdio, stdlib-only).
Exposes the sleep engine as MCP tools so Devin (Cognition) can drive it. No
third-party deps: speaks JSON-RPC 2.0 over stdio with just the handful of MCP
methods clients need. Same `sleep_*` interface and engine flags as
`plugins/copilot`, plus a Devin-specific harvest step.
Before each data-reading action this server runs `harvest_devin.py` to convert
locally available Devin data (ATIF-v1.7 transcripts, agentmemory memories, and
.devin skill files) into the Claude Code-compatible JSONL the engine consumes,
writing it under SKILLOPT_DEVIN_CLAUDE_HOME and pointing the engine there with
`--claude-home`. After `sleep_adopt` the evolved skill is synced back into the
workspace's `.devin/skills/`.
Tools: sleep_status, sleep_dry_run, sleep_run, sleep_adopt, sleep_harvest,
sleep_schedule, sleep_unschedule. Each shells out to
`python -m skillopt_sleep <action> ...`. Configure Devin to launch:
python plugins/devin/mcp_server.py
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
# expanduser wraps the whole value so a "~/..." env var is expanded too (not
# just a default) — otherwise a literal ~ dir gets created.
REPO_ROOT = os.path.expanduser(
os.environ.get("SKILLOPT_SLEEP_REPO")
or os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
)
PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
CLAUDE_HOME = os.path.expanduser(
os.environ.get("SKILLOPT_DEVIN_CLAUDE_HOME", "~/.skillopt-sleep-devin")
)
MANAGED_SKILL_NAME = os.environ.get("SKILLOPT_MANAGED_SKILL", "skillopt-sleep-learned")
PROTOCOL_VERSION = "2024-11-05"
TOOLS = [
{"name": "sleep_status", "action": "status",
"description": "Show how many SkillOpt-Sleep nights have run and the latest staged proposal."},
{"name": "sleep_dry_run", "action": "dry-run",
"description": "Preview a sleep cycle (harvest+mine+replay) without staging anything."},
{"name": "sleep_run", "action": "run",
"description": "Run a full sleep cycle; stages a reviewed proposal. Nothing live changes until adopt."},
{"name": "sleep_adopt", "action": "adopt",
"description": "Apply the latest staged proposal to the managed SKILL.md and sync it into .devin/skills/."},
{"name": "sleep_harvest", "action": "harvest",
"description": "Debug: list the recurring tasks mined from recent Devin sessions."},
{"name": "sleep_schedule", "action": "schedule",
"description": "Install a nightly cron entry to run the sleep cycle automatically."},
{"name": "sleep_unschedule", "action": "unschedule",
"description": "Remove the nightly cron entry for a project."},
]
_BY_NAME = {t["name"]: t for t in TOOLS}
_TOOL_SCHEMA = {
"type": "object",
"properties": {
"project": {"type": "string",
"description": "Project dir to evolve (default: cwd)."},
"backend": {"type": "string", "enum": ["mock", "claude", "codex", "copilot", "handoff"],
"description": "mock = no API spend (default); claude/codex/copilot = real; handoff = session answers prompts, no API subprocess."},
"scope": {"type": "string", "enum": ["invoked", "all"],
"description": "Harvest scope (default: invoked project only)."},
"source": {"type": "string", "enum": ["claude", "codex", "auto"],
"description": "Transcript source (default: claude)."},
"model": {"type": "string",
"description": "Backend-specific model override."},
"tasks_file": {"type": "string",
"description": "Path to reviewed TaskRecord JSON (skips harvest)."},
"target_skill_path": {"type": "string",
"description": "Explicit SKILL.md path to evolve/stage/adopt."},
"progress": {"type": "boolean",
"description": "Print phase progress to stderr."},
"max_sessions": {"type": "integer",
"description": "Cap harvested sessions per run."},
"max_tasks": {"type": "integer",
"description": "Cap mined tasks per run."},
"lookback_hours": {"type": "integer",
"description": "Harvest window in hours (default: 72)."},
"auto_adopt": {"type": "boolean",
"description": "Auto-adopt if gate passes (default: false)."},
"json": {"type": "boolean",
"description": "Return machine-readable JSON output."},
"edit_budget": {"type": "integer",
"description": "Max bounded edits per night (default: 4)."},
"hour": {"type": "integer",
"description": "Hour for schedule (0-23, default: 3)."},
"minute": {"type": "integer",
"description": "Minute for schedule (0-59, default: 17)."},
},
"additionalProperties": False,
}
# actions that read harvested Devin data (schedule/unschedule/adopt don't)
_HARVEST_ACTIONS = {"status", "dry-run", "run", "harvest"}
def _run_harvest() -> str:
"""Convert local Devin data into the JSONL the engine reads, under CLAUDE_HOME."""
harvester = os.path.join(PLUGIN_DIR, "harvest_devin.py")
env = dict(os.environ)
env["PYTHONPATH"] = REPO_ROOT + os.pathsep + env.get("PYTHONPATH", "")
try:
proc = subprocess.run(
[sys.executable, harvester, "--out-dir", CLAUDE_HOME],
capture_output=True, text=True, timeout=60, env=env,
)
out = (proc.stdout or "").strip()
err = (proc.stderr or "").strip()
return out + (("\n[harvest stderr]\n" + err) if err else "")
except Exception as exc:
return f"[harvest_devin] warning: {exc}"
def _sync_skill(project: str) -> str:
"""After adopt, copy the evolved skill into the workspace's .devin/skills/."""
src = os.path.join(CLAUDE_HOME, "skills", MANAGED_SKILL_NAME, "SKILL.md")
if not (os.path.isfile(src) and project and os.path.isdir(project)):
return ""
dot_root = os.path.join(project, ".devin")
if not os.path.isdir(dot_root):
return ""
dst_dir = os.path.join(dot_root, "skills", MANAGED_SKILL_NAME)
os.makedirs(dst_dir, exist_ok=True)
dst = os.path.join(dst_dir, "SKILL.md")
shutil.copy2(src, dst)
return f"\n[sleep] synced evolved skill → {dst}"
def _run_engine(action: str, args: dict) -> str:
harvest_out = _run_harvest() if action in _HARVEST_ACTIONS else ""
py = sys.executable or "python3"
cmd = [py, "-m", "skillopt_sleep", action, "--claude-home", CLAUDE_HOME]
# Devin transcripts are converted to the Claude format, so default source=claude
if not args.get("source"):
cmd += ["--source", "claude"]
# String-valued flags
for flag, key in [
("--project", "project"), ("--backend", "backend"),
("--scope", "scope"), ("--source", "source"),
("--model", "model"), ("--tasks-file", "tasks_file"),
("--target-skill-path", "target_skill_path"),
]:
val = args.get(key)
if val:
cmd += [flag, str(val)]
# Integer-valued flags
for flag, key in [
("--max-sessions", "max_sessions"), ("--max-tasks", "max_tasks"),
("--lookback-hours", "lookback_hours"), ("--edit-budget", "edit_budget"),
("--hour", "hour"), ("--minute", "minute"),
]:
val = args.get(key)
if val is not None:
cmd += [flag, str(int(val))]
# Boolean flags
for flag, key in [
("--progress", "progress"), ("--auto-adopt", "auto_adopt"), ("--json", "json"),
]:
if args.get(key):
cmd.append(flag)
env = dict(os.environ)
env["PYTHONPATH"] = REPO_ROOT + os.pathsep + env.get("PYTHONPATH", "")
try:
proc = subprocess.run(cmd, cwd=REPO_ROOT, capture_output=True,
text=True, timeout=3600, env=env)
except Exception as e:
return f"[harvest]\n{harvest_out}\n[error] failed to run engine: {e}"
out = (proc.stdout or "").strip()
err = (proc.stderr or "").strip()
result = (f"[harvest]\n{harvest_out}\n\n" if harvest_out else "") + f"[engine]\n{out}"
if err:
result += f"\n[stderr]\n{err}"
if action == "adopt":
result += _sync_skill(args.get("project") or os.getcwd())
return result
def _result(id_, result):
return {"jsonrpc": "2.0", "id": id_, "result": result}
def _error(id_, code, message):
return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": message}}
def handle(req: dict):
method = req.get("method")
id_ = req.get("id")
if method == "initialize":
return _result(id_, {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {"tools": {}},
"serverInfo": {"name": "skillopt-sleep", "version": "0.1.0"},
})
if method in ("notifications/initialized", "initialized"):
return None
if method == "tools/list":
return _result(id_, {"tools": [
{"name": t["name"], "description": t["description"], "inputSchema": _TOOL_SCHEMA}
for t in TOOLS
]})
if method == "tools/call":
params = req.get("params") or {}
name = params.get("name")
tool = _BY_NAME.get(name)
if not tool:
return _error(id_, -32602, f"unknown tool: {name}")
text = _run_engine(tool["action"], params.get("arguments") or {})
return _result(id_, {"content": [{"type": "text", "text": text}]})
if method == "ping":
return _result(id_, {})
return _error(id_, -32601, f"method not found: {method}")
def main() -> int:
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except Exception:
continue
resp = handle(req)
if resp is not None:
sys.stdout.write(json.dumps(resp) + "\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+115
View File
@@ -0,0 +1,115 @@
# OpenClaw reference adaptation for SkillOpt-Sleep
This directory is a contributed reference for connecting
[SkillOpt-Sleep](https://github.com/microsoft/SkillOpt) to
[OpenClaw](https://github.com/openclaw/openclaw) with a custom DeepSeek/Ollama
backend.
> **Reference status.** This is not one of the shared, plug-and-play
> `skillopt_sleep` wrappers. Several scripts and the sample config contain
> environment-specific absolute paths and assumptions from the original setup,
> and the contributed wrapper has unresolved Python 3.10 syntax and backend
> factory-signature gaps. The current checkout is not directly runnable; treat
> it as porting source material, not an installation.
## Included components
| File | Purpose |
|---|---|
| `run_sleep.py` | custom cycle entry point |
| `skillopt_sleep_openclaw.py` | DeepSeek Chat Completions backend plus local Ollama embeddings |
| `run_sleep_cron.sh` | category-oriented cron wrapper |
| `slash_sleep.py` | experimental `/sleep` command helper |
| `config.json` | example engine configuration |
| `SKILL.md` | OpenClaw skill manifest |
| `tests/*.json` | example task sets for research, DevOps, and wiki workflows |
The adaptation imports the shared engine but registers its own backend and
maintains its own wrapper behavior. Changes to the shared CLI documentation do
not automatically make every option available through these custom scripts.
## Intended cycle
```text
harvest supported session data or load a task file
→ replay with the current skill
→ propose bounded edits
→ validate the candidate on held-out tasks
→ stage a proposal for operator review
```
The intended safety boundary is manual adoption: review the generated report and
staged files before changing a live skill.
## Adapt before use
1. Clone SkillOpt into a location you control:
```bash
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt/plugins/openclaw
```
2. Inspect and replace the sample absolute paths in `run_sleep.py`,
`slash_sleep.py`, `run_sleep_cron.sh`, and `config.json`. Confirm the engine
checkout, OpenClaw workspace, state directory, skill directory, and task-file
paths all point to isolated test locations.
3. Review `config.json`. In particular, do not assume that values such as
`max_tokens_per_night` or `replay_mode` are enforced by this custom wrapper
merely because they appear in the example config.
4. Supply credentials through your normal secret-management mechanism. Do not
commit a DeepSeek key or place it in a world-readable file.
5. Resolve every known porting gap listed in [`SKILL.md`](SKILL.md), add
isolated tests for your adapted backend, and verify that `--help` imports
cleanly on Python 3.10+. Only then start with a dry run and one reviewed
task file. The target command should be shaped like:
```bash
cd /path/to/SkillOpt/plugins/openclaw
python3 run_sleep.py --config /path/to/reviewed-config.json \
--tasks tests/research-cron-tasks.json --dry-run
```
6. Inspect the report, paths, network destinations, and proposed edits before
considering a non-dry run or scheduling.
## Data boundary
The custom `openclaw-deepseek` backend sends task, skill, response, rubric, and
reflection content to the configured DeepSeek endpoint. Its embedding helper can
send truncated text to the configured local Ollama service. Do not assume these
outbound prompts have been fully redacted; inspect transcript/task inputs and the
provider's retention policy before using real data.
Use HTTPS for a remote DeepSeek-compatible endpoint. Keep any plaintext Ollama
endpoint on a trusted loopback interface. For a network-free engine smoke test,
use the shared SkillOpt-Sleep CLI with `--backend mock` rather than assuming this
custom wrapper is isolated.
## Scheduling
`run_sleep_cron.sh` and the scheduling helpers are examples, not portable
installers. Adapt their paths, create log directories, verify their environment,
and run the exact command manually before adding a cron entry. Scheduled runs
must preserve the same manual-adoption and credential boundaries as interactive
runs.
## Validation scope
The bundled JSON files are example held-out task sets, not a universal OpenClaw
benchmark. Provider cost and quality depend on the selected model, task content,
number of calls, and pricing at run time; this reference does not promise a fixed
nightly cost. Validate the adapted workflow in an isolated workspace before
using it on live skills.
For the supported shared-engine CLI and its current flags, see the
[integration reference](../README.md#supported-cli-surface). For measured
SkillOpt-Sleep results and limitations, see
[`docs/sleep/RESULTS.md`](../../docs/sleep/RESULTS.md).
## License
MIT, consistent with SkillOpt core.
+103
View File
@@ -0,0 +1,103 @@
---
name: skillopt-sleep
description: Reference-only OpenClaw adaptation of SkillOpt-Sleep. Use it to study or port the contributed DeepSeek wrapper, not as a ready-to-run installation.
---
# SkillOpt-Sleep OpenClaw reference adaptation
This directory is a contributed **reference**, not a supported, plug-and-play
OpenClaw integration. It illustrates one way to connect the shared
`skillopt_sleep` cycle to a custom DeepSeek Chat Completions backend and a set of
environment-specific task fixtures.
Do not run or schedule the files unchanged. Several scripts and the sample
configuration preserve assumptions from the contributor's original machine,
and parts of the wrapper have not yet been ported to the current shared-engine
interfaces. Start with the directory's [README.md](README.md), which is the
authoritative status and adaptation guide.
## What is included
- `skillopt_sleep_openclaw.py` — a contributed DeepSeek backend prototype. It
also contains an Ollama embedding helper, but that helper is not wired into
the current shared sleep cycle.
- `run_sleep.py` — a custom cycle wrapper with environment-specific paths and a
backend-registration shim.
- `slash_sleep.py` — an experimental command helper written for an older
staging-manifest shape.
- `run_sleep_cron.sh` — a machine-specific category runner, not a portable cron
installer.
- `config.json` — a sample configuration, not a set of guaranteed or enforced
runtime limits.
- `tests/*.json` — example task fixtures from one environment, not a universal
OpenClaw benchmark.
## Known porting gaps
Before treating this as an integration, a maintainer must at least:
1. Replace every absolute workspace, repository, state, skill, log, and task
path with explicit user configuration.
2. Update the custom backend factory to the current `get_backend` call contract,
including the project directory, and update its backend methods and edit
records to the current protocol.
3. Replace the experimental adoption logic with the current staging manifest
and `skillopt_sleep.staging.adopt` behavior. Current staging artifacts use
`proposed_SKILL.md` / `proposed_CLAUDE.md`, `manifest.json`, and report files;
they do not expose the old `manifest.proposed_skill` field.
4. Decide how real OpenClaw transcripts are converted into a supported session
format. Pointing `claude_home` at an arbitrary agent directory does not by
itself make its files Claude Code-compatible JSONL.
5. Build scheduling around the adapted wrapper. The shared scheduler launches
the shared CLI; it does not automatically preserve this custom backend or
its category task-file flow.
6. Add isolated end-to-end tests for dry-run, accepted/rejected gates, staging,
adoption and backup, credential failure, and scheduled execution.
Until those gaps are resolved, use the supported shared
`python -m skillopt_sleep` CLI with `--backend mock` to test SkillOpt-Sleep itself,
and treat this directory only as source material for a future OpenClaw port.
## Shared-engine features are not wrapper features
At this revision the supported shared CLI backends are `mock`, `claude`,
`codex`, `copilot`, `handoff`, and `azure_openai`; the
[plugin integration reference](../README.md#supported-cli-surface) is the
authoritative list. The shared engine can consolidate a selected skill and
project `CLAUDE.md` memory (controlled by `evolve_skill` and `evolve_memory`),
and its `schedule` / `unschedule` actions manage shared-engine cron entries.
Those capabilities do **not** make the custom OpenClaw wrapper portable: the
shared scheduler will not invoke the prototype backend or its category
fixtures. Use the shared documentation for those features, not this reference
SKILL.
## Data and credential boundary
The prototype DeepSeek backend sends task, skill, memory, response, rubric, and
reflection content to its configured Chat Completions endpoint. Its source also
contains a helper that can send text to an Ollama service if a future port wires
that helper into the cycle. Neither path should be assumed to remove every
secret or private detail.
Before any port is tested with real data:
- use isolated, synthetic or explicitly reviewed task files;
- replace sample business names, personal references, URLs, and machine paths;
- load credentials through the operator's secret-management mechanism;
- verify TLS and retention policy for every remote endpoint; and
- inspect all staged artifacts before adoption.
The bundled fixtures are examples only. Their scores and any old cost estimates
do not establish effectiveness, safety, or a stable nightly price for another
OpenClaw deployment.
## Further information
- [OpenClaw README](README.md) — current reference status and adaptation checklist
- [plugin integration reference](../README.md) — supported shared-engine CLI
surface and data boundary
- [SkillOpt-Sleep documentation](../../docs/sleep/README.md) — concepts,
results, and limitations
Contributions that turn this reference into a portable integration should add
tests and update all three documents together.
+30
View File
@@ -0,0 +1,30 @@
{
"_comment": "OpenClaw adaptation of skillopt-sleep. Edit and run via run_sleep.py",
"claude_home": "/home/ethanclaw/.openclaw/agents",
"invoked_project": "/home/ethanclaw/.openclaw/workspace",
"projects": "invoked",
"lookback_hours": 168,
"max_tasks_per_night": 12,
"max_tokens_per_night": 800000,
"holdout_fraction": 0.34,
"val_fraction": 0.34,
"test_fraction": 0.0,
"backend": "openclaw-deepseek",
"model": "deepseek-v4-pro",
"gate_mode": "on",
"edit_budget": 3,
"gate_metric": "mixed",
"gate_mixed_weight": 0.5,
"replay_mode": "fresh",
"evolve_memory": true,
"evolve_skill": true,
"llm_mine": false,
"auto_adopt": false,
"managed_skill_name": "skillopt-sleep-learned",
"redact_secrets": true,
"seed": 42
}
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""run_sleep.py — OpenClaw entry point for SkillOpt-Sleep.
Runs one nightly sleep cycle:
1. harvest recent session transcripts
2. mine recurring task patterns
3. replay tasks with current skill (baseline) + candidate skill (with proposed edit)
4. gate candidate vs baseline on held-out accuracy
5. stage the proposal in ~/.skillopt-sleep/staging/<night>/
6. leave adoption to Ethan (auto_adopt=false)
Usage:
python3 run_sleep.py # one cycle, default config
python3 run_sleep.py --dry-run # compute report only, no staging
python3 run_sleep.py --tasks path.json # use a pre-built task file
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Ensure the skillopt_sleep package is importable (it lives in the cloned repo)
REPO = Path("/home/ethanclaw/.openclaw/workspace/SkillOpt")
sys.path.insert(0, str(REPO))
# Register our backend before importing cycle
from skillopt_sleep_openclaw import OpenClawDeepSeekBackend
import skillopt_sleep.backend as _b
_b._BACKENDS = getattr(_b, "_BACKENDS", {})
_b._BACKENDS["openclaw-deepseek"] = OpenClawDeepSeekBackend
# Patch get_backend to know about our backend
_orig_get_backend = _b.get_backend
def get_backend(name, model="", codex_path=""):
if name == "openclaw-deepseek":
return OpenClawDeepSeekBackend(model=model or "deepseek-v4-pro")
return _orig_get_backend(name, model=model, codex_path=codex_path)
_b.get_backend = get_backend
from skillopt_sleep.cycle import run_sleep_cycle
from skillopt_sleep.config import load_config
def main() -> int:
ap = argparse.ArgumentParser(description="OpenClaw SkillOpt-Sleep nightly cycle")
ap.add_argument("--dry-run", action="store_true", help="Compute but don't stage")
ap.add_argument("--config", default="/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep/config.json")
ap.add_argument("--tasks", default=None, help="Path to pre-built tasks JSON")
ap.add_argument("--verbose", action="store_true")
args = ap.parse_args()
# Load config from file then override with our defaults
overrides = {}
if os.path.exists(args.config):
with open(args.config) as f:
overrides.update(json.load(f))
overrides.pop("_comment", None)
cfg = load_config(**overrides)
seed_tasks = None
if args.tasks:
from skillopt_sleep.types import TaskRecord
with open(args.tasks) as f:
raw = json.load(f)
# Translate our test-set fields → TaskRecord fields
seed_tasks = []
for t in raw:
seed_tasks.append(TaskRecord(
id=t['id'],
project=t.get('project', 'openclaw'),
intent=t.get('intent') or t.get('prompt', ''),
context_excerpt=t.get('context_excerpt', ''),
attempted_solution=t.get('attempted_solution', ''),
outcome=t.get('outcome', 'unknown'),
reference_kind=t.get('reference_kind', 'rubric'),
reference=t.get('reference', ''),
judge=t.get('judge', {}),
tags=t.get('tags', []),
source_sessions=t.get('source_sessions', []),
split=t.get('split', 'train'),
))
print(f"[skillopt-sleep] starting cycle...")
print(f" backend: {cfg.get('backend')}")
print(f" project: {cfg.get('invoked_project')}")
print(f" max tasks: {cfg.get('max_tasks_per_night')}")
print(f" edit budget: {cfg.get('edit_budget')}")
print(f" dry_run: {args.dry_run}")
outcome = run_sleep_cycle(cfg, seed_tasks=seed_tasks, dry_run=args.dry_run)
r = outcome.report
print(f"\n=== Report — night {r.night} ===")
print(f" sessions harvested: {r.n_sessions}")
print(f" tasks mined: {r.n_tasks} (replayed: {r.n_replayed})")
print(f" baseline: {r.baseline_score:.3f} -> candidate: {r.candidate_score:.3f}")
print(f" gate: {r.gate_action} accepted={r.accepted}")
print(f" tokens: {r.tokens_used}")
if r.edits:
print(f" applied edits ({len(r.edits)}):")
for e in r.edits:
print(f" [{e.target}/{e.op}] {e.content[:80]}...")
if r.rejected_edits:
print(f" rejected edits ({len(r.rejected_edits)}) — kept as negative feedback")
if r.notes:
for n in r.notes:
print(f" note: {n}")
if outcome.staging_dir:
print(f"\n STAGED at: {outcome.staging_dir}")
print(f" Review with: ls {outcome.staging_dir}")
return 0 if r.accepted or r.candidate_score >= r.baseline_score else 1
if __name__ == "__main__":
sys.exit(main())
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# run_sleep_cron.sh — wrapper for cron-driven nightly sleep cycle
#
# Usage: bash run_sleep_cron.sh [category1 category2 ...]
# No args: run on all categories in tests/
# With args: run only on listed categories (research-cron, devops, wiki)
#
# Cron (3am MYT daily):
# 0 3 * * * cd /home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep && bash run_sleep_cron.sh >> ~/.skillopt-sleep/nightly.log 2>&1
set -euo pipefail
SKILL_DIR="/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep"
TESTS_DIR="$SKILL_DIR/tests"
LOG_DIR="$HOME/.skillopt-sleep/logs"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
LOG_FILE="$LOG_DIR/night-$TIMESTAMP.log"
# category → test file map
declare -A CATEGORIES=(
["research-cron"]="research-cron-tasks.json"
["devops"]="devops-tasks.json"
["wiki"]="wiki-tasks.json"
)
# Determine which categories to run
if [ $# -eq 0 ]; then
CATS=("research-cron" "devops" "wiki")
else
CATS=("$@")
fi
{
echo "=========================================="
echo "SkillOpt-Sleep nightly — $TIMESTAMP"
echo "Categories: ${CATS[*]}"
echo "=========================================="
} | tee -a "$LOG_FILE"
# Pre-flight: check DeepSeek API key
if ! grep -q "DEEPSEEK_API_KEY=" "$HOME/.openclaw/.env" 2>/dev/null; then
echo "ERROR: DEEPSEEK_API_KEY not found in ~/.openclaw/.env" | tee -a "$LOG_FILE"
exit 1
fi
EXIT_CODE=0
for cat in "${CATS[@]}"; do
tasks_file="$TESTS_DIR/${CATEGORIES[$cat]:-}"
if [ ! -f "$tasks_file" ]; then
echo "SKIP: $cat (no tasks file: $tasks_file)" | tee -a "$LOG_FILE"
continue
fi
echo "" | tee -a "$LOG_FILE"
echo "--- [$cat] starting cycle ---" | tee -a "$LOG_FILE"
cd "$SKILL_DIR"
if python3 run_sleep.py --tasks "$tasks_file" 2>&1 | tee -a "$LOG_FILE"; then
echo "--- [$cat] OK ---" | tee -a "$LOG_FILE"
else
EC=$?
echo "--- [$cat] FAILED (exit $EC) ---" | tee -a "$LOG_FILE"
EXIT_CODE=$EC
fi
done
{
echo ""
echo "=========================================="
echo "Done. Exit: $EXIT_CODE"
echo "=========================================="
} | tee -a "$LOG_FILE"
exit $EXIT_CODE
+275
View File
@@ -0,0 +1,275 @@
"""OpenClaw backend for SkillOpt-Sleep.
Adapts the skillopt_sleep Backend protocol to our DeepSeek + Ollama stack:
- attempt/judge/reflect -> DeepSeek V4 Pro (or Flash for cost)
- embeddings -> Ollama nomic-embed-text (already configured)
This backend NEVER mutates live state. It only returns text + EditRecord
proposals that the gate stages for human review.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
from typing import Any, Dict, List, Optional, Tuple
from skillopt_sleep.backend import Backend, _normalize, exact_score
from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
# ── DeepSeek + Ollama OpenAI-compatible API client (curl-based, no extra deps) ──
def _chat(messages: List[Dict[str, str]], *, model: str, temperature: float = 0.2, max_tokens: int = 1500) -> str:
"""Call DeepSeek V4 Pro via curl + jq. No extra Python deps needed."""
import json as _json
import urllib.request
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
if not api_key:
# try loading from .env
env_path = os.path.expanduser("~/.openclaw/.env")
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
if line.startswith("DEEPSEEK_API_KEY="):
api_key = line.split("=", 1)[1].strip()
break
base = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": False,
}
req = urllib.request.Request(
f"{base}/chat/completions",
data=_json.dumps(payload).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
)
try:
with urllib.request.urlopen(req, timeout=180) as resp:
data = _json.loads(resp.read().decode("utf-8"))
return data["choices"][0]["message"]["content"]
except Exception as e:
return f"[BACKEND_ERROR] {type(e).__name__}: {str(e)[:200]}"
def _embed(text: str) -> List[float]:
"""Call Ollama for embeddings. Uses the configured nomic-embed-text model."""
import json as _json
import urllib.request
try:
req = urllib.request.Request(
"http://127.0.0.1:11434/api/embeddings",
data=_json.dumps({"model": "nomic-embed-text:latest", "prompt": text[:2000]}).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = _json.loads(resp.read().decode("utf-8"))
return data.get("embedding", [])
except Exception:
return []
# ── Backend implementation ────────────────────────────────────────────────────
class OpenClawDeepSeekBackend(Backend):
"""Use DeepSeek V4 Pro for attempt/judge/reflect, Ollama for embeddings.
- "model" passed to constructor = optimizer model (default: deepseek-v4-pro)
- "judge_model" = judge model (default: deepseek-v4-pro for quality)
- "cheap_model" = budget-fallback (deepseek-v4-flash)
"""
name = "openclaw-deepseek"
def __init__(
self,
model: str = "deepseek-v4-pro",
judge_model: str = "deepseek-v4-pro",
cheap_model: str = "deepseek-v4-flash",
):
self._model = model
self._judge_model = judge_model
self._cheap_model = cheap_model
self._tokens = 0 # rough estimate
def tokens_used(self) -> int:
return self._tokens
# ── 1. attempt: produce a response given the task + skill + memory ──
def attempt(self, task: TaskRecord, skill: str, memory: str) -> str:
sys = (
"You are an OpenClaw agent (Kobe ecosystem). Use the skill and memory below to complete the task. "
"If the task asks for a structured output, follow the rubric exactly. "
"Be concise. No preamble, no explanation unless the task asks for it."
)
usr = f"""## SKILL
{skill or '(no skill yet)'}
## MEMORY
{memory or '(no memory yet)'}
## TASK
{task.intent}
## CONTEXT (if any)
{task.context_excerpt or '(none)'}
## RESPONSE
"""
out = _chat(
[{"role": "system", "content": sys}, {"role": "user", "content": usr}],
model=self._model,
temperature=0.2,
)
self._tokens += len(usr) // 4 + 200
return out
# ── 2. judge: score the response ──
def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
# Hard score: exact-match against task.reference (if available)
hard = exact_score(task.reference or "", response)
# Soft score: LLM judge against rubric (reference if reference_kind=='rubric')
rubric_text = task.reference if task.reference_kind == "rubric" else ""
if rubric_text:
judge_prompt = f"""You are a strict grader. Score the response 0.0-1.0 against the rubric.
## TASK
{task.intent}
## REFERENCE
{task.reference or '(none)'}
## RUBRIC
{rubric_text}
## RESPONSE
{response[:3000]}
## INSTRUCTIONS
Return ONLY a single float 0.0-1.0 on one line. No explanation. No markdown.
"""
try:
j_out = _chat(
[{"role": "user", "content": judge_prompt}],
model=self._judge_model,
temperature=0.0,
max_tokens=20,
).strip()
soft = float(re.search(r"[\d.]+", j_out.splitlines()[0]).group())
soft = max(0.0, min(1.0, soft))
except Exception:
soft = hard
self._tokens += 600
else:
soft = hard
rationale = f"hard={hard:.2f} soft={soft:.2f}"
return hard, soft, rationale
# ── 3. reflect: produce bounded EditRecord proposals ──
def reflect(
self,
failures: List[Tuple[TaskRecord, ReplayResult]],
successes: List[Tuple[TaskRecord, ReplayResult]],
skill: str,
memory: str,
*,
edit_budget: int,
evolve_skill: bool,
evolve_memory: bool,
) -> List[EditRecord]:
# Compact digest of failures + successes
fail_digest = "\n".join(
f"- TASK: {t.intent[:200]}\n RESPONSE: {r.response[:300]}\n WHY FAIL: {r.judge_rationale or r.fail_reason or 'unknown'}\n REFERENCE: {t.reference[:200]}"
for t, r in failures[:5]
) or "(none)"
succ_digest = "\n".join(
f"- TASK: {t.intent[:150]} -> OK ({r.judge_rationale or 'high score'})"
for t, r in successes[:3]
) or "(none)"
rubric_text = ""
if failures:
rubric_text = f"\n\n## REFERENCE ANSWERS\n{chr(10).join(f'Q: {t.intent[:120]}\\nA: {t.reference}' for t, _ in failures[:3] if t.reference)}"
sys = (
"You are SkillOpt-Sleep's bounded-edit optimizer. Your job is to propose 1-4 MINIMAL text edits to a skill or memory document "
"that, if applied, would help future agents do better on the failed tasks. "
"NEVER propose adding new sections wholesale. NEVER delete entire sections. "
"Edit primitives: ADD (append a step/rule at end), DELETE (remove a specific line by exact match), REPLACE (swap a specific line for another by exact match). "
"If you cannot identify a clear, minimal improvement, return an empty list."
)
usr = f"""## CURRENT SKILL
{skill or '(empty)'}
## CURRENT MEMORY
{memory or '(empty)'}
## FAILED TASKS
{fail_digest}
## SUCCESSFUL TASKS
{succ_digest}
{rubric_text}
## CONSTRAINTS
- max {edit_budget} edits total
- edits go to {"skill + memory" if (evolve_skill and evolve_memory) else ("skill" if evolve_skill else "memory")}
- if evolve_skill=False, target="memory" only; if evolve_memory=False, target="skill" only
- target must be "skill" or "memory"
## OUTPUT FORMAT (JSON, no markdown)
{{"edits": [{{"op": "ADD"|"DELETE"|"REPLACE", "target": "skill"|"memory", "content": "the text to add or replace with", "old_text": "for REPLACE/DELETE, the exact line to find", "rationale": "one short sentence why"}}]}}
"""
out = _chat(
[{"role": "system", "content": sys}, {"role": "user", "content": usr}],
model=self._model,
temperature=0.4,
max_tokens=2000,
)
self._tokens += len(usr) // 3 + 1500
# parse
try:
# strip markdown fences if any
cleaned = out.strip()
if cleaned.startswith("```"):
cleaned = re.sub(r"^```[a-z]*\n?", "", cleaned)
cleaned = re.sub(r"\n?```$", "", cleaned)
data = json.loads(cleaned)
edits: List[EditRecord] = []
for e in data.get("edits", [])[:edit_budget]:
if e.get("op") not in ("ADD", "DELETE", "REPLACE"):
continue
target = e.get("target", "skill")
if target not in ("skill", "memory"):
continue
if not evolve_skill and target == "skill":
continue
if not evolve_memory and target == "memory":
continue
edits.append(EditRecord(
op=e["op"],
target=target,
content=e.get("content", ""),
old_text=e.get("old_text", ""),
rationale=e.get("rationale", ""),
))
return edits
except Exception as e:
# log + return empty list (no edit is better than a bad edit)
return []
+330
View File
@@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""slash_sleep.py — OpenClaw slash command equivalent of SkillOpt's /sleep.
Use from the main session as a /sleep command:
/sleep status show current state + last 5 nights
/sleep run trigger one cycle (all categories) right now
/sleep run research-cron one cycle, single category
/sleep adopt [night] adopt the most recent (or specified) staged proposal
/sleep reject [night] discard the most recent (or specified) staging dir
/sleep dry-run report-only cycle
/sleep cost estimate per-night cost for current config
This script is a thin shell over run_sleep.py. It can be invoked either
manually from the main session or by an OpenClaw command handler.
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
from datetime import datetime
SKILL_DIR = Path("/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep")
STATE_DIR = Path(os.path.expanduser("~/.skillopt-sleep")) # default
STAGING_ROOT = STATE_DIR
def _resolve_state_dir():
"""Find the actual state dir.
Priority: scan in order:
1. ~/.skillopt-sleep/ (default)
2. /home/ethanclaw/.openclaw/workspace/.skillopt-sleep/ (when staging is there)
3. /home/ethanclaw/.openclaw/.skillopt-sleep/ (parent of overridden claude_home)
Pick the first one that has a state.json OR staging dir.
"""
candidates = [
Path(os.path.expanduser("~/.skillopt-sleep")),
Path("/home/ethanclaw/.openclaw/workspace/.skillopt-sleep"),
Path("/home/ethanclaw/.openclaw/.skillopt-sleep"),
]
# Prefer the one with state.json
for c in candidates:
if (c / "state.json").exists():
return c
# Then the one with staging
for c in candidates:
if (c / "staging").exists():
return c
return candidates[0]
TESTS_DIR = SKILL_DIR / "tests"
def status() -> int:
state_dir = _resolve_state_dir()
state_file = state_dir / "state.json"
staging_dir = state_dir / "staging"
print(f"=== SkillOpt-Sleep status ===")
print(f" state dir: {state_dir}")
print(f" staging dir: {staging_dir}")
if staging_dir.exists():
stages = sorted(staging_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)
print(f" staging entries: {len(stages)}")
for s in stages[:3]:
print(f" {s.name}")
if not state_file.exists():
print(" no state.json — run a cycle first (state is written at end of each non-dry-run)")
return 0
with open(state_file) as f:
state = json.load(f)
nights = state.get("history") or state.get("nights", [])
print(f" total nights: {len(nights)}")
print(f" accepted: {sum(1 for n in nights if n.get('accepted'))}")
print(f" rejected: {sum(1 for n in nights if not n.get('accepted'))}")
if nights:
last = nights[-1]
print(f" last night: {last.get('night')}")
print(f" accepted: {last.get('accepted')}")
print(f" baseline: {last.get('baseline'):.3f} -> candidate: {last.get('candidate'):.3f}")
print(f" staging: {last.get('staging') or '(none)'}")
return 0
def run_category(category: str, *, dry_run: bool = False) -> int:
cat_to_file = {
"research-cron": "research-cron-tasks.json",
"devops": "devops-tasks.json",
"wiki": "wiki-tasks.json",
}
tasks_file = TESTS_DIR / cat_to_file.get(category, f"{category}-tasks.json")
if not tasks_file.exists():
print(f"ERROR: no tasks file for category '{category}': {tasks_file}")
return 1
cmd = [sys.executable, str(SKILL_DIR / "run_sleep.py")]
if dry_run:
cmd.append("--dry-run")
cmd.extend(["--tasks", str(tasks_file)])
print(f"=== /sleep run {category}{' (dry-run)' if dry_run else ''} ===")
print(f" cmd: {' '.join(cmd)}")
rc = os.system(" ".join(f'"{c}"' for c in cmd))
return rc
def run_all(*, dry_run: bool = False) -> int:
rc = 0
for cat in ("research-cron", "devops", "wiki"):
r = run_category(cat, dry_run=dry_run)
if r != 0:
rc = r
return rc
def adopt(night: str = None) -> int:
state_dir = _resolve_state_dir()
state_file = state_dir / "state.json"
if not state_file.exists():
print("ERROR: no state to adopt from")
return 1
with open(state_file) as f:
state = json.load(f)
nights = state.get("history") or state.get("nights", [])
if not nights:
print("ERROR: no nights recorded")
return 1
target = None
if night:
target = next((n for n in nights if str(n.get("night")) == night), None)
if not target:
print(f"ERROR: night '{night}' not found")
return 1
else:
# most recent accepted
candidates = [n for n in nights if n.get("accepted") and n.get("staging")]
if not candidates:
print("ERROR: no accepted nights with staging to adopt")
return 1
target = candidates[-1]
staging = target["staging"]
if not os.path.isdir(staging):
print(f"ERROR: staging dir missing: {staging}")
return 1
print(f"=== /sleep adopt night {target['night']} ===")
print(f" staging: {staging}")
print(f" baseline: {target.get('baseline'):.3f} candidate: {target.get('candidate'):.3f}")
# Read proposed skill from staging
manifest = Path(staging) / "manifest.json"
if manifest.exists():
with open(manifest) as f:
m = json.load(f)
proposed = m.get("proposed_skill")
if proposed and Path(proposed).exists():
live = STATE_DIR / "live_skill.md"
backup = STATE_DIR / f"live_skill.md.bak-{target['night']}"
if live.exists():
shutil.copy2(live, backup)
print(f" backed up current live skill → {backup}")
shutil.copy2(proposed, live)
print(f" adopted proposed skill → {live}")
print()
print("✅ Adoption complete. Next cycle will use the new skill.")
return 0
print("ERROR: no proposed_skill in manifest")
return 1
def reject(night: str = None) -> int:
state_dir = _resolve_state_dir()
state_file = state_dir / "state.json"
if not state_file.exists():
print("ERROR: no state")
return 1
with open(state_file) as f:
state = json.load(f)
nights = state.get("history") or state.get("nights", [])
target = None
if night:
target = next((n for n in nights if str(n.get("night")) == night), None)
else:
candidates = [n for n in reversed(nights) if n.get("staging")]
target = candidates[0] if candidates else None
if not target or not target.get("staging"):
print("ERROR: nothing to reject")
return 1
staging = target["staging"]
if os.path.isdir(staging):
shutil.rmtree(staging)
print(f"🗑️ Removed staging: {staging}")
# remove from state
state["history"] = [n for n in nights if n.get("night") != target["night"]]
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
print("✅ Rejected. State updated.")
return 0
def schedule_cmd(hour: int, minute: int) -> int:
"""Install a nightly cron entry via the shared SkillOpt-Sleep scheduler.
Note: this schedules the shared engine (``python -m skillopt_sleep run``),
not the OpenClaw-specific ``run_sleep.py``. Use ``run_sleep_cron.sh`` if
you need the OpenClaw-native backend and category task files instead.
"""
try:
from skillopt_sleep.scheduler import schedule
except ImportError:
print("ERROR: skillopt_sleep.scheduler not available — is SkillOpt-Sleep installed?")
return 1
project = str(SKILL_DIR)
ok, msg = schedule(project, hour=hour, minute=minute)
print(msg)
return 0 if ok else 1
def unschedule_cmd(all_projects: bool) -> int:
"""Remove cron entry via the shared SkillOpt-Sleep scheduler."""
try:
from skillopt_sleep.scheduler import unschedule
except ImportError:
print("ERROR: skillopt_sleep.scheduler not available — is SkillOpt-Sleep installed?")
return 1
project = str(SKILL_DIR)
ok, msg = unschedule(project, all_projects=all_projects)
print(msg)
return 0 if ok else 1
def cost() -> int:
"""Estimate per-night cost based on the actual measurement from Phase 2.
From the real dry-run: 5 devops tasks used 14,427 tokens total.
That is ~2,885 tokens per task (all 3 phases combined).
"""
cfg_path = SKILL_DIR / "config.json"
cfg = {}
if cfg_path.exists():
cfg = json.loads(cfg_path.read_text())
cfg.pop("_comment", None)
max_tasks = cfg.get("max_tasks_per_night", 12)
model = cfg.get("model", "deepseek-v4-pro")
# DeepSeek V4 pricing
if "pro" in model:
cost_in = 0.435 # per 1M
cost_out = 0.87
elif "flash" in model:
cost_in = 0.14
cost_out = 0.28
else:
cost_in, cost_out = 0.5, 1.0
# Measured: ~2,900 tokens per task, 30% output / 70% input
toks_per_task = 2900
input_toks = int(toks_per_task * 0.7)
output_toks = int(toks_per_task * 0.3)
cost_in_total = (input_toks * max_tasks / 1_000_000) * cost_in
cost_out_total = (output_toks * max_tasks / 1_000_000) * cost_out
cost = cost_in_total + cost_out_total
print(f"=== Cost estimate (per actual measurement) ===")
print(f" model: {model}")
print(f" max tasks/night: {max_tasks}")
print(f" ~tokens/night: {toks_per_task * max_tasks:,}")
print(f" cost/night: ${cost:.3f}")
print(f" cost/month (30 nights): ${cost*30:.2f}")
print(f" cost/year (365 nights): ${cost*365:.2f}")
return 0
def main():
ap = argparse.ArgumentParser(description="OpenClaw /sleep command")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("status", help="show state + last 5 nights")
p_run = sub.add_parser("run", help="trigger one cycle")
p_run.add_argument("category", nargs="?", default=None,
choices=["research-cron", "devops", "wiki", None])
p_run.add_argument("--dry-run", action="store_true")
sub.add_parser("dry-run", help="report-only cycle (all categories)")
p_adopt = sub.add_parser("adopt", help="adopt most recent accepted staging")
p_adopt.add_argument("night", nargs="?", default=None)
p_reject = sub.add_parser("reject", help="discard most recent staging")
p_reject.add_argument("night", nargs="?", default=None)
sub.add_parser("cost", help="estimate cost")
p_schedule = sub.add_parser("schedule", help="install nightly cron entry")
p_schedule.add_argument("--hour", type=int, default=3, help="hour (0-23)")
p_schedule.add_argument("--minute", type=int, default=17, help="minute (0-59)")
p_unschedule = sub.add_parser("unschedule", help="remove cron entry")
p_unschedule.add_argument("--all", dest="all_projects", action="store_true",
help="remove entries for all projects")
args = ap.parse_args()
if args.cmd == "status":
return status()
if args.cmd == "run":
if args.category:
return run_category(args.category, dry_run=args.dry_run)
return run_all(dry_run=args.dry_run)
if args.cmd == "dry-run":
return run_all(dry_run=True)
if args.cmd == "adopt":
return adopt(args.night)
if args.cmd == "reject":
return reject(args.night)
if args.cmd == "cost":
return cost()
if args.cmd == "schedule":
return schedule_cmd(args.hour, args.minute)
if args.cmd == "unschedule":
return unschedule_cmd(args.all_projects)
return 1
if __name__ == "__main__":
sys.exit(main())
+87
View File
@@ -0,0 +1,87 @@
[
{
"id": "do-01",
"reference": "[STATUS] devops-agent | Site Uptime \u2192 geoxylia.com OK (200) | 14/06 22:30 MYT",
"rubric": "Score 1.0 if output matches the exact format [STATUS] devops-agent | Site Uptime \u2192 geoxylia.com OK (200) | DD/MM HH:MM MYT, with a real current time. Score 0.5 if format is close but missing one field. Score 0.0 if wrong format or hallucinated values.",
"project": "devops-infrastructure-check",
"intent": "Site Uptime check. Run: `curl -o /dev/null -s -w '%{http_code}' https://geoxylia.com`. Interpret the result 200, and report in our standard format: 'STATUS | TASK \u2192 RESULT | TIME'. If not 200, escalate.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"devops-infrastructure-check"
],
"source_sessions": [],
"split": "val"
},
{
"id": "do-02",
"reference": "Backup complete. Files: 87, Size: 1.2G, Last: 2026-06-14 22:00:00 MYT",
"rubric": "Score 1.0 if output includes the exact 'Backup complete. Files: N, Size: X, Last: timestamp' structure with plausible values. Score 0.5 if structure is close but one field missing. Score 0.0 if hallucinated or wrong structure.",
"project": "devops-infrastructure-check",
"intent": "Daily Memory Backup. Confirm this ran successfully by checking: `ls -t ~/backups/memory/memory-backup-*.tar.gz | head -3`. Report the file count, total size, and most recent backup time. Use format: 'Backup complete. Files: [N], Size: [X], Last: [timestamp]'.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"devops-infrastructure-check"
],
"source_sessions": [],
"split": "val"
},
{
"id": "do-03",
"reference": "1) Vercel CSP missing frame-ancestors: MEDIUM. Allows clickjacking if anyone embeds our pages; not exploitable for our content, but best-practice gap.\n2) OpenClaw plaintext API keys: LOW. The config is chmod 600, loopback-only, not in git. Standard OpenClaw behavior. Rotating would add zero real security given current exposure.",
"rubric": "Score 1.0 if both are classified correctly (MEDIUM and LOW respectively) and justifications are accurate (not panicky, not dismissive). Score 0.5 if classifications are wrong by one tier or justifications are weak. Score 0.0 if both over-classified as CRITICAL or both wrong.",
"project": "devops-infrastructure-check",
"intent": "Security Check daily run. Two findings: 1) Vercel CSP header missing 'frame-ancestors' directive, 2) OpenClaw config has 3 plaintext API keys. Classify each as: CRITICAL / HIGH / MEDIUM / LOW / INFO. Justify each in 1 sentence.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"devops-infrastructure-check"
],
"source_sessions": [],
"split": "train"
},
{
"id": "do-04",
"reference": "[INCIDENT] supabase.audit_results: anon role has no RLS policy \u2014 anyone with the URL can read all audit results. Fix: add policy 'audit_results_select_own' granting SELECT WHERE user_id = auth.uid(). Severity: HIGH (data exposure). Estimated 2-min fix.",
"rubric": "Score 1.0 if: (a) severity correctly identified as HIGH, (b) fix is a real RLS policy (not just 'enable RLS' since it's already enabled), (c) under 50 words, (d) Telegram-friendly format. Score 0.5 if severity right but fix is generic. Score 0.0 if missing severity or wrong fix.",
"project": "devops-infrastructure-check",
"intent": "Incident Check. The Supabase RLS check returned: 'table public.audit_results: rls enabled but policy missing for anon role'. Interpret severity, propose fix, and format as a Telegram alert (max 50 words).",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"devops-infrastructure-check"
],
"source_sessions": [],
"split": "val"
},
{
"id": "do-05",
"reference": "\ud83d\udee1\ufe0f Week security digest:\n\n\u2022 0 critical incidents, 1 high resolved (Supabase RLS policy added)\n\u2022 22 plaintext secrets: expected OpenClaw behavior, no action\n\u2022 1 medium open: Vercel CSP frame-ancestors, schedule for next sprint\n\nTrend: stable. No regressions vs last week.",
"rubric": "Score 1.0 if all 3 priority tiers mentioned with correct counts, ends with a trend statement, Telegram-friendly. Score 0.5 if structure is right but one tier wrong. Score 0.0 if missing a tier or wrong format.",
"project": "devops-infrastructure-check",
"intent": "Weekly security digest. Synthesize this week's findings: 22 plaintext secrets in openclaw.json (expected), 0 critical incidents, 1 high (Supabase RLS), 1 medium (CSP frame-ancestors), 0 low. Output a 3-bullet Telegram status.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"devops-infrastructure-check"
],
"source_sessions": [],
"split": "train"
}
]
@@ -0,0 +1,87 @@
[
{
"id": "rc-01",
"reference": "COMPETITOR MOVES: Otterly adds Perplexity tracker, joining Profound and LLMRefs in multi-platform citations.\nBACKLINK OPPORTUNITIES: 3 SEO directories (G2, Capterra, GetApp) have not been claimed.\nAGENCY BLUEPRINT: Top 2 agency sites bundle GEO audit + content refresh as $3K/mo tier.\nACTION ITEMS: Build Perplexity citation test into GeoXylia audit; claim G2 listing by Friday.",
"rubric": "Score 1.0 if all 4 section headings present in correct order, each with a substantive (not generic) 1-sentence content. Score 0.5 if headings present but content is generic. Score 0.0 if any heading missing or order wrong.",
"project": "research-cron-output",
"intent": "Weekly Competitive Deep Dive for GeoXylia. The competitor otterly.ai just added a Perplexity citation tracker. Produce the report header (top section) in our standard format: COMPETITOR MOVES, BACKLINK OPPORTUNITIES, AGENCY BLUEPRINT, ACTION ITEMS. Keep it to 4 lines, one per section heading with a 1-sentence placeholder.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"research-cron-output"
],
"source_sessions": [],
"split": "train"
},
{
"id": "rc-02",
"reference": "1. 'ai seo audit tool': 420 imp, pos 8.2, on page 1 \u2014 needs CTR lift (snippet/schema).\n2. 'geo audit tool': 230 imp, pos 12.5, page 2 \u2014 target blog post could push to page 1.\n3. 'llm optimization': 85 imp, pos 18.3, deep page-2 \u2014 fresh content with answer capsule could compete.",
"rubric": "Score 1.0 if the response correctly identifies 'ai seo audit tool', 'geo audit tool', and 'llm optimization' as the top 3 (NOT 'best free seo audit' which is already converting well, NOT 'free audit tool' which has too few impressions). Each must have correct impression count, position, and a substantive rationale. Score 0.5 if correct 3 keywords but rationale is weak. Score 0.0 if wrong keywords selected.",
"project": "research-cron-output",
"intent": "GSC keyword opportunity scan. From this snippet of GSC data, identify the top 3 keyword opportunities (high impressions, low CTR, position 5-15):\n\n1. 'ai seo audit tool' \u2014 420 imp, 12 clicks, pos 8.2\n2. 'best free seo audit' \u2014 1100 imp, 95 clicks, pos 4.1\n3. 'geo audit tool' \u2014 230 imp, 4 clicks, pos 12.5\n4. 'llm optimization' \u2014 85 imp, 1 click, pos 18.3\n5. 'free audit tool' \u2014 50 imp, 0 clicks, pos 22.0\n\nOutput: one line per opportunity, format 'KEYWORD: impressions, position, why-it-matters (1 short clause)'.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"research-cron-output"
],
"source_sessions": [],
"split": "train"
},
{
"id": "rc-03",
"reference": "Google AI Overviews now show source links more prominently + author bylines. For GeoXylia: this favors pages with clear authorship (add author schema to blog posts). Action: this week, add author + E-E-A-T schema markup to top 10 blog posts. Source: Google Search Central blog.",
"rubric": "Score 1.0 if: (a) under 60 words, (b) names the change, (c) gives GeoXylia-specific implication, (d) gives a concrete action item, (e) cites the source. Score 0.5 if missing 1-2 of these. Score 0.0 if over 60 words or missing 3+.",
"project": "research-cron-output",
"intent": "Daily Industry News scan. The Google Search Central blog just announced: 'AI Overviews now showing source links more prominently, with author bylines for E-E-A-T-heavy content.' Write a 1-paragraph Telegram alert (max 60 words) for Ethan. Include: 1) what changed, 2) what it means for GeoXylia, 3) any action item.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"research-cron-output"
],
"source_sessions": [],
"split": "val"
},
{
"id": "rc-04",
"reference": "Hi [Name], I saw seo-skill.com's resources page is one of the most-respected SEO learning hubs in the industry \u2014 your 2026 algorithm breakdown was spot-on. We just published a free 2026 AI SEO Audit comparison that your readers would find genuinely useful (no paywall, no signup). It covers the 8 leading AI-audit tools with hands-on screenshots and a clear feature matrix. GeoXylia is the only fully-free option in the comparison, so it's a natural fit for a 'tools to know' section. Mind if I share the link for inclusion?",
"rubric": "Score 1.0 if exactly 4 sentences, all four functional pieces present (compliment / mention resource / audience benefit / GeoXylia one-liner), conversational tone, no aggressive sales language. Score 0.5 if 3 of 4 pieces present or tone is too salesy. Score 0.0 if more than 5 sentences or missing 2+ pieces.",
"project": "research-cron-output",
"intent": "Backlink Outreach draft for the blog post 'Free AI SEO Audit Tool: 2026 Comparison'. The prospect is seo-skill.com (a popular SEO training site with a 'resources' page). Write a 4-sentence outreach email: 1) compliment, 2) mention our resource, 3) explain audience benefit, 4) one-line about GeoXylia.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"research-cron-output"
],
"source_sessions": [],
"split": "train"
},
{
"id": "rc-05",
"reference": "1) DO MORE: AI citation / LLM-mention topics \u2014 the 0.9% CTR at position 9.4 means we're visible but need richer answer capsules to lift CTR. Target 2x posts/week on this cluster.\n2) PAUSE: Pure schema-markup how-tos \u2014 'Schema Markup for SEO' has 0 clicks at position 41, the audience isn't searching this way. Rework as 'How to appear in AI answers' framing.\n3) TEST: 'Perplexity vs ChatGPT citation rates for [niche]' \u2014 unexplored angle, could capture comparison-intent traffic.",
"rubric": "Score 1.0 if all 3 are specific (not generic), cite actual data from the prompt, and contain a clear actionable change. Score 0.5 if 2 of 3 are specific. Score 0.0 if generic advice or no data citations.",
"project": "research-cron-output",
"intent": "Performance \u2192 Strategy feedback loop. Last week's top blog post was 'AI Citation Audit: Does Your Site Appear in ChatGPT?' with 4,200 impressions and 38 clicks (CTR 0.9%, position 9.4). The bottom post was 'Schema Markup for SEO: A 2026 Guide' with 110 impressions and 0 clicks (CTR 0%, position 41). Write 3 specific strategy adjustments: 1) what to do more of, 2) what to pause, 3) what new topic to test.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"research-cron-output"
],
"source_sessions": [],
"split": "val"
}
]
+70
View File
@@ -0,0 +1,70 @@
[
{
"id": "wk-01",
"reference": "1. What GEO is and isn't (define vs SEO/AEO, dispel the 'just add FAQ' myth)\n2. The 3 citation mechanisms LLMs use (RAG, fine-tuning, in-context; weight each)\n3. The 2026 citation data (real statistics from Profound/Otterly/Peec; what % of queries get citations)\n4. The action framework (a 5-step audit-and-fix process, concrete)\n5. Measurement (which metrics actually predict citation lift; vanity vs real)",
"rubric": "Score 1.0 if 5 sections, in a logical order, each with a substantive (not generic) purpose, and the section content is GEO-specific (not generic SEO). Score 0.5 if 5 sections but 1-2 are generic. Score 0.0 if wrong number of sections or wrong order.",
"project": "wiki-canonical-guide",
"intent": "Wiki canonical guide: 'GEO 2026 Standards'. Audience: a mid-level SEO specialist who has heard of GEO but not done it. Tone: technical, evidence-driven, no fluff. Length target: 1500-2200 words. Outline the 5 sections that should appear in order. For each, give a 1-sentence sub-purpose.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"wiki-canonical-guide"
],
"source_sessions": [],
"split": "val"
},
{
"id": "wk-02",
"reference": "Yes, add inbound links. (1) geo-2026-standards.md \u2192 '## Action Framework' section, anchor: 'platform-specific citation rules' \u2014 natural since GEO standards reference ChatGPT/Perplexity behavior. (2) seo-2026-standards.md \u2192 '## AI Overviews' section, anchor: 'AI platform citations' \u2014 links to the mechanism guide. (3) content-strategy.md \u2192 '## Content Types' section, anchor: 'per-platform citation' \u2014 content strategy needs to know which platform favors which content.",
"rubric": "Score 1.0 if all 3 inbound links proposed with specific section + natural anchor text, demonstrating the link solves a real navigational gap (not just SEO-link-building). Score 0.5 if 2 of 3 are well-placed. Score 0.0 if generic anchors like 'click here' or no specific sections named.",
"project": "wiki-canonical-guide",
"intent": "Cross-link audit. The wiki page 'ai-platform-citation-guide.md' has 4 outbound links to other wiki pages, but no inbound links from: 'geo-2026-standards.md', 'seo-2026-standards.md', 'content-strategy.md'. Should we add inbound links? In which page should each inbound link go, and what anchor text would be natural?",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"wiki-canonical-guide"
],
"source_sessions": [],
"split": "val"
},
{
"id": "wk-03",
"reference": "Priorities:\n1. Refresh 'geo-glossary.md' (last update 2026-04-12, 63 days) \u2014 add new terms like RAG, in-context citation, agentic SEO.\n2. Refresh 'competitor-pricing.md' (last update 2026-05-01, 44 days) \u2014 Profound raised enterprise tier.\n3. No structural fixes needed.\n\nTelegram: 'Wiki lint: 2 stale pages flagged (geo-glossary 63d, competitor-pricing 44d). No broken links. Both need refresh this week.'",
"rubric": "Score 1.0 if both stale pages correctly identified with specific (not generic) refresh notes, and Telegram summary is under 40 words with the right action. Score 0.5 if stale pages identified but refresh notes are vague. Score 0.0 if missing stale pages or Telegram over 40 words.",
"project": "wiki-canonical-guide",
"intent": "Wiki lint report. Today's scan: 14 wiki pages, 2 with 'Updated' dates > 30 days old ('geo-glossary.md' and 'competitor-pricing.md'), 0 broken internal links, 0 missing YAML frontmatter. Output: 1) prioritized action list, 2) Telegram summary (max 40 words).",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"wiki-canonical-guide"
],
"source_sessions": [],
"split": "train"
},
{
"id": "wk-04",
"reference": "Index rebuilt: 14 wiki pages registered in _index.md (was 12 \u2014 added competitor-pricing-rev2 and citations-q2-2026).\nQuestion for Ethan: should 'competitor-pricing.md' and 'competitor-pricing-rev2.md' be merged? They're 78% similar in content.",
"rubric": "Score 1.0 if both sentences are accurate (count matches, names are plausible) and the question identifies a real consolidation opportunity (not a fabricated one). Score 0.5 if structure is right but content vague. Score 0.0 if wrong format or no question.",
"project": "wiki-canonical-guide",
"intent": "Index rebuild check. Run `python3 ~/agent-shared/scripts/update-index.py` (assume it works). After the run, the new wiki/_index.md should list all 14 pages. Generate a 2-sentence confirmation message + 1 question for Ethan to verify.",
"context_excerpt": "",
"attempted_solution": "",
"outcome": "unknown",
"reference_kind": "rubric",
"judge": {},
"tags": [
"wiki-canonical-guide"
],
"source_sessions": [],
"split": "train"
}
]
+122
View File
@@ -0,0 +1,122 @@
@echo off
setlocal enabledelayedexpansion
:: Resolve REPO_ROOT
set "SCRIPT_DIR=%~dp0"
:: Strip trailing backslash
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "REPO_ROOT="
if exist "%SCRIPT_DIR%\..\skillopt_sleep" (
cd /d "%SCRIPT_DIR%\.."
set "REPO_ROOT=%CD%"
goto root_resolved
)
if not "%CLAUDE_PLUGIN_ROOT%"=="" if exist "%CLAUDE_PLUGIN_ROOT%\..\..\skillopt_sleep" (
cd /d "%CLAUDE_PLUGIN_ROOT%\..\.."
set "REPO_ROOT=%CD%"
goto root_resolved
)
if not "%SKILLOPT_SLEEP_REPO%"=="" if exist "%SKILLOPT_SLEEP_REPO%\skillopt_sleep" (
set "REPO_ROOT=%SKILLOPT_SLEEP_REPO%"
goto root_resolved
)
:: Search upward from current directory
set "d=%CD%"
:loop
if exist "!d!\skillopt_sleep" (
set "REPO_ROOT=!d!"
goto root_resolved
)
for %%I in ("!d!") do set "parent=%%~dpI"
:: Strip trailing backslash from parent if it's not root
if "!parent!"=="!d!" goto root_resolved
set "parent=!parent:~0,-1!"
if "!parent!"=="" goto root_resolved
set "d=!parent!"
goto loop
:root_resolved
if "%REPO_ROOT%"=="" goto fallback_mode
:: ── Source Checkout Mode ───────────────────────────────────────────
set "PY="
if not "%SKILLOPT_SLEEP_PYTHON%"=="" (
set "PY=%SKILLOPT_SLEEP_PYTHON%"
goto py_found
)
for %%p in (python3.exe python.exe py.exe) do (
where %%p >nul 2>nul
if !errorlevel! equ 0 (
%%p -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" >nul 2>nul
if !errorlevel! equ 0 (
set "PY=%%p"
goto py_found
)
)
)
:py_found
if "%PY%"=="" (
echo [sleep] ERROR: need Python >= 3.10 (found none). >&2
exit /b 1
)
cd /d "%REPO_ROOT%"
if "%~1" == "" (
"%PY%" -m skillopt_sleep status
) else (
"%PY%" -m skillopt_sleep %*
)
exit /b !errorlevel!
:: ── Fallback Mode (No Source Checkout) ─────────────────────────────
:fallback_mode
:: Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
where skillopt-sleep >nul 2>nul
if !errorlevel! neq 0 goto try_fallback_2
if "%~1" == "" (
skillopt-sleep status
) else (
skillopt-sleep %*
)
exit /b !errorlevel!
:try_fallback_2
:: Fallback 2: importable as a module (pip install into the active Python).
set "PY="
for %%p in (python3.exe python.exe py.exe) do (
where %%p >nul 2>nul
if !errorlevel! equ 0 (
%%p -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" >nul 2>nul
if !errorlevel! equ 0 (
%%p -c "import skillopt_sleep" >nul 2>nul
if !errorlevel! equ 0 (
set "PY=%%p"
goto py_import_found
)
)
)
)
:py_import_found
if "%PY%" == "" goto not_found
if "%~1" == "" (
"%PY%" -m skillopt_sleep status
) else (
"%PY%" -m skillopt_sleep %*
)
exit /b !errorlevel!
:not_found
echo [sleep] ERROR: could not locate the skillopt_sleep package. >&2
echo [sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt', >&2
echo [sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo. >&2
exit /b 1
+94
View File
@@ -0,0 +1,94 @@
# SkillOpt-Sleep shared runner for Windows PowerShell
# Resolves the repo root, picks Python >= 3.10, and runs the engine CLI.
#
# Usage: .\run-sleep.ps1 [status|run|dry-run|adopt|...] [args...]
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot
$RepoRoot = $null
if (Test-Path (Join-Path $ScriptDir "..\skillopt_sleep")) {
$RepoRoot = Resolve-Path (Join-Path $ScriptDir "..")
} elseif ($env:CLAUDE_PLUGIN_ROOT -and (Test-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..\skillopt_sleep"))) {
$RepoRoot = Resolve-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..")
} elseif ($env:SKILLOPT_SLEEP_REPO -and (Test-Path (Join-Path $env:SKILLOPT_SLEEP_REPO "skillopt_sleep"))) {
$RepoRoot = Resolve-Path $env:SKILLOPT_SLEEP_REPO
} else {
# search upward from current location
$d = Get-Item .
while ($d -and $d.FullName -ne $d.Root.FullName) {
if (Test-Path (Join-Path $d.FullName "skillopt_sleep")) {
$RepoRoot = $d.FullName
break
}
$d = Split-Path -Parent $d.FullName -ErrorAction SilentlyContinue | Get-Item -ErrorAction SilentlyContinue
}
}
$argsList = @($args)
if ($argsList.Count -eq 0) {
$argsList = @("status")
}
if ($RepoRoot) {
$Py = ""
if ($env:SKILLOPT_SLEEP_PYTHON) {
$Py = $env:SKILLOPT_SLEEP_PYTHON
} else {
foreach ($cand in @("python3", "python", "py")) {
$cmd = Get-Command $cand -ErrorAction SilentlyContinue
if ($cmd) {
$ver = & $cand -c "import sys; print('%d%d' % sys.version_info[:2])" 2>$null
if ($ver -and [int]$ver -ge 310) {
$Py = $cand
break
}
}
}
}
if (-not $Py) {
[Console]::Error.WriteLine("[sleep] ERROR: need Python >= 3.10 (found none).")
exit 1
}
Set-Location $RepoRoot
& $Py -m skillopt_sleep $argsList
exit $LASTEXITCODE
}
# No source checkout found — fall back to an installed engine.
# Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
$cliCmd = Get-Command "skillopt-sleep" -ErrorAction SilentlyContinue
if ($cliCmd) {
& skillopt-sleep $argsList
exit $LASTEXITCODE
}
# Fallback 2: importable as a module (pip install into the active Python).
$Py = ""
foreach ($cand in @("python3", "python", "py")) {
$cmd = Get-Command $cand -ErrorAction SilentlyContinue
if ($cmd) {
$ver = & $cand -c "import sys; print('%d%d' % sys.version_info[:2])" 2>$null
if ($ver -and [int]$ver -ge 310) {
$hasModule = & $cand -c "import skillopt_sleep" 2>$null
if ($LASTEXITCODE -eq 0) {
$Py = $cand
break
}
}
}
}
if ($Py) {
& $Py -m skillopt_sleep $argsList
exit $LASTEXITCODE
}
[Console]::Error.WriteLine("[sleep] ERROR: could not locate the skillopt_sleep package.")
[Console]::Error.WriteLine("[sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt',")
[Console]::Error.WriteLine("[sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo.")
exit 1
+43 -10
View File
@@ -24,23 +24,56 @@ else
d="$(dirname "$d")"
done
fi
if [ -z "${REPO_ROOT:-}" ]; then
echo "[sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root." >&2
exit 1
if [ "$#" -eq 0 ]; then set -- status; fi
if [ -n "${REPO_ROOT:-}" ]; then
# Source checkout: run from repo root so skillopt_sleep/ is importable.
PY=""
# Allow explicit Python override (useful on macOS with old system Python).
if [ -n "${SKILLOPT_SLEEP_PYTHON:-}" ]; then
PY="$SKILLOPT_SLEEP_PYTHON"
else
for cand in python3.12 python3.11 python3.10 python3; do
if command -v "$cand" >/dev/null 2>&1; then
ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)"
if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi
fi
done
fi
if [ -z "$PY" ]; then
echo "[sleep] ERROR: need Python >= 3.10 (found none)." >&2
exit 1
fi
cd "$REPO_ROOT"
exec "$PY" -m skillopt_sleep "$@"
fi
# No source checkout found — fall back to an installed engine.
# Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
# Checked before the import fallback because uv tool install / pipx isolate the
# package from the system Python's import path, so `python -c "import
# skillopt_sleep"` would fail even though the CLI is available.
if command -v skillopt-sleep >/dev/null 2>&1; then
exec skillopt-sleep "$@"
fi
# Fallback 2: importable as a module (pip install into the active Python).
# Pick a Python >= 3.10 and check importability.
PY=""
for cand in python3.12 python3.11 python3.10 python3; do
if command -v "$cand" >/dev/null 2>&1; then
ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)"
if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi
if [ "${ver:-0}" -ge 310 ] && "$cand" -c "import skillopt_sleep" >/dev/null 2>&1; then
PY="$cand"; break
fi
fi
done
if [ -z "$PY" ]; then
echo "[sleep] ERROR: need Python >= 3.10 (found none)." >&2
exit 1
if [ -n "$PY" ]; then
exec "$PY" -m skillopt_sleep "$@"
fi
if [ "$#" -eq 0 ]; then set -- status; fi
cd "$REPO_ROOT"
exec "$PY" -m skillopt_sleep "$@"
echo "[sleep] ERROR: could not locate the skillopt_sleep package." >&2
echo "[sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt'," >&2
echo "[sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo." >&2
exit 1
+14 -6
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "skillopt"
version = "0.1.0"
version = "0.2.0"
description = "SkillOpt: Agentic Skill Optimization via Reflective Training Loops"
readme = "README.md"
license = {text = "MIT"}
@@ -37,9 +37,11 @@ dependencies = [
# Benchmark-specific dependencies
alfworld = ["alfworld>=0.4.0", "gymnasium>=0.29.0"]
# Claude model backend
claude = ["claude-agent-sdk>=0.1.0"]
claude = ["claude-agent-sdk>=0.1.0", "json_repair>=0.61.0"]
# Qwen local model backend (via vLLM)
qwen = ["vllm>=0.4.0"]
qwen = ["vllm>=0.4.0", "json_repair>=0.61.0"]
# SearchQA data materialization
searchqa = ["datasets>=2.18.0"]
# Documentation site
docs = ["mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.24.0"]
# WebUI dashboard
@@ -51,11 +53,13 @@ all = [
"alfworld>=0.4.0",
"gymnasium>=0.29.0",
"claude-agent-sdk>=0.1.0",
"json_repair>=0.61.0",
]
[project.scripts]
skillopt-train = "scripts.train:main"
skillopt-eval = "scripts.eval_only:main"
skillopt-sleep = "skillopt_sleep.__main__:main"
[project.urls]
Homepage = "https://github.com/microsoft/SkillOpt"
@@ -64,9 +68,13 @@ Repository = "https://github.com/microsoft/SkillOpt"
Issues = "https://github.com/microsoft/SkillOpt/issues"
[tool.setuptools.packages.find]
# skillopt* = the research package; skillopt_sleep = the open-source Sleep tool
# (decoupled, zero dependency on the research code).
include = ["skillopt", "skillopt.*", "skillopt_sleep", "skillopt_sleep.*", "scripts*"]
# skillopt* = the research package
# skillopt_sleep = the open-source Sleep tool (decoupled, zero research dep)
# skillopt_webui = the Gradio dashboard (installed via the `webui` extra)
include = ["skillopt", "skillopt.*", "skillopt_sleep", "skillopt_sleep.*", "skillopt_webui", "skillopt_webui.*", "scripts*"]
[tool.setuptools.package-data]
"*" = ["*.md"]
[tool.ruff]
line-length = 120
+6
View File
@@ -17,6 +17,12 @@ httpx>=0.27.0
# ── Optional: Qwen local model (via vLLM) ────────
# vllm>=0.4.0
# ── Optional: tolerant JSON repair for free-form output from non-OpenAI
# backends (Claude/Qwen). Without it extract_json() falls back safely and
# drops a malformed analyst edit instead of repairing it. Installed by the
# `claude`, `qwen`, and `all` extras in pyproject.toml.
# json_repair>=0.61.0
# ── Optional: WebUI dashboard ────────────────────
# gradio>=4.0.0
+55 -3
View File
@@ -28,6 +28,8 @@ from skillopt.model import (
configure_azure_openai,
configure_claude_code_exec,
configure_codex_exec,
configure_qwen_chat,
configure_minimax_chat,
set_reasoning_effort,
set_target_backend,
set_target_deployment,
@@ -137,7 +139,7 @@ def parse_args() -> argparse.Namespace:
# Legacy flat overrides
p.add_argument("--env", type=str)
p.add_argument("--backend", type=str,
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec"])
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "minimax", "minimax_chat"])
p.add_argument("--optimizer_model", type=str)
p.add_argument("--target_model", type=str)
p.add_argument("--optimizer_backend", type=str)
@@ -179,6 +181,12 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--claude_code_exec_use_sdk", type=str)
p.add_argument("--claude_code_exec_effort", type=str)
p.add_argument("--claude_code_exec_max_thinking_tokens", type=int)
p.add_argument("--minimax_base_url", type=str)
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
p.add_argument("--minimax_temperature", type=float)
p.add_argument("--minimax_max_tokens", type=int)
p.add_argument("--minimax_enable_thinking", type=_BOOL)
p.add_argument("--out_root", type=str)
p.add_argument("--data_path", type=str)
p.add_argument("--split_mode", type=str,
@@ -254,6 +262,12 @@ def main() -> None:
"claude_code_exec_use_sdk": "model.claude_code_exec_use_sdk",
"claude_code_exec_effort": "model.claude_code_exec_effort",
"claude_code_exec_max_thinking_tokens": "model.claude_code_exec_max_thinking_tokens",
"minimax_base_url": "model.minimax_base_url",
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
"minimax_temperature": "model.minimax_temperature",
"minimax_max_tokens": "model.minimax_max_tokens",
"minimax_enable_thinking": "model.minimax_enable_thinking",
"seed": "train.seed",
"test_env_num": "evaluation.test_env_num",
"env": "env.name",
@@ -306,11 +320,16 @@ def main() -> None:
cfg.setdefault("optimizer_backend", "claude_chat")
cfg.setdefault("target_backend", "claude_chat")
elif backend in {"codex", "codex_exec"}:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "codex_exec")
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
cfg["optimizer_backend"] = "codex_exec"
if not _has_model_override("model.target_backend", "target_backend"):
cfg["target_backend"] = "codex_exec"
elif backend == "claude_code_exec":
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "claude_code_exec")
elif backend in {"minimax", "minimax_chat"}:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "minimax_chat")
else:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "openai_chat")
@@ -336,6 +355,15 @@ def main() -> None:
and not _has_model_override("model.target", "target_model")
):
cfg["target_model"] = default_model_for_backend("claude_chat")
if cfg.get("target_backend") == "minimax_chat":
if (
str(cfg.get("target_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS
and not _has_model_override("model.target", "target_model")
):
cfg["target_model"] = (
cfg.get("minimax_model")
or default_model_for_backend("minimax_chat")
)
if not cfg.get("out_root"):
env = cfg.get("env", "unknown")
@@ -401,6 +429,30 @@ def main() -> None:
effort=cfg.get("claude_code_exec_effort", cfg.get("reasoning_effort", "medium")),
max_thinking_tokens=cfg.get("claude_code_exec_max_thinking_tokens", 16384),
)
configure_qwen_chat(
base_url=cfg.get("qwen_chat_base_url") or None,
api_key=cfg.get("qwen_chat_api_key") or None,
temperature=cfg.get("qwen_chat_temperature"),
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
max_tokens=cfg.get("qwen_chat_max_tokens"),
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
target_base_url=cfg.get("target_qwen_chat_base_url") or None,
target_api_key=cfg.get("target_qwen_chat_api_key") or None,
target_temperature=cfg.get("target_qwen_chat_temperature"),
target_timeout_seconds=cfg.get("target_qwen_chat_timeout_seconds"),
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
)
configure_minimax_chat(
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
temperature=cfg.get("minimax_temperature"),
max_tokens=cfg.get("minimax_max_tokens"),
enable_thinking=cfg.get("minimax_enable_thinking"),
)
minimax_model_cfg = cfg.get("minimax_model")
if minimax_model_cfg and cfg.get("target_backend") == "minimax_chat":
set_target_deployment(str(minimax_model_cfg))
set_reasoning_effort(cfg.get("reasoning_effort", "") or None)
# Build adapter
+170
View File
@@ -0,0 +1,170 @@
"""Materialize runnable SearchQA splits from the released ID manifest."""
from __future__ import annotations
import argparse
import json
from collections.abc import Iterable, Mapping
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
SPLITS = ("train", "val", "test")
REQUIRED_FIELDS = ("question", "context", "answers")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--manifest-dir",
type=Path,
default=PROJECT_ROOT / "data" / "searchqa_id_split",
help="Directory containing train/val/test ID manifests.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=PROJECT_ROOT / "data" / "searchqa_split",
help="Directory to write runnable train/val/test splits.",
)
parser.add_argument(
"--dataset",
default="lucadiliello/searchqa",
help="Hugging Face dataset repository to load.",
)
return parser.parse_args()
def load_manifest_ids(manifest_dir: Path) -> dict[str, list[str]]:
split_ids = {}
for split in SPLITS:
path = manifest_dir / split / "items.json"
with path.open(encoding="utf-8") as file:
items = json.load(file)
split_ids[split] = [str(item["id"]) for item in items]
return split_ids
def _reject_duplicate_manifest_ids(split_ids: Mapping[str, Iterable[str]]) -> None:
seen: dict[str, str] = {}
duplicates: dict[str, list[str]] = {}
for split, ids in split_ids.items():
for item_id in ids:
if item_id in seen:
duplicates.setdefault(item_id, [seen[item_id]]).append(split)
else:
seen[item_id] = split
if duplicates:
preview = ", ".join(
f"{item_id} ({'/'.join(splits)})"
for item_id, splits in sorted(duplicates.items())[:5]
)
raise ValueError(
"SearchQA split manifest contains duplicate IDs across splits. "
f"First IDs: {preview}"
)
def _iter_dataset_rows(dataset: Mapping[str, Iterable[dict]]) -> Iterable[dict]:
for source_split in dataset.values():
yield from source_split
def _normalize_row(row: dict) -> dict:
try:
key = str(row["key"])
except KeyError as exc:
raise ValueError("SearchQA source row is missing required field: key") from exc
missing = [field for field in REQUIRED_FIELDS if field not in row]
if missing:
raise ValueError(f"SearchQA source row {key!r} is missing required fields: {', '.join(missing)}")
return {
"id": key,
"question": row["question"],
"context": row["context"],
"answers": row["answers"],
}
def materialize_searchqa_splits(
manifest_dir: Path,
output_dir: Path,
dataset: Mapping[str, Iterable[dict]],
*,
dataset_name: str,
) -> dict[str, int]:
"""Write runnable SearchQA train/val/test splits from a source dataset."""
manifest_dir = manifest_dir.resolve()
output_dir = output_dir.resolve()
split_ids = load_manifest_ids(manifest_dir)
_reject_duplicate_manifest_ids(split_ids)
wanted_ids = {item_id for ids in split_ids.values() for item_id in ids}
selected: dict[str, dict] = {}
duplicate_ids: set[str] = set()
for row in _iter_dataset_rows(dataset):
key = str(row.get("key", ""))
if key not in wanted_ids:
continue
if key in selected:
duplicate_ids.add(key)
continue
selected[key] = _normalize_row(row)
if duplicate_ids:
preview = ", ".join(sorted(duplicate_ids)[:5])
raise ValueError(f"SearchQA source dataset contains duplicate manifest IDs. First IDs: {preview}")
missing = sorted(wanted_ids - selected.keys())
if missing:
preview = ", ".join(missing[:5])
raise RuntimeError(f"SearchQA source dataset is missing {len(missing)} manifest IDs. First IDs: {preview}")
counts = {}
for split, ids in split_ids.items():
items = [selected[item_id] for item_id in ids]
split_dir = output_dir / split
split_dir.mkdir(parents=True, exist_ok=True)
with (split_dir / "items.json").open("w", encoding="utf-8") as file:
json.dump(items, file, ensure_ascii=False, indent=2)
counts[split] = len(items)
manifest = {
"source_manifest_dir": str(manifest_dir),
"source_dataset": dataset_name,
"counts": counts,
"item_fields": ["id", *REQUIRED_FIELDS],
}
with (output_dir / "split_manifest.json").open("w", encoding="utf-8") as file:
json.dump(manifest, file, ensure_ascii=False, indent=2)
return counts
def main() -> None:
args = parse_args()
try:
from datasets import load_dataset
except ImportError as exc:
raise SystemExit(
"Missing dependency 'datasets'. Install it with:\n"
" python -m pip install 'skillopt[searchqa]'\n"
"or:\n"
" python -m pip install datasets"
) from exc
print(f"Loading {args.dataset}...")
dataset = load_dataset(args.dataset)
counts = materialize_searchqa_splits(
args.manifest_dir,
args.output_dir,
dataset,
dataset_name=args.dataset,
)
print(f"Wrote SearchQA splits to {args.output_dir.resolve()}: {counts}")
if __name__ == "__main__":
main()
+4 -2
View File
@@ -438,8 +438,10 @@ def load_config(args: argparse.Namespace) -> dict:
flat.setdefault("optimizer_backend", "claude_chat")
flat.setdefault("target_backend", "claude_chat")
elif backend in {"codex", "codex_exec"}:
flat.setdefault("optimizer_backend", "openai_chat")
flat.setdefault("target_backend", "codex_exec")
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
flat["optimizer_backend"] = "codex_exec"
if not _has_model_override("model.target_backend", "target_backend"):
flat["target_backend"] = "codex_exec"
elif backend == "claude_code_exec":
flat.setdefault("optimizer_backend", "openai_chat")
flat.setdefault("target_backend", "claude_code_exec")
+9 -10
View File
@@ -1778,6 +1778,8 @@
<a href="#evolution">Evolution</a>
<a href="#transfer">Transfer</a>
<a href="#citation">Citation</a>
<a href="https://microsoft.github.io/SkillOpt/blog/">Blog</a>
<a href="https://github.com/microsoft/SkillOpt/blob/main/docs/index.md" target="_blank" rel="noopener">Docs</a>
<a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a>
</nav>
</header>
@@ -1915,7 +1917,7 @@
<h3>A skill is external state for an agent.</h3>
<p>
Instead of fine-tuning a model or hand-maintaining prompts, SkillOpt runs
the frozen agent on scored batches, asks a separate optimizer model to
the frozen agent on scored batches, asks an optimizer model to
propose structured edits, and accepts a candidate only when validation
performance improves.
</p>
@@ -2416,21 +2418,18 @@
<div class="bibtex-box">
<button class="copy-btn" type="button" onclick="copyBibtex(this)">Copy</button>
<pre><code>@misc{yang2026skilloptexecutivestrategyselfevolving,
title={SkillOpt: Executive Strategy for Self-Evolving Agent Skills},
author={Yifan Yang and Ziyang Gong and Weiquan Huang and Qihao Yang and Ziwei Zhou and Zisu Huang and Yan Li and Xuemei Gao and Qi Dai and Bei Liu and Kai Qiu and Yuqing Yang and Dongdong Chen and Xue Yang and Chong Luo},
year={2026},
eprint={2605.23904},
archivePrefix={arXiv},
primaryClass={cs.AI},
url={https://arxiv.org/abs/2605.23904},
<pre><code>@article{yang2026skillopt,
title={Skillopt: Executive strategy for self-evolving agent skills},
author={Yang, Yifan and Gong, Ziyang and Huang, Weiquan and Yang, Qihao and Zhou, Ziwei and Huang, Zisu and Li, Yan and Gao, Xuemei and Dai, Qi and Liu, Bei and others},
journal={arXiv preprint arXiv:2605.23904},
year={2026}
}</code></pre>
</div>
</section>
<footer class="footer">
<span>SkillOpt: Executive Strategy for Self-Evolving Agent Skills</span>
<span><a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a> / <a href="#citation">Citation</a></span>
<span><a href="https://microsoft.github.io/SkillOpt/blog/">Blog</a> / <a href="https://github.com/microsoft/SkillOpt/blob/main/docs/index.md" target="_blank" rel="noopener">Docs</a> / <a href="https://github.com/microsoft/SkillOpt" target="_blank" rel="noopener">Code</a> / <a href="#citation">Citation</a></span>
</footer>
</main>
<script>
+1 -1
View File
@@ -12,7 +12,7 @@ Pipeline stages:
6. Evaluate validate candidate skill, accept/reject
"""
__version__ = "0.1.0"
__version__ = "0.2.0"
from skillopt.types import ( # noqa: F401
BatchSpec,
+4 -1
View File
@@ -125,6 +125,9 @@ _FLATTEN_MAP: dict[str, str] = {
"evaluation.use_gate": "use_gate",
"evaluation.gate_metric": "gate_metric",
"evaluation.gate_mixed_weight": "gate_mixed_weight",
"evaluation.use_semantic_density": "use_semantic_density",
"evaluation.semantic_density_weight": "semantic_density_weight",
"evaluation.leading_words": "leading_words",
"evaluation.sel_env_num": "sel_env_num",
"evaluation.test_env_num": "test_env_num",
"evaluation.eval_test": "eval_test",
@@ -158,7 +161,7 @@ def _load_yaml(path: str, _visited: set[str] | None = None) -> dict:
raise ValueError(f"Circular _base_ inheritance: {abs_path}")
_visited.add(abs_path)
with open(abs_path) as f:
with open(abs_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
base_ref = cfg.pop("_base_", None)
+58 -26
View File
@@ -670,12 +670,14 @@ class ReflACTTrainer:
optimizer_backend = cfg.get("optimizer_backend")
target_backend = cfg.get("target_backend")
if not optimizer_backend or not target_backend:
if backend in {"claude", "claude_chat"}:
optimizer_backend = optimizer_backend or "claude_chat"
target_backend = target_backend or "claude_chat"
elif backend in {"codex", "codex_exec"}:
optimizer_backend = optimizer_backend or "openai_chat"
target_backend = target_backend or "codex_exec"
if backend in {"claude", "claude_chat"}:
optimizer_backend = optimizer_backend or "claude_chat"
target_backend = target_backend or "claude_chat"
elif backend in {"codex", "codex_exec"}:
if optimizer_backend in (None, "", "openai_chat"):
optimizer_backend = "codex_exec"
if target_backend in (None, "", "openai_chat"):
target_backend = "codex_exec"
elif backend == "claude_code_exec":
optimizer_backend = optimizer_backend or "openai_chat"
target_backend = target_backend or "claude_code_exec"
@@ -709,26 +711,26 @@ class ReflACTTrainer:
effort=cfg.get("claude_code_exec_effort", cfg.get("reasoning_effort", "medium")),
max_thinking_tokens=cfg.get("claude_code_exec_max_thinking_tokens", 16384),
)
configure_qwen_chat(
base_url=cfg.get("qwen_chat_base_url") or None,
api_key=cfg.get("qwen_chat_api_key") or None,
temperature=cfg.get("qwen_chat_temperature"),
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
max_tokens=cfg.get("qwen_chat_max_tokens"),
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
optimizer_base_url=cfg.get("optimizer_qwen_chat_base_url") or None,
optimizer_api_key=cfg.get("optimizer_qwen_chat_api_key") or None,
optimizer_temperature=cfg.get("optimizer_qwen_chat_temperature"),
optimizer_timeout_seconds=cfg.get("optimizer_qwen_chat_timeout_seconds"),
optimizer_max_tokens=cfg.get("optimizer_qwen_chat_max_tokens"),
optimizer_enable_thinking=cfg.get("optimizer_qwen_chat_enable_thinking"),
target_base_url=cfg.get("target_qwen_chat_base_url") or None,
target_api_key=cfg.get("target_qwen_chat_api_key") or None,
target_temperature=cfg.get("target_qwen_chat_temperature"),
target_timeout_seconds=cfg.get("target_qwen_chat_timeout_seconds"),
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
)
configure_qwen_chat(
base_url=cfg.get("qwen_chat_base_url") or None,
api_key=cfg.get("qwen_chat_api_key") or None,
temperature=cfg.get("qwen_chat_temperature"),
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
max_tokens=cfg.get("qwen_chat_max_tokens"),
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
optimizer_base_url=cfg.get("optimizer_qwen_chat_base_url") or None,
optimizer_api_key=cfg.get("optimizer_qwen_chat_api_key") or None,
optimizer_temperature=cfg.get("optimizer_qwen_chat_temperature"),
optimizer_timeout_seconds=cfg.get("optimizer_qwen_chat_timeout_seconds"),
optimizer_max_tokens=cfg.get("optimizer_qwen_chat_max_tokens"),
optimizer_enable_thinking=cfg.get("optimizer_qwen_chat_enable_thinking"),
target_base_url=cfg.get("target_qwen_chat_base_url") or None,
target_api_key=cfg.get("target_qwen_chat_api_key") or None,
target_temperature=cfg.get("target_qwen_chat_temperature"),
target_timeout_seconds=cfg.get("target_qwen_chat_timeout_seconds"),
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
)
configure_minimax_chat(
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
@@ -964,6 +966,15 @@ class ReflACTTrainer:
f"got {gate_metric!r}"
)
gate_mixed_weight = float(cfg.get("gate_mixed_weight", 0.5))
use_semantic_density = bool(cfg.get("use_semantic_density", False))
semantic_density_weight = float(cfg.get("semantic_density_weight", 0.05))
leading_words_raw = cfg.get("leading_words", None)
leading_words = None
if leading_words_raw is not None:
if isinstance(leading_words_raw, str):
leading_words = [w.strip() for w in leading_words_raw.split(",") if w.strip()]
else:
leading_words = list(leading_words_raw)
if not 0.0 <= gate_mixed_weight <= 1.0:
raise ValueError(
f"evaluation.gate_mixed_weight must be in [0, 1], "
@@ -1003,6 +1014,10 @@ class ReflACTTrainer:
baseline_hard, baseline_soft = compute_score(baseline_results)
current_score = select_gate_score(
baseline_hard, baseline_soft, gate_metric, gate_mixed_weight,
skill_content=skill_init,
use_semantic_density=use_semantic_density,
semantic_density_weight=semantic_density_weight,
leading_words=leading_words,
)
best_score = current_score
sh = skill_hash(skill_init)
@@ -1450,9 +1465,16 @@ class ReflACTTrainer:
cand_soft=cand_soft,
metric=gate_metric,
mixed_weight=gate_mixed_weight,
use_semantic_density=use_semantic_density,
semantic_density_weight=semantic_density_weight,
leading_words=leading_words,
) if use_gate else None
cand_gate_score = select_gate_score(
cand_hard, cand_soft, gate_metric, gate_mixed_weight,
skill_content=candidate_skill,
use_semantic_density=use_semantic_density,
semantic_density_weight=semantic_density_weight,
leading_words=leading_words,
)
if not use_gate:
# Validation ran (scores recorded above) but the gate is
@@ -1844,6 +1866,9 @@ class ReflACTTrainer:
cand_soft=slow_sel_soft,
metric=gate_metric,
mixed_weight=gate_mixed_weight,
use_semantic_density=use_semantic_density,
semantic_density_weight=semantic_density_weight,
leading_words=leading_words,
)
slow_result["selection_hard"] = slow_sel_hard
slow_result["selection_soft"] = slow_sel_soft
@@ -2093,6 +2118,10 @@ class ReflACTTrainer:
final_gate_score = select_gate_score(
final_selection_hard, final_selection_soft,
gate_metric, gate_mixed_weight,
skill_content=current_skill,
use_semantic_density=use_semantic_density,
semantic_density_weight=semantic_density_weight,
leading_words=leading_words,
)
print(
f"\n [final skill val] items={fval_n} "
@@ -2133,6 +2162,7 @@ class ReflACTTrainer:
)
print(f" Test items: {test_n}")
baseline_test_dir = os.path.join(out_root, "test_eval_baseline")
os.makedirs(baseline_test_dir, exist_ok=True)
baseline_test_results = adapter.rollout(test_env, skill_init, baseline_test_dir)
baseline_test_hard, baseline_test_soft = compute_score(baseline_test_results)
baseline_buckets = _compute_task_type_buckets(baseline_test_results, task_types)
@@ -2167,6 +2197,7 @@ class ReflACTTrainer:
)
print(f" Test items: {test_n2}")
test_dir = os.path.join(out_root, "test_eval")
os.makedirs(test_dir, exist_ok=True)
test_results = adapter.rollout(test_env2, best_skill, test_dir)
test_hard, test_soft = compute_score(test_results)
best_buckets = _compute_task_type_buckets(test_results, task_types)
@@ -2230,6 +2261,7 @@ class ReflACTTrainer:
)
print(f" Test items: {test_n3}")
final_test_dir = os.path.join(out_root, "test_eval_final")
os.makedirs(final_test_dir, exist_ok=True)
final_test_results = adapter.rollout(test_env3, current_skill, final_test_dir)
final_test_hard, final_test_soft = compute_score(final_test_results)
final_buckets = _compute_task_type_buckets(final_test_results, task_types)
+10 -7
View File
@@ -28,13 +28,16 @@ This directory provides scaffold files for adding a new benchmark to SkillOpt.
`TemplateBenchmarkLoader → YourBenchmarkLoader`)
and fix the cross-import in `adapter.py`.
3. **Implement the TODO blocks** inside `adapter.py:rollout` and the
`_normalize_item` helper in `dataloader.py`. (`reflect` is inherited from
`EnvAdapter`; override it only for custom reflection logic.)
4. **Register** the adapter — add a `try / except ImportError` block in
`scripts/train.py`'s `_register_builtins()` mapping the registry key
to your `YourBenchmarkAdapter` class. There is no
`BENCHMARK_REGISTRY` dict in `skillopt/envs/__init__.py`; the live
registry is `_ENV_REGISTRY` in `scripts/train.py`.
`_normalize_item` helper in `dataloader.py`. In addition to returning
`id`/`hard`/`soft`, persist each non-empty trajectory at
`<out_dir>/predictions/<id>/conversation.json`; the inherited `reflect`
method reads those files. Override `reflect` only for custom reflection
logic.
4. **Register** the adapter — add matching `try / except ImportError` blocks
to `_register_builtins()` in both `scripts/train.py` and
`scripts/eval_only.py`, mapping the registry key to your
`YourBenchmarkAdapter` class. There is no `BENCHMARK_REGISTRY` dict in
`skillopt/envs/__init__.py`; each CLI keeps its own lazy `_ENV_REGISTRY`.
5. **Create the config** at `configs/your_benchmark/default.yaml`
(start from `config_template.yaml`). `_base_` is a **string path**,
not a list.
+2 -2
View File
@@ -50,6 +50,6 @@ evaluation:
# ── Model ────────────────────────────────────────
# Override only what differs from the inherited defaults.
model:
optimizer_backend: openai_chat # openai_chat | claude_chat | qwen_chat | minimax_chat
target_backend: openai_chat # plus codex_exec / claude_code_exec for target only
optimizer_backend: openai_chat # openai_chat | claude_chat | qwen_chat | minimax_chat | codex_exec
target_backend: openai_chat # chat backends plus codex_exec / claude_code_exec
reasoning_effort: medium

Some files were not shown because too many files have changed in this diff Show More