- Home
- Plugins
- UI Enhancements
- dsh-harness-call
dsh-harness-call
domencai/dsh-harness-call
Delegate work to external coding agents (Claude Code / Codex CLI / Grok CLI) from DeepSeek Harness: a model-facing harness_call tool, @claude/@codex/@grok mentions, live progress cards with a thinking/tool timeline, and a details side panel.
Install
dsh plugin --profile web add github:domencai/dsh-harness-callREADME
dsh-harness-call
Delegate work to external coding agents — Claude Code, Codex CLI, Grok CLI — from inside DeepSeek Harness (DSH).
One model-facing harness_call tool, @claude / @codex / @grok composer mentions, live progress cards with a thinking/tool-call timeline, and a details side panel — with every harness translated into one normalized event model, so adding another CLI is a host-side change only.
Features
harness_calltool — the agent calls any installed harness with a self-contained prompt and gets back the final reply, a bounded process digest (the last 40 events, one line each), and arunIdhandle to the full structured timeline.@harnessmentions — type@in the composer to pick@claude/@codex/@grok. A mention is an intent marker, not command syntax: mentions bind to the question that follows them, so@claude @codex what do you think of Xsends the same question to both, while@claude review this log @codex write a testsends two different prompts. Enter on a bare mention is swallowed so the tag stays on one line while you type.- Normalized event model — each adapter translates its CLI's native JSONL into one
HarnessEventunion (session/reasoning/text/tool/file/error/usage/note), so the store, the cards and the panel all speak one language and a new harness needs no rendering change. - Runs keyed by
runId— the host keeps a run table, not one snapshot per harness: several calls to the same harness run concurrently without overwriting each other. A card finds its own run by the toolcallId, falling back to the newest run of that harness. - Incremental polling — the browser asks
get(runId, sinceSeq)and receives only the events after its cursor; one shared poller (2s) serves every card on the page and stops entirely once no call is live. - Process timeline — clicking a card opens the floating panel: the process list is collapsed on a finished run (open while it is still live), reasoning is de-emphasized, tool calls show their exit code and complete arguments behind a disclosure, and cost/turn accounting sits in the footer.
- No silent truncation — when a run's ring buffer evicts, the count is reported as
droppedEventsand the panel says the timeline starts mid-run. - Session auto-continue — each harness continues its own most recent successful session by default, so multi-turn follow-ups share context;
newSession/sessionIdoverride. - Adapter architecture — one harness is one adapter (
buildargv /translateevents /finalizeverdict) undersrc/host/adapters/; run identity, sequence numbers, relative timestamps and retention belong to the store, not to the adapters.
Requirements
- DSH with the web GUI (profile
web) - Node.js >= 20
- At least one of the CLIs installed and authenticated on PATH:
claude,codex, orgrok
Install
From npm — pinned releases:
dsh plugin --profile web add dsh-harness-call
Or straight from GitHub — tracks main; the built lib/ is committed, so nothing has to build on install:
dsh plugin --profile web add github:DomenCai/dsh-harness-call
dsh plugin forwards to pnpm inside the profile directory and then reconciles dsh.profile.bundles itself. This package declares dsh.bundle, so it joins the profile's layer stack automatically — no manual package.json edit. Restart DSH and hard-refresh the web GUI: the @ menu gains the harness group and the agent sees the harness_call tool.
Usage
Just talk to the agent:
@claude 帮我看看这个目录里有什么— delegates that question to Claude Code@claude @codex 各自怎么看这个架构方案— same question to both, replies synthesized@codex 分析这个崩溃日志 @grok 查一下相关 issue— two different prompts
Or ask naturally: “让 Claude 看看这个问题”, “对比一下 codex 和 grok 的回答”.
Settings
Settings → External harnesses has one card per CLI:
- Access: read-only, workspace-write, full access, or Model decides
- Reasoning effort: low / medium / high / xhigh, or Model decides
A concrete value is applied on the next launch and the tool arguments cannot override it. Model decides is the only case that reads this call's access / effort (codexSandbox remains a deprecated alias of access).
Defaults: Claude and Codex leave both fields to the model; Grok effort is pinned to high so an interactive TUI xhigh cannot leak in. A process that is already running keeps its original flags. Codex / Grok sandbox flags apply to new sessions only; a resume keeps the profile it was created with.
Configuration
All three settings are retention bounds on the host's in-memory run store. Override them in the profile's own patch layer, ~/.dsh/profiles/web/cordis.patch.yml:
- id: dsh-harness-call
config:
maxEventsPerRun: 400 # events kept per run before the ring buffer evicts
maxRuns: 50 # runs kept before the oldest finished one is dropped
promptPreviewCharacters: 280 # prompt characters kept for a card's preview line
The values above are the defaults; omit a key to keep its default.
Security posture
- Access comes from Settings. Full access maps onto each CLI's widest headless mode (Claude
bypassPermissions, Codexdanger-full-accessplus approval bypass, GrokoffplusbypassPermissions) — only turn it on in a trusted environment. - codex still defaults a new session to
read-onlywhen Settings is “Model decides” and the tool did not passaccess. A resumed session keeps the sandbox and writable roots it was created with. - claude runs on its own credential store:
ANTHROPIC_AUTH_TOKENandANTHROPIC_BASE_URLare removed from the child environment (removed, not blanked), so host-injected gateway credentials never reach it. - grok still pins
--reasoning-effort highwhen Settings is “Model decides” and the tool did not passeffort, so a TUIdefault_reasoning_effort = "xhigh"in~/.grok/config.tomldoes not leak into delegated one-shot calls. - Every run has a hard timeout (default 900s, clamped to 60–3600) and is terminated tree-wide on expiry or cancellation — SIGTERM, then SIGKILL after a 10s grace window.
Adding a harness
One adapter, one roster entry, one line of copy. The UI is untouched: it only knows normalized events.
src/host/adapters/<key>.ts— implementHarnessAdapter<S>; the contract lives insrc/host/adapter.ts:
export const myAdapter: HarnessAdapter<MyState> = {
key: 'myagent',
label: 'My Agent',
bin: 'myagent',
createState() { return { text: '', errorItems: [] } }, // this adapter's private fold state
build(req) { return { argv, stdin, env } }, // spawn spec; an `undefined` env value REMOVES an inherited variable
translate(native, state) { return [/* HarnessEvent[] */] }, // one native JSONL line → 0..n normalized events
finalize(state, outcome, info) { return { ok, text, sessionId, errors, extras } },
}
translate is translation only — no truncation, no sequence numbers, no timestamps, no display decisions. seq, the relative at, the ring buffer and droppedEvents belong to RunStore in src/host/runs.ts.
src/host/adapters/index.ts— add it toADAPTERS.src/shared/harness.ts— add the key toHARNESS_KEYSand its label; the tool'sharnessenum and the composer's mention list both derive from there.src/client/locales.ts— onecand.<key>line per locale, the mention's description.
Both the registry (typed total over HarnessKey) and the locale dictionaries are compiler-enforced, so a missed step is a build error rather than a runtime surprise.