From c09512f26a7f9e4c5972f8dc9cc72072246bcfe5 Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Tue, 27 Oct 2026 15:58:23 +0800 Subject: [PATCH] Sync third-party and MCP marketplace plugins Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed. --- .agents/plugins/marketplace.json | 12 + README.md | 4 +- config/external-sources.json | 49 +- config/external-sources.lock.json | 27 +- .../codex/.agents/plugins/marketplace.json | 12 + .../.codex-plugin/plugin.json | 14 +- .../THIRD_PARTY_SOURCE.json | 6 +- .../artemis-use/references/diagnostics.md | 11 +- .../skills/artemis-use/references/plugins.md | 2 + .../find-skills/THIRD_PARTY_SOURCE.json | 4 +- .../plugins/grill-me/THIRD_PARTY_SOURCE.json | 4 +- .../plugins/mcp-playwright/MCP_SOURCE.json | 2 +- .../next-skills/.codex-plugin/plugin.json | 6 +- .../next-skills/THIRD_PARTY_SOURCE.json | 4 +- .../superpowers-zh/.codex-plugin/plugin.json | 48 + plugins/codex/plugins/superpowers-zh/LICENSE | 21 + .../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 | 164 +++ .../brainstorming/builder-check-prompt.md | 22 + .../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 ++ .../skills/brainstorming/visual-companion.md | 288 +++++ .../skills/diagnosing-superpowers/SKILL.md | 120 ++ .../prompts/analyst-common.md | 37 + .../prompts/cost-and-time.md | 27 + .../prompts/plan-adherence.md | 29 + .../prompts/quality-evidence.md | 26 + .../prompts/repeated-work.md | 29 + .../prompts/request-conflicts.md | 20 + .../prompts/scrub-audit.md | 27 + .../diagnosing-superpowers/prompts/scrub.md | 29 + .../prompts/similar-session.md | 38 + .../prompts/skill-timeline.md | 28 + .../prompts/stumbles.md | 27 + .../references/context-safety.md | 19 + .../references/github-issues.md | 45 + .../references/redaction-policy.md | 35 + .../references/session-discovery.md | 15 + .../templates/bundle-README.md | 76 ++ .../diagnosing-superpowers/templates/case.md | 63 + .../diagnosing-superpowers/templates/issue.md | 68 + .../templates/report.md | 82 ++ .../dispatching-parallel-agents/SKILL.md | 167 +++ .../skills/executing-plans/SKILL.md | 175 +++ .../skills/executing-plans/scripts/task-done | 52 + .../skills/executing-plans/scripts/task-start | 28 + .../finishing-a-development-branch/SKILL.md | 213 ++++ .../skills/receiving-code-review/SKILL.md | 203 +++ .../skills/requesting-code-review/SKILL.md | 95 ++ .../requesting-code-review/code-reviewer.md | 188 +++ .../subagent-driven-development/SKILL.md | 250 ++++ .../implementer-prompt.md | 141 ++ .../re-review-prompt.md | 101 ++ .../references/batching.md | 5 + .../references/cannot-verify.md | 10 + .../references/final-review.md | 15 + .../references/fix-loop.md | 32 + .../references/handling-reports.md | 15 + .../references/preflight.md | 13 + .../references/resuming.md | 11 + .../scripts/review-package | 53 + .../scripts/sdd-workspace | 87 ++ .../scripts/task-brief | 43 + .../task-reviewer-prompt.md | 163 +++ .../systematic-debugging/CREATION-LOG.md | 119 ++ .../skills/systematic-debugging/SKILL.md | 278 ++++ .../condition-based-waiting-example.ts | 158 +++ .../condition-based-waiting.md | 113 ++ .../systematic-debugging/defense-in-depth.md | 121 ++ .../systematic-debugging/find-polluter.sh | 72 ++ .../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 | 322 +++++ .../writing-good-tests.md | 173 +++ .../skills/using-git-worktrees/SKILL.md | 174 +++ .../skills/using-superpowers/SKILL.md | 66 + .../references/antigravity-tools.md | 23 + .../references/claude-code-tools.md | 26 + .../references/codex-tools.md | 59 + .../references/gemini-tools.md | 63 + .../references/hermes-tools.md | 54 + .../references/muse-tools.md | 35 + .../using-superpowers/references/pi-tools.md | 16 + .../verification-before-completion/SKILL.md | 118 ++ .../skills/writing-plans/SKILL.md | 179 +++ .../skills/writing-skills/SKILL.md | 388 ++++++ .../references/CLAUDE_MD_TESTING.md | 187 +++ .../references/anthropic-best-practices.md | 1129 +++++++++++++++++ .../references/bulletproofing.md | 79 ++ .../writing-skills/references/discovery.md | 200 +++ .../writing-skills/references/flowcharts.md | 37 + .../references/graphviz-conventions.dot | 172 +++ .../references/persuasion-principles.md | 186 +++ .../testing-skills-with-subagents.md | 380 ++++++ .../skills/writing-skills/render-graphs.js | 169 +++ scripts/sync_external_plugins.ps1 | 9 +- 104 files changed, 10480 insertions(+), 43 deletions(-) create mode 100644 plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json create mode 100644 plugins/codex/plugins/superpowers-zh/LICENSE 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/builder-check-prompt.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/visual-companion.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/analyst-common.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/cost-and-time.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/plan-adherence.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/quality-evidence.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/repeated-work.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/request-conflicts.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/scrub-audit.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/scrub.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/similar-session.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/skill-timeline.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/prompts/stumbles.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/references/context-safety.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/references/github-issues.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/references/redaction-policy.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/references/session-discovery.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/templates/bundle-README.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/templates/case.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/templates/issue.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/diagnosing-superpowers/templates/report.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 100755 plugins/codex/plugins/superpowers-zh/skills/executing-plans/scripts/task-done create mode 100755 plugins/codex/plugins/superpowers-zh/skills/executing-plans/scripts/task-start 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 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/re-review-prompt.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/batching.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/cannot-verify.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/final-review.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/fix-loop.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/handling-reports.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/preflight.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/subagent-driven-development/references/resuming.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/writing-good-tests.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/claude-code-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/gemini-tools.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/references/hermes-tools.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/using-superpowers/references/muse-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-skills/SKILL.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/CLAUDE_MD_TESTING.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/anthropic-best-practices.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/bulletproofing.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/discovery.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/flowcharts.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/graphviz-conventions.dot create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/persuasion-principles.md create mode 100644 plugins/codex/plugins/superpowers-zh/skills/writing-skills/references/testing-skills-with-subagents.md create mode 100755 plugins/codex/plugins/superpowers-zh/skills/writing-skills/render-graphs.js diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index de818e29..78e72f70 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -316,6 +316,18 @@ }, "category": "Developer Tools" }, + { + "name": "superpowers-zh", + "source": { + "source": "local", + "path": "./plugins/codex/plugins/superpowers-zh" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + }, { "name": "updating-gitea-repositories", "source": { diff --git a/README.md b/README.md index 75c61988..8f91f238 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Codex 插件页的大标题按 marketplace 来源分组,不是按插件 `categ 有些插件是 skill 集合,安装一个 `plugin-id` 后会同时提供多个 `$skill-name`。可安装项和安装后可用的 skills 均以 Codex 的实际输出为准。 +`superpowers-zh` 是 Superpowers 的简体中文版本,提供规划、测试驱动开发、调试和代码审查等工作流。请使用上述插件 ID 安装;其内容跟随中文版正式发版,Codex 组件保持上游声明。 + `docx`、`pdf`、`pptx` 和 `xlsx` 提供简体中文入口与 Windows PowerShell 指引。首次使用先检查 Python、Node、中文字体和实际需要的依赖;Office 转换需要 LibreOffice,PDF 逐页预览使用 PyMuPDF,无需 Poppler。中文文件路径必须加引号,交付前检查缺字、版式与公式结果。 `find-skills` 使用 Vercel Skills CLI 搜索技能,需要 Node.js 与 `npx`。搜索结果里的安装命令适用于其上游生态;安装本市场插件仍使用上述 Codex 命令。 @@ -39,7 +41,7 @@ Codex 插件页的大标题按 marketplace 来源分组,不是按插件 `categ `embedded-app-pc-debug`、`firmware-app-development` 和 `firmware-lowlevel-development` 分别覆盖 Windows 电脑端设备调试、固件应用层开发及 BSP/PSP 等底层开发流程。使用前应确认项目工具链、目标设备和访问权限;构建、设备写入、刷机、升级、上传和发布等操作必须按插件门禁取得明确确认。 -`artemis-automation` 提供 Android 真机或模拟器的自然语言测试、批量用例、失败诊断、历史检索和报告交付流程。市场只分发 Skill;使用前需单独部署并配置兼容的 ARTEMIS MCP、Android 设备和模型网关。 +`artemis-automation` 提供 Android、电脑和跨端测试,以及探索、批量用例、失败诊断和报告交付流程。市场只分发 Skill;使用前需单独部署兼容的 ARTEMIS MCP 和模型网关,并按测试目标配置 Android 设备或授权的电脑插件。 `artemis-case-prep` 根据原始 Android 测试工作簿筛选可自动化用例,保留原表副本并生成七列 ARTEMIS 任务表;它只准备文档,不启动测试。导入前请检查表头与当前 ARTEMIS 版本是否匹配。 diff --git a/config/external-sources.json b/config/external-sources.json index cf505550..9df9474c 100644 --- a/config/external-sources.json +++ b/config/external-sources.json @@ -507,6 +507,37 @@ "LICENSE" ] }, + { + "id": "superpowers-zh", + "repo": "https://github.com/AreChen/superpowers-zh.git", + "releaseChannel": "latest-stable", + "releaseTagSuffix": "zh", + "adapter": "codex-plugin", + "pluginName": "superpowers-zh", + "category": "Developer Tools", + "sourcePath": ".", + "sparseCheckout": [ + ".codex-plugin", + "skills", + "assets/superpowers-small.svg", + "assets/app-icon.png", + "LICENSE" + ], + "include": [ + ".codex-plugin", + "skills", + "assets", + "LICENSE" + ], + "manifestOverrides": { + "name": "superpowers-zh", + "interface": { + "displayName": "Superpowers-zh", + "developerName": "AreChen / Jesse Vincent", + "category": "Developer Tools" + } + } + }, { "id": "caveman", "repo": "https://github.com/JuliusBrussee/caveman.git", @@ -1234,7 +1265,7 @@ ], "brandColor": "#2563EB", "manifestOverrides": { - "description": "使用 ARTEMIS MCP 完成 Android 自动化测试、批量执行、失败诊断、历史检索和报告交付。", + "description": "使用 ARTEMIS MCP 执行 Android、电脑和跨端测试,诊断任务并交付报告。", "author": { "name": "chenmingxuan", "url": "https://git.playones.com/chenmingxuan" @@ -1250,9 +1281,9 @@ "mcp" ], "interface": { - "displayName": "ARTEMIS Android 自动化", - "shortDescription": "通过 ARTEMIS MCP 执行 Android 测试并交付报告。", - "longDescription": "通过 ARTEMIS MCP 在 Android 真机或模拟器上执行自然语言测试、批量用例、失败诊断、历史检索及 Word/Excel 报告交付。插件只分发 Skill;使用前必须单独部署并配置与该版本兼容的 ARTEMIS MCP、Android 设备和所需模型网关。", + "displayName": "ARTEMIS 自动化", + "shortDescription": "通过 ARTEMIS MCP 执行 Android、电脑和跨端测试。", + "longDescription": "通过 ARTEMIS MCP 执行 Android、电脑和跨端测试,支持探索、批量执行、失败诊断及报告交付。插件只分发 Skill;使用前须单独部署兼容的 ARTEMIS MCP 和模型网关,按测试目标配置 Android 设备或授权的电脑插件。", "developerName": "chenmingxuan", "category": "Developer Tools", "capabilities": [ @@ -1261,8 +1292,8 @@ "Write" ], "defaultPrompt": [ - "使用 ARTEMIS Android 自动化检查设备并执行这个测试用例。", - "使用 ARTEMIS Android 自动化批量执行这些用例并导出报告。" + "使用 ARTEMIS 自动化检查测试环境并执行这个用例。", + "使用 ARTEMIS 自动化批量执行这些用例并导出报告。" ], "websiteURL": "https://git.playones.com/chenmingxuan/Artemis-Eapil", "privacyPolicyURL": "https://git.playones.com/chenmingxuan/Artemis-Eapil", @@ -1450,7 +1481,7 @@ "brandColor": "#000000", "manifestOverrides": { "version": "0.2.0", - "description": "Next.js Agent Skills 提供 Cache Components 迁移、优化和 dev loop 验证工作流。", + "description": "Next.js Agent Skills 提供缓存、预取、打包优化和 dev loop 验证工作流。", "author": { "name": "Vercel", "url": "https://github.com/vercel" @@ -1468,8 +1499,8 @@ ], "interface": { "displayName": "Next.js Agent Skills", - "shortDescription": "Next.js Cache Components 迁移、优化和 dev loop 验证工作流。", - "longDescription": "Next.js Agent Skills 现在跟随 Next.js 仓库 canary/skills 版本,包含 next-cache-components-adoption、next-cache-components-optimizer 和 next-dev-loop 三个技能,覆盖 Cache Components 启用迁移、静态壳/即时导航优化,以及通过 Next.js dev MCP 与真实浏览器验证运行时行为。", + "shortDescription": "Next.js 缓存、预取、打包优化与 dev loop 验证工作流。", + "longDescription": "Next.js Agent Skills 跟随 Next.js 仓库 canary/skills,覆盖 Cache Components、部分预取的采用和优化、打包分析,以及通过 Next.js dev MCP 与真实浏览器验证运行时行为。技能数量以上游目录和安装结果为准。", "developerName": "Vercel", "category": "Developer Tools", "capabilities": [ diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index 28bcacb8..d4937edb 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -42,8 +42,8 @@ "repo": "https://github.com/vercel-labs/skills.git", "ref": "main", "adapter": "claude-skill", - "commit": "a80e4c9d12d607631249d4d7d059f835d4b2f13a", - "syncedAt": "2026-10-21T06:48:34Z" + "commit": "533c7df02f9b4d913fb8eba553aa99d86103d1d4", + "syncedAt": "2026-10-27T07:55:04Z" }, { "id": "generate-agents-md", @@ -108,6 +108,15 @@ "commit": "28b3b8dee8c976df4fa3e94aed4e092ad43fcb03", "syncedAt": "2026-10-26T16:00:00Z" }, + { + "id": "superpowers-zh", + "pluginName": "superpowers-zh", + "repo": "https://github.com/AreChen/superpowers-zh.git", + "ref": "v7.0.1-zh.1", + "adapter": "codex-plugin", + "commit": "a41f784bb852f0c86d4cc891ecdb14d1344aa063", + "syncedAt": "2026-10-27T07:55:04Z" + }, { "id": "caveman", "pluginName": "caveman", @@ -177,8 +186,8 @@ "repo": "https://github.com/mattpocock/skills.git", "ref": "main", "adapter": "skill-collection", - "commit": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", - "syncedAt": "2026-10-21T06:48:34Z" + "commit": "59db609eb1e18aaf59b63284d0e52255aff0c63c", + "syncedAt": "2026-10-27T07:55:04Z" }, { "id": "updating-gitea-repositories", @@ -220,10 +229,10 @@ "id": "artemis-automation", "pluginName": "artemis-automation", "repo": "https://git.playones.com/chenmingxuan/Artemis-Eapil.git", - "ref": "v2.1.0", + "ref": "v2.2.0", "adapter": "claude-skill", - "commit": "83bcfd54d578a76aaea73e059a8547e3dc07ddbd", - "syncedAt": "2026-10-23T16:00:00Z" + "commit": "be6e8c2abddc0905c86feb4a78cbd2b7113c38f1", + "syncedAt": "2026-10-27T07:55:04Z" }, { "id": "embedded-app-pc-debug", @@ -258,8 +267,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "b0e1bf792f18c039933d8a9ce9d50d35f930f1be", - "syncedAt": "2026-10-26T16:00:00Z" + "commit": "5988244ce7247f5112f00beb8050860330d93ca0", + "syncedAt": "2026-10-27T07:55:04Z" }, { "id": "shuorenhua", diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index f452998e..d8ca7cbe 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -316,6 +316,18 @@ }, "category": "Developer Tools" }, + { + "name": "superpowers-zh", + "source": { + "source": "local", + "path": "./plugins/superpowers-zh" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + }, { "name": "updating-gitea-repositories", "source": { diff --git a/plugins/codex/plugins/artemis-automation/.codex-plugin/plugin.json b/plugins/codex/plugins/artemis-automation/.codex-plugin/plugin.json index f98fe519..8236163c 100644 --- a/plugins/codex/plugins/artemis-automation/.codex-plugin/plugin.json +++ b/plugins/codex/plugins/artemis-automation/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "artemis-automation", - "version": "2.1.0", - "description": "使用 ARTEMIS MCP 完成 Android 自动化测试、批量执行、失败诊断、历史检索和报告交付。", + "version": "2.2.0", + "description": "使用 ARTEMIS MCP 执行 Android、电脑和跨端测试,诊断任务并交付报告。", "author": { "name": "chenmingxuan", "url": "https://git.playones.com/chenmingxuan" @@ -18,9 +18,9 @@ ], "skills": "./skills/", "interface": { - "displayName": "ARTEMIS Android 自动化", - "shortDescription": "通过 ARTEMIS MCP 执行 Android 测试并交付报告。", - "longDescription": "通过 ARTEMIS MCP 在 Android 真机或模拟器上执行自然语言测试、批量用例、失败诊断、历史检索及 Word/Excel 报告交付。插件只分发 Skill;使用前必须单独部署并配置与该版本兼容的 ARTEMIS MCP、Android 设备和所需模型网关。", + "displayName": "ARTEMIS 自动化", + "shortDescription": "通过 ARTEMIS MCP 执行 Android、电脑和跨端测试。", + "longDescription": "通过 ARTEMIS MCP 执行 Android、电脑和跨端测试,支持探索、批量执行、失败诊断及报告交付。插件只分发 Skill;使用前须单独部署兼容的 ARTEMIS MCP 和模型网关,按测试目标配置 Android 设备或授权的电脑插件。", "developerName": "chenmingxuan", "category": "Developer Tools", "capabilities": [ @@ -29,8 +29,8 @@ "Write" ], "defaultPrompt": [ - "使用 ARTEMIS Android 自动化检查设备并执行这个测试用例。", - "使用 ARTEMIS Android 自动化批量执行这些用例并导出报告。" + "使用 ARTEMIS 自动化检查测试环境并执行这个用例。", + "使用 ARTEMIS 自动化批量执行这些用例并导出报告。" ], "websiteURL": "https://git.playones.com/chenmingxuan/Artemis-Eapil", "privacyPolicyURL": "https://git.playones.com/chenmingxuan/Artemis-Eapil", diff --git a/plugins/codex/plugins/artemis-automation/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/artemis-automation/THIRD_PARTY_SOURCE.json index e720214f..dede38e4 100644 --- a/plugins/codex/plugins/artemis-automation/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/artemis-automation/THIRD_PARTY_SOURCE.json @@ -1,9 +1,9 @@ { "sourceId": "artemis-automation", "repo": "https://git.playones.com/chenmingxuan/Artemis-Eapil.git", - "ref": "v2.1.0", - "commit": "83bcfd54d578a76aaea73e059a8547e3dc07ddbd", + "ref": "v2.2.0", + "commit": "be6e8c2abddc0905c86feb4a78cbd2b7113c38f1", "adapter": "claude-skill", "sourcePath": "skills/artemis-use", - "syncedAt": "2026-10-23T16:00:00Z" + "syncedAt": "2026-10-27T07:55:04Z" } diff --git a/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/diagnostics.md b/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/diagnostics.md index 4b10d6ae..c6871af4 100644 --- a/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/diagnostics.md +++ b/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/diagnostics.md @@ -10,7 +10,7 @@ | 问题 | inspect action | 要核对的证据 | | --- | --- | --- | -| 长时间没动、API 超时、no tool call | view_requests | 请求开始/结束、模型、阶段、请求ID、重试、是否终止;HTTP 200 不表示输出结束 | +| 长时间没动、API 超时、499、no tool call | view_requests | 请求开始/结束、模型、阶段、请求/尝试/响应/链路编号、重试、取消来源与时限层级;HTTP 200 不表示输出结束,499 不能单独归因 | | token 特别多、缓存是否生效 | view_usage,再按需 view_requests | prompt/completion/cached、调用数、角色/模型分组;未记录字段不能补猜 | | Python、ADB、插件错误 | view_logs | 该任务 stdout/stderr 有限尾部;脱敏文本和实际错误链 | | 点错、未验证、与结论矛盾 | search → view_step_details / view_step_screenshots | 动作前后画面、工具参数与实际回执、窗口和坐标尺寸 | @@ -18,7 +18,7 @@ `offset/limit` 分页,使用返回的 next_offset;view_logs 的 offset 从最新行向前计数,返回每页仍按时间正序,超过尾部读取窗口的旧字节会注明。text_truncated=true 时缩小范围或读取指定步骤,不把截断当成内容缺失。 -`search` 使用 query 或 step_range,不会执行历史动作。`view_summary` 也有分页,不用它一次加载整条长任务。 +`search` 使用 query 或 step_range,不会执行历史动作。`view_summary` 返回 summary Markdown 与 steps.items/total/offset/limit/next_offset,按 next_offset 翻页,不一次加载整条长任务。items 中 step_id 是存储编号,不能用模型口述步号替代。JSON 结果同时保留 structuredContent 与兼容文本;调用严格按工具 schema,MCP 的 task_desc 与 HTTP 的 goal 不能混用,未知参数在执行前报错。 ## 缓存与耗时 @@ -27,7 +27,10 @@ - 按角色/模型分组判断消耗来源,不能把 Checker、视频分析和执行模型混为一个请求。旧数据未记录模型时注明 unknown。 - 请求阶段和重试用于解释等待,时间戳 Unix 秒及 UTC 转成用户当地时区;注明时区。无终止事件或 unknown 不能当成功。 - 区分模型生成耗时、设备/插件操作耗时、排队及用户等待。先找实际重复观测、工具参数膨胀、无进展循环,再建议优化。 -- 最终验收读取 `view_requests` 返回的 `final_reviews`:核对 stage、total/elapsed/remaining_seconds、request_count、retry_count 和 status;用 review_id 关联请求的 final_review 字段。最终验收与中间检查点独立计时。budget_exhausted 是验收总预算耗尽,request_retries_exhausted 是模型请求重试耗尽,cancelled 是停止或指导中断,不能混称为 API 超时。验收耗尽不自动重跑设备操作;流式半截 passed 和验收 completed 都不能代替正式 test_result。 +- `mobile_manage_task(action="status")` 的 `lifecycle` 与网页进度、报告同源:`durations` 分开记录 total、queued、resource_wait、paused、execution、final_review、report_export,缺少记录为 null。execution 包含初始化与整理,不能当作模型生成速度;旧任务分项未知不能补成零。 +- 指导回执的 accepted_at/timestamp 表示接收,consumed_at 表示执行器已读取;是否被实际应用仍需核对后续步骤。pending/reading 是等待或领取中,终态未消费为 unapplied,终态领取后未确认消费为 unconfirmed。接收成功不能表述为“已执行”或“模型已应用”。 +- 最终验收读取 `view_requests` 返回的 `final_reviews`:核对 stage、total/elapsed/remaining_seconds、request_count、retry_count 和 status;用 review_id 关联请求的 final_review 字段。最终验收与中间检查点独立计时。budget_exhausted 是验收总预算耗尽,request_retries_exhausted 是模型请求重试耗尽;cancelled 的具体来源看 cancel_reason,没有该字段或为 unknown 时保留未知,不能混称为 API 超时。验收耗尽不自动重跑设备操作;流式半截 passed 和验收 completed 都不能代替正式 test_result。 +- 取消记录核对 cancel_reason 与 cancel_phase:user_stop、guidance_replaced、task_paused、task_replaced、superseded 和 outer_timeout 分别表示已明确归因的中断。timeout_scope 的 request、transport、outer、final_review 区分单次请求、传输、外层等待和验收总预算。自动重试用 request_id 分组,以 attempt_id、previous_attempt_id 关联;可用的 client_request_id、server_request_id、response_id、server_trace_id 和 traceparent 用于对照服务端日志。HTTP 499 或裸取消不能证明来源,旧记录不补猜。 ## 环境与插件 @@ -39,6 +42,8 @@ 给出具体结论、支持它的任务/步骤/请求编号、影响与缺失证据。执行失败不必然是被测产品缺陷;验收失败也不必然是工具故障。 +报告与证据索引的 evidence_sources 区分实际执行界面与接口、文件、终端等辅助来源。业务后台名称或网址不决定执行平台,UI 截图不能单独证明后端持久状态。引用实际 step_id/call_id/source_id,保留未验证边界;来源清单不是新的验收结论,也不改写 AI 原判或人工复验。 + 用户只问复盘时不重新执行、不改验证结果。用户明确给出人工复验结论时,使用 mobile_manage_task(action="review", trace_id=..., review_status="passed"或"failed", review_reason=用户理由, user_confirmed=true)。AI 自己的分析不能使用这个入口冒充人工判定。 用户需要资料时再 mobile_export_report;已有 report/evidence 查看不会生成 ZIP。交付真实路径或链接,并说明未验证项。 diff --git a/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/plugins.md b/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/plugins.md index a3f67912..9d2c685a 100644 --- a/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/plugins.md +++ b/plugins/codex/plugins/artemis-automation/skills/artemis-use/references/plugins.md @@ -17,6 +17,8 @@ 示例中的插件必须已安装;不要为解决缺插件而调用宿主私有电脑工具代替 ARTEMIS 执行。`computer` 无需 `device_serial`,`mixed` 同时需要手机与插件资源。批次每个用例的 `task_config` 使用相同字段,可混合不同配置和执行目标。 +网页首页的“仅 PC”勾选项与 `runtime_overrides.execution_target="computer"` 等价,Flash、Pro、探索均支持;Android 离线、锁屏或未安装 ADB 不阻止电脑任务。电脑任务仍需有效模型配置及至少一个已启用、已授权并提供画面观察的电脑插件。探索创建时使用 `task_config`,批量 Excel 的“任务配置(JSON)”可直接填写 `{"execution_target":"computer"}`。不要把 `pc_only` 或 `include_devices` 当作提交参数;后者只用于工作区发现。 + 未提供 `plugins` 表示使用该目标下已启用插件的授权默认配置;空数组明确不用插件。只提供插件 ID 和配置名称,版本由服务在入队时固定。安装、授权、连接凭据和资源保护解除由用户在插件页面处理。 插件超时/取消后先查任务状态和错误,不自动重放点击或配置写入。`mobile_workspace.blocked_resources` 非空时说明存在尚未确认停止的资源,不能声称暂停已安全完成。工具返回成功还需从后续画面或结构化读数验证业务结果。 diff --git a/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json index 55aab93d..169b2e55 100644 --- a/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "find-skills", "repo": "https://github.com/vercel-labs/skills.git", "ref": "main", - "commit": "a80e4c9d12d607631249d4d7d059f835d4b2f13a", + "commit": "533c7df02f9b4d913fb8eba553aa99d86103d1d4", "adapter": "claude-skill", "sourcePath": "skills/find-skills", - "syncedAt": "2026-10-21T06:48:34Z" + "syncedAt": "2026-10-27T07:55:04Z" } diff --git a/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json index 3862736d..e6cd5cc3 100644 --- a/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "grill-me", "repo": "https://github.com/mattpocock/skills.git", "ref": "main", - "commit": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", + "commit": "59db609eb1e18aaf59b63284d0e52255aff0c63c", "adapter": "skill-collection", "sourcePath": "skills/productivity", - "syncedAt": "2026-10-21T06:48:34Z" + "syncedAt": "2026-10-27T07:55:04Z" } diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 2a5c22b5..d8434061 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-10-26T16:03:14Z" + "syncedAt": "2026-10-27T07:58:23Z" } diff --git a/plugins/codex/plugins/next-skills/.codex-plugin/plugin.json b/plugins/codex/plugins/next-skills/.codex-plugin/plugin.json index 08b00cd5..82788832 100644 --- a/plugins/codex/plugins/next-skills/.codex-plugin/plugin.json +++ b/plugins/codex/plugins/next-skills/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "next-skills", "version": "0.2.0", - "description": "Next.js Agent Skills 提供 Cache Components 迁移、优化和 dev loop 验证工作流。", + "description": "Next.js Agent Skills 提供缓存、预取、打包优化和 dev loop 验证工作流。", "author": { "name": "Vercel", "url": "https://github.com/vercel" @@ -20,8 +20,8 @@ "skills": "./skills/", "interface": { "displayName": "Next.js Agent Skills", - "shortDescription": "Next.js Cache Components 迁移、优化和 dev loop 验证工作流。", - "longDescription": "Next.js Agent Skills 现在跟随 Next.js 仓库 canary/skills 版本,包含 next-cache-components-adoption、next-cache-components-optimizer 和 next-dev-loop 三个技能,覆盖 Cache Components 启用迁移、静态壳/即时导航优化,以及通过 Next.js dev MCP 与真实浏览器验证运行时行为。", + "shortDescription": "Next.js 缓存、预取、打包优化与 dev loop 验证工作流。", + "longDescription": "Next.js Agent Skills 跟随 Next.js 仓库 canary/skills,覆盖 Cache Components、部分预取的采用和优化、打包分析,以及通过 Next.js dev MCP 与真实浏览器验证运行时行为。技能数量以上游目录和安装结果为准。", "developerName": "Vercel", "category": "Developer Tools", "capabilities": [ diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index d322a086..451a8464 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "b0e1bf792f18c039933d8a9ce9d50d35f930f1be", + "commit": "5988244ce7247f5112f00beb8050860330d93ca0", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-10-26T16:00:00Z" + "syncedAt": "2026-10-27T07:55:04Z" } 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..ac1e09a3 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/.codex-plugin/plugin.json @@ -0,0 +1,48 @@ +{ + "name": "superpowers-zh", + "version": "7.0.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-zh", + "shortDescription": "面向编程 Agent 的规划、TDD、调试与交付工作流", + "longDescription": "使用 Superpowers 中文版引导 Agent 完成头脑风暴、实施规划、测试驱动开发、系统化调试、并行执行、代码审查和分支收尾工作流。", + "developerName": "AreChen / Jesse Vincent", + "category": "Developer Tools", + "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/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/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/superpowers-zh/THIRD_PARTY_SOURCE.json new file mode 100644 index 00000000..48013acc --- /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": "v7.0.1-zh.1", + "commit": "a41f784bb852f0c86d4cc891ecdb14d1344aa063", + "adapter": "codex-plugin", + "sourcePath": ".", + "syncedAt": "2026-10-27T07:55:04Z" +} 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..455bee07 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/SKILL.md @@ -0,0 +1,164 @@ +--- +name: brainstorming +description: "在进行任何创造性工作之前,你都必须使用此技能——包括创建功能、构建组件、添加功能、修改行为,或规划任何新事物,无论是在软件领域还是其他领域(演讲、企业、装修)。" +--- + +# 头脑风暴 + +你很擅长构建东西。你构建的是你认为人类伙伴想要的东西,而这种判断通常没有你感觉的那么扎实。本技能用于在构建任何东西之前,弄清他们真正想要什么,以及为什么。 + +人们往往还没有完全想清楚自己想要什么。好的问题能帮助他们想清楚。当你理解了原因,就能按照他们自己会选择的方式,做出他们从未提及的那一百个决定。 + +## 首先:你究竟知道什么? + +在回复之前,问问自己:对于他们想要什么,以及为什么想要,我知道什么? + +- **所有重要信息都在请求中**(“把图标改成矢车菊蓝”)。这是一个快速、明确的任务。完成它,并说明你做了什么。 +- **缺少某些不可忽视的信息。** 开始对话。 + +在开始实现之前,你必须依次: + +1. 单独用一条消息列出方案,并让你的人类伙伴选择一个(见“列出方案”)。 +2. 编写设计文档,简短版或完整版均可(见“确定工作规模”)。 +3. 获得他们对该文档的批准。 +4. 调用 superpowers:writing-plans 技能。 + +有两件事不算实现:他们已经同意的探索性试作(见“通过探索性试作(Spike)摸清情况”),以及在他们选择跳过提问之后完成他们的请求。 + +## 先了解,再提问 + +对于已有项目,或当他们已经提供了上下文(规格说明、计划、交接材料、他们指出的文件、先前的对话)时,在发出第一条消息前先深入了解。阅读他们提及的内容,以及与之关联的代码或材料,持续阅读,直到你能解释相关部分目前如何运作,以及这次改动会涉及哪些部分。绝不要询问项目或他们提供的上下文已经回答的问题。 + +## 你的第一条消息 + +用以下一种方式推进工作: + +- **他们已经说了自己想要什么**(规格说明、计划、详细请求):整条消息以大约 200 词为目标。用你自己的话复述他们的请求,让他们看出你是否把握了整体:在他们已经说明的范围内,他们想要什么、为谁而做,以及为什么。补充你发现的相关情况:相关部分目前如何运作,以及这次改动会涉及哪些部分。如果你发现了他们可能未曾考虑、并且会改变他们的请求或构建方式的情况,例如约束、冲突或成本更低的路径,就直白地说出来。如何构建要等到你理解他们想要什么之后再谈(见“列出方案”)。结尾先用一行话请他们纠正,再提出一个关于他们想要什么的问题:请求中尚未明确、且其答案对构建结果影响最大的那一点。规格说明往往并不完整,因此在以它为基础继续推进之前,先加以确认。 +- **他们还没有说:** 用他们自己的说法,针对请求中最大的未决事项,提出一个开放式问题。说明你为什么问:答案会如何改变你构建的东西。对于“给我的面包店做一个网站”:“如果主要是给正在决定要不要来店里的人看,我会优先展示营业时间和今天展示柜里有什么;要接单的话,网站就不一样了。你希望人们来到网站后做什么?” + +消息如何结尾,取决于他们的消息是否提出了头脑风暴的要求: + +- **他们提出了要求**(直接点名、调用此技能,或要求讨论设计):以你的问题结尾。他们想要这场对话。 +- **他们没有提出要求:** 他们可能根本不想对话。在你的问题后,轻松地加上一行,表示也可以直接开始,比如“或者,如果你更愿意让我直接动手,说一声就行。”如果他们选择这样做,本技能就到此结束:用一行直白的话说明在制作方式上作出的重要选择(平台、语言、媒介),然后通过正常流程完成他们的请求。 + +整条消息仅此而已。 + +## 对话 + +每条消息都以一个问题结尾:只用一句话,后面不要附上示例答案列表。使用浅显的语言,难度只比人类伙伴当前的水平稍高一点。沿用他们的用词;绝不要使用流程术语。你无需向你的人类伙伴引用本技能的内容。 + +清晰简洁。先说重要的事。避免行话。报告发现时,给出最重要的发现,并表示可以提供其余内容。 + +**让他们描述。** 先用开放式问题。他们自己的话承载着你的选项无法承载的意图。只有在他们卡住时,才给出一组简短的选项。当他们说“你猜吧”或“你怎么看?”时,提出具体的建议。 + +**如何制作。** 每个项目都建立在对媒介、工具、材料和资源的选择之上:应用的平台、编程语言和库;艺术作品的媒介。及早弄清这些选择中哪些他们想亲自做、哪些交给你,以及他们已经有哪些可用的东西。有明确主见或专长的人会想把这些讨论清楚。依靠你的人需要合理的默认选择,并且需要你用直白的话说出来。已有工作时,在它现有采用的东西上继续构建。你做出的任何选择都应在设计中标明是你自己的决定,绝不能写成他们同意了的内容。 + +**需要覆盖的方面。** 这是探索范围,不是脚本。绝不要把以下内容当作问题念出来: + +- 这是什么类型的东西,以及它有什么特别之处 +- 为什么现在做,以及在他们看来怎样才算成功 +- 目标、非目标和反目标(即使技术上能运行,也会让这件事成为失败的情况) +- 既有参考:他们在别处见过什么,喜欢或讨厌什么 +- 现状:现在有什么,他们尝试过什么 +- 他们在意的细节 + +**每个问题都要具体**,紧扣他们的项目和他们告诉你的内容。像一位敏锐的同事那样,以苏格拉底式的方式提问:每个问题都建立在他们上一个回答或你发现的某件事之上,帮助他们想清楚可能遗漏的后果或情形(“投票在会议前截止,而本月有两名成员加入。在本轮中途加入的人有投票权吗?”)。如果原因还不明显,就说明你希望了解什么,或者答案会改变什么。一个他们可以纠正的建议,加上你提出它的理由,往往比一个问题更有效。 + +**提议去探查。** 有了一些基本了解,而且进一步探查会有所帮助时,提议去看看:更深入地探查他们的代码库或项目,查看他们自己的文件和数据,或者搜索网络。说明你会查找什么。是否让你去,由他们决定。 + +**列出方案。** 一旦理解了他们想要什么,在把任何内容整理成文之前,单独用一条消息列出构建它的不同方式。想出几个真正的备选方案,淘汰薄弱的,包括所有与他们想做这件事的原因相冲突的,以及既有工作不允许采用的方案。展示剩下的两三个方案,每个方案都要以项目实际在做的事情为依据,并说明各自的权衡。推荐一个并说明原因,然后询问他们想走哪条路。当确实只有一种合理的方式时,就明确说明,并解释其他方式为什么不合适。这是针对如何构建而言;至于他们想要什么,先让他们描述。 + +**展示,而非讲述。** 只要看比读更有帮助,就提议使用可视化伴侣(见下文): + +- 他们会看的东西:界面、页面、打印件、标牌 +- 布局或安排:界面、房间、日程 +- 流程或一系列步骤 +- 外观各不相同的选项 +- 比阅读更容易通过观看理解的结构:图表、时间线、地图 + +一旦他们接受,就用它展示供他们反馈的原型图、并排的选项,以及他们可以点击的一次性原型。 + +**通过探索性试作(Spike)摸清情况。** Agent 工作是瀑布式的,但速度非常、非常快。快速构建一个一次性版本,往往是了解他们想要什么或某件事是否行得通的最低成本方式。有帮助时,就提议做一次探索性试作。当他们只想做探索性试作时,不要挡路。探索性试作不写测试或只写最少量的测试,并非万无一失,还会跳过计划、实现和审查流程。把探索性试作所得的认识带回对话。 + +## 复述你的理解 + +随着对话推进不断复述。一旦你对某一部分的理解已经足以描述它(例如,它有什么用、服务于谁,或某一部分如何表现),就用大约 200-300 词把这一部分复述给他们,然后回到关于下一部分的提问。在整个对话中,复述和提问交替进行;最后一段涵盖所有剩余内容。 + +只有他们说过的内容,才能写成他们的想法。凡是你补充的内容,都要标明是你的建议,并附上理由。每段结尾都要问有什么不对或遗漏。 + +发送一段复述之前,先检查:它是否描述了他们会看的东西,比如页面、界面、日程表或打印件?如果是,而且他们还没有看到过可视化伴侣的提议,就改为发送提议(下方所示,单独一条消息),把这段复述留到下一条消息。一旦他们接受,就在描述旁放上一个原型图。 + +## 确定工作规模 + +一旦理解了他们想要什么,而且他们已选定一个方案,就用直白的话说明工作规模。他们可以推翻你的判断。 + +| 规模 | 设计文档 | +|------|----------------| +| 小改动 | 简短版:把你们已经共同讨论过的内容写下来 | +| 项目 | 完整的书面设计(见下文) | + +无论简短版还是完整版,设计文档都要包含一个**方案**部分:你考虑过的两三种构建方式、各自的权衡、他们选择了哪一种,以及原因。他们在对话中作出选择,然后你再编写文档。 + +如果任务中途规模扩大,停下来,说明情况,并升级到更大的规模。 + +如果这实际上是几个相互独立的项目,指出这一点,商定顺序,然后一次对一个项目进行头脑风暴。 + +## 书面设计 + +写一份用直白语言写成的文档,让该领域优秀的实现者无需再询问你的人类伙伴就能据此制定计划。涵盖: + +- 意图及其原因 +- 目标、非目标、反目标 +- 约束 +- 方案(见上文) +- 制作方式中由他们决定的部分,按他们的决定如实记录 +- 留给实现者决定的部分 + +对于小改动,简短版设计对同样的要点各用一两行加以说明,并直接交给你的人类伙伴,无需进行实现者检查。 + +**存放位置:** 在软件仓库中,保存到 +`docs/superpowers/specs/YYYY-MM-DD--design.md` 并提交。 +其他情况,询问他们。人类伙伴的偏好优先于上述两种安排。 + +**实现者检查**(针对项目):使用本目录中的 +`builder-check-prompt.md` 派发一个全新的子 Agent。当你告诉人类伙伴你正在整理成文时,说明会先由一位审查者阅读,并且你可能会再回来一次,问几个问题。检查结果返回前,不要交付文档。把实现者提出的问题分为三类: + +- **已经回答**:对话或项目已给出答案,把答案写入文档。 +- **次要问题:** 一个有合理判断力的实现者能够在不改变构建结果的情况下决定。你自行决定,并在文档中标明这是你的决定。 +- **由他们决定:** 只有你的人类伙伴才能回答,且答案会改变构建结果。 + +把“由他们决定”这一类问题集中在一条消息中提出,最重要的放在前面。根据他们的答案更新文档,然后交付文档,并附上一份你自行作出的决定的简短清单,让他们在阅读时检查这些决定。这是该检查唯一会产生的一轮提问。没有子 Agent 工具时,你自己代入这位实现者的角色来阅读文档。 + + +在人类伙伴批准设计文档之前,不得构建任何东西:小改动使用简短版,项目使用完整版。构建包括编写产品代码、搭建脚手架、安装依赖、创建项目,以及调用实现技能。阅读和探索可以进行。 + +有两个例外。如果他们选择跳过提问,本技能就已结束,关卡也随之取消。可以构建他们已经同意的探索性试作;它始终标记为一次性产物,保留其产出则是一个新请求,必须重新经过这道关卡。 + +设计文档获批之后,下一步是制定计划:调用 superpowers:writing-plans,不得调用其他任何技能。 + + +## 危险信号 + +| 想法 | 事实 | +|---------|---------| +| “我会提供一组他们可能想要的选项” | 选项会引导方向。先让他们描述。 | +| “我会补上合理的默认选择” | 很好。把它们说出来。他们从未听说的默认选择,他们也就无法拒绝。 | +| “我已经回答了他们的问题,所以要开始写文档了” | 先单独用一条消息列出方案。 | +| “我应该先解释流程” | 提出你的问题。流程会自然显现。 | +| “他们对这个想法说了‘听起来不错’” | 批准只涵盖你向他们展示过的内容。还没写出来的设计文档不算获批。 | +| “探索性试作能行,我会继续在它的基础上构建” | 保留它是一个新请求。重新经过关卡。 | + +## 可视化伴侣 + +一个浏览器标签页,用于展示原型图、图表和原型。它是工具,不是模式:接受它并不意味着每个问题都要发送到浏览器。 + +**在恰当时机提议使用它。** 上面提到的情况第一次出现时,就提议使用它,绝不要一开始就提出。该提议应单独作为一条消息,不含任何其他内容: + +> “如果展示给你看,可能更容易理解。要我打开一个浏览器标签页,放上几个原型图吗?” + +如果他们拒绝,则继续仅使用文本,并且不要再次提出使用它,除非他们主动提起。 + +**针对每个问题都要问:通过观看,他们会不会更容易理解?** +原型图、布局、图表和并排的设计放在浏览器中。需求、范围、权衡和概念性选择留在文本中。关于 UI 主题的问题并不自动等同于视觉问题。 + +如果他们接受,请在启动服务器之前阅读本目录中的 `visual-companion.md`。 diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/builder-check-prompt.md b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/builder-check-prompt.md new file mode 100644 index 00000000..bb9caabf --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/builder-check-prompt.md @@ -0,0 +1,22 @@ +# 实现者检查提示词 + +书面设计完成后,使用此提示词派发一个全新的通用子 Agent。 +填入领域和文档路径。 + +``` +你是一位才华出众的 [DOMAIN,例如 "软件工程师"、"活动策划师"、 +"面包店顾问"]。你拿到了一份设计文档,并被要求制定计划, +实现文档中描述的内容。你无法与撰写该文档的人沟通。 + +阅读 [DOCUMENT_PATH]。 + +只报告在能够妥善制定计划之前,你仍需要得到解答的问题: +也就是那些你不得不猜测,而猜错又会产生实质影响的地方。 +对于每个问题,给出问题本身,并说明答案为何会改变计划。 +按重要性排序。 + +跳过文档明确留给实现者决定的事项,以及任何你能够合理地 +自行决定的事项。不要挑剔措辞。 + +如果没有剩余的重要问题,就说 "没有剩余的重要问题"。 +``` 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..cc6c2aee --- /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/visual-companion.md b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md new file mode 100644 index 00000000..dbc7ee82 --- /dev/null +++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/visual-companion.md @@ -0,0 +1,288 @@ +# 可视化伴侣指南 + +基于浏览器的可视化头脑风暴伴侣,用于展示模型稿、图示和选项。 + +## 何时使用 + +针对每个问题而非每个会话进行决定。判断标准是:**用户通过看它而不是阅读它,是否能理解得更好?** + +当内容本身是可视化内容时,**使用浏览器**: + +- **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 +# 默认模式即可——脚本会自行将服务器置于后台运行。 +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 并 +# 切换到前台模式。正常运行即可——无需额外标志。 +bash scripts/start-server.sh --project-dir /path/to/project --open +``` + +**Gemini CLI:** +```bash +# 使用 --foreground,并在 shell 工具调用中设置 is_background: true, +# 使进程能够跨轮次持续运行 +bash scripts/start-server.sh --project-dir /path/to/project --open --foreground +``` + +**Copilot CLI:** +```bash +# 使用 Copilot CLI 的非阻塞/后台 shell 机制启动,使服务器跨轮次持续运行。 +# 保留 --foreground,让运行平台而不是脚本负责后台化。启动脚本是 .sh, +# 因此通过 bash 调用(Windows 上从 PowerShell 工具调用 Git Bash 的 bash.exe)。 +bash scripts/start-server.sh --project-dir /path/to/project --open --foreground +``` + +**其他环境:**服务器必须在后台跨对话轮次持续运行。如果你的环境会清理已分离的进程,请使用 `--foreground`,并通过你所在平台的后台执行机制启动该命令。 + +如果你的浏览器无法访问该 URL(这在远程/容器化环境中很常见),请绑定非环回主机: + +```bash +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,也不需要 `