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/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.1",
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, '&lt;')
238
+ .replace(/>/g, '&gt;')
239
+ .replace(/"/g, '&quot;');
240
+ }
241
+
230
242
  function brandMarkup() {
231
- const escHtml = (v) => String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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 qIdx = req.url.indexOf('?');
379
- const pathname = qIdx >= 0 ? req.url.slice(0, qIdx) : req.url;
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 as given (this env var is trusted operator input).
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.exec(process.env.BRAINSTORM_OPEN_CMD + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ }
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
- ```bash
166
- GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
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 worktree path is under `.worktrees/` or `worktrees/`:** Superpowers created this worktree — we own cleanup.
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