dsh-judge-worker
Verifieddsh-judge-worker · v0.1.0 · MIT
DSH plugin: dual-model orchestration for code tasks — a high-capability judge writes Task Contracts and reviews evidence, a cheap worker implements in a git worktree, and the plugin verifies with real commands.
Install
dsh plugin add dsh-judge-worker Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-judge-worker
A DSH plugin that runs one code task through two models with different jobs:
User intent
↓
Judge — a high-capability model: decides WHAT should be done, and whether it was done
↓ Task Contract (scope, invariants, acceptance, budget)
Worker — a cheap coding model: implements inside a disposable git worktree
↓
Verifier — the plugin itself: runs the contract's commands, reads the real diff
↓
Judge — reviews measured reality and decides: PASS / RETRY / RECONTRACT / SPLIT / ESCALATE
↓
Report — what changed, what was measured, what it cost, where the artifacts are
The judge decides, the worker produces, the verifier measures, the plugin enforces.
The point of the plugin is not the prompt. It is that the boundaries are enforced by code: a worker cannot widen its own scope, cannot accept its own work, and cannot hide behind a claim — because the plugin runs the commands and reads the diff itself.
Install
dsh plugin --profile web add dsh-judge-worker # or: add /path/to/this/checkout
The bundle patch mounts the plugin row; /judge-worker appears in the slash-command menu of every
session. Adding a bundle to a profile changes package.json, which the live patch watcher does not
reload — restart dsh web once after installing.
Configure the two models
Both models are configuration, never hard-coded. With no configuration the plugin follows the session's own model (and the deployment default for a fresh session), which is the cheapest way to try it:
# ~/.dsh/profiles/web/cordis.patch.yml
- id: judge-worker
config:
models:
judge: # the expensive one: plans and reviews
provider: gpt
model: gpt-6-astra
reasoningEffort: high
timeoutSeconds: 900
worker: # the cheap one: implements and repairs
provider: isrc
model: DeepSeek-V4.1-Flash
reasoningEffort: off
verification:
repositoryDefaults: # always run, a contract may add but never remove
- npm run typecheck
- npm test
reasoningEffort is adapter-owned and optional: it is passed through to the provider untouched, and
an effort the model does not advertise is refused before any request is sent. A judge benefits from
a high effort; a worker usually does not — the plugin measures the difference in the report's cost
section.
A patch replaces the row's whole config, so restate every key you want to keep. Per-run overrides:
--judge <model>, --worker <model>, --judge-provider <id>, --worker-provider <id>.
Use
/judge-worker fix the refresh-token flow so a client can log in again after expiry
/judge-worker --plan-only <task> # show the Task Contract the judge would use, then stop
/judge-worker --dry-run <task> # plan and validate the setup without changing any file
/judge-worker resume <run-id> # continue a run that was interrupted
/judge-worker runs # list this repository's runs
Options: --judge <model>, --worker <model>, --judge-provider <id>, --worker-provider <id>,
--max-attempts <n>, --keep-worktree, and -- to protect task text that starts with a dash.
Output:
Run: jw_20260919T141826_5y7mi1
Status: PASS
Judge calls: 2 · Worker calls: 2
Artifacts: <repo>/.dsh/judge-worker/runs/jw_20260919T141826_5y7mi1
...
What the plugin enforces (not what it asks for)
| Guarantee | How it is true |
|---|---|
| No contract, no execution | a worker only ever starts from a stored, validated ContractRevision |
| A worker cannot widen its scope | every write passes the scope guard; the real git diff is re-checked afterwards |
| A worker cannot accept work | the only path to ACCEPTED is a validated Judge PASS over a passing verifier |
| Machine facts beat model claims | the verifier runs the contract's commands and reads the diff itself |
| Cheap failures stay cheap | a failing test routes to a worker repair, never to the Judge |
| Scope violations are never auto-repaired | SCOPE_VIOLATION routes straight to review |
| No unbounded loop | every loop consults a budget guard before each costly step |
| A crash loses nothing | state is persisted after every transition; resume re-enters the state machine |
| The Judge never sees the transcript | review input is facts and references; content is fetched on request, bounded |
The worker runs as a plugin-driven tool loop, not as a subagent: the worker model emits one JSON
action per step (read_file, write_file, edit_file, search, list_dir, run_command,
finish) and the plugin executes it. That is what puts the scope guard and the shell policy in front
of every write and every command, and it is why a worker cannot escape its sandbox by asking nicely.
What a run leaves behind
<repo>/.dsh/judge-worker/runs/<run-id>/
├── intent.json plan.json state.json decision.json
├── contract-r1.json contract-r2.json probe-facts.md final-report.md
├── evidence/manifest.json evidence/probe-*.json
└── attempts/<task-id>/<n>/
├── worker-result.json worker-transcript.json verification.json
├── diff.patch diff.stat.json
└── logs/<command>.stdout.txt logs/<command>.stderr.txt
Worktrees live in <repo>/.dsh-worktrees/<run-id>/<task-id> and are kept by default — on failure so
you can inspect what went wrong, and on success because the accepted change lives there. The report
ends with the exact git apply command for every accepted task. The plugin never commits, merges,
or pushes: deciding how an accepted change enters your branch is yours.
Configuration reference
Every key is optional; defaults shown.
- id: judge-worker
config:
models:
judge: { provider: <id>, model: <id>, temperature: 0.1, maxTokens: 8192, reasoningEffort: <id>, timeoutSeconds: 600 }
worker: { provider: <id>, model: <id>, temperature: 0.1, maxTokens: 8192, reasoningEffort: <id>, timeoutSeconds: 600 }
execution:
maxWorkerAttempts: 3 # worker attempts per task before the judge is asked
maxRecontracts: 2 # how often the judge may rewrite a contract
maxTotalTasks: 12 # hard cap per run, including SPLIT-created tasks
parallelism: 1 # MVP: one task at a time
maxJudgeCallsPerTask: 4
maxWorkerCallsPerTask: 5
maxWorkerStepsPerAttempt: 24 # model steps inside one worker attempt
maxWorkerHistoryChars: 60000 # step-history budget in the worker's context
maxPlannerProbes: 2 # read-only probe rounds before a contract is required
commandBackend: host # `host` reuses the DSH shell sandbox; `local` runs directly
integrationVerification: true # combine accepted patches and re-run the repository checks
sandbox:
worktreeRoot: <repo>/.dsh-worktrees
keepWorktreeOnFailure: true
keepWorktreeOnSuccess: true # the accepted checkout is the deliverable; nothing is merged
allowNetwork: false # network-reaching commands are refused while false
extraDenyCommands: [] # extra regular expressions refused in worker commands
allowedCommands: [] # regular expressions overriding the built-in refusals
protectedPaths: [] # extra paths no contract can open
verification:
commandTimeoutSeconds: 600
maxLogBytes: 200000
failOnScopeViolation: true
runCommandsWhenScopeFails: false
repositoryDefaults: [] # e.g. ["npm run typecheck", "npm test"]
review:
includeFullDiff: false # the judge gets a bounded excerpt and can fetch more
includeWorkerTranscript: false
maxDiffExcerptLines: 160
evidence: { maxFetchLines: 400, maxFetchBytes: 65536 }
storage: { root: <repo>/.dsh/judge-worker, writeFinalReport: true }
prompts: { judgePlan: "", judgeReview: "", workerExecute: "", workerRepair: "", probe: "" }
logging: { verbose: false }
Protected paths (VCS metadata, dependency trees, .env, keys, credential files) can never be opened
by a contract, and a contract that claims the whole repository is rejected before a worker runs.
Development
npm install
npm run verify # typecheck + 88 unit/integration tests + build
npm run build # tsc → lib/, plus the prompt documents
The tests run the real controller against a real git repository with real command execution; only the
two models are scripted (tests/helpers/scripted-model.ts). The suite covers the design's acceptance
cases: a cheap repair loop that never consults the judge, a scope violation produced through the
shell, a blocked worker whose contract the judge widens, budget exhaustion, resume after a crash,
judge context isolation, and the plugin entry point driven through the host-facing surface.
Two of them are regressions from real production runs: a planner that returned an absolute path in
scope.allow (now rejected and handed back for correction instead of aborting the run), and a probe
whose absolute read paths reached the scope guard (now sanitized — and the facts it gathers now reach
the next planning round).
notes/ holds the verified API references this plugin was built against (the DSH plugin surface:
commands, ctx.llm, tools/presets, subagents, exec/fs/config), each with the exact declarations and
file paths they were read from. They are working notes, not part of the package.
Architecture notes:
src/adapters/is the only place that knows about DSH. Everything else depends on the ports insrc/adapters/types.ts, which is why the pipeline is testable without a host.src/contracts/,src/state/,src/sandbox/, andsrc/verifier/contain no model calls.src/prompts/*.mdis the policy layer; each document can be replaced wholesale throughprompts.
Releasing
scripts/release.sh 0.1.1 # verify → bump → commit → tag → push; CI publishes
Publishing runs in GitHub Actions (.github/workflows/release.yml) through npm trusted
publishing: CI authenticates with a short-lived OIDC token, so no long-lived npm credential exists
on any machine, and every published version carries a provenance attestation. The workflow refuses to
publish when the tag and package.json disagree.
One-time setup on npmjs.com — package → Settings → Trusted Publisher → GitHub Actions:
theonlyrnh / dsh-judge-worker / release.yml.
Publishing by hand is still possible (npm login && npm publish); prepublishOnly runs the full
verification first. Either way the package is built, never committed: lib/ is git-ignored and
produced by the prepare script, so a git-installed copy
(dsh plugin add github:theonlyrnh/dsh-judge-worker) builds itself.
License
MIT