Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hooks/hooks-cursor.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"hooks": {
"sessionStart": [
{
"command": "./hooks/session-start"
"command": "./hooks/run-hook.cmd session-start"
}
]
}
Expand Down
50 changes: 33 additions & 17 deletions skills/brainstorming/scripts/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ function decodeFrame(buffer) {
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
const STATE_DIR = path.join(SESSION_DIR, 'state');
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;

const MIME_TYPES = {
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
Expand Down Expand Up @@ -112,10 +114,10 @@ function wrapInFrame(content) {
}

function getNewestScreen() {
const files = fs.readdirSync(SCREEN_DIR)
const files = fs.readdirSync(CONTENT_DIR)
.filter(f => f.endsWith('.html'))
.map(f => {
const fp = path.join(SCREEN_DIR, f);
const fp = path.join(CONTENT_DIR, f);
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
Expand All @@ -142,7 +144,7 @@ function handleRequest(req, res) {
res.end(html);
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
const fileName = req.url.slice(7);
const filePath = path.join(SCREEN_DIR, path.basename(fileName));
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not found');
Expand Down Expand Up @@ -230,7 +232,7 @@ function handleMessage(text) {
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
const eventsFile = path.join(STATE_DIR, 'events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
Expand Down Expand Up @@ -258,32 +260,33 @@ const debounceTimers = new Map();
// ========== Server Startup ==========

function startServer() {
if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true });
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });

// Track known files to distinguish new screens from updates.
// macOS fs.watch reports 'rename' for both new files and overwrites,
// so we can't rely on eventType alone.
const knownFiles = new Set(
fs.readdirSync(SCREEN_DIR).filter(f => f.endsWith('.html'))
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
);

const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);

const watcher = fs.watch(SCREEN_DIR, (eventType, filename) => {
const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
if (!filename || !filename.endsWith('.html')) return;

if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
debounceTimers.set(filename, setTimeout(() => {
debounceTimers.delete(filename);
const filePath = path.join(SCREEN_DIR, filename);
const filePath = path.join(CONTENT_DIR, filename);

if (!fs.existsSync(filePath)) return; // file was deleted
touchActivity();

if (!knownFiles.has(filename)) {
knownFiles.add(filename);
const eventsFile = path.join(SCREEN_DIR, '.events');
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
} else {
Expand All @@ -297,10 +300,10 @@ function startServer() {

function shutdown(reason) {
console.log(JSON.stringify({ type: 'server-stopped', reason }));
const infoFile = path.join(SCREEN_DIR, '.server-info');
const infoFile = path.join(STATE_DIR, 'server-info');
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
fs.writeFileSync(
path.join(SCREEN_DIR, '.server-stopped'),
path.join(STATE_DIR, 'server-stopped'),
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
);
watcher.close();
Expand All @@ -309,8 +312,8 @@ function startServer() {
}

function ownerAlive() {
if (!OWNER_PID) return true;
try { process.kill(OWNER_PID, 0); return true; } catch (e) { return false; }
if (!ownerPid) return true;
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
}

// Check every 60s: exit if owner process died or idle for 30 minutes
Expand All @@ -320,14 +323,27 @@ function startServer() {
}, 60 * 1000);
lifecycleCheck.unref();

// Validate owner PID at startup. If it's already dead, the PID resolution
// was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
// Disable monitoring and rely on the idle timeout instead.
if (ownerPid) {
try { process.kill(ownerPid, 0); }
catch (e) {
if (e.code !== 'EPERM') {
console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
ownerPid = null;
}
}
}

server.listen(PORT, HOST, () => {
const info = JSON.stringify({
type: 'server-started', port: Number(PORT), host: HOST,
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
screen_dir: SCREEN_DIR
screen_dir: CONTENT_DIR, state_dir: STATE_DIR
});
console.log(info);
fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n');
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
});
}

Expand Down
23 changes: 9 additions & 14 deletions skills/brainstorming/scripts/start-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,17 @@ fi
SESSION_ID="$$-$(date +%s)"

if [[ -n "$PROJECT_DIR" ]]; then
SCREEN_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
fi

PID_FILE="${SCREEN_DIR}/.server.pid"
LOG_FILE="${SCREEN_DIR}/.server.log"
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
LOG_FILE="${STATE_DIR}/server.log"

# Create fresh session directory
mkdir -p "$SCREEN_DIR"
# Create fresh session directory with content and state peers
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"

# Kill any existing server
if [[ -f "$PID_FILE" ]]; then
Expand All @@ -106,22 +107,16 @@ if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
OWNER_PID="$PPID"
fi

# On Windows/MSYS2, the MSYS2 PID namespace is invisible to Node.js.
# Skip owner-PID monitoring — the 30-minute idle timeout prevents orphans.
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) OWNER_PID="" ;;
esac

# Foreground mode for environments that reap detached/background processes.
if [[ "$FOREGROUND" == "true" ]]; then
echo "$$" > "$PID_FILE"
env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
exit $?
fi

# Start server, capturing output to log file
# Use nohup to survive shell exit; disown to remove from job table
nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
disown "$SERVER_PID" 2>/dev/null
echo "$SERVER_PID" > "$PID_FILE"
Expand Down
17 changes: 9 additions & 8 deletions skills/brainstorming/scripts/stop-server.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
#!/usr/bin/env bash
# Stop the brainstorm server and clean up
# Usage: stop-server.sh <screen_dir>
# Usage: stop-server.sh <session_dir>
#
# Kills the server process. Only deletes session directory if it's
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
# kept so mockups can be reviewed later.

SCREEN_DIR="$1"
SESSION_DIR="$1"

if [[ -z "$SCREEN_DIR" ]]; then
echo '{"error": "Usage: stop-server.sh <screen_dir>"}'
if [[ -z "$SESSION_DIR" ]]; then
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
exit 1
fi

PID_FILE="${SCREEN_DIR}/.server.pid"
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"

if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
Expand Down Expand Up @@ -42,11 +43,11 @@ if [[ -f "$PID_FILE" ]]; then
exit 1
fi

rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log"
rm -f "$PID_FILE" "${STATE_DIR}/server.log"

# Only delete ephemeral /tmp directories
if [[ "$SCREEN_DIR" == /tmp/* ]]; then
rm -rf "$SCREEN_DIR"
if [[ "$SESSION_DIR" == /tmp/* ]]; then
rm -rf "$SESSION_DIR"
fi

echo '{"status": "stopped"}'
Expand Down
18 changes: 8 additions & 10 deletions skills/requesting-code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: 完成任务、实现重要功能或合并前使用,用于验证

# 请求代码审查

派遣 superpowers:code-reviewer 子代理来在问题扩散之前发现它们。审查者获得的是精心组织的评估上下文——绝不是你的会话历史。这样可以让审查者专注于工作成果而非你的思考过程,同时保留你自己的上下文以便继续工作。
派遣代码审查子代理,在问题扩散之前发现它们。审查者获得的是精心组织的评估上下文——绝不是你的会话历史。这样可以让审查者专注于工作成果而非你的思考过程,同时保留你自己的上下文以便继续工作。

**核心原则:** 早审查,勤审查。

Expand All @@ -29,16 +29,15 @@ BASE_SHA=$(git rev-parse HEAD~1) # 或 origin/main
HEAD_SHA=$(git rev-parse HEAD)
```

**2. 派遣 code-reviewer 子代理:**
**2. 派遣代码审查子代理:**

使用 Task 工具,指定 superpowers:code-reviewer 类型,填写 `code-reviewer.md` 中的模板
使用 Task 工具,指定 `general-purpose` 类型,填写 `code-reviewer.md` 中的模板

**占位符说明:**
- `{WHAT_WAS_IMPLEMENTED}` - 你刚完成的内容
- `{DESCRIPTION}` - 你刚完成的内容简要说明
- `{PLAN_OR_REQUIREMENTS}` - 预期功能
- `{BASE_SHA}` - 起始提交
- `{HEAD_SHA}` - 结束提交
- `{DESCRIPTION}` - 简要说明

**3. 处理反馈:**
- Critical 问题立即修复
Expand All @@ -56,12 +55,11 @@ HEAD_SHA=$(git rev-parse HEAD)
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
HEAD_SHA=$(git rev-parse HEAD)

[派遣 superpowers:code-reviewer 子代理]
WHAT_WAS_IMPLEMENTED: 会话索引的验证和修复功能
[派遣代码审查子代理]
DESCRIPTION: 添加了 verifyIndex() 和 repairIndex(),支持 4 种问题类型
PLAN_OR_REQUIREMENTS: docs/superpowers/plans/deployment-plan.md 中的任务 2
BASE_SHA: a7981ec
HEAD_SHA: 3df7661
DESCRIPTION: 添加了 verifyIndex() 和 repairIndex(),支持 4 种问题类型

[子代理返回]:
优点:架构清晰,测试真实
Expand All @@ -82,8 +80,8 @@ HEAD_SHA=$(git rev-parse HEAD)
- 修复后再进入下一个任务

**执行计划:**
- 每批(3 个任务)后审查
- 获取反馈,修复,继续
- 每个任务完成后或在自然 checkpoint 审查
- 获取反馈,应用,继续

**临时开发:**
- 合并前审查
Expand Down
Loading
Loading