@zfdx123/dsh-superpowers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/LICENSE.superpowers +21 -0
- package/README.md +109 -0
- package/README.zh.md +109 -0
- package/cordis.patch.yml +16 -0
- package/index.js +395 -0
- package/package.json +68 -0
- package/skills/brainstorming/SKILL.md +250 -0
- package/skills/brainstorming/scripts/frame-template.html +213 -0
- package/skills/brainstorming/scripts/helper.js +179 -0
- package/skills/brainstorming/scripts/server.cjs +781 -0
- package/skills/brainstorming/scripts/start-server.sh +209 -0
- package/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/skills/brainstorming/visual-companion.md +299 -0
- package/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/skills/executing-plans/SKILL.md +64 -0
- package/skills/finishing-a-development-branch/SKILL.md +225 -0
- package/skills/receiving-code-review/SKILL.md +205 -0
- package/skills/requesting-code-review/SKILL.md +95 -0
- package/skills/requesting-code-review/code-reviewer.md +181 -0
- package/skills/subagent-driven-development/SKILL.md +568 -0
- package/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/skills/subagent-driven-development/scripts/review-package +46 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
- package/skills/subagent-driven-development/scripts/task-brief +41 -0
- package/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +283 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +72 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/test-driven-development/SKILL.md +320 -0
- package/skills/test-driven-development/writing-good-tests.md +198 -0
- package/skills/using-git-worktrees/SKILL.md +167 -0
- package/skills/using-superpowers/SKILL.md +63 -0
- package/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/skills/using-superpowers/references/codex-tools.md +108 -0
- package/skills/using-superpowers/references/gemini-tools.md +63 -0
- package/skills/using-superpowers/references/hermes-tools.md +56 -0
- package/skills/using-superpowers/references/pi-tools.md +16 -0
- package/skills/verification-before-completion/SKILL.md +120 -0
- package/skills/writing-plans/SKILL.md +171 -0
- package/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/skills/writing-skills/SKILL.md +679 -0
- package/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/writing-skills/persuasion-principles.md +187 -0
- package/skills/writing-skills/render-graphs.js +172 -0
- package/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/test/index.test.js +332 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import test from 'node:test'
|
|
8
|
+
|
|
9
|
+
import { Config, apply } from '../index.js'
|
|
10
|
+
|
|
11
|
+
const packageRoot = dirname(fileURLToPath(new URL('../index.js', import.meta.url)))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the smallest context the plugin consumes.
|
|
15
|
+
* @param options - `catalog` replaces the registry's effective view; by default
|
|
16
|
+
* every registered skill wins, which is the unshadowed deployment.
|
|
17
|
+
* @param options.catalog - the effective skills the registry would report.
|
|
18
|
+
* @returns the context plus the registrations, sections, warnings, and events it observed.
|
|
19
|
+
*/
|
|
20
|
+
function makeContext(options = {}) {
|
|
21
|
+
const registered = []
|
|
22
|
+
const sections = []
|
|
23
|
+
const warnings = []
|
|
24
|
+
const listeners = new Map()
|
|
25
|
+
return {
|
|
26
|
+
ctx: {
|
|
27
|
+
skills: {
|
|
28
|
+
register(skill) {
|
|
29
|
+
registered.push(skill)
|
|
30
|
+
return () => {}
|
|
31
|
+
},
|
|
32
|
+
async list() {
|
|
33
|
+
if (typeof options.catalog === 'function') return options.catalog()
|
|
34
|
+
if (options.catalog !== undefined) return options.catalog
|
|
35
|
+
return registered.map((skill) => ({
|
|
36
|
+
name: skill.name,
|
|
37
|
+
description: skill.description,
|
|
38
|
+
provider: skill.provider ?? 'runtime',
|
|
39
|
+
source: skill.source,
|
|
40
|
+
path: skill.path,
|
|
41
|
+
}))
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
systemPrompt: {
|
|
45
|
+
section(section) {
|
|
46
|
+
sections.push(section)
|
|
47
|
+
return () => {}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
logger: {
|
|
51
|
+
warn(message) {
|
|
52
|
+
warnings.push(message)
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
on(event, listener) {
|
|
56
|
+
listeners.set(event, listener)
|
|
57
|
+
return () => {}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
registered,
|
|
61
|
+
sections,
|
|
62
|
+
warnings,
|
|
63
|
+
listeners,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One agent shape the skill lookup reads: a scope key plus its session cwd. */
|
|
68
|
+
function makeAgent(cwd = 'E:/work/ai') {
|
|
69
|
+
return { id: 'agent-1', session: { header: { cwd } } }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readCurrentScalar(frontmatter, key) {
|
|
73
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))
|
|
74
|
+
assert.notEqual(match, null, `${key} should exist in source frontmatter`)
|
|
75
|
+
const value = match[1].trim()
|
|
76
|
+
if (value.startsWith('"')) return JSON.parse(value)
|
|
77
|
+
if (value.startsWith("'")) return value.slice(1, -1).replace(/''/g, "'")
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function writeSkill(root, directory, frontmatter, body = '# Fixture\n\nInstructions.') {
|
|
82
|
+
const skillDir = join(root, 'skills', directory)
|
|
83
|
+
await mkdir(skillDir, { recursive: true })
|
|
84
|
+
await writeFile(join(skillDir, 'SKILL.md'), `---\n${frontmatter}\n---\n${body}\n`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Place a copy of the plugin in a fixture root so `skills/` resolves there.
|
|
89
|
+
* The copy still imports the declared schema dependency, so the fixture needs
|
|
90
|
+
* the same `node_modules` the real module resolves from.
|
|
91
|
+
* @param root - the fixture root that receives `index.mjs` and `node_modules`.
|
|
92
|
+
*/
|
|
93
|
+
async function copyPluginInto(root) {
|
|
94
|
+
await copyFile(join(packageRoot, 'index.js'), join(root, 'index.mjs'))
|
|
95
|
+
const modules = join(packageRoot, 'node_modules')
|
|
96
|
+
if (existsSync(modules)) await symlink(modules, join(root, 'node_modules'), 'junction')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test('registers all vendored skills with source-accurate metadata', async () => {
|
|
100
|
+
const state = makeContext()
|
|
101
|
+
apply(state.ctx)
|
|
102
|
+
|
|
103
|
+
const entries = (await readdir(join(packageRoot, 'skills'), { withFileTypes: true }))
|
|
104
|
+
.filter((entry) => entry.isDirectory())
|
|
105
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
106
|
+
|
|
107
|
+
assert.equal(entries.length, 14)
|
|
108
|
+
assert.equal(state.registered.length, 14)
|
|
109
|
+
assert.deepEqual(state.warnings, [])
|
|
110
|
+
assert.equal(state.sections.length, 1)
|
|
111
|
+
assert.match(state.sections[0].text, /`interrupt_agent`/)
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const raw = await readFile(join(packageRoot, 'skills', entry.name, 'SKILL.md'), 'utf8')
|
|
115
|
+
const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
116
|
+
assert.notEqual(frontmatter, null)
|
|
117
|
+
|
|
118
|
+
const expectedName = readCurrentScalar(frontmatter[1], 'name')
|
|
119
|
+
const expectedDescription = readCurrentScalar(frontmatter[1], 'description')
|
|
120
|
+
const actual = state.registered.find((skill) => skill.name === expectedName)
|
|
121
|
+
assert.notEqual(actual, undefined, `${expectedName} should be registered`)
|
|
122
|
+
assert.equal(actual.description, expectedDescription)
|
|
123
|
+
assert.equal(actual.path, join(packageRoot, 'skills', entry.name, 'SKILL.md'))
|
|
124
|
+
assert.equal(actual.resourceBase.path, join(packageRoot, 'skills', entry.name))
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('parses folded and literal YAML block scalars', async () => {
|
|
129
|
+
const fixtureRoot = await mkdtemp(join(tmpdir(), 'dsh-superpowers-parser-'))
|
|
130
|
+
try {
|
|
131
|
+
await copyPluginInto(fixtureRoot)
|
|
132
|
+
await writeSkill(
|
|
133
|
+
fixtureRoot,
|
|
134
|
+
'folded',
|
|
135
|
+
[
|
|
136
|
+
'name: folded',
|
|
137
|
+
'description: >-',
|
|
138
|
+
' Use when a future release',
|
|
139
|
+
' wraps its description.',
|
|
140
|
+
'',
|
|
141
|
+
' Preserve paragraphs.',
|
|
142
|
+
'whenToUse: >-',
|
|
143
|
+
' During metadata',
|
|
144
|
+
' compatibility tests.',
|
|
145
|
+
].join('\n'),
|
|
146
|
+
)
|
|
147
|
+
await writeSkill(
|
|
148
|
+
fixtureRoot,
|
|
149
|
+
'literal',
|
|
150
|
+
['name: literal', 'description: |-', ' first line', ' second line'].join('\n'),
|
|
151
|
+
)
|
|
152
|
+
await writeSkill(
|
|
153
|
+
fixtureRoot,
|
|
154
|
+
'indented',
|
|
155
|
+
['name: indented', 'description: >-', ' first', ' indented', ' last'].join('\n'),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const fixture = await import(`${pathToFileURL(join(fixtureRoot, 'index.mjs')).href}?fixture=${Date.now()}`)
|
|
159
|
+
const state = makeContext()
|
|
160
|
+
fixture.apply(state.ctx, { bootstrap: false })
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(state.warnings, [])
|
|
163
|
+
assert.equal(
|
|
164
|
+
state.registered.find((skill) => skill.name === 'folded').description,
|
|
165
|
+
'Use when a future release wraps its description.\nPreserve paragraphs.',
|
|
166
|
+
)
|
|
167
|
+
assert.equal(
|
|
168
|
+
state.registered.find((skill) => skill.name === 'folded').whenToUse,
|
|
169
|
+
'During metadata compatibility tests.',
|
|
170
|
+
)
|
|
171
|
+
assert.equal(state.registered.find((skill) => skill.name === 'literal').description, 'first line\nsecond line')
|
|
172
|
+
assert.equal(state.registered.find((skill) => skill.name === 'indented').description, 'first\n indented\nlast')
|
|
173
|
+
} finally {
|
|
174
|
+
await rm(fixtureRoot, { recursive: true, force: true })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('warns and skips damaged or duplicate skill metadata', async () => {
|
|
179
|
+
const fixtureRoot = await mkdtemp(join(tmpdir(), 'dsh-superpowers-errors-'))
|
|
180
|
+
try {
|
|
181
|
+
await copyPluginInto(fixtureRoot)
|
|
182
|
+
await writeSkill(fixtureRoot, 'a-valid', 'name: duplicate\ndescription: first')
|
|
183
|
+
await writeSkill(fixtureRoot, 'b-duplicate', 'name: duplicate\ndescription: second')
|
|
184
|
+
await writeSkill(fixtureRoot, 'missing-description', 'name: missing-description')
|
|
185
|
+
await writeSkill(fixtureRoot, 'empty-body', 'name: empty-body\ndescription: empty', '')
|
|
186
|
+
|
|
187
|
+
const fixture = await import(`${pathToFileURL(join(fixtureRoot, 'index.mjs')).href}?fixture=${Date.now()}`)
|
|
188
|
+
const state = makeContext()
|
|
189
|
+
fixture.apply(state.ctx, { bootstrap: false })
|
|
190
|
+
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
state.registered.map((skill) => skill.name),
|
|
193
|
+
['duplicate'],
|
|
194
|
+
)
|
|
195
|
+
assert.equal(state.warnings.length, 3)
|
|
196
|
+
assert.ok(state.warnings.some((message) => message.includes('duplicates skill name "duplicate"')))
|
|
197
|
+
assert.ok(state.warnings.some((message) => message.includes('requires non-empty name and description')))
|
|
198
|
+
assert.ok(state.warnings.some((message) => message.includes('empty instruction body')))
|
|
199
|
+
} finally {
|
|
200
|
+
await rm(fixtureRoot, { recursive: true, force: true })
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('does no filesystem or registry work when both features are disabled', () => {
|
|
205
|
+
assert.doesNotThrow(() => apply({}, { skills: false, bootstrap: false }))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('exports a config schema that defaults every documented option', () => {
|
|
209
|
+
assert.deepEqual(Config({}), { skills: true, bootstrap: true, toolMapping: true, order: 50 })
|
|
210
|
+
assert.deepEqual(Config({ bootstrap: false, order: 10 }), {
|
|
211
|
+
skills: true,
|
|
212
|
+
bootstrap: false,
|
|
213
|
+
toolMapping: true,
|
|
214
|
+
order: 10,
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('rejects a malformed configuration instead of applying a guess', () => {
|
|
219
|
+
assert.throws(() => Config({ order: 'first' }), TypeError)
|
|
220
|
+
assert.throws(() => Config({ skills: 'yes' }), TypeError)
|
|
221
|
+
assert.throws(() => apply(makeContext().ctx, { order: 'first' }), TypeError)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('warns when a higher-priority skill shadows a bundled one', async () => {
|
|
225
|
+
const state = makeContext({
|
|
226
|
+
catalog: [
|
|
227
|
+
{
|
|
228
|
+
name: 'brainstorming',
|
|
229
|
+
description: 'project copy',
|
|
230
|
+
provider: 'filesystem',
|
|
231
|
+
source: 'project-dsh',
|
|
232
|
+
path: 'E:/work/ai/.dsh/skills/brainstorming/SKILL.md',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
})
|
|
236
|
+
apply(state.ctx)
|
|
237
|
+
|
|
238
|
+
await state.listeners.get('agent/created')({ agent: makeAgent() })
|
|
239
|
+
|
|
240
|
+
const report = state.warnings.join('\n')
|
|
241
|
+
assert.match(report, /brainstorming/)
|
|
242
|
+
assert.match(report, /project-dsh/)
|
|
243
|
+
assert.match(report, /filesystem/)
|
|
244
|
+
assert.match(report, /E:\/work\/ai\/\.dsh\/skills\/brainstorming\/SKILL\.md/)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('stays quiet when every bundled skill wins its own name', async () => {
|
|
248
|
+
const state = makeContext()
|
|
249
|
+
apply(state.ctx)
|
|
250
|
+
|
|
251
|
+
await state.listeners.get('agent/created')({ agent: makeAgent() })
|
|
252
|
+
await state.listeners.get('agent/created')({ agent: makeAgent('E:/other') })
|
|
253
|
+
|
|
254
|
+
assert.deepEqual(state.warnings, [])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('reports one shadowing warning per workspace, not one per agent', async () => {
|
|
258
|
+
const state = makeContext({
|
|
259
|
+
catalog: [
|
|
260
|
+
{
|
|
261
|
+
name: 'brainstorming',
|
|
262
|
+
description: 'project copy',
|
|
263
|
+
provider: 'filesystem',
|
|
264
|
+
source: 'project-dsh',
|
|
265
|
+
path: 'E:/work/ai/.dsh/skills/brainstorming/SKILL.md',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
})
|
|
269
|
+
apply(state.ctx)
|
|
270
|
+
|
|
271
|
+
await state.listeners.get('agent/created')({ agent: makeAgent() })
|
|
272
|
+
await state.listeners.get('agent/created')({ agent: makeAgent() })
|
|
273
|
+
assert.equal(state.warnings.length, 1)
|
|
274
|
+
|
|
275
|
+
await state.listeners.get('agent/created')({ agent: makeAgent('E:/elsewhere') })
|
|
276
|
+
assert.equal(state.warnings.length, 2)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('reports a failing skill lookup without failing agent creation', async () => {
|
|
280
|
+
const state = makeContext({
|
|
281
|
+
catalog: () => {
|
|
282
|
+
throw new Error('provider exploded')
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
apply(state.ctx)
|
|
286
|
+
|
|
287
|
+
await assert.doesNotReject(async () => state.listeners.get('agent/created')({ agent: makeAgent() }))
|
|
288
|
+
assert.equal(state.warnings.length, 1)
|
|
289
|
+
assert.match(state.warnings[0], /provider exploded/)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('documents the prompt-order placement the same way in code and both READMEs', async () => {
|
|
293
|
+
const sources = ['index.js', 'README.md', 'README.zh.md']
|
|
294
|
+
for (const source of sources) {
|
|
295
|
+
const text = await readFile(join(packageRoot, source), 'utf8')
|
|
296
|
+
assert.doesNotMatch(text, /100\s*[–-]\s*199/, `${source} still claims the stale tool-guidance band`)
|
|
297
|
+
assert.match(text, /[((]500[))]/, `${source} should name the plan policy's order`)
|
|
298
|
+
assert.match(text, /1000\+/, `${source} should state where tool guidance starts`)
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('keeps the English and Chinese READMEs structurally in sync', async () => {
|
|
303
|
+
const [english, chinese] = await Promise.all(
|
|
304
|
+
['README.md', 'README.zh.md'].map((file) => readFile(join(packageRoot, file), 'utf8')),
|
|
305
|
+
)
|
|
306
|
+
for (const pattern of [/^#{2,3} /gm, /^\| /gm, /^```/gm, /^```sh$/gm]) {
|
|
307
|
+
assert.equal(
|
|
308
|
+
[...chinese.matchAll(pattern)].length,
|
|
309
|
+
[...english.matchAll(pattern)].length,
|
|
310
|
+
`README.zh.md and README.md disagree on ${pattern}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('ships every path its test script and manifest reference', async () => {
|
|
316
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
|
|
317
|
+
assert.equal(manifest.scripts.test, 'node --test')
|
|
318
|
+
assert.ok(
|
|
319
|
+
manifest.files.some((entry) => entry === 'test' || entry.startsWith('test/')),
|
|
320
|
+
'package.json files[] must ship test/ so `npm test` finds the suite in the tarball',
|
|
321
|
+
)
|
|
322
|
+
for (const entry of manifest.files) {
|
|
323
|
+
if (entry.includes('*')) continue
|
|
324
|
+
assert.ok(existsSync(join(packageRoot, entry)), `files[] names a missing path: ${entry}`)
|
|
325
|
+
}
|
|
326
|
+
for (const dependency of Object.keys(manifest.dependencies ?? {})) {
|
|
327
|
+
assert.ok(
|
|
328
|
+
existsSync(join(packageRoot, 'node_modules', ...dependency.split('/'))),
|
|
329
|
+
`declared dependency ${dependency} is not installed in this checkout`,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
})
|