superpowers-mcp 6.0.1 → 6.0.3
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 +205 -0
- package/README.ko.md +205 -0
- package/README.md +41 -3
- package/README.zh-TW.md +40 -3
- package/out/server.js +25 -22
- package/package.json +5 -1
- package/skills/brainstorming/scripts/helper.js +4 -0
- package/skills/brainstorming/scripts/server.cjs +30 -7
- package/skills/brainstorming/scripts/start-server.ps1 +232 -0
- package/skills/brainstorming/scripts/stop-server.ps1 +84 -0
- package/skills/brainstorming/visual-companion.md +8 -0
- package/skills/finishing-a-development-branch/SKILL.md +11 -10
- package/skills/subagent-driven-development/SKILL.md +7 -7
- package/skills/subagent-driven-development/scripts/review-package +1 -1
- package/skills/subagent-driven-development/scripts/review-package.ps1 +52 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace.ps1 +11 -0
- package/skills/subagent-driven-development/scripts/task-brief.ps1 +51 -0
- package/skills/subagent-driven-development/task-reviewer-prompt.md +3 -2
- package/skills/systematic-debugging/find-polluter.ps1 +60 -0
- package/skills/systematic-debugging/find-polluter.sh +12 -3
- package/skills/systematic-debugging/root-cause-tracing.md +3 -0
- package/skills/test-driven-development/SKILL.md +1 -1
- package/skills/writing-skills/SKILL.md +1 -1
- package/skills/writing-skills/anthropic-best-practices.md +2 -2
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.0.
|
|
5
|
+
"version": "6.0.3",
|
|
6
6
|
"publisher": "superpowers",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"out/server.js",
|
|
30
30
|
"skills"
|
|
31
31
|
],
|
|
32
|
+
"overrides": {
|
|
33
|
+
"@hono/node-server": ">=2.0.5",
|
|
34
|
+
"fast-uri": ">=3.1.4"
|
|
35
|
+
},
|
|
32
36
|
"scripts": {
|
|
33
37
|
"build": "node esbuild.js --production",
|
|
34
38
|
"watch": "node esbuild.js --watch",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
el.style.setProperty('--status-color', color);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Self-styled so it works on framed and full-document screens alike.
|
|
60
61
|
function showTombstone() {
|
|
61
62
|
if (tombstoneShown) return;
|
|
62
63
|
tombstoneShown = true;
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// Capture clicks on choice elements
|
|
135
137
|
document.addEventListener('click', (e) => {
|
|
136
138
|
const target = e.target.closest('[data-choice]');
|
|
137
139
|
if (!target) return;
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
|
|
146
148
|
});
|
|
147
149
|
|
|
150
|
+
// Frame UI: selection tracking
|
|
148
151
|
window.selectedChoice = null;
|
|
149
152
|
|
|
150
153
|
window.toggleSelect = function(el) {
|
|
@@ -161,6 +164,7 @@
|
|
|
161
164
|
window.selectedChoice = el.dataset.choice;
|
|
162
165
|
};
|
|
163
166
|
|
|
167
|
+
// Expose API for explicit use
|
|
164
168
|
window.brainstorm = {
|
|
165
169
|
send: sendEvent,
|
|
166
170
|
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
|
|
@@ -62,6 +62,10 @@ function decodeFrame(buffer) {
|
|
|
62
62
|
offset = 10;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
|
|
66
|
+
throw new Error('WebSocket frame payload exceeds maximum allowed size');
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
const maskOffset = offset;
|
|
66
70
|
const dataOffset = offset + 4;
|
|
67
71
|
const totalLen = dataOffset + payloadLen;
|
|
@@ -227,9 +231,16 @@ function isTruthyEnv(value) {
|
|
|
227
231
|
return !['0', 'false', 'no', 'off'].includes(normalized);
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
function escapeHtmlText(value) {
|
|
235
|
+
return String(value)
|
|
236
|
+
.replace(/&/g, '&')
|
|
237
|
+
.replace(/</g, '<')
|
|
238
|
+
.replace(/>/g, '>')
|
|
239
|
+
.replace(/"/g, '"');
|
|
240
|
+
}
|
|
241
|
+
|
|
230
242
|
function brandMarkup() {
|
|
231
|
-
const
|
|
232
|
-
const version = escHtml(SUPERPOWERS_VERSION);
|
|
243
|
+
const version = escapeHtmlText(SUPERPOWERS_VERSION);
|
|
233
244
|
const text = SUPERPOWERS_TELEMETRY_DISABLED
|
|
234
245
|
? 'Prime Radiant Superpowers v' + version
|
|
235
246
|
: 'Superpowers v' + version;
|
|
@@ -296,6 +307,7 @@ function isRegularFileInsideContentDir(filePath) {
|
|
|
296
307
|
stat = fs.lstatSync(filePath);
|
|
297
308
|
if (stat.isSymbolicLink()) return false;
|
|
298
309
|
if (!stat.isFile()) return false;
|
|
310
|
+
if (stat.nlink !== 1) return false;
|
|
299
311
|
realContentDir = fs.realpathSync(CONTENT_DIR);
|
|
300
312
|
realFilePath = fs.realpathSync(filePath);
|
|
301
313
|
} catch (e) {
|
|
@@ -340,6 +352,17 @@ function isAuthorized(req) {
|
|
|
340
352
|
return false;
|
|
341
353
|
}
|
|
342
354
|
|
|
355
|
+
function pathnameOf(url) {
|
|
356
|
+
const q = url.indexOf('?');
|
|
357
|
+
return q >= 0 ? url.slice(0, q) : url;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function queryKey(url) {
|
|
361
|
+
const q = url.indexOf('?');
|
|
362
|
+
if (q < 0) return null;
|
|
363
|
+
return new URLSearchParams(url.slice(q + 1)).get('key');
|
|
364
|
+
}
|
|
365
|
+
|
|
343
366
|
function securityHeaders(headers = {}) {
|
|
344
367
|
return {
|
|
345
368
|
'Referrer-Policy': 'no-referrer',
|
|
@@ -375,9 +398,8 @@ function handleRequest(req, res) {
|
|
|
375
398
|
res.setHeader('Set-Cookie',
|
|
376
399
|
COOKIE_NAME + '=' + TOKEN + '; HttpOnly; SameSite=Strict; Path=/');
|
|
377
400
|
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const keyFromQuery = qIdx >= 0 ? new URLSearchParams(req.url.slice(qIdx + 1)).get('key') : null;
|
|
401
|
+
const pathname = pathnameOf(req.url);
|
|
402
|
+
const keyFromQuery = queryKey(req.url);
|
|
381
403
|
if (req.method === 'GET' && pathname === '/' && keyFromQuery && timingSafeEqualStr(keyFromQuery, TOKEN)) {
|
|
382
404
|
res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' }));
|
|
383
405
|
res.end(bootstrapPage(TOKEN));
|
|
@@ -513,9 +535,10 @@ function maybeOpenBrowser() {
|
|
|
513
535
|
if (clients.size > 0) return; // the user already opened it
|
|
514
536
|
const url = companionUrl(); // must carry the key or the gate 403s it
|
|
515
537
|
const cp = require('child_process');
|
|
516
|
-
// Operator-provided launcher: run
|
|
538
|
+
// Operator-provided launcher: run via execFile (no shell) so a malicious env var
|
|
539
|
+
// cannot inject commands through shell metacharacters.
|
|
517
540
|
if (process.env.BRAINSTORM_OPEN_CMD) {
|
|
518
|
-
try { cp.
|
|
541
|
+
try { cp.execFile(process.env.BRAINSTORM_OPEN_CMD, [url], () => {}); } catch (e) { /* best effort */ }
|
|
519
542
|
return;
|
|
520
543
|
}
|
|
521
544
|
// Platform launchers: pass the URL as an argv element via execFile (no shell),
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Start the brainstorm server and output connection info.
|
|
3
|
+
# Usage: ./start-server.ps1 [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = "Stop"
|
|
6
|
+
|
|
7
|
+
function Write-JsonError {
|
|
8
|
+
param([string]$Message)
|
|
9
|
+
[pscustomobject]@{ error = $Message } | ConvertTo-Json -Compress
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function New-ServerId {
|
|
13
|
+
$bytes = New-Object byte[] 24
|
|
14
|
+
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
|
15
|
+
try {
|
|
16
|
+
$rng.GetBytes($bytes)
|
|
17
|
+
} finally {
|
|
18
|
+
$rng.Dispose()
|
|
19
|
+
}
|
|
20
|
+
-join ($bytes | ForEach-Object { $_.ToString("x2") })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Set-ProcessEnvironment {
|
|
24
|
+
param([hashtable]$Values)
|
|
25
|
+
foreach ($key in $Values.Keys) {
|
|
26
|
+
Set-Item -Path "Env:$key" -Value ([string]$Values[$key])
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Protect-PathForCurrentUser {
|
|
31
|
+
param([string]$Path)
|
|
32
|
+
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
|
|
37
|
+
$acl = Get-Acl -LiteralPath $item.FullName
|
|
38
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
39
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
40
|
+
$rights = [System.Security.AccessControl.FileSystemRights]::FullControl
|
|
41
|
+
if ($item.PSIsContainer) {
|
|
42
|
+
$inheritance = [System.Security.AccessControl.InheritanceFlags]"ContainerInherit,ObjectInherit"
|
|
43
|
+
} else {
|
|
44
|
+
$inheritance = [System.Security.AccessControl.InheritanceFlags]::None
|
|
45
|
+
}
|
|
46
|
+
$propagation = [System.Security.AccessControl.PropagationFlags]::None
|
|
47
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
48
|
+
$identity,
|
|
49
|
+
$rights,
|
|
50
|
+
$inheritance,
|
|
51
|
+
$propagation,
|
|
52
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
53
|
+
)
|
|
54
|
+
$acl.SetAccessRule($rule)
|
|
55
|
+
Set-Acl -LiteralPath $item.FullName -AclObject $acl
|
|
56
|
+
} catch {
|
|
57
|
+
# Best-effort parity with the Unix helper's umask 077.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$scriptDir = Split-Path -Parent $PSCommandPath
|
|
62
|
+
$projectDir = ""
|
|
63
|
+
$foreground = $false
|
|
64
|
+
$forceBackground = $false
|
|
65
|
+
$bindHost = "127.0.0.1"
|
|
66
|
+
$urlHost = ""
|
|
67
|
+
$idleTimeoutMinutes = ""
|
|
68
|
+
|
|
69
|
+
for ($i = 0; $i -lt $args.Count; $i++) {
|
|
70
|
+
switch ($args[$i]) {
|
|
71
|
+
"--project-dir" {
|
|
72
|
+
$i++
|
|
73
|
+
if ($i -ge $args.Count) { Write-JsonError "Missing value for --project-dir"; exit 1 }
|
|
74
|
+
$projectDir = $args[$i]
|
|
75
|
+
}
|
|
76
|
+
"--host" {
|
|
77
|
+
$i++
|
|
78
|
+
if ($i -ge $args.Count) { Write-JsonError "Missing value for --host"; exit 1 }
|
|
79
|
+
$bindHost = $args[$i]
|
|
80
|
+
}
|
|
81
|
+
"--url-host" {
|
|
82
|
+
$i++
|
|
83
|
+
if ($i -ge $args.Count) { Write-JsonError "Missing value for --url-host"; exit 1 }
|
|
84
|
+
$urlHost = $args[$i]
|
|
85
|
+
}
|
|
86
|
+
"--idle-timeout-minutes" {
|
|
87
|
+
$i++
|
|
88
|
+
if ($i -ge $args.Count) { Write-JsonError "Missing value for --idle-timeout-minutes"; exit 1 }
|
|
89
|
+
$idleTimeoutMinutes = $args[$i]
|
|
90
|
+
}
|
|
91
|
+
"--open" {
|
|
92
|
+
$env:BRAINSTORM_OPEN = "1"
|
|
93
|
+
}
|
|
94
|
+
{ $_ -eq "--foreground" -or $_ -eq "--no-daemon" } {
|
|
95
|
+
$foreground = $true
|
|
96
|
+
}
|
|
97
|
+
{ $_ -eq "--background" -or $_ -eq "--daemon" } {
|
|
98
|
+
$forceBackground = $true
|
|
99
|
+
}
|
|
100
|
+
default {
|
|
101
|
+
Write-JsonError "Unknown argument: $($args[$i])"
|
|
102
|
+
exit 1
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ($urlHost -eq "") {
|
|
108
|
+
if ($bindHost -eq "127.0.0.1" -or $bindHost -eq "localhost") {
|
|
109
|
+
$urlHost = "localhost"
|
|
110
|
+
} else {
|
|
111
|
+
$urlHost = $bindHost
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ($idleTimeoutMinutes -ne "") {
|
|
116
|
+
$parsedIdle = 0
|
|
117
|
+
if ((-not [int]::TryParse($idleTimeoutMinutes, [ref]$parsedIdle)) -or $parsedIdle -lt 1) {
|
|
118
|
+
Write-JsonError "--idle-timeout-minutes must be a positive integer"
|
|
119
|
+
exit 1
|
|
120
|
+
}
|
|
121
|
+
$env:BRAINSTORM_IDLE_TIMEOUT_MS = [string]($parsedIdle * 60 * 1000)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ($env:CODEX_CI -and -not $foreground -and -not $forceBackground) {
|
|
125
|
+
$foreground = $true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
$sessionId = "$PID-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
|
|
129
|
+
$brainstormRoot = ""
|
|
130
|
+
if ($projectDir -ne "") {
|
|
131
|
+
$brainstormRoot = Join-Path $projectDir ".superpowers/brainstorm"
|
|
132
|
+
$sessionDir = Join-Path $brainstormRoot $sessionId
|
|
133
|
+
$env:BRAINSTORM_PORT_FILE = Join-Path $brainstormRoot ".last-port"
|
|
134
|
+
$env:BRAINSTORM_TOKEN_FILE = Join-Path $brainstormRoot ".last-token"
|
|
135
|
+
} else {
|
|
136
|
+
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
$stateDir = Join-Path $sessionDir "state"
|
|
140
|
+
$contentDir = Join-Path $sessionDir "content"
|
|
141
|
+
$pidFile = Join-Path $stateDir "server.pid"
|
|
142
|
+
$logFile = Join-Path $stateDir "server.log"
|
|
143
|
+
$serverIdFile = Join-Path $stateDir "server-instance-id"
|
|
144
|
+
|
|
145
|
+
New-Item -ItemType Directory -Force -Path $contentDir, $stateDir | Out-Null
|
|
146
|
+
if ($brainstormRoot -ne "") {
|
|
147
|
+
Protect-PathForCurrentUser -Path $brainstormRoot
|
|
148
|
+
}
|
|
149
|
+
Protect-PathForCurrentUser -Path $sessionDir
|
|
150
|
+
Protect-PathForCurrentUser -Path $contentDir
|
|
151
|
+
Protect-PathForCurrentUser -Path $stateDir
|
|
152
|
+
|
|
153
|
+
$serverId = New-ServerId
|
|
154
|
+
Set-Content -Path $serverIdFile -Value $serverId -NoNewline -Encoding utf8
|
|
155
|
+
Protect-PathForCurrentUser -Path $serverIdFile
|
|
156
|
+
|
|
157
|
+
if (Test-Path -LiteralPath $pidFile) {
|
|
158
|
+
$oldPid = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
159
|
+
if ($oldPid) {
|
|
160
|
+
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
|
|
161
|
+
}
|
|
162
|
+
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$envValues = @{
|
|
166
|
+
BRAINSTORM_DIR = $sessionDir
|
|
167
|
+
BRAINSTORM_HOST = $bindHost
|
|
168
|
+
BRAINSTORM_URL_HOST = $urlHost
|
|
169
|
+
BRAINSTORM_OWNER_PID = ""
|
|
170
|
+
BRAINSTORM_PORT_FILE = $env:BRAINSTORM_PORT_FILE
|
|
171
|
+
BRAINSTORM_TOKEN_FILE = $env:BRAINSTORM_TOKEN_FILE
|
|
172
|
+
BRAINSTORM_IDLE_TIMEOUT_MS = $env:BRAINSTORM_IDLE_TIMEOUT_MS
|
|
173
|
+
BRAINSTORM_OPEN = $env:BRAINSTORM_OPEN
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ($foreground) {
|
|
177
|
+
foreach ($key in $envValues.Keys) {
|
|
178
|
+
Set-Item -Path "Env:$key" -Value ([string]$envValues[$key])
|
|
179
|
+
}
|
|
180
|
+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
181
|
+
$psi.FileName = "node"
|
|
182
|
+
$psi.WorkingDirectory = $scriptDir
|
|
183
|
+
$psi.UseShellExecute = $false
|
|
184
|
+
$psi.RedirectStandardOutput = $false
|
|
185
|
+
$psi.RedirectStandardError = $false
|
|
186
|
+
$psi.Arguments = "server.cjs --brainstorm-server-id=$serverId"
|
|
187
|
+
$process = [System.Diagnostics.Process]::new()
|
|
188
|
+
$process.StartInfo = $psi
|
|
189
|
+
$null = $process.Start()
|
|
190
|
+
Set-Content -Path $pidFile -Value ([string]$process.Id) -NoNewline -Encoding ascii
|
|
191
|
+
Protect-PathForCurrentUser -Path $pidFile
|
|
192
|
+
try {
|
|
193
|
+
$process.WaitForExit()
|
|
194
|
+
exit $process.ExitCode
|
|
195
|
+
} finally {
|
|
196
|
+
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Set-ProcessEnvironment -Values $envValues
|
|
201
|
+
$errFile = Join-Path $stateDir "server.err"
|
|
202
|
+
$process = Start-Process `
|
|
203
|
+
-FilePath "node" `
|
|
204
|
+
-ArgumentList @("server.cjs", "--brainstorm-server-id=$serverId") `
|
|
205
|
+
-WorkingDirectory $scriptDir `
|
|
206
|
+
-RedirectStandardOutput $logFile `
|
|
207
|
+
-RedirectStandardError $errFile `
|
|
208
|
+
-PassThru
|
|
209
|
+
Set-Content -Path $pidFile -Value ([string]$process.Id) -NoNewline -Encoding ascii
|
|
210
|
+
Protect-PathForCurrentUser -Path $pidFile
|
|
211
|
+
|
|
212
|
+
$deadline = (Get-Date).AddSeconds(5)
|
|
213
|
+
$reported = $false
|
|
214
|
+
while ((Get-Date) -lt $deadline) {
|
|
215
|
+
if ($process.HasExited) {
|
|
216
|
+
Write-JsonError "Server exited before startup"
|
|
217
|
+
exit 1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Start-Sleep -Milliseconds 100
|
|
221
|
+
$infoFile = Join-Path $stateDir "server-info"
|
|
222
|
+
if (Test-Path -LiteralPath $infoFile) {
|
|
223
|
+
Get-Content -LiteralPath $infoFile | Select-Object -First 1
|
|
224
|
+
$reported = $true
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (-not $reported) {
|
|
230
|
+
Write-JsonError "Server failed to start within 5 seconds"
|
|
231
|
+
exit 1
|
|
232
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Stop the brainstorm server and clean up.
|
|
3
|
+
# Usage: ./stop-server.ps1 <session_dir>
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = "Stop"
|
|
6
|
+
|
|
7
|
+
if ($args.Count -ne 1) {
|
|
8
|
+
[pscustomobject]@{ error = "Usage: stop-server.ps1 <session_dir>" } | ConvertTo-Json -Compress
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
$sessionDir = $args[0]
|
|
13
|
+
$stateDir = Join-Path $sessionDir "state"
|
|
14
|
+
$pidFile = Join-Path $stateDir "server.pid"
|
|
15
|
+
$serverIdFile = Join-Path $stateDir "server-instance-id"
|
|
16
|
+
|
|
17
|
+
function Mark-Stopped {
|
|
18
|
+
param([string]$Reason)
|
|
19
|
+
Remove-Item -LiteralPath (Join-Path $stateDir "server-info") -Force -ErrorAction SilentlyContinue
|
|
20
|
+
[pscustomobject]@{
|
|
21
|
+
reason = $Reason
|
|
22
|
+
timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
23
|
+
} | ConvertTo-Json -Compress | Set-Content -Path (Join-Path $stateDir "server-stopped") -Encoding utf8
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Read-ExpectedServerId {
|
|
27
|
+
if (-not (Test-Path -LiteralPath $serverIdFile)) { return $null }
|
|
28
|
+
$id = (Get-Content -LiteralPath $serverIdFile -Raw).Trim()
|
|
29
|
+
if ($id -match '^[A-Za-z0-9_-]{32,64}$') { return $id }
|
|
30
|
+
return $null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Test-BrainstormServer {
|
|
34
|
+
param([int]$ProcessId)
|
|
35
|
+
$expected = Read-ExpectedServerId
|
|
36
|
+
if (-not $expected) { return $false }
|
|
37
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
38
|
+
if (-not $process) { return $false }
|
|
39
|
+
return ($process.CommandLine -like "*--brainstorm-server-id=$expected*")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Test-Path -LiteralPath $pidFile) {
|
|
43
|
+
$pidText = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
44
|
+
$serverPid = 0
|
|
45
|
+
if ((-not [int]::TryParse($pidText, [ref]$serverPid)) -or -not (Test-BrainstormServer -ProcessId $serverPid)) {
|
|
46
|
+
Remove-Item -LiteralPath $pidFile, $serverIdFile -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Mark-Stopped "stale_pid"
|
|
48
|
+
[pscustomobject]@{ status = "stale_pid" } | ConvertTo-Json -Compress
|
|
49
|
+
exit 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Stop-Process -Id $serverPid -ErrorAction SilentlyContinue
|
|
53
|
+
$deadline = (Get-Date).AddSeconds(2)
|
|
54
|
+
while ((Get-Date) -lt $deadline -and (Get-Process -Id $serverPid -ErrorAction SilentlyContinue)) {
|
|
55
|
+
Start-Sleep -Milliseconds 100
|
|
56
|
+
}
|
|
57
|
+
if (Get-Process -Id $serverPid -ErrorAction SilentlyContinue) {
|
|
58
|
+
Stop-Process -Id $serverPid -Force -ErrorAction SilentlyContinue
|
|
59
|
+
Start-Sleep -Milliseconds 100
|
|
60
|
+
}
|
|
61
|
+
if (Get-Process -Id $serverPid -ErrorAction SilentlyContinue) {
|
|
62
|
+
[pscustomobject]@{ status = "failed"; error = "process still running" } | ConvertTo-Json -Compress
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Remove-Item -LiteralPath $pidFile, $serverIdFile, (Join-Path $stateDir "server.log") -Force -ErrorAction SilentlyContinue
|
|
67
|
+
Mark-Stopped "stop-server.ps1"
|
|
68
|
+
|
|
69
|
+
$tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
|
70
|
+
if (-not $tempRoot.EndsWith([string][System.IO.Path]::DirectorySeparatorChar)) {
|
|
71
|
+
$tempRoot += [System.IO.Path]::DirectorySeparatorChar
|
|
72
|
+
}
|
|
73
|
+
$resolvedSession = (Resolve-Path -LiteralPath $sessionDir -ErrorAction SilentlyContinue).Path
|
|
74
|
+
if ($resolvedSession) {
|
|
75
|
+
$resolvedSession = [System.IO.Path]::GetFullPath($resolvedSession)
|
|
76
|
+
}
|
|
77
|
+
if ($resolvedSession -and $resolvedSession.StartsWith($tempRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
78
|
+
Remove-Item -LiteralPath $sessionDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[pscustomobject]@{ status = "stopped" } | ConvertTo-Json -Compress
|
|
82
|
+
} else {
|
|
83
|
+
[pscustomobject]@{ status = "not_running" } | ConvertTo-Json -Compress
|
|
84
|
+
}
|
|
@@ -37,6 +37,9 @@ The server watches a directory for HTML files and serves the newest one to the b
|
|
|
37
37
|
# the first screen; --project-dir persists mockups and enables same-port restart.
|
|
38
38
|
scripts/start-server.sh --project-dir /path/to/project --open
|
|
39
39
|
|
|
40
|
+
# Windows PowerShell:
|
|
41
|
+
scripts/start-server.ps1 --project-dir C:\path\to\project --open
|
|
42
|
+
|
|
40
43
|
# Returns: {"type":"server-started","port":52341,
|
|
41
44
|
# "url":"http://localhost:52341/?key=ab12…",
|
|
42
45
|
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
|
|
@@ -67,6 +70,8 @@ scripts/start-server.sh --project-dir /path/to/project --open
|
|
|
67
70
|
|
|
68
71
|
On Windows, the script auto-detects and switches to foreground mode (which blocks the tool call). Use `run_in_background: true` on the Bash tool call so the server survives across conversation turns, then read `$STATE_DIR/server-info` on the next turn to get the URL and port.
|
|
69
72
|
|
|
73
|
+
For native PowerShell, use `scripts/start-server.ps1` with the same flags. It writes the same `server-info` file and supports `--project-dir`, `--host`, `--url-host`, `--idle-timeout-minutes`, `--open`, `--foreground`, and `--background`.
|
|
74
|
+
|
|
70
75
|
**Codex:**
|
|
71
76
|
```bash
|
|
72
77
|
# Codex reaps background processes. The script auto-detects CODEX_CI and
|
|
@@ -281,6 +286,9 @@ If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser
|
|
|
281
286
|
|
|
282
287
|
```bash
|
|
283
288
|
scripts/stop-server.sh $SESSION_DIR
|
|
289
|
+
|
|
290
|
+
# Windows PowerShell:
|
|
291
|
+
scripts/stop-server.ps1 $SESSION_DIR
|
|
284
292
|
```
|
|
285
293
|
|
|
286
294
|
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
|
|
@@ -44,6 +44,9 @@ Stop. Don't proceed to Step 2.
|
|
|
44
44
|
```bash
|
|
45
45
|
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
|
46
46
|
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
|
47
|
+
# Capture now, while still inside the workspace — Step 5 changes directory
|
|
48
|
+
# before cleanup (Step 6) needs this value
|
|
49
|
+
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
This determines which menu to show and how cleanup works:
|
|
@@ -123,6 +126,8 @@ git branch -d <feature-branch>
|
|
|
123
126
|
```bash
|
|
124
127
|
# Push branch
|
|
125
128
|
git push -u origin <feature-branch>
|
|
129
|
+
# From a detached HEAD, name the new branch on the remote:
|
|
130
|
+
# git push origin HEAD:refs/heads/<new-branch>
|
|
126
131
|
```
|
|
127
132
|
|
|
128
133
|
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
|
@@ -160,21 +165,17 @@ git branch -D <feature-branch>
|
|
|
160
165
|
|
|
161
166
|
### Step 6: Cleanup Workspace
|
|
162
167
|
|
|
163
|
-
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
|
168
|
-
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
|
169
|
-
```
|
|
168
|
+
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree. Both callers have already changed directory to the
|
|
169
|
+
main repo root — worktree removal must run from outside the worktree —
|
|
170
|
+
and use the `GIT_DIR`/`GIT_COMMON`/`WORKTREE_PATH` values captured in
|
|
171
|
+
Step 2, from before that directory change.
|
|
170
172
|
|
|
171
173
|
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
|
|
172
174
|
|
|
173
|
-
**If
|
|
175
|
+
**If `WORKTREE_PATH` is under `.worktrees/` or `worktrees/`:** Superpowers
|
|
176
|
+
created this worktree — we own cleanup:
|
|
174
177
|
|
|
175
178
|
```bash
|
|
176
|
-
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
|
177
|
-
cd "$MAIN_ROOT"
|
|
178
179
|
git worktree remove "$WORKTREE_PATH"
|
|
179
180
|
git worktree prune # Self-healing: clean up any stale registrations
|
|
180
181
|
```
|
|
@@ -133,7 +133,7 @@ that implementer. Single-file mechanical fixes also take the cheapest tier.
|
|
|
133
133
|
|
|
134
134
|
Implementer subagents report one of four statuses. Handle each appropriately:
|
|
135
135
|
|
|
136
|
-
**DONE:** Generate the review package (`scripts/review-package BASE HEAD`, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.
|
|
136
|
+
**DONE:** Generate the review package (`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.
|
|
137
137
|
|
|
138
138
|
**DONE_WITH_CONCERNS:** The implementer completed the work but flagged doubts. Read the concerns before proceeding. If the concerns are about correctness or scope, address them before review. If they're observations (e.g., "this file is getting large"), note them and proceed to review.
|
|
139
139
|
|
|
@@ -179,7 +179,7 @@ final whole-branch review. When you fill a reviewer template:
|
|
|
179
179
|
test hygiene, review method) — the constraints block is for what THIS
|
|
180
180
|
project's spec demands.
|
|
181
181
|
- Hand the reviewer its diff as a file: run this skill's
|
|
182
|
-
`scripts/review-package BASE HEAD` and pass the reviewer the file path
|
|
182
|
+
`scripts/review-package BASE HEAD` (or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell) and pass the reviewer the file path
|
|
183
183
|
it prints (or, without bash: `git log --oneline`, `git diff --stat`,
|
|
184
184
|
and `git diff -U10` for the range, redirected to one uniquely named
|
|
185
185
|
file). The output never enters your own context, and the reviewer sees
|
|
@@ -194,14 +194,14 @@ final whole-branch review. When you fill a reviewer template:
|
|
|
194
194
|
- Dispatch fix subagents for Critical and Important findings. Record Minor
|
|
195
195
|
findings in the progress ledger as you go, and point the final
|
|
196
196
|
whole-branch review at that list so it can triage which must be fixed
|
|
197
|
-
before merge.
|
|
197
|
+
before merge. A roll-up nobody reads is a silent discard.
|
|
198
198
|
- A finding labeled plan-mandated — or any finding that conflicts with
|
|
199
199
|
what the plan's text requires — is the human's decision, like any plan
|
|
200
200
|
contradiction: present the finding and the plan text, ask which governs.
|
|
201
201
|
Do not dismiss the finding because the plan mandates it, and do not
|
|
202
202
|
dispatch a fix that contradicts the plan without asking.
|
|
203
203
|
- The final whole-branch review gets a package too: run
|
|
204
|
-
`scripts/review-package MERGE_BASE HEAD` (MERGE_BASE = the commit the
|
|
204
|
+
`scripts/review-package MERGE_BASE HEAD` (or `scripts/review-package.ps1 MERGE_BASE HEAD` on Windows PowerShell; MERGE_BASE = the commit the
|
|
205
205
|
branch started from, e.g. `git merge-base main HEAD`) and include the
|
|
206
206
|
printed path in the final review dispatch, so the final reviewer reads
|
|
207
207
|
one file instead of re-deriving the branch diff with git commands.
|
|
@@ -223,7 +223,7 @@ prints back — stays resident in your context for the rest of the session
|
|
|
223
223
|
and is re-read on every later turn. Hand artifacts over as files:
|
|
224
224
|
|
|
225
225
|
- **Task brief:** before dispatching an implementer, run this skill's
|
|
226
|
-
`scripts/task-brief PLAN_FILE N` — it extracts the task's full text to a
|
|
226
|
+
`scripts/task-brief PLAN_FILE N` (or `scripts/task-brief.ps1 PLAN_FILE N` on Windows PowerShell) — it extracts the task's full text to a
|
|
227
227
|
uniquely named file and prints the path. Compose the dispatch so the
|
|
228
228
|
brief stays the single source of requirements. Your dispatch should
|
|
229
229
|
contain: (1) one line on where this task fits in the project; (2) the
|
|
@@ -372,7 +372,7 @@ Done!
|
|
|
372
372
|
- Proceed with unfixed issues
|
|
373
373
|
- Dispatch multiple implementation subagents in parallel (conflicts)
|
|
374
374
|
- Make a subagent read the whole plan file (hand it its task brief —
|
|
375
|
-
`scripts/task-brief` — instead)
|
|
375
|
+
`scripts/task-brief` / `scripts/task-brief.ps1` — instead)
|
|
376
376
|
- Skip scene-setting context (subagent needs to understand where task fits)
|
|
377
377
|
- Ignore subagent questions (answer before letting them proceed)
|
|
378
378
|
- Accept "close enough" on spec compliance (reviewer found spec issues = not done)
|
|
@@ -382,7 +382,7 @@ Done!
|
|
|
382
382
|
dispatch prompt ("treat it as Minor at most") — the plan's example code is
|
|
383
383
|
a starting point, not evidence that its weaknesses were chosen
|
|
384
384
|
- Dispatch a task reviewer without a diff file — generate it first
|
|
385
|
-
(`scripts/review-package BASE HEAD`) and name the printed path in the
|
|
385
|
+
(`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell) and name the printed path in the
|
|
386
386
|
prompt
|
|
387
387
|
- Move to next task while the review has open Critical/Important issues
|
|
388
388
|
- Re-dispatch a task the progress ledger already marks complete — check
|
|
@@ -23,7 +23,7 @@ git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&
|
|
|
23
23
|
if [ $# -eq 3 ]; then
|
|
24
24
|
out=$3
|
|
25
25
|
else
|
|
26
|
-
dir=$("$(dirname "$0")"/sdd-workspace)
|
|
26
|
+
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
|
|
27
27
|
out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff"
|
|
28
28
|
fi
|
|
29
29
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Generate a review package: commit list, stat summary, and net diff.
|
|
3
|
+
# Usage: ./review-package.ps1 BASE HEAD [OUTFILE]
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = "Stop"
|
|
6
|
+
|
|
7
|
+
if ($args.Count -lt 2 -or $args.Count -gt 3) {
|
|
8
|
+
Write-Error "usage: review-package.ps1 BASE HEAD [OUTFILE]"
|
|
9
|
+
exit 2
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
$base = $args[0]
|
|
13
|
+
$head = $args[1]
|
|
14
|
+
|
|
15
|
+
& git rev-parse --verify --quiet $base *> $null
|
|
16
|
+
if ($LASTEXITCODE -ne 0) {
|
|
17
|
+
Write-Error "bad BASE: $base"
|
|
18
|
+
exit 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
& git rev-parse --verify --quiet $head *> $null
|
|
22
|
+
if ($LASTEXITCODE -ne 0) {
|
|
23
|
+
Write-Error "bad HEAD: $head"
|
|
24
|
+
exit 2
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ($args.Count -eq 3) {
|
|
28
|
+
$out = $args[2]
|
|
29
|
+
} else {
|
|
30
|
+
$scriptDir = Split-Path -Parent $PSCommandPath
|
|
31
|
+
$dir = (& (Join-Path $scriptDir "sdd-workspace.ps1")).Trim()
|
|
32
|
+
$baseShort = (& git rev-parse --short $base).Trim()
|
|
33
|
+
$headShort = (& git rev-parse --short $head).Trim()
|
|
34
|
+
$out = Join-Path $dir "review-$baseShort..$headShort.diff"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$content = New-Object System.Collections.Generic.List[string]
|
|
38
|
+
$content.Add("# Review package: ${base}..${head}")
|
|
39
|
+
$content.Add("")
|
|
40
|
+
$content.Add("## Commits")
|
|
41
|
+
(& git log --oneline "${base}..${head}") | ForEach-Object { $content.Add($_) }
|
|
42
|
+
$content.Add("")
|
|
43
|
+
$content.Add("## Files changed")
|
|
44
|
+
(& git diff --stat "${base}..${head}") | ForEach-Object { $content.Add($_) }
|
|
45
|
+
$content.Add("")
|
|
46
|
+
$content.Add("## Diff")
|
|
47
|
+
(& git diff -U10 "${base}..${head}") | ForEach-Object { $content.Add($_) }
|
|
48
|
+
|
|
49
|
+
Set-Content -Path $out -Value $content -Encoding utf8
|
|
50
|
+
$commits = (& git rev-list --count "${base}..${head}").Trim()
|
|
51
|
+
$bytes = (Get-Item -LiteralPath $out).Length
|
|
52
|
+
Write-Output "wrote ${out}: $commits commit(s), $bytes bytes"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Resolve and ensure the working-tree directory SDD uses for short-lived artifacts.
|
|
3
|
+
# Usage: ./sdd-workspace.ps1
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = "Stop"
|
|
6
|
+
|
|
7
|
+
$root = (& git rev-parse --show-toplevel).Trim()
|
|
8
|
+
$dir = Join-Path $root ".superpowers/sdd"
|
|
9
|
+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
10
|
+
Set-Content -Path (Join-Path $dir ".gitignore") -Value "*" -NoNewline -Encoding ascii
|
|
11
|
+
(Resolve-Path $dir).Path
|