* Add DeepSeek Harness (dsh) integration
New plugins/dsh/ integration wrapping the shared skillopt_sleep engine
for DeepSeek Harness: a Cordis plugin registering 7 native skillopt_*
tools (status/dry-run/run/adopt/harvest/schedule/unschedule), a bundled
SKILL.md, a bundle patch layer (cordis.patch.yml), and a bootstrap
script. Register the plugin in the plugins/README.md integration table.
* Fix dsh integration per review: safe argv, operator-only auto-adopt, parity tests, English skill
Addresses all review points from the SkillOpt maintainer.
Blocker 1 — shell injection / broken documented example:
- Replace buildCommand() (string join, no quoting) with buildArgv() returning
an argv array; execute() quotes every element with the POSIX-safe '\'' spelling
before shell.resolve(). Model/config-controlled values (project, model,
preferences, source) cannot break out of their argument — verified with a
real-bash injection audit (7 payloads). The documented preferences example
now round-trips as one argument.
- Resolve scripts/sleep.py via an absolute path from the plugin dir so it works
regardless of the dsh cwd.
Blocker 2 — auto-adopt no longer model-callable:
- autoAdopt was a model-facing tool parameter forwarding --auto-adopt. Moved to
operator-only config (default false); the tool parameter is removed. The
canary asserts a model-supplied autoAdopt is ignored.
Should fix — plugin registry parity test:
- Register dsh SKILL.md in tests/test_plugin_sync.py PLUGIN_SKILL_MDS. The
parity tests now cover dsh (backends, schedule/unschedule, memory
consolidation). 13/13 pass.
Minor — English-first skill doc:
- SKILL.md rewritten in English; Chinese README stays as README.zh.md.
Runtime correctness (from the first review round):
- execute() goes through shell.resolve() so workdir/output-cap/sandbox defaults apply.
- Consumes rc.8 CollectedOutput { text, truncated, spillPath }; distinguishes
timeout (exit=timeout) from abort (exit=signal).
- package.json includes cordis.patch.yml in files and declares schemastery.
- scripts/sleep.py mirrors the official runner (repo-root resolution, Python >=
3.10 selection, CLI/installed-package fallback).
- New scripts/canary.mjs: pack + load + invoke checks.
Tested locally: canary 21 checks, real-bash quoting 10 checks, real-DSH (rc.6)
13 checks, repo parity 13/13 — no regressions, nothing touches the shared engine.
* Add LICENSE, portable test scripts; align README.zh.md and pack files with the established plugin pattern
* Security: strip control chars in argv quoting (defense in depth)
Model-controlled values containing \\r, \\r\\n or other control characters
would split a single-quoted word into multiple argv words (broken command,
not RCE — quotes never execute), and corrupt the engine's arg parsing. Strip
C0 control characters to a space so every value arrives as exactly one
argument. Verified: new audit-control-chars.mjs covers \\n, \\r, \\r\\n, tab,
NUL, backtick, quotes — all neutralized (single arg, no file, no execution).
* Fix dsh install command in READMEs: dsh is a global CLI, not a pnpm dependency
The previous form 'pnpm dsh web --patch ...' made pnpm try to fetch a
nonexistent @deepseek-ai/dsh-type-meta package and fail with 404. dsh is
installed as a global CLI; the correct overlay invocation is
'dsh web --patch ./plugins/dsh/cordis.patch.yml' (verified with --dump-config).
* Security: enforce per-tool parameter whitelist (block undeclared arg injection)
dsh's parameter schema accepts undeclared properties by default (no
additionalProperties:false), and buildArgv() forwarded both model-supplied
values and operator config defaults for every known key to the engine. A
model (or prompt-injected transcript) could therefore pass backend/model/
json/editBudget/etc. to tools that do not declare them — including
skillopt_adopt, the live-change boundary.
- buildArgv() now takes an explicit per-tool llowed key set; keys outside
it are neither read from args nor filled from config defaults.
- Each tool's build() passes exactly the keys it declares (whitelist).
- canary.mjs: new 7b step asserts adopt drops undeclared backend/model/
maxTasks/json while keeping declared project; step 4 now drives the
nonzero-exit path via preferences (a declared run parameter).
- audit-*.mjs: BASH_PATH env override for non-Windows portability.
* Security: value-domain guard for path params; unschedule --all is operator-only
The engine re-interpolates model-supplied values into its OWN shell command
strings: scheduler.py splices --project into a crontab line and a Windows
run.cmd executed by schtasks (no escaping), and write_tasks_file() turns an
arbitrary --output into abspath+makedirs+overwrite. argv-level quoting in the
plugin protects the dsh bash -c boundary but cannot protect those secondarysplices. A model-controlled project containing shell metacharacters (quote,
ampersand, semicolon, pipe, dollar, backtick, angle brackets, braces, glob,
control chars) would break out and execute as a separate command under thescheduler shell; an absolute or traversal output would overwrite an arbitrary
file.
- assertSafePath(): rejects shell metacharacters in project and output values.
- assertSafeOutput(): refuses absolute paths and .. traversal for --output.
- execute() runs both guards before buildArgv, so a bad value never reaches
the engine; the rejection is returned to the model as tool output.
- skillopt_unschedule: removed model-callable --all; now operator-only via
config.unscheduleAll (same pattern as autoAdopt).
- canary.mjs: new 7c step asserts injected project / absolute / traversal
output are rejected and legit paths pass (32 checks total).
* Security: clock range guard for schedule; pin dependency versions
- schedule hour/minute were spliced by the engine into a crontab line and a
schtasks start time without validation; out-of-range values (99, -1) would
create broken scheduled entries. execute() now enforces hour in [0,23] and
minute in [0,59] before building argv.
- package.json: replace bare '*' dependency ranges with known-good pinned
versions (@deepseek-ai/schemastery ^3.18.1, cordis ^4.0.1, dsh-tools
^0.1.0-rc.8) so installs are reproducible and not silently broken by a
future upstream release.
- canary.mjs: new 7d step asserts hour=99 / minute=-1 are rejected and legit
clock values pass (35 checks total).
* Align with DSH ecosystem plugin conventions; document both patch-invocation forms
- package.json: add peerDependenciesMeta marking @deepseek-ai/cordis and
@deepseek-ai/dsh-tools optional, matching the official ecosystem practice
(dsh-office-tools et al. declare host-provided peers optional). Without it a
plain 'npm install dsh-skillopt' would hard-fail when the host DSH version
differs from the pinned peer range, instead of warning.
- README.md / plugins/README.md: document BOTH overlay forms - 'pnpm dsh web
--patch' for a DeepSeek Harness source checkout (the official dev workflow)
and 'dsh web --patch' for a globally installed dsh.
* Docs: fix parameter name in SKILL.md (maxTasks, not max_tasks)
The skill's parameter table listed max_tasks (snake_case) but the tools declare
maxTasks (camelCase); a model following the skill doc would send max_tasks and be
rejected by dsh's parameter validation (undeclared property).
* Canary: actually pack + extract and load the packed bundle (review requirement)
The review asked for a clean-package canary that 'loads the packed bundle'.
The previous canary verified the pack file list via --dry-run but then imported
the plugin from the source tree. It now runs 'npm pack --json', extracts the
tarball, and loads src/index.js FROM THE EXTRACTED package/ artifact for every
step (register, status, error paths, quoting, whitelist, value guard, clock),
so the artifact under test is exactly what the 'files' list ships. Tarball and
scratch dir are removed on exit.
* Docs: complete README config keys table (all schema keys, corrected module default)
The config keys table now lists every Config schema key (added engineScript,
scope, autoAdopt, unscheduleAll, timeoutMs) and no longer claims module defaults
to 'skillopt_sleep' (the default path is the scripts/sleep.py bootstrap; module
is an explicit override).
---------
Co-authored-by: WODE25500 <WODE25500@users.noreply.github.com>
17 KiB
SkillOpt-Sleep integrations
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.
The shared engine lives in skillopt_sleep/ and has no
runtime dependency on the paper's skillopt/ experiment package.
Available integrations
Six integrations wrap the shared skillopt_sleep CLI. OpenClaw is a separate
reference adaptation with its own backend and setup assumptions.
| Platform | Folder | Mechanism | Status |
|---|---|---|---|
| Claude Code | claude-code/ |
marketplace plugin, commands, skill, and hooks | installable shared-engine integration |
| Codex | codex/ |
user-level skill and shared runner | installable shared-engine integration |
| Cursor | cursor/ |
native command and skill, project skill target, and shared runner | installable shared-engine integration |
| GitHub Copilot | copilot/ |
MCP server exposing seven sleep_* tools |
shared-engine MCP integration |
| Devin | devin/ |
MCP server plus Devin transcript conversion | shared-engine MCP integration |
| DeepSeek Harness | dsh/ |
Cordis plugin: 7 native skillopt_* tools, skill, bundle patch layer |
installable shared-engine integration |
| 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 | 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 |
| Cursor | bash plugins/cursor/install.sh (macOS/Linux) or powershell -File plugins/cursor/install.ps1 (Windows) |
/skillopt-sleep status |
| 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 |
| DeepSeek Harness | add dsh-skillopt to the profile's bundles, or patch it in — from a DSH source checkout: pnpm dsh web --patch ./plugins/dsh/cordis.patch.yml; with global dsh: dsh web --patch ./plugins/dsh/cordis.patch.yml |
ask the agent to use skillopt_status |
| OpenClaw | follow and adapt openclaw/README.md |
validate paths, credentials, and tasks locally |
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 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.
Version note. This integration reference tracks
main. PyPI 0.2.0 supports the base Sleep CLI, while Cursor source/backend/plugin support, Pi source/backend support, handoff, Sleep support for non-Azure OpenAI-compatible endpoints, OpenCode Sleep source/backend support, and--preferences, multi-skill fan-out, and reviewed subset adoption require a source checkout frommainuntil the next release.
One sleep cycle
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
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.
Data boundary
-
Harvesting is local and read-only. The
mockandhandoffbackends make no network calls; handoff writes prompts for separate, user-controlled completion. -
A real backend sends mining, replay, judging, and reflection prompts derived from truncated transcript excerpts and tasks to the selected provider.
-
The Cursor source reads local user/assistant message text, explicit turn errors, and tool names from
~/.cursor/projects/*/agent-transcripts; it does not retain tool arguments, tool outputs, or other record types. Known secret-shaped strings are redacted, but this is defense in depth rather than a guarantee that outbound prompts are secret-free. -
The Cursor backend sends prompts through the installed, authenticated
cursor-agentCLI. Ordinary calls use read-only Ask mode in a new empty temporary workspace with project file access denied. Cursor tasks containingtool_calledvalidation fail before Agent mode starts; use another backend for those tasks. Cursor and the model provider selected by Cursor can receive the resulting prompt content. -
The Pi backend sends prompts through the installed, authenticated Pi CLI to the provider configured by the user. It disables tools, skills, context files, extensions, prompt templates, themes, and session writes for these calls, but retains the user's Pi authentication and model configuration. Pi's offline startup mode prevents configured npm/git package installation, package updates, and model-catalog refresh; it does not prevent the selected provider call. These controls are not a guarantee of permanent or complete isolation.
-
The Pi source retains user/assistant text, tool names, and lexical feedback found in user text. It excludes thinking, tool arguments, tool outputs, images, and unrelated metadata. The absolute project
cwdfrom the session header is retained for scope filtering and may appear in miner prompts sent to a real backend and its provider. Known secret-shaped strings in retained message text are redacted only as defense in depth. -
The core
opencodesource reads local OpenCode SQLite history without the CLI, authentication, or provider access. See the CLI reference for its retained-data boundary. -
The core
opencodebackend uses the installed OpenCode CLI. Plain calls disable project configuration, model-initiated tool invocation, external plugins, and configured MCP servers. Tool-aware replay requires explicit opt-in. It exposes only temporary synthetic tools with randomized names and fixed results, then verifies which tools OpenCode actually invoked. See the CLI reference for history and isolation details. A native OpenCode plugin or command is not included. -
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 totrue, and then use the task file with a real backend: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 --progressReal backends reject task files that are still marked unreviewed.
For the separate API-key and Azure managed-identity transport boundaries, see OpenAI-compatible endpoints.
Supported CLI surface
Actions:
| Action | Behavior |
|---|---|
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 |
Common implemented flags include:
| Flag | Default | Purpose |
|---|---|---|
--backend mock|claude|codex|cursor|copilot|pi|opencode|handoff|azure_openai |
mock |
select who performs model calls |
--model NAME |
backend default | select a backend-specific model |
--source claude|codex|copilot|cursor|pi|opencode|auto |
claude |
select the transcript source; auto retains Codex-then-Claude precedence and does not select Copilot, Cursor, Pi, or OpenCode |
--cursor-home PATH |
~/.cursor |
override the Cursor transcript home |
--cursor-path PATH |
auto-detect cursor-agent |
select the Cursor Agent CLI executable |
--pi-home PATH |
~/.pi |
select the parent directory containing agent/sessions |
--pi-path PATH |
auto-detect pi |
select the Pi coding-agent CLI executable |
--opencode-path PATH |
SKILLOPT_SLEEP_OPENCODE_PATH, then opencode on PATH/PATHEXT |
select the OpenCode CLI executable |
--opencode-db PATH |
OPENCODE_DB, %LOCALAPPDATA%/%APPDATA% (Windows), or ${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db |
select the OpenCode SQLite history database |
--opencode-tool-replay |
off | enable OpenCode tool-aware replay for tool_called checks in rule judges |
--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 |
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.
For the Cursor backend, --project also selects target files, state, and the
staging location, but it does not make that directory the Cursor Agent execution
workspace. The target skill is inserted as prompt text rather than invoked as a
native skill. Real-backend dry-run performs the same mining and replay model
calls while suppressing staging, adoption, and persisted state changes. The
current Sleep cycle does not implement fresh-worktree replay; a replay: mock
report label describes prompt replay and is independent of --backend mock.
Preferences
--preferences is the main user-facing steering knob:
python -m skillopt_sleep run --backend codex --project "$(pwd)" \
--preferences "Prefer pytest. Keep commit subjects imperative and concise."
Preferences guide reflection but remain subject to the validation gate.
Pi source and backend
Pi transcript harvesting is explicit: --source pi reads session JSONL files
below ~/.pi/agent/sessions; use --pi-home to select the parent directory
that contains agent/sessions. This source does not require the Pi CLI or
provider authentication. It retains user/assistant text, tool names, and lexical
feedback found in user text, while excluding thinking, tool arguments, tool
outputs, images, and unrelated metadata. The absolute project cwd from the
session header is retained for scope filtering and may appear in miner prompts
sent to a real backend and its provider. Known secret-shaped strings in retained
message text are redacted as defense in depth, not as a guarantee. --source auto keeps Codex-then-Claude
precedence and does not select Pi.
The source and backend are independent. --backend pi uses a locally installed,
authenticated Pi CLI to make real model-provider calls for mining, replay,
judging, and reflection. Select another executable with --pi-path and a model
with --model:
python -m skillopt_sleep run --project "$(pwd)" \
--source pi --backend pi --pi-path /absolute/path/to/pi \
--model provider/model --max-sessions 5 --max-tasks 3 --progress
Pi calls disable tools, skills, context files, extensions, prompt templates, themes, and session writes. They still use the user's Pi authentication and model configuration. Pi's offline startup mode also prevents configured npm/git package installation, package updates, and model-catalog refresh; it does not prevent the selected provider call. This is bounded invocation setup rather than permanent or complete isolation. Transcript-derived prompts reach the provider configured in Pi; review that provider's data-retention and privacy policy before using sensitive sessions.
The managed scheduler stores the selected backend but does not persist
--source, --pi-home, --pi-path, or --model. Before scheduling Pi, set
transcript_source, pi_home, pi_path, and model in
~/.skillopt-sleep/config.json; prefer an absolute pi_path and verify that the
scheduled account is authenticated.
Cursor source and backend
Cursor transcript harvesting is explicit: use --source cursor rather than
--source auto. Invoked-project scope uses Cursor's recorded workspace path,
with the sanitized storage directory as a fallback; --scope all scans every
Cursor workspace under ~/.cursor/projects. The model-driven backend requires
an installed, authenticated cursor-agent; use --cursor-path,
SKILLOPT_SLEEP_CURSOR_PATH, or the cursor_path config key when it is not on
PATH, and use --model or SKILLOPT_SLEEP_CURSOR_MODEL to choose a model.
Target the project skill explicitly so accepted learning becomes visible to Cursor without changing the plugin's own workflow skill:
python -m skillopt_sleep run --project "$(pwd)" \
--source cursor --backend cursor \
--target-skill-path .cursor/skills/skillopt-sleep-learned/SKILL.md \
--max-sessions 5 --max-tasks 3 --progress
Advanced config
The JSON/YAML config under ~/.skillopt-sleep/ supports additional engine keys,
including gate_mode, gate_metric, gate_no_regression, 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", gate_no_regression=false,
dream_rollouts=1, dream_factor=0, and recall_k=0.
The managed schedule command stores only the project, backend, time, and
optional auto-adopt setting. It does not copy --source, --cursor-home,
--cursor-path, --model, or --target-skill-path into the scheduled command.
For a Cursor schedule, set transcript_source, cursor_home, cursor_path,
model, and target_skill_path in ~/.skillopt-sleep/config.json first. Keep
the target project-relative, use an absolute CLI path because cron and Task
Scheduler may have a minimal PATH, and confirm that cursor-agent is
authenticated for the account that runs the job.
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:
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:
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. The benchmark recipes are not
the shipping CLI defaults.
Safety summary
- Session harvesting is read-only.
mockandhandoffmake no network calls.runstages proposals;adoptis the normal live-change boundary.- Adoption backs up existing target files.
--max-sessionsand--max-tasksbound 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.