24 KiB
Raw Blame History

DSH port — design and spike notes

Status: spike 0.2.1, live guard/require-plan/bootstrap verified; /role command works, the model-invocable role skill and the asynchronous SDD protocol are verified live (three-task trial run). 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 + /role command + model-invocable role skill close
Hard rules PreToolUse shell hooks ctx.tools.guard Cordis guard + approval gates A/B/C logic reused, wrapper new
Superpowers plugin + SessionStart hook pinned sync of tag v7.0.1 (DSH_SUPERPOWERS_REF) into ~/.dsh/skills + agent/pre-step bootstrap content adapted by sync, bootstrap new
SDD Task (synchronous) subagent (asynchronous) / workflow semantics differ
Gitea CLI + skills bash + skills 1:1
OpenSpec CLI + .claude/skills + /opsx:* CLI + проектные .agents/skills close: нет слэш-команд
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. Roles — /role is a real command (ctx.commands.register); it reads ~/.dsh/roles/<role>.md, falls back to ~/.claude/roles/<role>.md, then to the repository's roles/, and reads the project .claude/roles/<role>.md. It injects the role text as a user message and starts a turn for the five-line summary. Because a model cannot call a slash command, skills/role/SKILL.md is the model-invocable loader: it infers the role, reads the file, and tells the model to follow it. 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.

  5. OpenSpec — воркфлоу идут проектными скиллами .agents/skills/openspec-*, которые пишет openspec init --tools agents. DSH читает этот каталог нативно; проверено сабагентом с cwd в проекте — все шесть имён в каталоге скиллов. Глобальной копии в ~/.dsh/skills нет: генератор обновляется вместе с CLI проекта, а копия старела бы молча. Слэш-команд нет — только скиллы. Установка в проект — bash dsh/install.sh --openspec; проверка — --check.

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 parity: the Claude Task is synchronous, while DSH subagent returns a child id and reports later. The sdd skill implements the asynchronous analogue (report files, notification as the wait, send_message for fix rounds, ledger as memory); there is no parity and the skill says so.
  • 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.
  • Pinned sync of superpowers (change dsh-port-v2, D-2): "vendored superpowers" is gone — no upstream skill text is copied as-is. The default install source is the clone at ~/.dsh/superpowers pinned to $DSH_SUPERPOWERS_REF (default v7.0.1). install.sh --skills clones or updates it, runs the vendored dsh/sync/sync-from-upstream.mjs (asserted transforms, namespace strip, dsh-tools.md override) and dsh/sync/verify-skills.mjs, and copies only the adapted tree; a broken or empty synced tree is never installed. Explicit sources (--superpowers-from, $DSH_SUPERPOWERS_SOURCE) are synced directly with no network. Upstream moves independently, so each release is a deliberate pin bump (an issue per release, D-7; the procedure is in dsh/README.md). Removing the legacy ~/.claude fallbacks is deferred until every machine has rerun --skills --roles; until then both runtime and installer warn when they use one.

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
  skills/role/SKILL.md    # model-invocable role loader (installed to ~/.dsh)
  install.sh              # --check / --global / --skills / --roles / --all
  mcp.codegraph.yml       # optional CodeGraph MCP row

The bundle imports only Node builtins and files inside this package. It deliberately does not import @deepseek-ai/dsh-llm or @deepseek-ai/schemastery: a local-directory profile install resolves imports from the profile, not from the DSH installation, so a DSH-internal import fails at activation with ERR_MODULE_NOT_FOUND. lib/message.js is a local copy of the tiny createUserMessage constructor, and Config was replaced by defaults normalized in apply(). The pure modules are testable with plain Node; this is why the port's 26 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
    approvalGates: true          # gates A/B/C; active only inside the guard chain
    bootstrap: true
    roleCommand: true
    selfCheck: true              # activation check of the DSH contract; not-ok is logged
    checkCommand: true           # register the dsh-harness-check command
    bootstrapSubagents: false
    hooksDir: ''                 # empty -> bundle/hooks -> ../../hooks
    rolesDir: ''                 # empty -> ~/.dsh/roles, legacy ~/.claude/roles, repo roles/
    projectRolesDir: ''          # empty -> <cwd>/.claude/roles
    roleNames: [dev, admin, analyst, qa]
    bootstrapSkillPath: ''       # empty -> ~/.dsh/skills, then legacy Claude Code paths
    toolMappingPath: ''          # empty -> bundle/dsh-tools.md
    bootstrapMaxBytes: 65536

Model routes and delegation depth

SDD needs two things the default profile does not give: a model choice for subagent and a second level of delegation for a background controller. dsh/subagent-models.yml is the opt-in overlay for both:

