/** * Superpowers bootstrap for DeepSeek Harness. * * Claude Code delivers `using-superpowers` through a SessionStart hook. DSH has * no shell hook layer, so this module builds the same bootstrap text and the * plugin injects it once per session from `agent/pre-step`. The marker at the * top makes the injection idempotent and lets a later step re-inject after * compaction (the old message is no longer in `session.surface.nodes`). */ import { createHash } from 'node:crypto' import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import { isLegacyClaudePath, warnLegacyOnce } from './legacy.js' const DEFAULT_MAPPING = [ 'Use the tools reported by the harness. In particular:', '- read/write/edit/bash for files and commands;', '- skill({ name }) to load a skill (no `superpowers:` namespace);', '- todo_write for task tracking;', '- subagent for asynchronous delegation, or workflow for synchronous fan-out.', ].join('\n') const cache = new Map() /** Remove a leading YAML frontmatter block, if present. */ export function stripFrontmatter(text) { if (typeof text !== 'string') return '' return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim() } /** Truncate to a UTF-8 byte budget, keeping a visible notice. */ export function truncateUtf8(text, maxBytes) { const limit = Math.max(1024, Number(maxBytes) || 65_536) if (Buffer.byteLength(text, 'utf8') <= limit) return text const suffix = '\n\n[bootstrap truncated by dsh-harness maxBytes]\n' const head = Buffer.from(text, 'utf8') .subarray(0, Math.max(0, limit - Buffer.byteLength(suffix, 'utf8'))) .toString('utf8') .replace(/\uFFFD+$/u, '') return head + suffix } /** Sort dotted version directory names without a semver dependency. */ export function compareVersions(a, b) { const pa = String(a).split(/[.-]/) const pb = String(b).split(/[.-]/) const n = Math.max(pa.length, pb.length) for (let i = 0; i < n; i += 1) { const x = pa[i] ?? '' const y = pb[i] ?? '' const nx = Number(x) const ny = Number(y) if (Number.isFinite(nx) && Number.isFinite(ny) && nx !== ny) return nx - ny const c = String(x).localeCompare(String(y), undefined, { numeric: true }) if (c !== 0) return c } return 0 } /** Candidate `using-superpowers/SKILL.md` paths, most specific first. */ export function superpowersCandidates({ home = homedir(), env = process.env } = {}) { const out = [] if (env.DSH_SUPERPOWERS_SKILL) out.push(env.DSH_SUPERPOWERS_SKILL) // DSH-owned copies come first: `dsh/install.sh --skills` copies the skill // bodies into ~/.dsh/skills, so a Claude Code rollback cannot break the // bootstrap. out.push(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')) out.push(join(home, '.agents', 'skills', 'using-superpowers', 'SKILL.md')) const root = join(home, '.claude', 'plugins', 'cache', 'claude-plugins-official', 'superpowers') if (existsSync(root)) { let entries = [] try { entries = readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .sort(compareVersions) .reverse() } catch { entries = [] } for (const version of entries) { out.push(join(root, version, 'skills', 'using-superpowers', 'SKILL.md')) } } out.push(join(home, '.claude', 'skills', 'using-superpowers', 'SKILL.md')) return out } /** Resolve the skill file: explicit config, env override, DSH-owned copy, then Claude Code fallbacks. */ export function resolveBootstrapSkill({ configuredPath = '', env = process.env, home = homedir() } = {}) { if (configuredPath && existsSync(configuredPath)) return configuredPath return superpowersCandidates({ home, env }).find((path) => existsSync(path)) } /** Build the exact text injected before the first model step. */ export function buildBootstrapText({ skillBody, mapping, marker }) { return [ marker, '', 'You have superpowers.', '', "**Below is the full content of your 'using-superpowers' skill — your introduction to using skills. On this harness, load other skills with the `skill` tool. There is no `superpowers:` namespace; use the skill name, for example `brainstorming`.**", '', skillBody, '', '## DSH tool mapping', '', mapping, '', '', ].join('\n') } function statKey(path) { try { const stat = statSync(path) return `${stat.mtimeMs}:${stat.size}` } catch { return '' } } /** * Build (or reuse) the bootstrap payload. * @returns {{path: string, marker: string, text: string}|undefined} */ export function prepareBootstrap({ skillPath = '', mappingPath = '', maxBytes = 65_536, logger, env = process.env, home = homedir(), } = {}) { const resolved = resolveBootstrapSkill({ configuredPath: skillPath, env, home }) if (!resolved) { logger?.warn?.('[dsh-harness] superpowers bootstrap skill not found; skipping injection') return undefined } if (isLegacyClaudePath(resolved, home)) { warnLegacyOnce(logger, 'bootstrap', resolved, 'skills') } const mappingFile = mappingPath && existsSync(mappingPath) ? mappingPath : '' const key = `${resolved}|${mappingFile}|${maxBytes}` const fingerprint = `${statKey(resolved)}|${mappingFile ? statKey(mappingFile) : 'default'}` const hit = cache.get(key) if (hit && hit.fingerprint === fingerprint) return hit.value let skillText try { skillText = readFileSync(resolved, 'utf8') } catch (error) { logger?.warn?.('[dsh-harness] cannot read superpowers bootstrap %s: %s', resolved, error?.message ?? error) return undefined } const skillBody = stripFrontmatter(skillText) if (!skillBody) { logger?.warn?.('[dsh-harness] superpowers bootstrap is empty: %s', resolved) return undefined } let mapping = DEFAULT_MAPPING if (mappingFile) { try { mapping = readFileSync(mappingFile, 'utf8').trim() || DEFAULT_MAPPING } catch { mapping = DEFAULT_MAPPING } } const hash = createHash('sha256').update(skillBody).update('\0').update(mapping).digest('hex').slice(0, 16) const marker = `` const text = truncateUtf8(buildBootstrapText({ skillBody, mapping, marker }), maxBytes) const value = { path: resolved, marker, text } cache.set(key, { fingerprint, value }) return value } /** Read model-visible text from a session message or a session event payload. */ export function textOf(value) { if (typeof value === 'string') return value if (Array.isArray(value)) return value.map(textOf).join('\n') if (!value || typeof value !== 'object') return '' if (Array.isArray(value.content)) return value.content.map(textOf).join('\n') if (typeof value.content === 'string') return value.content if (typeof value.text === 'string') return value.text return '' } /** * Whether this exact injection marker is still visible in the session. * Messages under consideration by the current pre-step are checked first; * then the durable visible surface, which drops a message after compaction. */ export function isInjectionVisible(agent, messages, marker) { if (!marker) return false if (Array.isArray(messages) && messages.some((message) => textOf(message).includes(marker))) return true const session = agent?.session if (!session || typeof session.eventAt !== 'function') return false const nodes = session.surface?.nodes if (!nodes) return false for (const seq of new Set(nodes)) { try { const event = session.eventAt(seq) if (event?.type === 'user/message' && textOf(event.data).includes(marker)) return true } catch { // A seq outside the current surface is not evidence of visibility. } } return false } /** Back-compat name for the pre-refactor bootstrap visibility check. */ export const isBootstrapVisible = isInjectionVisible