superpowers-mcp 6.0.0 → 6.0.2

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.0",
5
+ "version": "6.0.2",
6
6
  "publisher": "superpowers",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -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,12 +398,11 @@ 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
- res.end(bootstrapPage(keyFromQuery));
405
+ res.end(bootstrapPage(TOKEN));
384
406
  } else if (req.method === 'GET' && pathname === '/') {
385
407
  const screenFile = getNewestScreen();
386
408
  let html = screenFile
@@ -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.
@@ -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
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env pwsh
2
+ # Extract one task's full text from an implementation plan.
3
+ # Usage: ./task-brief.ps1 PLAN_FILE TASK_NUMBER [OUTFILE]
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ if ($args.Count -lt 2 -or $args.Count -gt 3) {
8
+ Write-Error "usage: task-brief.ps1 PLAN_FILE TASK_NUMBER [OUTFILE]"
9
+ exit 2
10
+ }
11
+
12
+ $plan = $args[0]
13
+ $taskNumber = $args[1]
14
+ if (-not (Test-Path -LiteralPath $plan -PathType Leaf)) {
15
+ Write-Error "no such plan file: $plan"
16
+ exit 2
17
+ }
18
+
19
+ if ($args.Count -eq 3) {
20
+ $out = $args[2]
21
+ } else {
22
+ $scriptDir = Split-Path -Parent $PSCommandPath
23
+ $dir = (& (Join-Path $scriptDir "sdd-workspace.ps1")).Trim()
24
+ $out = Join-Path $dir "task-$taskNumber-brief.md"
25
+ }
26
+
27
+ $inFence = $false
28
+ $inTask = $false
29
+ $pattern = "^#+[ \t]+Task[ \t]+$([regex]::Escape($taskNumber))([^0-9]|$)"
30
+ $selected = New-Object System.Collections.Generic.List[string]
31
+
32
+ foreach ($line in [System.IO.File]::ReadLines((Resolve-Path -LiteralPath $plan).Path)) {
33
+ if ($line -match '^```') {
34
+ $inFence = -not $inFence
35
+ }
36
+ if (-not $inFence -and $line -match '^#+[ \t]+Task[ \t]+[0-9]+') {
37
+ $inTask = ($line -match $pattern)
38
+ }
39
+ if ($inTask) {
40
+ $selected.Add($line)
41
+ }
42
+ }
43
+
44
+ Set-Content -Path $out -Value $selected -Encoding utf8
45
+ if ((-not (Test-Path -LiteralPath $out)) -or ((Get-Item -LiteralPath $out).Length -eq 0)) {
46
+ Write-Error "task $taskNumber not found in $plan (no heading matching 'Task $taskNumber')"
47
+ exit 3
48
+ }
49
+
50
+ $lineCount = ($selected | Measure-Object).Count
51
+ Write-Output "wrote ${out}: $lineCount lines"
@@ -167,7 +167,8 @@ Subagent (general-purpose):
167
167
 
168
168
  **Placeholders:**
169
169
  - `[MODEL]` — REQUIRED: reviewer model per SKILL.md Model Selection
170
- - `[BRIEF_FILE]` — REQUIRED: the task brief file (`scripts/task-brief PLAN N`
170
+ - `[BRIEF_FILE]` — REQUIRED: the task brief file (`scripts/task-brief PLAN N`,
171
+ or `scripts/task-brief.ps1 PLAN N` on Windows PowerShell,
171
172
  prints the path; same file the implementer worked from)
172
173
  - `[GLOBAL_CONSTRAINTS]` — the binding requirements copied verbatim from
173
174
  the plan's Global Constraints section or the spec: exact values, formats,
@@ -178,7 +179,7 @@ Subagent (general-purpose):
178
179
  - `[BASE_SHA]` — commit before this task
179
180
  - `[HEAD_SHA]` — current commit
180
181
  - `[DIFF_FILE]` — REQUIRED: the path the controller wrote the review
181
- package to (`scripts/review-package BASE HEAD` prints the unique path it
182
+ package to (`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell, prints the unique path it
182
183
  wrote; the package never enters the controller's context)
183
184
 
184
185
  **Reviewer returns:** Spec Compliance verdict (✅/❌/⚠️), Strengths, Issues