11 KiB

DSH port — design and spike notes

Status: spike 0.1.0, live verification pending. This directory is the DeepSeek Harness adapter for claude-harness; the Claude Code installation and the repository's source of truth are unchanged.

1. Verdict

This is a port, not a copy. Three layers map differently:

Layer Claude Code DeepSeek Harness Status
Instructions ~/.claude/CLAUDE.md, project CLAUDE.md $DSH_HOME/AGENTS.md, project CLAUDE.md/AGENTS.md, nested almost 1:1
Skills ~/.claude/skills, .claude/skills ~/.dsh/skills, .agents/skills, customSkillDirs 1:1 format
Roles roles/, /role skill same files + a real /role command close
Hard rules PreToolUse shell hooks ctx.tools.guard Cordis guard logic reused, wrapper new
Superpowers plugin + SessionStart hook skills + agent/pre-step bootstrap content 1:1, bootstrap new
SDD Task (synchronous) subagent (asynchronous) / workflow semantics differ
Gitea, OpenSpec CLI + skills bash + skills 1:1
codegraph MCP MCP (mcp__codegraph__*) 1:1 with names
auto-memory Claude Code feature none dropped/replaced with files
install install.sh + settings.json bundle + profile manager new

The important discovery: DSH loads CLAUDE.md as a project instruction file, so the project templates and nested instructions need no rewrite. The global file has to be installed as $DSH_HOME/AGENTS.md because DSH does not read ~/.claude/CLAUDE.md.

2. What this spike implements

  1. guard-main and require-plan through ctx.tools.guard. The adapter synthesizes the Claude hook payload ({cwd, tool_name, tool_input}), runs the existing hooks/guard-main.sh / hooks/require-plan.sh, turns exit 2 into a denial reason, and fails open on every other error. The Python core (hooks/harness_target.py) and its tests remain the source of truth. A guard is synchronous and monotonic: it runs after tools/pre-execute, and no later listener can turn the denial back into permission.
  2. Superpowers bootstrap through agent/pre-step. It reads the installed using-superpowers/SKILL.md, strips frontmatter, appends bundle/dsh-tools.md, wraps the result in <EXTREMELY_IMPORTANT>, and injects it once per session. A content-hash marker handles dedupe; scanning session.surface.nodes re-injects after compaction.
  3. /role as a real command (ctx.commands.register). It reads the global ~/.claude/roles/<role>.md and the project .claude/roles/<role>.md, injects the role text as a user message, and starts a turn for the five-line summary. Auto-memory is explicitly dropped; the role's "Читать на старте" section is left to the model.
  4. Display metadata and an optional CodeGraph MCP overlay so the bundle can be installed through the Plugins page and codegraph stays available.

3. What is deliberately deferred

  • harness-check and role-hint: non-blocking SessionStart hooks. They can become agent/pre-step injections later; the first message role rule already covers most of role-hint.
  • briefing: needs an agent preset with read-only tools and a model route; not part of the guard/bootstrap spike.
  • SDD: the Claude Task is synchronous, while DSH subagent returns a child id and reports later. This needs a semantic rewrite (async controller with send_message + maxDepth: 2, or a workflow script). Do not claim SDD parity from this spike.
  • Codegraph by default: dsh/mcp.codegraph.yml is an opt-in overlay; the bundle does not assume the binary is installed.
  • harness-doctor: its scripts inspect ~/.claude; a DSH variant is a separate task.
  • Auto-memory, caveman: no direct DSH equivalents; caveman is off by default anyway.
  • Full installer: install.sh prepares global instructions and skills; the bundle itself is installed by plugin_manager because the Desktop profile is Electron-managed.

4. Architecture

dsh/
  bundle/
    package.json          # dsh.bundle.patch, exports, display resources
    cordis.patch.yml      # inserts the dsh-harness row
    index.js              # ctx.tools.guard + agent/pre-step + /role wiring
    command-role.js       # /role command glue (DSH imports live only here)
    lib/guards.js         # Claude-hook JSON adapter, fail-open
    lib/bootstrap.js      # skill resolution, marker, dedupe, compaction
    lib/role.js           # role parsing and text building (pure)
    dsh-tools.md          # superpowers tool mapping
    hooks -> ../../hooks  # local-install symlink to the tested bash/Python hooks
    locale/{en,ru}.json
    icon.svg
  tests/                  # Node tests, no DSH runtime needed
  install.sh              # --check / --global / --skills / --all
  mcp.codegraph.yml       # optional CodeGraph MCP row

index.js is the only file importing DSH packages (@deepseek-ai/dsh-llm, @deepseek-ai/schemastery). The pure modules are testable with plain Node; this is why the port's 22 tests run without a DSH process.

Guard flow

model calls write/edit/bash
  -> tools/pre-execute (approval, extensible policy)
  -> ctx.tools.guard (dsh-harness, synchronous, monotonic)
     -> lib/guards.js maps tool + args
     -> bash hooks/guard-main.sh (cwd, tool_name, tool_input)
        -> python3 hooks/harness_target.py guard-main
     -> exit 2 => deny reason (final)
     -> exit 0 or any failure => allow
  -> require-plan.sh for write/edit
  -> execution

The guard returns a denial; the tool pipeline materializes it as the model-facing error. Other listeners cannot turn it back into permission. A script failure, missing Python, or malformed JSON fails open, matching the harness invariant.

Bootstrap flow

