Commit Graph

257 Commits

Author SHA1 Message Date
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
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>
v0.2.0
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