superpowers-mcp 6.3.6 → 6.3.7
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/README.ja.md +35 -56
- package/README.ko.md +35 -56
- package/README.md +35 -56
- package/README.zh-TW.md +35 -56
- package/out/server.js +23 -23
- package/package.json +10 -5
- package/scripts/upstream-drift.js +346 -0
- package/skills/brainstorming/SKILL.md +125 -25
- package/skills/brainstorming/scripts/start-server.ps1 +20 -1
- package/skills/brainstorming/scripts/start-server.sh +2 -2
- package/skills/executing-plans/SKILL.md +7 -1
- package/skills/finishing-a-development-branch/SKILL.md +15 -0
- package/skills/subagent-driven-development/SKILL.md +121 -36
- package/skills/subagent-driven-development/implementer-prompt.md +19 -0
- package/skills/subagent-driven-development/re-review-prompt.md +10 -4
- package/skills/subagent-driven-development/scripts/review-package +6 -0
- package/skills/subagent-driven-development/scripts/review-package.ps1 +7 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace +8 -1
- package/skills/subagent-driven-development/scripts/sdd-workspace.ps1 +23 -2
- package/skills/subagent-driven-development/task-reviewer-prompt.md +28 -10
- package/skills/systematic-debugging/SKILL.md +1 -1
- package/skills/test-driven-development/SKILL.md +27 -3
- package/skills/test-driven-development/writing-good-tests.md +7 -0
- package/skills/using-git-worktrees/SKILL.md +12 -0
- package/skills/verification-before-completion/SKILL.md +54 -1
- package/skills/writing-plans/SKILL.md +20 -5
- package/skills/writing-skills/SKILL.md +30 -0
package/package.json
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
"name": "superpowers-mcp",
|
|
3
3
|
"displayName": "Superpowers MCP",
|
|
4
4
|
"description": "Superpowers skills library (TDD, debugging, collaboration workflows) as an MCP server for VSCode and Antigravity",
|
|
5
|
-
"version": "6.3.
|
|
5
|
+
"version": "6.3.7",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
6
9
|
"publisher": "superpowers",
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"repository": {
|
|
9
12
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/Poseidoncode/superpowers-mcp"
|
|
13
|
+
"url": "git+https://github.com/Poseidoncode/superpowers-mcp.git"
|
|
11
14
|
},
|
|
12
15
|
"homepage": "https://github.com/Poseidoncode/superpowers-mcp",
|
|
13
16
|
"keywords": [
|
|
@@ -23,8 +26,8 @@
|
|
|
23
26
|
"agent"
|
|
24
27
|
],
|
|
25
28
|
"bin": {
|
|
26
|
-
"superpowers-mcp": "
|
|
27
|
-
"superpowers-setup": "
|
|
29
|
+
"superpowers-mcp": "out/server.js",
|
|
30
|
+
"superpowers-setup": "out/setup.js"
|
|
28
31
|
},
|
|
29
32
|
"files": [
|
|
30
33
|
"out/server.js",
|
|
@@ -43,7 +46,9 @@
|
|
|
43
46
|
"build": "node esbuild.js --production",
|
|
44
47
|
"watch": "node esbuild.js --watch",
|
|
45
48
|
"pretest": "npm run build",
|
|
46
|
-
"test": "node tests/edge_cases_test.js && node tests/run_test.js && node tests/brainstorm_server_test.js && node tests/prompts_compositions_test.js && node tests/setup_test.js",
|
|
49
|
+
"test": "node tests/edge_cases_test.js && node tests/run_test.js && node tests/brainstorm_server_test.js && node tests/prompts_compositions_test.js && node tests/upstream_sync_test.js && node tests/mcp_coverage_test.js && node tests/drift_test.js && node tests/setup_test.js",
|
|
50
|
+
"drift": "node scripts/upstream-drift.js --fetch",
|
|
51
|
+
"drift:record": "node scripts/upstream-drift.js --record",
|
|
47
52
|
"prepublishOnly": "npm run build"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Upstream drift report for the skills this fork ships.
|
|
4
|
+
*
|
|
5
|
+
* The fork imports upstream superpowers content in reviewed batches. Between
|
|
6
|
+
* batches nothing records which upstream files moved, so every sync starts with
|
|
7
|
+
* a manual hunt. This tool stores the upstream blob SHAs captured at the last
|
|
8
|
+
* sync (tests/upstream-sync-baseline.json) and reports:
|
|
9
|
+
*
|
|
10
|
+
* - adopted skill files that changed, appeared or disappeared upstream
|
|
11
|
+
* - tracked files that are missing from this fork
|
|
12
|
+
* - upstream skills this fork deliberately does not adopt
|
|
13
|
+
* - files this fork adds on top of upstream
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node scripts/upstream-drift.js # offline: baseline integrity + coverage
|
|
17
|
+
* node scripts/upstream-drift.js --fetch # compare the baseline against upstream
|
|
18
|
+
* node scripts/upstream-drift.js --record # refresh the baseline from upstream
|
|
19
|
+
*
|
|
20
|
+
* Options:
|
|
21
|
+
* --ref <ref> upstream ref to read (default: the baseline ref, else "dev")
|
|
22
|
+
* --repo <owner/name> upstream repository (default: obra/superpowers)
|
|
23
|
+
* --ignore <skill> record an upstream skill as deliberately not adopted
|
|
24
|
+
* (repeatable; --record only)
|
|
25
|
+
* --json machine-readable report
|
|
26
|
+
* --fail-on-drift exit 1 when an adopted file drifted (skipped on a
|
|
27
|
+
* truncated upstream listing)
|
|
28
|
+
*
|
|
29
|
+
* `--record` refuses truncated upstream listings so an incomplete API response
|
|
30
|
+
* can never replace the last complete baseline.
|
|
31
|
+
*
|
|
32
|
+
* The upstream git tree is read through `gh api` when the GitHub CLI is
|
|
33
|
+
* available and falls back to the public GitHub API (Node 18+). Zero
|
|
34
|
+
* dependencies. Only --record writes, and it writes the baseline file only.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require("fs");
|
|
38
|
+
const path = require("path");
|
|
39
|
+
const { execFileSync } = require("child_process");
|
|
40
|
+
|
|
41
|
+
const REPO_ROOT = path.join(__dirname, "..");
|
|
42
|
+
const SKILLS_DIR = path.join(REPO_ROOT, "skills");
|
|
43
|
+
const BASELINE_PATH = path.join(REPO_ROOT, "tests", "upstream-sync-baseline.json");
|
|
44
|
+
const DEFAULT_REPO = "obra/superpowers";
|
|
45
|
+
const DEFAULT_REF = "dev";
|
|
46
|
+
const MAX_LISTED = 40;
|
|
47
|
+
const NETWORK_TIMEOUT_MS = 20000;
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const opts = { fetch: false, record: false, json: false, failOnDrift: false, help: false, ref: "", repo: "", ignore: [] };
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg === "--fetch") opts.fetch = true;
|
|
54
|
+
else if (arg === "--record") opts.record = true;
|
|
55
|
+
else if (arg === "--json") opts.json = true;
|
|
56
|
+
else if (arg === "--fail-on-drift") opts.failOnDrift = true;
|
|
57
|
+
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
58
|
+
else if (arg === "--ref" || arg === "--repo" || arg === "--ignore") {
|
|
59
|
+
const value = argv[++i];
|
|
60
|
+
if (!value) throw new Error(`Missing value for ${arg}`);
|
|
61
|
+
if (arg === "--ref") opts.ref = value;
|
|
62
|
+
else if (arg === "--repo") opts.repo = value;
|
|
63
|
+
else opts.ignore.push(value);
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return opts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function printHelp() {
|
|
72
|
+
console.log(`Upstream drift report for the skills this fork ships.
|
|
73
|
+
|
|
74
|
+
node scripts/upstream-drift.js offline baseline integrity + local coverage
|
|
75
|
+
node scripts/upstream-drift.js --fetch compare the baseline against upstream
|
|
76
|
+
node scripts/upstream-drift.js --record refresh the baseline from upstream
|
|
77
|
+
|
|
78
|
+
Options: --ref <ref> --repo <owner/name> --ignore <skill> --json --fail-on-drift`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The skill directory an upstream path belongs to ("skills/<name>/..." -> "<name>"). */
|
|
82
|
+
function skillNameOf(filePath) {
|
|
83
|
+
const parts = String(filePath).split("/");
|
|
84
|
+
return parts.length > 2 && parts[0] === "skills" ? parts[1] : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readBaseline(baselinePath = BASELINE_PATH) {
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
|
|
89
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.files !== "object" || parsed.files === null) {
|
|
90
|
+
throw new Error(`${path.relative(REPO_ROOT, baselinePath)} is missing its "files" map`);
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Walk the fork's skills tree and return the upstream-style paths it covers. */
|
|
96
|
+
function localSkillPaths(skillsDir = SKILLS_DIR) {
|
|
97
|
+
const found = new Set();
|
|
98
|
+
const walk = (dir, relative) => {
|
|
99
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
100
|
+
const nextRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
walk(path.join(dir, entry.name), nextRelative);
|
|
103
|
+
} else if (entry.isFile()) {
|
|
104
|
+
found.add(`skills/${nextRelative}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
if (fs.existsSync(skillsDir)) {
|
|
109
|
+
walk(skillsDir, "");
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Classify the current upstream tree against the recorded baseline.
|
|
116
|
+
*
|
|
117
|
+
* `added` and `changed`/`removed` cover the skills this fork adopts, so they are
|
|
118
|
+
* actionable drift. Files that belong to a skill the fork does not adopt (and
|
|
119
|
+
* has not listed in ignoredUpstreamSkills) land in `unadoptedFiles`, which is a
|
|
120
|
+
* decision to make rather than drift to review.
|
|
121
|
+
*/
|
|
122
|
+
function classifyDrift(baselineFiles, upstreamFiles, ignoredSkills = []) {
|
|
123
|
+
const adoptedSkills = new Set(Object.keys(baselineFiles).map(skillNameOf).filter(Boolean));
|
|
124
|
+
const ignored = new Set(ignoredSkills);
|
|
125
|
+
const changed = [];
|
|
126
|
+
const added = [];
|
|
127
|
+
const removed = [];
|
|
128
|
+
const unadoptedFiles = [];
|
|
129
|
+
for (const [filePath, sha] of Object.entries(upstreamFiles)) {
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(baselineFiles, filePath)) {
|
|
131
|
+
if (baselineFiles[filePath] !== sha) changed.push(filePath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const skill = skillNameOf(filePath);
|
|
135
|
+
if (adoptedSkills.has(skill)) added.push(filePath);
|
|
136
|
+
else if (!ignored.has(skill)) unadoptedFiles.push(filePath);
|
|
137
|
+
}
|
|
138
|
+
for (const filePath of Object.keys(baselineFiles)) {
|
|
139
|
+
if (!Object.prototype.hasOwnProperty.call(upstreamFiles, filePath)) removed.push(filePath);
|
|
140
|
+
}
|
|
141
|
+
const byPath = (a, b) => a.localeCompare(b);
|
|
142
|
+
return {
|
|
143
|
+
changed: changed.sort(byPath),
|
|
144
|
+
added: added.sort(byPath),
|
|
145
|
+
removed: removed.sort(byPath),
|
|
146
|
+
unadoptedFiles: unadoptedFiles.sort(byPath),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Compare the recorded baseline with what the fork actually ships. */
|
|
151
|
+
function classifyCoverage(baseline, localPaths) {
|
|
152
|
+
const files = baseline.files || {};
|
|
153
|
+
const ignored = new Set(Array.isArray(baseline.ignoredUpstreamSkills) ? baseline.ignoredUpstreamSkills : []);
|
|
154
|
+
const localSkillDirs = new Set([...localPaths].map(skillNameOf).filter(Boolean));
|
|
155
|
+
const upstreamSkillDirs = new Set((baseline.upstreamSkills || Object.keys(files).map(skillNameOf)).filter(Boolean));
|
|
156
|
+
const byPath = (a, b) => a.localeCompare(b);
|
|
157
|
+
return {
|
|
158
|
+
missingLocal: Object.keys(files).filter((filePath) => !localPaths.has(filePath)).sort(byPath),
|
|
159
|
+
localExtra: [...localPaths].filter((filePath) => !Object.prototype.hasOwnProperty.call(files, filePath)).sort(byPath),
|
|
160
|
+
unadoptedSkills: [...upstreamSkillDirs].filter((name) => !localSkillDirs.has(name) && !ignored.has(name)).sort(byPath),
|
|
161
|
+
forkOnlySkills: [...localSkillDirs].filter((name) => !upstreamSkillDirs.has(name)).sort(byPath),
|
|
162
|
+
ignoredSkills: [...ignored].sort(byPath),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const encodePath = (value) => String(value).split("/").map(encodeURIComponent).join("/");
|
|
167
|
+
|
|
168
|
+
async function fetchUpstreamTree(repo, ref) {
|
|
169
|
+
const endpoint = `repos/${encodePath(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
|
170
|
+
let raw = null;
|
|
171
|
+
let source = "gh api";
|
|
172
|
+
let ghError = "";
|
|
173
|
+
try {
|
|
174
|
+
raw = execFileSync("gh", ["api", endpoint], {
|
|
175
|
+
encoding: "utf-8",
|
|
176
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
177
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
raw = null;
|
|
181
|
+
ghError = err && err.stderr ? String(err.stderr).trim().split("\n")[0] : String((err && err.message) || "");
|
|
182
|
+
}
|
|
183
|
+
if (!raw) {
|
|
184
|
+
source = "api.github.com";
|
|
185
|
+
const response = await fetch(`https://api.github.com/${endpoint}`, {
|
|
186
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "superpowers-mcp-drift" },
|
|
187
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Could not read ${repo}@${ref} (gh failed${ghError ? `: ${ghError}` : ""}; GitHub API HTTP ${response.status})`);
|
|
191
|
+
}
|
|
192
|
+
raw = await response.text();
|
|
193
|
+
}
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(raw);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw new Error(`Could not parse the upstream tree response: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
const files = {};
|
|
201
|
+
for (const entry of parsed.tree || []) {
|
|
202
|
+
if (entry && entry.type === "blob" && typeof entry.path === "string" && entry.path.startsWith("skills/")) {
|
|
203
|
+
files[entry.path] = entry.sha;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { commit: parsed.sha || "", files, source, truncated: Boolean(parsed.truncated) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function list(title, entries) {
|
|
210
|
+
if (entries.length === 0) return;
|
|
211
|
+
console.log(`${title} (${entries.length}):`);
|
|
212
|
+
for (const entry of entries.slice(0, MAX_LISTED)) console.log(` - ${entry}`);
|
|
213
|
+
if (entries.length > MAX_LISTED) console.log(` ... and ${entries.length - MAX_LISTED} more`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function reportCoverage(coverage) {
|
|
217
|
+
list("Tracked upstream files missing from the fork", coverage.missingLocal);
|
|
218
|
+
list("Upstream skills this fork does not adopt", coverage.unadoptedSkills);
|
|
219
|
+
list("Fork-only skills (not tracked upstream)", coverage.forkOnlySkills);
|
|
220
|
+
list("Fork-only files (not tracked upstream)", coverage.localExtra);
|
|
221
|
+
list("Upstream skills recorded as deliberately not adopted", coverage.ignoredSkills);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function reportDrift(drift) {
|
|
225
|
+
list("Adopted skill files changed upstream since the baseline", drift.changed);
|
|
226
|
+
list("New upstream files inside adopted skills", drift.added);
|
|
227
|
+
list("Adopted skill files removed upstream", drift.removed);
|
|
228
|
+
list("New upstream files in skills this fork does not adopt", drift.unadoptedFiles);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function requireCompleteTreeForRecord(tree) {
|
|
232
|
+
if (tree.truncated) {
|
|
233
|
+
throw new Error("Refusing to record a truncated upstream tree; the existing baseline was left unchanged");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function main() {
|
|
238
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
239
|
+
if (opts.help) {
|
|
240
|
+
printHelp();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let baseline = null;
|
|
245
|
+
try {
|
|
246
|
+
baseline = readBaseline();
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (!opts.record) throw err;
|
|
249
|
+
console.log(`No usable baseline yet (${err.message}); --record will create one.`);
|
|
250
|
+
baseline = { repo: DEFAULT_REPO, ref: DEFAULT_REF, commit: "", capturedAt: "", files: {}, upstreamSkills: [], ignoredUpstreamSkills: [] };
|
|
251
|
+
}
|
|
252
|
+
const localPaths = localSkillPaths();
|
|
253
|
+
|
|
254
|
+
if (opts.record) {
|
|
255
|
+
const repo = opts.repo || baseline.repo || DEFAULT_REPO;
|
|
256
|
+
const ref = opts.ref || baseline.ref || DEFAULT_REF;
|
|
257
|
+
const ignored = [...new Set([...(baseline.ignoredUpstreamSkills || []), ...opts.ignore])].sort();
|
|
258
|
+
const tree = await fetchUpstreamTree(repo, ref);
|
|
259
|
+
requireCompleteTreeForRecord(tree);
|
|
260
|
+
const previousFiles = baseline.files || {};
|
|
261
|
+
const previousDrift = classifyDrift(previousFiles, tree.files, ignored);
|
|
262
|
+
|
|
263
|
+
// Track only the upstream files this fork actually ships: the baseline
|
|
264
|
+
// states which upstream blobs were imported, so a later deletion of an
|
|
265
|
+
// adopted file shows up as a missing local path.
|
|
266
|
+
const files = {};
|
|
267
|
+
for (const filePath of Object.keys(tree.files).sort()) {
|
|
268
|
+
if (localPaths.has(filePath)) files[filePath] = tree.files[filePath];
|
|
269
|
+
}
|
|
270
|
+
const upstreamSkills = [...new Set(Object.keys(tree.files).map(skillNameOf).filter(Boolean))].sort();
|
|
271
|
+
const recorded = { repo, ref, commit: tree.commit, capturedAt: new Date().toISOString().slice(0, 10), files, upstreamSkills, ignoredUpstreamSkills: ignored };
|
|
272
|
+
fs.writeFileSync(BASELINE_PATH, JSON.stringify(recorded, null, 2) + "\n", "utf-8");
|
|
273
|
+
|
|
274
|
+
console.log(`Baseline recorded from ${tree.source}: ${repo}@${ref} ${tree.commit.slice(0, 12)} — ${Object.keys(files).length} tracked files of ${Object.keys(tree.files).length} upstream skill files`);
|
|
275
|
+
if (Object.keys(previousFiles).length > 0) {
|
|
276
|
+
const moved = previousDrift.changed.length + previousDrift.added.length + previousDrift.removed.length;
|
|
277
|
+
console.log(moved === 0
|
|
278
|
+
? "No adopted skill file moved upstream since the previous baseline."
|
|
279
|
+
: `warning: ${moved} adopted skill file(s) moved upstream since the previous baseline — record only what you actually reviewed.`);
|
|
280
|
+
}
|
|
281
|
+
if (opts.ignore.length > 0) {
|
|
282
|
+
console.log(`Recorded as deliberately not adopted: ${opts.ignore.join(", ")}`);
|
|
283
|
+
}
|
|
284
|
+
reportCoverage(classifyCoverage(recorded, localPaths));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const repo = opts.repo || baseline.repo || DEFAULT_REPO;
|
|
289
|
+
const ref = opts.ref || baseline.ref || DEFAULT_REF;
|
|
290
|
+
const coverage = classifyCoverage(baseline, localPaths);
|
|
291
|
+
const tracked = Object.keys(baseline.files).length;
|
|
292
|
+
const ignored = Array.isArray(baseline.ignoredUpstreamSkills) ? baseline.ignoredUpstreamSkills : [];
|
|
293
|
+
|
|
294
|
+
if (!opts.fetch) {
|
|
295
|
+
if (opts.json) {
|
|
296
|
+
console.log(JSON.stringify({ baseline: { repo, ref, commit: baseline.commit, capturedAt: baseline.capturedAt, tracked }, coverage }, null, 2));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log(`Baseline: ${repo}@${ref} ${String(baseline.commit).slice(0, 12)} (captured ${baseline.capturedAt})`);
|
|
300
|
+
console.log(`Tracked upstream skill files: ${tracked}`);
|
|
301
|
+
reportCoverage(coverage);
|
|
302
|
+
console.log("\nRun `node scripts/upstream-drift.js --fetch` to compare the baseline against upstream.");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const tree = await fetchUpstreamTree(repo, ref);
|
|
307
|
+
const drift = classifyDrift(baseline.files, tree.files, ignored);
|
|
308
|
+
const driftCount = drift.changed.length + drift.added.length + drift.removed.length;
|
|
309
|
+
if (opts.json) {
|
|
310
|
+
console.log(JSON.stringify({
|
|
311
|
+
baseline: { repo, ref, commit: baseline.commit, capturedAt: baseline.capturedAt, tracked },
|
|
312
|
+
upstream: { ref, commit: tree.commit, tracked: Object.keys(tree.files).length, source: tree.source, truncated: tree.truncated },
|
|
313
|
+
drift,
|
|
314
|
+
driftCount,
|
|
315
|
+
coverage,
|
|
316
|
+
}, null, 2));
|
|
317
|
+
} else {
|
|
318
|
+
console.log(`Baseline: ${repo}@${baseline.ref} ${String(baseline.commit).slice(0, 12)} (captured ${baseline.capturedAt})`);
|
|
319
|
+
console.log(`Upstream: ${repo}@${ref} ${String(tree.commit).slice(0, 12)} via ${tree.source}`);
|
|
320
|
+
reportDrift(drift);
|
|
321
|
+
reportCoverage(coverage);
|
|
322
|
+
if (driftCount === 0) {
|
|
323
|
+
console.log("\nNo drift in adopted skill files.");
|
|
324
|
+
} else {
|
|
325
|
+
console.log(`\n${driftCount} adopted skill file(s) changed upstream. Review them before the next sync.`);
|
|
326
|
+
}
|
|
327
|
+
if (drift.unadoptedFiles.length > 0) {
|
|
328
|
+
console.log(`${drift.unadoptedFiles.length} upstream file(s) belong to skills this fork does not adopt — adopt or record them with --ignore.`);
|
|
329
|
+
}
|
|
330
|
+
if (tree.truncated) {
|
|
331
|
+
console.log("warning: the upstream listing was truncated, so this comparison may be partial.");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (opts.failOnDrift && driftCount > 0 && !tree.truncated) {
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (require.main === module) {
|
|
340
|
+
main().catch((err) => {
|
|
341
|
+
console.error(`❌ upstream-drift: ${err.message}`);
|
|
342
|
+
process.exitCode = 2;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = { parseArgs, skillNameOf, readBaseline, localSkillPaths, classifyDrift, classifyCoverage, requireCompleteTreeForRecord };
|
|
@@ -11,12 +11,48 @@ Start by classifying how much process the request needs, then work
|
|
|
11
11
|
through your path: understand the context, refine the idea, present a
|
|
12
12
|
design, and get your human partner's approval.
|
|
13
13
|
|
|
14
|
+
## Establish Shared Understanding
|
|
15
|
+
|
|
16
|
+
The outcome of brainstorming is an understanding your human partner can
|
|
17
|
+
recognize and correct, grounded in what they want to accomplish.
|
|
18
|
+
|
|
19
|
+
1. **Discover intent.** Use the request and available context to identify
|
|
20
|
+
the intended outcome, who it is for, and what success looks like. When
|
|
21
|
+
that information is missing, ask one focused question about purpose or
|
|
22
|
+
intended use before proposing features or an approach. Knowing the app
|
|
23
|
+
genre does not tell you why your partner wants it. Gathering missing
|
|
24
|
+
requirements does not ask them to authorize the task again.
|
|
25
|
+
2. **Write back your understanding.** Summarize the intended outcome,
|
|
26
|
+
relevant constraints, and success criteria in a short note your partner
|
|
27
|
+
can assess. Separate what they said from assumptions. Invite correction
|
|
28
|
+
and incorporate their answer before treating this as the design brief.
|
|
29
|
+
3. **Carry intent into the design.** Preserve the agreed understanding in
|
|
30
|
+
the selected path's design artifact: the written spec for architectural
|
|
31
|
+
work, or the in-chat design/probe for bounded work and spikes. Check
|
|
32
|
+
proposed features and technical choices against that understanding.
|
|
33
|
+
|
|
34
|
+
When the request already supplies the purpose and constraints, reflect
|
|
35
|
+
that understanding instead of asking the same questions again. Keep the
|
|
36
|
+
note concise; its accuracy and the opportunity to correct it matter.
|
|
37
|
+
|
|
14
38
|
<HARD-GATE>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
Before taking any implementation action, including invoking an
|
|
40
|
+
implementation skill, writing product code, scaffolding, installing
|
|
41
|
+
product dependencies, or creating an external project, complete the
|
|
42
|
+
selected path's prerequisites:
|
|
43
|
+
|
|
44
|
+
- Spike: the human partner approves the question and probe.
|
|
45
|
+
- Bounded: the human partner approves the short in-chat design.
|
|
46
|
+
- Architectural: the human partner reviews and approves the written spec,
|
|
47
|
+
then reviews the written implementation plan and selects its execution
|
|
48
|
+
method. Conversational design approval only permits writing the spec;
|
|
49
|
+
written-spec approval only permits invoking writing-plans.
|
|
50
|
+
|
|
51
|
+
A reply approves the stage actually presented. Approval of an idea or
|
|
52
|
+
feature scope does not approve artifacts that do not exist yet. Resume
|
|
53
|
+
at the earliest incomplete stage; do not turn one approval into permission
|
|
54
|
+
to skip the rest of the selected path. Read-only project exploration is
|
|
55
|
+
allowed while those prerequisites remain incomplete.
|
|
20
56
|
</HARD-GATE>
|
|
21
57
|
|
|
22
58
|
## Three Paths
|
|
@@ -53,18 +89,17 @@ stop, say so, and step up. Nothing downgrades mid-task.
|
|
|
53
89
|
|
|
54
90
|
## Anti-Pattern: "Too Simple To Need Approval"
|
|
55
91
|
|
|
56
|
-
Every path ends with your human partner approving
|
|
57
|
-
implementation. A
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
artifact, never the approval.
|
|
92
|
+
Every path ends with your human partner approving the required design
|
|
93
|
+
before implementation. A bounded change may need only two sentences in
|
|
94
|
+
chat. A new todo-list project is architectural and requires the written
|
|
95
|
+
spec and planning handoffs. Scale the artifact to the selected path;
|
|
96
|
+
complete that path's reviews before implementation.
|
|
62
97
|
|
|
63
98
|
## Red Flags
|
|
64
99
|
|
|
65
100
|
| Thought | Reality |
|
|
66
101
|
|---------|---------|
|
|
67
|
-
| "This is too simple to need a design" |
|
|
102
|
+
| "This is too simple to need a design" | Follow the selected path: a bounded change gets a short chat design; an architectural change gets the written spec and planning handoffs. |
|
|
68
103
|
| "I'll call it bounded and skip the spec" | Reaching for a label to skip work IS the doubt — take the heavier path. |
|
|
69
104
|
| "It's bounded and the design is obvious — I'll start while they read it" | The gate is the approval, not the design's length. Present, then stop until you hear yes. |
|
|
70
105
|
| "I understand this kind of app, so it's bounded" | Bounded measures the repo, not your familiarity. A new project has no existing flow — it is architectural. |
|
|
@@ -98,7 +133,7 @@ your path and complete them in order.
|
|
|
98
133
|
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
|
99
134
|
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
|
100
135
|
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
|
101
|
-
7. **
|
|
136
|
+
7. **Planning-handoff review** — simulate the next planning stage, rate and inventory its burdens, and take the required bounded branch (see below)
|
|
102
137
|
8. **User reviews written spec** — ask user to review the spec file before proceeding
|
|
103
138
|
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
|
104
139
|
|
|
@@ -119,7 +154,7 @@ digraph brainstorming {
|
|
|
119
154
|
"Present design sections" [shape=box];
|
|
120
155
|
"User approves design?" [shape=diamond];
|
|
121
156
|
"Write design doc" [shape=box];
|
|
122
|
-
"
|
|
157
|
+
"Planning-handoff review\n(rate; burden ledger; bounded branch)" [shape=box];
|
|
123
158
|
"User reviews spec?" [shape=diamond];
|
|
124
159
|
"Invoke writing-plans skill" [shape=doublecircle];
|
|
125
160
|
"Hidden complexity? Upgrade path" [shape=box];
|
|
@@ -139,8 +174,8 @@ digraph brainstorming {
|
|
|
139
174
|
"Present design sections" -> "User approves design?";
|
|
140
175
|
"User approves design?" -> "Present design sections" [label="no, revise"];
|
|
141
176
|
"User approves design?" -> "Write design doc" [label="yes"];
|
|
142
|
-
"Write design doc" -> "
|
|
143
|
-
"
|
|
177
|
+
"Write design doc" -> "Planning-handoff review\n(rate; burden ledger; bounded branch)";
|
|
178
|
+
"Planning-handoff review\n(rate; burden ledger; bounded branch)" -> "User reviews spec?";
|
|
144
179
|
"User reviews spec?" -> "Write design doc" [label="changes requested"];
|
|
145
180
|
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
|
|
146
181
|
}
|
|
@@ -169,6 +204,7 @@ is the whole process.
|
|
|
169
204
|
- For appropriately-scoped projects, ask questions one at a time to refine the idea
|
|
170
205
|
- Prefer multiple choice questions when possible, but open-ended is fine too
|
|
171
206
|
- Only one question per message - if a topic needs more exploration, break it into multiple questions
|
|
207
|
+
- A request for context is not an answer - if the user asks for trade-offs, implications, or examples instead of choosing, give them that and then re-ask the same question. It stays the only open question until they decide; don't pair it with a new one
|
|
172
208
|
- Focus on understanding: purpose, constraints, success criteria
|
|
173
209
|
|
|
174
210
|
**Exploring approaches:**
|
|
@@ -209,15 +245,79 @@ is the whole process.
|
|
|
209
245
|
- Use elements-of-style:writing-clearly-and-concisely skill if available
|
|
210
246
|
- Commit the design document to git
|
|
211
247
|
|
|
212
|
-
**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
248
|
+
**Planning-Handoff Review:**
|
|
249
|
+
This is the spec self-review. Before asking for review, set the conversation
|
|
250
|
+
aside and temporarily act as a
|
|
251
|
+
fresh planning agent whose only inputs are the finished spec and the repository.
|
|
252
|
+
Begin mapping how you would turn the spec into an implementation plan, but do not
|
|
253
|
+
write that plan. Trace each affected entry point through the relevant components
|
|
254
|
+
and state transitions. Note each point where planning would require you to
|
|
255
|
+
rediscover design intent or invent a binding decision rather than choose an
|
|
256
|
+
implementation detail. Include placeholders, internal contradictions, scope
|
|
257
|
+
problems, and ambiguous requirements in this assessment; they are handoff seams,
|
|
258
|
+
not a separate review stage.
|
|
259
|
+
|
|
260
|
+
Rate the spec's planning-handoff readiness from 0.0–9.9 and explain what
|
|
261
|
+
prevents the next higher rating using repository and artifact evidence:
|
|
262
|
+
0 = reject; 2 = major redesign; 4 = substantial design rescue; 6 = usable but
|
|
263
|
+
planning must reconstruct a binding decision; 8 = handoff-ready with only
|
|
264
|
+
planning-owned choices; 9.9 = rare exemplary ceiling, never perfection. The
|
|
265
|
+
rating prompts the assessment; it does not decide whether the artifact improves.
|
|
266
|
+
|
|
267
|
+
Translate every design-owned reason preventing the next higher rating into a
|
|
268
|
+
burden ledger before editing. One burden is one independent binding decision or
|
|
269
|
+
piece of design reconstruction the planning agent must resolve before it can
|
|
270
|
+
specify implementation work. Record planning-owned choices separately rather
|
|
271
|
+
than counting them as burdens. For each burden, state the repository or artifact
|
|
272
|
+
evidence, what the planner would have to invent, and its weight:
|
|
273
|
+
|
|
274
|
+
- **minor (1):** a localized clarification or reconstruction;
|
|
275
|
+
- **major (2):** an unresolved binding decision or cross-component design
|
|
276
|
+
uncertainty. Record contradictions and departures from the approved design
|
|
277
|
+
separately; they are not ordinary tradeable burdens.
|
|
278
|
+
|
|
279
|
+
After the initial rating and ledger, take exactly one branch:
|
|
280
|
+
|
|
281
|
+
- If the rating is below 9.0 **or** the ledger contains any burden, preserve the
|
|
282
|
+
initial draft, return to the spec-writer role, and make one bounded improvement
|
|
283
|
+
pass targeting the evidence-based reasons preventing 9.0 and the named
|
|
284
|
+
burdens. Do not resolve a purely planning-owned choice merely to raise the
|
|
285
|
+
rating.
|
|
286
|
+
- Only if the rating is at least 9.0 **and** the burden ledger is empty, make no
|
|
287
|
+
edit.
|
|
288
|
+
|
|
289
|
+
The bounded pass is the only review-driven editing phase after the first
|
|
290
|
+
complete draft. It may clarify, reconcile, and complete the spec using the
|
|
291
|
+
approved design, stated requirements, and repository evidence; preserve the
|
|
292
|
+
binding decisions already approved in the conversation. When an improvement
|
|
293
|
+
would require a new or changed binding design decision, leave it as a burden
|
|
294
|
+
instead of choosing it.
|
|
295
|
+
|
|
296
|
+
After the pass, close editing and reassess both versions read-only. Trace every
|
|
297
|
+
concept changed by the pass through the whole spec, including the state model,
|
|
298
|
+
entry points, failure/recovery rules, and acceptance criteria. Give a fresh
|
|
299
|
+
rating, build the final burden ledger from scratch, and compare it with the
|
|
300
|
+
initial ledger. Include burdens that moved or appeared elsewhere. Check
|
|
301
|
+
separately for a newly introduced major burden, contradiction, departure from
|
|
302
|
+
approved design, or scope change. More specific wording is not automatically an
|
|
303
|
+
improvement.
|
|
304
|
+
|
|
305
|
+
Select the revised draft only when its total weighted burden is lower and it
|
|
306
|
+
introduces no new major burden, contradiction, or design departure. New minor
|
|
307
|
+
burdens are permitted only when the total burden still falls. Otherwise restore
|
|
308
|
+
the preserved initial draft. Restoring the original is the only spec mutation
|
|
309
|
+
permitted after reassessment: do not fix the revised draft or begin another
|
|
310
|
+
pass. Present the selected spec through the normal user review handoff, and
|
|
311
|
+
commit the selected draft — with any changes the user requests afterwards — so
|
|
312
|
+
that handoff always points at a committed revision.
|
|
313
|
+
Keep the ratings, burden ledgers, and comparison in your private review; do not
|
|
314
|
+
add them to the spec or require them as a separate handoff artifact.
|
|
315
|
+
If the original was restored, mention briefly that the tentative revision was
|
|
316
|
+
rejected and the original retained; do not add a separate review artifact.
|
|
317
|
+
|
|
318
|
+
Accept, restore, and report based on burden—not whether the rating rose. The
|
|
319
|
+
review never grants permission to begin planning. Do not produce task sequencing,
|
|
320
|
+
invoke `writing-plans`, or repeat the pass.
|
|
221
321
|
|
|
222
322
|
**User Review Gate:**
|
|
223
323
|
After the spec review loop passes, ask the user to review the written spec before proceeding:
|
|
@@ -67,7 +67,9 @@ $projectDir = ""
|
|
|
67
67
|
$foreground = $false
|
|
68
68
|
$forceBackground = $false
|
|
69
69
|
$bindHost = "127.0.0.1"
|
|
70
|
+
if ($env:BRAINSTORM_HOST) { $bindHost = $env:BRAINSTORM_HOST }
|
|
70
71
|
$urlHost = ""
|
|
72
|
+
if ($env:BRAINSTORM_URL_HOST) { $urlHost = $env:BRAINSTORM_URL_HOST }
|
|
71
73
|
$idleTimeoutMinutes = ""
|
|
72
74
|
|
|
73
75
|
for ($i = 0; $i -lt $args.Count; $i++) {
|
|
@@ -151,17 +153,34 @@ if ($env:CODEX_CI -and -not $foreground -and -not $forceBackground) {
|
|
|
151
153
|
$foreground = $true
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
$
|
|
156
|
+
$baseSessionId = "$PID-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
|
|
155
157
|
$brainstormRoot = ""
|
|
156
158
|
if ($projectDir -ne "") {
|
|
157
159
|
$brainstormRoot = Join-Path $projectDir ".superpowers/brainstorm"
|
|
160
|
+
# $PID is the invoking host process, so two starts in the same second from
|
|
161
|
+
# one pwsh session would resolve to the same id and die on the live
|
|
162
|
+
# session's redirected log file. Take the next free directory instead.
|
|
163
|
+
$sessionId = $baseSessionId
|
|
158
164
|
$sessionDir = Join-Path $brainstormRoot $sessionId
|
|
165
|
+
$sessionSuffix = 2
|
|
166
|
+
while (Test-Path -LiteralPath $sessionDir) {
|
|
167
|
+
$sessionId = "$baseSessionId-$sessionSuffix"
|
|
168
|
+
$sessionDir = Join-Path $brainstormRoot $sessionId
|
|
169
|
+
$sessionSuffix++
|
|
170
|
+
}
|
|
159
171
|
# Reuse the last bound port and session key so a restart keeps an
|
|
160
172
|
# already-open browser tab connected to the same URL with a valid cookie.
|
|
161
173
|
$env:BRAINSTORM_PORT_FILE = Join-Path $brainstormRoot ".last-port"
|
|
162
174
|
$env:BRAINSTORM_TOKEN_FILE = Join-Path $brainstormRoot ".last-token"
|
|
163
175
|
} else {
|
|
176
|
+
$sessionId = $baseSessionId
|
|
164
177
|
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
|
|
178
|
+
$sessionSuffix = 2
|
|
179
|
+
while (Test-Path -LiteralPath $sessionDir) {
|
|
180
|
+
$sessionId = "$baseSessionId-$sessionSuffix"
|
|
181
|
+
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
|
|
182
|
+
$sessionSuffix++
|
|
183
|
+
}
|
|
165
184
|
# $env: assignments persist in the invoking pwsh session; a stale project
|
|
166
185
|
# token/port file from an earlier --project-dir run must not leak into an
|
|
167
186
|
# ephemeral session (it would defeat key rotation and could overwrite the
|
|
@@ -23,8 +23,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
23
23
|
PROJECT_DIR=""
|
|
24
24
|
FOREGROUND="false"
|
|
25
25
|
FORCE_BACKGROUND="false"
|
|
26
|
-
BIND_HOST="127.0.0.1"
|
|
27
|
-
URL_HOST=""
|
|
26
|
+
BIND_HOST="${BRAINSTORM_HOST:-127.0.0.1}"
|
|
27
|
+
URL_HOST="${BRAINSTORM_URL_HOST:-}"
|
|
28
28
|
IDLE_TIMEOUT_MINUTES=""
|
|
29
29
|
while [[ $# -gt 0 ]]; do
|
|
30
30
|
case "$1" in
|