/** * dsh-harness — the DeepSeek Harness adapter for claude-harness. * * Three Claude Code integration points are replaced by Cordis hooks: * - PreToolUse Edit/Write/Bash -> `ctx.tools.guard` calling the existing * `hooks/guard-main.sh` and `hooks/require-plan.sh` through a thin adapter; * - SessionStart superpowers -> `agent/pre-step` injecting * `using-superpowers` once per session (and again after compaction); * - `/role` skill -> a real `/role` command registering on * `ctx.commands`. * * Everything else (CLAUDE.md, roles, OpenSpec, Gitea, codegraph, project * templates) stays in the repository and is consumed as ordinary files. * * The bundle intentionally has no `@deepseek-ai/*` imports: a local-directory * profile install resolves imports from the profile, not from the DSH * installation, so only Node builtins and files in this package are safe. */ import { existsSync } from 'node:fs' import { homedir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { registerRoleCommand } from './command-role.js' import { createGuard } from './lib/guards.js' import { isBootstrapVisible, prepareBootstrap } from './lib/bootstrap.js' import { createUserMessage } from './lib/message.js' import { DEFAULT_ROLE_NAMES } from './lib/role.js' export const name = 'dsh-harness' export const inject = ['tools', 'workingDirectory'] /** Resolve the hook directory for both a linked checkout and a packed package. */ function resolveHooksDir(bundleDir, configured) { if (configured) return configured const local = join(bundleDir, 'hooks') if (existsSync(join(local, 'guard-main.sh'))) return local return join(bundleDir, '..', '..', 'hooks') } /** Prefer DSH-installed roles, then Claude Code, then the repository source. */ export function resolveRolesDir(bundleDir, configured, home = homedir()) { if (configured) return configured const dshInstalled = join(home, '.dsh', 'roles') if (existsSync(join(dshInstalled, 'dev.md'))) return dshInstalled const claudeInstalled = join(home, '.claude', 'roles') if (existsSync(join(claudeInstalled, 'dev.md'))) return claudeInstalled const source = join(bundleDir, '..', '..', 'roles') if (existsSync(join(source, 'dev.md'))) return source return dshInstalled } function isSubagent(agent) { if (!agent) return false if (agent.parent || agent.parentAgent || agent.parentSessionId) return true if (typeof agent.depth === 'number' && agent.depth > 0) return true return false } function sessionCwd(workingDirectory, agent) { if (agent?.session) { try { const cwd = workingDirectory.get(agent.session) if (typeof cwd === 'string' && cwd !== '') return cwd } catch { // Fall through to the process directory. } } return process.cwd() } const DEFAULTS = { enabled: true, guards: true, bootstrap: true, bootstrapSubagents: false, roleCommand: true, hooksDir: '', rolesDir: '', projectRolesDir: '', roleNames: DEFAULT_ROLE_NAMES, bootstrapSkillPath: '', toolMappingPath: '', bootstrapMaxBytes: 65_536, } export function apply(ctx, rawConfig = {}) { const config = { ...DEFAULTS, ...(rawConfig ?? {}) } if (!Array.isArray(config.roleNames) || config.roleNames.length === 0) { config.roleNames = DEFAULT_ROLE_NAMES } const bundleDir = dirname(fileURLToPath(import.meta.url)) const hooksDir = resolveHooksDir(bundleDir, config.hooksDir) const rolesDir = resolveRolesDir(bundleDir, config.rolesDir) const guard = createGuard({ hooksDir, logger: ctx.logger }) if (config.enabled && config.guards) { // A synchronous, monotonic guard: it runs after the extensible // `tools/pre-execute` waterfall, so no other listener can turn a denial // back into permission. ctx.tools.guard((exec) => { const reason = guard.check(exec, sessionCwd(ctx.workingDirectory, exec.agent)) if (reason !== undefined) { ctx.logger?.info?.('[dsh-harness] blocked %s: %s', exec.name, reason.split('\n')[0]) } return reason }) } if (config.enabled && config.bootstrap) { ctx.on('agent/pre-step', async ({ agent, signal }, next) => { const decision = await next() if (decision.kind === 'reject' || signal?.aborted) return decision if (!config.bootstrapSubagents && isSubagent(agent)) return decision const prepared = prepareBootstrap({ skillPath: config.bootstrapSkillPath, mappingPath: config.toolMappingPath || join(bundleDir, 'dsh-tools.md'), maxBytes: config.bootstrapMaxBytes, logger: ctx.logger, }) if (!prepared) return decision if (isBootstrapVisible(agent, decision.messages, prepared.marker)) return decision return { ...decision, messages: [ ...(decision.messages ?? []), createUserMessage({ content: [{ type: 'text', text: prepared.text }], source: { kind: 'user' }, }), ], } }) } if (config.enabled && config.roleCommand) { ctx.inject(['commands'], (commandCtx) => { registerRoleCommand({ commands: commandCtx.commands, workingDirectory: ctx.workingDirectory, rolesDir, projectRolesDir: config.projectRolesDir, roleNames: config.roleNames, logger: ctx.logger, }) }) } ctx.logger?.info?.( '[dsh-harness] ready: hooks=%s, bootstrap=%s, /role=%s', config.guards ? hooksDir : 'off', config.bootstrap ? 'on' : 'off', config.roleCommand ? 'on' : 'off', ) }