# Conflicts: # docs/config-catalog.md # docs/event-producer-consumer.md # docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md # examples/acp-agent/tests/acp.snapshot.ts # packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts # packages/code-runtime/code-runtime-worker/tsdown.config.ts
10 KiB
AGENTS.md
DeepSeek Harness SDK is a plugin-based agent harness on vendored Cordis: everything is a plugin. Read docs/architecture.md before changing packages/; follow docs/AGENTS.md for documentation.
Pre-release stance: foundation over blast radius
Remove this section at the first tagged release. With no external consumers, prefer the correct foundation over compatibility shims: rename or repackage freely and update every reference together. Backends reject old on-disk formats. SQLite uses monotonic SCHEMA_VERSION; dsh-session keeps SESSION_FORMAT_VERSION at 0 with no compatibility promise.
Repository layout
vendor/ Vendored Cordis source — manifest + sync procedure in vendor/README.md
packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai/dsh-<pkg>
core/ product API spine: session, system-prompt, tools, agent, agent-loop, agent-core (the bundle)
llm/ LLM seam + the DeepSeek adapters (hand-rolled + pi-ai design twin)
bash/ bash executor seam + local impl + model-facing bash tools
fs/ filesystem seam + local impl + policy gate + read/write/edit tools
skill/ skill provider registry + local impl + catalog/loader tool
web/ web seam + search/fetch providers + model-facing web tools
compact/ compaction seam + basic backend
subagent/ subagent seam + spawn/fork/ACP backends + delegation tool
workflow/ workflow seam + worker-thread engine + the workflow tool
todo/ the todo_write tool
guard/ loop-hygiene plugins
cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime
hooks/ Claude Code / Codex hook bridges + shared wire-protocol library
session-persistence/ persistence seam + JSONL/SQLite backends
ui/ ACP bridge, JSON-RPC SDK server, app-boot glue, stdio/ACP/SDK app bins, user-approval and user-interaction seams, ask-user tool
support/ dev/test infrastructure packages
util/ zero-dependency utilities
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
Per-package map: the group READMEs, indexed from packages/README.md.
Commands
pnpm install # pnpm workspaces, node ^22.19 || >=24
pnpm run test # vitest unit tests
pnpm run test:coverage # THE gating test run: per-file 100% coverage on packages/*/*/src
pnpm run test:e2e # real-API tests; self-skip without DEEPSEEK_API_KEY
pnpm run test:snapshot # keyless ACP replay vs goldens; filter: -t <name>
pnpm run test:snapshot:record # re-record goldens (needs key)
pnpm run typecheck
pnpm run lint
pnpm run build # tsc emits lib/types, tsdown bundles runtime
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json
pnpm run demo:echo # mock-model REPL, no key needed
pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key)
pnpm run demo:acp # ACP server agent (needs DEEPSEEK_API_KEY)
Run the CI gates locally before marking a PR ready
Run narrow checks during implementation and this CI-equivalent sequence before marking a PR ready. Fresh worktrees need pnpm run build before publint and NodeNext inspect lib/:
set -euo pipefail
pnpm run typecheck
pnpm run lint
pnpm run test:coverage
pnpm run test:snapshot
pnpm run doc-sync
pnpm run verify-module-graph
pnpm run build
pnpm run hygiene
out=$(printf 'echo ci smoke\n' | pnpm run demo:echo 2>&1)
printf '%s\n' "$out" | grep -q '\[tool call\] echo({"text":"ci smoke"})'
printf '%s\n' "$out" | grep -q '\[tool result\] ECHO: CI SMOKE'
test -n "$(find .sessions -path '.sessions/cwd-*/main-session-*.jsonl' -type f -print -quit)"
rm -rf .sessions
pnpm exec vitest run --config vitest.e2e.config.ts packages/ui/stdio-agent/tests/built-bin.e2e.ts packages/ui/acp-agent/tests/built-bin.e2e.ts packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts
test:coverage, not test, is the gate (why); report only commands actually run.
Secrets / .env
Real-API tests and demos read DEEPSEEK_API_KEY and optional DEEPSEEK_BASE_URL from the environment or a gitignored root .env loaded by process.loadEnvFile(). cordis.yml uses !!js (never !js) for env vars. Never commit credentials. CI e2e self-skips without a key; docs/testing.md owns the with-key policy.
Conventions
- Every npm package is
@deepseek-ai/dsh-<name>; vendored packages keep upstream names and areprivate: true.cordisis a peerDependency (+ dev) of every harness package. - ESM everywhere (
"type": "module"). Cross-package imports use package names, never relative paths; in-package relative imports use explicit.tsextensions. Dev/test/demo run unbuilt via tsx + the root tsconfigpathsmap; builds are for outside consumers only. - Registrations are effects: every contribution goes through
ctx.effect()/ctx.on(); a registry'sregister()returns the disposer. - Typed events use declaration merging; extensible unions use merge-extensible maps. Event JSDoc needs
@modeand payload@paramtags; public service methods document parameters and non-void returns. Catalog gates enforce this. - Switch on discriminant tags. Closed unions end in
assertNever; merge-extensible unions fall through a documented default. - Waterfall listeners MUST call
next()to delegate; returning without it is the veto (semantics). - Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.
- Plugins, not loop changes: new behavior goes on the documented extension seams; changing
agent-looprequires updating docs/architecture.md. - Capability seams are three packages — interface / implementation / consumer; don't split preemptively.
- Explicit > implicit at package seams: defaulting is an explicit
resolve(request): Specstep in the owning implementation, never a hidden?? defaultinsiderun()(thedsh-bashrequest/spec split is the template). - No hardcoded tunables in plugins: deployment choices are defaulted, validated
Configfields changeable from cordis.yml; aDEFAULT_*constant or test seam is not configurability. Protocol constants, external specs, and security invariants stay fixed. - Misconfiguration fails loud at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent.
- Opaque cross-boundary ids are branded (
Branded<B>fromdsh-brand), never barestring. - An empty
catchnames what it swallows and why nothing else can reach it; keep thetryto one statement. - Prefer symmetry for parallel values; unexplained asymmetry usually signals a missed extraction.
- Tests describe behavior, not correctness. Change obsolete behavior with its tests; explain why in the PR.
- Validate RFC premises against current code; friction may expose overreach, so amend proposals before moving them to
implemented/. - Testing policy — docs/testing.md. Transcript changes need snapshots or a PR note. Fixtures must replay on macOS/Linux; fix fixtures, not normalizers.
- A tool's ACP render intent is part of its design, decided up front (
generic/terminal/diff,locations); presentation methods are pure functions ofargs(cookbook). - Plan unit, e2e, and snapshot coverage for new seams, lifecycle shapes, and transcript surfaces, and schedule any missing harness support before implementation.
- Merge PRs with merge commits, never squash/rebase or rewrite pushed branches. Put a review fix on its introducing PR, then merge down the stack (guide).
- TODO markers:
FIXME/TODO/XXXby urgency (semantics). - Files end with exactly one trailing newline;
git diff --check(pre-push) gates it.
Defensive patterns
Read docs/defensive-patterns.md before lifecycle, concurrency, subprocess, or teardown work.
Type safety and documentation
Everything compiles under strict: true with noImplicitAny; every remaining any explains why a narrower type is infeasible. Every module and export has concise JSDoc for its non-obvious contract; function-like exports include @param/@returns, as enforced by verify-export-jsdoc. Heritage-declared members, plugin-protocol slots, and constructors keep their docs at the declaring seam, protocol, or class.
Comments and docs preserve complete contracts and non-obvious orientation, not the author's reasoning process. Do not narrate control flow, walk through tests, preserve review history, or restate code. Keep every factual clause that affects behavior, failure, timing, ownership, or safe use; link aggressively to the owning rationale instead of duplicating it. Use dsh-prose-standard for prose decisions. Encode enforceable invariants in checks, using narrow justified escape hatches rather than disabling a rule globally.
Docs are part of every change: code changes update their README and JSDoc in the SAME change; a bilingual-pair edit updates the counterpart and re-records (i18n contract). The writing rules — document the current state never the history, one physical line per paragraph, one home per fact — and the word-budget gate live in docs/AGENTS.md.
Editing these instructions
CLAUDE.md symlinks AGENTS.md at root, packages/, and examples/; edit the real file. Keep each rule self-contained while linking high-level docs. Condense when clarity survives; raise a verify-doc-budgets ceiling when the contract genuinely needs more space.
Vendoring policy
vendor/ packages are pinned source copies (manifest with upstream SHAs in vendor/README.md). Update via the sync procedure there; re-apply or retire the logged local modifications; rerun pnpm run test && pnpm run build.