24. Developing the plugin
One package, plain npm, no monorepo:
npm ci && npm run build && npm test # the deterministic suite: unit + integration, no modelnpm run coverage:ci # the ≥85% gate, same numbers CI enforcesnpm run test:e2e:replay # the browser acceptance chain, off model TAPES(Deliberately no test count here: a number typed into a page goes stale on the next commit, and the honest figure lives in the release ledger on §26.)
Development environments
Section titled “Development environments”Three setup modes live in dev/install/ — pick whichever fits your workflow. All three install the same
plugin set (dsh-chapters, dsh-codegraph, dsh-tinyfish-search) with the full Cordis configuration mirror.
Centralized (your live profile)
Section titled “Centralized (your live profile)”Installs directly into your existing web profile. Edits in this checkout are reflected after rebuild
via the link: protocol — no republish needed. Best for day-to-day development on your main machine.
./dev/install/install.shdsh webCompartmentalized (isolated DSH_HOME)
Section titled “Compartmentalized (isolated DSH_HOME)”Same installer, different home. Your live sessions and config stay untouched. Best for testing changes that might break things, or running multiple configurations side by side.
DSH_HOME=/tmp/dsh-dev ./dev/install/install.shDSH_HOME=/tmp/dsh-dev dsh webContainerized (full stack with vLLM + test runner)
Section titled “Containerized (full stack with vLLM + test runner)”A self-contained Docker Compose platform with three services:
| Service | Port | Purpose |
|---|---|---|
vllm |
8080 | Qwen3-4B-AWQ model server (inference + systemone endpoints) |
dsh-web |
3080 | DSH Web UI, pre-configured to use vllm |
dsh-test |
(none) | Node.js 22 + Playwright + Chromium for acceptance tests |
No external API keys required. Model weights cache in a Docker volume after first download. Environment
variables are read from your shell — no .env file needed.
cd dev/installdocker compose up -d # start everythingdocker compose run --rm dsh-test npm run test:e2e:replay # run testsdocker compose exec dsh-test bash # interactive test shellRequires Docker ≥ 24, Compose v2, NVIDIA GPU with nvidia-container-toolkit. Full documentation in
dev/install/README.md.
Containerized SDK API service (headless harness for external systems)
Section titled “Containerized SDK API service (headless harness for external systems)”A fourth mode hosts the harness as an SDK JSON-RPC API instead of a Web UI — for another system
that uses DeepSeek Harness as its agent runtime. compose.sdk.yml runs the same image in a gateway
role: one WebSocket listener (:8765, token-authenticated before any spawn) that per connection
launches dsh --profile sdk and relays the stdio protocol verbatim. The sdk profile carries the
identical configuration to the Web UI profile — Alibaba/Qwen token-plan marketplace, local vLLM
provider, and all three dev plugins including dsh-chapters — so hosted agents get the same tools and
zero-inference chapter compaction, tuned for small local models.
The model server is NOT part of this stack (#27): boot compose.vllm.yml (its own file,
its own GPU reservation), or point LLM_BASE_URL at any OpenAI-compatible server already
running on the host — another project’s vLLM included. The stacks share one network so the
default vllm hostname resolves across files; an external URL works without joining.
cd dev/installdocker compose -f compose.vllm.yml up -d # optional shared model serverGATEWAY_TOKEN=$(openssl rand -hex 16) docker compose -f compose.sdk.yml up -dThe serving side is stock host (@deepseek-ai/dsh-sdk-jsonrpc-server); only the thin relay is ours.
TypeScript consumers don’t speak the raw wire: the connector (dev/install/sdk-gateway/connector.ts)
offers a HarnessClient-shaped connectToGateway({url, token}).run(prompt) over one WebSocket, and
its usage guide records the four measured caller rules (profile provider routes, flat event envelope,
one-connection runtime lifetime, no mid-turn cancel):
CONNECTOR.md.
Deeper integration is possible and measured: the harness is library-first internally (the SDK server is
a plugin calling ctx.agents.create() on an in-process Cordis tree; boot() from app-boot is the
embedder entry), so a Hono-style Node app can embed the runtime instead of spawning children — shapes,
costs, and the npm dist-tag pin trap are in
dev/install/sdk-embedding-analysis.md.
Callers use @deepseek-ai/dsh-sdk-client with provider: 'local' or 'qwen-token-plan' at
initialize — never deepseek-official. One connection owns one runtime process; sessions survive
reconnection through session/resume. Why stdio-shaped hosting looks like this, the alternatives
(ACP, socat), and the measured host facts:
dev/install/sdk-service-analysis.md.
CodeGraph — structural code search
Section titled “CodeGraph — structural code search”The installer includes @jiangzhenguo/dsh-codegraph,
which builds an AST-level index of every symbol and call edge in the project. Instead of running dozens
of grep/glob/read round-trips to find a function or trace a flow, the agent calls codegraph_explore
with a natural-language description and gets back verbatim source plus call paths — including
dynamic-dispatch hops that text search cannot follow.
The plugin is configured with surface: core (four tools: status, init, sync, explore) and
frontload: true (injects structural context into prompts so the agent starts with code structure).
The index lives at .codegraph/codegraph.db and updates incrementally via codegraph_sync after edits.
The tape system (why CI has no LLM)
Section titled “The tape system (why CI has no LLM)”What coverage:e2e promises. It is a coverage measurement, not an acceptance gate. Under NODE_V8_COVERAGE=1 the
server is instrumented for its whole lifetime, and the browser legs’ ceilings (a 120 s wait for a door to render, a 300 s
test timeout) were set against uninstrumented timing. Measured on one tree: the same leg passes clean and fails the door
wait with coverage on, and across four attempts the failure landed on a different leg each time (suite, prompt, all of
them, fanout) with a different signature. The ceilings are not being raised — widening a wait to satisfy an
instrumentation mode is how a test stops meaning anything — so treat the e2e percentage as a report, and the blocking
number as the deterministic coverage gate in CI’s merge job. When you quote the e2e figure, quote the tree and the machine
conditions with it.
Two stale things, and only one of them matters. A rebuild makes the stamp stale; a prompt-byte change makes the
corpus stale. npm run corpus:check tells them apart (#156): a build-only difference prints an advisory and the replay
still runs, then npm run corpus:stamp re-records the stamp on a green run. A change to what the model actually reads —
tool and command descriptions, the dock label, the preset persona, the realm ratios and thresholds that decide when relief
fires — refuses outright, and every affected project has to be re-distilled, because a partly re-recorded corpus matches
nothing reliably. Before this the gate hashed the built bundle, so it demanded a full re-record for a commit that only
added diagnostic fields — and refused nothing when it mattered, since the check wasn’t run in CI at all.
A live leg asserts invariants, not a tally. When a browser journey waits on a model-driven turn, the number of
internal steps it takes belongs to the run: two live runs of the oversized-turn leg on one commit and one realm produced
8 and 38 relief passes. The assertion that bounded it (passes ≤ 30) therefore failed with nothing broken. What the leg
asserts instead is what the mechanism promises — every pass removes at least one node, no node is shadowed twice, and
relief leaves the surface under the realm’s trigger, read from the engine’s own decision note, which is written during
the run rather than parsed after it. A count may remain as a runaway guard, commented as one.
E2E browser tests would normally need a live model, a GPU, and minutes per spec. Instead, recorded model exchanges are committed under tests/fixtures/model-tape/ and replayed by a proxy; every request must match a recorded exchange after normalization on both sides (volatile values — ports, paths, token counts — are masked out), or the run fails loudly with a miss journal + diff dump (candidates sorted by shared prefix). Usage numbers ride the tape, so token-meter thresholds fire at identical steps — threshold behavior under test with zero model calls. E2E_MODEL=record distills fresh tapes from the local model — once per scenario, whole projects at a time (specs share a boot); re-record when product prompts change — doc edits are free with one measured exception: near-threshold scenarios (heavy, fanout) digest injected docs into model-visible checkpoints.
The discipline, condensed
Section titled “The discipline, condensed”- the point of a test is to test, not skip — a test that skips for a missing dep is a bug report;
- measure before you attribute — three “bug” reports this year were each a different thing (a unit mismatch, a capacity fact, and a real defect) — the audits that decode provider units from meter units are in the repo history;
- a tape records the context it was distilled in — one journey per boot, never two GPU suites at once;
- loud beats lossy — refusals carry numbers; degradations name their step.
The contributor-facing full loop: docs/development.md and AGENTS.md in the repo (the site you’re reading is for users; those are for the agents and humans working on it). Next: 25. Internals map.