From 59c6b9f6cb0aab39d6ab51dc920d8c61edc2ee2f Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Tue, 14 Jul 2026 10:30:23 +0800 Subject: [PATCH] Add Superpowers Chinese marketplace plugin Constraint: Keep the official Superpowers plugin available under its existing ID while publishing the localized fork with a distinct ASCII ID. Rejected: Reusing the upstream superpowers manifest name | It collides with the official marketplace entry. Confidence: high Scope-risk: narrow Directive: Continue syncing this plugin from AreChen/superpowers-zh and preserve hooks: {}. Tested: Marketplace validator, Codex local install/list/remove, KeyInfo adapter tests, upstream Codex manifest and package checks except its platform-specific tar epoch assertion. --- .agents/plugins/marketplace.json | 12 + README.md | 2 + config/external-sources.json | 24 + config/external-sources.lock.json | 9 + .../codex/.agents/plugins/marketplace.json | 12 + .../superpowers-zh/.codex-plugin/plugin.json | 48 + .../plugins/superpowers-zh/CODE_OF_CONDUCT.md | 128 ++ plugins/codex/plugins/superpowers-zh/LICENSE | 21 + .../codex/plugins/superpowers-zh/README.md | 310 +++++ .../superpowers-zh/THIRD_PARTY_SOURCE.json | 9 + .../superpowers-zh/assets/app-icon.png | Bin 0 -> 48231 bytes .../assets/superpowers-small.svg | 1 + .../skills/brainstorming/SKILL.md | 155 +++ .../brainstorming/scripts/frame-template.html | 213 ++++ .../skills/brainstorming/scripts/helper.js | 167 +++ .../skills/brainstorming/scripts/server.cjs | 723 +++++++++++ .../brainstorming/scripts/start-server.sh | 209 +++ .../brainstorming/scripts/stop-server.sh | 120 ++ .../spec-document-reviewer-prompt.md | 48 + .../skills/brainstorming/visual-companion.md | 281 ++++ .../dispatching-parallel-agents/SKILL.md | 185 +++ .../skills/executing-plans/SKILL.md | 70 + .../finishing-a-development-branch/SKILL.md | 237 ++++ .../skills/receiving-code-review/SKILL.md | 210 +++ .../skills/requesting-code-review/SKILL.md | 103 ++ .../requesting-code-review/code-reviewer.md | 170 +++ .../subagent-driven-development/SKILL.md | 383 ++++++ .../implementer-prompt.md | 139 ++ .../scripts/review-package | 44 + .../scripts/sdd-workspace | 22 + .../scripts/task-brief | 40 + .../task-reviewer-prompt.md | 176 +++ .../systematic-debugging/CREATION-LOG.md | 119 ++ .../skills/systematic-debugging/SKILL.md | 291 +++++ .../condition-based-waiting-example.ts | 158 +++ .../condition-based-waiting.md | 113 ++ .../systematic-debugging/defense-in-depth.md | 121 ++ .../systematic-debugging/find-polluter.sh | 63 + .../root-cause-tracing.md | 167 +++ .../systematic-debugging/test-academic.md | 14 + .../systematic-debugging/test-pressure-1.md | 58 + .../systematic-debugging/test-pressure-2.md | 68 + .../systematic-debugging/test-pressure-3.md | 69 + .../skills/test-driven-development/SKILL.md | 365 ++++++ .../testing-anti-patterns.md | 293 +++++ .../skills/using-git-worktrees/SKILL.md | 199 +++ .../skills/using-superpowers/SKILL.md | 62 + .../references/antigravity-tools.md | 23 + .../references/codex-tools.md | 34 + .../using-superpowers/references/pi-tools.md | 16 + .../verification-before-completion/SKILL.md | 137 ++ .../skills/writing-plans/SKILL.md | 164 +++ .../plan-document-reviewer-prompt.md | 49 + .../skills/writing-skills/SKILL.md | 675 ++++++++++ .../anthropic-best-practices.md | 1129 +++++++++++++++++ .../examples/CLAUDE_MD_TESTING.md | 187 +++ .../writing-skills/graphviz-conventions.dot | 172 +++ .../writing-skills/persuasion-principles.md | 186 +++ .../skills/writing-skills/render-graphs.js | 168 +++ .../testing-skills-with-subagents.md | 380 ++++++ 60 files changed, 9751 insertions(+) create mode 100644 plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json create mode 100644 plugins/codex/plugins/superpowers-zh/CODE_OF_CONDUCT.md create mode 100644 plugins/codex/plugins/superpowers-zh/LICENSE create mode 100644 plugins/codex/plugins/superpowers-zh/README.md create mode 100644 plugins/codex/plugins/superpowers-zh/THIRD_PARTY_SOURCE.json create mode 100644 plugins/codex/plugins/superpowers-zh/assets/app-icon.png create mode 100644 plugins/codex/plugins/superpowers-zh/assets/superpowers-small.svg create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/frame-template.html create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/helper.js create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs create mode 100755 plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/start-server.sh create mode 100755 plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/stop-server.sh create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/spec-document-reviewer-prompt.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/dispatching-parallel-agents/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/executing-plans/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/finishing-a-development-branch/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/receiving-code-review/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/requesting-code-review/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/requesting-code-review/code-reviewer.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/implementer-prompt.md create mode 100755 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/scripts/review-package create mode 100755 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/scripts/sdd-workspace create mode 100755 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/scripts/task-brief create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/task-reviewer-prompt.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/CREATION-LOG.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/condition-based-waiting-example.ts create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/condition-based-waiting.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/defense-in-depth.md create mode 100755 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/find-polluter.sh create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/root-cause-tracing.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/test-academic.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/test-pressure-1.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/test-pressure-2.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/systematic-debugging/test-pressure-3.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/test-driven-development/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/test-driven-development/testing-anti-patterns.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-git-worktrees/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/references/antigravity-tools.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/references/codex-tools.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/references/pi-tools.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/verification-before-completion/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-plans/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-plans/plan-document-reviewer-prompt.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/anthropic-best-practices.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/examples/CLAUDE_MD_TESTING.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/graphviz-conventions.dot create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/persuasion-principles.md create mode 100755 plugins/codex/plugins/superpowers-zh/skills/writing-skills/render-graphs.js create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/testing-skills-with-subagents.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 86dd6dbe..eadba58b 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -351,6 +351,18 @@ "authentication": "ON_INSTALL" }, "category": "MCP" + }, + { + "name": "superpowers-zh", + "source": { + "source": "local", + "path": "./plugins/codex/plugins/superpowers-zh" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "开发工具" } ] } diff --git a/README.md b/README.md index da6e657b..e60fd1d6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ codex plugin add @eapil-skill-market 使用 `codex plugin list --marketplace eapil-skill-market` 输出里的精确 `plugin-id`。不要猜测中文名、翻译名或 display name。 +同一插件的原版与本地化版本可能同时存在。它们的展示名可以相近,但 `plugin-id` 必须不同;安装前应通过列表确认语言版本和精确 ID。 + 安装公共 skill 或第三方 Codex plugin 时也使用同一套命令;具体可安装项以 `codex plugin list --marketplace eapil-skill-market` 的实时输出为准。 有些插件是 skill 集合,安装一个 `plugin-id` 后会同时提供多个 `$skill-name`。可安装项和安装后可用的 skills 均以 Codex 的实际输出为准。 diff --git a/config/external-sources.json b/config/external-sources.json index f33b8ac3..e60ca3a1 100644 --- a/config/external-sources.json +++ b/config/external-sources.json @@ -46,6 +46,30 @@ "writing-skills": "创建、修改或验证 Agent skill 时使用,确保 skill 可触发、可维护、可测试。" } }, + { + "id": "superpowers-zh", + "repo": "https://github.com/AreChen/superpowers-zh.git", + "ref": "main", + "adapter": "codex-plugin", + "pluginName": "superpowers-zh", + "category": "开发工具", + "sourcePath": ".", + "include": [ + ".codex-plugin", + "skills", + "assets", + "README.md", + "LICENSE", + "CODE_OF_CONDUCT.md" + ], + "manifestOverrides": { + "name": "superpowers-zh", + "interface": { + "developerName": "AreChen / Jesse Vincent", + "category": "开发工具" + } + } + }, { "id": "oh-my-codex", "repo": "https://github.com/Yeachan-Heo/oh-my-codex.git", diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index d2fa1ac8..84f9d6bc 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -98,6 +98,15 @@ "adapter": "skill-collection", "commit": "41b823880d688b3e9f9aae3323b08ddd457240c2", "syncedAt": "2026-07-13T16:00:00Z" + }, + { + "id": "superpowers-zh", + "pluginName": "superpowers-zh", + "repo": "https://github.com/AreChen/superpowers-zh.git", + "ref": "main", + "adapter": "codex-plugin", + "commit": "c51f23adcd482fd908aa60928f2ece34d12f7768", + "syncedAt": "2026-07-14T02:27:30Z" } ] } diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index 9654e912..fb5be88f 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -351,6 +351,18 @@ "authentication": "ON_INSTALL" }, "category": "MCP" + }, + { + "name": "superpowers-zh", + "source": { + "source": "local", + "path": "./plugins/superpowers-zh" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "开发工具" } ] } diff --git a/plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json b/plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json new file mode 100644 index 00000000..b07cb6e7 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json @@ -0,0 +1,48 @@ +{ + "name": "superpowers-zh", + "version": "6.1.1-zh.1", + "description": "面向 Agent 的中文技能框架与软件开发方法论:规划、TDD、系统化调试和协作工作流。", + "author": { + "name": "Jesse Vincent", + "email": "jesse@fsck.com", + "url": "https://github.com/obra" + }, + "homepage": "https://github.com/AreChen/superpowers-zh", + "repository": "https://github.com/AreChen/superpowers-zh", + "license": "MIT", + "keywords": [ + "brainstorming", + "subagent-driven-development", + "skills", + "planning", + "tdd", + "debugging", + "code-review", + "workflow" + ], + "skills": "./skills/", + "hooks": {}, + "interface": { + "displayName": "Superpowers 中文版", + "shortDescription": "面向编程 Agent 的规划、TDD、调试与交付工作流", + "longDescription": "使用 Superpowers 中文版引导 Agent 完成头脑风暴、实施规划、测试驱动开发、系统化调试、并行执行、代码审查和分支收尾工作流。", + "developerName": "AreChen / Jesse Vincent", + "category": "开发工具", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "defaultPrompt": [ + "我有一个想实现的点子。", + "让我们给这个项目添加一个功能。" + ], + "websiteURL": "https://github.com/AreChen/superpowers-zh", + "privacyPolicyURL": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement", + "termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service", + "brandColor": "#F59E0B", + "composerIcon": "./assets/superpowers-small.svg", + "logo": "./assets/app-icon.png", + "screenshots": [] + } +} diff --git a/plugins/codex/plugins/superpowers-zh/CODE_OF_CONDUCT.md b/plugins/codex/plugins/superpowers-zh/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f6422545 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +jesse@primeradiant.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/plugins/codex/plugins/superpowers-zh/LICENSE b/plugins/codex/plugins/superpowers-zh/LICENSE new file mode 100644 index 00000000..abf03903 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Jesse Vincent + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/codex/plugins/superpowers-zh/README.md b/plugins/codex/plugins/superpowers-zh/README.md new file mode 100644 index 00000000..f16e76a4 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/README.md @@ -0,0 +1,310 @@ +# Superpowers 中文版 + +Superpowers 是一套面向编程 Agent 的完整软件开发方法论。它由一组可组合的技能和启动指令构成,确保 Agent 能在合适的时机主动调用这些技能。 + +本项目是 [obra/superpowers](https://github.com/obra/superpowers) 的中文分叉版本,主要面向使用 Codex、OpenCode 的中文用户,同时兼容 Windows、Linux 和 macOS。 + +## 版本对齐 + +- **当前中文发行版:** `v6.1.1-zh.1` +- **对齐的上游正式版:** [`obra/superpowers v6.1.1`](https://github.com/obra/superpowers/releases/tag/v6.1.1) +- **上游基线提交:** [`d884ae0`](https://github.com/obra/superpowers/commit/d884ae04edebef577e82ff7c4e143debd0bbec99) +- **对齐日期:** 2026-07-13 + +版本号中的 `zh.1` 表示:功能基线与上游 `v6.1.1` 对齐,这是该基线上的第 1 个中文发行版。后续同步新的上游版本时,会先更新前三段版本号,再从 `zh.1` 重新开始计数。 + +## 上游正在招聘 + +Superpowers 上游团队正在招聘一名全职工程师,协助社区运营与代码开发。 + +职位详情:https://primeradiant.com/jobs/superpowers-community-engineer/ + +如果你认识合适的人选,欢迎推荐给上游团队。 + +## 快速开始 + +为你的编程 Agent 安装 Superpowers 中文版:[Claude Code](#claude-code)、[Antigravity](#antigravity)、[Codex App](#codex-app)、[Codex CLI](#codex-cli)、[Cursor](#cursor)、[Factory Droid](#factory-droid)、[GitHub Copilot CLI](#github-copilot-cli)、[Kimi Code](#kimi-code)、[OpenCode](#opencode)、[Pi](#pi)。 + +## 工作原理 + +Superpowers 从你启动编程 Agent 的那一刻开始发挥作用。当 Agent 发现你准备构建某项功能时,它不会立刻埋头写代码,而是先退一步,询问你真正想解决的问题。 + +Agent 会通过对话逐步梳理出规格,并以便于阅读和确认的小段内容向你展示。 + +设计获批后,Agent 会编写一份足够清晰的实施计划:即使交给一名缺少项目背景、工程判断欠佳且不喜欢测试的初级工程师,也能据此完成任务。计划强调真正的红灯—绿灯 TDD、YAGNI(你不会需要它)和 DRY。 + +当你确认“开始”后,Agent 会启动由子 Agent 驱动的开发流程。不同 Agent 会依次完成各项工程任务,检查并审查彼此的工作,然后继续推进。只要计划足够清晰,Agent 连续自主工作数小时而不偏离目标并不罕见。 + +系统还包含更多细节,但以上就是核心流程。由于技能会自动触发,你不需要记忆特殊命令——安装后,你的编程 Agent 就拥有了 Superpowers。 + +## 商业服务 + +如果你在企业环境中使用 Superpowers,并需要商业支持、额外工具或托管式成本管理,可联系上游团队:sales@primeradiant.com。 + +## 安装 + +不同编程 Agent 的安装方式不同。如果你同时使用多个 Agent,需要分别安装。 + +### Claude Code + +推荐直接注册本中文仓库提供的插件市场: + +```bash +/plugin marketplace add AreChen/superpowers-zh +``` + +然后安装中文插件: + +```bash +/plugin install superpowers@superpowers-dev +``` + +如果你希望安装上游英文版,也可以使用 [Claude 官方插件市场](https://claude.com/plugins/superpowers): + +```bash +/plugin install superpowers@claude-plugins-official +``` + +### Antigravity + +直接从本仓库安装插件: + +```bash +agy plugin install https://github.com/AreChen/superpowers-zh +``` + +Antigravity 会运行插件的会话启动钩子,因此 Superpowers 会从第一条消息起生效。更新时重新执行同一条命令即可。 + +### Codex App + +先通过 Codex CLI 添加本项目的 Git 插件市场: + +```bash +codex plugin marketplace add AreChen/superpowers-zh --ref main +``` + +然后: + +1. 重启 Codex App。 +2. 在侧边栏打开“插件”。 +3. 选择 `Superpowers 中文版开发版` 市场。 +4. 找到 `Superpowers 中文版`,点击旁边的 `+` 并按提示安装。 + +### Codex CLI + +先添加本项目的插件市场: + +```bash +codex plugin marketplace add AreChen/superpowers-zh --ref main +``` + +打开插件界面: + +```text +/plugins +``` + +选择 `Superpowers 中文版开发版` 市场,搜索 `superpowers` 并安装插件。 + +查看已配置的市场: + +```bash +codex plugin marketplace list +``` + +更新市场: + +```bash +codex plugin marketplace upgrade superpowers-dev +``` + +### Cursor + +在 Cursor Agent 对话中从插件市场安装: + +```text +/add-plugin superpowers +``` + +也可以在插件市场中搜索 `superpowers`。请确认安装详情显示的是 `Superpowers 中文版`;如果只看到上游英文版,可改用本仓库的本地插件方式。 + +### Factory Droid + +注册本中文仓库的插件市场: + +```bash +droid plugin marketplace add https://github.com/AreChen/superpowers-zh +``` + +安装插件: + +```bash +droid plugin install superpowers@superpowers-dev +``` + +### GitHub Copilot CLI + +注册本中文仓库的插件市场: + +```bash +copilot plugin marketplace add AreChen/superpowers-zh +``` + +安装插件: + +```bash +copilot plugin install superpowers@superpowers-dev +``` + +### Kimi Code + +可以直接从本中文仓库安装: + +```text +/plugins install https://github.com/AreChen/superpowers-zh +``` + +也可以打开 Kimi Code 的插件管理器: + +```text +/plugins +``` + +进入 `Marketplace`,搜索并安装 `Superpowers`。请在安装前确认插件描述为中文版本。 + +详细说明:[docs/README.kimi.md](docs/README.kimi.md) + +### OpenCode + +OpenCode 使用自己的插件安装机制。即使你已经在其他编程 Agent 中安装了 Superpowers,仍需为 OpenCode 单独安装。 + +在全局或项目级 `opencode.json` 的 `plugin` 数组中加入: + +```json +{ + "plugin": [ + "superpowers@git+https://github.com/AreChen/superpowers-zh.git" + ] +} +``` + +重启 OpenCode。插件会自动注册本项目中的全部技能,无需手动为技能目录创建符号链接。 + +也可以让 OpenCode 获取并遵循仓库内的安装说明: + +```text +获取并遵循 https://raw.githubusercontent.com/AreChen/superpowers-zh/refs/heads/main/.opencode/INSTALL.md 中的说明 +``` + +详细说明:[docs/README.opencode.md](docs/README.opencode.md) + +### Pi + +从本仓库安装为 Pi 软件包: + +```bash +pi install git:github.com/AreChen/superpowers-zh +``` + +本地开发时,可以把当前检出目录作为临时软件包加载: + +```bash +pi -e /path/to/superpowers-zh +``` + +Pi 软件包会加载 Superpowers 技能,并通过一个小型扩展在会话启动和上下文压缩后注入 `using-superpowers` 引导。Pi 原生支持技能,因此不需要兼容性的 `Skill` 工具。子 Agent 和任务列表工具仍可作为可选的 Pi 配套软件包使用。 + +## 基本工作流 + +1. **brainstorming** —— 在编写代码前触发。通过提问澄清初步想法、探索替代方案,分段展示设计供用户确认,并保存设计文档。 + +2. **using-git-worktrees** —— 设计获批后触发。在新分支上创建隔离工作区,完成项目初始化,并验证测试基线干净。 + +3. **writing-plans** —— 获得批准的设计后触发。把工作拆分为小型任务,每项任务通常需要 2–5 分钟,并包含准确的文件路径、完整代码和验证步骤。 + +4. **subagent-driven-development** 或 **executing-plans** —— 计划完成后触发。前者为每项任务派遣新的子 Agent,并进行规格符合性和代码质量两阶段审查;后者按批次执行,并在关键节点等待人工确认。 + +5. **test-driven-development** —— 实施期间触发。强制执行“红灯—绿灯—重构”:先写失败测试并确认失败,再编写最小实现并确认通过,然后提交。测试之前编写的生产代码需要删除并重新按 TDD 实现。 + +6. **requesting-code-review** —— 在任务之间触发。根据计划审查实现,按严重程度报告问题;严重问题会阻止继续推进。 + +7. **finishing-a-development-branch** —— 全部任务完成后触发。验证测试并提供合并、创建 PR、保留或丢弃分支等选项,最后清理 worktree。 + +**Agent 会在执行任何任务前检查是否存在相关技能。** 这些是必须遵循的工作流,而不是可选建议。 + +## 包含内容 + +### 技能库 + +**测试** + +- **test-driven-development** —— 红灯—绿灯—重构循环,包含测试反模式参考。 + +**调试** + +- **systematic-debugging** —— 四阶段根因分析流程,包含根因追踪、纵深防御和基于条件的等待技术。 +- **verification-before-completion** —— 在声称完成前用证据确认问题确实已经解决。 + +**协作** + +- **brainstorming** —— 通过苏格拉底式提问完善设计。 +- **writing-plans** —— 编写详细实施计划。 +- **executing-plans** —— 带检查点的分批执行。 +- **dispatching-parallel-agents** —— 并行子 Agent 工作流。 +- **requesting-code-review** —— 请求审查前的完整流程。 +- **receiving-code-review** —— 理解并处理审查反馈。 +- **using-git-worktrees** —— 使用隔离分支并行开发。 +- **finishing-a-development-branch** —— 合并或创建 PR 的决策流程。 +- **subagent-driven-development** —— 通过规格符合性和代码质量两阶段审查快速迭代。 + +**元技能** + +- **writing-skills** —— 按最佳实践创建和测试新技能。 +- **using-superpowers** —— 技能系统的入口和使用规则。 + +## 核心理念 + +- **测试驱动开发** —— 始终先写测试。 +- **系统化优于临时应对** —— 遵循流程,不靠猜测。 +- **降低复杂度** —— 把简单作为首要目标。 +- **证据优于声明** —— 声称成功之前先验证。 + +参阅 [Superpowers 最初的发布公告](https://blog.fsck.com/2025/10/09/superpowers/)。 + +## 参与贡献 + +本项目跟随上游 Superpowers 的总体贡献原则。通常不接受随意新增技能;任何技能修改都必须能在项目支持的编程 Agent 和主要操作系统上正常工作。 + +1. Fork [AreChen/superpowers-zh](https://github.com/AreChen/superpowers-zh)。 +2. 同步最新的 `main` 分支。 +3. 为你的改动创建独立分支。 +4. 创建或修改技能时,遵循 `writing-skills` 技能完成编写和测试。 +5. 向 `main` 提交 PR,并完整填写 PR 模板。 + +技能行为测试使用 [superpowers-evals](https://github.com/prime-radiant-inc/superpowers-evals/) 中的 drill 评测框架,将其克隆到 `evals/` 后可按 `evals/README.md` 配置。插件基础设施测试位于 `tests/`,通过对应的 `run-*.sh` 或 `npm test` 运行。 + +完整指南见 [`skills/writing-skills/SKILL.md`](skills/writing-skills/SKILL.md)。 + +## 更新 + +Superpowers 的更新方式取决于你使用的编程 Agent。通过 Git 市场安装时,通常可以使用对应 Agent 的市场更新命令;通过仓库 URL 安装时,可重新执行安装命令或拉取最新的 `main` 分支。 + +## 许可证 + +本项目采用 MIT 许可证,详情见 [LICENSE](LICENSE)。 + +## Visual Companion 遥测 + +技能和插件通常不会向作者提供任何使用反馈,因此我们无法直接了解有多少人在使用 Superpowers。默认情况下,`brainstorming` 的可选 Visual Companion 会从 Prime Radiant 网站加载品牌图标,并附带当前 Superpowers 版本号。 + +该请求不包含你的项目、提示词或编程 Agent 的详细信息,也不会记录点击行为或你正在构建的内容。它只用于粗略了解使用人数和版本分布,并且完全可选。 + +如需关闭,请把环境变量 `SUPERPOWERS_DISABLE_TELEMETRY` 设置为任意真值。Superpowers 也会遵循 Claude Code 的 `DISABLE_TELEMETRY` 和 `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` 退出设置。 + +## 社区 + +Superpowers 由 [Jesse Vincent](https://blog.fsck.com) 和 [Prime Radiant](https://primeradiant.com) 团队创建。本中文分叉由 [AreChen](https://github.com/AreChen) 维护。 + +- **Discord**:[加入上游社区](https://discord.gg/35wsABTejz),获取支持、提出问题并分享你使用 Superpowers 构建的项目。 +- **问题反馈**:https://github.com/AreChen/superpowers-zh/issues +- **上游版本公告**:[订阅通知](https://primeradiant.com/superpowers/) diff --git a/plugins/codex/plugins/superpowers-zh/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/superpowers-zh/THIRD_PARTY_SOURCE.json new file mode 100644 index 00000000..01368767 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/THIRD_PARTY_SOURCE.json @@ -0,0 +1,9 @@ +{ + "sourceId": "superpowers-zh", + "repo": "https://github.com/AreChen/superpowers-zh.git", + "ref": "main", + "commit": "c51f23adcd482fd908aa60928f2ece34d12f7768", + "adapter": "codex-plugin", + "sourcePath": ".", + "syncedAt": "2026-07-14T02:27:30Z" +} diff --git a/plugins/codex/plugins/superpowers-zh/assets/app-icon.png b/plugins/codex/plugins/superpowers-zh/assets/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..25518da4625e850d97b15a9e0a27e7115969dc0d GIT binary patch literal 48231 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 \ No newline at end of file diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/SKILL.md b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/SKILL.md new file mode 100644 index 00000000..c00f5a9e --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/SKILL.md @@ -0,0 +1,155 @@ +--- +name: brainstorming +description: "在进行任何创造性工作之前,你都必须使用此技能——包括创建功能、构建组件、添加功能或修改行为。在实现之前探索用户意图、需求和设计。" +--- + +# 通过头脑风暴将想法转化为设计 + +通过自然的协作式对话,帮助将想法转化为完整成形的设计和规格说明。 + +首先了解当前项目的上下文,然后一次提出一个问题来完善想法。一旦你理解了要构建的内容,就展示设计并获得用户批准。 + + +在你展示设计并获得用户批准之前,不得调用任何实现技能、编写任何代码、搭建任何项目脚手架或采取任何实现行动。无论项目看起来多么简单,这都适用于每一个项目。 + + +## 反模式:“这太简单了,不需要设计” + +每个项目都要经过这一流程。待办事项列表、单函数实用工具、配置变更——无一例外。“简单”项目最容易因未经审视的假设而造成最多的工作浪费。设计可以很简短(对于真正简单的项目,只需几句话),但你必须展示设计并获得批准。 + +## 清单 + +你必须为以下每一项创建一项任务,并按顺序完成它们: + +1. **探索项目上下文**——检查文件、文档和最近的提交 +2. **在恰当时机提供视觉伴侣**——不要预先提供。当某个问题第一次确实用展示比用描述更清楚时,就在那时提出使用它(用一条单独的消息);获得批准后,它的浏览器标签页会为你打开。如果始终没有出现视觉问题,就绝不要提出使用它。参见下方的“视觉伴侣”一节。 +3. **提出澄清问题**——一次一个,了解目的、约束和成功标准 +4. **提出 2-3 种方案**——说明权衡取舍和你的建议 +5. **展示设计**——按复杂度划分并调整各节内容,每展示一节后都要获得用户批准 +6. **编写设计文档**——保存到 `docs/superpowers/specs/YYYY-MM-DD--design.md` 并提交 +7. **规格说明自审**——快速进行内联检查,查找占位符、矛盾、歧义和范围问题(见下文) +8. **用户审查已写好的规格说明** — 请用户在继续之前审查规格说明文件 +9. **过渡到实现** — 调用 writing-plans 技能来创建实现计划 + +## 流程 + +```dot +digraph brainstorming { + "Explore project context" [shape=box, label="探索项目上下文"]; + "Ask clarifying questions" [shape=box, label="提出澄清问题"]; + "Propose 2-3 approaches" [shape=box, label="提出 2-3 种方案"]; + "Present design sections" [shape=box, label="分节呈现设计"]; + "User approves design?" [shape=diamond, label="用户批准设计了吗?"]; + "Write design doc" [shape=box, label="编写设计文档"]; + "Spec self-review\n(fix inline)" [shape=box, label="规格说明自审\n(内联修正)"]; + "User reviews spec?" [shape=diamond, label="用户审查规格说明了吗?"]; + "Invoke writing-plans skill" [shape=doublecircle, label="调用 writing-plans 技能"]; + + "Explore project context" -> "Ask clarifying questions"; + "Ask clarifying questions" -> "Propose 2-3 approaches"; + "Propose 2-3 approaches" -> "Present design sections"; + "Present design sections" -> "User approves design?"; + "User approves design?" -> "Present design sections" [label="否,修改"]; + "User approves design?" -> "Write design doc" [label="是"]; + "Write design doc" -> "Spec self-review\n(fix inline)"; + "Spec self-review\n(fix inline)" -> "User reviews spec?"; + "User reviews spec?" -> "Write design doc" [label="要求更改"]; + "User reviews spec?" -> "Invoke writing-plans skill" [label="已批准"]; +} +``` +**终止状态是调用 writing-plans。** 绝不要调用 frontend-design、mcp-builder 或任何其他实现技能。在 brainstorming 之后,你调用的唯一技能是 writing-plans。 + +## 流程 + +**理解想法:** + +- 首先查看当前项目状态(文件、文档、最近的提交) +- 在询问详细问题之前,先评估范围:如果请求描述了多个相互独立的子系统(例如,“构建一个包含聊天、文件存储、计费和分析的平台”),应立即指出这一点。不要把问题浪费在细化一个首先需要拆分的项目的细节上。 +- 如果项目规模过大,无法用一份规格说明涵盖,请帮助用户将其拆分为多个子项目:哪些是独立的部分、它们如何关联、应按什么顺序构建?然后按照正常的设计流程,对第一个子项目进行头脑风暴。每个子项目都有其自己的规格说明 → 计划 → 实现周期。 +- 对于范围适当的项目,每次询问一个问题以细化想法 +- 尽可能优先使用多项选择题,但开放式问题也可以 +- 每条消息只问一个问题——如果某个主题需要进一步探索,请将其拆成多个问题 +- 专注于理解:目的、约束、成功标准 + +**探索方案:** + +- 提出 2-3 种不同的方案,并说明各自的权衡 +- 以对话方式呈现选项,并给出你的建议和理由 +- 先介绍你推荐的选项,并解释原因 + +**呈现设计:** + +- 一旦你确信自己理解了要构建的内容,就呈现设计 +- 根据每个部分的复杂程度调整篇幅:如果简单直接,就用几句话;如果细节微妙复杂,则最多使用 200-300 词 +- 在每个部分之后询问目前看来是否正确 +- 涵盖:架构、组件、数据流、错误处理、测试 +- 如果有些内容不合理,随时准备返回并澄清 + +**为隔离性和清晰性而设计:** +- 将系统拆分为更小的单元,每个单元都有一个明确的用途,通过定义清晰的接口进行通信,并且可以被独立理解和测试 +- 对于每个单元,你都应该能够回答:它做什么、如何使用它,以及它依赖什么? +- 其他人能否在不阅读某个单元内部实现的情况下理解它做什么?你能否在不破坏使用方的情况下更改其内部实现?如果不能,就需要改进边界。 +- 更小且边界清晰的单元也更便于你处理——对于能够一次性纳入上下文的代码,你可以进行更好的推理;当文件职责集中时,你的编辑也会更可靠。当一个文件变得很大时,这通常表明它承担了过多职责。 + +**在现有代码库中工作:** + +- 在提出更改之前,先探索当前结构。遵循现有模式。 +- 如果现有代码存在影响这项工作的问题(例如,文件变得过大、边界不清晰、职责纠缠),应将有针对性的改进纳入设计——就像优秀的开发者会改进自己正在处理的代码一样。 +- 不要提出无关的重构。始终专注于服务当前目标的内容。 + +## 设计之后 + +**文档:** + +- 将经过验证的设计(规格说明)写入 `docs/superpowers/specs/YYYY-MM-DD--design.md` + - (用户对规格说明位置的偏好会覆盖此默认设置) +- 如果可用,使用 elements-of-style:writing-clearly-and-concisely 技能 +- 将设计文档提交到 git + +**规格说明自审:** +写完规格说明文档后,以全新的视角审视它: + +1. **占位符扫描:** 是否有任何 "TBD"、"TODO"、未完成的章节或模糊的要求?修复它们。 +2. **内部一致性:** 是否有任何章节相互矛盾?架构是否与功能描述一致? +3. **范围检查:** 范围是否足够聚焦,可以用单个实现计划完成,还是需要进行拆分? +4. **歧义检查:** 是否有任何要求可能被理解为两种不同的含义?如果有,选择一种并明确写出。 + +直接修复所有问题。无需再次审查——修复后继续推进。 +**用户审查关卡:** +规格审查循环通过后,在继续之前,请用户审查已写好的规格说明: + +> “规格说明已写入 `` 并提交。请审查它,并告诉我在我们开始编写实施计划之前,你是否希望进行任何更改。” + +等待用户回复。如果他们要求更改,就进行更改并重新运行规格审查循环。只有在用户批准后才能继续。 + +**实施:** + +- 调用 writing-plans 技能来创建详细的实施计划 +- 不要调用任何其他技能。writing-plans 是下一步。 + +## 关键原则 + +- **一次只问一个问题** - 不要用多个问题让用户不知所措 +- **优先使用选择题** - 条件允许时,选择题比开放式问题更容易回答 +- **坚决贯彻 YAGNI** - 从所有设计中移除不必要的功能 +- **探索替代方案** - 在定案前始终提出 2-3 种方案 +- **增量验证** - 展示设计,获得批准后再继续 +- **保持灵活** - 当某些内容不合理时,返回并加以澄清 + +## 可视化伴侣 + +一个基于浏览器的伴侣工具,用于在头脑风暴期间展示原型图、图表和可视化选项。它作为工具提供——而不是一种模式。同意使用该伴侣工具,意味着它可用于那些采用可视化方式会更有帮助的问题;这并不意味着每个问题都要通过浏览器进行。 + +**提供伴侣工具(即时):** 不要一开始就提出使用它。等到某个问题确实用展示的方式会比口头描述更清楚时——真正涉及原型图 / 布局 / 图表的问题,而不仅仅是一个 UI *话题*。第一次出现这种情况时,就在那时提出使用它,并且该提议要单独作为一条消息: +> “接下来的这部分如果展示给你看,可能会更容易理解——在我们推进的过程中,我可以在浏览器标签页中制作原型图、图表和对比内容。它仍是新功能,并且可能会消耗大量 token。你想让我这样做吗?我会为你打开它。” + +**此提议必须单独作为一条消息发送。** 只发送该提议——不要包含澄清问题、摘要或任何其他内容。等待用户回复。如果他们接受,使用 `--open` 启动服务器,以便他们的浏览器自动打开第一个界面。如果他们拒绝,则继续仅使用文本,并且不要再次提出使用它,除非他们主动提起。 +**逐问题决策:** 即使用户接受了,也要针对每个问题分别决定使用浏览器还是终端。判断标准是:**相比阅读,用户通过观看是否能更好地理解这一点?** + +- **使用浏览器**呈现确实属于视觉内容的内容——模型图、线框图、布局对比、架构图、并排的视觉设计 +- **使用终端**呈现文本内容——需求问题、概念性选择、权衡清单、A/B/C/D 文本选项、范围决策 + +关于 UI 主题的问题并不自动等同于视觉问题。“在此上下文中,个性意味着什么?”是一个概念性问题——使用终端。“哪种向导布局效果更好?”是一个视觉问题——使用浏览器。 + +如果他们同意使用视觉伴侣,请在继续之前阅读详细指南: +`skills/brainstorming/visual-companion.md` diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/frame-template.html b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/frame-template.html new file mode 100644 index 00000000..b1cae3f1 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/frame-template.html @@ -0,0 +1,213 @@ + + + + + Superpowers 头脑风暴 + + + +
+ +
正在连接…
+
+ +
+
+ +
+
+ + + diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/helper.js b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/helper.js new file mode 100644 index 00000000..7987b3af --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/helper.js @@ -0,0 +1,167 @@ +(function() { + const MIN_RECONNECT_MS = 500; + const MAX_RECONNECT_MS = 30000; + const TOMBSTONE_AFTER_MS = 15000; // show the "paused" overlay after this long disconnected + + // Pure: next backoff delay (doubles, capped). Exported for unit tests. + function nextReconnectDelay(current, max) { + return Math.min(current * 2, max); + } + if (typeof module !== 'undefined' && module.exports) { + module.exports = { nextReconnectDelay, MIN_RECONNECT_MS, MAX_RECONNECT_MS, TOMBSTONE_AFTER_MS }; + } + + // Everything below is browser-only; bail out when loaded in Node (tests). + if (typeof window === 'undefined') return; + + let ws = null; + let eventQueue = []; + let reconnectDelay = MIN_RECONNECT_MS; + let reconnectTimer = null; + let disconnectedSince = null; + let everConnected = false; + let tombstoneShown = false; + + function sessionKey() { + try { + return window.sessionStorage && window.sessionStorage.getItem('brainstorm-session-key'); + } catch (e) {} + return null; + } + + function websocketUrl() { + const key = sessionKey(); + return 'ws://' + window.location.host + (key ? '/?key=' + encodeURIComponent(key) : ''); + } + + function reloadAfterRecovery() { + const key = sessionKey(); + if (key) { + window.location.replace('/?key=' + encodeURIComponent(key)); + } else { + window.location.reload(); + } + } + + // Reflect connection state in the frame's status pill (absent on full-doc screens). + function setStatus(state) { + const el = document.querySelector('.status'); + if (!el) return; + const map = { + connecting: ['正在连接…', 'var(--text-tertiary)'], + connected: ['已连接', 'var(--success)'], + reconnecting: ['正在重新连接…', 'var(--warning)'], + disconnected: ['已断开连接', 'var(--error)'] + }; + const [text, color] = map[state] || map.disconnected; + el.textContent = text; + el.style.setProperty('--status-color', color); + } + + // Self-styled so it works on framed and full-document screens alike. + function showTombstone() { + if (tombstoneShown) return; + tombstoneShown = true; + const el = document.createElement('div'); + el.id = 'bs-tombstone'; + el.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;' + + 'align-items:center;justify-content:center;padding:2rem;text-align:center;' + + 'background:rgba(20,20,22,0.92);color:#f5f5f7;font-family:system-ui,sans-serif'; + el.innerHTML = '
' + + '

伴侣已暂停

' + + '

头脑风暴伴侣已停止。' + + '请让你的编程 Agent 重新启动它——本页面会自动重新连接。

'; + if (document.body) document.body.appendChild(el); + } + + function connect() { + if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } + setStatus(everConnected ? 'reconnecting' : 'connecting'); + ws = new WebSocket(websocketUrl()); + + ws.onopen = () => { + const recovered = tombstoneShown; + everConnected = true; + disconnectedSince = null; + reconnectDelay = MIN_RECONNECT_MS; + tombstoneShown = false; + setStatus('connected'); + eventQueue.forEach(e => ws.send(JSON.stringify(e))); + eventQueue = []; + // Recovered from a tombstoned outage (e.g. the server restarted on the same + // port) — reload through the keyed bootstrap when possible so the cookie is + // refreshed before the visible URL returns to bare /. + if (recovered) reloadAfterRecovery(); + }; + + ws.onmessage = (msg) => { + let data; + try { data = JSON.parse(msg.data); } catch (e) { return; } + if (data.type === 'reload') window.location.reload(); + }; + + ws.onclose = () => { + ws = null; + if (disconnectedSince === null) disconnectedSince = Date.now(); + if (Date.now() - disconnectedSince >= TOMBSTONE_AFTER_MS) { + setStatus('disconnected'); + showTombstone(); + } else { + setStatus('reconnecting'); + } + reconnectTimer = setTimeout(connect, reconnectDelay); + reconnectDelay = nextReconnectDelay(reconnectDelay, MAX_RECONNECT_MS); + }; + + // Let onclose own reconnection so we don't schedule it twice. + ws.onerror = () => { try { ws.close(); } catch (e) {} }; + } + + function sendEvent(event) { + event.timestamp = Date.now(); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(event)); + } else { + eventQueue.push(event); + } + } + + // Capture clicks on choice elements + 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 + }); + + }); + + // Frame UI: selection tracking + 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; + }; + + // Expose API for explicit use + window.brainstorm = { + send: sendEvent, + choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata }) + }; + + connect(); +})(); diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs new file mode 100644 index 00000000..91fb8796 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs @@ -0,0 +1,723 @@ +const crypto = require('crypto'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// ========== WebSocket Protocol (RFC 6455) ========== + +const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A }; +const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024; + +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('Client frames must be masked'); + + 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; + const extendedLen = buffer.readBigUInt64BE(2); + if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) { + throw new Error('WebSocket frame payload exceeds maximum allowed size'); + } + payloadLen = Number(extendedLen); + offset = 10; + } + + if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) { + throw new Error('WebSocket frame payload exceeds maximum allowed size'); + } + + 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 }; +} + +// ========== Configuration ========== + +const PORT_FILE = process.env.BRAINSTORM_PORT_FILE || null; +const randomPort = () => 49152 + Math.floor(Math.random() * 16383); +// Prefer an explicit port, else the port this session last bound (so a restart +// reuses it and an already-open browser tab reconnects), else a random high port. +function preferredPort() { + if (process.env.BRAINSTORM_PORT) return Number(process.env.BRAINSTORM_PORT); + if (PORT_FILE) { + try { + const p = Number(fs.readFileSync(PORT_FILE, 'utf-8').trim()); + if (Number.isInteger(p) && p > 1023 && p < 65536) return p; + } catch (e) { /* no prior port recorded */ } + } + return randomPort(); +} +let PORT = preferredPort(); +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'); +const SUPERPOWERS_VERSION = readSuperpowersVersion(); +const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png'; +const TELEMETRY_DISABLE_ENV_VARS = [ + 'SUPERPOWERS_DISABLE_TELEMETRY', + 'DISABLE_TELEMETRY', + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC' +]; +const SUPERPOWERS_TELEMETRY_DISABLED = TELEMETRY_DISABLE_ENV_VARS.some(name => isTruthyEnv(process.env[name])); +let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; + +// Per-session secret key. The companion is reachable by any local browser tab +// and, when bound to a non-loopback host, by any host that can route to it. +// The key authenticates the real client uniformly across loopback, tunnel, and +// remote binds — and defeats DNS rebinding — where a Host/Origin allowlist +// cannot. It rides the served URL as ?key= and is mirrored into a cookie on +// first load so same-origin subresources and the WebSocket carry it for free. +// Persisted alongside the port (BRAINSTORM_TOKEN_FILE) so a restart keeps the +// same key and an already-open tab's cookie still validates. +const TOKEN_FILE = process.env.BRAINSTORM_TOKEN_FILE || null; +function generateToken() { + return crypto.randomBytes(32).toString('hex'); +} + +function chmodOwnerOnly(file) { + try { fs.chmodSync(file, 0o600); } catch (e) { /* best effort */ } +} + +function initialToken() { + if (process.env.BRAINSTORM_TOKEN) { + return { value: process.env.BRAINSTORM_TOKEN, source: 'env' }; + } + if (TOKEN_FILE) { + try { + const t = fs.readFileSync(TOKEN_FILE, 'utf-8').trim(); + if (/^[0-9a-f]{32,}$/i.test(t)) { + chmodOwnerOnly(TOKEN_FILE); + return { value: t, source: 'file' }; + } + } catch (e) { /* no prior token recorded */ } + } + return { value: generateToken(), source: 'generated' }; +} + +const tokenInfo = initialToken(); +let TOKEN = tokenInfo.value; +let tokenSource = tokenInfo.source; +let COOKIE_NAME = 'brainstorm-key-' + PORT; // refined to the actual bound port in onListen + +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' +}; + +// ========== Templates and Constants ========== + +function waitingPage() { + return renderBranding(` + +头脑风暴伴侣 + + +

头脑风暴伴侣

+

正在等待 Agent 推送界面…

`); +} + +const FORBIDDEN_PAGE = ` + +需要会话密钥 + + +

需要会话密钥

+

本页面需要编程 Agent 提供的完整 URL,其中必须包含 +?key=… 部分。请复制完整 URL 后重新打开。

`; + +function bootstrapPage(key) { + const jsonKey = JSON.stringify(String(key)); + return ` + +正在打开头脑风暴伴侣 + + + +`; +} + +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 = ''; + +// ========== Helper Functions ========== + +function readSuperpowersVersion() { + const root = path.join(__dirname, '../../..'); + const manifests = [ + path.join(root, 'package.json'), + path.join(root, '.codex-plugin/plugin.json') + ]; + + for (const manifest of manifests) { + try { + const data = JSON.parse(fs.readFileSync(manifest, 'utf-8')); + if (data.version) return String(data.version); + } catch (e) { + // Packaged Codex plugins omit package.json; try the next manifest. + } + } + + return 'unknown'; +} + +function isTruthyEnv(value) { + if (!value) return false; + const normalized = String(value).trim().toLowerCase(); + if (!normalized) return false; + return !['0', 'false', 'no', 'off'].includes(normalized); +} + +function escapeHtmlText(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function brandMarkup() { + const version = escapeHtmlText(SUPERPOWERS_VERSION); + const text = SUPERPOWERS_TELEMETRY_DISABLED + ? 'Prime Radiant Superpowers v' + version + : 'Superpowers v' + version; + const logo = SUPERPOWERS_TELEMETRY_DISABLED + ? '' + : ''; + + return ''; +} + +function renderBranding(html) { + return html.split('').join(brandMarkup()); +} + +function isFullDocument(html) { + const trimmed = html.trimStart().toLowerCase(); + return trimmed.startsWith('', content); +} + +function getNewestScreen() { + const files = fs.readdirSync(CONTENT_DIR) + .filter(f => !f.startsWith('.') && f.endsWith('.html')) + .map(f => { + const fp = path.join(CONTENT_DIR, f); + if (!isRegularFileInsideContentDir(fp)) return null; + return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; + }) + .filter(Boolean) + .sort((a, b) => b.mtime - a.mtime); + return files.length > 0 ? files[0].path : null; +} + +function urlHostForHttp(host) { + const h = String(host); + if (h.startsWith('[') && h.endsWith(']')) return h; + return h.includes(':') ? '[' + h + ']' : h; +} + +function companionUrl() { + return 'http://' + urlHostForHttp(URL_HOST) + ':' + PORT + '/?key=' + TOKEN; +} + +function browserLauncherForPlatform(url, { + platform = process.platform, + osRelease = require('os').release(), + env = process.env +} = {}) { + const isWSL = platform === 'linux' && /microsoft/i.test(osRelease); + if (platform === 'darwin') return { bin: 'open', args: [url] }; + if (platform === 'win32' || isWSL) { + return { bin: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }; + } + if (env.DISPLAY || env.WAYLAND_DISPLAY) return { bin: 'xdg-open', args: [url] }; + return null; +} + +function isRegularFileInsideContentDir(filePath) { + let stat, realContentDir, realFilePath; + try { + stat = fs.lstatSync(filePath); + if (stat.isSymbolicLink()) return false; + if (!stat.isFile()) return false; + if (stat.nlink !== 1) return false; + realContentDir = fs.realpathSync(CONTENT_DIR); + realFilePath = fs.realpathSync(filePath); + } catch (e) { + return false; + } + return realFilePath.startsWith(realContentDir + path.sep); +} + +// ========== Authentication ========== + +function timingSafeEqualStr(a, b) { + const ab = Buffer.from(String(a)); + const bb = Buffer.from(String(b)); + if (ab.length !== bb.length) return false; + return crypto.timingSafeEqual(ab, bb); +} + +function parseCookies(header) { + const out = {}; + if (!header) return out; + for (const part of header.split(';')) { + const eq = part.indexOf('='); + if (eq < 0) continue; + out[part.slice(0, eq).trim()] = part.slice(eq + 1).trim(); + } + return out; +} + +// A request is authorized if it carries the session key as ?key= or as the +// session cookie. Both are compared in constant time. +function isAuthorized(req) { + const q = req.url.indexOf('?'); + if (q >= 0) { + const params = new URLSearchParams(req.url.slice(q + 1)); + if (params.has('key')) { + const key = params.get('key'); + return Boolean(key && timingSafeEqualStr(key, TOKEN)); + } + } + const cookie = parseCookies(req.headers['cookie'])[COOKIE_NAME]; + if (cookie && timingSafeEqualStr(cookie, TOKEN)) return true; + return false; +} + +function pathnameOf(url) { + const q = url.indexOf('?'); + return q >= 0 ? url.slice(0, q) : url; +} + +function queryKey(url) { + const q = url.indexOf('?'); + if (q < 0) return null; + return new URLSearchParams(url.slice(q + 1)).get('key'); +} + +function securityHeaders(headers = {}) { + return { + 'Referrer-Policy': 'no-referrer', + 'Cache-Control': 'no-store', + 'X-Frame-Options': 'DENY', + 'Content-Security-Policy': "frame-ancestors 'none'", + 'Cross-Origin-Resource-Policy': 'same-origin', + ...headers + }; +} + +function isAllowedWebSocketOrigin(req) { + const origin = req.headers.origin; + if (!origin) return true; + const host = req.headers.host; + if (!host) return false; + return origin === 'http://' + host; +} + +// ========== HTTP Request Handler ========== + +function handleRequest(req, res) { + if (!isAuthorized(req)) { + res.writeHead(403, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(FORBIDDEN_PAGE); + return; + } + touchActivity(); // only authorized requests count as activity + + // Mirror the key into a cookie so same-origin subresources (/files/*) can + // authenticate after bootstrap. HttpOnly keeps it away from page scripts; the + // WebSocket Origin check below is what blocks cross-origin localhost injection. + res.setHeader('Set-Cookie', + COOKIE_NAME + '=' + TOKEN + '; HttpOnly; SameSite=Strict; Path=/'); + + const pathname = pathnameOf(req.url); + const keyFromQuery = queryKey(req.url); + if (req.method === 'GET' && pathname === '/' && keyFromQuery && timingSafeEqualStr(keyFromQuery, TOKEN)) { + res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(bootstrapPage(keyFromQuery)); + } else if (req.method === 'GET' && pathname === '/') { + const screenFile = getNewestScreen(); + let html = screenFile + ? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8')) + : waitingPage(); + + if (html.includes('')) { + html = html.replace('', helperInjection + '\n'); + } else { + html += helperInjection; + } + + res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(html); + } else if (req.method === 'GET' && pathname.startsWith('/files/')) { + const fileName = path.basename(pathname.slice(7)); + const filePath = path.join(CONTENT_DIR, fileName); + // Reject empty/dotfile names and anything that isn't a regular file — + // `/files/` would otherwise resolve to CONTENT_DIR and crash readFileSync (EISDIR). + if (!fileName || fileName.startsWith('.') || !isRegularFileInsideContentDir(filePath)) { + res.writeHead(404, securityHeaders()); + res.end('Not found'); + return; + } + const ext = path.extname(filePath).toLowerCase(); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + res.writeHead(200, securityHeaders({ 'Content-Type': contentType })); + res.end(fs.readFileSync(filePath)); + } else { + res.writeHead(404, securityHeaders()); + res.end('Not found'); + } +} + +// ========== WebSocket Connection Handling ========== + +const clients = new Set(); + +function handleUpgrade(req, socket) { + if (!isAuthorized(req) || !isAllowedWebSocketOrigin(req)) { socket.destroy(); return; } + + 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('Failed to parse WebSocket message:', e.message); + return; + } + touchActivity(); + console.log(JSON.stringify({ source: 'user-event', ...event })); + if (event && 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); } + } +} + +// Best-effort: open the user's browser the first time a screen is actually ready +// to show. Skips when disabled, on a non-loopback (remote) bind, or when a +// browser is already connected. Override the launcher with BRAINSTORM_OPEN_CMD. +let browserOpened = false; +function maybeOpenBrowser() { + if (browserOpened) return; + browserOpened = true; + if (!process.env.BRAINSTORM_OPEN) return; // opt-in: only after the user approves the companion + if (HOST !== '127.0.0.1' && HOST !== 'localhost') return; + if (clients.size > 0) return; // the user already opened it + const url = companionUrl(); // must carry the key or the gate 403s it + const cp = require('child_process'); + // Operator-provided launcher: run as given (this env var is trusted operator input). + if (process.env.BRAINSTORM_OPEN_CMD) { + try { cp.exec(process.env.BRAINSTORM_OPEN_CMD + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ } + return; + } + // Platform launchers: pass the URL as an argv element via execFile (no shell), + // so a url-host containing shell metacharacters can't inject a command. + const launcher = browserLauncherForPlatform(url); + if (!launcher) return; // headless: nothing to open + try { cp.execFile(launcher.bin, launcher.args, () => {}); } catch (e) { /* best effort */ } +} + +// ========== Activity Tracking ========== + +// Idle timeout: shut down after this long with no activity. Default 4 hours; +// override with BRAINSTORM_IDLE_TIMEOUT_MS (start-server.sh: --idle-timeout-minutes). +const IDLE_TIMEOUT_MS = (() => { + const ms = Number(process.env.BRAINSTORM_IDLE_TIMEOUT_MS); + return Number.isFinite(ms) && ms > 0 ? ms : 4 * 60 * 60 * 1000; +})(); +// How often the watchdog checks for owner-death / idleness. Configurable mainly +// so tests can run fast; production default is 60s. +const LIFECYCLE_CHECK_MS = (() => { + const ms = Number(process.env.BRAINSTORM_LIFECYCLE_CHECK_MS); + return Number.isFinite(ms) && ms > 0 ? ms : 60 * 1000; +})(); +let lastActivity = Date.now(); + +function touchActivity() { + lastActivity = Date.now(); +} + +// ========== File Watching ========== + +const debounceTimers = new Map(); + +// ========== Server Startup ========== + +function startServer() { + 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(CONTENT_DIR).filter(f => !f.startsWith('.') && f.endsWith('.html')) + ); + + const server = http.createServer(handleRequest); + server.on('upgrade', handleUpgrade); + + const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => { + if (!filename || filename.startsWith('.') || !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; // file was deleted + 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 })); + maybeOpenBrowser(); + } 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); + // Close any upgraded WebSocket sockets so server.close() can complete and + // the process actually exits instead of lingering on an open connection. + for (const socket of clients) { + try { socket.destroy(); } catch (e) { /* already gone */ } + } + server.close(() => process.exit(0)); + } + + function ownerAlive() { + if (!ownerPid) return true; + try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; } + } + + // Periodically exit if the owner process died or we've been idle too long. + const lifecycleCheck = setInterval(() => { + if (!ownerAlive()) shutdown('owner process exited'); + else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout'); + }, LIFECYCLE_CHECK_MS); + 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; + } + } + } + + // If the preferred port is already taken (e.g. a previous server is still + // alive), fall back to a random port once instead of failing. + let triedFallback = false; + + function onListen() { + // Cookie name keys on the ACTUAL bound port (may differ from the preferred + // one after an EADDRINUSE fallback) so it can't collide with another server's + // cookie in the shared localhost jar. + COOKIE_NAME = 'brainstorm-key-' + PORT; + // Record the bound port AND token so the next restart of this session reuses + // them — but ONLY when we got our preferred port. On a fallback we bound a + // *different* port because someone else holds the preferred one; persisting + // would overwrite the shared files and strand that other session's open tab. + if (PORT_FILE && !triedFallback) { + try { fs.writeFileSync(PORT_FILE, String(PORT)); } catch (e) { /* best effort */ } + if (TOKEN_FILE) { + try { + fs.writeFileSync(TOKEN_FILE, TOKEN, { mode: 0o600 }); + chmodOwnerOnly(TOKEN_FILE); + } catch (e) { /* best effort */ } + } + } + const info = JSON.stringify({ + type: 'server-started', port: Number(PORT), host: HOST, + url_host: URL_HOST, url: companionUrl(), + screen_dir: CONTENT_DIR, state_dir: STATE_DIR, idle_timeout_ms: IDLE_TIMEOUT_MS + }); + console.log(info); + // server-info embeds the key — keep it owner-only. + fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n', { mode: 0o600 }); + } + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE' && !triedFallback) { + if (tokenSource === 'env') { + console.error('Server failed to bind: preferred port is in use and BRAINSTORM_TOKEN is set; refusing fallback with explicit token'); + process.exit(1); + } + triedFallback = true; + PORT = randomPort(); + if (tokenSource === 'file') { + TOKEN = generateToken(); + tokenSource = 'generated-fallback'; + } + server.listen(PORT, HOST, onListen); + } else { + console.error('Server failed to bind:', err.message); + process.exit(1); + } + }); + server.listen(PORT, HOST, onListen); +} + +if (require.main === module) { + startServer(); +} + +module.exports = { + computeAcceptKey, + encodeFrame, + decodeFrame, + browserLauncherForPlatform, + OPCODES, + MAX_FRAME_PAYLOAD_BYTES +}; diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/start-server.sh b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/start-server.sh new file mode 100755 index 00000000..016a8e48 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/start-server.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Start the brainstorm server and output connection info +# Usage: start-server.sh [--project-dir ] [--host ] [--url-host ] [--foreground] [--background] +# +# Starts server on a random high port, outputs JSON with URL. +# Each session gets its own directory to avoid conflicts. +# +# Options: +# --project-dir Store session files under /.superpowers/brainstorm/ +# instead of /tmp. Files persist after server stops. +# --host Host/interface to bind (default: 127.0.0.1). +# Use 0.0.0.0 in remote/containerized environments. +# --url-host Hostname shown in returned URL JSON. +# --idle-timeout-minutes Shut down after n minutes idle (default 240 = 4h). +# --open Auto-open the browser on the first screen (use only +# after the user approves the visual companion). +# --foreground Run server in the current terminal (no backgrounding). +# --background Force background mode (overrides Codex auto-foreground). + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Parse arguments +PROJECT_DIR="" +FOREGROUND="false" +FORCE_BACKGROUND="false" +BIND_HOST="127.0.0.1" +URL_HOST="" +IDLE_TIMEOUT_MINUTES="" +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 + ;; + --idle-timeout-minutes) + IDLE_TIMEOUT_MINUTES="$2" + shift 2 + ;; + --open) + export BRAINSTORM_OPEN=1 + shift + ;; + --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 "$IDLE_TIMEOUT_MINUTES" ]]; then + if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then + echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}" + exit 1 + fi + export BRAINSTORM_IDLE_TIMEOUT_MS=$(( IDLE_TIMEOUT_MINUTES * 60 * 1000 )) +fi + +is_windows_like_shell() { + case "${OSTYPE:-}" in + msys*|cygwin*|mingw*) return 0 ;; + esac + if [[ -n "${MSYSTEM:-}" ]]; then + return 0 + fi + local uname_s + uname_s="$(uname -s 2>/dev/null || true)" + case "$uname_s" in + MSYS*|MINGW*|CYGWIN*) return 0 ;; + esac + return 1 +} + +# Some environments reap detached/background processes. Auto-foreground when detected. +if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then + FOREGROUND="true" +fi + +# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected. +if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then + if is_windows_like_shell; then + FOREGROUND="true" + fi +fi + +# Session files (server.log, server-info, .last-token) embed the session key — +# keep everything this script and the server create owner-only. +umask 077 + +# Generate unique session directory +SESSION_ID="$$-$(date +%s)" + +if [[ -n "$PROJECT_DIR" ]]; then + SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}" + # Persist the bound port and key per project so a restart reuses them and an + # already-open browser tab reconnects to the same URL with a valid cookie. + export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port" + export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token" +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" +SERVER_ID_FILE="${STATE_DIR}/server-instance-id" + +# Create fresh session directory with content and state peers +mkdir -p "${SESSION_DIR}/content" "$STATE_DIR" + +SERVER_ID="" +if [[ -r /dev/urandom ]]; then + SERVER_ID="$(od -An -N24 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || true)" +fi +if ! [[ "$SERVER_ID" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then + SERVER_ID="$(printf '%08x%08x%08x%08x' "$$" "$(date +%s)" "${RANDOM:-0}" "${RANDOM:-0}")" +fi +printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE" +chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true + +# Kill any existing server +if [[ -f "$PID_FILE" ]]; then + old_pid=$(cat "$PID_FILE") + kill "$old_pid" 2>/dev/null + rm -f "$PID_FILE" +fi + +cd "$SCRIPT_DIR" || exit 1 + +# Resolve the harness PID (grandparent of this script). +# $PPID is the ephemeral shell the harness spawned to run us — it dies +# when this script exits. The harness itself is $PPID's parent. +OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')" +if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then + OWNER_PID="$PPID" +fi + +# Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace. +# Passing a PID node cannot verify causes server to log owner-pid-invalid +# and self-terminate at the 60-second lifecycle check. Clear it so the +# watchdog is disabled and the idle timeout becomes the only shutdown trigger. +if is_windows_like_shell; then + OWNER_PID="" +fi + +# Foreground mode for environments that reap detached/background processes. +if [[ "$FOREGROUND" == "true" ]]; then + env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" & + SERVER_PID=$! + echo "$SERVER_PID" > "$PID_FILE" + wait "$SERVER_PID" + 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="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" > "$LOG_FILE" 2>&1 & +SERVER_PID=$! +disown "$SERVER_PID" 2>/dev/null +echo "$SERVER_PID" > "$PID_FILE" + +# Wait for server-started message (check log file) +for _ in {1..50}; do + if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then + # Verify server is still alive after a short window (catches process reapers) + 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\": \"Server started but was killed. Retry in a persistent terminal with: $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 + +# Timeout - server didn't start +echo '{"error": "Server failed to start within 5 seconds"}' +exit 1 diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/stop-server.sh b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/stop-server.sh new file mode 100755 index 00000000..7cacfe94 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/stop-server.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Stop the brainstorm server and clean up +# Usage: stop-server.sh +# +# 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. + +SESSION_DIR="$1" + +if [[ -z "$SESSION_DIR" ]]; then + echo '{"error": "Usage: stop-server.sh "}' + exit 1 +fi + +STATE_DIR="${SESSION_DIR}/state" +PID_FILE="${STATE_DIR}/server.pid" +SERVER_ID_FILE="${STATE_DIR}/server-instance-id" + +mark_stopped() { + local reason="$1" + rm -f "${STATE_DIR}/server-info" + printf '{"reason":"%s","timestamp":%s}\n' "$reason" "$(date +%s)" > "${STATE_DIR}/server-stopped" +} + +read_expected_server_id() { + [[ -f "$SERVER_ID_FILE" ]] || return 1 + local id + id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)" + [[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1 + printf '%s\n' "$id" +} + +command_line_for_pid() { + local pid="$1" + if [[ -r "/proc/$pid/cmdline" ]]; then + tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true + return 0 + fi + ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true +} + +command_has_server_id() { + local pid="$1" + local expected="$2" + local expected_arg="--brainstorm-server-id=$expected" + if [[ -r "/proc/$pid/cmdline" ]]; then + local arg + while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do + [[ "$arg" == "$expected_arg" ]] && return 0 + done < "/proc/$pid/cmdline" + return 1 + fi + local command_line + command_line="$(command_line_for_pid "$pid")" + [[ -n "$command_line" ]] || return 1 + case " $command_line " in + *" $expected_arg "*) return 0 ;; + *) return 1 ;; + esac +} + +# Confirm a PID has this session's per-start instance id, not just a familiar +# process name. Ambiguous or legacy metadata fails closed as stale_pid. +is_brainstorm_server() { + kill -0 "$1" 2>/dev/null || return 1 + local expected_id + expected_id="$(read_expected_server_id)" || return 1 + command_has_server_id "$1" "$expected_id" || return 1 + return 0 +} + +if [[ -f "$PID_FILE" ]]; then + pid=$(cat "$PID_FILE") + + # Refuse to signal a PID we can't prove is our server. A stale pid file may + # point at an unrelated process after a reboot/PID wraparound. + if ! is_brainstorm_server "$pid"; then + rm -f "$PID_FILE" "$SERVER_ID_FILE" + mark_stopped "stale_pid" + echo '{"status": "stale_pid"}' + exit 0 + fi + + # Try to stop gracefully, fallback to force if still alive + kill "$pid" 2>/dev/null || true + + # Wait for graceful shutdown (up to ~2s) + for _ in {1..20}; do + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + sleep 0.1 + done + + # If still running, escalate to SIGKILL + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + + # Give SIGKILL a moment to take effect + sleep 0.1 + fi + + if kill -0 "$pid" 2>/dev/null; then + echo '{"status": "failed", "error": "process still running"}' + exit 1 + fi + + rm -f "$PID_FILE" "$SERVER_ID_FILE" "${STATE_DIR}/server.log" + mark_stopped "stop-server.sh" + + # Only delete ephemeral /tmp directories + if [[ "$SESSION_DIR" == /tmp/* ]]; then + rm -rf "$SESSION_DIR" + fi + + echo '{"status": "stopped"}' +else + echo '{"status": "not_running"}' +fi diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/spec-document-reviewer-prompt.md b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/spec-document-reviewer-prompt.md new file mode 100644 index 00000000..35938dc7 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/spec-document-reviewer-prompt.md @@ -0,0 +1,48 @@ +# 规范文档审查员提示词模板 + +派遣规范文档审查员子 Agent 时,请使用此模板。 + +**目的:** 验证规范是否完整、一致,并已准备好进入实施规划阶段。 + +**派遣时机:** 规范文档已写入 docs/superpowers/specs/ 后 + +``` +子 Agent (general-purpose): + description: "审查规范文档" + prompt: | + 你是一名规范文档审查员。请验证此规范是否完整并已准备好进入规划阶段。 + + **待审查的规范:** [SPEC_FILE_PATH] + + ## 检查内容 + + | 类别 | 检查要点 | + |----------|------------------| + | 完整性 | TODOs、占位符、"TBD"、不完整的章节 | + | 一致性 | 内部矛盾、相互冲突的要求 | + | 清晰度 | 要求是否含糊到足以导致他人构建出错误的内容 | + | 范围 | 是否足够聚焦,可由单个计划涵盖——而不是覆盖多个相互独立的子系统 | + | YAGNI | 未请求的功能、过度工程化 | + + ## 校准 + + **只标记会在实施规划期间造成实际问题的问题。** + 缺少章节、存在矛盾,或某项要求含糊到可能被解读为两种不同的含义—— + 这些才是问题。轻微的措辞改进、风格偏好以及“某些章节不如其他章节详细”均不属于问题。 + + 除非存在会导致计划有缺陷的严重缺漏,否则应予以批准。 + + ## 输出格式 + + ## 规范审查 + + **状态:** 通过 | 发现问题 + + **问题(如有):** + - [第 X 节]:[具体问题] — [它为何会影响规划] + + **建议(仅供参考,不阻碍批准):** + - [改进建议] +``` + +**审查员返回:** 状态、问题(如有)、建议 diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md new file mode 100644 index 00000000..f57b9d61 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md @@ -0,0 +1,281 @@ +# 可视化伴侣指南 + +基于浏览器的可视化头脑风暴伴侣,用于展示模型稿、图示和选项。 + +## 何时使用 + +针对每个问题而非每个会话进行决定。判断标准是:**用户通过看它而不是阅读它,是否能理解得更好?** + +当内容本身是可视化内容时,**使用浏览器**: + +- **UI 模型稿**——线框图、布局、导航结构、组件设计 +- **架构图**——系统组件、数据流、关系图 +- **并排视觉比较**——比较两种布局、两套配色方案、两个设计方向 +- **设计润色**——当问题涉及观感、间距、视觉层级时 +- **空间关系**——以图示形式呈现的状态机、流程图、实体关系 + +当内容是文本或表格时,**使用终端**: + +- **需求和范围问题**——“X 是什么意思?”、“哪些功能在范围内?” +- **概念性 A/B/C 选择**——在用文字描述的方案之间进行选择 +- **权衡清单**——优缺点、比较表 +- **技术决策**——API 设计、数据建模、架构方案选择 +- **澄清问题**——任何答案是文字而非视觉偏好的问题 + +一个*关于* UI 主题的问题并不自动等同于视觉问题。“你想要哪种向导?”是概念性问题——使用终端。“这些向导布局中,哪一种感觉合适?”是视觉问题——使用浏览器。 + +## 工作原理 + +服务器会监视一个目录中的 HTML 文件,并将最新的文件提供给浏览器。你将 HTML 内容写入 `screen_dir`,用户会在其浏览器中看到内容,并可通过点击选择选项。选择结果会记录到 `state_dir/events`,供你在下一轮读取。 + +**内容片段与完整文档:**如果你的 HTML 文件以 `/.superpowers/brainstorm/` 中查找会话目录。 + +**注意:**将项目根目录作为 `--project-dir` 传入,以便模型稿持久保存在 `.superpowers/brainstorm/` 中,并在服务器重启后继续保留。如果不这样做,文件会写入 `/tmp` 并被清理。如果 `.superpowers/` 尚未加入 `.gitignore`,请提醒用户添加它。 + +**按平台启动服务器:** + +**Claude Code:** +```bash +# 默认模式即可——脚本会自行将服务器置于后台运行。 +scripts/start-server.sh --project-dir /path/to/project --open +``` + +在 Windows 上,脚本会自动检测并切换到前台模式(这会阻塞工具调用)。在 Bash 工具调用中使用 `run_in_background: true`,使服务器能够跨对话轮次持续运行,然后在下一轮读取 `$STATE_DIR/server-info` 以获取 URL 和端口。 + +**Codex:** +```bash +# Codex 会清理后台进程。脚本会自动检测 CODEX_CI 并 +# 切换到前台模式。正常运行即可——无需额外标志。 +scripts/start-server.sh --project-dir /path/to/project --open +``` + +**Copilot CLI:** +```bash +# 使用 --foreground,并通过 bash 工具以 mode: "async" 启动服务器, +# 使进程能够跨轮次持续运行。保存返回的 shellId,以便之后需要与其交互时 +# 用于 read_bash / stop_bash。 +scripts/start-server.sh --project-dir /path/to/project --open --foreground +``` + +**其他环境:**服务器必须在后台跨对话轮次持续运行。如果你的环境会清理已分离的进程,请使用 `--foreground`,并通过你所在平台的后台执行机制启动该命令。 + +如果你的浏览器无法访问该 URL(这在远程/容器化环境中很常见),请绑定非环回主机: + +```bash +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` 中的新文件: + - **必需:在提及 URL 或推送界面之前,确认服务器仍在运行。** 检查 `$STATE_DIR/server-info` 是否存在,并且 `$STATE_DIR/server-stopped` 不存在。如果服务器已关闭,请使用**相同的 `--project-dir`** 通过 `start-server.sh` 重新启动它——它会复用相同的端口,因此用户已打开的标签页会自行重新连接(服务器停机期间会显示“已暂停”遮罩层),你无需发送新的 URL。服务器空闲 4 小时后会自动退出(可通过 `--idle-timeout-minutes` 配置)。 + - 使用语义化文件名:`platform.html`、`visual-style.html`、`layout.html` + - **绝不要重复使用文件名**——每个界面都使用一个全新的文件 + - 使用你的文件创建工具——**绝不要使用 cat/heredoc**(会向终端倾倒大量杂乱信息) + - 服务器会自动提供最新的文件 + +2. **告诉用户会看到什么,然后结束你的回合:** + - 每一步都提醒他们 URL(而不只是第一步) + - 简要概括界面上显示的内容(例如,“正在展示主页的 3 种布局选项”) + - 请他们在终端中回复:“看一下,然后告诉我你的想法。如果愿意,请点击选择一个选项。” + +3. **在你的下一个回合中**——用户在终端中回复后: + - 如果 `$STATE_DIR/events` 存在,则读取它——其中以 JSON 行的形式包含用户的浏览器交互 + - 将其与用户的终端文本合并,以了解完整情况 + - 终端消息是主要反馈;`state_dir/events` 提供结构化交互数据 + +4. **迭代或推进**——如果反馈会改变当前界面,则写入一个新文件(例如 `layout-v2.html`)。只有当前步骤得到验证后,才能进入下一个问题。 + +5. **返回终端时卸载内容**——当下一步不需要浏览器时(例如,提出澄清问题、讨论权衡取舍),推送一个等待界面以清除过时内容: + + ```html + +
+

继续在终端中...

+
+ ``` + + 这样可避免在对话已经继续推进后,用户仍盯着一个已经解决的选择。当下一个可视化问题出现时,像往常一样推送一个新的内容文件。 + +6. 重复上述步骤,直到完成。 + +## 编写内容片段 + +只编写放入页面内部的内容。服务器会自动使用框架模板将其包装起来(页眉、主题 CSS、连接状态以及所有交互基础设施)。 + +**最小示例:** + +```html +

哪种布局更合适?

+

请考虑可读性和视觉层级

+ +
+
+
A
+
+

单栏

+

简洁、专注的阅读体验

+
+
+
+
B
+
+

双栏

+

侧边栏导航搭配主内容区

+
+
+
+``` + +就这些。不需要 ``、CSS,也不需要 `