fix(dsh): sync — полный набор transform-ов (16), фикстура 15 скиллов (T1.1-T1.2, #69)

This commit is contained in:
Andrey Epifancev 2026-10-24 23:25:14 +04:00
parent fce88f04d9
commit 6c003d1d86
22 changed files with 345 additions and 37 deletions

View File

@ -29,7 +29,10 @@
* 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.
* 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 <upstream-checkout> [--out <dir>] [--ref <tag>]
@ -80,12 +83,15 @@ const SKIP = [
'using-superpowers/references/pi-tools.md'
]
/** Optional manifest of a reduced checkout (test fixtures); absent upstream. */
const FIXTURE_MANIFEST = '.sync-fixture.json'
const DSH_HOME = '$DSH_HOME'
/**
* Explicit rewrites applied before the namespace strip. Each entry must match
* exactly `expect` times, or the sync aborts.
* 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 = [
{
@ -124,6 +130,142 @@ const TRANSFORMS = [
' `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 <path> --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 -- <test command>`",
replace: "Run this skill's `bash scripts/task-done PLAN_FILE N BASE -- <test command>`"
},
{
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/<session-id>/`',
replace: '`' + DSH_HOME + '/diagnosing-superpowers/<session-id>/`'
},
{
name: 'diagnosing-superpowers: case template uses the DSH home',
file: 'diagnosing-superpowers/templates/case.md',
find: 'Workspace: ~/.superpowers/diagnosing-superpowers/<session-id>/',
replace: 'Workspace: ' + DSH_HOME + '/diagnosing-superpowers/<session-id>/'
},
{
name: 'diagnosing-superpowers: report template uses the DSH home',
file: 'diagnosing-superpowers/templates/report.md',
find: 'Report path: ~/.superpowers/diagnosing-superpowers/<session-id>/report.md',
replace: 'Report path: ' + DSH_HOME + '/diagnosing-superpowers/<session-id>/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/<slugified-cwd>/session-<uuid>/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')
}
]
@ -198,13 +340,22 @@ function isTextFile(rel) {
return TEXT_EXTENSIONS.has(rel.slice(dot))
}
/** Apply every transform whose `file` matches, asserting the anchor exists. */
function applyTransforms(rel, text, applied) {
/**
* 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) fail(`anchor missing in ${rel} for "${t.name}"\n find: ${JSON.stringify(t.find.slice(0, 120))}`)
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)
@ -212,25 +363,6 @@ function applyTransforms(rel, text, applied) {
return text
}
/**
* A real obra/superpowers checkout ships the 15 skills above; the minimal test
* fixture ships two and declares them in `.sync-fixture.json`. The skill-set
* assertion runs in both cases, against the declared set when one exists.
*
* @returns {Promise<{expected: string[], source: string}>}
*/
async function expectedSkillNames(upstreamRoot) {
try {
const manifest = JSON.parse(await readFile(join(upstreamRoot, FIXTURE_MANIFEST), 'utf8'))
if (Array.isArray(manifest.expectedSkills) && manifest.expectedSkills.length > 0) {
return { expected: manifest.expectedSkills, source: FIXTURE_MANIFEST }
}
} catch {
/* real checkouts have no fixture manifest */
}
return { expected: UPSTREAM_SKILLS, source: 'UPSTREAM_SKILLS' }
}
async function main() {
const args = parseArgs(process.argv.slice(2))
const upstreamRoot = args.upstream
@ -244,17 +376,16 @@ async function main() {
// 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 { expected, source } = await expectedSkillNames(upstreamRoot)
const upstreamSkillDirs = (await readdir(upstreamSkills, { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort()
const expectedSorted = [...expected].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 ${source}) — decide before syncing:\n` +
`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.'
@ -280,6 +411,7 @@ async function main() {
await mkdir(staging, { recursive: true })
const applied = new Set()
const optionalMisses = []
let copied = 0
let stripped = 0
@ -296,7 +428,7 @@ async function main() {
}
let text = await readFile(sourceFile, 'utf8')
text = applyTransforms(rel, text, applied)
text = applyTransforms(rel, text, applied, optionalMisses)
const namespaced = text.split('superpowers:').length - 1
if (namespaced > 0) {
@ -314,12 +446,15 @@ async function main() {
await cp(dshToolsOverride, overrideTarget)
copied++
// Every transform that names a file 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 && !applied.has(t.name))
// 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'),

View File

@ -1,4 +0,0 @@
{
"comment": "Reduced upstream checkout for tests. The skill-set assertion still runs, against this declared set instead of the 15 skills of a real obra/superpowers tree.",
"expectedSkills": ["brainstorming", "using-superpowers"]
}

View File

@ -0,0 +1,5 @@
# Visual companion
Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
Minimal upstream stub for sync tests.

View File

@ -0,0 +1,10 @@
---
name: diagnosing-superpowers
description: Upstream stub for diagnosing-superpowers used by sync tests.
---
# Diagnosing superpowers
Case workspace: `~/.superpowers/diagnosing-superpowers/<session-id>/`
Minimal upstream stub for sync tests.

View File

@ -0,0 +1,3 @@
# Session discovery
When a record is missing, state the limitation and ask for the missing path, export, or identifying detail.

View File

@ -0,0 +1,3 @@
# Case
Workspace: ~/.superpowers/diagnosing-superpowers/<session-id>/

View File

@ -0,0 +1,3 @@
# Report
Report path: ~/.superpowers/diagnosing-superpowers/<session-id>/report.md

View File

@ -0,0 +1,8 @@
---
name: dispatching-parallel-agents
description: Upstream stub for dispatching-parallel-agents used by sync tests.
---
# Dispatching parallel agents
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,20 @@
---
name: executing-plans
description: Upstream stub for executing-plans used by sync tests.
---
# Executing plans
- 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.
Run this skill's `scripts/task-start PLAN_FILE N`.
Run this skill's `scripts/task-done PLAN_FILE N BASE -- <test command>`.
Use `../subagent-driven-development/scripts/sdd-workspace PLAN_FILE`.
Run `../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`.

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
sdd=$(dirname "$0")
plan=$1
dir=$("$sdd/sdd-workspace" "$plan")
printf '%s\n' "$dir"

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
sdd=$(dirname "$0")
plan=$1
n=$2
out=$("$sdd/task-brief" "$plan" "$n")
printf '%s\n' "$out"

View File

@ -0,0 +1,8 @@
---
name: finishing-a-development-branch
description: Upstream stub for finishing-a-development-branch used by sync tests.
---
# Finishing a development branch
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: receiving-code-review
description: Upstream stub for receiving-code-review used by sync tests.
---
# Receiving code review
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: requesting-code-review
description: Upstream stub for requesting-code-review used by sync tests.
---
# Requesting code review
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,14 @@
---
name: subagent-driven-development
description: Upstream stub for subagent-driven-development used by sync tests.
---
# Subagent-driven development
Setup example (upstream v7.0.0 wording, kept so the port exercises its
home-dir transform; v7.0.1 dropped the example):
You: "User level (~/.config/superpowers/hooks/)"
Human: "Project level"
Minimal upstream stub for sync tests.

View File

@ -0,0 +1,8 @@
---
name: systematic-debugging
description: Upstream stub for systematic-debugging used by sync tests.
---
# Systematic debugging
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: test-driven-development
description: Upstream stub for test-driven-development used by sync tests.
---
# Test-driven development
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: using-git-worktrees
description: Upstream stub for using-git-worktrees used by sync tests.
---
# Using git worktrees
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: verification-before-completion
description: Upstream stub for verification-before-completion used by sync tests.
---
# Verification before completion
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,8 @@
---
name: writing-plans
description: Upstream stub for writing-plans used by sync tests.
---
# Writing plans
Minimal upstream stub for sync tests. No DSH-specific text is rewritten here.

View File

@ -0,0 +1,10 @@
---
name: writing-skills
description: Upstream stub for writing-skills used by sync tests.
---
# Writing skills
**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.
Minimal upstream stub for sync tests.

View File

@ -32,6 +32,10 @@ test('sync happy path: transforms, namespace strip, override, meta', () => {
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
assert.match(result.stdout, /synced \d+ files/)
assert.match(result.stdout, /transforms applied: 16/)
const skillDirs = readdirSync(out).filter((name) => name !== '.sync-meta.json')
assert.equal(skillDirs.length, 15, `expected all 15 upstream skills, got ${skillDirs.join(', ')}`)
const using = readFileSync(join(out, 'using-superpowers', 'SKILL.md'), 'utf8')
assert.match(using, /references\/dsh-tools\.md/)
@ -83,6 +87,26 @@ test('sync fails loud on a broken transform anchor and leaves --out untouched',
}
})
test('sync fails loud on a broken anchor of another transform', () => {
const work = tempDir('dsh-sync-anchor-executing-plans-')
try {
const upstream = fixtureCopy(work)
const skillPath = join(upstream, 'skills', 'executing-plans', 'SKILL.md')
const anchor = 'Run `../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`'
const original = readFileSync(skillPath, 'utf8')
assert.ok(original.includes(anchor), 'fixture must carry the review-package anchor verbatim')
write(skillPath, original.replace(anchor, 'Run `../subagent-driven-development/scripts/review-package PLAN_FILE HEAD`'))
const result = runSync([upstream, '--out', join(work, 'out'), '--ref', 'v7.0.1'])
assert.equal(result.status, 1)
assert.match(result.stderr, /invoke review-package through bash/)
assert.equal(existsSync(join(work, 'out')), false)
} finally {
cleanup(work)
}
})
test('sync fails on a changed skill set', () => {
const work = tempDir('dsh-sync-skill-set-')
try {