Repository navigation
Add DeepSeek Harness (dsh) as a CLI Base Agent (acting agent and dsh Evolver) - #90
Merged
Merged
Conversation
…Evolver) DeepSeek Harness (`dsh`, npm @deepseek-ai/dsh 0.2.2-rc.1) beside codex, claude and pi, in the same three layers as Pi (#89), on the runner's outcome() of #77. - Layer 1: a <backend>/<model> reference becomes a route of dsh's pi-ai adapter (Binding.dsh_provider): the backend's URL in the first dialect it serves, the key by its variable's name (apiKeyEnv), the context window. dsh reports no cost: the backend's price applies. - Layer 2: Dsh(CliAgent) runs `dsh --profile headless --json` in a private DSH_HOME, HOME and DSH_AGENTS_HOME, telemetry off, the caller's own DSH_* variables dropped. The session's settings are a patch layer of dsh's own rows (--patch): the bound model as the default model (effort as its reasoningEffort, image_input as Pi's), the call's tools as an MCP server offered directly (1800 s per call), web search and fetch off unless asked for, no title request, session log, plugin list, sub-agents or workflows. A server that needs no key gets the placeholder key through HERMIT_DSH_NO_KEY (a route that names none is refused by dsh). Approvals are never asked (--approval-policy never); permission_mode picks dsh's own sandbox. Resume by --session-id when the home keeps the session; the workspace's .dsh/skills are also the session's own (dsh reads project skills only at the nearest folder with a .git). outcome(): a run without a completed `final` has failed, whatever dsh's exit code (SIGTERM exits 0). Usage from step_end events; the trajectory from the stream, a command's exit code from dsh's end markers (a timeout or a stop: unknown; "(no output)": empty); every unfinished end is an error step. dsh's outage codes (RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, EMPTY_RESPONSE, STREAM_CLOSED) join UNAVAILABLE. Roles, a login and container isolation are refused. - Layer 3: hermit/hosts/dsh.py (`hermit deploy --host dsh`): AGENTS.md, .dsh/skills and the adapter's tool server (120 s per call) as an insert row of .dsh/hermit.patch.yml, which the acting agent passes with --patch. The AgentEvolver, the loader and the config accept dsh; methods/dsh/ is the new Evolver (dsh's own sandbox off in the evolver sandbox, as Codex's is). - Tests: tests/core/test_dsh_agent.py over streams recorded from dsh 0.2.2-rc.1 (tests/fixtures/dsh) and, marked integration, the installed dsh: a session unsandboxed and in bubblewrap, the acting agent inside a git repository (the deployed skill offered, the adapter's own tool server answering), a keyless server, and --dump-config holding every row the session patches; the deploy host; selection entries. Checked against dsh's source at the tag dsh-v0.2.2-rc.1 and with the installed dsh. First built on #89's earlier head; rebuilt on main after #89 merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix was checked against dsh's source at dsh-v0.2.2-rc.1 and, where it shows, against the installed dsh (new recorded streams in tests/fixtures/dsh); each has a test that fails without it. - Stream reading: a bash result dsh's --json cut at 8 KiB (`truncated`) lost its exit marker and read as exit 0; now unknown, and the text says it was cut. A command started in the background has no status yet (unknown). The end markers are anchored at the very end (\Z), as dsh's JavaScript reads them, so a command that printed a marker of its own and exited 0 is a clean exit. - Usage: a step that answered counts as a call even when the provider reported no usage (dsh reports zeros), and so does a step dsh reports no usage for (an attempt without any); what dsh never reports (compaction, a step's partial usage) is documented. parse_log reads Pi's and dsh's streams with their own readers. - outcome(): a session whose tools did not start (dsh writes the MCP row's error on stderr and runs on without them) has failed. A run that wrote only an `error` event (a usage error) or nothing (a working directory's .env that sets PATH, PYTHONPATH, ...) started no session, so the acting agent reports it instead of recording each task as a wrong answer: outcome() may now say `started` (shared runner; absent, as before). - The workspace is the project wherever it lies: the session's layer sets the skill-filesystem row to the workspace's .dsh/skills alone and the agent-instructions row to the deployment's marker (.hermit-deploy.json) as the project root, so a workspace inside a git repository gets its own skills and none of the repository's skills or AGENTS.md (they outranked the adapter's). This replaces copying the skills to the home. - `--add-dir` takes the real path of an existing directory, as dsh requires (a link failed every command and write); an effort without a model is refused; user_home() follows DSH_HOME. - The acting agent passes agent.dsh.permission_mode on (it was dropped) and refuses a mode dsh has not. - hermit deploy --host dsh: the patch layer also names the workspace's .dsh/skills (a workspace below a repository's root kept none), and a context past the 65,536 bytes dsh reads of a project's instructions is refused. - The AHE template has a `dsh` wire (as Pi's: hooks run by hand hand the memory over); mwm and RRSI refuse a backend other than Codex or Claude Code at setup (pi and dsh were accepted and ran without their model). - Docstrings say what dsh reads (.env files, the instruction chain) and sends (its own provider's ids). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix was checked against dsh's source at dsh-v0.2.2-rc.1 and, where it shows, against the installed dsh; each has a test that fails without it. - A task's data is bound in the workspace, not linked (binds_mounts, as for Claude Code): dsh's glob and grep run ripgrep without --follow, so a linked folder was never listed or matched. dsh's own sandbox still runs nested in that mount namespace (integration test). - Web search with a model of another backend: dsh's search is DeepSeek's own service (its Messages API on DEEPSEEK_API_KEY), so the session searched on a provider and key the run did not bind, outside its usage. Such a session gets web fetch only, as Codex's hosted search is off for a backend other than OpenAI's; a DeepSeek model or dsh's own provider keeps search. - Usage: a step dsh reports no usage for counts as a call only if it produced something (a step whose requests all failed is none), and price_usage leaves calls of which no token was reported unpriced: their cost is unknown, not $0.00 (shared: a usage with calls and no tokens is no longer priced at 0 for any CLI). - The instructions' project root: no root marker, so it is the working directory, the workspace. The deployment marker (.hermit-deploy.json, which every host's deployment writes) made a session without a deployment take a deployed folder above it for its root, and read its AGENTS.md. - hermit deploy --host dsh: the tool server's row fails at a session's start when dsh cannot start it (failOnStartupError, as Codex's `required`), which the acting agent reads as a failed session; it was retried in the background while the session ran without the adapter's tools. The 65,536-byte check counts dsh's frame around AGENTS.md (304 bytes, a </system-reminder> one byte more): an AGENTS.md just under the cap was deployed and cut. Docstring: the command names its profile; the skills row replaces a skill-filesystem config of the user's own. - dsh's effort levels and permission modes are checked as the run config loads (an unknown one failed every session, one task at a time), and an effort level dsh has not is refused in the session. - The AHE learners' texts name a Pi or dsh acting agent's stream (agent-pi.jsonl, agent-dsh.jsonl) instead of agent-codex.jsonl (Pi's entry was missing on main too). - Tests: a recorded stream of a session stopped by SIGTERM (stopped.jsonl: exit 0, no `final`, an `error` event) replaces a cut success stream, and is run through the acting agent and an AgentEvolver step; the deploy refuses at the exact limit and an MCP server name dsh refuses. Correction to round 1's message: on 03fafff a dsh usage error was already an error record per task (failed), not a wrong answer; `started` now makes the acting agent stop the run and say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix was checked against dsh's source at dsh-v0.2.2-rc.1 and, where it shows, against the
installed dsh; each has a test that fails without it.
- Usage: round 2's shared rule (a usage with calls and no tokens is not priced) is gone. It
made Claude Code's results of refused requests (a 529, a 401: one turn, no tokens) cost
unknown instead of $0.00, and the AHE and RRSI evolvers' merged usage then dropped the
step's known spend from the budget. Usage gains `unreported_calls`, the calls whose tokens a
CLI did not report; only dsh's reader sets it (a step that produced something without
usage: the provider reported none, or dsh retried the step and so left its usage out), and
price_usage leaves such a usage's cost unknown rather than pricing the part reported, which
under rate limits may be far less.
- dsh's transcripts are plain JSON lines (the session layer sets session-persistence-jsonl to
compression none, its root as dsh's own `!!js dshHomePath('sessions')`): they were zstd,
which hermit.redact cannot scrub a key from. A scrubbed transcript still resumes.
- The dsh Evolver's sessions take no instruction file (instruction_files false: the
agent-instructions budget 0): dsh adds a folder's AGENTS.md when its file tools touch it, so
the learner was given the adapter's AGENTS.md, the acting agent's policy, as instructions.
Codex's learner is given none.
- dsh's settings are checked as the run loads for the dsh Evolver too (an effort it has not
failed every learner step, after its rollouts); evolver.dsh.permission_mode is refused (the
Evolver runs dsh's own sandbox off), and YAML's bare `off`, a boolean, is `off`.
- Web search is kept only for a model reached at api.deepseek.com on DEEPSEEK_API_KEY, where
dsh's search goes (a `deepseek` backend moved by DEEPSEEK_BASE_URL searched elsewhere).
- A run that ends CONTEXT_WINDOW_EXCEEDED with a backend that declares no context_window says
to declare it (recorded: context_window.jsonl, vLLM's refusal of dsh's 128,000 output
tokens; with the window declared dsh sizes the request and completes).
- A unit test with the fake dsh for the task data's binding, which CI runs where bubblewrap can
bind (the only test needed the dsh CLI).
- Wording: the deployed tool server's row makes dsh report a server it cannot start and the
acting agent fail the session (dsh itself goes on); a run dsh never started is each task's
error record, not a stopped run (round 2's message said so); the deploy's size check is exact
for a session at the workspace's root; the linked-data warning names dsh's glob and grep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix was checked against dsh's source at dsh-v0.2.2-rc.1 and against the installed dsh; each has a test that fails without it. - A step dsh retried after the provider refused a request lost its cache counts: the refused attempt reports zeros, pi-ai reports a count of 0 as none, and dsh sums a count into the step's only when every attempt reported it, while the step's totalTokens still holds them. The reader counts what the total holds beyond the counts reported as input, and the call as unreported (its cost unknown: those tokens were read from the cache or written to it, which are priced apart). Recorded: retried.jsonl (a 503, then 1000 prompt tokens, 900 cached: dsh reports inputTokens 100, totalTokens 1010). Round 3 said such a step's usage is left out; dsh does that only for an attempt that reports no usage at all (a stream that ended before its last event, an attempt a stop cut short). - The dsh Evolver's settings are checked from its params as the Trainer has them (its method's own under the run's, the model bound), in login.evolver_problems, which train runs before its first step and hermit check now reports for an Evolver without a login. Round 3's check, as the run loaded, missed the dsh method named with its params (`backend: dsh` comes from the method's manifest) and refused an effort given with the run's own model. The acting agent's are still checked as the run loads; one function (cli.dsh_problems) holds both, and isolate.py's change is that call. - dsh's own provider (a model of no binding, DeepSeek's API) takes the efforts off, low, high and max; another level failed every session (UNSUPPORTED_REASONING_EFFORT). - evolver.dsh.instruction_files is refused, as evolver.dsh.permission_mode is: the Evolver dropped it. - The caller's DEEPSEEK_SEARCH_BASE_URL is dropped with its DSH_* variables: without the sandbox it moved dsh's web search off DeepSeek's API. - The context_window hint follows dsh's error whole (once), not its first 300 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix was checked against dsh's source at dsh-v0.2.2-rc.1 and against the installed dsh; each has a test that fails without it. - A step dsh retried after a refused request is priced again, its dropped cache tokens counted as input. Round 4 made such a session's cost unknown, and a USD budget then counted none of its spend (rollouts and learner steps add no dollars for an unknown cost), under the very load that causes retries. Its tokens are known, and hermit prices a cache write as input: the price is the step's with the counts reported, or more if those tokens were read from the cache, never less. A call stays unreported, and its session's cost unknown, only when dsh reported none of its tokens. - The caller's DEEPSEEK_BASE_URL is dropped as DEEPSEEK_SEARCH_BASE_URL is: without the sandbox it sent the requests of dsh's own provider, with DEEPSEEK_API_KEY, elsewhere. A variable the session is given (`env`) still passes. - dsh's settings check takes what each agent uses: the Evolver's danger-full-access and instruction_files false are accepted (round 4 refused them); the acting agent's instruction_files false is refused (it was dropped; it would cut the adapter's context, which reaches dsh as its AGENTS.md). - Tests pin the session's tool row (failOnStartupError, the 30-minute call timeout), every row DSH_OFF turns off, all six outage codes, and dsh's own provider's levels for the Evolver; the outage test is named for what it checks. - Wording: effort off asks a bound model for no reasoning level, which leaves its server's default (dsh's own provider turns thinking off); the context-window hint keeps dsh's error line as far as it fits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each finding was checked against dsh's source at dsh-v0.2.2-rc.1 and the installed dsh; each test added fails without what it checks. No behaviour changes: the fixes are tests and claims. - Tests: a session's own `env` reaches dsh while the caller's DEEPSEEK_BASE_URL is dropped; the integration test of dsh's own sandbox shows a command's write outside the workspace refused (it passed with the sandbox off); a bash call that failed to run (a result dsh marks `error`) has no exit code; a linked session folder, or a session in a linked folder, is not resumed; a stopped session's usage is pinned (none). - The reader's account: the last step of a session a stop cut short is not counted (dsh writes a step's usage at its end, after its tool calls ran, and a stopped session writes nothing more); the docstring said such a step was a call of unknown tokens. dsh's own adapter, not pi-ai, drops a cache count of 0. A retried step's price is never less than with its counts for a price whose cached input is no dearer than its input; on the Responses API the total is the server's own `total_tokens`, taken as given. - dropped_env says why a sandbox's grant of DEEPSEEK_BASE_URL does not reach dsh (hermit's own is its deepseek backend's chat completions root, which dsh reads as its own provider's Messages root) and that `env` gives dsh its own; the Settings paragraph says so too. - A stale test docstring: the Evolver's own permission mode and instruction_files are taken. Not changed, disclosed: the acting agent's re-run after an outage keeps only its last attempt's usage, for every CLI (shared code, a separate change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pi fixes this branch carried change Pi's behaviour on main, not dsh's: parse_log's Pi reader, the AHE Evolver's texts for a Pi acting agent's stream, and mwm and RRSI refusing a Pi learner. They go to their own PRs (fix/pi-stream-readers, fix/mwm-rrsi-backend-check). This branch keeps dsh's entries only: parse_log reads a dsh stream as dsh's, the AHE texts name a dsh acting agent's stream, and mwm and RRSI refuse dsh (the other PR's general check covers it too, whichever merges first). The integration test of dsh's own sandbox keeps the session's home under ~/.cache/hermit-tests, as the test checklist asks of a check under bubblewrap (it was under /var/tmp). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 9, 2026
Review round 7 (five reviewers on 37aee8c, one lens each, every finding then reproduced by a separate agent told to refute it) found 34 issues, all confirmed: one of medium severity, the rest low. The medium one: no test guarded the reader's three rules for a step whose tokens dsh did not report (a step that only called a tool is a call, so is one that only thought, and what a step produced does not carry over to the next); each, undone, left every test passing while it misstated a session's cost. The new test fails under each. The low ones are listed in the PR description, not changed. Also: a round-6 test docstring rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a hermit dsh session a command still running at its timeout (dsh's default is 60 s) is
moved to a background job, unless dsh's job limit is reached: the session's layer leaves
tool-bash's promoteOnTimeout at its default, true. Its result ends `[still running after
<ms>ms; moved to background job <id>]` with no exit marker, and only the DSH_BACKGROUND check
keeps it from reading as exit 0. No test held that case: bash.jsonl was recorded with
promotion off. Review round 7 found this (rated medium by its reviewer, low by the agent that
reproduced it, as the code was right); the author asked for it fixed.
promoted.jsonl is recorded through make("dsh").run, as the other Base Agent streams are, and
the test now reads it, and also a command stopped from outside the call (`[stopped: <reason>]`,
which no headless session writes, so written as dsh's tool-bash renders it), with and without
a marker after it. Each check undone (the background check, the `stopped:` alternative, the
markers after a stop) fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 8 (two reviewers on f4792e0, one on the test and its fixture, one on the claims, each finding then reproduced by a separate agent told to refute it) found: - dsh prints a command's timeout as the model gave it, a JavaScript number (`1000.5`, `0.5`), and both timeout markers were matched only at a whole number of milliseconds: a command still running at such a timeout, moved to a background job, read as exit 0, and one killed at it that trapped the signal read as its trapped exit (rated medium). Both markers now match any number dsh prints. Recorded: promoted.jsonl's third call (1000.5 ms, moved to the background) and bash.jsonl's ninth (1000.5 ms, killed, exit 0 trapped). - The test of a promoted command held only one with output before its marker; the marker alone, what a command that printed nothing yet gets, rests on DSH_BACKGROUND matching without a newline before it, which nothing held (rated medium). promoted.jsonl's second call is `sleep 5`. - Wording (low, in f4792e0's own docstring): the stop marker comes before the signal or exit marker, not before every other marker (a sandbox denial's come first). Each fix undone fails its test: the marker with a newline required before it, and either marker at whole milliseconds only. bash.jsonl is recorded again by the same script, its first eight results unchanged; its steps' count in the usage test follows (10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 9 (two reviewers on d25958a, one on the code and its tests, one on the claims, each finding then reproduced by a separate agent told to refute it) found 6 issues (4 distinct), all low. Three are wording this change's own text got wrong, corrected here (comments, a docstring and the fixture README; no code or test changes): - dsh prints the timeout it applied, not always the model's: its default (60 s) when the model gives none, and at most its cap (600 s). - DSH_NO_OUTPUT's comment said `(no output)` comes before the marker of a command that printed nothing, which a command moved to the background does not get (its marker stands alone). The fourth, the exponent form both timeout patterns match (`1e-7`, a timeout under 0.000001 ms) held by no test, is listed in the PR description, not changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…built-ins alone (post-PR audit) - Dsh.outcome read `unavailable` from every line of the stream that is no event, and dsh starts a deployed stdio tool server on its own stderr, which the stream keeps: outside the task sandbox, a tool's line with an outage's words (`Service Unavailable`) made a session that failed for another reason (`max-tokens`) an outage, run `retries` times and then an error of unknown cost. It is now read from the `final` event's error code (DSH_OUTAGES, Dsh.outage), and UNAVAILABLE is main's again. - test_dsh_refuses_roles_and_a_container_and_searches_only_on_deepseeks_own_provider bound its DeepSeek models through the developer's own backends (HA_BACKENDS or ~/.config/hermit/backends.yaml, DEEPSEEK_BASE_URL, DS_BASE_URL), so it failed wherever those move `deepseek`: it binds them on the built-ins alone. - The fixture README: tools_down.jsonl's port is one Node's fetch refuses, and the Base Agent's stream has that run's stderr lines first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: main's new Base Agent (mini-swe-agent) and this branch's (dsh) are both kept, mini-swe-agent's first as main has it, in every list of CLIs, bases, hosts, stream readers, bindings and settings sections (cli.py's CLIS, records.parse_log, trajectory's STREAM_HOSTS and READERS, backends' NEEDS, CLI_AGENTS and CLI_KINDS, config's FREE, BASES and CLI_BASES, defaults.yaml, the acting agent's names and checks, isolate.validate's login check, `hermit deploy --host`, the context-tool test's hosts). parse_log keeps main's Pi reader. The acting package's table of agents, which now lists mini-swe-agent, lists dsh too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview round 10) - cacb4ae read an outage from the final event's code only, which narrowed a43698d's rule: a provider overloaded inside a 200 stream (the Messages API's `overloaded_error` event, a chat server's error chunk) has no status, and dsh ends the run with pi-ai's unclassified PI_AI_ERROR, which it does not retry; the task was then not run again. Such an end is now read by its words (UNAVAILABLE, as Pi's are), and only dsh's own end is read so, never a tool's stderr. Recorded through the Base Agent: overloaded.jsonl. - A test docstring described parse_log as main had it before #49. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A kept home's patch layer is written as a file of its own, never through a link or into a pipe the last session left (the dsh Evolver's agent_home): the guard had no test. - An end dsh leaves unclassified (PI_AI_ERROR) that is no outage, with a tool's outage words on dsh's stderr, runs once: ace8fab's new path had no test that its words come from the end alone. - PI_AI_ERROR is the code of dsh's pi-ai adapter, not pi-ai's; outcome() and errors() no longer say the end's code alone tells an outage. - test_deploy.py: the dsh test is named for what it holds, and its comments say what dsh does (a server that does not start is reported; the dsh acting agent fails the session; a project's root may be the working directory itself), as hermit/hosts/dsh.py now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iamziyuzhao
marked this pull request as ready for review
November 10, 2026 05:47
…h branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DeepSeek Harness joins the pages that name the CLI Base Agents, their settings, deployment, models and installation, in the wording of Pi's and mini-swe-agent's entries; what is its own: its inner sandbox (permission_mode, and what it does to environment.write and /tmp), its web search, its patch layer and how a session loads it, a tool server that does not start, and the usage it leaves out. No code changed. convert's lists are untouched: dsh has no plugin format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 13 read f0d2091: 16 findings, all low. dsh's commands each have a /tmp of their own and its default sandbox refuses a declared writable path (the sandbox guide); how to write a provider of one's own (model-backends); what a web fetch reaches; which price the cost follows; what Landlock, a patch layer and the request left out of the usage are. No code changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sandbox guide's two ways to reach a path under /tmp are for reading it (a mount is read-only); the requests that summarize a session's history are uncounted each time, not once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #91 (the AHE Evolver's texts name a Pi or mini-swe-agent acting agent's stream), #94 (OpenCode as a CLI Base Agent), #97 (hermit deploy retires skills the adapter no longer exports) and #99 (agent skills for this repository). #94 adds OpenCode at the places this branch adds dsh, so 36 files conflicted. Each conflict keeps both sides, OpenCode before dsh: - CLIS, the readers (records.parse_log, trajectory's STREAM_HOSTS, READERS and MARKS, the AHE debugger's STREAM_TEXTS) and every list of CLIs name both, dsh last; cli.py's Dsh part follows OpenCode's; - main's NO_LOGIN replaces this branch's tuples in login.py, isolate.py and acting/workspace.py, and gains "dsh"; - a comment that no longer fits 120 columns with both names is on its own line; - docs: both names in each list. docs/methods/agent.md's dsh learner now "signs in as the pi learner does", since OpenCode's sentence stands between them; in docs/methods/ahe_adapter.md, #91's general wording ("the one its CLI wrote") replaces this branch's list of streams. dsh does not call #97's check_skills and retire_skills yet; the next commit does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hermit deploy --host dsh now calls check_skills before it writes anything (a redeployment refuses to replace a skill folder no deployment wrote, or to write through a linked skills folder) and retire_skills after it copies the skills (a skill the last deployment exported and this one does not moves to .retired-skills/). dsh copies its skills before it writes its patch layer, whose skill-filesystem row names them, so retire_skills follows copy_skills there. #97's redeployment test gains the dsh case, which fails without the retire_skills call; no test covers the check_skills call, as on main for every host but Codex. #97's CHANGELOG entry names .dsh/skills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
haizhongzheng
approved these changes
Nov 11, 2026
Member
There was a problem hiding this comment.
Thanks for this, and for the careful description. We reviewed 947f146 with two independent reviewers, verified each finding against the code, and found nothing that blocks the merge. We made no changes.
What we checked
- No change for the existing CLIs. The shared-code changes leave Codex, Claude Code, Pi, mini-swe-agent and OpenCode as they were:
startedinCliAgent.run,unreported_callsinprice_usage,parse_log, the stream marks,needsand the CLI lists. - Config checks, run on a real config. A dsh agent with a bound model loads with
effort: medium, and YAML's bareoffis read asoff. A wrong effort orpermission_modeis refused at load.hermit checkreports a dsh Evolver's wrong effort and itspermission_mode. - Copied files. The user's
~/.dsh/.envis copied only when there is neither a binding nor a login. - dsh is listed everywhere. Every list of CLIs in
hermit/anddocs/names dsh. - Tests.
python -m pytest --changed --base origin/mainselected every group: 1287 passed, 110 skipped, 0 failed. Of the skips, 10 need the dsh CLI, which this machine lacks; the rest are optional groups that CI installs. CI passed on this head. - Merging. The PR merges cleanly into
main(886c89d).
isolate.py
The only change is the config check: dsh refuses login, and the acting agent's dsh settings are checked. The sandbox mechanics are unchanged. As maintainer, I agree to this change, which the sandbox checklist asks for.
Noted, not changed (both are on main already)
- A dsh
.envescapes redaction. Without a binding,~/.dsh/.envis copied into the session's home.hermit.redactknows only the values inos.environ, so a key that exists only in that file is not scrubbed if a session prints it.maindoes the same with mini-swe-agent's.envand with Codex's and Claude Code's credential files. A shared fix would cover every CLI. extra_bodyis dropped.Binding.dsh_providerdoes not pass a backend'sextra_body.pi_provider, which uses the same pi-ai, does not either, and neither do Codex and Claude Code; only OpenCode and mini-swe-agent apply it. The docs' "added to every chat request" already overstates this for Pi.
🤖 Generated with Claude Code
haizhongzheng
added a commit
that referenced
this pull request
Nov 11, 2026
Conflicts: mwm's and RRSI's backend checks keep this PR's list (codex, claude, pi), which refuses dsh as #90 did; the tests add dsh to the refused backends. Docs list both Pi and dsh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 of 11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
Adds DeepSeek Harness (
dsh; npm@deepseek-ai/dsh0.2.2-rc.1,deepseek-ai/deepseek-harness) as a sixth CLI Base Agent beside
codex,claude,pi,mini-swe-agentandopencode. It is oneCliAgentsubclass that reports its runs through the runner'soutcome()(#77). It can act (agent.base: dsh) and learn (the newdshEvolver). It follows #89 (Pi).On
mainat1b0d897:mainis merged into the branch (0ddba64,mainat6e421a4, with conflicts in 11files where #49, mini-swe-agent, and this PR both add a CLI;
77f3d75, atf469f59, without a conflict;1f25839, at1b0d897, with conflicts in 36 files where #94, OpenCode, and this PR both add a CLI; every conflict keeps both sides).The first version of this PR was stacked on #89's earlier head; the branch was rebuilt on
main(d5f0a64), so itsdiff is only dsh: 75 files, +2675 −201 against
1b0d897, the documentation included. Wheremainand this PR both listthe CLIs (bases, hosts, readers, settings sections), dsh comes last, after mini-swe-agent and OpenCode. The head,
947f146, brings #97's handling of stale skills tohermit deploy --host dsh.<backend>/<model>reference becomes a route of dsh's pi-ai adapter (Binding.dsh_provider): thebackend's URL in the first dialect it serves (as for Pi: chat completions, then Messages, then Responses), the key
by its variable's name (
apiKeyEnv, never the value) and the context window. dsh reports no cost, so the backend'sprice is applied to the usage it reports.
Dsh(CliAgent)runsdsh --profile headless --jsonin a private home (DSH_HOME,HOME,DSH_AGENTS_HOME), with telemetry off and the caller's ownDSH_*,DEEPSEEK_BASE_URLandDEEPSEEK_SEARCH_BASE_URLdropped (without the sandbox they moved dsh's own provider and its web search), whatevera sandbox lets through: hermit's own
DEEPSEEK_BASE_URLis itsdeepseekbackend's chat completions root, whichdsh reads as its own provider's Messages root. The session's
envgives dsh one of its own.--patch, written in the home):effortas itsreasoningEffort;image_input: truedeclares a modelthat takes images);
mcp__<server>__<tool>;web_search: true. dsh's search is DeepSeek's own service (its Messages APIon
DEEPSEEK_API_KEY), so unless the model is reached there on that key the session gets web fetch only, asCodex's hosted search is off for a backend other than OpenAI's;
hermit.redactscrubs akey a session wrote from its home, as from every other record;
DSH_TELEMETRY_DISABLED=1), the session log andplugin list that dsh adds to requests to DeepSeek's own API, and sub-agents and workflows (their tokens are in
no usage figure dsh reports).
from the workspace's
.dsh/skills(skill-filesystem), and instructions only from the working directory, theworkspace (
agent-instructionswith no root marker). Without them, a workspace inside a git repository also gotthat repository's skills and
AGENTS.md, which outranked the adapter's. The dsh Evolver's sessions take noinstruction file at all (
instruction_files: false): dsh adds a folder'sAGENTS.mdwhen its file tools touchthe folder, and an adapter's
AGENTS.mdis the acting agent's policy, which Codex's learner is never given.Code (
binds_mounts): dsh's glob and grep run ripgrep without--follow, so a link was never searched.localthrough a variable of the session(
HERMIT_DSH_NO_KEY), because dsh refuses every request of a route that names no key.agent.dsh.permission_mode, defaultworkspace-write).writablepaths are granted by their real paths, as dsh requires. Approvals are never asked(
--approval-policy never). A permission mode or an effort level dsh does not have is refused before anysession: the acting agent's as the run config loads, the dsh Evolver's by
hermit checkand beforetrain'sfirst step, from its params as the Trainer has them (the method's own and the run's model in). dsh's own provider
takes fewer levels (
off,low,high,max) than a bound model's route. YAML's bareoff, a boolean, isoff.--session-idwhen the home keeps the session (dsh resumes only in the directory the session ranin, which an AgentEvolver's calls share).
{{...}}otherthan
{{provider}}and{{model}}, so system instructions go at the top of the prompt as a marked block.login, and container isolation.outcome():finalevent has failed, whatever dsh's exit code (dsh exits 0 when SIGTERM stopsit).
sessionevent (a usage error, or a working directory's.envthat sets a name dshkeeps, such as
PATH) started no session (started: false), so each task's error record says that dsh exitedbefore starting a session, not that its session failed (the run goes on).
unavailable: the acting agent runs the task again) is told only bythe
finalevent's error: one of dsh's codes for it (RATE_LIMIT,SERVER,TIMEOUT,TRANSPORT,EMPTY_RESPONSE,STREAM_CLOSED: those dsh retries itself, and a stream the provider closed), or the codedsh's pi-ai adapter gives an error it could not classify (
PI_AI_ERROR) with an outage's words (UNAVAILABLE,which Pi's notices are matched with: an overload a provider sends inside a 200 stream, such as the Messages API's
overloaded_error, has no status). Not by the stream's other lines: dsh starts a deployed tool server on its ownstderr, and a tool's words of an outage are the session's work (a JSON line of type
finalthat a tool writes isread as that event: see Known issues).
statusstep_endevents (records.parse_dsh), each the sum of the step's attempts. Astep that produced something but whose tokens dsh did not report (the provider reported no usage, or an attempt
reported none and dsh left the step's usage out) is an unreported call, and the session's cost is then unknown
rather than the price of the tokens reported. A step dsh retried after a refused request loses its cache counts
(dsh keeps a count only when every attempt reported one), which its
totalTokensstill holds: those tokens arecounted as input and priced as hermit prices a cache write, never less than the step's price with the counts
(for a price whose cached input is no dearer than its input), so a USD budget still counts the session. On the
Responses API the total is the server's own
total_tokens, taken as given.CONTEXT_WINDOW_EXCEEDEDwith a backend that declares nocontext_windowsays to declare it. dsh sizes a model it does not know as 1,000,000 tokens with 128,000 output tokens a request,
which a vLLM server refuses; with the window declared, dsh sizes each request by it.
trajectory.dsh_steps). A command's exit code is read from dsh's own endmarkers, anchored at the end as dsh reads them. It is unknown for a command that reached its timeout (in a
session dsh moves it to a background job, or stops it when it cannot; the timeout printed is the one dsh applied:
the model's,
1000.5too, else 60 s, at most 600 s), was stopped, was killed by a signal or runs in thebackground, and for a result dsh's
--jsoncut at 8 KiB (the marker is in the cut part; the step says it wascut). Every unfinished end (
error,aborted,max-tokens,blocked, ...) is an error step.hermit/hosts/dsh.py(hermit deploy --host dsh) writesAGENTS.md,.dsh/skills, and a patch layer,.dsh/hermit.patch.yml, because dsh has no project file for MCP servers or skill folders. The layer holds theadapter's tool server (an
insertrow, 120 s per call) and names.dsh/skills. A server dsh cannot start is notretried in the background (
failOnStartupError), but dsh only reports it on stderr(
dsh: warning: 1 entry did not activate) and runs the session on without the adapter's tools, exiting 0: thedshacting agent reads that warning and fails the session, but a session started by hand does not stop, unlikeCodex's
required. A context past what dsh gives the model of a project's instructions (65,536 bytes, its304-byte frame around
AGENTS.mdincluded) is refused. The acting agent passes the layer with--patch. Asevery host does since hermit deploy retires skills the adapter no longer exports #97 (
947f146), a redeployment refuses, before writing anything, to replace a skill folder nodeployment wrote or to write through a linked
.dsh/skills, and moves a skill the adapter no longer exports to.retired-skills/.AgentEvolver, the loader and the config acceptdsh.methods/dsh/is the new Evolver. It turns dsh's ownsandbox off inside the evolver sandbox (
permission_mode: danger-full-access), as Codex's is, and copies nologin (
home: []).hook_hosts.pygains adshwire).deepseek-official), and the user's~/.dsh/.env(or$DSH_HOME/.env) is copied into the session's home, as Codex'sauth.jsonis. This path was notrun (see Validation).
CliAgent.run):outcome()may now saystarted. When it does not,startedis as before (the CLIwrote a JSON line).
records.parse_logreads a dsh stream with dsh's reader (it read one as Claude Code's).Usagegainsunreported_calls, the calls whose tokens a CLI did not report, andrecords.price_usageleavessuch a usage's cost unknown (the budget counts it as such). Only dsh's reader sets it: Codex, Claude Code, Pi,
mini-swe-agent and OpenCode are priced as on
main.agent-dsh.jsonl) instead ofagent-codex.jsonl(as Name a Pi or mini-swe-agent acting agent's event stream in the AHE Evolver's texts #91 and Add OpenCode as a CLI Base Agent (acting agent and opencode Evolver) #94 name Pi's, mini-swe-agent's and OpenCode's).
mwmandRRSIEvolvers refusedshat setup: mwm would run it on dsh's own provider instead of its boundmodel (its session settings carry no
provider), and RRSI's read-only roles would run with all of dsh's tools(their settings are Codex's or Claude Code's).
pihas the same problem onmain: Run every method on Pi (evolver.base: pi) #92, an opendraft, sets mwm and RRSI up for Pi and refuses every backend but
codex,claudeandpi, which coversdshtoo.f469f59; itslines came in with
1f25839. Run every method on Pi (evolver.base: pi) #92 (an open draft, now Pi on every method: at03ed729, 54 files, 17 ofthem changed here too): whichever of it and this PR merges second resolves the conflicts by keeping both sides;
Run every method on Pi (evolver.base: pi) #92's check replaces this PR's
dshone, but its message names Pi (codex, claude or pi, not 'dsh'), so this PR'stwo tests of that message (in
test_mwm_evolver.pyandtest_rrsi_adapter.py) change with it.login.evolver_problems, whichtrainruns before its first step, also checks a dsh Evolver's settings(
cli.dsh_problems), andhermit checknow reports it for an Evolver without a login too (it did only with one).hermit/isolate.py: only its config check (validate). dsh refuses alogin, as Pi does, and the acting agent'sdsh settings are checked there, as Claude Code's effort is on a login. No sandbox mechanics change (
.dshis aone-level folder). Pending: the core team's agreement, which the sandbox checklist asks for any
isolate.pychange.
f0d2091,a7883d8,d66105a; no code): dsh joins the pages Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 and mini-swe-agent: a Base Agent, an acting agent and a deployment host #49 changed for Pi and mini-swe-agent(
agent-layers.md,model-backends.md,config-fields.md,task.md,evolver.md,methods/agent.md,custom-evolver.md,compatibility.md,installation.md, CHANGELOG,commands.md,adapter.md),methods/ahe_adapter.mdfor the AHE hooks,
guides/sandbox.mdanddeveloper/sandbox-design.md, and the pages that list the hosts, the CLIs or theCLIs integration tests run:
faq.md,what-is-a-harness-adapter.md,configuration.md,service-design.md,docs/web/index.html,tests/README.md,CONTRIBUTING.md, and the AHE template'sREADME.mdand.hermit/EVOLVER.md(which the AHE learner reads). What the pages say of dsh's own: its inner sandbox (
permission_mode, and what it does toenvironment.writeand/tmp), its web search and fetch, its patch layer and how a session loads it (with a warningthat a session started by hand can edit it), a tool server that does not start, the limit on a project's instructions,
what its usage leaves out, and a
providerof one's own.README-pypi.mdis unchanged, as mini-swe-agent: a Base Agent, an acting agent and a deployment host #49 and Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 left it, and so areconvert's lists: dsh has no plugin format.hermit convert --target dsh(an adapter as a dsh plugin package).Validation
Run in an Ubuntu 24.04 container (arm64), as root with
--privilegedbecause the VM's AppArmor setting refusesunprivileged user namespaces. Python 3.12, bubblewrap, Node 24.22.2, and dsh 0.2.2-rc.1 from npm (its pi-ai dependency
resolved to 1.5.0), with the
rrsigroup and themini-swe-agentextra installed as CI installs them (not the gepa,skillopt and harness2 groups). The toy runs ran on
207ea88, and the scripted one again on1e75c6f, ona7883d8and onthe head,
947f146.After
207ea88,1bbd57cadds one test (and a docstring's line breaks);f4792e0a recorded stream (promoted.jsonl) and,in an existing test, its case and two
[stopped: …]results written by hand as dsh's bash tool renders them;d25958awidens the two timeout markers' patterns (round 8) and records
promoted.jsonlandbash.jsonlagain with the callsits tests read;
a43698dcorrects wording only (round 9: comments, a docstring, the fixture README);cacb4aefixesthe post-PR audit's findings;
0ddba64mergesmain(6e421a4);ace8fabfixes round 10's medium finding in the code(an overload dsh leaves unclassified);
1e75c6fadds a test and a case of an existing one (round 11's two guards) andcorrects wording (docstrings, test comments, the deploy test's name);
77f3d75mergesmain(f469f59);f0d2091,a7883d8andd66105aadd the documentation and change no code (d66105achanges two of its sentences);1f25839mergesmain(
1b0d897), and the head,947f146, hashermit deploy --host dshretire stale skills (#97). The full suite ran on6e421a4, on1e75c6f, on1e75c6fmerged withf469f59(aabd6e8, a merge made for that check, with the tree77f3d75has), ona7883d8and on the head, each split over six containers at once (one pytest, serially, in each); each run'sfolder (the author's own records, not published) names the commit it copied.
python -m pytest --integration,main(6e421a4)1e75c6f)a7883d8(mainatf469f59merged, the documentation added)947f146:mainat1b0d897merged, dsh retiring stale skills)opencodeCLI, not in this image, among them); all 24 undo checks held; the developer's environment as below (364 passed, 10 skipped: one more, OpenCode's case of the installed-CLI test); the scripted toy run and the sandboxed tool server as belowbash docs/build.sh: Sphinx, warnings are errors), ona7883d8and on the headmainat1b0d897). In the site's theme (looked at ona7883d8)adapter.md's seven-column table (eight columns on the head, with OpenCode's) scrolls inside its wrapper, as the six-column one does, and the warning box incommands.mdrendersf0d2091(round 13;a7883d8andd66105achange none of the texts this row names)hermit checkaccepts, every setting intask.md's 8000 row read;hermit deploy --host dshmatchingadapter.md's table and tree, and the documenteddsh --profile headless --patch ...started from the workspace offering and answering the adapter's tooltests/core/test_dsh_agent.py(43 tests) andtest_deploy.py::test_dsh*DEEPSEEK_BASE_URL,DS_BASE_URLthat movedeepseek, DeepSeek keys,DSH_HOMEwith a key,DSH_*test_isolateone (8 skip, as in the plain run: 7 for a CLI not installed here, 1 for no system site-packages directory);a43698d's web-search test fails theresandbox.enforce: required) with an adapter tool (docs/_examples/file_adapter), scripted model, on1e75c6fHA_ADAPTER_SANDBOX); the tool and the session answeredhermit train+hermit evalon the docs' toy task, sandbox enforced, dsh acting and learning, scripted model, on207ea88and1e75c6fhermit checkok; train 0.0 → 1.0; eval: final adapter 1.0, none 0.0; no call of unknown costapi.fireworks.ai, and its probe found the network and the run's data closed; the key in no file the run wrotepar-main-6e421a4-6sh,par-1e75c6f,par-merge-f469f59,par-docs-a7883d8,par-merge-947f146). Two failwhen run as root:
tests/core/test_isolate.py::test_a_rollout_sees_what_is_declared_and_nothing_elseandtests/methods/ahe/test_ahe_hook_runner.py::test_logs_that_cannot_be_written_lose_the_line_not_the_call. The third,tests/methods/ahe/test_ahe_evolver.py::test_the_evolve_prompt_is_start_md_and_the_query, depends on the order: itmakes a temporary directory in
~/.cache/hermit-testswithout creating that folder, which other tests create, and inall four runs it ran before them (in pytest's own order, and in some other splits, it runs after them and passes).
tests/fixtures/dsh/) are dsh's output unchanged, recorded as their README says.dsh-v0.2.2-rc.1and with the installed dsh.starts, every command fails with
SANDBOX_UNAVAILABLE, visibly.~/.dsh/.env.overloaded.jsonlwas recorded with aMessages one: a run that ended at its first request).
hermit trainwith a real model since207ea88(the Fireworks run): the merge ofmainwas checked with thescripted model.
Known dsh behaviours (not changed here)
searches (with
web_search: trueon DeepSeek's API, each search is a request of its own on the same key): thesession's cost still reads as known without them, though a search can cost more than the steps around it.
Counting each search as a call of unknown tokens instead would leave the session's whole cost out of a USD budget;
as a call of unknown tokens);
usage at its end, after its tool calls ran, and a stopped session writes nothing more. Its cost is then that of
its earlier steps; a session stopped in its first step reports no usage, so the acting agent's record has an
unknown cost.
unavailable) keeps only its last attempt's usage, for every CLI: the actingagent's retry loop on
mainreports the last result's usage, and none when every attempt ends in an outage (thetask's error record has no cost and no tokens: a USD budget adds nothing for it and notes one call of unknown cost, a
tokens budget none of the attempts' tokens). dsh's outage ends reach that loop, and a dsh session can end with one
after billed steps. A separate change (it is shared code).
so a provider's message that names a network, a connection, a timeout or a rate limit (a refusal's explanation, say)
is retried by dsh and then taken for an outage, run again and left at an unknown cost; no real refusal text with such
a word was found. A chat-completions reply whose only output is the
refusalfield and that endsstophas nocontent for dsh, because pi-ai does not read that field: it ends
EMPTY_RESPONSEwhatever the refusal says, so dshretries it and hermit takes it for an outage. No provider was shown to send such a reply to dsh's requests, which ask
for no structured output. For an error dsh leaves unclassified (
PI_AI_ERROR) the outage's words are matched as Pi'snotices are: a server error inside a 200 stream that names no overload ends the task as failed, as Pi's does, and a
malformed event whose quoted text holds an outage's words is taken for one.
holds both kinds: such a run cannot be resumed on this head.
main) fails each step: dsh's own provider is notreached from the evolver sandbox.
.envfiles: dsh also reads a.envin the working directory. One that sets a reserved name (PATH,HOME,a proxy,
DSH_*) stops dsh before it writes any event; the acting agent reports that the session did not start.hermit deploy --host dsh) is a file of the workspace,.dsh/hermit.patch.yml, anddsh's sandbox keeps no folder of the workspace read-only (Codex keeps
.codexread-only underworkspace-write): asession started by hand can edit it, and a stdio row it adds there runs outside dsh's sandbox in the next session
started with
--patch(reproduced with the installed dsh). Start such a session from a fresh deployment (a newfolder), or read the layer first. hermit's own sessions are not affected: only the acting agent loads this layer, and
it deploys the adapter anew into an emptied workspace before each attempt.
caps the output tokens instead, and compaction starts late on a small-context model.
AGENTS.mdwhen its file tools touch the folder. The acting agent'sadapter copy (
hermit/<id>/) holds one, identical to the context in the shipped templates.SANDBOX_UNAVAILABLE) andthe session still answers. With a task's data bound, SIGTERM ends the binding's bubblewrap, and dsh with it.
/tmpunderworkspace-write: dsh's mode lets a session write/tmpas well as the workspace, as dsh documentsit. Under bubblewrap its commands get an empty
/tmpof their own; under Landlock they write the real one, and so doits file tools (write, edit, ...) under either. A run whose directory lies under
/tmp(agent.params.logs, bydefault
.logsbeside the config, as in this PR's toy runs) therefore lets the session write its home, its logdirectory and the call's directory, as a Claude Code or Pi session can. Under bubblewrap its commands also do not see
a declared path under
/tmpoutside the workspace (a mount withoutat, anenvironment.readpath) or the session'shome, which dsh's read, glob and grep do see. Keep a dsh run's directory and its data outside
/tmp, or mount thedata inside the workspace (
at:).Known issues from review rounds 7 and 9-14 and the post-PR audit on
a43698d(low severity, not changed)own message before any request): Claude Code's
claude_permission_modein a dsh agent's params becomes dsh'spermission mode; a
providerroute given with no model. Aproviderwhosemodelsare plain strings (round 13) failsat the session's start too, but with a Python error (
AttributeError, orTypeErrorwithimage_input: trueand noeffort) instead of dsh's message; the documentation givesthe form, a list of
{id: ...}.DSH_HOMEwith a leading~(asexport DSH_HOME="~/x"leaves it): dsh expands it, hermit does not, so theuser's
.envis not copied for dsh's own provider, whose sessions then lack the key.sessionsfolder replaced by a link (by the session itself) is still resumed through it, fromwhere the session could read anyway.
agent.base: dshselecting dsh by its name; the dispatch ofhermit deploy --host dsh;agent: {name: cli, base: dsh}binding its model as dsh's; the dialecthermit checkandtrainrequire of a dshmodel's backend (
needs()); a one-key override of a dsh section, andagent.name: dshwith anotheragent.baserefused; a session's own tool server name dsh refuses (
tools_name; the deployed server's name is tested); a--patch=<path>inargs; the clamp of a Responses total below the counts; a cut message, thought or tool resultmarked as cut; a tool other than bash, write, edit or an MCP tool kept as a tool-call step; the key half of the
web-search rule;
effort: offturning thinking off on dsh's own provider; (round 9) the exponent form of a timeout inboth timeout markers (
1e-7ms: JavaScript prints a timeout under 0.000001 ms so), which round 8's patterns match andwhich read as exit 0 before them; only the decimal form (
1000.5ms) is tested; (round 12) a dangling link at thelayer's path of a kept home (its test plants only a link to an existing file). Not a behaviour but a gap in that test
(round 12): the call whose session leaves the pipe has no time limit, so a change that opens the layer after the
session hangs the test, and CI's job until its 45-minute limit, instead of failing it; and the test does not check
that a pipe was left.
evolver sandbox, and
hermit trainruns it in the sandbox only in the toy runs, which leaveevolver.resumeoff (Pion
mainhas no such test either; Codex has one).allows; round 10):
main's shared stream readers (cli._objects,trajectory.events) split lines there too, sosuch a run's
finalevent is not read: a completed run reads as failed, its answer dropped, and an outage as none.Claude Code's
resultevent is read the same way: a change of its own (split on\nonly).the task sandbox, or a row of a
--patchlayer inargs): whether the run completed, and whether it was an outage,are read from the last JSON line of type
finalin the stream, dsh's own unless a tool writes one. A tool's wholetext line changes neither, but a JSON line of that type written after dsh's end (dsh stops its stdio servers after
writing it), or at any time in a run dsh ended without one (SIGTERM), is taken for the run's end, so a failed run can
read as a success with the tool's text as its answer, and a completed one as failed or as an outage; no known tool
writes one. Any other JSON object line a tool writes is read as dsh's event too: of type
statusand phasestep_end, its usage is added to the session's tokens and cost (with no usage, after a line of typetext, the costbecomes unknown); of type
session, itssessionIdreplaces the session's; of typestool_callandtool_result,it adds a step to the trajectory. Output with no line end just before a line of dsh's (a tool's progress dots) hides
that line's event: if it is dsh's
final, a completed run then reads as failed and an outage as none; if it is astep's
statusline, that step's tokens are not counted. Whether its tools started is read from every stderr line, soa tool's line in dsh's form for a row that did not start fails a completed run, and in a failed run's text takes the
place of dsh's end; so does, with a backend that declares no
context_window, a tool line holdingCONTEXT_WINDOW_EXCEEDED, which brings the context-window hint to a run that ended otherwise. A tool's other textlines come before dsh's end in a failed session's text, whose last 600 characters keep that end; a JSON line of type
errorthat a tool writes is read as dsh's, in the stream's order, so one written after dsh's end comes after it, anda long one (about 600 characters) pushes that end out of the text.
fake,mockandlocal, soa developer whose environment sets
FAKE_BASE_URL,MOCK_BASE_URLorLOCAL_BASE_URLsees up to 10 of them fail,as
main's Pi tests do forfake; two runs on dsh's own provider copy a developer's$DSH_HOME/.envinto thetest's home for the call (the fake dsh never reads it).
with Claude Code's effort, which is checked at load only on a login; one says only dsh's own provider can turn
thinking off (dsh's pi-ai routes can too; hermit declares none for a bound model); one says dsh's project is the
nearest folder above with a
.git(it is the nearest with one, the working directory included, else the workingdirectory:
hermit/hosts/dsh.pysays so);Dsh.outage's says only thefinalevent tells an outage (see the itemon a tool's stderr).
check_skillscall (removing it fails no test, as onmainfor every host but Codex, the only one whose refusals hermit deploy retires skills the adapter no longer exports #97 tests). The merge commit1f25839alone, without947f146, lets a dsh redeployment replace a user's own.dsh/skills/<name>, since hermit deploy retires skills the adapter no longer exports #97'scopy_skillsreplaces askill's folder. The refusal looks only for a folder named as the exported skill: a user's flat
.dsh/skills/<name>.md, or a folder whoseSKILL.mdgives the samename, is not refused, and dsh then offers oneof the two by its own order (nothing on disk is replaced).
d66105a) is written only when the deployment copies a skill.After a redeployment that copies none, a person's
dsh --patch .dsh/hermit.patch.ymlsession in a workspace below arepository's root no longer loads the skills the user left in
.dsh/skills. The acting agent is unaffected: its ownlayer always names that folder.
main's test that the AHE Evolver's texts name every acting agent's stream now alsocovers dsh's, so this PR's dsh-only test repeats it, but for dsh's command (
dsh --profile headless --json), whichonly that test checks.
Review
Tests and documentation cover the affected behavior, where applicable. Tests: see Validation. Documentation: see
Docs under Change.
Commands, defaults, and public interfaces are consistent. New:
agent.base: dsh(agent.name: dsh),evolver.base: dsh, thedshEvolver, adsh:settings section (permission_mode,image_input, ...),hermit deploy --host dsh, and theprovidersetting for dsh.hermit checkpasses on a dsh config.Shared logs and examples contain no credentials or private data. The patch layer holds the key's variable
name, never its value. A grep found the Fireworks key in no record of the real run. The fixtures hold no key.
Documentation-only: N/A.
Core changes: see Change.
Failures reported accurately: tests cover a failed request (an outage sets
unavailable, so the task is runagain), a run stopped by SIGTERM (dsh exits 0), a run cut at the token limit, tools that did not start, a run that
started no session, a tool's stderr line with an outage's words (no outage: the task runs once), an overload a
provider sent inside a 200 stream (an outage), a command moved to a background job at its timeout or killed at it
(also at a timeout of 1000.5 ms), stopped, run in the background or cut (exit code unknown), and refused roles, login
and container. Two corner cases are not reported accurately, both under Known issues: a JSON line of type
finalwritten by a tool can make a failed run read as a success (no known tool writes one), and a line separator in a run's
last text makes a completed run read as failed.
Real entry points: in tests,
make("dsh").run, the acting agent'srun(with the deployed adapter's ownstdio tool server) and the dsh Evolver's
step(without the sandbox);hermit trainandhermit evalin the toyruns (see Validation), which leave
evolver.resumeoff: no test runs dsh through the Trainer (see Known issues). Thestand-ins are only at the edges (the CLI, the model).
Test checklist: the dsh tests are in
tests/core/test_dsh_agent.py(andtest_deploy.py, the AHE, mwm andRRSI groups for their parts), named for the behaviour each holds;
tests/fixtures/dsh/is listed intests/selection.pyandtests/README.md, and its README says how each stream was recorded (dsh's output unchanged).Each review round's fixes were undone one at a time to see their test fail (round 8's: the decimal form of a timeout,
not its exponent form; see Known issues). The stand-ins are a fake
dshthat replays a recorded stream and a scriptedmodel on the loopback. Some inputs are made in
test_dsh_agent.pyitself: usage removed from a recorded stream (asdsh writes a step whose usage it left out), a stream's events trimmed or spliced, an end's code swapped (for codes dsh
writes;
AUTH's keeps the recorded 503 words on purpose, so that a refusal's words are shown not to be read) or itsmessage replaced (pi-ai's
No API key), a tool's stderr line inserted, and three bash results: the two[stopped: …]ones, as dsh's bash tool renders them, and one marked
error, whose text is a placeholder. Tests that run theinstalled dsh are marked
integrationand skip without it or without bubblewrap. Tests write totmp_path, and thecheck under bubblewrap to a directory of its own under
~/.cache/hermit-tests(outside/tmp: see Known dshbehaviours), removed after it. Runtime (
--durations, in the test container,par-1e75c6f): the 43 tests oftest_dsh_agent.pyand the 2 dsh tests oftest_deploy.py, about 29 s with other containers running beside them: the11 that run the installed dsh 15.8 s (the slowest 2.8 s), the 32 others, which CI runs, 12.2 s (none over 1.1 s), and
the 2 of
test_deploy.py1.1 s; the[dsh]cases oftest_ahe_hosts.pyandtest_sandbox.py0.1-0.2 s each, themwm and RRSI parts under 0.1 s.
Sandbox checklist: every dsh session runs through
hermit.sandbox: the acting agent in the task sandbox(
isolate.py's worker), the dsh Evolver in the Trainer's evolver sandbox (the Fireworks run's records, below).dsh's own sandbox runs nested inside the run's for the acting agent (
workspace-write), and is off inside theevolver sandbox, as Codex's is. dsh starts the deployed adapter's tool server (the stdio row of
.dsh/hermit.patch.yml) outside its own sandbox but inside the task sandbox, with dsh's environment; the row carriesHA_ADAPTER_SANDBOX, so the adapter's code runs in the Session's nested sandbox, as with Codex's server (seeValidation). The task data's binding bubblewrap also runs inside the task sandbox and only adds mounts. The sandbox
itself is unchanged;
isolate.pychanges only in its config check (see Core changes), which the checklist counts asa core change: pending the core team's agreement, so this stays unticked until it is linked here.
The Fireworks run's sandbox records on
207ea88(excerpt)rollout-1.json: role rollout, enforced, network isolated, allowapi.fireworks.ai, probe passed (the networkoutside and the run's data closed); environment names
DSH_HOME,FIREWORKS_API_KEY,HA_ADAPTER_SANDBOX_ENFORCE,HA_RESULT_FD,HOME,PATH,PYTHONPATHand the proxy variables.learner-1.json: role learner, enforced, network isolated, allowapi.fireworks.ai, probe passed; programdsh's
lib/bin.js; environment namesDSH_AGENTS_HOME,DSH_HOME,DSH_TELEMETRY_DISABLED,FIREWORKS_API_KEY,HERMIT_DSH_NO_KEY,HOME,PATH,PYTHONPATHand the proxy variables; binds/w/adapter,/w/material,/w/task-info,/w/history,/w/state,/w/scratch,/w/output,/w/root,/w/method,/w/agent-logs,/w/agent_home.Review rounds before this update (fresh agents, separate from the author's session; in rounds 1-6 each
was given the general, test and sandbox checklists):
03fafff: two reviewers, 23 findings. Fixed in7a076fb, each with a test that fails withoutits fix: bash results cut at 8 KiB read as exit 0; markers anchored with Python's
$; background commands readas exit 0; tools that did not start went unnoticed;
startedtrue for a run that wrote only an error;--add-dirbroke every command for a path through a link;agent.dsh.permission_modewas dropped; a gitrepository's skills and
AGENTS.mdreached an unsandboxed session; a workspace below a repository's root lostits deployed skills; no check of the 64 KiB instruction cap; usage of a step that answered without usage was not
counted; an
effortwithout a model was dropped;user_home()ignoredDSH_HOME; AHE had no dsh wire; mwm andRRSI accepted backends they cannot run;
parse_logread Pi's and dsh's streams as Claude Code's; tests fordsh's sandbox inside the run's, the context-tool portability loop and the deployed server's sandbox variables;
docstrings. Explained, not changed: what dsh does not report (compaction), the agent-writable patch layer, resume
in the same directory, and the
.envstop (reported as no session). Docs: pending.7a076fb: two reviewers, 20 findings and questions (three found by both). Fixed in9d87553, eachwith a test that fails without its fix: task data was linked, which dsh's search does not follow; web search with
another backend's model went to DeepSeek on
DEEPSEEK_API_KEY; calls without reported tokens were priced at $0.00;the deployment marker could make a folder above the workspace the instructions' root; the deployed tool server did
not fail the session when it could not start; the size check left out dsh's 304-byte frame; the AHE texts named
Codex's stream for Pi and dsh; an effort level or permission mode dsh lacks failed every session instead of the
config; docstrings (the deploy command named no profile); tests for an AgentEvolver step, the deploy's limit and a
server name dsh refuses, and a recorded SIGTERM stream (
stopped.jsonl). Explained, not changed: dsh injects anested
AGENTS.mdwhen its file tools touch a folder (the templates' copies equal their context); the deployedskills row replaces a
skill-filesystemconfig of the user's own (documented); the context window. The round-1message overstated one change: a dsh usage error was already an error record per task, not a wrong answer; that
record now says dsh exited before starting a session (the run goes on).
9d87553: two reviewers, 16 findings and questions (two found by both). Fixed in2021944,each with a test that fails without its fix. One was a regression of round 2's own fix: its shared pricing rule
made Claude Code's results of refused requests (one turn, no tokens) cost unknown, and the AHE and RRSI merges
then dropped a step's known spend from the budget; it is replaced by the dsh-only
unreported_calls. The others:dsh's zstd transcripts escaped key scrubbing; the dsh learner was given an adapter's
AGENTS.mdas instructions;the dsh Evolver's settings were not checked at load, and YAML's bare
offwas notoff; the search rule went bythe backend's name; a missing
context_windowfailed every request on vLLM without saying why; the task-databinding had no test CI runs; wording (the deployed server row, a run dsh never started, the size check's scope,
the linked-data warning). Explained, not changed: dsh's refusal of every command without bubblewrap or Landlock
(not reproduced: a container without privileges fell back to Landlock); a session with bound data stopped by
SIGTERM ends without an event.
2021944: two reviewers, 14 findings, questions and checklist items (two found by both). Fixed inf4b7b09, each with a test that fails without its fix: a step dsh retried after a refused request lost its cachetokens and was priced as known (dsh drops the step's cache counts; recorded:
retried.jsonl), which also correctsround 3's account of retries; the dsh Evolver's checks missed the method named with its params and refused an effort
with the run's own model (they now run on the Trainer's params, in
hermit checkand beforetrain's first step);efforts dsh's own provider refuses passed;
evolver.dsh.instruction_fileswas dropped, not refused; the caller'sDEEPSEEK_SEARCH_BASE_URLmoved dsh's search without the sandbox; the context-window hint cut dsh's error.Explained, not changed: a home of compressed transcripts from earlier commits; an Evolver with no model reference
(as Pi's on
main);agent.dsh.instruction_files, like other free-form settings, does not reach the acting agent;a successful result keeps an unread
failuretext, as Pi's does.f4b7b09: two reviewers, 14 findings, questions and test gaps (one found by both). Fixed inf1a92ea, each with a test that fails without its fix: round 4's unknown cost for a retried step made a wholesession's spend invisible to a USD budget, while its tokens are known and hermit prices a cache write as input
(now priced); the caller's
DEEPSEEK_BASE_URLmoved dsh's own provider without the sandbox; the Evolver's checkrefused its own values, and the acting agent's
instruction_files: false, which would cut the adapter's context,was dropped; tests now pin the session's tool row (
failOnStartupError, its 30-minute call timeout), every rowDSH_OFFturns off and all six outage codes; wording:effort: offasks a bound model for no level (itsserver's default), and the context-window hint keeps dsh's error line as far as it fits. Explained, not changed:
a Responses server's own
total_tokensis taken as pi-ai gives it; a session with an unreported call has anunknown cost, which a USD budget does not count (as any unknown cost); a deployed tool server's stderr reaches
dsh's (not reproduced as a problem; the post-PR audit reproduced one outside the task sandbox, fixed in
cacb4ae).f1a92ea: two reviewers, 11 findings (none found by both); the fixes change claims and tests, nobehaviour. Fixed in
37aee8c: tests, each failing without what it checks: a session's ownenvreaching dsh had notest; the integration test of dsh's own sandbox now shows a command's write outside the workspace refused (it passed
with the sandbox off); a bash call that failed to run has no exit code, and a session whose folder, or the folder it
lies in, is a link is not resumed (neither was tested); and wording, in the reader's account: a stopped session's
last step is not counted (its docstring said it was a call of unknown tokens), dsh's own adapter, not pi-ai, drops a
cache count of 0, and a retried step's price is never less only for a price whose cached input is no dearer than its
input, and on the Responses API only as far as the server's
total_tokensholds the tokens; a stale test docstring.Explained, not changed: a sandbox's grant of
DEEPSEEK_BASE_URLdoes not reach dsh (see Layer 2); a task run againafter an outage keeps only the last attempt's usage, for every CLI (see Known dsh behaviours).
37aee8c: a workflow of five reviewers, one lens each (usage and cost; settings and entry points;the session at run time; parity with Pi and shared code; tests and claims), each finding then reproduced by a
separate agent told to refute it. 34 findings (27 distinct), all confirmed, none a wrong result in a correct
configuration. One was rated medium: no test guarded which steps of dsh's stream count as calls, the rules a USD
budget rests on; tested in
1bbd57c. Three more were rated medium by their reviewers and low by the agents thatreproduced them (the code was right). One was changed at the author's request: no test held a command dsh moves to a
background job at its timeout, what a timeout gives in a session unless dsh's job limit is reached, and only one
check keeps it from reading as exit 0.
f4792e0records one (promoted.jsonl) and tests it, and also a commandstopped from outside the call (
[stopped: <reason>]), whose two results are written by hand as dsh's bash toolrenders them: no headless session writes one. The other two were listed under Known issues as correct but untested:
agent.base: dshselecting dsh by its name, and the session's patch layer written past a link a session left in akept home, which round 11 rated medium and tested (
1e75c6f). The Pi fixes this branch carried moved to Name a Pi or mini-swe-agent acting agent's event stream in the AHE Evolver's texts #91 and Run every method on Pi (evolver.base: pi) #92(
207ea88), so that this PR changes only dsh, and the check under bubblewrap keeps its home under~/.cache/hermit-tests. Corrected in this description: its claims that mwm and RRSI both ranpiwithout its model(RRSI ran it on its model, with role settings Pi does not read), that round 6's fixes were all tests, that a stopped
session's cost is always known, that the acting agent's check at load is as Claude Code's effort check (that one
runs at load only on a login), and its list of usage dsh does not report (web searches were missing). The other low
ones are not changed (the author's call): see Known issues.
f4792e0: two reviewers, one on the test and its recorded stream, one on the claims, eachfinding then reproduced by a separate agent told to refute it. 6 findings (5 distinct), all confirmed, one in
part. Two were medium, both fixed in
d25958awith recorded results: dsh prints the timeout it applied to acommand, the model's when it gave one (
1000.5), and both timeout markers were matched only at a whole numberof milliseconds, so a command still running at such a timeout read as exit 0, and one killed at it that trapped
the signal as its trapped exit; and the marker alone, what a command that printed nothing yet gets, was
untested. The low ones are corrected: two in this round's own text (a docstring's order of dsh's markers, and
that the
[stopped: …]results are written by hand), and this description's diff size, stale since1bbd57c(
f4792e0added a file).d25958a: two reviewers, one on the code and its tests, one on the claims, each finding thenreproduced by a separate agent told to refute it. 6 findings (4 distinct), all low, all confirmed. Three were
wording this change's own text got wrong, corrected in
a43698dand here: dsh prints the timeout it applied(60 s when the model gives none, at most 600 s), not always the model's;
DSH_NO_OUTPUT's comment against abackground marker that stands alone; round 8's account of its low findings. The fourth, the exponent form of a
timeout untested, is not changed: see Known issues.
a43698d: four auditors, one lens each (correctness; tests; the sandbox; this description andthe docs plan), with the general, test and sandbox checklists; 11 of the 13 findings then went to a separate agent
told to refute it (not the docs plan's missing pages, nor the tests' runtime, which another finding's refuter
measured). 13 findings: 1 high, 2 medium, 10 low (the refuters held all 11, 6 in part). Fixed in
cacb4ae, eachwith a test that fails without it: (high) the web-search test bound its DeepSeek models through the developer's own
backends (
HA_BACKENDSor~/.config/hermit/backends.yaml,DEEPSEEK_BASE_URL,DS_BASE_URL), so it failedwherever those move
deepseek: it now binds them on the built-ins alone (in such an environmenta43698d's testfails and the head's tests pass); (medium) a deployed tool server writes on dsh's stderr, which the Base Agent read
as dsh's own: outside the task sandbox (
environment.runner: host,sandbox.enabled: false) a tool's line with anoutage's words made a session that failed for another reason (
max-tokens) an outage, runretriestimes and thenan error of unknown cost, which a USD budget did not count;
unavailableis now read from thefinalevent's errorcode, and
UNAVAILABLEismain's again. Corrected in this description: (medium) Layer 3's claim that the deployedtool server stops a session at its start as Codex's
requireddoes; the two accounts ofstarted; Name a Pi or mini-swe-agent acting agent's event stream in the AHE Evolver's texts #91 and Run every method on Pi (evolver.base: pi) #92 asopen drafts, and the files they share with this PR; the sandbox checklist, ticked while its
isolate.pyagreementis pending (it now also gives its records and the deployed tool server); the test checklist's runtime; the docs
plan's missing pages; the real entry points; in the fixture README,
tools_down.jsonl's port and where the BaseAgent keeps its stderr lines. Disclosed, not changed:
/tmpunder dsh'sworkspace-write, and no test through theTrainer. The drop of the caller's
DSH_*andDEEPSEEK_*variables under a sandbox that passes them needed nochange: the shared drop's test (Claude Code's) holds it.
0ddba64: a workflow of three reviewers, one lens each (the audit's fixes; the merge ofmain;this description), each lens's findings then reproduced by a separate agent told to refute them. 14 findings, all
held (2 in part). Two were medium. One was a regression of
cacb4ae's fix, fixed inace8fabwith a recordedstream (
overloaded.jsonl): a provider overloaded inside a 200 stream (the Messages API'soverloaded_error) endsthe run with dsh's unclassified
PI_AI_ERROR, which dsh does not retry, so the task was no longer run again; suchan end is now told by its words, matched as Pi's notices are. The other: this description had ticked the independent
review, which neither round 10's fix nor the head then pushed had had (unticked until round 12 had reviewed the
head). The low ones are corrected here (dsh as the fifth CLI; mini-swe-agent among those priced as on
mainandmissing from the AHE texts; a stale untested item; the audit entry's count; the runtime; the developer-environment
row, now run with the
rrsigroup; records that name their commit) and in a test docstring (parse_logbeforemini-swe-agent: a Base Agent, an acting agent and a deployment host #49), or disclosed under Known issues (a line separator in a run's last text, a tool's stderr in a failed session's
text, the tests' own backend names). One is outside this PR: the docs plan's proposed wording quotes
mainbeforemini-swe-agent: a Base Agent, an acting agent and a deployment host #49.
ace8fab(the post-PR audit of the published head): four reviewers, one lens each, aimed at whereearlier medium findings came from:
ace8fab's rule, as a table of every way a dsh run ends; the low findings andthe "not reproduced" or "explained" items of rounds 1-10, checked again in conditions they had not been judged in
(in two halves: the session at run time; configuration, the sandbox and deployment); and claims corrected in one
place but left in another, and every ticked box. Each finding then went to a separate agent told to refute it (one
agent per finding for the claims lens). 15 findings, all held (4 in part); 4 rated medium by their reviewers. Two
held as medium, fixed in
1e75c6f, each with a test that fails without it: the kept home's patch layer is writtenas a file of its own, never through a link or into a pipe the last session left (round 7's untested guard); an end
dsh leaves unclassified that is no outage, with a tool's outage words on dsh's stderr, runs once (
ace8fab's pathhad no such test). The other two were rated low by their refuters, and their items are corrected: a task whose every
attempt ends in an outage keeps no usage at all (
main's loop), and web searches and a compaction's request leavea cost that reads as known; a code change that counted each search as a call of unknown tokens was withdrawn, since
it would leave such a session's whole cost out of a USD budget. A refuter checking an "explained" item found one
more medium: the deployed patch layer's parity with Codex's
.codex/config.tomlis false (Codex keeps.codexread-only; see Known dsh behaviours). The low ones are corrected here and in
1e75c6f(PI_AI_ERRORas dsh's code,not pi-ai's; docstrings and test comments that
ace8fabmade stale or that misstated dsh; the tool's stderr item; alist item nested under the wrong one; the run results' split order; the round-7 account; the deploy test's name;
the inputs written by hand), or added to Known dsh behaviours (dsh tells an error's kind by its words).
1e75c6f(the post-PR audit of what changed sinceace8fab): two reviewers, one on the commit andits tests, one on this description's edits, each finding then reproduced by a separate agent told to refute it. 10
findings, none above low: 9 held (3 in part), 1 refuted (the Core changes box, which asks what changes and why, as
Change answers). The code is left as reviewed. The low ones are corrected here (a refusal that needs no word to be
an outage; what a tool's stderr can change; why hermit's own sessions are unaffected by an edited layer; the inputs
made by hand; what
1e75c6fadds) or listed under Known issues (the kept home's test: a dangling link, no timelimit on the call that leaves the pipe, and no check that a pipe was left; the project root in a test docstring).
f0d2091(the documentation): four reviewers, one lens each (the pages on the agents and thelearners; settings, limits and the sandbox; deployment, models, installation and the changelog; the documentation
followed as a new user, its commands run in the test image), each lens's findings then reproduced by a separate agent
told to refute them. 16 findings (10 distinct), none above low: 12 held, 4 in part. Corrected in
a7883d8, all ten:a dsh command's
/tmpis its own, the default sandbox refuses a declared writable path, and how to mount a pathinside the workspace (
guides/sandbox.md); how to write aprovider, and what a web fetch reaches; which price thecost follows;
dshnamed as DeepSeek Harness on the custom Evolver page; Landlock, the patch layer,llm-pi-aiandthe request left out of the usage are explained; a sentence's word order. One of them also showed a code behaviour,
listed under Known issues (a
providerwith plain strings formodels). Thena7883d8and this description'supdate were read back by two separate readers, each finding reproduced by a separate agent: 9 findings, all held (2
in part). Two were on the documentation (low), corrected in
d66105a: the sandbox guide's two ways to reach a pathunder
/tmpare for reading it, and the requests that summarize a session's history are uncounted each time. Sevenwere on this description (two medium: Run every method on Pi (evolver.base: pi) #92's account, stale since Run every method on Pi (evolver.base: pi) #92 8000 span> was rewritten, and a merge of
maincalledconflict-free that had conflicts), corrected here. These corrections were not read again.
1f25839and947f146(the merge of1b0d897, andhermit deploy --host dshretiring staleskills): four reviewers, one lens each (the conflict resolutions in the code, read with
git show --remerge-diff;OpenCode and dsh side by side in the code they share; dsh's adoption of hermit deploy retires skills the adapter no longer exports #97; the tests and, lightly, the
documentation), each lens's findings then reproduced by a separate agent told to refute them. The resolutions held:
every definition one side changed keeps that side's version, the 26 both sides changed are unions, every registry
and table gives each CLI its parent's behaviour (
main's for all but dsh,d66105a's for all but OpenCode, comparedby probes run on both parents), and every test of either parent is on the merged tree with the same outcome (one of
main's as the Pi case of this PR's parametrized version of it). 8findings (7 distinct), none above low: 5 held, 3 in part. Five are on this PR and listed under Known issues (dsh's
check_skillscall untested, found by two reviewers; what the skill refusal does not see; the patch layer's skillsrow; a repeated test). Three are on
main's side, the same on1b0d897, and are not changed here: an OpenCodeworkspace's AHE hooks run by hand do nothing and report nothing (as mini-swe-agent's since before this PR; the AHE
page says OpenCode is not one of the template's hosts); no test checks
hermit deploy --host opencoderefusing anadapter that is not plugin-portable; a change to
tests/fixtures/pi/alone does not select the RRSI test thatreads it.
This description: read end to end against the code at
207ea88and the run records it cites; the parts1bbd57c,f4792e0,d25958a,a43698d,cacb4ae,0ddba64,ace8fab,1e75c6fchange, against those commits.Rounds 11 and 12 read the corrections of the rounds before them, and two separate readers those made from round 12;
what those two found is corrected here and was not read again. This update (the documentation, the merge of
f469f59, round 13) was read back as round 13's entry says. The next one (the merge of1b0d897,947f146, round14) was not read back.
Independent review (post-PR audit): on
a43698d(above), onace8fab(round 11), on1e75c6f(round 12) and, forthe documentation, on
f0d2091(round 13) anda7883d8(its read-back), and for the merge of1b0d897, on1f25839and947f146(round 14): no high or medium finding remains. Their low findings arecorrected here or listed under Known issues. The code has not changed since
1e75c6fexcept by the merges off469f59(
77f3d75, no conflict) and1b0d897(1f25839) and by947f146, which round 14 reviewed; the checks above ran onthe head.
d66105a's two documentation sentences (the read-back's) were not reviewed again.🤖 Generated with Claude Code