#!/usr/bin/env node /** * Sync `skills/` from an obra/superpowers checkout, re-applying this port's * DSH adaptations. * * Vendored from LayneChai/superpowers-dsh `scripts/sync-from-upstream.mjs` * (MIT), adapted for claude-harness: * * - no `package.json` binding: the upstream checkout and the destination * come from argv (``, `--out`), not from the package; * - the DSH-only override is copied from `dsh/bundle/dsh-tools.md` over the * synced tree as `using-superpowers/references/dsh-tools.md` (resolved * from `import.meta.url`, never from cwd); * - the upstream version is recorded in `/.sync-meta.json` together * with the pinned ref, instead of being compared to package.json; * - the destination is replaced by a staging tree only after every * assertion passed, so a broken upstream never leaves a half-synced * `--out` behind. * * The upstream skills are the source of truth for what a skill says. This * port ships a small, explicit, asserted set of deltas: * * 1. `dsh/bundle/dsh-tools.md` replaces the per-harness reference files * upstream ships for Claude Code, Codex, Pi, Antigravity, Hermes and * Muse. * 2. The transforms below rewrite upstream text that names another harness * and drop the Claude-Code-only subagent stop gate. * 3. Every remaining `superpowers:` namespace prefix is stripped: the DSH * skill registry addresses skills by bare name. * * A missing anchor fails the run loudly: upstream rewrote text this port * depends on, so a human must decide what the DSH wording becomes. The one * exception is `optional: true` — a transform whose upstream text v7.0.1 has * already dropped: it is reported as a warning, not a failure, so the port * keeps the transform ready for the release that brings the text back. * * Usage: * node dsh/sync/sync-from-upstream.mjs [--out ] [--ref ] * * Defaults: `--out /.sync-out`, `--ref unknown`. * Staging tree: `.tmp-`; it is removed on any failure and only * renamed onto `` after all assertions pass. * * @module dsh/sync/sync-from-upstream */ import { rmSync } from 'node:fs' import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises' import { dirname, join, relative, resolve, sep } from 'node:path' import { fileURLToPath } from 'node:url' const scriptDir = dirname(fileURLToPath(import.meta.url)) /** The single source of the DSH tool mapping: `dsh/sync/..` = `dsh/`. */ const dshToolsOverride = resolve(scriptDir, '..', 'bundle', 'dsh-tools.md') /** Path of the override inside the synced skill tree. */ const OVERRIDE_TARGET = 'using-superpowers/references/dsh-tools.md' const UPSTREAM_SKILLS = [ 'brainstorming', 'diagnosing-superpowers', 'dispatching-parallel-agents', 'executing-plans', 'finishing-a-development-branch', 'receiving-code-review', 'requesting-code-review', 'subagent-driven-development', 'systematic-debugging', 'test-driven-development', 'using-git-worktrees', 'using-superpowers', 'verification-before-completion', 'writing-plans', 'writing-skills' ] /** Per-harness reference files upstream v7.0.1 ships that dsh-tools.md replaces. */ const SKIP = [ 'using-superpowers/references/antigravity-tools.md', 'using-superpowers/references/claude-code-tools.md', 'using-superpowers/references/codex-tools.md', 'using-superpowers/references/gemini-tools.md', 'using-superpowers/references/hermes-tools.md', 'using-superpowers/references/muse-tools.md', 'using-superpowers/references/pi-tools.md' ] const DSH_HOME = '$DSH_HOME' /** * Explicit rewrites applied before the namespace strip. Each entry must match * exactly `expect` times, or the sync aborts — unless it is `optional: true`, * in which case an absent anchor is reported as a warning. Optional entries * exist for upstream deletions: v7.0.1 dropped the subagent hook example, but * an upstream release may bring it back, and the adaptation must then return * automatically instead of being silently absent from the transform list. */ const TRANSFORMS = [ { name: 'using-superpowers: drop the upstream subagent stop gate', file: 'using-superpowers/SKILL.md', find: '\nIf you were dispatched as a subagent to execute a specific task, ignore this skill.\n\n\n', replace: '' }, { name: 'using-superpowers: DSH platform reference replaces the harness list', file: 'using-superpowers/SKILL.md', find: [ 'If your harness appears here, read its reference file for special instructions:', '', '- Claude Code: `references/claude-code-tools.md`', '- Codex: `references/codex-tools.md`', '- Pi: `references/pi-tools.md`', '- Antigravity: `references/antigravity-tools.md`', '- Hermes Agent: `references/hermes-tools.md`', '- Muse: `references/muse-tools.md`' ].join('\n'), replace: [ 'This package is the DeepSeek Harness port. Its platform reference is', '`references/dsh-tools.md`: the Claude-Code → DSH tool mapping plus the', 'DSH-specific mechanisms these skills rely on (`skill`, `subagent` /', '`subagent_fork`, `workflow`, `goal`, and the Windows `pwsh` notes).', '', '## Invoking skills on DSH', '', 'Call the `skill` tool with the exact skill name from the catalog before', 'acting on a task that names or clearly matches that skill:', '', '- Load applicable skills FIRST, then follow their full instructions.', '- Skills are addressable by bare name: `brainstorming`, `writing-plans`,', ' `systematic-debugging`, `test-driven-development`, `using-git-worktrees`,', ' `subagent-driven-development`, `dispatching-parallel-agents`, and the rest.', '- `using-superpowers` itself is discoverable the same way.' ].join('\n') }, { name: 'brainstorming: DSH platform note for the visual companion', file: 'brainstorming/visual-companion.md', find: 'Browser-based visual brainstorming companion for showing mockups, diagrams, and options.\n', replace: [ 'Browser-based visual brainstorming companion for showing mockups, diagrams, and options.', '', '## Platform notes (DeepSeek Harness port)', '', 'The companion server is `scripts/server.cjs` (plain Node.js, runs anywhere', 'Node runs, including Windows). The `start-server.sh` / `stop-server.sh`', 'helpers are bash-only: on Windows, start the server directly with', '`node scripts/server.cjs --project-dir --open` and stop it by killing', 'the process. All other guidance below is platform-neutral.', '' ].join('\n') }, { name: 'executing-plans: DSH availability of subagents', file: 'executing-plans/SKILL.md', find: [ '- You have a plan from superpowers:writing-plans and your human partner', ' chose inline execution at the handoff.', '- Your harness has no subagent tool (see the per-platform references in', ' `../using-superpowers/references/`). Never fabricate a dispatch; run', ' the plan here.' ].join('\n'), replace: [ '- You have a plan from writing-plans and your human partner chose inline', ' (Native) execution at the handoff.', '- No subagent tool is available in this session, or your human partner', ' chose Native. On the DeepSeek Harness `subagent` / `subagent_fork`', ' normally ARE available (see `../using-superpowers/references/dsh-tools.md`),', ' so prefer subagent-driven-development; run the plan here when the work', ' must stay in one context. Never fabricate a dispatch.' ].join('\n') }, { name: 'executing-plans: invoke sdd-workspace through bash', file: 'executing-plans/SKILL.md', find: '`../subagent-driven-development/scripts/sdd-workspace PLAN_FILE`', replace: '`bash ../subagent-driven-development/scripts/sdd-workspace PLAN_FILE`' }, { name: 'executing-plans: invoke task-start through bash', file: 'executing-plans/SKILL.md', find: "Run this skill's `scripts/task-start PLAN_FILE N`.", replace: "Run this skill's `bash scripts/task-start PLAN_FILE N`." }, { name: 'executing-plans: invoke task-done through bash', file: 'executing-plans/SKILL.md', find: "Run this skill's `scripts/task-done PLAN_FILE N BASE -- `", replace: "Run this skill's `bash scripts/task-done PLAN_FILE N BASE -- `" }, { name: 'executing-plans: invoke review-package through bash', file: 'executing-plans/SKILL.md', find: 'Run `../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`', replace: 'Run `bash ../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`' }, { name: 'executing-plans: task-start calls task-brief through bash', file: 'executing-plans/scripts/task-start', find: 'out=$("$sdd/task-brief" "$plan" "$n")', replace: 'out=$("${BASH:-bash}" "$sdd/task-brief" "$plan" "$n")' }, { name: 'executing-plans: task-done calls sdd-workspace through bash', file: 'executing-plans/scripts/task-done', find: 'dir=$("$sdd/sdd-workspace" "$plan")', replace: 'dir=$("${BASH:-bash}" "$sdd/sdd-workspace" "$plan")' }, { name: 'writing-skills: DSH skills directory replaces the cross-harness paths', file: 'writing-skills/SKILL.md', find: "**Personal skills live in your runtime's skills directory** (`~/.claude/skills/` on Claude Code) — see [codex-tools.md](../using-superpowers/references/codex-tools.md) or [gemini-tools.md](../using-superpowers/references/gemini-tools.md) for the path on those runtimes. Codex, Copilot CLI, and Gemini CLI all also recognize `~/.agents/skills/` as a cross-runtime alias.", replace: "**Personal skills live in your runtime's skills directory**: on the DeepSeek Harness, that is `" + DSH_HOME + "/skills/` (user level, discovered automatically by `skill-filesystem`), the project `.dsh/skills/` / " + '`.agents/skills/` roots, or — for packaged skills — the `skills/` directory inside a plugin bundle like this ' + 'one. See [dsh-tools.md](../using-superpowers/references/dsh-tools.md) for the full mechanism.' }, { // optional: upstream v7.0.1 dropped the hook example this transform rewrites. name: 'subagent-driven-development: hook example uses the DSH home', file: 'subagent-driven-development/SKILL.md', find: 'You: "User level (~/.config/superpowers/hooks/)"', replace: 'You: "Install the hook at the user level (' + DSH_HOME + ')"', optional: true }, { name: 'diagnosing-superpowers: case workspace lives under the DSH home', file: 'diagnosing-superpowers/SKILL.md', find: '`~/.superpowers/diagnosing-superpowers//`', replace: '`' + DSH_HOME + '/diagnosing-superpowers//`' }, { name: 'diagnosing-superpowers: case template uses the DSH home', file: 'diagnosing-superpowers/templates/case.md', find: 'Workspace: ~/.superpowers/diagnosing-superpowers//', replace: 'Workspace: ' + DSH_HOME + '/diagnosing-superpowers//' }, { name: 'diagnosing-superpowers: report template uses the DSH home', file: 'diagnosing-superpowers/templates/report.md', find: 'Report path: ~/.superpowers/diagnosing-superpowers//report.md', replace: 'Report path: ' + DSH_HOME + '/diagnosing-superpowers//report.md' }, { name: 'diagnosing-superpowers: DSH session discovery facts', file: 'diagnosing-superpowers/references/session-discovery.md', find: 'limitation and ask for the missing path, export, or identifying detail.', replace: [ 'limitation and ask for the missing path, export, or identifying detail.', '', '## DeepSeek Harness', '', 'Verified in this environment; confirm it against the records you actually read.', '', '- Transcripts: `' + DSH_HOME + '/sessions//session-/session*.jsonl.zstd`', ' (`' + DSH_HOME + '` defaults to `~/.dsh`). The slug is the working directory with', ' separators and non-alphanumeric characters folded to `-`, wrapped in `--`.', '- Child sessions — work dispatched through `subagent` / `subagent_fork` — are', ' peer directories under the same slug whose header record carries', ' `delegationDepth: 1`.', '- The schema version is in the filename (`session.v4.jsonl.zstd`; older files', ' are `session.v3.jsonl.zstd` or `session.jsonl.zstd`). Read the header', " record's `version` instead of guessing from the name.", '- Each file is a concatenation of independent zstd frames, and the `zstd` CLI', ' is usually absent. See `../using-superpowers/references/dsh-tools.md` for the', ' frame-by-frame Node decompression command.', '- Records are JSON lines carrying `type`, `seq`, `time`, and `data`. Establish', ' what each type means from the records themselves — never import a meaning', ' from another harness.' ].join('\n') } ] const TEXT_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.js', '.cjs', '.mjs', '.ts', '.sh', '.dot', '.html']) const USAGE = 'usage: node dsh/sync/sync-from-upstream.mjs [--out ] [--ref ]' /** Staging tree, set once the destination is known. */ let staging = null function fail(message) { if (staging) rmSync(staging, { recursive: true, force: true }) console.error(`\n[FAIL] ${message}\n`) process.exit(1) } function usage(message) { if (message) console.error(`[FAIL] ${message}\n`) console.error(USAGE) process.exit(message ? 2 : 0) } /** @returns {{upstream: string, out: string, ref: string}} */ function parseArgs(argv) { let upstream = null let out = null let ref = null for (let i = 0; i < argv.length; i++) { const arg = argv[i] if (arg === '--out' || arg === '--ref') { const value = argv[++i] if (!value) usage(`missing value for ${arg}`) if (arg === '--out') out = value else ref = value } else if (arg === '--help' || arg === '-h') { usage() } else if (arg.startsWith('--')) { usage(`unknown option ${arg}`) } else if (upstream === null) { upstream = arg } else { usage(`unexpected argument ${arg}`) } } if (!upstream) usage('missing ') const upstreamRoot = resolve(upstream) return { upstream: upstreamRoot, out: out === null ? join(upstreamRoot, '.sync-out') : resolve(out), ref: ref === null ? 'unknown' : ref } } /** @returns {Promise} repo-relative file paths under `root`. */ async function walk(root, current = root) { const out = [] for (const entry of await readdir(current, { withFileTypes: true })) { const full = join(current, entry.name) if (entry.isDirectory()) out.push(...(await walk(root, full))) else out.push(relative(root, full).split(sep).join('/')) } return out } function isTextFile(rel) { const dot = rel.lastIndexOf('.') const slash = rel.lastIndexOf('/') if (dot <= slash) return true return TEXT_EXTENSIONS.has(rel.slice(dot)) } /** * Apply every transform whose `file` matches, asserting the anchor exists. * Optional transforms whose anchor is gone are collected for a warning. */ function applyTransforms(rel, text, applied, optionalMisses) { for (const t of TRANSFORMS) { if (t.file !== rel) continue if (t.find === t.replace) continue const count = text.split(t.find).length - 1 if (count === 0) { if (t.optional) { optionalMisses.push(t) continue } fail(`anchor missing in ${rel} for "${t.name}"\n find: ${JSON.stringify(t.find.slice(0, 120))}`) } if (count > 1) fail(`anchor matched ${count} times in ${rel} for "${t.name}"`) text = text.replace(t.find, t.replace) applied.add(t.name) } return text } async function main() { const args = parseArgs(process.argv.slice(2)) const upstreamRoot = args.upstream const upstreamSkills = join(upstreamRoot, 'skills') const upstreamStats = await stat(upstreamSkills).catch(() => null) if (!upstreamStats?.isDirectory()) { fail(`no skills/ directory in ${upstreamRoot}`) } // A new (or removed) upstream skill must be a deliberate decision: it // changes the skill catalog, the README tables, and possibly the DSH // adaptations. const upstreamSkillDirs = (await readdir(upstreamSkills, { withFileTypes: true })) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .sort() const expectedSorted = [...UPSTREAM_SKILLS].sort() if (upstreamSkillDirs.join('\n') !== expectedSorted.join('\n')) { const added = upstreamSkillDirs.filter((name) => !expectedSorted.includes(name)) const removed = expectedSorted.filter((name) => !upstreamSkillDirs.includes(name)) fail( `the upstream skill set changed (expected ${expectedSorted.length} skills from UPSTREAM_SKILLS) — decide before syncing:\n` + (added.length > 0 ? ` added upstream: ${added.join(', ')}\n` : '') + (removed.length > 0 ? ` removed upstream: ${removed.join(', ')}\n` : '') + ' Update UPSTREAM_SKILLS in this script and the README skill tables.' ) } // Report the upstream version so the destination can record it. let upstreamVersion = 'unknown' try { const manifest = JSON.parse(await readFile(join(upstreamRoot, '.claude-plugin', 'plugin.json'), 'utf8')) upstreamVersion = manifest.version ?? 'unknown' } catch { /* version is informational only */ } const overrideStats = await stat(dshToolsOverride).catch(() => null) if (!overrideStats?.isFile()) { fail(`missing DSH override ${dshToolsOverride} (dsh/bundle/dsh-tools.md)`) } staging = `${args.out}.tmp-${process.pid}` await rm(staging, { recursive: true, force: true }) await mkdir(staging, { recursive: true }) const applied = new Set() const optionalMisses = [] let copied = 0 let stripped = 0 for (const rel of await walk(upstreamSkills)) { if (SKIP.includes(rel)) continue const sourceFile = join(upstreamSkills, rel) const target = join(staging, rel) await mkdir(dirname(target), { recursive: true }) if (!isTextFile(rel)) { await cp(sourceFile, target) copied++ continue } let text = await readFile(sourceFile, 'utf8') text = applyTransforms(rel, text, applied, optionalMisses) const namespaced = text.split('superpowers:').length - 1 if (namespaced > 0) { text = text.replaceAll('superpowers:', '') stripped += namespaced } await writeFile(target, text, 'utf8') copied++ } // The DSH-only file replaces upstream text wholesale. const overrideTarget = join(staging, OVERRIDE_TARGET) await mkdir(dirname(overrideTarget), { recursive: true }) await cp(dshToolsOverride, overrideTarget) copied++ // Every required transform must have fired; one that silently stopped // matching would drop a DSH adaptation from the next release's sync. const unfired = TRANSFORMS.filter((t) => t.find !== t.replace && !t.optional && !applied.has(t.name)) if (unfired.length > 0) { fail(`transform(s) never matched a copied file: ${unfired.map((t) => `${t.name} (${t.file})`).join('; ')}`) } for (const t of optionalMisses) { console.warn(`[warn] optional transform did not match, anchor gone upstream: "${t.name}" (${t.file})`) } await writeFile( join(staging, '.sync-meta.json'), JSON.stringify({ ref: args.ref, version: upstreamVersion }, null, 2) + '\n', 'utf8' ) // Everything applied: swap the staging tree in. await mkdir(dirname(args.out), { recursive: true }) await rm(args.out, { recursive: true, force: true }) await rename(staging, args.out) staging = null console.log(`synced ${copied} files from obra/superpowers ${upstreamVersion} (ref ${args.ref}) into ${args.out}`) console.log(` namespace prefixes stripped: ${stripped}`) console.log(` transforms applied: ${applied.size}`) console.log(` dsh override: ${relative(process.cwd(), dshToolsOverride) || dshToolsOverride} -> ${OVERRIDE_TARGET}`) } main().catch((error) => { if (staging) rmSync(staging, { recursive: true, force: true }) console.error(`\n[FAIL] unexpected error: ${error?.stack ?? error}\n`) process.exit(1) })