agent/pre-step
  -> next() (other injections happen first)
  -> skip if reject/aborted, or if subagent and bootstrapSubagents=false
  -> prepareBootstrap(): resolve skill, read mapping, hash content, build text
  -> isBootstrapVisible(agent, decision.messages, marker)?
       yes -> no-op
       no  -> append createUserMessage({ source: {kind:"user"} })

The marker is an HTML comment at the top of the message. The visible-surface scan uses agent.session.surface.nodes + eventAt(seq), the same mechanism the skill catalog uses, so a compacted message is re-injected on the next step.

5. Configuration

The bundle row can be configured from the profile patch. Defaults are suitable for the repository layout:

- id: dsh-harness
  config:
    enabled: true
    guards: true
    bootstrap: true
    roleCommand: true
    bootstrapSubagents: false
    hooksDir: ''                 # empty -> bundle/hooks -> ../../hooks
    rolesDir: ''                 # empty -> installed ~/.claude/roles, else repo roles/
    projectRolesDir: ''          # empty -> <cwd>/.claude/roles
    roleNames: [dev, admin, analyst, qa]
    bootstrapSkillPath: ''       # empty -> newest superpowers plugin cache
    toolMappingPath: ''          # empty -> bundle/dsh-tools.md
    bootstrapMaxBytes: 65536

bootstrapSkillPath resolution order: explicit path, DSH_SUPERPOWERS_SKILL, the newest ~/.claude/plugins/cache/claude-plugins-official/superpowers/*/, then ~/.claude/skills, ~/.dsh/skills, and ~/.agents/skills.

6. Install and verify

Prerequisites: claude-harness installed in ~/.claude (hooks, roles, skills), superpowers 6.4.x, DSH 0.2.1-rc.3, python3, git, Node.

# 1. Static checks
bash dsh/install.sh --check

# 2. Global instructions and skills (run from a terminal; writes outside the
#    session workspace)
bash dsh/install.sh --global
bash dsh/install.sh --skills

# 3. Install the bundle in DSH:
#    Plugins -> install bundle from dsh/bundle
#    or ask the agent:
#    plugin_manager install_bundle target=/absolute/path/to/claude-harness/dsh/bundle

# 4. Optional: CodeGraph MCP
#    apply dsh/mcp.codegraph.yml as a --patch overlay; the tool is
#    mcp__codegraph__codegraph_explore.

Live checks:

  1. New session: ask "what skills do you have?" — superpowers skills and the DSH catalog should be visible.
  2. On main, ask the agent to create a file — expect BLOCKED [guard-main].
  3. In a worktree on a feature branch, edit src/... without a plan — expect BLOCKED [require-plan]; add .bounded and retry.
  4. /role dev — expect the five-line summary and the injected role rules.
  5. The acceptance prompt "Let's make a react todo list" must trigger brainstorming before code. If it does not, the bootstrap is not visible; check the plugin row and the skill path.
  6. Guard marker: the injected message starts with <!-- DSH-HARNESS-BOOTSTRAP:<hash> -->; after compaction a new marker is appended.

Automated tests:

bash dsh/tests/run.sh

They cover main/detached guards, branch allowance, Bash writes, require-plan with .bounded and a branch plan, bootstrap frontmatter/marker/dedupe, role-layer composition, and package metadata.

7. Risks and open questions

  • DSH is 0.2.1-rc.3. The plugin/event API is not frozen; pin the version and re-run the tests after upgrades.
  • ctx.tools.guard is synchronous. The guard resolves cwd through workingDirectory.get(session) (sync) and runs the hooks with execFileSync. If a future DSH version changes the guard contract or makes cwd resolution async, the adapter needs a tools/pre-execute fallback. The guard is global and monotonic; it runs after tools/pre-execute, so an earlier user approval cannot turn a guard-main denial back into permission.
  • agent/pre-step injection. The code follows the shipped skill-catalog pattern (next() then append messages), but the live behavior of createUserMessage({source:{kind:"user"}}) in a host bundle still needs a smoke test. The source kind is intentionally user to avoid an unknown session source variant.
  • Subagent bootstrap. isSubagent probes common fields; if none are present on this version, the bootstrap reaches children too. The skill's <SUBAGENT-STOP> makes that harmless but token-expensive.
  • Model tiers. sonnet/haiku/opus are not DSH models. Map them to live provider/model routes before porting the SDD text.
  • Superpowers porting rules. The upstream guide requires a session-start bootstrap with no per-session opt-in; agent/pre-step satisfies that shape, but a real acceptance transcript is still required before claiming a port.
  • Hooks symlink. Local-directory installation links the checkout, so bundle/hooks -> ../../hooks works. A packed/published bundle must include a real copy of hooks/; the files field already lists it.

8. Next phases

  1. Live smoke test in DSH: guard, bootstrap marker, /role.
  2. Add harness-check and optional role-hint as non-blocking pre-step injections.
  3. Build briefing as an agent preset (read-only tools + model route).
  4. SDD spike: async controller (subagent + send_message, maxDepth: 2) for one plan, or a workflow-based executor; measure which fits the harness.
  5. Enable CodeGraph MCP by default when the binary is present; update role and project instructions to mcp__codegraph__codegraph_explore.
  6. Port harness-doctor checks to DSH paths.
  7. Decide the long-term home: keep dsh/ in this repository while it shares hooks/roles/skills, then extract a dsh-harness repository when the bundle gets its own release cadence.