
Superpowers Brainstorming
- 1 installs
- Updated July 16, 2026
- afk101/gd-claude-code-plugin
superpowers-brainstorming is a Claude Code skill that turns an idea into approved spec and plan documents through a guided design conversation before implementation.
About
superpowers-brainstorming is a Claude Code skill that turns an idea into a design and implementation plan through collaborative conversation. It explores project context, asks clarifying questions one at a time, proposes two or three approaches, and produces spec, findings and plan documents at fixed paths before any code is written. A developer uses it at the start of a feature to scope and design before implementing. Documentation is written in Chinese.
- Turns an idea into approved design specs and implementation plans through guided one-question-at-a-time dialogue
- Writes spec, findings and plan docs to fixed docs/superpowers/ paths and commits them together
- Hard-gates against any implementation until the user approves the design
Superpowers Brainstorming by the numbers
- 1 all-time installs (skills.sh)
- Ranked #2,476 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 17, 2026 (Skillselion catalog sync)
superpowers-brainstorming capabilities & compatibility
- Capabilities
- brainstorming · design spec · implementation planning · requirements gathering
- Use cases
- planning · project management · documentation
What superpowers-brainstorming says it does
通过协作对话探索需求、设计方案,输出 finding、spec、plan 文档。
在呈现设计并获得用户批准之前,不要调用任何实现技能、编写任何代码、搭建任何项目或采取任何实现行动。
npx skills add https://github.com/afk101/gd-claude-code-plugin --skill superpowers-brainstormingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | July 16, 2026 |
| Repository | afk101/gd-claude-code-plugin ↗ |
What it does
Turn a rough feature idea into an approved design spec and implementation plan before writing code.
Who is it for?
Developers who want to scope and design a feature or fix collaboratively before writing any code.
Skip if: Tasks that are already fully specified and ready to implement, or pure debugging.
When should I use this skill?
You are starting a new feature or fix and need to explore requirements and produce a design spec and plan first.
What you get
A validated design plus committed spec, findings and plan documents ready for implementation.
- design spec document
- findings document
- implementation plan document
By the numbers
- 9-step checklist
- 3 output document types (spec, findings, plan)
- proposes 2-3 approaches per decision
Files
将创意头脑风暴转化为设计与实现计划
通过自然的协作对话,帮助将创意转化为完整的设计规范和实现计划。
首先理解当前项目上下文,然后逐一提问以完善创意。一旦理解了要构建的内容,呈现设计并获得用户批准,然后同步产出 spec 和 plan 两份文档。
两种调用场景
场景 A:新增功能 / 未经过 systematic-debugging(默认行为) 从零开始:探索上下文、澄清需求、产出 spec + plan,同时新建 findings 文件记录调研过程。
场景 B:经过 systematic-debugging 之后 根本原因已由 systematic-debugging 调查清楚,findings 文件已存在。此时 brainstorming 的职责是:
- 与用户讨论修复方案(2-3 种方案及权衡)
- 产出 spec 文档(修复目标、验收标准、回归测试要求)+ plan 文档
- 不新建 findings 文件,如有新发现追加到已有文件
如何判断当前场景: 若 docs/superpowers/findings/ 下已存在与本次任务相关的 findings 文件,则为场景 B。
<HARD-GATE> 在呈现设计并获得用户批准之前,不要调用任何实现技能、编写任何代码、搭建任何项目或采取任何实现行动。这适用于每个项目,无论看起来多么简单。 </HARD-GATE>
<HARD-GATE>
文件路径硬约束
所有输出文件必须严格遵循以下路径格式,禁止任何简化、缩写或自由发挥:
| 文件类型 | 路径格式 |
|---|---|
| Spec(设计文档) | docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md |
| Findings(发现文档) | docs/superpowers/findings/YYYY-MM-DD-<topic>-findings.md |
| Plan(实现计划) | docs/superpowers/plans/YYYY-MM-DD-<topic>.md |
路径各段说明:
docs/superpowers/— 固定前缀目录,不可省略specs//findings//plans/— 按文件类型分的子目录,不可省略YYYY-MM-DD-— 当日日期前缀(如2026-05-15-),不可省略<topic>— 主题的 kebab-case slug(如upgrade-by-tag),从任务主题自动生成-design.md/-findings.md— 固定后缀,不可更改(spec 文件后缀是-design.md不是-spec.md)
具体示例:
- 主题 "upgrade by tag" + 日期 2026-05-15 →
- Spec:
docs/superpowers/specs/2026-05-15-upgrade-by-tag-design.md - Findings:
docs/superpowers/findings/2026-05-15-upgrade-by-tag-findings.md - Plan:
docs/superpowers/plans/2026-05-15-upgrade-by-tag.md
写入前必须执行路径校验: 在写文件之前,显式列出完整路径,逐段核对是否符合上表格式。路径不合规则禁止写入,必须修正后重试。 </HARD-GATE>
反模式:"这太简单了不需要设计"
每个项目都必须经历这个过程。待办列表、单函数工具、配置修改——所有这些都包括在内。"简单"项目往往是未经验证的假设导致最多返工的地方。设计可以简短(对于真正简单的项目只需几句话),但你必须呈现设计并获得批准。
检查清单
你必须为以下每项创建任务并按顺序完成:
1. 探索项目上下文 — 检查文件、文档、最近的提交;
- 场景 A(未经过 systematic-debugging)(新增功能):同时新建 findings 文件开始记录(路径必须为
docs/superpowers/findings/YYYY-MM-DD-<topic>-findings.md,见"文件路径硬约束"区块) - 场景 B(经过 systematic-debugging)(bug 修复):findings 文件已由 systematic-debugging 创建,追加到已有文件,不新建
2. 提供可视化伴侣(如果主题将涉及可视化问题)— 这是单独的消息,不与澄清问题结合。参见下方的可视化伴侣部分。 3. 提出澄清问题 — 一次一个,理解目的/约束/成功标准;每次使用浏览器/搜索工具后更新 findings 4. 提出 2-3 种方案 — 包含权衡取舍和你的推荐;将技术决策及理由记入 findings 5. 呈现设计 — 按复杂度分段呈现,每个部分后获得用户批准 6. 编写设计文档与实现计划 — 先核对路径格式(见"文件路径硬约束"区块),然后同步产出:
- Spec 保存到
docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md - Plan 保存到
docs/superpowers/plans/YYYY-MM-DD-<topic>.md - Findings 一并提交
- 三份文件一次性提交到 git
7. 规范自审 — 快速内联检查占位符、矛盾、歧义、范围(见下文);同步检查计划的占位符、规格覆盖、类型一致性 8. 用户审查书面规范与计划 — 要求用户在继续之前同时审查 spec 和 plan 文件 9. 过渡到实现 — 调用 superpowers-subagent-driven-development
流程图
digraph brainstorming {
"探索项目上下文" [shape=box];
"是否有可视化问题?" [shape=diamond];
"提供可视化伴侣\n(单独消息,无其他内容)" [shape=box];
"提出澄清问题" [shape=box];
"提出 2-3 种方案" [shape=box];
"呈现设计部分" [shape=box];
"用户批准设计?" [shape=diamond];
"同步编写 spec + plan + findings" [shape=box];
"规范与计划自审\n(内联修复)" [shape=box];
"用户审查 spec + plan?" [shape=diamond];
"批准,调用 subagent-driven-development" [shape=doublecircle];
"探索项目上下文" -> "是否有可视化问题?";
"是否有可视化问题?" -> "提供可视化伴侣\n(单独消息,无其他内容)" [label="是"];
"是否有可视化问题?" -> "提出澄清问题" [label="否"];
"提供可视化伴侣\n(单独消息,无其他内容)" -> "提出澄清问题";
"提出澄清问题" -> "提出 2-3 种方案";
"提出 2-3 种方案" -> "呈现设计部分";
"呈现设计部分" -> "用户批准设计?";
"用户批准设计?" -> "呈现设计部分" [label="否,修订"];
"用户批准设计?" -> "同步编写 spec + plan + findings" [label="是"];
"同步编写 spec + plan + findings" -> "规范与计划自审\n(内联修复)";
"规范与计划自审\n(内联修复)" -> "用户审查 spec + plan?";
"用户审查 spec + plan?" -> "同步编写 spec + plan + findings" [label="请求修改"];
"用户审查 spec + plan?" -> "批准,调用 subagent-driven-development" [label="批准"];
}终止状态是调用 superpowers-subagent-driven-development。 不得调用任何其他 skill,不得开始实现。头脑风暴后你调用的唯一 skill 是 superpowers-subagent-driven-development(需要用户批准 spec + plan 后才调用)。
Findings 规范
Findings 文件与 spec、plan 文件并列产出,贯穿整个头脑风暴过程持续更新,不是最后才写。
文件路径
docs/superpowers/findings/YYYY-MM-DD-<topic>-findings.md
此路径不可简化或缩写! 必须包含 superpowers/findings/ 子目录 + 日期前缀 + -findings.md 后缀。详见"文件路径硬约束"区块。
创建时机
在第 1 步"探索项目上下文"开始时立即创建,用空模板占位,边研究边填充。
强制更新规则
每执行 2 次查看 / 浏览器 / 搜索操作后,必须更新 findings 文件。 目的:防止视觉信息和调研结论滞留在上下文中后丢失。
强制记录内容
技术决策必须记录理由:
| 决策 | 理由 |
|---|---|
| 使用 Redis 做缓存 | 需要高频读写,内存缓存足够快 |
遇到的问题必须记录解决方案:
| 问题 | 解决方案 |
|---|---|
| API 响应慢 | 添加 Redis 缓存层 |
视觉 / 多模态内容立即文本化: 看到任何图表、截图、UI 时,立即用文字记录关键信息,不依赖记忆。
文档模板
# 发现与决策
## 需求
- [列出原始需求]
## 研究发现
- [记录调研结果]
## 技术决策
| 决策 | 理由 |
|------|------|
| | |
## 遇到的问题
| 问题 | 解决方案 |
|------|---------|
| | |
## 资源
- [有用的链接、文档、代码位置]
## 视觉 / 浏览器发现
<!-- 每执行 2 次查看/浏览器操作后必须更新此部分 -->
<!-- 多模态内容必须立即以文本形式记录 -->
- [看到的视觉内容文本化记录]
---
*每执行 2 次查看/浏览器/搜索操作后更新此文件*提交时机
在第 6 步编写设计文档与实现计划时随 spec、plan 一起提交。
流程
理解创意:
- 首先检查当前项目状态(文件、文档、最近的提交)
- 在提出详细问题之前,评估范围:如果请求描述了多个独立的子系统(例如"构建一个包含聊天、文件存储、账单和分析的平台"),立即标记。不要花时间完善需要先分解的项目细节。
- 如果项目对于单个规范来说太大,帮助用户分解为子项目:独立的部分有哪些、它们如何关联、应该按什么顺序构建?然后通过正常的设计流程对第一个子项目进行头脑风暴。每个子项目都有自己的规范 → 计划 → 实现循环。
- 对于范围适当的项目,逐一提问以完善创意
- 尽可能使用选择题,但开放式问题也可以
- 每条消息只问一个问题 - 如果某个主题需要更多探索,分解为多个问题
- 重点关注理解:目的、约束、成功标准
探索方案:
- 提出 2-3 种不同的方案及其权衡取舍
- 以对话方式呈现选项,给出你的推荐和理由
- 首先展示你推荐的选项并解释原因
呈现设计:
- 一旦你认为理解了要构建的内容,呈现设计
- 根据复杂度调整每个部分:简单的内容几句话,复杂的内容最多 200-300 字
- 每个部分后询问目前看起来是否正确
- 涵盖:架构、组件、数据流、错误处理、测试
- 准备好回头澄清不清楚的地方
为隔离和清晰而设计:
- 将系统分解为更小的单元,每个单元有单一明确的目的,通过定义良好的接口通信,并能独立理解和测试
- 对于每个单元,你应该能回答:它做什么、如何使用它、它依赖什么?
- 某人能否在不阅读内部实现的情况下理解单元的功能?能否在不破坏使用者的情况下更改内部实现?如果不能,边界需要改进。
- 更小、边界良好的单元也更容易让你处理 - 你能更好地推理可以一次性装入上下文的代码,当文件聚焦时你的编辑也更可靠。当文件变大时,这通常是它做了太多事情的信号。
在现有代码库中工作:
- 在提出变更之前探索当前结构。遵循现有模式。
- 在现有代码存在影响工作的问题的地方(例如文件变得太大、边界不清、职责混乱),将有针对性的改进作为设计的一部分 - 就像优秀的开发者在他们工作的代码中改进代码一样。
- 不要提出无关的重构。专注于服务于当前目标的内容。
设计之后:同步产出 spec + plan
用户批准设计后,同时编写以下三份文档:
Spec 文档
将验证过的设计写入 docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md
此路径不可简化或缩写! 必须包含 superpowers/specs/ 子目录 + 日期前缀 + -design.md 后缀。详见"文件路径硬约束"区块。
Plan 文档
将实现计划写入 docs/superpowers/plans/YYYY-MM-DD-<topic>.md
Plan 文档假设工程师对代码库零了解且品味存疑,记录他们需要知道的一切:每个任务需要触及哪些文件、代码、测试,以及如何测试。以小粒度任务形式给出整个计划。DRY。YAGNI。TDD。频繁提交。
范围检查
如果规格说明涵盖多个独立的子系统,建议将其分解为单独的计划 — 每个子系统一个计划。每个计划都应该能独立产生可工作、可测试的软件。
文件结构规划
在定义任务之前,规划出将要创建或修改哪些文件以及每个文件的职责:
- 设计具有清晰边界和明确定义接口的单元,每个文件一个职责
- 优先选择较小的、聚焦的文件,而不是做太多事情的大型文件
- 一起变化的文件放在一起,按职责分离而不是按技术层分离
- 在现有代码库中,遵循既定模式;如果要修改的文件已经变得笨重,在计划中包含拆分是合理的
任务粒度
每个步骤是一个动作(2-5 分钟):
- "编写失败的测试" - 步骤
- "运行它以确保它失败" - 步骤
- "实现使测试通过的最小代码" - 步骤
- "运行测试并确保它们通过" - 步骤
- "提交" - 步骤
Plan 文档头部
每个计划必须以此头部开始:
# [Feature Name] Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** [One sentence describing what this builds]
**Architecture:** [2-3 sentences about approach]
**Tech Stack:** [Key technologies/libraries]
---任务结构模板
````markdown
Task N: [Component Name]
Files:
- Create:
exact/path/to/file.py - Modify:
exact/path/to/existing.py:123-145 - Test:
tests/exact/path/to/test.py
- [ ] Step 1: Write the failing test
def test_specific_behavior():
result = function(input)
assert result == expected- [ ] Step 2: Run test to verify it fails
Run: pytest tests/path/test.py::test_name -v Expected: FAIL with "function not defined"
- [ ] Step 3: Write minimal implementation
def function(input):
return expected- [ ] Step 4: Run test to verify it passes
Run: pytest tests/path/test.py::test_name -v Expected: PASS
- [ ] Step 5: Commit
git add tests/path/test.py src/path/file.py
git commit -m "feat: add specific feature"````
禁止占位符
每个步骤必须包含工程师需要的实际内容。这些是计划失败 — 永远不要写它们:
- "TBD"、"TODO"、"implement later"、"fill in details"
- "添加适当的错误处理" / "添加验证" / "处理边界情况"
- "为上述编写测试"(没有实际的测试代码)
- "类似于任务 N"(重复代码 — 工程师可能不按顺序阅读任务)
- 描述做什么而不展示如何做的步骤(代码步骤需要代码块)
- 引用未在任何任务中定义的类型、函数或方法
自审
编写完 spec 和 plan 后,用全新的眼光同时审视两份文档:
Spec 自审: 1. 占位符扫描: 是否有 "TBD"、"TODO"、不完整的部分或模糊的需求?修复它们。 2. 内部一致性: 是否有部分互相矛盾?架构是否与功能描述匹配? 3. 范围检查: 是否足够聚焦以适合单个实现计划,还是需要分解? 4. 歧义检查: 是否有任何需求可以有两种不同的解释?如果有,选择一种并明确说明。
Plan 自审: 1. 规格覆盖: 快速浏览 spec 中的每个需求,能指出实现它的任务吗?列出任何缺口。 2. 占位符扫描: 搜索上文"禁止占位符"中的任何模式,修复它们。 3. 类型一致性: 后续任务中使用的类型、方法签名、属性名是否与早期任务中定义的匹配?
内联修复任何问题,无需重新审查。
用户审查关卡
自审通过后,要求用户同时审查 spec 和 plan:
"spec 和 plan 已编写并提交:
- Spec: <spec-path>- Plan: <plan-path>>
请审查这两份文档,在我们开始实现之前让我知道是否要进行任何更改。"
等待用户响应。如果请求更改,修改对应文档并重新运行自审。仅在用户批准后才继续。
实现
<HARD-GATE> 用户批准 spec + plan 后,下一步必须是 `superpowers-subagent-driven-development`。不得调用任何其他 skill,不得开始实现。
调用 superpowers-subagent-driven-development 前必须等待用户明确批准。 </HARD-GATE>
关键原则
- 一次一个问题 - 不要用多个问题让人不知所措
- 优先使用选择题 - 尽可能比开放式问题更容易回答
- 严格遵循 YAGNI - 从所有设计中移除不必要的功能
- 探索替代方案 - 在确定之前始终提出 2-3 种方案
- 增量验证 - 呈现设计,在继续之前获得批准
- 保持灵活 - 当某些内容不清楚时回头澄清
可视化伴侣
基于浏览器的伴侣,用于在头脑风暴期间展示模型、图表和可视化选项。作为工具提供 — 而非模式。接受伴侣意味着它可以用于受益于可视化处理的问题;这并不意味着每个问题都通过浏览器处理。
提供伴侣: 当你预期即将到来的问题将涉及可视化内容(模型、布局、图表)时,提供一次以获得同意:
"我们要处理的一些内容如果能在 Web 浏览器中向你展示可能会更容易解释。我可以组合模型、图表、比较和其他可视化内容。此功能仍然很新,可能会消耗大量 token。想试试吗?(需要打开本地 URL)"
此提议必须是单独的消息。 不要将其与澄清问题、上下文摘要或任何其他内容结合。消息应仅包含上述提议,别无其他。在继续之前等待用户的响应。如果他们拒绝,继续纯文本头脑风暴。
每个问题的决策: 即使在用户接受后,也要针对每个问题决定是使用浏览器还是终端。测试标准:用户通过看比阅读能更好地理解这个内容吗?
- 使用浏览器 处理可视化内容 — 模型、线框图、布局比较、架构图、并排可视化设计
- 使用终端 处理文本内容 — 需求问题、概念选择、权衡列表、A/B/C/D 文本选项、范围决策
关于 UI 主题的问题并不自动成为可视化问题。"个性在此上下文中意味着什么?" 是一个概念问题 — 使用终端。"哪种向导布局更好?" 是一个可视化问题 — 使用浏览器。
如果他们同意使用伴侣,请在继续之前阅读详细指南: skills/superpowers-brainstorming/visual-companion.md
计划文档审查者提示词模板
当调度计划文档审查子代理时使用此模板。
目的: 验证计划是否完整、是否符合规格说明以及任务分解是否合理。
调度时机: 完整计划编写完成后。
Task tool (general-purpose):
description: "Review plan document"
prompt: |
你是一名计划文档审查者。请验证此计划是否完整并准备好进行实施。
**待审查的计划:** [PLAN_FILE_PATH]
**参考规格说明:** [SPEC_FILE_PATH]
## 检查内容
| 类别 | 查找内容 |
|----------|------------------|
| 完整性 | TODOs、占位符、未完成任务、缺失步骤 |
| 规格对齐 | 计划覆盖规格要求,无重大范围蔓延 |
| 任务分解 | 任务边界清晰,步骤可操作 |
| 可构建性 | 工程师能否按照此计划执行而不会卡住? |
## 校准标准
**仅标记会在实施过程中导致实际问题的问题。**
实施者构建错误的内容或卡住属于问题。
措辞、风格偏好和"锦上添花"的建议不属于问题。
除非存在严重缺陷——规格中缺失需求、步骤矛盾、占位符内容或任务过于模糊无法执行——否则应予批准。
## 输出格式
## 计划审查
**状态:** Approved | Issues Found
**问题(如有):**
- [任务 X, 步骤 Y]: [具体问题] - [对实施的影响]
**建议(仅供参考,不阻止批准):**
- [改进建议]审查者返回: 状态、问题(如有)、建议
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Superpowers 头脑风暴</title>
<style>
/*
* 头脑风暴伴侣框架模板
*
* 此模板提供一致的框架,包含:
* - 操作系统感知的亮色/暗色主题
* - 固定头部和选择指示栏
* - 可滚动的主内容区域
* - 常见 UI 模式的 CSS 辅助工具
*
* 内容通过 #claude-content 中的占位符注释注入。
*/
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
/* ===== 主题变量 ===== */
:root {
--bg-primary: #f5f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #e5e5e7;
--border: #d1d1d6;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--text-tertiary: #aeaeb2;
--accent: #0071e3;
--accent-hover: #0077ed;
--success: #34c759;
--warning: #ff9f0a;
--error: #ff3b30;
--selected-bg: #e8f4fd;
--selected-border: #0071e3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1d1d1f;
--bg-secondary: #2d2d2f;
--bg-tertiary: #3d3d3f;
--border: #424245;
--text-primary: #f5f5f7;
--text-secondary: #86868b;
--text-tertiary: #636366;
--accent: #0a84ff;
--accent-hover: #409cff;
--selected-bg: rgba(10, 132, 255, 0.15);
--selected-border: #0a84ff;
}
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
flex-direction: column;
line-height: 1.5;
}
/* ===== 框架结构 ===== */
.header {
background: var(--bg-secondary);
padding: 0.5rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
.header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; }
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; }
.main { flex: 1; overflow-y: auto; }
#claude-content { padding: 2rem; min-height: 100%; }
.indicator-bar {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 0.5rem 1.5rem;
flex-shrink: 0;
text-align: center;
}
.indicator-bar span {
font-size: 0.75rem;
color: var(--text-secondary);
}
.indicator-bar .selected-text {
color: var(--accent);
font-weight: 500;
}
/* ===== 排版 ===== */
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
.section { margin-bottom: 2rem; }
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
/* ===== 选项(用于 A/B/C 选择) ===== */
.options { display: flex; flex-direction: column; gap: 0.75rem; }
.option {
background: var(--bg-secondary);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem 1.25rem;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: flex-start;
gap: 1rem;
}
.option:hover { border-color: var(--accent); }
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
.option .letter {
background: var(--bg-tertiary);
color: var(--text-secondary);
width: 1.75rem; height: 1.75rem;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
}
.option.selected .letter { background: var(--accent); color: white; }
.option .content { flex: 1; }
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
/* ===== 卡片(用于显示设计/模型) ===== */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card.selected { border-color: var(--selected-border); border-width: 2px; }
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
.card-body { padding: 1rem; }
.card-body h3 { margin-bottom: 0.25rem; }
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
/* ===== 模型容器 ===== */
.mockup {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.mockup-header {
background: var(--bg-tertiary);
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
}
.mockup-body { padding: 1.5rem; }
/* ===== 分割视图(并排比较) ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
/* ===== 优缺点 ===== */
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
.pros li, .cons li { margin-bottom: 0.25rem; }
/* ===== 占位符(用于模型区域) ===== */
.placeholder {
background: var(--bg-tertiary);
border: 2px dashed var(--border);
border-radius: 8px;
padding: 2rem;
text-align: center;
color: var(--text-tertiary);
}
/* ===== 内联模型元素 ===== */
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
.mock-content { padding: 1.5rem; flex: 1; }
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
</style>
</head>
<body>
<div class="header">
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers 头脑风暴</a></h1>
<div class="status">已连接</div>
</div>
<div class="main">
<div id="claude-content">
<!-- CONTENT -->
</div>
</div>
<div class="indicator-bar">
<span id="indicator-text">点击上方的选项,然后返回终端</span>
</div>
</body>
</html>
(function() {
const WS_URL = 'ws://' + window.location.host;
let ws = null;
let eventQueue = [];
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
eventQueue = [];
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'reload') {
window.location.reload();
}
};
ws.onclose = () => {
setTimeout(connect, 1000);
};
}
function sendEvent(event) {
event.timestamp = Date.now();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(event));
} else {
eventQueue.push(event);
}
}
// 捕获选项元素的点击事件
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-choice]');
if (!target) return;
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice,
id: target.id || null
});
// 更新指示条(延迟执行以便 toggleSelect 先运行)
setTimeout(() => {
const indicator = document.getElementById('indicator-text');
if (!indicator) return;
const container = target.closest('.options') || target.closest('.cards');
const selected = container ? container.querySelectorAll('.selected') : [];
if (selected.length === 0) {
indicator.textContent = '点击上方选项,然后返回终端';
} else if (selected.length === 1) {
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' 已选择</span> — 返回终端继续';
} else {
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' 已选择</span> — 返回终端继续';
}
}, 0);
});
// Frame UI:选择追踪
window.selectedChoice = null;
window.toggleSelect = function(el) {
const container = el.closest('.options') || el.closest('.cards');
const multi = container && container.dataset.multiselect !== undefined;
if (container && !multi) {
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
}
if (multi) {
el.classList.toggle('selected');
} else {
el.classList.add('selected');
}
window.selectedChoice = el.dataset.choice;
};
// 暴露 API 以便显式使用
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
};
connect();
})();
const crypto = require('crypto');
const http = require('http');
const fs = require('fs');
const path = require('path');
// ========== WebSocket 协议 (RFC 6455) ==========
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
}
function encodeFrame(opcode, payload) {
const fin = 0x80;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = fin | opcode;
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = fin | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = fin | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
return Buffer.concat([header, payload]);
}
function decodeFrame(buffer) {
if (buffer.length < 2) return null;
const secondByte = buffer[1];
const opcode = buffer[0] & 0x0F;
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7F;
let offset = 2;
if (!masked) throw new Error('客户端帧必须被掩码覆盖');
if (payloadLen === 126) {
if (buffer.length < 4) return null;
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return null;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskOffset = offset;
const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen;
if (buffer.length < totalLen) return null;
const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
}
return { opcode, payload: data, bytesConsumed: totalLen };
}
// ========== 配置 ==========
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 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',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
};
// ========== 模板和常量 ==========
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }</style>
</head>
<body><h1>Brainstorm 伴侣</h1>
<p>等待代理推送屏幕...</p></body></html>`;
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = '<script>\n' + helperScript + '\n</script>';
// ========== 辅助函数 ==========
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
function getNewestScreen() {
const files = fs.readdirSync(CONTENT_DIR)
.filter(f => f.endsWith('.html'))
.map(f => {
const fp = path.join(CONTENT_DIR, f);
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
// ========== HTTP 请求处理器 ==========
function handleRequest(req, res) {
touchActivity();
if (req.method === 'GET' && req.url === '/') {
const screenFile = getNewestScreen();
let html = screenFile
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
: WAITING_PAGE;
if (html.includes('</body>')) {
html = html.replace('</body>', helperInjection + '\n</body>');
} else {
html += helperInjection;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
const fileName = req.url.slice(7);
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('未找到');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(fs.readFileSync(filePath));
} else {
res.writeHead(404);
res.end('Not found');
}
}
// ========== WebSocket 连接处理 ==========
const clients = new Set();
function handleUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
if (!key) { socket.destroy(); return; }
const accept = computeAcceptKey(key);
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
);
let buffer = Buffer.alloc(0);
clients.add(socket);
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length > 0) {
let result;
try {
result = decodeFrame(buffer);
} catch (e) {
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
}
if (!result) break;
buffer = buffer.slice(result.bytesConsumed);
switch (result.opcode) {
case OPCODES.TEXT:
handleMessage(result.payload.toString());
break;
case OPCODES.CLOSE:
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
case OPCODES.PING:
socket.write(encodeFrame(OPCODES.PONG, result.payload));
break;
case OPCODES.PONG:
break;
default: {
const closeBuf = Buffer.alloc(2);
closeBuf.writeUInt16BE(1003);
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
clients.delete(socket);
return;
}
}
}
});
socket.on('close', () => clients.delete(socket));
socket.on('error', () => clients.delete(socket));
}
function handleMessage(text) {
let event;
try {
event = JSON.parse(text);
} catch (e) {
console.error('解析 WebSocket 消息失败:', e.message);
return;
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(STATE_DIR, 'events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
function broadcast(msg) {
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
for (const socket of clients) {
try { socket.write(frame); } catch (e) { clients.delete(socket); }
}
}
// ========== 活动追踪 ==========
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 分钟
let lastActivity = Date.now();
function touchActivity() {
lastActivity = Date.now();
}
// ========== 文件监控 ==========
const debounceTimers = new Map();
// ========== 服务器启动 ==========
function startServer() {
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
// 追踪已知文件以区分新屏幕和更新。
// macOS fs.watch 对新文件和覆盖都报告 'rename',
// 所以我们不能仅依赖 eventType。
const knownFiles = new Set(
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
);
const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);
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(CONTENT_DIR, filename);
if (!fs.existsSync(filePath)) return; // 文件已被删除
touchActivity();
if (!knownFiles.has(filename)) {
knownFiles.add(filename);
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
} else {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
}
broadcast({ type: 'reload' });
}, 100));
});
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
function shutdown(reason) {
console.log(JSON.stringify({ type: 'server-stopped', reason }));
const infoFile = path.join(STATE_DIR, 'server-info');
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
fs.writeFileSync(
path.join(STATE_DIR, 'server-stopped'),
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
);
watcher.close();
clearInterval(lifecycleCheck);
server.close(() => process.exit(0));
}
function ownerAlive() {
if (!ownerPid) return true;
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
}
// 每 60 秒检查一次:如果所有者进程已死亡或空闲 30 分钟则退出
const lifecycleCheck = setInterval(() => {
if (!ownerAlive()) shutdown('owner process exited');
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
}, 60 * 1000);
lifecycleCheck.unref();
// 在启动时验证所有者 PID。如果已经死亡,则 PID 解析
// 错误(常见于 WSL、Tailscale SSH 和跨用户场景)。
// 禁用监控并依赖空闲超时。
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: CONTENT_DIR, state_dir: STATE_DIR
});
console.log(info);
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
});
}
if (require.main === module) {
startServer();
}
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
#!/usr/bin/env bash
# 启动 brainstorm 服务器并输出连接信息
# 用法: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
#
# 在随机高端口上启动服务器,输出包含 URL 的 JSON。
# 每个会话都有独立的目录以避免冲突。
#
# 选项:
# --project-dir <path> 在 <path>/.superpowers/brainstorm/ 下存储会话文件
# 而非 /tmp。服务器停止后文件仍然保留。
# --host <bind-host> 绑定的主机/接口(默认:127.0.0.1)。
# 在远程/容器环境中使用 0.0.0.0。
# --url-host <host> 返回的 URL JSON 中显示的主机名。
# --foreground 在当前终端中运行服务器(不后台运行)。
# --background 强制后台模式(覆盖 Codex 自动前台模式)。
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 解析参数
PROJECT_DIR=""
FOREGROUND="false"
FORCE_BACKGROUND="false"
BIND_HOST="127.0.0.1"
URL_HOST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--project-dir)
PROJECT_DIR="$2"
shift 2
;;
--host)
BIND_HOST="$2"
shift 2
;;
--url-host)
URL_HOST="$2"
shift 2
;;
--foreground|--no-daemon)
FOREGROUND="true"
shift
;;
--background|--daemon)
FORCE_BACKGROUND="true"
shift
;;
*)
echo "{\"error\": \"Unknown argument: $1\"}"
exit 1
;;
esac
done
if [[ -z "$URL_HOST" ]]; then
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
URL_HOST="localhost"
else
URL_HOST="$BIND_HOST"
fi
fi
# 某些环境会回收分离的/后台进程。检测到时自动切换到前台模式。
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
FOREGROUND="true"
fi
# Windows/Git Bash 会回收 nohup 后台进程。检测到时自动切换到前台模式。
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
esac
if [[ -n "${MSYSTEM:-}" ]]; then
FOREGROUND="true"
fi
fi
# 生成唯一的会话目录
SESSION_ID="$$-$(date +%s)"
if [[ -n "$PROJECT_DIR" ]]; then
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
LOG_FILE="${STATE_DIR}/server.log"
# 创建新的会话目录,包含 content 和 state 子目录
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
# 终止任何现有的服务器
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill "$old_pid" 2>/dev/null
rm -f "$PID_FILE"
fi
cd "$SCRIPT_DIR"
# 获取 harness PID(此脚本的祖父进程)。
# $PPID 是 harness 生成用来运行我们的临时 shell —— 当此脚本
# 退出时它会死亡。harness 本身是 $PPID 的父进程。
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
OWNER_PID="$PPID"
fi
# 前台模式,用于会回收分离/后台进程的环境
if [[ "$FOREGROUND" == "true" ]]; then
echo "$$" > "$PID_FILE"
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
exit $?
fi
# 启动服务器,将输出捕获到日志文件
# 使用 nohup 以在 shell 退出后继续存活;使用 disown 从作业表中移除
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"
# 等待 server-started 消息(检查日志文件)
for i in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
# 在短时间窗口后验证服务器仍然存活(捕获进程回收器)
alive="true"
for _ in {1..20}; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
alive="false"
break
fi
sleep 0.1
done
if [[ "$alive" != "true" ]]; then
echo "{\"error\": \"服务器已启动但被终止。请在持久终端中重试:$SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
exit 1
fi
grep "server-started" "$LOG_FILE" | head -1
exit 0
fi
sleep 0.1
done
# 超时 - 服务器未启动
echo '{"error": "服务器在 5 秒内未能启动"}'
exit 1
#!/usr/bin/env bash
# 停止 brainstorming 服务器并清理资源
# 用法: stop-server.sh <session_dir>
#
# 终止服务器进程。仅当 session 目录位于 /tmp 下时才删除(临时目录)。
# 持久化目录(.superpowers/)会保留,以便后续查看 mockups。
SESSION_DIR="$1"
if [[ -z "$SESSION_DIR" ]]; then
echo '{"error": "用法: stop-server.sh <session_dir>"}'
exit 1
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
# 尝试优雅地停止进程,如果进程仍存活则强制终止
kill "$pid" 2>/dev/null || true
# 等待优雅关闭(最多约 2 秒)
for i in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
sleep 0.1
done
# 如果进程仍在运行,升级为 SIGKILL
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
# 给 SIGKILL 一点时间生效
sleep 0.1
fi
if kill -0 "$pid" 2>/dev/null; then
echo '{"status": "failed", "error": "进程仍在运行"}'
exit 1
fi
rm -f "$PID_FILE" "${STATE_DIR}/server.log"
# 仅删除临时的 /tmp 目录
if [[ "$SESSION_DIR" == /tmp/* ]]; then
rm -rf "$SESSION_DIR"
fi
echo '{"status": "stopped"}'
else
echo '{"status": "not_running"}'
fi
Spec Document 审查者提示模板
当派遣 spec document 审查者子代理时使用此模板。
目的: 验证规范文档是完整的、一致的,并且已准备好进行实施规划。
派遣时机: Spec document 已写入 docs/superpowers/specs/
Task tool (general-purpose):
description: "Review spec document"
prompt: |
你是一个 spec document 审查者。验证此规范文档是完整的并已准备好进行规划。
**待审查的 Spec:** [SPEC_FILE_PATH]
## 检查内容
| 类别 | 查找内容 |
|----------|------------------|
| 完整性 | TODOs、占位符、"TBD"、未完成的章节 |
| 一致性 | 内部矛盾、冲突的需求 |
| 清晰度 | 需求模糊到可能导致他人构建错误的东西 |
| 范围 | 足够聚焦于单个计划 — 不覆盖多个独立的子系统 |
| YAGNI | 未请求的功能、过度工程化 |
## 校准标准
**仅标记会在实施规划期间导致真实问题的问题。**
缺失的章节、矛盾、或模糊到可以有两种不同解释的需求 — 这些是问题。
措辞的微调、风格偏好、"章节不如其他详细"等不是问题。
除非存在会导致有缺陷计划的严重缺陷,否则应批准。
## 输出格式
## Spec 审查
**Status:** Approved | Issues Found
**问题 (如有):**
- [Section X]: [具体问题] - [对规划的影响]
**建议 (仅供参考,不阻止批准):**
- [改进建议]审查者返回: Status、问题(如有)、建议
可视化伴侣指南
基于浏览器的可视化头脑风暴伴侣,用于展示模型、图表和选项。
何时使用
按问题决定,而非按会话决定。测试标准:用户通过查看内容是否比阅读文字更容易理解?
使用浏览器 当内容本身是可视化的:
- UI 模型 — 线框图、布局、导航结构、组件设计
- 架构图 — 系统组件、数据流、关系图
- 并排视觉对比 — 比较两个布局、两个配色方案、两个设计方向
- 设计打磨 — 当问题关于外观和感觉、间距、视觉层次时
- 空间关系 — 状态机、流程图、实体关系图渲染
使用终端 当内容是文本或表格时:
- 需求和范围问题 — "X 是什么意思?"、"哪些功能在范围内?"
- 概念性 A/B/C 选择 — 在用文字描述的方法之间选择
- 权衡列表 — 优缺点、对比表
- 技术决策 — API 设计、数据建模、架构方法选择
- 澄清问题 — 任何答案是用文字而非视觉偏好表达的问题
关于 UI 主题的问题不一定是视觉问题。"您想要什么样的向导?"是概念性的 — 使用终端。"这些向导布局中哪个感觉合适?"是视觉性的 — 使用浏览器。
工作原理
服务器监视目录中的 HTML 文件并将最新的文件提供给浏览器。您将 HTML 内容写入 screen_dir,用户在浏览器中查看并可以点击选择选项。选择记录到 state_dir/events,您在下一轮读取它。
内容片段 vs 完整文档: 如果您的 HTML 文件以 <!DOCTYPE 或 <html 开头,服务器将按原样提供(仅注入辅助脚本)。否则,服务器会自动将您的内容包装在框架模板中 — 添加页眉、CSS 主题、选择指示器和所有交互基础设施。默认编写内容片段。 只有当您需要完全控制页面时才编写完整文档。
启动会话
# 使用持久化启动服务器(模型保存到项目)
scripts/start-server.sh --project-dir /path/to/project
# 返回: {"type":"server-started","port":52341,"url":"http://localhost:52341",
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}从响应中保存 screen_dir 和 state_dir。告诉用户打开 URL。
查找连接信息: 服务器将其启动 JSON 写入 $STATE_DIR/server-info。如果您在后台启动服务器且没有捕获 stdout,请读取该文件以获取 URL 和端口。使用 --project-dir 时,检查 <project>/.superpowers/brainstorm/ 以获取会话目录。
注意: 将项目根目录作为 --project-dir 传递,这样模型会持久化在 .superpowers/brainstorm/ 中,并在服务器重启后保留。没有它,文件会进入 /tmp 并被清理。提醒用户将 .superpowers/ 添加到 .gitignore(如果尚未添加)。
按平台启动服务器:
Claude Code (macOS / Linux):
# 默认模式即可 — 脚本本身会将服务器置于后台
scripts/start-server.sh --project-dir /path/to/projectClaude Code (Windows):
# Windows 自动检测并使用前台模式,这会阻塞工具调用。
# 在 Bash 工具调用上使用 run_in_background: true,以便服务器
# 在对话轮次之间保持存活。
scripts/start-server.sh --project-dir /path/to/project通过 Bash 工具调用时,设置 run_in_background: true。然后在下一轮读取 $STATE_DIR/server-info 以获取 URL 和端口。
Codex:
# Codex 会回收后台进程。脚本自动检测 CODEX_CI 并
# 切换到前台模式。正常运行即可 — 无需额外标志。
scripts/start-server.sh --project-dir /path/to/projectGemini CLI:
# 使用 --foreground 并在 shell 工具调用上设置 is_background: true
# 以便进程在轮次之间保持存活
scripts/start-server.sh --project-dir /path/to/project --foreground其他环境: 服务器必须在对话轮次之间在后台保持运行。如果您的环境会回收分离的进程,请使用 --foreground 并使用平台的后台执行机制启动命令。
如果 URL 无法从您的浏览器访问(在远程/容器化设置中常见),绑定非回环主机:
scripts/start-server.sh \
--project-dir /path/to/project \
--host 0.0.0.0 \
--url-host localhost使用 --url-host 控制返回的 URL JSON 中打印的主机名。
循环
1. 检查服务器是否存活,然后将 HTML 写入 screen_dir 中的新文件:
- 每次写入前,检查
$STATE_DIR/server-info是否存在。如果不存在(或$STATE_DIR/server-stopped存在),说明服务器已关闭 — 在继续之前使用start-server.sh重启。服务器在 30 分钟不活动后自动退出。 - 使用语义化文件名:
platform.html、visual-style.html、layout.html - 切勿重用文件名 — 每个屏幕使用新文件
- 使用 Write 工具 — 切勿使用 cat/heredoc(会将噪声转储到终端)
- 服务器自动提供最新文件
2. 告诉用户期待什么并结束您的回合:
- 提醒他们 URL(每一步,不仅仅是第一次)
- 简要文字总结屏幕上的内容(例如,"显示主页的 3 个布局选项")
- 要求他们在终端中响应:"看一下并告诉我您的想法。如果愿意,可以点击选择选项。"
3. 在您的下一轮 — 用户在终端中响应后:
- 如果存在,读取
$STATE_DIR/events— 这包含用户的浏览器交互(点击、选择)作为 JSON 行 - 与用户的终端文本合并以获得完整图片
- 终端消息是主要反馈;
state_dir/events提供结构化交互数据
4. 迭代或推进 — 如果反馈改变了当前屏幕,写入新文件(例如 layout-v2.html)。只有在当前步骤经过验证后才移动到下一个问题。
5. 返回终端时卸载 — 当下一步不需要浏览器时(例如,澄清问题、权衡讨论),推送等待屏幕以清除陈旧内容:
<!-- 文件名: waiting.html (或 waiting-2.html, 等) -->
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">在终端中继续...</p>
</div>这可以防止用户在对话继续时盯着已解决的选择。当下一个视觉问题出现时,像往常一样推送新内容文件。
6. 重复直到完成。
编写内容片段
只编写页面内的内容。服务器自动将其包装在框架模板中(页眉、主题 CSS、选择指示器和所有交互基础设施)。
最小示例:
<h2>哪种布局更好?</h2>
<p class="subtitle">考虑可读性和视觉层次</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>单列</h3>
<p>简洁、专注的阅读体验</p>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>双列</h3>
<p>侧边栏导航与主内容</p>
</div>
</div>
</div>就是这样。不需要 <html>、不需要 CSS、不需要 <script> 标签。服务器提供了所有这些。
可用的 CSS 类
框架模板为您的内容提供以下 CSS 类:
选项(A/B/C 选择)
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>标题</h3>
<p>描述</p>
</div>
</div>
</div>多选: 向容器添加 data-multiselect 以允许用户选择多个选项。每次点击切换项目。指示条显示计数。
<div class="options" data-multiselect>
<!-- 相同的选项标记 — 用户可以选择/取消选择多个 -->
</div>卡片(视觉设计)
<div class="cards">
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
<div class="card-image"><!-- 模型内容 --></div>
<div class="card-body">
<h3>名称</h3>
<p>描述</p>
</div>
</div>
</div>模型容器
<div class="mockup">
<div class="mockup-header">预览: 仪表板布局</div>
<div class="mockup-body"><!-- 您的模型 HTML --></div>
</div>分割视图(并排)
<div class="split">
<div class="mockup"><!-- 左侧 --></div>
<div class="mockup"><!-- 右侧 --></div>
</div>优缺点
<div class="pros-cons">
<div class="pros"><h4>优点</h4><ul><li>好处</li></ul></div>
<div class="cons"><h4>缺点</h4><ul><li>缺点</li></ul></div>
</div>模型元素(线框构建块)
<div class="mock-nav">Logo | 首页 | 关于 | 联系</div>
<div style="display: flex;">
<div class="mock-sidebar">导航</div>
<div class="mock-content">主要内容区域</div>
</div>
<button class="mock-button">操作按钮</button>
<input class="mock-input" placeholder="输入字段">
<div class="placeholder">占位符区域</div>排版和部分
h2— 页面标题h3— 部分标题.subtitle— 标题下方的次要文字.section— 带底部边距的内容块.label— 小型大写标签文字
浏览器事件格式
当用户在浏览器中点击选项时,他们的交互被记录到 $STATE_DIR/events(每行一个 JSON 对象)。当您推送新屏幕时,文件会自动清空。
{"type":"click","choice":"a","text":"选项 A - 简单布局","timestamp":1706000101}
{"type":"click","choice":"c","text":"选项 C - 复杂网格","timestamp":1706000108}
{"type":"click","choice":"b","text":"选项 B - 混合","timestamp":1706000115}完整的事件流显示用户的探索路径 — 他们可能在确定之前点击多个选项。最后一个 choice 事件通常是最终选择,但点击模式可以揭示犹豫或值得询问的偏好。
如果 $STATE_DIR/events 不存在,说明用户没有与浏览器交互 — 仅使用他们的终端文本。
设计技巧
- 根据问题调整保真度 — 布局使用线框图,打磨问题使用打磨
- 在每个页面上解释问题 — "哪种布局感觉更专业?",而不是仅仅"选一个"
- 推进前先迭代 — 如果反馈改变了当前屏幕,编写新版本
- 每个屏幕最多 2-4 个选项
- 在重要时使用真实内容 — 对于摄影作品集,使用实际图片(Unsplash)。占位符内容会掩盖设计问题。
- 保持模型简单 — 关注布局和结构,而非像素级完美的设计
文件命名
- 使用语义化名称:
platform.html、visual-style.html、layout.html - 切勿重用文件名 — 每个屏幕必须是一个新文件
- 对于迭代:附加版本后缀,如
layout-v2.html、layout-v3.html - 服务器按修改时间提供最新文件
清理
scripts/stop-server.sh $SESSION_DIR如果会话使用了 --project-dir,模型文件会持久化在 .superpowers/brainstorm/ 中以供后续参考。只有 /tmp 会话在停止时会被删除。
参考
- 框架模板(CSS 参考):
scripts/frame-template.html - 辅助脚本(客户端):
scripts/helper.js