# 1. copy the two entries into the profile patch (desktop is Electron-managed)
open -e ~/.dsh/profiles/desktop/cordis.patch.yml   # append dsh/subagent-models.yml
# 2. restart DSH, then check
bash dsh/install.sh --check     # "ok: subagent maxDepth=2 (...)"

After the restart the subagent and subagent_fork schemas accept provider, model, reasoning_effort, and a background controller subagent can dispatch an implementer. Routes measured in 0.2.1-rc.3: deepseek-official/deepseek-flash (transcription and mechanics) and deepseek-official/deepseek-v4-pro (integration and every review). Without the overlay the agent runs everything on the session model and at depth 1; the sdd skill must say so in its report instead of pretending the routes exist.

The overlay stays out of dsh/bundle/cordis.patch.yml on purpose: the subagent-model-selection-settings row exists in the web/desktop profile and is absent in headless, and enabling it from the bundle would break headless boot with "requires ... in the Host scope".

bootstrapSkillPath resolution order: explicit path, DSH_SUPERPOWERS_SKILL, ~/.dsh/skills, ~/.agents/skills, the newest ~/.claude/plugins/cache/claude-plugins-official/superpowers/*/, then ~/.claude/skills. The DSH copy wins because dsh/install.sh --skills copies real files there; the Claude Code locations are a legacy fallback and the bundle logs one warning per process when it resolves one of them.

Install-time source resolution is separate and stricter (change dsh-install-source, pin in change dsh-port-v2): --superpowers-from → $DSH_SUPERPOWERS_SOURCE → the pinned clone ~/.dsh/superpowers at $DSH_SUPERPOWERS_REF (default v7.0.1, cloned or updated from $DSH_SUPERPOWERS_REPO) → the legacy cache → ~/.claude/skills. Explicit sources are synced in place with no network and must be a checkout root or its skills/ directory — a bare skills directory outside that layout is invalid, and --check and --skills fail the same way (narrower than change dsh-install-source #49; the spec delta records the MODIFIED). Explicit syncs stage into ~/.dsh/.skills.synced beside the clone dir, which stays reserved for the pin; an existing clone is updated by fetching $DSH_SUPERPOWERS_REPO (not its origin) and reused offline only when already on the ref. An invalid explicit source is a hard FAIL; a machine offline without a clone is a FAIL with the clone hint, falling back to legacy only when a legacy source exists.

OpenSpec skills

Воркфлоу OpenSpec ставит генератор CLI, а не обвязка:

cd <project>
openspec init --tools agents     # или: bash <harness>/dsh/install.sh --openspec
openspec update                  # обновить скиллы после апгрейда CLI

Скиллы лежат в .agents/skills/openspec-{propose,apply-change,archive-change,explore,sync-specs,update-change}/SKILL.md и перезаписываются генератором: править их руками нельзя. Команды адаптер agents не ставит, поэтому /openspec в DSH нет — модель вызывает скилл по фразе «openspec propose» / «openspec archive».

Цикл без Claude Code:

openspec new change <id>
openspec status --change <id> --json
openspec instructions proposal --change <id> --json
openspec validate <id> --strict
openspec archive <id> -y

6. Install and verify

Prerequisites: the claude-harness checkout, DSH 0.2.1-rc.3, python3, git, Node. Claude Code is not required at all: --skills clones the pinned superpowers checkout itself (DSH_SUPERPOWERS_REF, default v7.0.1) and runs the DSH sync + verify before installing, so a manual clone is no longer the first step; --roles copies the role files; the hooks come from this checkout.

# 1. Static checks (offline; reports the configured pin and the source ref)
bash dsh/install.sh --check

# 2. Global instructions, skills, and roles (run from a terminal; writes
#    outside the session workspace). --skills clones the pinned superpowers
#    checkout (DSH_SUPERPOWERS_REF, default v7.0.1) and syncs it; or pass
#    --superpowers-from / DSH_SUPERPOWERS_SOURCE to sync a checkout directly.
bash dsh/install.sh --global
bash dsh/install.sh --skills                      # or: --skills --superpowers-from DIR
bash dsh/install.sh --roles

# or all three at once:
bash dsh/install.sh --global --skills --roles

# 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.

--openspec добавляет agents-адаптер в текущий проект ($PWD) и требует существующий openspec/: без него скрипт отказывается и печатает готовую команду. В живой сессии проверка такая: в проекте с .agents/skills спросить «what skills do you have?» — должны быть шесть openspec-*.

--roles also appends the DSH SDD note to ~/.dsh/roles/dev.md (the bullet that points dev at the sdd skill). The note lives only in the installed copy: the shared roles/dev.md stays Claude Code-compatible, so a Claude session never sees DSH-only instructions. The insertion is idempotent — every --roles run recopies the role from the repository and re-inserts exactly one note.

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.

Verified live on 2026-10-24 in the desktop profile:

  • the bootstrap marker <!-- DSH-HARNESS-BOOTSTRAP:6b676dd9bb8a0234 --> and the full using-superpowers body arrived in a fresh session, and the skill catalog showed the superpowers skills;
  • write to a file on main returned BLOCKED [guard-main];
  • a Bash redirect write on main returned BLOCKED [guard-main];
  • a write on a feature branch was allowed;
  • write to src/probe.ts on the branch returned BLOCKED [require-plan], and the same write succeeded after creating .bounded.

Still pending: /role dev, the fresh-session "react todo list" acceptance, and re-injection after /compact.

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.

Composition smoke test (no model call): the bundle was installed with dsh plugin --profile test add dsh/bundle into a temporary DSH_HOME based on the shipped headless profile. --dump-config shows the dsh-harness row, the profile lists @local/dsh-harness in dsh.profile.bundles, and the package's internal hooks symlink resolves to the repository hooks/. The temporary profile was removed; the real desktop profile was not touched.

7. Risks and open questions

  • Instructions are not enforcement — closed by gates A/B/C. The upstream loop (builder-check → /approve <id> design → plan → /approve <id> plan → sdd) exists in superpowers as text, and the 24.10.2026 session skipped builder-check while the plan was written before design approval. Change enforce-upstream-loop adds lib/gates.js + command-approve.js: gate A denies a plan for an unapproved design, gate B denies an sdd start (a dispatch or a .superpowers/sdd/** write) for an unapproved or changed plan (sha256), gate C keeps approval.md /approve-only, and bash write targets are parsed by harness_target.py and gated too. Residual risk: only a session with the reinstalled bundle is gated (D-13), and terminal_* tools stay outside the chain (T3.6); the contract is pinned in openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md.
  • DSH is 0.2.1-rc.3. The plugin/event API is not frozen; pin the version and re-run the tests after upgrades.
  • Contract self-check. The plugin/event API drift is now caught from inside DSH: at activation the bundle checks the contract it depends on (ctx.tools.guard, ctx.on for agent/pre-step, ctx.inject for commands, workingDirectory.get), logs [dsh-harness] self-check: <feature> not-ok: <detail> for each missing service, and skips the feature whose service is missing (guards, bootstrap, or commands — each gated by its own check), so a DSH version without one of the services does not crash the bundle. /dsh-harness-check re-runs the same check on demand. This is the in-DSH compat check that closes RV-11 (D-6) — the terminal --check cannot see the SDK inside app.asar.
  • Local profile imports are not resolved from the DSH installation. The first install attempt failed at activation with ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/schemastery'. The bundle now imports only Node builtins and local files: lib/message.js replaces createUserMessage from @deepseek-ai/dsh-llm, and Config was dropped in favor of defaults normalized in apply(). Any future DSH service must be reached through ctx (injected service), never through an @deepseek-ai/* import; a package test enforces this.
  • 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. Verified live: the bootstrap marker and body arrived in a fresh session, and the skill catalog showed the superpowers skills. The local createUserMessage copy works with source.kind: "user", which avoids an unknown session source variant. Re-injection after compaction is still pending.
  • 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. The port uses two live routes (deepseek-flash, deepseek-v4-pro) from the opt-in overlay in dsh/subagent-models.yml; without it every dispatch runs on the session model.
  • One-sided model route. The trial's «child LLM provider and model must be supplied together» was not a race: the logged tool call carried only model (issue #52). DSH rejects a one-sided route correctly, so no upstream report is needed; the sdd skill now says to retry with both fields.
  • ~/.claude dependence. No longer needed for install, bootstrap, or roles: the install-time source is explicit, and --skills --roles copy real files into ~/.dsh. The Claude Code locations remain a legacy migration fallback; the installer, --check, and the runtime warn when they are used (runtime — once per process). An existing installation must rerun --skills --roles once to stop relying on them; removal of the fallbacks is a separate task.
  • 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. Finish the live smoke test: the fresh-session role skill acceptance, plus the bootstrap and /compact checks already verified.
  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 — done (change dsh-sdd): the sdd skill (async controller, reports, review, fix-loop, ledger) plus the overlay in §5; proven by a three-task trial run (openspec/changes/dsh-sdd/trial/, results in design.md).
  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.
  8. OpenSpec — done (change dsh-openspec): проектные скиллы, --openspec, --check; приёмка — в дизайне change.
  9. Install without ~/.claude — done (change dsh-install-source): --superpowers-from/DSH_SUPERPOWERS_SOURCE/~/.dsh/superpowers, hard FAIL instead of a silent empty bootstrap, DSH-owned role paths in the installed AGENTS.md, legacy warnings at install and runtime.