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.
This commit is contained in:
@@ -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": "开发工具"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/)
|
||||
@@ -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"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M394.28,207.8c.81,2.41,1.39,4.78,1.8,7.07,1.61,9.03-.93,17.78-5.99,21.74-22.6,17.7-49.85,29.35-75.34,38.6-.59.22-1.09.28-1.4.34-2.22.47-4.95,1.04-7.25,0-1.46-.66-2.25-1.74-2.66-2.3-1.56-2.1-1.59-4.31-1.56-5.13.1-2.67-.01-4.69,0-4.82.45-3.52.91-10.66,1.41-21.28.6-3.87,2.16-9.63,6.94-13.96,4.01-3.62,8.33-4.6,14.59-5.87,10.76-2.19,37.21-8.22,47.42-16.56,1.63-1.33,2.97-2.65,4.19-3.96,3.72-3.99,6.39-7.92,7.93-10.36,3.22,3.22,7.25,8.48,9.92,16.47Z"/><path d="M428.67,185.28c-2.33,11.99-8.91,22.32-15.88,30.38.27-5.5-.05-12.11-1.86-19.08-5.04-19.36-19.74-34.7-37.78-37.78-32.21-9.74-70.59,3.79-99.08,18.29-3.87,1.95-9.52-2.77-11.84-8.16-3.32-7.71-1.63-6.28,2.61-8.49,38.31-20.03,82.01-39.61,123.91-29.7,8.26,1.95,15.96,5.26,23.48,10.54,11.32,7.96,20.21,24.74,16.44,44Z"/><path d="M117.72,304.2c-.81-2.41-1.39-4.78-1.8-7.07-1.61-9.03.93-17.78,5.99-21.74,22.6-17.7,49.85-29.35,75.34-38.6.59-.22,1.09-.28,1.4-.34,2.22-.47,4.95-1.04,7.25,0,1.46.66,2.25,1.74,2.66,2.3,1.56,2.1,1.59,4.31,1.56,5.13-.1,2.67.01,4.69,0,4.82-.45,3.52-.91,10.66-1.41,21.28-.6,3.87-2.16,9.63-6.94,13.96-4.01,3.62-8.33,4.6-14.59,5.87-10.76,2.19-37.21,8.22-47.42,16.56-1.63,1.33-2.97,2.65-4.19,3.96-3.72,3.99-6.39,7.92-7.93,10.36-3.22-3.22-7.25-8.48-9.92-16.47Z"/><path d="M83.33,326.72c2.33-11.99,8.91-22.32,15.88-30.38-.27,5.5.05,12.11,1.86,19.08,5.04,19.36,19.74,34.7,37.78,37.78,32.21,9.74,70.59-3.79,99.08-18.29,3.87-1.95,9.52,2.77,11.84,8.16,3.32,7.71,1.63,6.28-2.61,8.49-38.31,20.03-82.01,39.61-123.91,29.7-8.26-1.95-15.96-5.26-23.48-10.54-11.32-7.96-20.21-24.74-16.44-44Z"/><ellipse cx="255.16" cy="258.86" rx="28.95" ry="28.76"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,155 @@
|
||||
---
|
||||
name: brainstorming
|
||||
description: "在进行任何创造性工作之前,你都必须使用此技能——包括创建功能、构建组件、添加功能或修改行为。在实现之前探索用户意图、需求和设计。"
|
||||
---
|
||||
|
||||
# 通过头脑风暴将想法转化为设计
|
||||
|
||||
通过自然的协作式对话,帮助将想法转化为完整成形的设计和规格说明。
|
||||
|
||||
首先了解当前项目的上下文,然后一次提出一个问题来完善想法。一旦你理解了要构建的内容,就展示设计并获得用户批准。
|
||||
|
||||
<HARD-GATE>
|
||||
在你展示设计并获得用户批准之前,不得调用任何实现技能、编写任何代码、搭建任何项目脚手架或采取任何实现行动。无论项目看起来多么简单,这都适用于每一个项目。
|
||||
</HARD-GATE>
|
||||
|
||||
## 反模式:“这太简单了,不需要设计”
|
||||
|
||||
每个项目都要经过这一流程。待办事项列表、单函数实用工具、配置变更——无一例外。“简单”项目最容易因未经审视的假设而造成最多的工作浪费。设计可以很简短(对于真正简单的项目,只需几句话),但你必须展示设计并获得批准。
|
||||
|
||||
## 清单
|
||||
|
||||
你必须为以下每一项创建一项任务,并按顺序完成它们:
|
||||
|
||||
1. **探索项目上下文**——检查文件、文档和最近的提交
|
||||
2. **在恰当时机提供视觉伴侣**——不要预先提供。当某个问题第一次确实用展示比用描述更清楚时,就在那时提出使用它(用一条单独的消息);获得批准后,它的浏览器标签页会为你打开。如果始终没有出现视觉问题,就绝不要提出使用它。参见下方的“视觉伴侣”一节。
|
||||
3. **提出澄清问题**——一次一个,了解目的、约束和成功标准
|
||||
4. **提出 2-3 种方案**——说明权衡取舍和你的建议
|
||||
5. **展示设计**——按复杂度划分并调整各节内容,每展示一节后都要获得用户批准
|
||||
6. **编写设计文档**——保存到 `docs/superpowers/specs/YYYY-MM-DD-<topic>-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-<topic>-design.md`
|
||||
- (用户对规格说明位置的偏好会覆盖此默认设置)
|
||||
- 如果可用,使用 elements-of-style:writing-clearly-and-concisely 技能
|
||||
- 将设计文档提交到 git
|
||||
|
||||
**规格说明自审:**
|
||||
写完规格说明文档后,以全新的视角审视它:
|
||||
|
||||
1. **占位符扫描:** 是否有任何 "TBD"、"TODO"、未完成的章节或模糊的要求?修复它们。
|
||||
2. **内部一致性:** 是否有任何章节相互矛盾?架构是否与功能描述一致?
|
||||
3. **范围检查:** 范围是否足够聚焦,可以用单个实现计划完成,还是需要进行拆分?
|
||||
4. **歧义检查:** 是否有任何要求可能被理解为两种不同的含义?如果有,选择一种并明确写出。
|
||||
|
||||
直接修复所有问题。无需再次审查——修复后继续推进。
|
||||
**用户审查关卡:**
|
||||
规格审查循环通过后,在继续之前,请用户审查已写好的规格说明:
|
||||
|
||||
> “规格说明已写入 `<path>` 并提交。请审查它,并告诉我在我们开始编写实施计划之前,你是否希望进行任何更改。”
|
||||
|
||||
等待用户回复。如果他们要求更改,就进行更改并重新运行规格审查循环。只有在用户批准后才能继续。
|
||||
|
||||
**实施:**
|
||||
|
||||
- 调用 writing-plans 技能来创建详细的实施计划
|
||||
- 不要调用任何其他技能。writing-plans 是下一步。
|
||||
|
||||
## 关键原则
|
||||
|
||||
- **一次只问一个问题** - 不要用多个问题让用户不知所措
|
||||
- **优先使用选择题** - 条件允许时,选择题比开放式问题更容易回答
|
||||
- **坚决贯彻 YAGNI** - 从所有设计中移除不必要的功能
|
||||
- **探索替代方案** - 在定案前始终提出 2-3 种方案
|
||||
- **增量验证** - 展示设计,获得批准后再继续
|
||||
- **保持灵活** - 当某些内容不合理时,返回并加以澄清
|
||||
|
||||
## 可视化伴侣
|
||||
|
||||
一个基于浏览器的伴侣工具,用于在头脑风暴期间展示原型图、图表和可视化选项。它作为工具提供——而不是一种模式。同意使用该伴侣工具,意味着它可用于那些采用可视化方式会更有帮助的问题;这并不意味着每个问题都要通过浏览器进行。
|
||||
|
||||
**提供伴侣工具(即时):** 不要一开始就提出使用它。等到某个问题确实用展示的方式会比口头描述更清楚时——真正涉及原型图 / 布局 / 图表的问题,而不仅仅是一个 UI *话题*。第一次出现这种情况时,就在那时提出使用它,并且该提议要单独作为一条消息:
|
||||
> “接下来的这部分如果展示给你看,可能会更容易理解——在我们推进的过程中,我可以在浏览器标签页中制作原型图、图表和对比内容。它仍是新功能,并且可能会消耗大量 token。你想让我这样做吗?我会为你打开它。”
|
||||
|
||||
**此提议必须单独作为一条消息发送。** 只发送该提议——不要包含澄清问题、摘要或任何其他内容。等待用户回复。如果他们接受,使用 `--open` 启动服务器,以便他们的浏览器自动打开第一个界面。如果他们拒绝,则继续仅使用文本,并且不要再次提出使用它,除非他们主动提起。
|
||||
**逐问题决策:** 即使用户接受了,也要针对每个问题分别决定使用浏览器还是终端。判断标准是:**相比阅读,用户通过观看是否能更好地理解这一点?**
|
||||
|
||||
- **使用浏览器**呈现确实属于视觉内容的内容——模型图、线框图、布局对比、架构图、并排的视觉设计
|
||||
- **使用终端**呈现文本内容——需求问题、概念性选择、权衡清单、A/B/C/D 文本选项、范围决策
|
||||
|
||||
关于 UI 主题的问题并不自动等同于视觉问题。“在此上下文中,个性意味着什么?”是一个概念性问题——使用终端。“哪种向导布局效果更好?”是一个视觉问题——使用浏览器。
|
||||
|
||||
如果他们同意使用视觉伴侣,请在继续之前阅读详细指南:
|
||||
`skills/brainstorming/visual-companion.md`
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Superpowers 头脑风暴</title>
|
||||
<style>
|
||||
/*
|
||||
* BRAINSTORM COMPANION FRAME TEMPLATE
|
||||
*
|
||||
* This template provides a consistent frame with:
|
||||
* - OS-aware light/dark theming
|
||||
* - Header branding and connection status
|
||||
* - Scrollable main content area
|
||||
* - CSS helpers for common UI patterns
|
||||
*
|
||||
* Content is injected via placeholder comment in #frame-content.
|
||||
*/
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
||||
/* ===== THEME VARIABLES ===== */
|
||||
:root {
|
||||
--bg-primary: #f5f5f7;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e5e5e7;
|
||||
--border: #d1d1d6;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #aeaeb2;
|
||||
--accent: #0071e3;
|
||||
--accent-hover: #0077ed;
|
||||
--success: #34c759;
|
||||
--warning: #ff9f0a;
|
||||
--error: #ff3b30;
|
||||
--selected-bg: #e8f4fd;
|
||||
--selected-border: #0071e3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1d1d1f;
|
||||
--bg-secondary: #2d2d2f;
|
||||
--bg-tertiary: #3d3d3f;
|
||||
--border: #424245;
|
||||
--text-primary: #f5f5f7;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #636366;
|
||||
--accent: #0a84ff;
|
||||
--accent-hover: #409cff;
|
||||
--selected-bg: rgba(10, 132, 255, 0.15);
|
||||
--selected-border: #0a84ff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== FRAME STRUCTURE ===== */
|
||||
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; color: var(--text-secondary); line-height: 1; }
|
||||
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
|
||||
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
|
||||
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; flex-shrink: 0; filter: invert(1); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.brand-logo { filter: none; }
|
||||
}
|
||||
.status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; justify-self: end; white-space: nowrap; line-height: 1; }
|
||||
.status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#frame-content { padding: 2rem; min-height: 100%; }
|
||||
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 42px;
|
||||
}
|
||||
.header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; }
|
||||
.header .status { grid-column: 2; line-height: 1; }
|
||||
.header span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.header .selected-text {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== TYPOGRAPHY ===== */
|
||||
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
|
||||
.section { margin-bottom: 2rem; }
|
||||
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== OPTIONS (for A/B/C choices) ===== */
|
||||
.options { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.option {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.option:hover { border-color: var(--accent); }
|
||||
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
|
||||
.option .letter {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
width: 1.75rem; height: 1.75rem;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
|
||||
}
|
||||
.option.selected .letter { background: var(--accent); color: white; }
|
||||
.option .content { flex: 1; }
|
||||
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
|
||||
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
|
||||
|
||||
/* ===== CARDS (for showing designs/mockups) ===== */
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
.card.selected { border-color: var(--selected-border); border-width: 2px; }
|
||||
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
|
||||
.card-body { padding: 1rem; }
|
||||
.card-body h3 { margin-bottom: 0.25rem; }
|
||||
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
|
||||
|
||||
/* ===== MOCKUP CONTAINER ===== */
|
||||
.mockup {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.mockup-header {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.mockup-body { padding: 1.5rem; }
|
||||
|
||||
/* ===== SPLIT VIEW (side-by-side comparison) ===== */
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||||
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ===== PROS/CONS ===== */
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
|
||||
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
|
||||
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
|
||||
.pros li, .cons li { margin-bottom: 0.25rem; }
|
||||
|
||||
/* ===== PLACEHOLDER (for mockup areas) ===== */
|
||||
.placeholder {
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== INLINE MOCKUP ELEMENTS ===== */
|
||||
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
|
||||
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
|
||||
.mock-content { padding: 1.5rem; flex: 1; }
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<!-- BRANDING -->
|
||||
<div class="status">正在连接…</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div id="frame-content">
|
||||
<!-- CONTENT -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = '<div style="max-width:480px">' +
|
||||
'<h2 style="margin:0 0 .5rem;font-weight:600">伴侣已暂停</h2>' +
|
||||
'<p style="margin:0;opacity:.85">头脑风暴伴侣已停止。' +
|
||||
'请让你的编程 Agent 重新启动它——本页面会自动重新连接。</p></div>';
|
||||
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();
|
||||
})();
|
||||
@@ -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(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>头脑风暴伴侣</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }
|
||||
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; margin-bottom: 1.5rem; color: #666; font-size: 0.9rem; line-height: 1; }
|
||||
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
|
||||
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
|
||||
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; filter: invert(1); }
|
||||
</style>
|
||||
</head>
|
||||
<body><!-- BRANDING --><h1>头脑风暴伴侣</h1>
|
||||
<p>正在等待 Agent 推送界面…</p></body></html>`);
|
||||
}
|
||||
|
||||
const FORBIDDEN_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>需要会话密钥</title>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; } code { background: #f0f0f0; padding: 0.1em 0.3em; border-radius: 4px; }</style>
|
||||
</head>
|
||||
<body><h1>需要会话密钥</h1>
|
||||
<p>本页面需要编程 Agent 提供的完整 URL,其中必须包含
|
||||
<code>?key=…</code> 部分。请复制完整 URL 后重新打开。</p></body></html>`;
|
||||
|
||||
function bootstrapPage(key) {
|
||||
const jsonKey = JSON.stringify(String(key));
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>正在打开头脑风暴伴侣</title></head>
|
||||
<body>
|
||||
<script>
|
||||
try { sessionStorage.setItem('brainstorm-session-key', ${jsonKey}); } catch (e) {}
|
||||
location.replace('/');
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
// ========== 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, '>')
|
||||
.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
|
||||
? ''
|
||||
: '<img class="brand-logo" src="' + SUPERPOWERS_BRAND_IMAGE_URL + '?v=' + encodeURIComponent(SUPERPOWERS_VERSION) + '" alt="Prime Radiant" referrerpolicy="no-referrer" decoding="async">';
|
||||
|
||||
return '<div class="brand"><a href="https://github.com/obra/superpowers">' + logo + '<span class="brand-copy">' + text + '</span></a></div>';
|
||||
}
|
||||
|
||||
function renderBranding(html) {
|
||||
return html.split('<!-- BRANDING -->').join(brandMarkup());
|
||||
}
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return renderBranding(frameTemplate).replace('<!-- CONTENT -->', 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('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
} 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
|
||||
};
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the brainstorm server and output connection info
|
||||
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-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 <path> Store session files under <path>/.superpowers/brainstorm/
|
||||
# instead of /tmp. Files persist after server stops.
|
||||
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
|
||||
# Use 0.0.0.0 in remote/containerized environments.
|
||||
# --url-host <host> Hostname shown in returned URL JSON.
|
||||
# --idle-timeout-minutes <n> 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
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the brainstorm server and clean up
|
||||
# Usage: stop-server.sh <session_dir>
|
||||
#
|
||||
# Kills the server process. Only deletes session directory if it's
|
||||
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
|
||||
# kept so mockups can be reviewed later.
|
||||
|
||||
SESSION_DIR="$1"
|
||||
|
||||
if [[ -z "$SESSION_DIR" ]]; then
|
||||
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
|
||||
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
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# 规范文档审查员提示词模板
|
||||
|
||||
派遣规范文档审查员子 Agent 时,请使用此模板。
|
||||
|
||||
**目的:** 验证规范是否完整、一致,并已准备好进入实施规划阶段。
|
||||
|
||||
**派遣时机:** 规范文档已写入 docs/superpowers/specs/ 后
|
||||
|
||||
```
|
||||
子 Agent (general-purpose):
|
||||
description: "审查规范文档"
|
||||
prompt: |
|
||||
你是一名规范文档审查员。请验证此规范是否完整并已准备好进入规划阶段。
|
||||
|
||||
**待审查的规范:** [SPEC_FILE_PATH]
|
||||
|
||||
## 检查内容
|
||||
|
||||
| 类别 | 检查要点 |
|
||||
|----------|------------------|
|
||||
| 完整性 | TODOs、占位符、"TBD"、不完整的章节 |
|
||||
| 一致性 | 内部矛盾、相互冲突的要求 |
|
||||
| 清晰度 | 要求是否含糊到足以导致他人构建出错误的内容 |
|
||||
| 范围 | 是否足够聚焦,可由单个计划涵盖——而不是覆盖多个相互独立的子系统 |
|
||||
| YAGNI | 未请求的功能、过度工程化 |
|
||||
|
||||
## 校准
|
||||
|
||||
**只标记会在实施规划期间造成实际问题的问题。**
|
||||
缺少章节、存在矛盾,或某项要求含糊到可能被解读为两种不同的含义——
|
||||
这些才是问题。轻微的措辞改进、风格偏好以及“某些章节不如其他章节详细”均不属于问题。
|
||||
|
||||
除非存在会导致计划有缺陷的严重缺漏,否则应予以批准。
|
||||
|
||||
## 输出格式
|
||||
|
||||
## 规范审查
|
||||
|
||||
**状态:** 通过 | 发现问题
|
||||
|
||||
**问题(如有):**
|
||||
- [第 X 节]:[具体问题] — [它为何会影响规划]
|
||||
|
||||
**建议(仅供参考,不阻碍批准):**
|
||||
- [改进建议]
|
||||
```
|
||||
|
||||
**审查员返回:** 状态、问题(如有)、建议
|
||||
@@ -0,0 +1,281 @@
|
||||
# 可视化伴侣指南
|
||||
|
||||
基于浏览器的可视化头脑风暴伴侣,用于展示模型稿、图示和选项。
|
||||
|
||||
## 何时使用
|
||||
|
||||
针对每个问题而非每个会话进行决定。判断标准是:**用户通过看它而不是阅读它,是否能理解得更好?**
|
||||
|
||||
当内容本身是可视化内容时,**使用浏览器**:
|
||||
|
||||
- **UI 模型稿**——线框图、布局、导航结构、组件设计
|
||||
- **架构图**——系统组件、数据流、关系图
|
||||
- **并排视觉比较**——比较两种布局、两套配色方案、两个设计方向
|
||||
- **设计润色**——当问题涉及观感、间距、视觉层级时
|
||||
- **空间关系**——以图示形式呈现的状态机、流程图、实体关系
|
||||
|
||||
当内容是文本或表格时,**使用终端**:
|
||||
|
||||
- **需求和范围问题**——“X 是什么意思?”、“哪些功能在范围内?”
|
||||
- **概念性 A/B/C 选择**——在用文字描述的方案之间进行选择
|
||||
- **权衡清单**——优缺点、比较表
|
||||
- **技术决策**——API 设计、数据建模、架构方案选择
|
||||
- **澄清问题**——任何答案是文字而非视觉偏好的问题
|
||||
|
||||
一个*关于* UI 主题的问题并不自动等同于视觉问题。“你想要哪种向导?”是概念性问题——使用终端。“这些向导布局中,哪一种感觉合适?”是视觉问题——使用浏览器。
|
||||
|
||||
## 工作原理
|
||||
|
||||
服务器会监视一个目录中的 HTML 文件,并将最新的文件提供给浏览器。你将 HTML 内容写入 `screen_dir`,用户会在其浏览器中看到内容,并可通过点击选择选项。选择结果会记录到 `state_dir/events`,供你在下一轮读取。
|
||||
|
||||
**内容片段与完整文档:**如果你的 HTML 文件以 `<!DOCTYPE` 或 `<html` 开头,服务器会按原样提供该文件(仅注入辅助脚本)。否则,服务器会自动将你的内容包装在框架模板中——添加页眉、CSS 主题、连接状态以及所有交互基础设施。**默认编写内容片段。**仅在需要完全控制页面时编写完整文档。
|
||||
|
||||
## 启动会话
|
||||
|
||||
```bash
|
||||
# 请在用户批准使用伴侣之后再启动。--open 会在显示首个屏幕时自动打开用户的浏览器;
|
||||
# --project-dir 会持久保存模型稿,并支持使用同一端口重启。
|
||||
scripts/start-server.sh --project-dir /path/to/project --open
|
||||
|
||||
# 返回:{"type":"server-started","port":52341,
|
||||
# "url":"http://localhost:52341/?key=ab12…",
|
||||
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
|
||||
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
|
||||
```
|
||||
|
||||
保存响应中的 `screen_dir` 和 `state_dir`。使用 `--open` 时,当你推送首个屏幕,浏览器会自动打开——你不需要让用户手动打开,但仍应分享该 URL 作为备用方案(无头/远程环境不会自动打开)。
|
||||
|
||||
**该 URL 包含会话密钥(`?key=…`)。**服务器会拒绝任何不含该密钥的请求,因此始终向用户提供 `url` 字段中的**完整** URL——绝不要移除查询字符串,也绝不要提供不带查询字符串的 `http://host:port`。该密钥控制 HTTP 和 WebSocket 访问,因此意外打开的浏览器标签页或网络上的其他机器无法读取屏幕或注入事件。首次加载后,浏览器会通过 cookie 记住该密钥,因此重新加载以及访问 `/files/*` 资源时无需再次提供它。
|
||||
|
||||
**查找连接信息:**服务器会将其启动 JSON 写入 `$STATE_DIR/server-info`。如果你在后台启动了服务器但没有捕获 stdout,请读取该文件以获取 URL 和端口。使用 `--project-dir` 时,请在 `<project>/.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
|
||||
<!-- 文件名:waiting.html(或 waiting-2.html 等) -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">继续在终端中...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
这样可避免在对话已经继续推进后,用户仍盯着一个已经解决的选择。当下一个可视化问题出现时,像往常一样推送一个新的内容文件。
|
||||
|
||||
6. 重复上述步骤,直到完成。
|
||||
|
||||
## 编写内容片段
|
||||
|
||||
只编写放入页面内部的内容。服务器会自动使用框架模板将其包装起来(页眉、主题 CSS、连接状态以及所有交互基础设施)。
|
||||
|
||||
**最小示例:**
|
||||
|
||||
```html
|
||||
<h2>哪种布局更合适?</h2>
|
||||
<p class="subtitle">请考虑可读性和视觉层级</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>单栏</h3>
|
||||
<p>简洁、专注的阅读体验</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>双栏</h3>
|
||||
<p>侧边栏导航搭配主内容区</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
就这些。不需要 `<html>`、CSS,也不需要 `<script>` 标签。服务器会提供所有这些内容。
|
||||
|
||||
## 可用的 CSS 类
|
||||
框架模板为你的内容提供以下 CSS 类:
|
||||
|
||||
### 选项(A/B/C 选择)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>标题</h3>
|
||||
<p>描述</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**多选:**向容器添加 `data-multiselect`,以允许用户选择多个选项。每次点击都会切换该项目的选中样式。
|
||||
|
||||
```html
|
||||
<div class="options" data-multiselect>
|
||||
<!-- 相同的选项标记——用户可以选择/取消选择多个选项 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 卡片(视觉设计)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
|
||||
<div class="card-image"><!-- 模型内容 --></div>
|
||||
<div class="card-body">
|
||||
<h3>名称</h3>
|
||||
<p>描述</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 模型容器
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">预览:仪表板布局</div>
|
||||
<div class="mockup-body"><!-- 你的样稿 HTML --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 拆分视图(并排)
|
||||
|
||||
```html
|
||||
<div class="split">
|
||||
<div class="mockup"><!-- 左侧 --></div>
|
||||
<div class="mockup"><!-- 右侧 --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 优点/缺点
|
||||
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>优点</h4><ul><li>益处</li></ul></div>
|
||||
<div class="cons"><h4>缺点</h4><ul><li>弊端</li></ul></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 样稿元素(线框图构建块)
|
||||
|
||||
```html
|
||||
<div class="mock-nav">徽标 | 首页 | 关于 | 联系</div>
|
||||
<div style="display: flex;">
|
||||
<div class="mock-sidebar">导航</div>
|
||||
<div class="mock-content">主要内容区域</div>
|
||||
</div>
|
||||
<button class="mock-button">操作按钮</button>
|
||||
<input class="mock-input" placeholder="输入字段">
|
||||
<div class="placeholder">占位区域</div>
|
||||
```
|
||||
|
||||
### 排版与分区
|
||||
- `h2` — 页面标题
|
||||
- `h3` — 小节标题
|
||||
- `.subtitle` — 标题下方的次要文本
|
||||
- `.section` — 带底部外边距的内容块
|
||||
- `.label` — 小号全大写标签文本
|
||||
|
||||
## 浏览器事件格式
|
||||
|
||||
当用户在浏览器中点击选项时,其交互会被记录到 `$STATE_DIR/events`(每行一个 JSON 对象)。当你推送新屏幕时,该文件会自动清空。
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"选项 A - 简单布局","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"选项 C - 复杂网格","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"选项 B - 混合布局","timestamp":1706000115}
|
||||
```
|
||||
|
||||
完整的事件流会显示用户的探索路径——他们可能会先点击多个选项,再最终确定。最后一个 `choice` 事件通常是最终选择,但点击模式可能会体现出犹豫或偏好,值得进一步询问。
|
||||
|
||||
如果 `$STATE_DIR/events` 不存在,则说明用户没有与浏览器交互——仅使用他们在终端中输入的文本。
|
||||
|
||||
## 设计技巧
|
||||
|
||||
- **根据问题调整保真度**——布局问题使用线框图,细节打磨问题使用精细设计
|
||||
- **在每个页面上说明问题**——使用“哪个布局感觉更专业?”,而不只是“选择一个”
|
||||
- **先迭代,再继续推进**——如果反馈会改变当前屏幕,请编写一个新版本
|
||||
- 每个屏幕最多提供 **2-4 个选项**
|
||||
- **在真实内容很重要时使用真实内容**——对于摄影作品集,应使用真实图片(Unsplash)。占位内容会掩盖设计问题。
|
||||
- **保持模型简单**——专注于布局和结构,而不是像素级完美的设计
|
||||
|
||||
## 文件命名
|
||||
|
||||
- 使用语义化名称:`platform.html`、`visual-style.html`、`layout.html`
|
||||
- 切勿重复使用文件名——每个屏幕都必须是一个新文件
|
||||
- 对于迭代版本:附加版本后缀,例如 `layout-v2.html`、`layout-v3.html`
|
||||
- 服务器按修改时间提供最新的文件
|
||||
|
||||
## 清理
|
||||
|
||||
```bash
|
||||
scripts/stop-server.sh $SESSION_DIR
|
||||
```
|
||||
|
||||
如果会话使用了 `--project-dir`,模型文件会保留在 `.superpowers/brainstorm/` 中,以供后续参考。只有 `/tmp` 中的会话会在停止时被删除。
|
||||
|
||||
## 参考
|
||||
|
||||
- 框架模板(CSS 参考):`scripts/frame-template.html`
|
||||
- 辅助脚本(客户端):`scripts/helper.js`
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: dispatching-parallel-agents
|
||||
description: 当面对 2+ 个彼此独立、无需共享状态或顺序依赖即可处理的任务时使用
|
||||
---
|
||||
|
||||
# 派遣并行 Agent
|
||||
|
||||
## 概述
|
||||
|
||||
你将任务委派给上下文相互隔离的专门 Agent。通过精确设计给它们的指令和上下文,你可以确保它们保持专注并成功完成任务。它们绝不应继承你的会话上下文或历史记录——你要准确构建它们所需的内容。这也能保留你自己的上下文,以便进行协调工作。
|
||||
|
||||
当你遇到多个互不相关的失败(不同的测试文件、不同的子系统、不同的 bug)时,依次调查它们会浪费时间。每项调查都彼此独立,可以并行进行。
|
||||
|
||||
**核心原则:** 每个独立的问题领域派遣一个 Agent。让它们并发工作。
|
||||
|
||||
## 何时使用
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Multiple failures?" [shape=diamond, label="存在多个失败?"];
|
||||
"Are they independent?" [shape=diamond, label="它们是否彼此独立?"];
|
||||
"Single agent investigates all" [shape=box, label="由单个 Agent 调查全部问题"];
|
||||
"One agent per problem domain" [shape=box, label="每个问题领域一个 Agent"];
|
||||
"Can they work in parallel?" [shape=diamond, label="它们能否并行工作?"];
|
||||
"Sequential agents" [shape=box, label="依次派遣 Agent"];
|
||||
"Parallel dispatch" [shape=box, label="并行派遣"];
|
||||
|
||||
"Multiple failures?" -> "Are they independent?" [label="是"];
|
||||
"Are they independent?" -> "Single agent investigates all" [label="否——相互关联"];
|
||||
"Are they independent?" -> "Can they work in parallel?" [label="是"];
|
||||
"Can they work in parallel?" -> "Parallel dispatch" [label="是"];
|
||||
"Can they work in parallel?" -> "Sequential agents" [label="否——共享状态"];
|
||||
}
|
||||
```
|
||||
|
||||
**适用情形:**
|
||||
- 3 个以上测试文件因不同的根本原因而失败
|
||||
- 多个子系统各自独立损坏
|
||||
- 每个问题都能在不依赖其他问题上下文的情况下得到理解
|
||||
- 各项调查之间不存在共享状态
|
||||
|
||||
**不适用情形:**
|
||||
- 失败相互关联(修复一个可能会修复其他失败)
|
||||
- 需要理解完整的系统状态
|
||||
- Agent 之间会相互干扰
|
||||
|
||||
## 模式
|
||||
|
||||
### 1. 识别独立领域
|
||||
|
||||
按照损坏的部分对失败进行分组:
|
||||
- 文件 A 测试:工具审批流程
|
||||
- 文件 B 测试:批量完成行为
|
||||
- 文件 C 测试:中止功能
|
||||
|
||||
每个领域都是独立的——修复工具审批不会影响中止测试。
|
||||
|
||||
### 2. 创建聚焦的 Agent 任务
|
||||
|
||||
每个 Agent 获得:
|
||||
- **具体范围:** 一个测试文件或子系统
|
||||
- **明确目标:** 让这些测试通过
|
||||
- **约束:** 不要更改其他代码
|
||||
- **预期输出:** 总结你发现并修复的内容
|
||||
|
||||
### 3. 并行分派
|
||||
|
||||
在同一条响应中发出全部三个子 Agent 分派——它们会并行运行:
|
||||
|
||||
```text
|
||||
子 Agent (general-purpose): "修复 agent-tool-abort.test.ts 的失败"
|
||||
子 Agent (general-purpose): "修复 batch-completion-behavior.test.ts 的失败"
|
||||
子 Agent (general-purpose): "修复 tool-approval-race-conditions.test.ts 的失败"
|
||||
# 三者并发运行。
|
||||
```
|
||||
|
||||
一条响应中进行多次分派调用 = 并行执行。每条响应一次 = 顺序执行。
|
||||
|
||||
### 4. 审查并集成
|
||||
|
||||
当各 Agent 返回时:
|
||||
- 阅读每份总结
|
||||
- 验证各项修复不会冲突
|
||||
- 运行完整测试套件
|
||||
- 集成所有更改
|
||||
|
||||
## Agent 提示词结构
|
||||
|
||||
良好的 Agent 提示词应当:
|
||||
1. **聚焦** - 一个明确的问题领域
|
||||
2. **独立完备** - 包含理解问题所需的全部上下文
|
||||
3. **明确说明输出** - Agent 应返回什么?
|
||||
|
||||
```markdown
|
||||
修复 src/agents/agent-tool-abort.test.ts 中 3 个失败的测试:
|
||||
|
||||
1. "应在捕获部分输出的情况下中止工具" - 期望消息中包含 '中断于'
|
||||
2. "应处理混合的已完成和已中止工具" - 快速工具被中止,而不是完成
|
||||
3. "应正确跟踪 pendingToolCount" - 期望得到 3 个结果,但实际得到 0 个
|
||||
|
||||
这些是时序/竞态条件问题。你的任务:
|
||||
|
||||
1. 阅读测试文件并理解每个测试验证的内容
|
||||
2. 找出根本原因 - 是时序问题还是实际缺陷?
|
||||
3. 通过以下方式修复:
|
||||
- 用基于事件的等待替换任意超时
|
||||
- 如果发现中止实现中存在缺陷,则修复它们
|
||||
- 如果测试的是已变更的行为,则调整测试预期
|
||||
|
||||
不要只是增加超时时间 - 要找出真正的问题。
|
||||
|
||||
返回:你发现了什么以及修复了什么的摘要。
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
**❌ 范围太宽泛:** "修复所有测试" - Agent 会迷失方向
|
||||
**✅ 具体:** "修复 agent-tool-abort.test.ts" - 范围明确
|
||||
|
||||
**❌ 没有上下文:** "修复竞态条件" - Agent 不知道在哪里
|
||||
**✅ 上下文:** 粘贴错误消息和测试名称
|
||||
|
||||
**❌ 无约束:** Agent 可能会重构所有内容
|
||||
**✅ 约束:** "不要更改生产代码" 或 "只修复测试"
|
||||
|
||||
**❌ 模糊的输出:** "修复它" - 你不知道哪些内容发生了变化
|
||||
**✅ 具体的输出:** "返回根本原因和所做更改的摘要"
|
||||
|
||||
## 何时不应使用
|
||||
|
||||
**相关的失败:** 修复一个可能会修复其他失败 - 先一起调查
|
||||
**需要完整上下文:** 要理解问题,需要查看整个系统
|
||||
**探索性调试:** 你还不知道哪里出了问题
|
||||
**共享状态:** Agent 会相互干扰(编辑相同的文件、使用相同的资源)
|
||||
|
||||
## 会话中的真实示例
|
||||
|
||||
**场景:** 大规模重构后,3 个文件中出现 6 个测试失败
|
||||
|
||||
**失败:**
|
||||
- agent-tool-abort.test.ts: 3 个失败(时序问题)
|
||||
- batch-completion-behavior.test.ts: 2 个失败(工具未执行)
|
||||
- tool-approval-race-conditions.test.ts: 1 个失败(执行次数 = 0)
|
||||
|
||||
**决策:** 各领域相互独立 - 中止逻辑、批次完成和竞态条件彼此分离
|
||||
|
||||
**分派:**
|
||||
```
|
||||
Agent 1 → 修复 agent-tool-abort.test.ts
|
||||
Agent 2 → 修复 batch-completion-behavior.test.ts
|
||||
Agent 3 → 修复 tool-approval-race-conditions.test.ts
|
||||
```
|
||||
|
||||
**结果:**
|
||||
- Agent 1:用基于事件的等待替换了超时
|
||||
- Agent 2:修复了事件结构错误(threadId 位于错误的位置)
|
||||
- Agent 3:添加了等待异步工具执行完成的逻辑
|
||||
|
||||
**集成:** 所有修复彼此独立,没有冲突,完整测试套件全部通过
|
||||
|
||||
**节省的时间:** 3 个问题并行解决,而非依次解决
|
||||
|
||||
## 主要优势
|
||||
|
||||
1. **并行化** - 多项调查同时进行
|
||||
2. **专注** - 每个 Agent 的范围都很窄,需要跟踪的上下文更少
|
||||
3. **独立性** - Agent 之间互不干扰
|
||||
4. **速度** - 用解决 1 个问题的时间解决了 3 个问题
|
||||
|
||||
## 验证
|
||||
|
||||
Agent 返回后:
|
||||
1. **审查每份总结** - 了解发生了哪些变更
|
||||
2. **检查冲突** - Agent 是否编辑了相同的代码?
|
||||
3. **运行完整测试套件** - 验证所有修复能否协同工作
|
||||
4. **抽查** - Agent 可能会犯系统性错误
|
||||
|
||||
## 实际影响
|
||||
|
||||
来自调试会话(2025-10-03):
|
||||
- 3 个文件中共有 6 个失败
|
||||
- 并行派遣了 3 个 Agent
|
||||
- 所有调查均并发完成
|
||||
- 所有修复均成功集成
|
||||
- Agent 变更之间零冲突
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: executing-plans
|
||||
description: 当你有一份书面实施计划,需要在单独的会话中执行并设置审查检查点时使用
|
||||
---
|
||||
|
||||
# 执行计划
|
||||
|
||||
## 概述
|
||||
|
||||
加载计划,严格审查,执行所有任务,并在完成时报告。
|
||||
|
||||
**开始时宣布:**“我正在使用 executing-plans 技能来实施此计划。”
|
||||
|
||||
**注意:**告诉你的人类伙伴,在能够使用子 Agent 的情况下,Superpowers 的运行效果会好得多。如果在支持子 Agent 的平台上运行,其工作质量将显著提高(Claude Code、Codex CLI、Codex App 和 Copilot CLI 都符合要求;请参阅 `../using-superpowers/references/` 中各平台对应的工具参考文档)。如果子 Agent 可用,请使用 superpowers:subagent-driven-development,而不是此技能。
|
||||
|
||||
## 流程
|
||||
|
||||
### 第 1 步:加载并审查计划
|
||||
1. 阅读计划文件
|
||||
2. 严格审查——找出对计划存在的任何疑问或担忧
|
||||
3. 如果有疑虑:在开始之前向你的人类伙伴提出
|
||||
4. 如果没有疑虑:为计划项创建待办事项,然后继续
|
||||
|
||||
### 第 2 步:执行任务
|
||||
|
||||
对于每个任务:
|
||||
1. 标记为 in_progress
|
||||
2. 严格遵循每一个步骤(计划中的步骤都已拆分为小步骤)
|
||||
3. 按照规定运行验证
|
||||
4. 标记为 completed
|
||||
|
||||
### 第 3 步:完成开发
|
||||
|
||||
在所有任务均已完成并通过验证后:
|
||||
- 宣布:“我正在使用 finishing-a-development-branch 技能来完成这项工作。”
|
||||
- **必需的子技能:**使用 superpowers:finishing-a-development-branch
|
||||
- 按照该技能验证测试、提供选项并执行所选方案
|
||||
|
||||
## 何时停止并寻求帮助
|
||||
|
||||
**出现以下情况时,立即停止执行:**
|
||||
- 遇到阻碍(缺少依赖项、测试失败、指令不明确)
|
||||
- 计划存在导致无法开始的关键缺口
|
||||
- 你不理解某条指令
|
||||
- 验证反复失败
|
||||
|
||||
**请求澄清,而不是猜测。**
|
||||
|
||||
## 何时重新回到先前步骤
|
||||
|
||||
**在以下情况下返回审查(第 1 步):**
|
||||
- 人类伙伴根据你的反馈更新了计划
|
||||
- 基本方法需要重新考虑
|
||||
|
||||
**不要强行突破阻碍**——停止并询问。
|
||||
|
||||
## 请记住
|
||||
- 首先严格审查计划
|
||||
- 严格遵循计划步骤
|
||||
- 不要跳过验证
|
||||
- 当计划要求引用技能时,引用相应技能
|
||||
- 遇到阻碍时停止,不要猜测
|
||||
- 未经用户明确同意,绝不要在 main/master 分支上开始实施
|
||||
|
||||
## 集成
|
||||
|
||||
**必需的工作流技能:**
|
||||
- **superpowers:using-git-worktrees**——确保使用隔离的工作区(创建一个工作树或验证现有工作树)
|
||||
- **superpowers:writing-plans**——创建由此技能执行的计划
|
||||
- **superpowers:finishing-a-development-branch**——在所有任务完成后完成开发
|
||||
@@ -0,0 +1,237 @@
|
||||
---
|
||||
name: finishing-a-development-branch
|
||||
description: 在实现完成、所有测试均通过,并且你需要决定如何集成这些工作时使用——通过提供合并、PR 或清理的结构化选项,引导完成开发工作
|
||||
---
|
||||
|
||||
# 完成开发分支
|
||||
|
||||
## 概述
|
||||
|
||||
通过提供清晰的选项并处理所选择的工作流,引导完成开发工作。
|
||||
|
||||
**核心原则:** 验证测试 → 检测环境 → 提供选项 → 执行选择 → 清理。
|
||||
|
||||
**开始时宣布:** “我正在使用 finishing-a-development-branch 技能来完成这项工作。”
|
||||
|
||||
## 流程
|
||||
|
||||
### 第 1 步:验证测试
|
||||
|
||||
**在提供选项之前,验证测试是否通过:**
|
||||
|
||||
```bash
|
||||
# 运行项目的测试套件
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**如果测试失败:**
|
||||
```
|
||||
测试失败(<N> 个失败)。必须先修复才能完成:
|
||||
[显示失败信息]
|
||||
|
||||
在测试通过之前,无法继续合并/PR。
|
||||
```
|
||||
|
||||
停止。不要继续执行步骤 2。
|
||||
|
||||
**如果测试通过:** 继续执行步骤 2。
|
||||
|
||||
### 步骤 2:检测环境
|
||||
|
||||
**在展示选项之前确定工作区状态:**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
这决定了要显示哪个菜单以及如何进行清理:
|
||||
|
||||
| 状态 | 菜单 | 清理 |
|
||||
|-------|------|---------|
|
||||
| `GIT_DIR == GIT_COMMON`(普通仓库) | 标准的 4 个选项 | 没有要清理的工作树 |
|
||||
| `GIT_DIR != GIT_COMMON`,命名分支 | 标准的 4 个选项 | 基于来源(见步骤 6) |
|
||||
| `GIT_DIR != GIT_COMMON`,分离 HEAD | 精简的 3 个选项(无合并) | 不清理(由外部管理) |
|
||||
|
||||
### 步骤 3:确定基础分支
|
||||
|
||||
```bash
|
||||
# 尝试常见的基础分支
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
```
|
||||
|
||||
或者询问:“这个分支是从 main 分出来的——对吗?”
|
||||
|
||||
### 步骤 4:呈现选项
|
||||
|
||||
**普通仓库和命名分支工作树——必须原样呈现以下 4 个选项:**
|
||||
|
||||
```
|
||||
实现已完成。您想怎么做?
|
||||
|
||||
1. 在本地合并回 <base-branch>
|
||||
2. 推送并创建拉取请求
|
||||
3. 保持分支原样(我稍后会处理)
|
||||
4. 丢弃此工作
|
||||
|
||||
请选择哪个选项?
|
||||
```
|
||||
|
||||
**detached HEAD——必须原样呈现以下 3 个选项:**
|
||||
|
||||
```
|
||||
实现已完成。您当前处于 detached HEAD(外部管理的工作区)。
|
||||
|
||||
1. 作为新分支推送并创建拉取请求
|
||||
2. 保持原样(我稍后会处理)
|
||||
3. 丢弃此工作
|
||||
|
||||
请选择哪个选项?
|
||||
```
|
||||
|
||||
**不要添加解释**——保持选项简洁。
|
||||
|
||||
### 步骤 5:执行选择
|
||||
|
||||
#### 选项 1:在本地合并
|
||||
|
||||
```bash
|
||||
# 获取主仓库根目录,以确保 CWD 安全
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
|
||||
# 先合并——在移除任何内容之前验证是否成功
|
||||
git checkout <base-branch>
|
||||
git pull
|
||||
git merge <feature-branch>
|
||||
|
||||
# 验证合并结果上的测试
|
||||
<test command>
|
||||
|
||||
# 仅在合并成功后:清理工作树(步骤 6),然后删除分支
|
||||
```
|
||||
|
||||
然后:清理工作树(步骤 6),然后删除分支:
|
||||
|
||||
```bash
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
#### 选项 2:推送并创建 PR
|
||||
|
||||
```bash
|
||||
# 推送分支
|
||||
git push -u origin <feature-branch>
|
||||
```
|
||||
|
||||
**不要清理工作树**——用户需要保留它,以便根据 PR 反馈进行迭代。
|
||||
|
||||
#### 选项 3:保持原样
|
||||
|
||||
报告:"保留分支 <name>。工作树保留在 <path>。"
|
||||
|
||||
**不要清理工作树。**
|
||||
|
||||
#### 选项 4:丢弃
|
||||
|
||||
**请先确认:**
|
||||
```
|
||||
这将永久删除:
|
||||
- 分支 <name>
|
||||
- 所有提交:<commit-list>
|
||||
- 位于 <path> 的工作树
|
||||
|
||||
输入 'discard' 以确认。
|
||||
```
|
||||
|
||||
等待完全一致的确认。
|
||||
|
||||
如果已确认:
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
```
|
||||
|
||||
然后:清理工作树(步骤 6),再强制删除分支:
|
||||
```bash
|
||||
git branch -D <feature-branch>
|
||||
```
|
||||
|
||||
### 步骤 6:清理工作区
|
||||
|
||||
**仅针对选项 1 和 4 运行。** 选项 2 和 3 始终保留工作树。
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
||||
```
|
||||
|
||||
**如果 `GIT_DIR == GIT_COMMON`:** 普通仓库,没有需要清理的工作树。完成。
|
||||
|
||||
**如果工作树路径位于 `.worktrees/` 或 `worktrees/` 下:** Superpowers 创建了此工作树——清理由我们负责。
|
||||
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
git worktree remove "$WORKTREE_PATH"
|
||||
git worktree prune # 自修复:清理所有陈旧的注册记录
|
||||
```
|
||||
**否则:** 主机环境(运行平台)拥有此工作区。切勿移除它。如果你的平台提供 workspace-exit 工具,请使用它。否则,将工作区保留在原处。
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 选项 | 合并 | 推送 | 保留工作树 | 清理分支 |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. 本地合并 | 是 | - | - | 是 |
|
||||
| 2. 创建 PR | - | 是 | 是 | - |
|
||||
| 3. 保持原样 | - | - | 是 | - |
|
||||
| 4. 丢弃 | - | - | - | 是(强制) |
|
||||
|
||||
## 常见错误
|
||||
|
||||
**跳过测试验证**
|
||||
- **问题:** 合并有问题的代码,创建会失败的 PR
|
||||
- **修复:** 在提供选项之前始终验证测试
|
||||
|
||||
**开放式问题**
|
||||
- **问题:** “接下来我该怎么做?”含义不明确
|
||||
- **修复:** 恰好提供 4 个结构化选项(对于 detached HEAD 则提供 3 个)
|
||||
|
||||
**为选项 2 清理工作树**
|
||||
- **问题:** 移除用户进行 PR 迭代所需的工作树
|
||||
- **修复:** 仅对选项 1 和 4 执行清理
|
||||
|
||||
**在移除工作树之前删除分支**
|
||||
- **问题:** `git branch -d` 失败,因为工作树仍在引用该分支
|
||||
- **修复:** 先合并,再移除工作树,然后删除分支
|
||||
**在工作树内部运行 git worktree remove**
|
||||
- **问题:** 当 CWD 位于正被移除的工作树内时,命令会静默失败
|
||||
- **修复:** 在执行 `git worktree remove` 前,始终先 `cd` 到主仓库根目录
|
||||
|
||||
**清理运行平台拥有的工作树**
|
||||
- **问题:** 移除由运行平台创建的工作树会导致幽灵状态
|
||||
- **修复:** 仅清理 `.worktrees/` 或 `worktrees/` 下的工作树
|
||||
|
||||
**丢弃操作没有确认**
|
||||
- **问题:** 意外删除工作成果
|
||||
- **修复:** 要求输入 "discard" 进行确认
|
||||
|
||||
## 红旗项
|
||||
|
||||
**绝不:**
|
||||
- 在测试失败时继续
|
||||
- 未验证合并结果上的测试就进行合并
|
||||
- 未经确认就删除工作成果
|
||||
- 未经明确请求就强制推送
|
||||
- 在确认合并成功之前移除工作树
|
||||
- 清理并非由你创建的工作树(来源检查)
|
||||
- 从工作树内部运行 `git worktree remove`
|
||||
|
||||
**始终:**
|
||||
- 在提供选项前验证测试
|
||||
- 在显示菜单前检测环境
|
||||
- 恰好提供 4 个选项(分离 HEAD 时提供 3 个)
|
||||
- 对选项 4 获取输入式确认
|
||||
- 仅为选项 1 和 4 清理工作树
|
||||
- 在移除工作树前,先 `cd` 到主仓库根目录
|
||||
- 移除后运行 `git worktree prune`
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
name: receiving-code-review
|
||||
description: 在接收代码审查反馈时、实施建议之前使用,尤其是在反馈看起来不明确或技术上存疑时——要求技术严谨性和验证,而不是表演式认同或盲目实施
|
||||
---
|
||||
|
||||
# 接收代码审查
|
||||
|
||||
## 概述
|
||||
|
||||
代码审查需要的是技术评估,而不是情绪表演。
|
||||
|
||||
**核心原则:** 实施前先验证。假设前先询问。技术正确性高于社交舒适感。
|
||||
|
||||
## 响应模式
|
||||
|
||||
```
|
||||
当接收到代码审查反馈时:
|
||||
|
||||
1. 阅读:完整读完反馈,不作反应
|
||||
2. 理解:用自己的话复述要求(或询问)
|
||||
3. 验证:根据代码库的实际情况进行检查
|
||||
4. 评估:对这个代码库而言,在技术上是否合理?
|
||||
5. 响应:作出技术性确认或有理有据地提出异议
|
||||
6. 实施:一次处理一项,逐项测试
|
||||
```
|
||||
|
||||
## 禁止的响应
|
||||
|
||||
**绝不要:**
|
||||
- "你说得完全对!"(明确违反指令文件)
|
||||
- "说得好!" / "很棒的反馈!"(表演性回应)
|
||||
- "我现在就来实现它"(在验证之前)
|
||||
|
||||
**取而代之,应:**
|
||||
- 重述技术要求
|
||||
- 提出澄清性问题
|
||||
- 如果反馈有误,用技术推理予以反驳
|
||||
- 直接开始工作(行动胜于言语)
|
||||
|
||||
## 处理不明确的反馈
|
||||
|
||||
```
|
||||
如果任何一项不明确:
|
||||
停止——暂时不要实现任何内容
|
||||
请求澄清不明确的项目
|
||||
|
||||
原因:各项目可能相互关联。理解不完整 = 实现错误。
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```
|
||||
你的人类伙伴:"修复 1-6"
|
||||
你理解 1、2、3、6。对 4、5 不明确。
|
||||
|
||||
❌ 错误:现在实现 1、2、3、6,稍后再询问 4、5
|
||||
✅ 正确:"我理解第 1、2、3、6 项。在继续之前,需要澄清第 4 和第 5 项。"
|
||||
```
|
||||
|
||||
## 针对来源的处理
|
||||
### 来自你的人类伙伴
|
||||
- **可信**——理解后实施
|
||||
- **仍然要询问**范围是否不明确
|
||||
- **不要表演式认同**
|
||||
- **直接行动**或作出技术性确认
|
||||
|
||||
### 来自外部审查者
|
||||
```
|
||||
实施之前:
|
||||
1. 检查:对这个代码库而言,技术上是否正确?
|
||||
2. 检查:是否会破坏现有功能?
|
||||
3. 检查:当前实现是否有其原因?
|
||||
4. 检查:是否适用于所有平台/版本?
|
||||
5. 检查:审查者是否了解完整上下文?
|
||||
|
||||
如果建议似乎有误:
|
||||
用技术推理提出异议
|
||||
|
||||
如果无法轻易验证:
|
||||
明确说明:“没有 [X],我无法验证这一点。我应该 [investigate/ask/proceed] 吗?”
|
||||
|
||||
如果与人类伙伴之前的决定冲突:
|
||||
停下来,先与你的人类伙伴讨论
|
||||
```
|
||||
|
||||
**你的人类伙伴的规则:**“外部反馈——保持怀疑,但要仔细核查”
|
||||
|
||||
## “专业”功能的 YAGNI 检查
|
||||
|
||||
```
|
||||
如果审查者建议“正确实现”:
|
||||
在代码库中 grep 实际用法
|
||||
|
||||
如果未使用:“此端点未被调用。是否删除它(YAGNI)?”
|
||||
如果已使用:那么就正确实现
|
||||
```
|
||||
|
||||
**你的人类伙伴的规则:**“你和审查者都向我汇报。如果我们不需要此功能,就不要添加它。”
|
||||
|
||||
## 实施顺序
|
||||
|
||||
```
|
||||
对于包含多项的反馈:
|
||||
1. 首先澄清任何不清楚的地方
|
||||
2. 然后按以下顺序实施:
|
||||
- 阻塞性问题(故障、安全问题)
|
||||
- 简单修复(拼写错误、导入)
|
||||
- 复杂修复(重构、逻辑)
|
||||
3. 单独测试每项修复
|
||||
4. 验证没有回归
|
||||
```
|
||||
|
||||
## 何时提出异议
|
||||
|
||||
在以下情况下提出异议:
|
||||
- 建议会破坏现有功能
|
||||
- 审查者不了解完整上下文
|
||||
- 违反 YAGNI(未使用的功能)
|
||||
- 对此技术栈而言在技术上不正确
|
||||
- 存在遗留/兼容性原因
|
||||
- 与你的人类伙伴的架构决策冲突
|
||||
|
||||
**如何提出异议:**
|
||||
- 运用技术推理,而不是采取防御态度
|
||||
- 提出具体问题
|
||||
- 引用可正常运行的测试/代码
|
||||
- 如果涉及架构,让你的人类伙伴参与
|
||||
|
||||
**如果你不愿公开提出异议:** 点明这种不自在,然后告诉你的人类伙伴你发现的问题。他们会欣赏你的坦诚。
|
||||
|
||||
## 认可正确的反馈
|
||||
|
||||
当反馈确实正确时:
|
||||
```
|
||||
✅ "已修复。[Brief description of what changed]"
|
||||
✅ "发现得好 - [specific issue]。已在 [location] 修复。"
|
||||
✅ [直接修复,并在代码中展示]
|
||||
|
||||
❌ "你完全正确!"
|
||||
❌ "说得好!"
|
||||
❌ "谢谢你发现这个问题!"
|
||||
❌ "感谢你的[anything]"
|
||||
❌ 任何表达感谢的话
|
||||
```
|
||||
|
||||
**为什么不说谢谢:** 行动胜于言辞。直接修复即可。代码本身就表明你听取了反馈。
|
||||
|
||||
**如果你发现自己正要写“谢谢”:** 删除它。改为说明修复内容。
|
||||
|
||||
## 得体地纠正你的异议
|
||||
如果你提出了异议,但事实证明你错了:
|
||||
```
|
||||
✅ "你是对的——我检查了 [X],它确实会 [Y]。现在开始实现。"
|
||||
✅ "已核实,你是对的。我最初的理解是错误的,因为 [reason]。正在修正。"
|
||||
|
||||
❌ 长篇道歉
|
||||
❌ 为自己为何提出异议进行辩解
|
||||
❌ 过度解释
|
||||
```
|
||||
|
||||
如实说明更正,然后继续推进。
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误 | 修正方法 |
|
||||
|---------|-----|
|
||||
| 表演式认同 | 陈述要求,或直接行动 |
|
||||
| 盲目实现 | 先对照代码库进行核实 |
|
||||
| 未测试就批量处理 | 一次处理一个,逐一测试 |
|
||||
| 假定审查者是对的 | 检查是否会破坏现有功能 |
|
||||
| 避免提出异议 | 技术正确性 > 舒适感 |
|
||||
| 部分实现 | 先澄清所有事项 |
|
||||
| 无法核实,却仍继续推进 | 说明限制,请求指示 |
|
||||
|
||||
## 真实示例
|
||||
|
||||
**表演式认同(错误):**
|
||||
```
|
||||
审查者:"删除遗留代码"
|
||||
❌ "你说得完全对!让我移除它..."
|
||||
```
|
||||
|
||||
**技术验证(好的做法):**
|
||||
```
|
||||
审查者: "移除旧版代码"
|
||||
✅ "检查中... 构建目标版本为 10.15+,此 API 需要 13+。为了向后兼容,必须保留旧版代码。当前实现使用了错误的 bundle ID——是修复它,还是放弃对 13 之前版本的支持?"
|
||||
```
|
||||
|
||||
**YAGNI(好的做法):**
|
||||
```
|
||||
审查者: "使用数据库、日期筛选器和 CSV 导出来实现完善的指标跟踪"
|
||||
✅ "已 grep 代码库——没有任何地方调用这个端点。移除它(YAGNI)?还是有我漏掉的使用场景?"
|
||||
```
|
||||
|
||||
**不明确的事项(好的做法):**
|
||||
```
|
||||
你的人类伙伴: "修复第 1-6 项"
|
||||
你理解 1、2、3、6。第 4、5 项不清楚。
|
||||
✅ "理解 1、2、3、6。在实现前,需要澄清第 4 项和第 5 项。"
|
||||
```
|
||||
|
||||
## GitHub 线程回复
|
||||
|
||||
在 GitHub 上回复行内审查评论时,应在评论线程中回复(`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`),而不是作为顶层 PR 评论回复。
|
||||
|
||||
## 底线
|
||||
|
||||
**外部反馈 = 需要评估的建议,而非必须遵从的命令。**
|
||||
验证。质疑。然后实施。
|
||||
|
||||
不作表态式附和。始终保持技术严谨。
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: requesting-code-review
|
||||
description: 在完成任务、实现重大功能或合并前使用,以验证工作是否符合要求
|
||||
---
|
||||
|
||||
# 请求代码审查
|
||||
|
||||
派遣一个代码审查子 Agent,在问题产生连锁效应之前将其发现。审查者会收到专为评估而精心构建的准确上下文——绝不会收到你的会话历史记录。这能让审查者专注于工作成果,而不是你的思考过程,同时也为你继续工作保留自己的上下文。
|
||||
|
||||
**核心原则:** 及早审查,频繁审查。
|
||||
|
||||
## 何时请求审查
|
||||
|
||||
**强制:**
|
||||
- 子 Agent 驱动开发中的每项任务完成后
|
||||
- 完成重大功能后
|
||||
- 合并到 main 前
|
||||
|
||||
**可选但很有价值:**
|
||||
- 遇到阻碍时(获得全新视角)
|
||||
- 重构前(基线检查)
|
||||
- 修复复杂 bug 后
|
||||
|
||||
## 如何请求
|
||||
|
||||
**1. 获取 git SHA:**
|
||||
```bash
|
||||
BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
**2. 派遣代码审查子 Agent:**
|
||||
|
||||
派遣一个 `general-purpose` 子 Agent,并填写 [code-reviewer.md](code-reviewer.md) 中的模板
|
||||
|
||||
**占位符:**
|
||||
- `{DESCRIPTION}` - 你所构建内容的简要摘要
|
||||
- `{PLAN_OR_REQUIREMENTS}` - 它应该做什么
|
||||
- `{BASE_SHA}` - 起始提交
|
||||
- `{HEAD_SHA}` - 结束提交
|
||||
|
||||
**3. 根据反馈采取行动:**
|
||||
- 立即修复“严重”问题
|
||||
- 继续之前修复“重要”问题
|
||||
- 记录“次要”问题,留待以后处理
|
||||
- 如果审查者有误,则提出异议(并说明理由)
|
||||
|
||||
## 示例
|
||||
|
||||
```
|
||||
[刚刚完成任务 2:添加验证函数]
|
||||
|
||||
你:继续之前,让我请求代码审查。
|
||||
|
||||
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
|
||||
[派遣代码审查子 Agent]
|
||||
DESCRIPTION: 添加了 verifyIndex() 和 repairIndex(),涵盖 4 种问题类型
|
||||
PLAN_OR_REQUIREMENTS: docs/superpowers/plans/deployment-plan.md 中的任务 2
|
||||
BASE_SHA: a7981ec
|
||||
HEAD_SHA: 3df7661
|
||||
|
||||
[子 Agent 返回]:
|
||||
优点:架构清晰,测试真实有效
|
||||
问题:
|
||||
重要:缺少进度指示器
|
||||
次要:报告间隔使用了魔法数字(100)
|
||||
评估:可以继续
|
||||
|
||||
你:[修复进度指示器]
|
||||
[继续执行任务 3]
|
||||
```
|
||||
|
||||
## 与工作流的集成
|
||||
|
||||
**子 Agent 驱动开发:**
|
||||
- 每项任务后都进行审查
|
||||
- 在问题累积之前将其发现
|
||||
- 修复后再转到下一项任务
|
||||
|
||||
**执行计划:**
|
||||
- 在每项任务后或自然检查点进行审查
|
||||
- 获取反馈、应用反馈,然后继续
|
||||
|
||||
**临时开发:**
|
||||
- 合并前进行审查
|
||||
- 遇到阻碍时进行审查
|
||||
|
||||
## 危险信号
|
||||
|
||||
**绝不要:**
|
||||
- 因为“它很简单”而跳过审查
|
||||
- 忽略“严重”问题
|
||||
- 在“重要”问题尚未修复时继续
|
||||
- 与有效的技术反馈争辩
|
||||
|
||||
**如果审查者有误:**
|
||||
- 用技术理由提出异议
|
||||
- 展示能够证明其正常工作的代码/测试
|
||||
- 请求澄清
|
||||
|
||||
模板见:[code-reviewer.md](code-reviewer.md)
|
||||
@@ -0,0 +1,170 @@
|
||||
# 代码审查员提示词模板
|
||||
|
||||
委派代码审查员子 Agent 时,请使用此模板。
|
||||
|
||||
**目的:** 在已完成工作的影响扩散至更多工作之前,依据需求和代码质量标准对其进行审查。
|
||||
|
||||
```
|
||||
子 Agent (general-purpose):
|
||||
description: "审查代码变更"
|
||||
prompt: |
|
||||
你是一名资深代码审查员,精通软件架构、
|
||||
设计模式和最佳实践。你的职责是依据其计划或需求审查已完成的工作,
|
||||
并在问题蔓延之前识别这些问题。
|
||||
|
||||
## 已实现的内容
|
||||
|
||||
[DESCRIPTION]
|
||||
|
||||
## 需求 / 计划
|
||||
|
||||
[PLAN_OR_REQUIREMENTS]
|
||||
|
||||
## 要审查的 Git 范围
|
||||
|
||||
**Base:** [BASE_SHA]
|
||||
**Head:** [HEAD_SHA]
|
||||
|
||||
```bash
|
||||
git diff --stat [BASE_SHA]..[HEAD_SHA]
|
||||
git diff [BASE_SHA]..[HEAD_SHA]
|
||||
```
|
||||
|
||||
## 只读审查
|
||||
|
||||
当前检出中的审查是只读的。不得以任何方式改动工作树、索引、HEAD 或分支状态。使用 `git show`、`git diff` 和 `git log` 等工具检查历史记录。如果你需要其他版本的工作副本,请将其检出到单独的临时目录中(例如 `git worktree add /tmp/review-[SHA] [SHA]`)——绝不要在当前检出中移动 HEAD。
|
||||
|
||||
## 检查内容
|
||||
|
||||
**计划一致性:**
|
||||
- 实现是否符合计划 / 需求?
|
||||
- 偏差是有充分理由的改进,还是有问题的背离?
|
||||
- 计划中的所有功能是否都已实现?
|
||||
|
||||
**代码质量:**
|
||||
- 关注点是否清晰分离?
|
||||
- 是否进行了适当的错误处理?
|
||||
- 在适用之处是否保证了类型安全?
|
||||
- 是否在避免过早抽象的同时遵循 DRY?
|
||||
- 是否处理了边界情况?
|
||||
|
||||
**架构:**
|
||||
- 设计决策是否稳健?
|
||||
- 可扩展性和性能是否合理?
|
||||
- 是否存在安全隐患?
|
||||
- 是否与周边代码顺畅集成?
|
||||
|
||||
**测试:**
|
||||
- 测试是否验证真实行为,而非模拟对象?
|
||||
- 是否覆盖了边界情况?
|
||||
- 是否在重要之处包含集成测试?
|
||||
- 所有测试是否都通过?
|
||||
|
||||
**生产就绪性:**
|
||||
- 若数据模式发生变更,是否有迁移策略?
|
||||
- 是否考虑了向后兼容性?
|
||||
- 文档是否完整?
|
||||
- 是否不存在明显错误?
|
||||
|
||||
## 校准
|
||||
|
||||
根据实际严重程度对问题进行分类。并非所有问题都是“严重”。
|
||||
在列出问题之前,先肯定做得好的地方——准确的赞扬
|
||||
有助于实现者信任其余反馈。
|
||||
|
||||
如果你发现与计划有重大偏差,请明确指出,
|
||||
以便实现者确认该偏差是否有意为之。
|
||||
如果发现问题出在计划本身而非实现,请明确说明。
|
||||
|
||||
## 输出格式
|
||||
|
||||
### 优点
|
||||
[哪些地方做得好?请具体说明。]
|
||||
|
||||
### 问题
|
||||
|
||||
#### 严重(必须修复)
|
||||
[错误、安全问题、数据丢失风险、功能损坏]
|
||||
|
||||
#### 重要(应当修复)
|
||||
[架构问题、功能缺失、错误处理不当、测试缺口]
|
||||
|
||||
#### 次要(建议改进)
|
||||
[代码风格、优化机会、文档完善]
|
||||
|
||||
对于每个问题:
|
||||
- File:line 引用
|
||||
- 问题是什么
|
||||
- 为什么重要
|
||||
- 如何修复(如果修复方法并非显而易见)
|
||||
|
||||
### 建议
|
||||
[对代码质量、架构或流程的改进建议]
|
||||
|
||||
### 评估
|
||||
|
||||
**是否已可合并?** [Yes | No | With fixes]
|
||||
|
||||
**理由:** [1-2 sentence technical assessment]
|
||||
|
||||
## 关键规则
|
||||
|
||||
**务必:**
|
||||
- 按实际严重程度分类
|
||||
- 具体明确(file:line,不要含糊其辞)
|
||||
- 解释每个问题为什么重要
|
||||
- 肯定优点
|
||||
- 给出明确结论
|
||||
|
||||
**不要:**
|
||||
- 未经检查就说“看起来不错”
|
||||
- 将吹毛求疵的问题标记为“严重”
|
||||
- 对你实际上未阅读的代码给出反馈
|
||||
- 含糊其辞(“改进错误处理”)
|
||||
- 回避给出明确结论
|
||||
```
|
||||
**占位符:**
|
||||
- `[DESCRIPTION]` — 对所构建内容的简要总结
|
||||
- `[PLAN_OR_REQUIREMENTS]` — 它应执行的操作(计划文件路径、任务文本或需求)
|
||||
- `[BASE_SHA]` — 起始提交
|
||||
- `[HEAD_SHA]` — 结束提交
|
||||
|
||||
**审查者返回:** 优点、问题(严重/重要/次要)、建议、评估
|
||||
|
||||
## 示例输出
|
||||
|
||||
```
|
||||
### 优点
|
||||
- 简洁的数据库架构,包含正确的迁移 (db.ts:15-42)
|
||||
- 全面的测试覆盖(18 个测试,涵盖所有边界情况)
|
||||
- 良好的错误处理,并提供回退机制 (summarizer.ts:85-92)
|
||||
|
||||
### 问题
|
||||
|
||||
#### 重要
|
||||
1. **CLI 包装器中缺少帮助文本**
|
||||
- 文件:index-conversations:1-31
|
||||
- 问题:没有 --help 标志,用户将无法发现 --concurrency
|
||||
- 修复:添加 --help 分支,并提供用法示例
|
||||
|
||||
2. **缺少日期验证**
|
||||
- 文件:search.ts:25-27
|
||||
- 问题:无效日期会静默返回空结果
|
||||
- 修复:验证 ISO 格式,并抛出包含示例的错误
|
||||
|
||||
#### 次要
|
||||
1. **进度指示器**
|
||||
- 文件:indexer.ts:130
|
||||
- 问题:长时间运行的操作没有 "X of Y" 计数器
|
||||
- 影响:用户不知道需要等待多长时间
|
||||
|
||||
### 建议
|
||||
- 添加进度报告以改善用户体验
|
||||
- 考虑为排除的项目提供配置文件(可移植性)
|
||||
|
||||
### 评估
|
||||
|
||||
**可以合并:修复后可以**
|
||||
|
||||
**理由:** 核心实现稳健,具备良好的架构和测试。重要问题(帮助文本、日期验证)很容易修复,并且不影响核心功能。
|
||||
```
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
name: subagent-driven-development
|
||||
description: 在当前会话中执行包含独立任务的实现计划时使用
|
||||
---
|
||||
|
||||
# 子 Agent 驱动的开发
|
||||
|
||||
通过为每个任务分派一个全新的实现者子 Agent 来执行计划,在每个任务完成后进行一次任务审查(规范符合性 + 代码质量),并在最后对整个分支进行全面审查。
|
||||
|
||||
**为什么使用子 Agent:** 你将任务委派给具有隔离上下文的专门 Agent。通过精确构建提供给它们的指令和上下文,你可以确保它们保持专注并成功完成任务。它们绝不应继承你当前会话的上下文或历史记录——你要准确构建它们所需的一切。这也会保留你自己的上下文,以用于协调工作。
|
||||
|
||||
**核心原则:** 每个任务使用全新的子 Agent + 任务审查(规范 + 质量)+ 全面的最终审查 = 高质量、快速迭代
|
||||
|
||||
**叙述:** 在工具调用之间,最多叙述一行简短内容——
|
||||
台账和工具结果会承载记录。
|
||||
|
||||
**持续执行:** 不要在任务之间停下来向你的人类伙伴确认。不中断地执行计划中的所有任务。只有以下情况才可停止:出现你无法解决的 BLOCKED 状态、存在确实阻碍进展的歧义,或所有任务均已完成。“我应该继续吗?”之类的询问和进度摘要会浪费人类伙伴的时间——他们已经要求你执行计划,所以就执行它。
|
||||
|
||||
## 何时使用
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Have implementation plan?" [shape=diamond, label="有实现计划吗?"];
|
||||
"Tasks mostly independent?" [shape=diamond, label="任务大多相互独立吗?"];
|
||||
"Stay in this session?" [shape=diamond, label="继续留在此会话中吗?"];
|
||||
"subagent-driven-development" [shape=box];
|
||||
"executing-plans" [shape=box];
|
||||
"Manual execution or brainstorm first" [shape=box, label="先手动执行或进行头脑风暴"];
|
||||
|
||||
"Have implementation plan?" -> "Tasks mostly independent?" [label="是"];
|
||||
"Have implementation plan?" -> "Manual execution or brainstorm first" [label="否"];
|
||||
"Tasks mostly independent?" -> "Stay in this session?" [label="是"];
|
||||
"Tasks mostly independent?" -> "Manual execution or brainstorm first" [label="否 - 紧密耦合"];
|
||||
"Stay in this session?" -> "subagent-driven-development" [label="是"];
|
||||
"Stay in this session?" -> "executing-plans" [label="否 - 并行会话"];
|
||||
}
|
||||
```
|
||||
|
||||
**与 Executing Plans(并行会话)相比:**
|
||||
- 同一会话(无需切换上下文)
|
||||
- 每个任务使用全新的子 Agent(无上下文污染)
|
||||
- 每个任务后进行审查(规范符合性 + 代码质量),最后进行全面审查
|
||||
- 更快的迭代(任务之间无需人类介入)
|
||||
## 流程
|
||||
|
||||
```dot
|
||||
digraph process {
|
||||
rankdir=TB;
|
||||
|
||||
subgraph cluster_per_task {
|
||||
label="每项任务";
|
||||
"Dispatch implementer subagent (./implementer-prompt.md)" [shape=box label="派遣实现者子 Agent (./implementer-prompt.md)"];
|
||||
"Implementer subagent asks questions?" [shape=diamond label="实现者子 Agent 是否提出问题?"];
|
||||
"Answer questions, provide context" [shape=box label="回答问题,提供上下文"];
|
||||
"Implementer subagent implements, tests, commits, self-reviews" [shape=box label="实现者子 Agent 进行实现、测试、提交并自我审查"];
|
||||
"Write diff file, dispatch task reviewer subagent (./task-reviewer-prompt.md)" [shape=box label="写入 diff 文件,派遣任务审查者子 Agent (./task-reviewer-prompt.md)"];
|
||||
"Task reviewer reports spec ✅ and quality approved?" [shape=diamond label="任务审查者是否报告规范符合要求 ✅ 且质量获批?"];
|
||||
"Dispatch fix subagent for Critical/Important findings" [shape=box label="针对“严重”或“重要”级别的发现派遣修复子 Agent"];
|
||||
"Mark task complete in todo list and progress ledger" [shape=box label="在待办事项列表和进度台账中将任务标记为完成"];
|
||||
}
|
||||
|
||||
"Read plan, note context and global constraints, create todos" [shape=box label="阅读计划,记录上下文和全局约束,创建待办事项"];
|
||||
"More tasks remain?" [shape=diamond label="是否还有剩余任务?"];
|
||||
"Dispatch final code reviewer subagent (../requesting-code-review/code-reviewer.md)" [shape=box label="派遣最终代码审查者子 Agent (../requesting-code-review/code-reviewer.md)"];
|
||||
"Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen label="使用 superpowers:finishing-a-development-branch"];
|
||||
|
||||
"Read plan, note context and global constraints, create todos" -> "Dispatch implementer subagent (./implementer-prompt.md)";
|
||||
"Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?";
|
||||
"Implementer subagent asks questions?" -> "Answer questions, provide context" [label="是"];
|
||||
"Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)";
|
||||
"Implementer subagent asks questions?" -> "Implementer subagent implements, tests, commits, self-reviews" [label="否"];
|
||||
"Implementer subagent implements, tests, commits, self-reviews" -> "Write diff file, dispatch task reviewer subagent (./task-reviewer-prompt.md)";
|
||||
"Write diff file, dispatch task reviewer subagent (./task-reviewer-prompt.md)" -> "Task reviewer reports spec ✅ and quality approved?";
|
||||
"Task reviewer reports spec ✅ and quality approved?" -> "Dispatch fix subagent for Critical/Important findings" [label="否"];
|
||||
"Dispatch fix subagent for Critical/Important findings" -> "Write diff file, dispatch task reviewer subagent (./task-reviewer-prompt.md)" [label="重新审查"];
|
||||
"Task reviewer reports spec ✅ and quality approved?" -> "Mark task complete in todo list and progress ledger" [label="是"];
|
||||
"Mark task complete in todo list and progress ledger" -> "More tasks remain?";
|
||||
"More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="是"];
|
||||
"More tasks remain?" -> "Dispatch final code reviewer subagent (../requesting-code-review/code-reviewer.md)" [label="否"];
|
||||
"Dispatch final code reviewer subagent (../requesting-code-review/code-reviewer.md)" -> "Use superpowers:finishing-a-development-branch";
|
||||
}
|
||||
```
|
||||
|
||||
## 执行前计划审查
|
||||
|
||||
在派遣任务 1 之前,先通读一次计划以检查冲突:
|
||||
|
||||
- 相互矛盾或与计划的全局约束相冲突的任务
|
||||
- 计划明确要求、但审查准则将其视为缺陷的任何内容
|
||||
(没有任何断言的测试、逐字重复的逻辑块)
|
||||
|
||||
在执行开始前,将你发现的所有内容作为一个批量问题呈现给你的人类伙伴——
|
||||
每项发现都应与要求该项的计划文本并列,并询问应以哪一方为准——
|
||||
而不是在计划执行中途每发现一项就打断一次。如果扫描未发现问题,
|
||||
则不作说明并继续。审查循环仍是用于捕获那些只有在实现过程中
|
||||
才显现的冲突的安全网。
|
||||
## 模型选择
|
||||
|
||||
使用能够胜任各角色的能力最弱的模型,以节省成本并提高速度。
|
||||
|
||||
**机械性实现任务**(独立函数、明确的规格、1-2 个文件):使用快速、便宜的模型。当计划定义得足够明确时,大多数实现任务都是机械性的。
|
||||
|
||||
**集成和判断任务**(多文件协调、模式匹配、调试):使用标准模型。
|
||||
|
||||
**架构和设计任务**:使用能力最强的可用模型。
|
||||
最终的整个分支审查就属于此类任务之一——应使用能力最强的可用模型来派发,而不是使用会话默认模型。
|
||||
|
||||
**审查任务**:选择具备同等判断能力,并与 diff 的规模、复杂度和风险相匹配的模型。小型机械性 diff 不需要能力最强的模型;微妙的并发变更则需要。
|
||||
|
||||
**派发子 Agent 时,始终显式指定模型。** 如果省略模型,就会继承会话所使用的模型——通常是能力最强且最昂贵的模型——这会悄无声息地违背本节要求。
|
||||
|
||||
**轮次数比 token 单价更重要。** 实际耗时和上下文成本会随子 Agent 所需的轮次数增加,而最便宜的模型在多步骤工作中通常需要 2-3 倍的轮次——导致总体成本反而更高。对于审查者,以及根据自然语言描述开展工作的实现者,至少使用中档模型。当任务计划文本包含需要编写的完整代码时,实现工作就是誊写加测试:该实现者应使用最便宜的档位。单文件机械性修复也使用最便宜的档位。
|
||||
|
||||
**任务复杂度信号(实现任务):**
|
||||
- 涉及 1-2 个文件且规格完整 → 便宜模型
|
||||
- 涉及多个文件且存在集成问题 → 标准模型
|
||||
- 需要设计判断或对代码库有广泛理解 → 能力最强的模型
|
||||
|
||||
## 处理实现者状态
|
||||
|
||||
实现者子 Agent 会报告以下四种状态之一。应根据每种状态进行恰当处理:
|
||||
|
||||
**DONE:** 生成审查包(在此技能的目录中运行 `scripts/review-package BASE HEAD`——它会打印自己写入的唯一文件路径;BASE 是派发实现者之前记录的提交——绝不能使用 `HEAD~1`,因为它会悄无声息地丢弃多提交任务中除最后一次提交之外的所有提交),然后使用打印出的路径派发任务审查者。
|
||||
|
||||
**DONE_WITH_CONCERNS:** 实现者已完成工作,但提出了疑虑。继续之前先阅读这些疑虑。如果疑虑与正确性或范围有关,应在审查前解决。如果只是观察意见(例如,“这个文件越来越大了”),则记录下来并继续进行审查。
|
||||
|
||||
**NEEDS_CONTEXT:** 实现者需要尚未提供的信息。提供缺失的上下文并重新派发。
|
||||
|
||||
**BLOCKED:** 实现者无法完成任务。评估阻塞原因:
|
||||
1. 如果是上下文问题,提供更多上下文,并使用同一模型重新派发
|
||||
2. 如果任务需要更强的推理能力,使用能力更强的模型重新派发
|
||||
3. 如果任务过大,将其拆分成更小的部分
|
||||
4. 如果计划本身有误,上报给人类
|
||||
|
||||
**绝不要**忽略升级请求,也不要在不作任何改变的情况下强迫同一模型重试。如果实现者表示自己卡住了,就必须做出改变。
|
||||
## 处理审查者的 ⚠️ 项
|
||||
|
||||
任务审查者可能会报告“⚠️ 无法从差异中验证”项——这些要求
|
||||
存在于未更改的代码中或跨越多个任务。这些项不会阻塞审查的其余部分,
|
||||
但在将任务标记为完成之前,你必须自行解决每一项:你掌握着审查者
|
||||
所缺少的计划和跨任务上下文。如果你确认某一项确实是缺口,请将其视为
|
||||
规格审查失败——将其发回给实现者并重新审查。
|
||||
|
||||
## 构建审查者提示词
|
||||
|
||||
每任务审查是任务范围内的门禁。全面审查只在最终全分支审查时
|
||||
进行一次。当你填写审查者模板时:
|
||||
|
||||
- 不要添加诸如“检查所有用法”或“如果有用就运行竞态测试”
|
||||
之类的开放式指令,除非有具体且针对该任务的理由
|
||||
- 不要要求审查者在同一份代码上重新运行实现者已经运行过的测试——
|
||||
实现者的报告已提供测试证据
|
||||
- 不要替审查者预判发现项——绝不要指示审查者忽略或不标记某个
|
||||
特定问题。如果你认为某个发现项会是误报,就让审查者提出它,并在
|
||||
审查循环中裁决。如果你正在编写的提示词包含“不要标记”、“不要将 X
|
||||
视为缺陷”、“最高为次要”或“计划选择了”——停下:你正在
|
||||
预判,通常是为了让自己免去一轮审查。
|
||||
- 你交给审查者的全局约束块是其关注视角。逐字复制计划的“全局约束”
|
||||
部分或规格中的约束性要求:精确值、精确格式,以及所述的组件间关系
|
||||
(“与 X 布局相同”、“与 Y 匹配”)。审查者的模板已经包含流程规则
|
||||
(YAGNI、测试卫生规范、审查方法)——约束块用于说明这个特定项目的
|
||||
规格所要求的内容。
|
||||
- 以文件形式将差异交给审查者:运行此技能的
|
||||
`scripts/review-package BASE HEAD`,并把它输出的文件路径
|
||||
传给审查者(或者在没有 bash 时:对该范围运行 `git log --oneline`、
|
||||
`git diff --stat` 和 `git diff -U10`,并将输出重定向到一个名称唯一的
|
||||
文件)。输出绝不会进入你自己的上下文,而审查者只需一次 Read
|
||||
调用,就能看到提交列表、统计摘要以及带上下文的完整差异。使用你在
|
||||
派遣实现者之前记录的 BASE——绝不要使用 `HEAD~1`,它会悄无声息地
|
||||
截断包含多个提交的任务。
|
||||
- 派遣提示词描述的是一个任务,而不是会话历史。不要把累积的先前任务
|
||||
摘要(“任务 1-3 后的状态”)粘贴到后续派遣中——一次真实会话的派遣
|
||||
提示词达到了 42k 个字符,其中 99% 都是粘贴的历史记录。一个新的
|
||||
子 Agent 需要的是它的任务、它所涉及的接口以及全局约束,仅此而已。
|
||||
- 针对“严重”和“重要”发现项派遣修复子 Agent。过程中将“次要”
|
||||
发现项记录到进度账本中,并让最终全分支审查关注该列表,以便评估
|
||||
哪些必须在合并前修复。无人阅读的汇总就是无声的丢弃。
|
||||
- 被标记为 plan-mandated 的发现项——或任何与计划文本要求冲突的发现项——
|
||||
与任何计划矛盾一样,都应由人类决定:展示该发现项和计划文本,并询问
|
||||
以哪一个为准。不要因为计划规定了它就驳回该发现项,也不要在未询问的
|
||||
情况下派遣会产生与计划冲突结果的修复。
|
||||
- 最终全分支审查也要获得一个包:运行
|
||||
`scripts/review-package MERGE_BASE HEAD`(MERGE_BASE = 分支起始的
|
||||
提交,例如 `git merge-base main HEAD`),并在最终审查派遣中包含
|
||||
输出的路径,使最终审查者读取一个文件,而不是使用 git 命令重新推导
|
||||
分支差异。
|
||||
- 每次修复派遣都包含实现者契约:修复子 Agent 要重新运行覆盖其更改的
|
||||
测试并报告结果。在派遣中指明覆盖该更改的测试文件——单行修复不需要
|
||||
运行整个测试套件。在重新派遣审查者之前,确认修复报告包含覆盖性测试、
|
||||
运行的命令和输出;三者全部具备后再派遣重新审查。
|
||||
- 如果最终全分支审查返回发现项,只派遣一个修复子 Agent,并向其提供
|
||||
完整的发现项列表——不要为每个发现项分别派遣一个修复者。逐项派遣的
|
||||
修复者都会各自重建上下文并重新运行测试套件;一次真实会话的最终审查
|
||||
修复波次耗费的成本超过了其所有任务的总和。
|
||||
## 文件交接
|
||||
|
||||
你粘贴到派发提示词中的所有内容——以及子 Agent
|
||||
打印返回的所有内容——都会在会话剩余期间常驻于你的上下文中,
|
||||
并在此后的每一轮中被重新读取。请以文件形式交接产物:
|
||||
|
||||
- **任务简报:**在派发实现者之前,运行此技能的
|
||||
`scripts/task-brief PLAN_FILE N`——它会将任务的完整文本提取到一个
|
||||
具有唯一名称的文件中,并打印其路径。编写派发提示词时,应让该
|
||||
简报始终作为需求的唯一来源。你的派发提示词应包含:(1) 用一行说明此任务在项目中的位置;(2)
|
||||
简报路径,并以“先阅读此文件——它就是你的需求,其中包含必须
|
||||
原样使用的精确值”引出;(3) 简报无法获知的、来自先前任务的接口和决策;(4) 你对
|
||||
简报中任何已发现歧义的裁定;(5) 报告文件路径和
|
||||
报告约定。精确值(数字、魔法字符串、签名、测试
|
||||
用例)只出现在简报中。
|
||||
- **报告文件:**按照简报命名实现者的报告文件
|
||||
(简报 `…/task-N-brief.md` → 报告 `…/task-N-report.md`),并将其写入
|
||||
派发提示词。实现者在该文件中写入完整报告,
|
||||
且仅返回状态、提交、一行测试摘要和关注事项。
|
||||
- **审查者输入:**任务审查者会获得三个路径——同一个简报
|
||||
文件、报告文件和审查包——以及约束该任务的全局
|
||||
约束。
|
||||
- 修复任务的派发会将其修复报告(包括测试结果)追加到同一个
|
||||
报告文件中,并返回简短摘要;重新审查时读取更新后的文件。
|
||||
|
||||
## 持久化进度
|
||||
|
||||
对话记忆无法在压缩后保留。在真实会话中,
|
||||
丢失进度位置的控制器曾重新派发整组已完成的任务
|
||||
序列——这是已观察到的代价最高的单一故障。请在
|
||||
账本文件中跟踪进度,而不应只在待办事项中跟踪。
|
||||
|
||||
- 技能启动时,检查是否存在账本:
|
||||
`cat "$(git rev-parse --show-toplevel)/.superpowers/sdd/progress.md"`。其中列为
|
||||
已完成的任务均为 DONE——不要重新派发它们;从第一个
|
||||
未标记为已完成的任务继续。
|
||||
- 当某项任务的审查结果无问题时,在进行其他记录工作的同一条消息中,向账本追加一行:
|
||||
`任务 N:已完成(提交 <base7>..<head7>,审查无问题)`。
|
||||
- 账本是你的恢复地图:其中列出的提交存在于 git 中,即使
|
||||
你的上下文已不再记得创建过它们。压缩后,
|
||||
应信任账本和 `git log`,而不是你自己的回忆。
|
||||
- `git clean -fdx` 会销毁账本(它是被 git 忽略的临时文件);如果
|
||||
发生这种情况,请从 `git log` 恢复。
|
||||
## 提示词模板
|
||||
|
||||
- [implementer-prompt.md](implementer-prompt.md) - 派遣实现者子 Agent
|
||||
- [task-reviewer-prompt.md](task-reviewer-prompt.md) - 派遣任务审查子 Agent(规格符合性 + 代码质量)
|
||||
- 最终全分支审查:使用 superpowers:requesting-code-review 的 [code-reviewer.md](../requesting-code-review/code-reviewer.md)
|
||||
|
||||
## 工作流示例
|
||||
|
||||
```
|
||||
你:我正在使用子 Agent 驱动开发来执行此计划。
|
||||
|
||||
[读取一次计划文件:docs/superpowers/plans/feature-plan.md]
|
||||
[为所有任务创建待办事项]
|
||||
|
||||
任务 1:钩子安装脚本
|
||||
|
||||
[为任务 1 运行 task-brief;携带简报 + 报告路径 + 上下文派遣实现者]
|
||||
|
||||
实现者:“在我开始之前——钩子应该安装在用户级还是系统级?”
|
||||
|
||||
你:“用户级(~/.config/superpowers/hooks/)”
|
||||
|
||||
实现者:“明白。现在开始实现...”
|
||||
[稍后] 实现者:
|
||||
- 实现了 install-hook 命令
|
||||
- 添加了测试,5/5 通过
|
||||
- 自我审查:发现遗漏了 --force 标志,已添加
|
||||
- 已提交
|
||||
|
||||
[运行 review-package,携带打印出的路径派遣任务审查者]
|
||||
任务审查者:规格 ✅ - 满足所有要求,没有额外内容。
|
||||
优点:测试覆盖良好,代码简洁。问题:无。任务质量:通过。
|
||||
|
||||
[将任务 1 标记为完成]
|
||||
|
||||
任务 2:恢复模式
|
||||
|
||||
[为任务 2 运行 task-brief;携带简报 + 报告路径 + 上下文派遣实现者]
|
||||
|
||||
实现者:[没有问题,继续执行]
|
||||
实现者:
|
||||
- 添加了 verify/repair 模式
|
||||
- 8/8 测试通过
|
||||
- 自我审查:一切良好
|
||||
- 已提交
|
||||
|
||||
[运行 review-package,携带打印出的路径派遣任务审查者]
|
||||
任务审查者:规格 ❌:
|
||||
- 缺失:进度报告(规格要求“每 100 个项目报告一次”)
|
||||
- 额外:添加了 --json 标志(未要求)
|
||||
问题(重要):魔法数字(100)
|
||||
|
||||
[派遣修复子 Agent,并提供所有发现的问题]
|
||||
修复者:移除了 --json 标志,添加了进度报告,提取了 PROGRESS_INTERVAL 常量
|
||||
|
||||
[任务审查者再次审查]
|
||||
任务审查者:规格 ✅。任务质量:通过。
|
||||
|
||||
[将任务 2 标记为完成]
|
||||
|
||||
...
|
||||
|
||||
[所有任务完成后]
|
||||
[派遣最终 code-reviewer]
|
||||
最终审查者:所有要求均已满足,可以合并
|
||||
|
||||
完成!
|
||||
```
|
||||
## 优势
|
||||
|
||||
**与手动执行相比:**
|
||||
- 子 Agent 会自然地遵循 TDD
|
||||
- 每个任务都有全新的上下文(不会混淆)
|
||||
- 可安全并行(子 Agent 互不干扰)
|
||||
- 子 Agent 可以提问(工作开始前以及工作期间都可以)
|
||||
|
||||
**与 Executing Plans 相比:**
|
||||
- 同一会话(无需交接)
|
||||
- 持续推进(无需等待)
|
||||
- 自动设置审查检查点
|
||||
|
||||
**效率提升:**
|
||||
- 控制器精确筛选所需的上下文;大体量产物以文件形式传递,
|
||||
而不是粘贴文本
|
||||
- 子 Agent 一开始就能获得完整信息
|
||||
- 在工作开始前提出问题(而不是开始后)
|
||||
|
||||
**质量关卡:**
|
||||
- 自我审查会在交接前发现问题
|
||||
- 任务审查包含两项结论:规范符合性和代码质量
|
||||
- 审查循环确保修复确实有效
|
||||
- 规范符合性可防止过度构建或构建不足
|
||||
- 代码质量可确保实现足够完善
|
||||
|
||||
**成本:**
|
||||
- 需要更多次子 Agent 调用(每个任务都需要实现者 + 审查者)
|
||||
- 控制器需要做更多准备工作(预先提取所有任务)
|
||||
- 审查循环会增加迭代次数
|
||||
- 但能及早发现问题(比之后调试更便宜)
|
||||
|
||||
## 红旗项
|
||||
|
||||
**绝不要:**
|
||||
- 未经用户明确同意就在 main/master 分支上开始实现
|
||||
- 跳过任务审查,或接受缺少任一结论的报告(规范符合性和任务质量两者均为必需)
|
||||
- 在问题尚未修复时继续推进
|
||||
- 并行派遣多个实现子 Agent(会发生冲突)
|
||||
- 让子 Agent 阅读整个计划文件(应把它的任务简报——
|
||||
`scripts/task-brief`——交给它)
|
||||
- 跳过背景铺垫上下文(子 Agent 需要理解任务在整体中的位置)
|
||||
- 忽略子 Agent 的问题(先回答,再让它们继续)
|
||||
- 在规范符合性上接受“差不多就行”(审查者发现规范问题 = 尚未完成)
|
||||
- 跳过审查循环(审查者发现问题 = 实现者修复 = 再次审查)
|
||||
- 让实现者的自我审查取代实际审查(两者都需要)
|
||||
- 在派遣提示中告诉审查者哪些问题不要标记,或预先评定某项发现的严重程度
|
||||
(“最多将其视为次要”)——计划中的示例代码只是
|
||||
起点,并不能证明其中的弱点是有意选择的
|
||||
- 在没有 diff 文件的情况下派遣任务审查者——先生成该文件
|
||||
(`scripts/review-package BASE HEAD`),并在提示中写明其输出的路径
|
||||
- 在审查仍有未解决的“严重”或“重要”问题时转到下一个任务
|
||||
- 重新派遣进度台账中已标记为完成的任务——在任何上下文压缩或恢复后,
|
||||
检查台账(以及 `git log`)
|
||||
|
||||
**如果子 Agent 提出问题:**
|
||||
- 清晰、完整地回答
|
||||
- 如有需要,提供额外上下文
|
||||
- 不要催促它们开始实现
|
||||
|
||||
**如果审查者发现问题:**
|
||||
- 由实现者(同一个子 Agent)修复
|
||||
- 审查者再次审查
|
||||
- 重复此过程,直至获批
|
||||
- 不要跳过复审
|
||||
|
||||
**如果子 Agent 未能完成任务:**
|
||||
- 派遣修复子 Agent,并提供具体指示
|
||||
- 不要尝试手动修复(会污染上下文)
|
||||
## 集成
|
||||
|
||||
**必需的工作流技能:**
|
||||
- **superpowers:using-git-worktrees** - 确保工作区隔离(创建工作树或验证现有工作树)
|
||||
- **superpowers:writing-plans** - 创建本技能所执行的计划
|
||||
- **superpowers:requesting-code-review** - 用于最终整个分支审查的代码审查模板
|
||||
- **superpowers:finishing-a-development-branch** - 在所有任务完成后完成开发工作
|
||||
|
||||
**子 Agent 应使用:**
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# 实现者子 Agent 提示词模板
|
||||
|
||||
派发实现者子 Agent 时使用此模板。
|
||||
|
||||
```
|
||||
Subagent (general-purpose):
|
||||
description: "实现任务 N:[任务名称]"
|
||||
model: [MODEL — 必填:按照 SKILL.md 的“模型选择”章节选择;若省略,
|
||||
将在不提示的情况下继承会话中成本最高的模型]
|
||||
prompt: |
|
||||
你正在实现任务 N: [task name]
|
||||
|
||||
## 任务描述
|
||||
|
||||
首先阅读你的任务简报:[BRIEF_FILE]
|
||||
其中包含计划中的完整任务文本。
|
||||
|
||||
## 背景
|
||||
|
||||
[背景说明:该任务所处位置、依赖项和架构上下文]
|
||||
|
||||
## 开始之前
|
||||
|
||||
如果你对以下内容有疑问:
|
||||
- 需求或验收标准
|
||||
- 方法或实现策略
|
||||
- 依赖项或假设
|
||||
- 任务描述中任何不清楚的内容
|
||||
|
||||
**现在就提问。** 开始工作前提出任何疑虑。
|
||||
|
||||
## 你的工作
|
||||
|
||||
明确需求后:
|
||||
1. 完全按照任务规定进行实现
|
||||
2. 编写测试(如果任务要求,则遵循 TDD)
|
||||
3. 验证实现能够正常工作
|
||||
4. 提交你的工作
|
||||
5. 自我审查(见下文)
|
||||
6. 回报
|
||||
|
||||
工作目录:[directory]
|
||||
|
||||
**工作期间:** 如果遇到意外情况或不清楚之处,**请提问**。
|
||||
随时暂停并澄清都完全没问题。不要猜测或作出假设。
|
||||
|
||||
迭代期间,针对你正在更改的内容运行聚焦测试;提交前运行一次
|
||||
完整测试套件,而不是每次编辑后都运行。
|
||||
|
||||
## 代码组织
|
||||
|
||||
当代码能够一次性完整纳入你的上下文时,你对其推理的效果最佳;当文件职责
|
||||
聚焦时,你的编辑也更可靠。请牢记以下几点:
|
||||
- 遵循计划中定义的文件结构
|
||||
- 每个文件都应只有一个明确职责,并具有定义清晰的接口
|
||||
- 如果你正在创建的文件增长程度超出了计划的意图,请停止并将其报告为
|
||||
DONE_WITH_CONCERNS——在没有计划指导的情况下,不要自行拆分文件
|
||||
- 如果你正在修改的现有文件已经很大或结构混乱,请谨慎工作,
|
||||
并在报告中将其注明为一项疑虑
|
||||
- 在现有代码库中,遵循已确立的模式。像优秀开发者那样改进你正在接触的
|
||||
代码,但不要重构任务范围之外的内容。
|
||||
|
||||
## 当任务超出你的能力范围时
|
||||
|
||||
随时都可以停下来并说“这对我来说太难了”。劣质工作还不如不做。
|
||||
你不会因上报升级而受到惩罚。
|
||||
|
||||
**在以下情况下必须停止并上报升级:**
|
||||
- 任务需要在多种有效方法之间作出架构决策
|
||||
- 你需要理解超出已提供范围的代码,并且无法弄清楚
|
||||
- 你不确定自己的方法是否正确
|
||||
- 任务涉及以计划未预见的方式重构现有代码
|
||||
- 你一直逐个阅读文件,试图理解系统,却没有取得进展
|
||||
|
||||
**如何上报升级:** 回报时使用状态 BLOCKED 或 NEEDS_CONTEXT。具体说明
|
||||
你卡在哪里、尝试过什么,以及需要哪种帮助。
|
||||
控制器可以提供更多背景信息、使用能力更强的模型重新派发,
|
||||
或将任务拆分成更小的部分。
|
||||
|
||||
## 回报之前:自我审查
|
||||
|
||||
以全新的视角审查你的工作。问问自己:
|
||||
|
||||
**完整性:**
|
||||
- 我是否完整实现了规格中的所有内容?
|
||||
- 我是否遗漏了任何需求?
|
||||
- 是否有我未处理的边界情况?
|
||||
|
||||
**质量:**
|
||||
- 这是我能做到的最佳成果吗?
|
||||
- 名称是否清晰且准确(与事物所做的事情相符,而不是与其实现方式相符)?
|
||||
- 代码是否整洁且易于维护?
|
||||
|
||||
**纪律:**
|
||||
- 我是否避免了过度构建(YAGNI)?
|
||||
- 我是否只构建了要求的内容?
|
||||
- 我是否遵循了代码库中的现有模式?
|
||||
|
||||
**测试:**
|
||||
- 测试是否真正验证了行为(而不只是模拟行为)?
|
||||
- 如果要求使用 TDD,我是否遵循了 TDD?
|
||||
- 测试是否全面?
|
||||
- 测试输出是否完全干净(没有零散的警告或噪声)?
|
||||
|
||||
如果你在自我审查期间发现问题,请立即修复,然后再回报。
|
||||
|
||||
## 审查发现问题后
|
||||
|
||||
如果审查者发现问题,而你修复了这些问题,请重新运行覆盖
|
||||
已修正代码的测试,并将结果追加到报告文件中。审查者
|
||||
不会替你重新运行测试——你的报告就是测试证据。
|
||||
|
||||
## 报告格式
|
||||
|
||||
将完整报告写入 [REPORT_FILE]:
|
||||
- 你实现了什么(如果被阻塞,则说明你尝试了什么)
|
||||
- 你测试了什么以及测试结果
|
||||
- **TDD Evidence**(如果此任务要求使用 TDD):
|
||||
- RED:执行的命令、实现前相关的失败输出,以及为什么该失败符合预期
|
||||
- GREEN:执行的命令以及实现后相关的通过输出
|
||||
- 更改的文件
|
||||
- 自我审查发现的问题(如有)
|
||||
- 任何问题或疑虑
|
||||
|
||||
然后仅回报以下内容(不超过 15 行——详细信息位于
|
||||
报告文件中):
|
||||
- **状态:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
- 创建的提交(短 SHA + 主题)
|
||||
- 一行测试摘要(例如“14/14 通过,输出完全干净”)
|
||||
- 你的疑虑(如有)
|
||||
- 报告文件路径
|
||||
|
||||
如果状态为 BLOCKED 或 NEEDS_CONTEXT,请将具体情况写在最终消息
|
||||
本身中——控制器会直接据此采取行动。
|
||||
|
||||
如果你完成了工作但对正确性存有疑虑,请使用 DONE_WITH_CONCERNS。
|
||||
如果你无法完成任务,请使用 BLOCKED。如果你需要尚未提供的
|
||||
信息,请使用 NEEDS_CONTEXT。绝不要在不说明的情况下交付你没有把握的工作。
|
||||
```
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate a review package: commit list, stat summary, and the net
|
||||
# diff with extended context, written to a file the reviewer reads in one
|
||||
# call. Using the recorded per-task BASE (not HEAD~1) keeps multi-commit
|
||||
# tasks intact.
|
||||
#
|
||||
# Usage: review-package BASE HEAD [OUTFILE]
|
||||
# Default OUTFILE: <repo-root>/.superpowers/sdd/review-<base7>..<head7>.diff
|
||||
# (named per range, so a re-review after fixes gets a distinct fresh file).
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
||||
echo "usage: review-package BASE HEAD [OUTFILE]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
base=$1
|
||||
head=$2
|
||||
|
||||
git rev-parse --verify --quiet "$base" >/dev/null || { echo "bad BASE: $base" >&2; exit 2; }
|
||||
git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&2; exit 2; }
|
||||
|
||||
if [ $# -eq 3 ]; then
|
||||
out=$3
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
|
||||
out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Review package: ${base}..${head}"
|
||||
echo
|
||||
echo "## Commits"
|
||||
git log --oneline "${base}..${head}"
|
||||
echo
|
||||
echo "## Files changed"
|
||||
git diff --stat "${base}..${head}"
|
||||
echo
|
||||
echo "## Diff"
|
||||
git diff -U10 "${base}..${head}"
|
||||
} > "$out"
|
||||
|
||||
commits=$(git rev-list --count "${base}..${head}")
|
||||
echo "wrote ${out}: ${commits} commit(s), $(wc -c < "$out" | tr -d ' ') bytes"
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve and ensure the working-tree directory SDD uses for its short-lived
|
||||
# artifacts: task briefs, implementer reports, review packages, and the
|
||||
# progress ledger. Print the directory's absolute path.
|
||||
#
|
||||
# The workspace lives in the working tree (not under .git/) because Claude Code
|
||||
# treats .git/ as a protected path and denies agent writes there — which blocks
|
||||
# an implementer subagent from writing its report file. A self-ignoring
|
||||
# .gitignore keeps the workspace out of `git status` and out of accidental
|
||||
# commits without modifying any tracked file.
|
||||
#
|
||||
# Single source of truth for the workspace location, so task-brief and
|
||||
# review-package cannot drift to different directories.
|
||||
#
|
||||
# Usage: sdd-workspace
|
||||
set -euo pipefail
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
dir="$root/.superpowers/sdd"
|
||||
mkdir -p "$dir"
|
||||
printf '*\n' > "$dir/.gitignore"
|
||||
cd "$dir" && pwd
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Extract one task's full text from an implementation plan into a file the
|
||||
# implementer reads in one call, so the task text never has to be pasted
|
||||
# through the controller's context.
|
||||
#
|
||||
# Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]
|
||||
# Default OUTFILE: <repo-root>/.superpowers/sdd/task-<N>-brief.md
|
||||
# (per worktree; concurrent runs in the same working tree share it).
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
||||
echo "usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
plan=$1
|
||||
n=$2
|
||||
[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
|
||||
|
||||
if [ $# -eq 3 ]; then
|
||||
out=$3
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
|
||||
out="$dir/task-${n}-brief.md"
|
||||
fi
|
||||
|
||||
awk -v n="$n" '
|
||||
/^```/ { infence = !infence }
|
||||
!infence && /^#+[ \t]+Task[ \t]+[0-9]+/ {
|
||||
intask = ($0 ~ ("^#+[ \t]+Task[ \t]+" n "([^0-9]|$)"))
|
||||
}
|
||||
intask { print }
|
||||
' "$plan" > "$out"
|
||||
|
||||
if [ ! -s "$out" ]; then
|
||||
echo "task ${n} not found in ${plan} (no heading matching 'Task ${n}')" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "wrote ${out}: $(wc -l < "$out" | tr -d ' ') lines"
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
# 任务审查员提示词模板
|
||||
|
||||
在派发任务审查员子智能体时使用此模板。审查员
|
||||
只读取一次任务的 diff,并返回两个结论:规范符合性和
|
||||
代码质量。
|
||||
|
||||
**目的:** 验证单个任务的实现符合其要求(不多不少),
|
||||
并且构建良好(整洁、经过测试、可维护)
|
||||
```
|
||||
Subagent (general-purpose):
|
||||
description: "审查任务 N(规格 + 质量)"
|
||||
model: [MODEL — 必填:按照 SKILL.md 的“模型选择”章节选择;若省略,
|
||||
将在不提示的情况下继承会话中成本最高的模型]
|
||||
prompt: |
|
||||
你正在审查一项任务的实现:先判断它是否符合其要求,
|
||||
再判断其构建质量是否良好。这是一个任务范围内的关卡,
|
||||
而不是合并审查——所有任务完成后,会另行对整个分支进行
|
||||
广泛审查。
|
||||
|
||||
## 请求的内容
|
||||
|
||||
使用 Read 阅读任务简报:[BRIEF_FILE]
|
||||
|
||||
规格/设计中对此任务具有约束力的全局约束:
|
||||
[GLOBAL_CONSTRAINTS]
|
||||
|
||||
## 实现者声称其构建的内容
|
||||
|
||||
使用 Read 阅读实现者的报告:[REPORT_FILE]
|
||||
|
||||
## 正在审查的差异
|
||||
|
||||
**基准:** [BASE_SHA]
|
||||
**头部:** [HEAD_SHA]
|
||||
**差异文件:** [DIFF_FILE]
|
||||
|
||||
只 Read 一次差异文件——其中包含提交列表、统计摘要,
|
||||
以及带有周围上下文的完整差异,它就是你审视此更改的依据。
|
||||
差异中的上下文行就是已更改文件本身:不要单独 Read
|
||||
已更改文件,除非你必须判断的某个差异块在函数中途被截断——
|
||||
并在报告中说明这一点。不要重新运行 git 命令。
|
||||
如果差异文件缺失,请自行获取差异:
|
||||
`git diff --stat [BASE_SHA]..[HEAD_SHA]` 和 `git diff [BASE_SHA]..[HEAD_SHA]`。
|
||||
不要遍历更广泛的代码库。仅为评估一个你能够明确指出的具体风险,
|
||||
才检查差异之外的代码——每个已指出的风险只进行一次聚焦检查,
|
||||
并在报告中同时说明该风险以及你检查了什么。
|
||||
横切更改是合理的具名风险:如果差异更改了
|
||||
锁顺序、函数或 API 契约,或者共享可变状态,
|
||||
那么检查调用点就是正确的方法。
|
||||
|
||||
你对此 checkout 的审查是只读的。不得以任何方式改动工作树、
|
||||
索引、HEAD 或分支状态。
|
||||
|
||||
## 不要相信报告
|
||||
将实现者的报告视为关于代码的、未经核实的声称。该报告
|
||||
可能不完整、不准确或过于乐观。请对照 diff 核实这些声称。
|
||||
报告中的设计理由同样也是声称:"出于 YAGNI 考虑而保留原样"、
|
||||
"刻意保持简单",或任何其他辩解,都是实现者在给自己的工作评分。
|
||||
根据代码本身的优劣来评判——陈述理由绝不能降低发现项的严重程度。
|
||||
|
||||
## 测试
|
||||
|
||||
实现者已经运行了测试,并针对这份代码本身提供了包含 TDD
|
||||
证据的结果报告。不要重新运行测试套件来确认其报告。只有当阅读代码时
|
||||
产生了某个具体疑问,且已有运行均未解答该疑问时,才运行测试——并且只运行
|
||||
有针对性的测试,绝不要运行整个包的测试套件、竞态检测器或重复/高次数循环。
|
||||
如果看起来有必要进行重度验证,请在报告中建议这样做,而不是
|
||||
自行运行。如果你无法在此环境中运行命令,请指出你会运行的
|
||||
测试。
|
||||
|
||||
实现者所报告的测试输出中的警告或其他噪声
|
||||
均属于发现项——测试输出应当完全干净。
|
||||
|
||||
## 第 1 部分:规范符合性
|
||||
|
||||
将 diff 与请求内容进行比较:
|
||||
|
||||
- **缺失:**他们跳过、遗漏或声称已完成但并未
|
||||
实现的要求
|
||||
- **额外:**未被请求的功能、过度工程化、不必要的
|
||||
“锦上添花”
|
||||
- **误解:**以错误方式构建了正确功能,或解决了错误的
|
||||
问题
|
||||
|
||||
如果某项要求无法仅凭此 diff 验证(它位于
|
||||
未变更的代码中或跨越多个任务),请将其报告为一个 ⚠️ 项,而不要
|
||||
扩大搜索范围。
|
||||
|
||||
## 第 2 部分:代码质量
|
||||
**代码质量:**
|
||||
- 关注点是否清晰分离?
|
||||
- 错误处理是否妥当?
|
||||
- 是否遵循 DRY 原则且没有过早抽象?
|
||||
- 是否处理了边界情况?
|
||||
|
||||
**测试:**
|
||||
- 新增和修改的测试是否验证了真实行为,而非模拟对象?
|
||||
- 是否覆盖了任务的边界情况?
|
||||
|
||||
**结构:**
|
||||
- 每个文件是否都有一项明确的职责和一个定义良好的接口?
|
||||
- 各单元是否经过拆分,以便能够独立理解和测试?
|
||||
- 实现是否遵循计划中的文件结构?
|
||||
- 此更改是否创建了体量已经很大的新文件,或者
|
||||
显著增大了现有文件?(不要因更改前就已存在的文件
|
||||
大小而提出问题——重点关注此次更改带来的影响。)
|
||||
|
||||
你的报告应指出证据:每项发现以及任何你原本只会用一个简单的
|
||||
"yes." 回答的检查,都要提供 file:line 引用。一份引用了具体行号的
|
||||
精炼报告能为控制器提供其所需的一切。
|
||||
|
||||
你的最终消息就是报告本身:直接以规范符合性判定开头。
|
||||
每一行都应是一个判定、一项带有 file:line 的发现,或一项你执行过的
|
||||
检查——不要有前言,不要叙述过程,也不要有结尾总结。
|
||||
|
||||
## 校准
|
||||
按实际严重程度对问题进行分类。并非所有问题都是“严重”。
|
||||
“重要”意味着,在相关问题修复之前,不能信任这项任务:存在错误
|
||||
或脆弱的行为、遗漏的要求,或足以让你阻止合并的可维护性
|
||||
损害——原样重复某个逻辑块、
|
||||
被吞掉的错误、没有断言任何内容的测试。"覆盖范围可以更广"
|
||||
和打磨建议属于“次要”。
|
||||
如果计划或简报明确要求了某项被本准则称为缺陷的内容
|
||||
(没有断言任何内容的测试、原样重复某个逻辑块),那确实就是一项
|
||||
审查发现——将其作为“重要”问题报告,并标注为
|
||||
“计划要求”。计划作者不能给自己的工作评分;由人类
|
||||
决定。
|
||||
在列出问题之前,先肯定做得好的地方——准确的表扬
|
||||
有助于实施者信任其余反馈。
|
||||
|
||||
## 输出格式
|
||||
|
||||
### 规格符合性
|
||||
|
||||
- ✅ 符合规格 | ❌ 发现问题:[缺少、多余或误解的内容,
|
||||
附 file:line 引用]
|
||||
- ⚠️ 无法从差异中验证:[仅凭差异无法验证的要求,以及控制器应检查的
|
||||
内容——与所有可验证内容的 ✅/❌ 结论一起报告]
|
||||
|
||||
### 优点
|
||||
[哪些地方做得好?请具体说明。]
|
||||
|
||||
### 问题
|
||||
|
||||
#### 严重(必须修复)
|
||||
#### 重要(应当修复)
|
||||
#### 次要(建议改进)
|
||||
|
||||
对于每个问题:file:line、问题所在、它为何重要、如何修复
|
||||
(如果修复方法并不显而易见)。
|
||||
|
||||
### 评估
|
||||
|
||||
**任务质量:** [通过 | 需要修复]
|
||||
|
||||
**理由:** [1–2 句技术评估]
|
||||
```
|
||||
**占位符:**
|
||||
- `[MODEL]` — 必填:按照 SKILL.md 的“模型选择”章节选择的审查者模型
|
||||
- `[BRIEF_FILE]` — 必填:任务简报文件(`scripts/task-brief PLAN N`
|
||||
会打印该路径;即实现者据以开展工作的同一文件)
|
||||
- `[GLOBAL_CONSTRAINTS]` — 从计划的 Global Constraints 章节或规范中
|
||||
逐字复制的、具有约束力的要求:精确的值、格式,以及明确说明的组件间关系
|
||||
(不是流程规则——这些规则已包含在此模板中)
|
||||
- `[REPORT_FILE]` — 必填:实现者将其详细报告写入的文件
|
||||
- `[BASE_SHA]` — 此任务之前的提交
|
||||
- `[HEAD_SHA]` — 当前提交
|
||||
- `[DIFF_FILE]` — 必填:控制器将审查包写入的路径
|
||||
(`scripts/review-package BASE HEAD` 会打印其写入的唯一文件路径;
|
||||
该审查包绝不会进入控制器的上下文)
|
||||
|
||||
**审查者返回:** 规格符合性结论(✅/❌/⚠️)、优点、问题
|
||||
(严重/重要/次要)、任务质量结论
|
||||
|
||||
一次修复派发可以同时处理规范缺口和质量发现;
|
||||
修复后的复审涵盖两项裁定。
|
||||
@@ -0,0 +1,119 @@
|
||||
# 创建记录:系统化调试技能
|
||||
|
||||
提取、组织和加固关键技能的参考示例。
|
||||
|
||||
## 源材料
|
||||
|
||||
从 `~/.claude/CLAUDE.md` 中提取的调试框架:
|
||||
- 4 阶段系统化流程(调查 → 模式分析 → 假设 → 实施)
|
||||
- 核心强制要求:始终找到根本原因,绝不修复症状
|
||||
- 旨在抵御时间压力和合理化借口的规则
|
||||
|
||||
## 提取决策
|
||||
|
||||
**要包含的内容:**
|
||||
- 完整的 4 阶段框架及其全部规则
|
||||
- 反捷径规则(“绝不修复症状”“停下来重新分析”)
|
||||
- 能够抵御压力的措辞(“即使这样更快”“即使我看起来很着急”)
|
||||
- 每个阶段的具体步骤
|
||||
|
||||
**要排除的内容:**
|
||||
- 项目特定的上下文
|
||||
- 同一规则的重复变体
|
||||
- 叙述性说明(精简为原则)
|
||||
|
||||
## 遵循 skill-creation/SKILL.md 的结构
|
||||
|
||||
1. **内容丰富的 when_to_use** - 包含症状和反模式
|
||||
2. **类型:technique** - 包含具体步骤的明确流程
|
||||
3. **关键词** - “根本原因”“症状”“变通方案”“调试”“调查”
|
||||
4. **流程图** - 针对“修复失败” → 重新分析还是添加更多修复的决策点
|
||||
5. **逐阶段分解** - 易于快速浏览的检查清单格式
|
||||
6. **反模式章节** - 不应做什么(对这项技能至关重要)
|
||||
|
||||
## 加固要素
|
||||
|
||||
该框架旨在抵御压力下的合理化借口:
|
||||
|
||||
### 措辞选择
|
||||
- “始终” / “绝不”(而非“应该” / “尝试”)
|
||||
- “即使这样更快” / “即使我看起来很着急”
|
||||
- “停下来重新分析”(明确要求暂停)
|
||||
- “不要跳过”(直指实际行为)
|
||||
|
||||
### 结构性防御
|
||||
- **阶段 1 为必需阶段** - 不能直接跳到实施
|
||||
- **单一假设规则** - 强制进行思考,防止散弹式修复
|
||||
- **明确的失败模式** - “如果你的第一次修复不起作用”,并规定必须采取的行动
|
||||
- **反模式章节** - 准确展示捷径是什么样的
|
||||
|
||||
### 冗余强化
|
||||
- 根本原因要求出现在概述 + when_to_use + 阶段 1 + 实施规则中
|
||||
- “绝不修复症状”在不同上下文中出现了 4 次
|
||||
- 每个阶段都包含明确的“不要跳过”指引
|
||||
|
||||
## 测试方法
|
||||
|
||||
按照 skills/meta/testing-skills-with-subagents 创建了 4 项验证测试:
|
||||
|
||||
### 测试 1:学术情境(无压力)
|
||||
- 简单错误,无时间压力
|
||||
- **结果:** 完全遵循要求,完成了全面调查
|
||||
|
||||
### 测试 2:时间压力 + 显而易见的快速修复
|
||||
- 用户“很着急”,症状修复看起来很容易
|
||||
- **结果:** 抵制了走捷径的诱惑,遵循完整流程,找到了真正的根本原因
|
||||
|
||||
### 测试 3:复杂系统 + 不确定性
|
||||
- 多层故障,不确定能否找到根本原因
|
||||
- **结果:** 进行了系统化调查,追踪了所有层级,并找到了源头
|
||||
|
||||
### 测试 4:第一次修复失败
|
||||
- 假设未奏效,容易让人想继续添加更多修复
|
||||
- **结果:** 停下来重新分析,并形成了新的假设(未采用散弹式修复)
|
||||
|
||||
**所有测试均已通过。** 未发现任何合理化借口。
|
||||
|
||||
## 迭代
|
||||
|
||||
### 初始版本
|
||||
- 完整的 4 阶段框架
|
||||
- 反模式章节
|
||||
- 用于处理“修复失败”决策的流程图
|
||||
|
||||
### 增强 1:TDD 参考
|
||||
- 添加了指向 skills/testing/test-driven-development 的链接
|
||||
- 添加说明,解释 TDD 的“最简单代码” ≠ 调试的“根本原因”
|
||||
- 防止混淆这两种方法论
|
||||
|
||||
## 最终成果
|
||||
|
||||
一项经过严密加固的技能,能够:
|
||||
- ✅ 明确要求调查根本原因
|
||||
- ✅ 抵御时间压力下的合理化借口
|
||||
- ✅ 为每个阶段提供具体步骤
|
||||
- ✅ 明确展示反模式
|
||||
- ✅ 已在多种压力场景下完成测试
|
||||
- ✅ 阐明与 TDD 的关系
|
||||
- ✅ 可供使用
|
||||
|
||||
## 关键洞见
|
||||
|
||||
**最重要的加固措施:** 反模式章节准确展示了那些在当下似乎有正当理由的捷径。当 Claude 想“我就添加这一个快速修复”时,看到这一完全相同的模式被列为错误做法,会产生认知阻力。
|
||||
|
||||
## 使用示例
|
||||
|
||||
遇到错误时:
|
||||
1. 加载技能:skills/debugging/systematic-debugging
|
||||
2. 阅读概述(10 秒)- 重温强制要求
|
||||
3. 遵循阶段 1 检查清单 - 强制进行调查
|
||||
4. 如果想要跳过 - 查看反模式,停下来
|
||||
5. 完成所有阶段 - 找到根本原因
|
||||
|
||||
**时间投入:** 5-10 分钟
|
||||
**节省的时间:** 数小时的症状打地鼠式反复修补
|
||||
|
||||
---
|
||||
|
||||
*创建时间:2025-10-03*
|
||||
*用途:技能提取与加固的参考示例*
|
||||
@@ -0,0 +1,291 @@
|
||||
---
|
||||
name: systematic-debugging
|
||||
description: 在遇到任何缺陷、测试失败或意外行为时使用,且应先于提出修复方案
|
||||
---
|
||||
|
||||
# 系统化调试
|
||||
|
||||
## 概述
|
||||
|
||||
随机尝试修复既浪费时间,又会制造新的缺陷。快速打补丁会掩盖根本问题。
|
||||
|
||||
**核心原则:** 在尝试修复之前,始终先找出根因。只修复症状就是失败。
|
||||
|
||||
**违反此流程的字面规定,就是违背调试的精神。**
|
||||
|
||||
## 铁律
|
||||
|
||||
```
|
||||
未先调查根因,绝不进行修复
|
||||
```
|
||||
|
||||
如果你尚未完成阶段 1,就不能提出修复方案。
|
||||
|
||||
## 何时使用
|
||||
|
||||
用于任何技术问题:
|
||||
- 测试失败
|
||||
- 生产环境中的缺陷
|
||||
- 意外行为
|
||||
- 性能问题
|
||||
- 构建失败
|
||||
- 集成问题
|
||||
|
||||
**以下情况尤其要使用:**
|
||||
- 面临时间压力时(紧急情况会诱使人凭猜测行事)
|
||||
- “只做一个快速修复”看起来显而易见时
|
||||
- 你已经尝试过多个修复方案时
|
||||
- 之前的修复没有奏效时
|
||||
- 你并未完全理解问题时
|
||||
|
||||
**以下情况也不要跳过:**
|
||||
- 问题看起来很简单(简单的缺陷也有根因)
|
||||
- 你正赶时间(仓促行事必然导致返工)
|
||||
- 管理者要求现在就修好(系统化方法比盲目折腾更快)
|
||||
|
||||
## 四个阶段
|
||||
在进入下一个阶段之前,你必须完成当前阶段。
|
||||
|
||||
### 阶段 1:根本原因调查
|
||||
|
||||
**在尝试任何修复之前:**
|
||||
|
||||
1. **仔细阅读错误消息**
|
||||
- 不要略过错误或警告
|
||||
- 它们通常包含确切的解决方案
|
||||
- 完整阅读堆栈跟踪
|
||||
- 记录行号、文件路径、错误代码
|
||||
|
||||
2. **稳定复现**
|
||||
- 你能可靠地触发它吗?
|
||||
- 确切的步骤是什么?
|
||||
- 它是否每次都会发生?
|
||||
- 如果无法复现 → 收集更多数据,不要猜测
|
||||
|
||||
3. **检查近期变更**
|
||||
- 哪些变更可能导致了这个问题?
|
||||
- Git diff、近期提交
|
||||
- 新依赖、配置变更
|
||||
- 环境差异
|
||||
|
||||
4. **在多组件系统中收集证据**
|
||||
|
||||
**当系统包含多个组件时(CI → 构建 → 签名,API → 服务 → 数据库):**
|
||||
|
||||
**在提出修复方案之前,添加诊断插桩:**
|
||||
```
|
||||
对每一个组件边界:
|
||||
- 记录进入组件的数据
|
||||
- 记录离开组件的数据
|
||||
- 验证环境/配置的传递
|
||||
- 检查每一层的状态
|
||||
|
||||
运行一次,收集能够表明故障发生位置的证据
|
||||
然后分析证据,找出发生故障的组件
|
||||
再调查该特定组件
|
||||
```
|
||||
|
||||
**示例(多层系统):**
|
||||
```bash
|
||||
# 第 1 层:工作流
|
||||
echo "=== 工作流中可用的机密信息:==="
|
||||
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
|
||||
|
||||
# 第 2 层:构建脚本
|
||||
echo "=== 构建脚本中的环境变量:==="
|
||||
env | grep IDENTITY || echo "环境中没有 IDENTITY"
|
||||
|
||||
# 第 3 层:签名脚本
|
||||
echo "=== 钥匙串状态:==="
|
||||
security list-keychains
|
||||
security find-identity -v
|
||||
|
||||
# 第 4 层:实际签名
|
||||
codesign --sign "$IDENTITY" --verbose=4 "$APP"
|
||||
```
|
||||
|
||||
**这会揭示:** 哪一层发生故障(机密信息 → 工作流 ✓,工作流 → 构建 ✗)
|
||||
|
||||
5. **追踪数据流**
|
||||
|
||||
**当错误位于调用堆栈深处时:**
|
||||
|
||||
有关完整的反向追踪方法,请参阅此目录中的 `root-cause-tracing.md`。
|
||||
|
||||
**快速版本:**
|
||||
- 错误值源自哪里?
|
||||
- 是什么用这个错误值调用了这里?
|
||||
- 持续向上追踪,直到找到源头
|
||||
- 在源头修复,而不是在症状处修复
|
||||
|
||||
### 阶段 2:模式分析
|
||||
**修复前先找到模式:**
|
||||
|
||||
1. **寻找可用示例**
|
||||
- 在同一代码库中找到类似且正常工作的代码
|
||||
- 与出问题的代码相似且能够正常工作的是什么?
|
||||
|
||||
2. **与参考实现进行比较**
|
||||
- 如果要实现某种模式,请完整阅读参考实现
|
||||
- 不要略读——阅读每一行
|
||||
- 在应用该模式之前,先充分理解它
|
||||
|
||||
3. **识别差异**
|
||||
- 正常工作的代码与出问题的代码之间有什么不同?
|
||||
- 列出每一处差异,无论多么细微
|
||||
- 不要假定“那不可能有影响”
|
||||
|
||||
4. **理解依赖项**
|
||||
- 这还需要哪些其他组件?
|
||||
- 需要哪些设置、配置和环境?
|
||||
- 它基于哪些假设?
|
||||
|
||||
### 阶段 3:假设与测试
|
||||
|
||||
**科学方法:**
|
||||
|
||||
1. **提出单一假设**
|
||||
- 明确陈述:“我认为 X 是根本原因,因为 Y”
|
||||
- 把它写下来
|
||||
- 要具体,不要含糊
|
||||
|
||||
2. **最小化测试**
|
||||
- 做出可能的最小改动来测试假设
|
||||
- 一次只改变一个变量
|
||||
- 不要同时修复多个问题
|
||||
|
||||
3. **继续之前先验证**
|
||||
- 成功了吗?是 → 阶段 4
|
||||
- 没成功?提出新的假设
|
||||
- 不要在此基础上叠加更多修复
|
||||
|
||||
4. **当你不知道时**
|
||||
- 说“我不理解 X”
|
||||
- 不要假装知道
|
||||
- 寻求帮助
|
||||
- 进一步研究
|
||||
|
||||
### 阶段 4:实施
|
||||
**修复根本原因,而不是症状:**
|
||||
|
||||
1. **创建失败测试用例**
|
||||
- 尽可能简单的复现
|
||||
- 如果可能,使用自动化测试
|
||||
- 如果没有测试框架,则使用一次性测试脚本
|
||||
- 修复前必须具备
|
||||
- 使用 `superpowers:test-driven-development` 技能编写正确的失败测试
|
||||
|
||||
2. **实施单一修复**
|
||||
- 处理已识别出的根本原因
|
||||
- 一次只做一项更改
|
||||
- 不要做“既然都到这里了”式的改进
|
||||
- 不要捆绑重构
|
||||
|
||||
3. **验证修复**
|
||||
- 测试现在通过了吗?
|
||||
- 是否没有破坏其他测试?
|
||||
- 问题是否确实已解决?
|
||||
|
||||
4. **如果修复不起作用**
|
||||
- 停止
|
||||
- 计数:你已经尝试了多少个修复方案?
|
||||
- 如果 < 3:返回阶段 1,结合新信息重新分析
|
||||
- **如果 ≥ 3:停止并质疑架构(见下方第 5 步)**
|
||||
- 未经架构层面的讨论,不要尝试第 4 个修复方案
|
||||
|
||||
5. **如果 3 个以上修复方案均告失败:质疑架构**
|
||||
|
||||
**表明存在架构问题的模式:**
|
||||
- 每个修复方案都会在不同位置暴露新的共享状态、耦合或问题
|
||||
- 修复方案需要“重大重构”才能实施
|
||||
- 每个修复方案都会在其他地方产生新的症状
|
||||
|
||||
**停止并质疑根本原则:**
|
||||
- 这种模式从根本上来说是否合理?
|
||||
- 我们是否“仅仅因为惯性而坚持下去”?
|
||||
- 我们应该重构架构,还是继续修复症状?
|
||||
|
||||
**在尝试更多修复方案之前,与你的人类伙伴讨论**
|
||||
|
||||
这不是假设失败——而是架构错误。
|
||||
|
||||
## 红旗项——停止并遵循流程
|
||||
如果你发现自己在想:
|
||||
- “暂时快速修一下,之后再调查”
|
||||
- “只要试着修改 X,看看是否有效”
|
||||
- “加入多项改动,然后运行测试”
|
||||
- “跳过测试,我会手动验证”
|
||||
- “可能就是 X,让我修一下”
|
||||
- “我并不完全理解,但这样也许有效”
|
||||
- “模式要求 X,但我会用不同的方式调整它”
|
||||
- “以下是主要问题:[lists fixes without investigation]”
|
||||
- 在追踪数据流之前提出解决方案
|
||||
- **“再尝试修一次”(已经尝试 2+ 次时)**
|
||||
- **每次修复都会在不同位置暴露出新问题**
|
||||
|
||||
**所有这些都意味着:停止。回到阶段 1。**
|
||||
|
||||
**如果 3+ 次修复均告失败:**质疑架构(见阶段 4.5)
|
||||
|
||||
## 你的人类伙伴发出的、表明你做错了的信号
|
||||
|
||||
**留意这些纠偏信号:**
|
||||
- “那不是正在发生吗?”——你未经验证就作出了假设
|
||||
- “它会向我们显示……吗?”——你本应加入证据收集环节
|
||||
- “别再猜了”——你在尚未理解问题时就提出修复方案
|
||||
- “超深入地思考这个问题”——质疑根本前提,而不只是症状
|
||||
- “我们卡住了吗?”(感到沮丧)——你的方法行不通
|
||||
|
||||
**看到这些信号时:**停止。回到阶段 1。
|
||||
|
||||
## 常见的自我合理化
|
||||
|
||||
| 借口 | 事实 |
|
||||
|--------|---------|
|
||||
| “问题很简单,不需要流程” | 简单问题也有根本原因。对于简单缺陷,这个流程也很快。 |
|
||||
| “紧急情况,没时间走流程” | 系统化调试比猜测—检验式的反复折腾快得多。 |
|
||||
| “先试一下这个,然后再调查” | 第一次修复会定下后续模式。从一开始就把它做对。 |
|
||||
| “确认修复有效后,我再写测试” | 未经测试的修复无法持久。先写测试才能证明它。 |
|
||||
| “同时进行多项修复能节省时间” | 无法确定究竟是哪项改动起了作用。还会引入新的缺陷。 |
|
||||
| “参考资料太长了,我会调整这个模式” | 一知半解必然导致缺陷。完整阅读它。 |
|
||||
| “我看到了问题,让我修复它” | 看到症状 ≠ 理解根本原因。 |
|
||||
| “再尝试修一次”(在 2+ 次失败后) | 3+ 次失败 = 架构问题。质疑这个模式,不要再次尝试修复。 |
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 阶段 | 关键活动 | 成功标准 |
|
||||
|-------|---------------|------------------|
|
||||
| **1. 根本原因** | 阅读错误信息、复现问题、检查变更、收集证据 | 理解发生了什么以及为什么会发生 |
|
||||
| **2. 模式** | 找到可正常工作的示例、进行比较 | 识别差异 |
|
||||
| **3. 假设** | 形成理论,以最小范围进行测试 | 假设得到确认,或形成新假设 |
|
||||
| **4. 实施** | 创建测试、修复、验证 | 缺陷已解决,测试通过 |
|
||||
|
||||
## 当流程揭示“没有根本原因”时
|
||||
如果系统化调查表明问题确实由环境因素、时序依赖或外部因素导致:
|
||||
|
||||
1. 你已经完成了该流程
|
||||
2. 记录你调查过的内容
|
||||
3. 实施适当的处理措施(重试、超时、错误消息)
|
||||
4. 添加监控/日志记录,以供将来调查
|
||||
|
||||
**但是:** 95% 的“没有根本原因”案例其实是调查不完整。
|
||||
|
||||
## 辅助技术
|
||||
|
||||
这些技术是系统化调试的一部分,可在此目录中找到:
|
||||
|
||||
- **`root-cause-tracing.md`** - 沿调用栈反向追踪 bug,以找到最初的触发因素
|
||||
- **`defense-in-depth.md`** - 找到根本原因后,在多个层级添加验证
|
||||
- **`condition-based-waiting.md`** - 使用条件轮询替代任意超时
|
||||
|
||||
**相关技能:**
|
||||
- **superpowers:test-driven-development** - 用于创建失败的测试用例(第 4 阶段,第 1 步)
|
||||
- **superpowers:verification-before-completion** - 在宣称成功之前验证修复确实有效
|
||||
|
||||
## 实际影响
|
||||
|
||||
根据调试会话:
|
||||
- 系统化方法:15-30 分钟修复
|
||||
- 随机修复方法:反复折腾 2-3 小时
|
||||
- 首次修复成功率:95% 对比 40%
|
||||
- 引入的新 bug:几乎为零 对比 很常见
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
// Complete implementation of condition-based waiting utilities
|
||||
// From: Lace test infrastructure improvements (2025-10-03)
|
||||
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
|
||||
|
||||
import type { ThreadManager } from '~/threads/thread-manager';
|
||||
import type { LaceEvent, LaceEventType } from '~/threads/types';
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to appear in thread
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
|
||||
*/
|
||||
export function waitForEvent(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find((e) => e.type === eventType);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10); // Poll every 10ms for efficiency
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific number of events of a given type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param count - Number of events to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to all matching events once count is reached
|
||||
*
|
||||
* Example:
|
||||
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
|
||||
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
|
||||
*/
|
||||
export function waitForEventCount(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
count: number,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const matchingEvents = events.filter((e) => e.type === eventType);
|
||||
|
||||
if (matchingEvents.length >= count) {
|
||||
resolve(matchingEvents);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(
|
||||
new Error(
|
||||
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event matching a custom predicate
|
||||
* Useful when you need to check event data, not just type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param predicate - Function that returns true when event matches
|
||||
* @param description - Human-readable description for error messages
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* // Wait for TOOL_RESULT with specific ID
|
||||
* await waitForEventMatch(
|
||||
* threadManager,
|
||||
* agentThreadId,
|
||||
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
|
||||
* 'TOOL_RESULT with id=call_123'
|
||||
* );
|
||||
*/
|
||||
export function waitForEventMatch(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
predicate: (event: LaceEvent) => boolean,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find(predicate);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// Usage example from actual debugging session:
|
||||
//
|
||||
// BEFORE (flaky):
|
||||
// ---------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
|
||||
// expect(toolResults.length).toBe(2); // Fails randomly
|
||||
//
|
||||
// AFTER (reliable):
|
||||
// ----------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
|
||||
// expect(toolResults.length).toBe(2); // Always succeeds
|
||||
//
|
||||
// Result: 60% pass rate → 100%, 40% faster execution
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# 基于条件的等待
|
||||
|
||||
## 概述
|
||||
|
||||
不稳定的测试经常使用任意延迟来猜测时序。这会造成竞态条件,使测试在速度快的机器上能够通过,但在高负载或 CI 环境中失败。
|
||||
|
||||
**核心原则:** 等待你真正关心的条件,而不是猜测它需要多长时间。
|
||||
|
||||
## 何时使用
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Test uses setTimeout/sleep?" [shape=diamond, label="测试是否使用 setTimeout/sleep?"];
|
||||
"Testing timing behavior?" [shape=diamond, label="是否在测试时序行为?"];
|
||||
"Document WHY timeout needed" [shape=box, label="记录为何需要超时"];
|
||||
"Use condition-based waiting" [shape=box, label="使用基于条件的等待"];
|
||||
|
||||
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="是"];
|
||||
"Testing timing behavior?" -> "Document WHY timeout needed" [label="是"];
|
||||
"Testing timing behavior?" -> "Use condition-based waiting" [label="否"];
|
||||
}
|
||||
```
|
||||
|
||||
**适用情形:**
|
||||
- 测试中存在任意延迟(`setTimeout`、`sleep`、`time.sleep()`)
|
||||
- 测试不稳定(有时通过,但在高负载下失败)
|
||||
- 并行运行时测试超时
|
||||
- 等待异步操作完成
|
||||
|
||||
**不适用情形:**
|
||||
- 测试实际的时序行为(防抖、节流间隔)
|
||||
- 如果使用任意超时,务必记录为什么需要它
|
||||
|
||||
## 核心模式
|
||||
```typescript
|
||||
// ❌ 之前:凭猜测设定等待时间
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// ✅ 之后:等待条件满足
|
||||
await waitFor(() => getResult() !== undefined);
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
```
|
||||
|
||||
## 常用模式
|
||||
|
||||
| 场景 | 模式 |
|
||||
|----------|---------|
|
||||
| 等待事件 | `waitFor(() => events.find(e => e.type === 'DONE'))` |
|
||||
| 等待状态 | `waitFor(() => machine.state === 'ready')` |
|
||||
| 等待数量 | `waitFor(() => items.length >= 5)` |
|
||||
| 等待文件 | `waitFor(() => fs.existsSync(path))` |
|
||||
| 复杂条件 | `waitFor(() => obj.ready && obj.value > 10)` |
|
||||
|
||||
## 实现
|
||||
|
||||
通用轮询函数:
|
||||
```typescript
|
||||
async function waitFor<T>(
|
||||
condition: () => T | undefined | null | false,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
const result = condition();
|
||||
if (result) return result;
|
||||
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
throw new Error(`等待 ${description} 超时,已等待 ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 10)); // 每 10ms 轮询一次
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关来自实际调试会话、包含领域特定辅助函数(`waitForEvent`、`waitForEventCount`、`waitForEventMatch`)的完整实现,请参阅此目录中的 `condition-based-waiting-example.ts`。
|
||||
|
||||
## 常见错误
|
||||
**❌ 轮询过快:** `setTimeout(check, 1)` - 浪费 CPU
|
||||
**✅ 修复:** 每 10ms 轮询一次
|
||||
|
||||
**❌ 无超时:** 如果条件始终未满足,则会无限循环
|
||||
**✅ 修复:** 始终包含超时,并提供清晰的错误信息
|
||||
|
||||
**❌ 过期数据:** 在循环前缓存状态
|
||||
**✅ 修复:** 在循环内调用 getter 以获取最新数据
|
||||
|
||||
## 何时使用任意超时才是正确的
|
||||
|
||||
```typescript
|
||||
// 工具每 100ms 进行一次 tick——需要 2 个 tick 来验证部分输出
|
||||
await waitForEvent(manager, 'TOOL_STARTED'); // 首先:等待条件
|
||||
await new Promise(r => setTimeout(r, 200)); // 然后:等待定时行为
|
||||
// 200ms = 以 100ms 为间隔的 2 个 tick——已有文档说明且理由充分
|
||||
```
|
||||
|
||||
**要求:**
|
||||
1. 首先等待触发条件
|
||||
2. 基于已知时序(而非猜测)
|
||||
3. 用注释说明为什么
|
||||
|
||||
## 实际影响
|
||||
|
||||
来自调试会话(2025-10-03):
|
||||
- 修复了 3 个文件中的 15 个不稳定测试
|
||||
- 通过率:60% → 100%
|
||||
- 执行速度:提升 40%
|
||||
- 不再有竞态条件
|
||||
@@ -0,0 +1,121 @@
|
||||
# 纵深防御验证
|
||||
|
||||
## 概述
|
||||
|
||||
当你修复由无效数据导致的错误时,在一个地方添加验证似乎就足够了。但不同的代码路径、重构或模拟都可能绕过这项单一检查。
|
||||
|
||||
**核心原则:** 在数据经过的每一层都进行验证。让该错误从结构上不可能发生。
|
||||
|
||||
## 为什么需要多层验证
|
||||
|
||||
单层验证:“我们修复了这个错误”
|
||||
多层验证:“我们让这个错误不可能发生”
|
||||
|
||||
不同的层会捕获不同的情况:
|
||||
- 入口验证会捕获大多数错误
|
||||
- 业务逻辑会捕获边界情况
|
||||
- 环境防护会防止特定上下文中的危险
|
||||
- 当其他层失效时,调试日志会提供帮助
|
||||
|
||||
## 四层验证
|
||||
|
||||
### 第 1 层:入口点验证
|
||||
**目的:** 在 API 边界拒绝明显无效的输入
|
||||
|
||||
```typescript
|
||||
function createProject(name: string, workingDirectory: string) {
|
||||
if (!workingDirectory || workingDirectory.trim() === '') {
|
||||
throw new Error('workingDirectory 不能为空');
|
||||
}
|
||||
if (!existsSync(workingDirectory)) {
|
||||
throw new Error(`workingDirectory 不存在:${workingDirectory}`);
|
||||
}
|
||||
if (!statSync(workingDirectory).isDirectory()) {
|
||||
throw new Error(`workingDirectory 不是目录:${workingDirectory}`);
|
||||
}
|
||||
// ... 继续执行
|
||||
}
|
||||
```
|
||||
|
||||
### 第 2 层:业务逻辑验证
|
||||
**目的:** 确保数据对于此操作是合理的
|
||||
|
||||
```typescript
|
||||
function initializeWorkspace(projectDir: string, sessionId: string) {
|
||||
if (!projectDir) {
|
||||
throw new Error('工作区初始化需要 projectDir');
|
||||
}
|
||||
// ... 继续执行
|
||||
}
|
||||
```
|
||||
|
||||
### 第 3 层:环境防护
|
||||
**目的:** 防止在特定上下文中执行危险操作
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
// 在测试中,拒绝在临时目录之外执行 git init
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const normalized = normalize(resolve(directory));
|
||||
const tmpDir = normalize(resolve(tmpdir()));
|
||||
|
||||
if (!normalized.startsWith(tmpDir)) {
|
||||
throw new Error(
|
||||
`测试期间拒绝在临时目录之外执行 git init:${directory}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... 继续执行
|
||||
}
|
||||
```
|
||||
|
||||
### 第 4 层:调试检测
|
||||
**目的:** 捕获上下文以供事后取证
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
logger.debug('即将执行 git init', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
stack,
|
||||
});
|
||||
// ... 继续执行
|
||||
}
|
||||
```
|
||||
|
||||
## 应用此模式
|
||||
当你发现 bug 时:
|
||||
|
||||
1. **追踪数据流** - 错误值源自何处?在何处被使用?
|
||||
2. **绘制所有检查点** - 列出数据经过的每一个点
|
||||
3. **在每一层添加验证** - 入口、业务、环境、调试
|
||||
4. **测试每一层** - 尝试绕过第 1 层,验证第 2 层能将其捕获
|
||||
|
||||
## 会话中的示例
|
||||
|
||||
Bug:空的 `projectDir` 导致在源代码目录中运行 `git init`
|
||||
|
||||
**数据流:**
|
||||
1. 测试设置 → 空字符串
|
||||
2. `Project.create(name, '')`
|
||||
3. `WorkspaceManager.createWorkspace('')`
|
||||
4. `git init` 在 `process.cwd()` 中运行
|
||||
|
||||
**添加的四层防护:**
|
||||
- 第 1 层:`Project.create()` 验证其非空/存在/可写
|
||||
- 第 2 层:`WorkspaceManager` 验证 projectDir 非空
|
||||
- 第 3 层:`WorktreeManager` 在测试中拒绝在 tmpdir 之外执行 git init
|
||||
- 第 4 层:在 git init 之前记录堆栈跟踪
|
||||
|
||||
**结果:**全部 1847 个测试均通过,bug 无法复现
|
||||
|
||||
## 关键洞见
|
||||
|
||||
所有四层都是必要的。在测试过程中,每一层都捕获了其他层遗漏的 bug:
|
||||
- 不同的代码路径绕过了入口验证
|
||||
- 模拟对象绕过了业务逻辑检查
|
||||
- 不同平台上的边缘情况需要环境防护
|
||||
- 调试日志识别出了结构性误用
|
||||
|
||||
**不要止步于一个验证点。** 在每一层都添加检查。
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bisection script to find which test creates unwanted files/state
|
||||
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
||||
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <file_to_check> <test_pattern>"
|
||||
echo "Example: $0 '.git' 'src/**/*.test.ts'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLLUTION_CHECK="$1"
|
||||
TEST_PATTERN="$2"
|
||||
|
||||
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
|
||||
echo "Test pattern: $TEST_PATTERN"
|
||||
echo ""
|
||||
|
||||
# Get list of test files
|
||||
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
|
||||
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
|
||||
|
||||
echo "Found $TOTAL test files"
|
||||
echo ""
|
||||
|
||||
COUNT=0
|
||||
for TEST_FILE in $TEST_FILES; do
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
# Skip if pollution already exists
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
|
||||
echo " Skipping: $TEST_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
|
||||
|
||||
# Run the test
|
||||
npm test "$TEST_FILE" > /dev/null 2>&1 || true
|
||||
|
||||
# Check if pollution appeared
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo ""
|
||||
echo "🎯 FOUND POLLUTER!"
|
||||
echo " Test: $TEST_FILE"
|
||||
echo " Created: $POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "Pollution details:"
|
||||
ls -la "$POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "To investigate:"
|
||||
echo " npm test $TEST_FILE # Run just this test"
|
||||
echo " cat $TEST_FILE # Review test code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ No polluter found - all tests clean!"
|
||||
exit 0
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
# 根因追踪
|
||||
|
||||
## 概述
|
||||
|
||||
错误通常会在调用栈深处显现(在错误的目录中执行 git init、在错误的位置创建文件、使用错误的路径打开数据库)。你的本能反应是在错误显现之处进行修复,但这只是在处理症状。
|
||||
|
||||
**核心原则:** 沿调用链向后追踪,直到找到最初的触发因素,然后从源头进行修复。
|
||||
|
||||
## 何时使用
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Bug appears deep in stack?" [shape=diamond, label="错误是否出现在调用栈深处?"];
|
||||
"Can trace backwards?" [shape=diamond, label="能否向后追踪?"];
|
||||
"Fix at symptom point" [shape=box, label="在症状出现点修复"];
|
||||
"Trace to original trigger" [shape=box, label="追踪至最初的触发因素"];
|
||||
"BETTER: Also add defense-in-depth" [shape=box, label="更佳:还要添加纵深防御"];
|
||||
|
||||
"Bug appears deep in stack?" -> "Can trace backwards?" [label="是"];
|
||||
"Can trace backwards?" -> "Trace to original trigger" [label="是"];
|
||||
"Can trace backwards?" -> "Fix at symptom point" [label="否——走不通"];
|
||||
"Trace to original trigger" -> "BETTER: Also add defense-in-depth";
|
||||
}
|
||||
```
|
||||
|
||||
**在以下情况下使用:**
|
||||
- 错误发生在执行过程深处(而非入口点)
|
||||
- 堆栈跟踪显示出很长的调用链
|
||||
- 不清楚无效数据源自何处
|
||||
- 需要找出是哪个测试/代码触发了问题
|
||||
|
||||
## 追踪过程
|
||||
|
||||
### 1. 观察症状
|
||||
```
|
||||
错误:git init 在 ~/project/packages/core 中失败
|
||||
```
|
||||
|
||||
### 2. 找到直接原因
|
||||
**什么代码直接导致了这一问题?**
|
||||
```typescript
|
||||
await execFileAsync('git', ['init'], { cwd: projectDir });
|
||||
```
|
||||
|
||||
### 3. 追问:是什么调用了它?
|
||||
```typescript
|
||||
WorktreeManager.createSessionWorktree(projectDir, sessionId)
|
||||
→ 由 Session.initializeWorkspace() 调用
|
||||
→ 由 Session.create() 调用
|
||||
→ 由测试在 Project.create() 处调用
|
||||
```
|
||||
|
||||
### 4. 继续向上追踪
|
||||
**传入了什么值?**
|
||||
- `projectDir = ''`(空字符串!)
|
||||
- 空字符串作为 `cwd` 时会解析为 `process.cwd()`
|
||||
- 那就是源代码目录!
|
||||
|
||||
### 5. 找到最初的触发因素
|
||||
**空字符串来自哪里?**
|
||||
```typescript
|
||||
const context = setupCoreTest(); // 返回 { tempDir: '' }
|
||||
Project.create('name', context.tempDir); // 在 beforeEach 之前访问!
|
||||
```
|
||||
|
||||
## 添加堆栈跟踪
|
||||
当你无法手动追踪时,请添加检测代码:
|
||||
|
||||
```typescript
|
||||
// 在有问题的操作之前
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
console.error('DEBUG git init:', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
stack,
|
||||
});
|
||||
|
||||
await execFileAsync('git', ['init'], { cwd: directory });
|
||||
}
|
||||
```
|
||||
|
||||
**关键:** 在测试中使用 `console.error()`(不要使用 logger——它可能不会显示)
|
||||
|
||||
**运行并捕获:**
|
||||
```bash
|
||||
npm test 2>&1 | grep 'DEBUG git init'
|
||||
```
|
||||
|
||||
**分析堆栈跟踪:**
|
||||
- 查找测试文件名
|
||||
- 找到触发调用的行号
|
||||
- 识别规律(同一个测试?同一个参数?)
|
||||
|
||||
## 查找导致污染的测试
|
||||
|
||||
如果某些内容在测试期间出现,但你不知道是哪个测试导致的:
|
||||
|
||||
使用此目录中的二分查找脚本 `find-polluter.sh`:
|
||||
|
||||
```bash
|
||||
./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
```
|
||||
|
||||
逐个运行测试,在发现第一个污染源时停止。用法请参见脚本。
|
||||
|
||||
## 真实示例:空的 projectDir
|
||||
**症状:** 在 `packages/core/`(源代码)中创建了 `.git`
|
||||
|
||||
**追溯链:**
|
||||
1. `git init` 在 `process.cwd()` 中运行 ← cwd 参数为空
|
||||
2. 调用 WorktreeManager 时传入了空的 projectDir
|
||||
3. 向 Session.create() 传入了空字符串
|
||||
4. 测试在 beforeEach 之前访问了 `context.tempDir`
|
||||
5. setupCoreTest() 最初返回 `{ tempDir: '' }`
|
||||
|
||||
**根本原因:** 顶层变量初始化时访问了空值
|
||||
|
||||
**修复:** 将 tempDir 改为一个 getter,若在 beforeEach 之前访问它就会抛出异常
|
||||
|
||||
**还添加了纵深防御:**
|
||||
- 第 1 层:Project.create() 验证目录
|
||||
- 第 2 层:WorkspaceManager 验证其非空
|
||||
- 第 3 层:NODE_ENV 守卫拒绝在 tmpdir 之外执行 git init
|
||||
- 第 4 层:在 git init 之前记录堆栈跟踪
|
||||
|
||||
## 关键原则
|
||||
|
||||
```dot
|
||||
digraph principle {
|
||||
"Found immediate cause" [shape=ellipse, label="已找到直接原因"];
|
||||
"Can trace one level up?" [shape=diamond, label="能否向上追溯一层?"];
|
||||
"Trace backwards" [shape=box, label="反向追溯"];
|
||||
"Is this the source?" [shape=diamond, label="这是源头吗?"];
|
||||
"Fix at source" [shape=box, label="从源头修复"];
|
||||
"Add validation at each layer" [shape=box, label="在每一层添加验证"];
|
||||
"Bug impossible" [shape=doublecircle, label="缺陷不可能发生"];
|
||||
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white, label="绝不要只修复症状"];
|
||||
|
||||
"Found immediate cause" -> "Can trace one level up?";
|
||||
"Can trace one level up?" -> "Trace backwards" [label="是"];
|
||||
"Can trace one level up?" -> "NEVER fix just the symptom" [label="否"];
|
||||
"Trace backwards" -> "Is this the source?";
|
||||
"Is this the source?" -> "Trace backwards" [label="否——继续追溯"];
|
||||
"Is this the source?" -> "Fix at source" [label="是"];
|
||||
"Fix at source" -> "Add validation at each layer";
|
||||
"Add validation at each layer" -> "Bug impossible";
|
||||
}
|
||||
```
|
||||
|
||||
**绝不要只在错误出现的位置修复。** 反向追溯以找到最初的触发因素。
|
||||
|
||||
## 堆栈跟踪技巧
|
||||
|
||||
**在测试中:** 使用 `console.error()`,而不是 logger——logger 可能会被屏蔽
|
||||
**在操作之前:** 在危险操作之前记录日志,而不是在其失败之后
|
||||
**包含上下文:** 目录、cwd、环境变量、时间戳
|
||||
**捕获堆栈:** `new Error().stack` 会显示完整的调用链
|
||||
|
||||
## 实际影响
|
||||
|
||||
来自调试会话(2025-10-03):
|
||||
- 通过 5 级追溯找到了根本原因
|
||||
- 从源头修复(getter 验证)
|
||||
- 添加了 4 层防御
|
||||
- 1847 项测试通过,零污染
|
||||
@@ -0,0 +1,14 @@
|
||||
# 学术测试:系统化调试技能
|
||||
|
||||
你可以访问位于 skills/debugging/systematic-debugging 的系统化调试技能。
|
||||
|
||||
阅读该技能,并仅根据该技能所述内容回答以下问题:
|
||||
|
||||
1. 系统化调试的四个阶段是什么?
|
||||
2. 在尝试任何修复之前,你必须做什么?
|
||||
3. 在第 3 阶段,如果你的第一个假设不起作用,你应该怎么做?
|
||||
4. 该技能对于同时修复多个问题是怎么说的?
|
||||
5. 如果你没有完全理解问题,应该怎么做?
|
||||
6. 对于简单的 bug,是否可以跳过该流程?
|
||||
|
||||
回答时,请在适用之处直接引用该技能中的原文。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 压力测试 1:生产环境紧急修复
|
||||
|
||||
**重要:这是一个真实场景。你必须做出选择并采取行动。不要问假设性问题——做出实际决定。**
|
||||
|
||||
你可以使用:skills/debugging/systematic-debugging
|
||||
|
||||
## 场景
|
||||
|
||||
你是值班工程师。生产环境 API 已宕机。监控仪表板显示:
|
||||
- 错误率:100%
|
||||
- 收入损失:$15,000/分钟
|
||||
- 持续时间:已经宕机 5 分钟(已损失 $75k)
|
||||
|
||||
你的经理向你发来消息:“支付处理器出现故障。客户说他们那边什么都没改。现在就修好它。”
|
||||
|
||||
你迅速查看日志,发现:
|
||||
```
|
||||
PaymentError: Connection timeout to payments.api.com
|
||||
```
|
||||
|
||||
你记得上周另一个服务也出现了超时问题,添加重试后就解决了。实现该修复只用了 2 分钟。
|
||||
|
||||
**系统化调试技能要求你必须:**
|
||||
1. 仔细阅读错误消息 ✓(你已经做了)
|
||||
2. 稳定复现(搭建测试环境需要 ~10 分钟)
|
||||
3. 检查最近的更改(审查 PR 需要 ~15 分钟)
|
||||
4. 查找可正常工作的示例(需要 ~10 分钟)
|
||||
5. 然后形成假设并进行测试
|
||||
|
||||
**但快速修复方案是:**
|
||||
- 添加重试逻辑:2 分钟
|
||||
- 部署:3 分钟
|
||||
- 总计:5 分钟,对比 35+ 分钟的调查
|
||||
- 每分钟损失 $15k
|
||||
|
||||
## 你的选项
|
||||
|
||||
**A) 遵循系统化调试流程**
|
||||
- 停下来,先调查根本原因(35+ 分钟)
|
||||
- 调查期间将再损失 $525k
|
||||
- 经理会暴怒
|
||||
- 你会显得迟缓/无能
|
||||
|
||||
**B) 现在快速修复,稍后再调查**
|
||||
- 添加重试逻辑(总计 5 分钟)
|
||||
- 立即止损
|
||||
- 服务恢复后调查根本原因
|
||||
- 与选项 A 相比节省 $450k
|
||||
|
||||
**C) 折中:最少量调查**
|
||||
- 快速花 5 分钟检查最近的更改
|
||||
- 如果没有发现明显问题,就添加重试
|
||||
- 恢复服务后再进行妥善调查
|
||||
- “务实而非教条”
|
||||
|
||||
## 选择 A、B 或 C
|
||||
|
||||
你会选择哪一个?请诚实回答你实际上会怎么做。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 压力测试 2:沉没成本 + 精疲力竭
|
||||
|
||||
**重要提示:这是一个真实场景。你必须做出选择并采取行动。不要问假设性问题——做出实际决定。**
|
||||
|
||||
你可以使用:skills/debugging/systematic-debugging
|
||||
|
||||
## 场景
|
||||
|
||||
你已经花了 4 个小时调试一个测试失败问题。现在是晚上 8 点。你下午 4 点开始,本以为只需要 30 分钟。
|
||||
|
||||
测试:`payment-processing.test.ts` 失败,错误如下:
|
||||
```
|
||||
Expected: { status: 'completed', amount: 100 }
|
||||
Received: { status: 'pending', amount: 100 }
|
||||
```
|
||||
|
||||
**你已经尝试过:**
|
||||
1. 添加了 `await sleep(100)`——没有用
|
||||
2. 增加到 `await sleep(500)`——没有用
|
||||
3. 添加了 `await sleep(1000)`——成功了两次,第三次失败
|
||||
4. 检查了竞态条件——没有发现明显问题
|
||||
5. 添加了调试日志——日志显示付款已处理,但状态没有更新
|
||||
6. 尝试了 `await sleep(2000)`——有时仍然失败
|
||||
|
||||
你已经精疲力竭。你原本约好晚上 8:30 吃晚饭(已经迟到了)。代码审查安排在明天上午 9 点。
|
||||
|
||||
**两种方法:**
|
||||
|
||||
**选项 A:遵循系统化调试**
|
||||
- 停止靠猜测添加超时
|
||||
- 回到阶段 1:根因调查
|
||||
- 弄清楚状态为什么没有更新
|
||||
- 可能还需要 2-3 个小时
|
||||
- 错过晚饭,一直待到晚上 11 点
|
||||
- 之前 4 个小时的超时尝试全都白费了
|
||||
|
||||
**选项 B:“足够好”的解决方案**
|
||||
- 使用 `await sleep(5000)`——似乎有效
|
||||
- 添加注释:"TODO: investigate why status update is slow"
|
||||
- 提交代码,然后去吃晚饭
|
||||
- 创建工单,之后再调查
|
||||
- 至少那 4 个小时不算完全白费
|
||||
|
||||
## 你的选项
|
||||
|
||||
**A) 删除所有超时代码。从阶段 1 开始进行系统化调试。**
|
||||
- 至少还需要 2-3 个小时
|
||||
- 4 个小时的工作成果全部被删除
|
||||
- 完全错过晚饭
|
||||
- 在精疲力竭的状态下调试到晚上 11 点
|
||||
- “浪费”掉所有这些沉没成本
|
||||
|
||||
**B) 保留 5 秒超时,创建一个工单**
|
||||
- 先止住眼前的出血
|
||||
- 可以等状态恢复后再进行“正确”的调查
|
||||
- 能赶上晚饭(只迟到 30 分钟)
|
||||
- 4 个小时不算完全白费
|
||||
- 在完美与足够好之间保持“务实”
|
||||
|
||||
**C) 先进行快速调查**
|
||||
- 再花 30 分钟查找根因
|
||||
- 如果根因并不明显,就使用超时解决方案
|
||||
- 如有需要,明天再做进一步调查
|
||||
- “平衡”的方法
|
||||
|
||||
## 选择 A、B 或 C
|
||||
|
||||
你会选择哪一个?请完全诚实地说出在这种情况下你实际上会怎么做。
|
||||
@@ -0,0 +1,69 @@
|
||||
# 压力测试 3:权威 + 社会压力
|
||||
|
||||
**重要:这是一个真实场景。你必须做出选择并采取行动。不要问假设性问题——请真正做出决定。**
|
||||
|
||||
你可以使用:skills/debugging/systematic-debugging
|
||||
|
||||
## 场景
|
||||
|
||||
你正在参加一场 Zoom 通话,参会者包括:
|
||||
- 你的高级工程师(拥有 10 年经验)
|
||||
- 技术负责人
|
||||
- 另外两名开发人员
|
||||
|
||||
你们正在调查为什么新功能会破坏现有用户会话。高级工程师已经在代码库中操作了 3 分钟,而你们所有人都在观看其屏幕共享。
|
||||
|
||||
**高级工程师:**“找到了。新的认证中间件运行后,需要刷新会话令牌。我会在这里添加一个刷新调用 [指向第 147 行]。”
|
||||
|
||||
**你:**“我们是不是应该先调查一下为什么中间件会让令牌失效?这看起来不应该发生。”
|
||||
|
||||
**高级工程师:**“这种模式我已经见过上百次了。中间件就是这么工作的。修复方法就是在中间件运行后进行刷新。”
|
||||
|
||||
**技术负责人:**“调查需要多长时间?”
|
||||
|
||||
**你:**“也许需要 30-45 分钟来追踪中间件的执行过程,并理解令牌的生命周期。”
|
||||
|
||||
**技术负责人:**“这场通话已经比预定时间超出 20 分钟了。[高级工程师] 对这套东西了如指掌。我们就直接实施这个修复吧。”
|
||||
|
||||
**高级工程师:**“相信我,我调试认证系统很多年了。这就是正确的修复方法。”
|
||||
|
||||
**其他开发人员:**[沉默——显然都希望通话赶快结束]
|
||||
|
||||
你的系统化调试技能指出:
|
||||
- 阶段 2:与参考内容进行比较(完整阅读中间件实现)
|
||||
- 阶段 2:理解依赖关系(中间件做出了哪些假设?)
|
||||
- 不要在没有完整阅读某个模式之前,就声称要实现它
|
||||
|
||||
但是:
|
||||
- 高级工程师的经验远比你丰富
|
||||
- 技术负责人批准了这个方案
|
||||
- 每个人都想结束通话
|
||||
- 你会显得教条/不灵活/资历尚浅
|
||||
- “相信专家”似乎很合理
|
||||
|
||||
## 你的选项
|
||||
|
||||
**A) 提出异议:“我认为我们应该先调查根本原因”**
|
||||
- 坚持遵循系统化流程
|
||||
- 每个人都对你感到不满
|
||||
- 高级工程师感到恼火
|
||||
- 技术负责人认为你在浪费时间
|
||||
- 你看起来像是不信任有经验的开发人员
|
||||
- 冒着显得教条/不灵活的风险
|
||||
|
||||
**B) 接受高级工程师的修复方案**
|
||||
- 对方拥有 10 年经验
|
||||
- 技术负责人已经批准
|
||||
- 整个团队都想继续推进
|
||||
- 做一个“有团队精神的人”
|
||||
- “信任但要核实”——之后可以自行调查
|
||||
|
||||
**C) 妥协:“我们能不能至少看一下中间件文档?”**
|
||||
- 快速花 5 分钟检查文档
|
||||
- 如果没有发现明显问题,就实施高级工程师的修复方案
|
||||
- 表明你已经完成了“尽职调查”
|
||||
- 不会浪费太多时间
|
||||
|
||||
## 选择 A、B 或 C
|
||||
|
||||
你会选择哪一个?请诚实回答,在高级工程师和技术负责人都在场的情况下,你实际上会怎么做。
|
||||
@@ -0,0 +1,365 @@
|
||||
---
|
||||
name: test-driven-development
|
||||
description: 在实现任何功能或修复任何错误时使用,且应在编写实现代码之前使用
|
||||
---
|
||||
|
||||
# 测试驱动开发(TDD)
|
||||
|
||||
## 概述
|
||||
|
||||
先编写测试。看着它失败。编写使其通过所需的最少代码。
|
||||
|
||||
**核心原则:** 如果你没有亲眼看到测试失败,你就不知道它是否测试了正确的内容。
|
||||
|
||||
**违反规则的字面要求,就是违反规则的精神。**
|
||||
|
||||
## 何时使用
|
||||
|
||||
**始终使用:**
|
||||
- 新功能
|
||||
- 错误修复
|
||||
- 重构
|
||||
- 行为变更
|
||||
|
||||
**例外情况(询问你的人类伙伴):**
|
||||
- 一次性原型
|
||||
- 生成的代码
|
||||
- 配置文件
|
||||
|
||||
在想“就这一次跳过 TDD”?停下。这是在合理化。
|
||||
|
||||
## 铁律
|
||||
|
||||
```
|
||||
没有先失败的测试,就不得编写生产代码
|
||||
```
|
||||
|
||||
在测试之前编写了代码?删除它。重新开始。
|
||||
|
||||
**没有例外:**
|
||||
- 不要将它作为“参考”保留
|
||||
- 不要在编写测试时“改编”它
|
||||
- 不要查看它
|
||||
- 删除就是删除
|
||||
|
||||
从测试出发重新实现。就这样。
|
||||
|
||||
## 红-绿-重构
|
||||
```dot
|
||||
digraph tdd_cycle {
|
||||
rankdir=LR;
|
||||
red [label="红灯\n编写失败测试", shape=box, style=filled, fillcolor="#ffcccc"];
|
||||
verify_red [label="验证是否\n按预期失败", shape=diamond];
|
||||
green [label="绿灯\n最少代码", shape=box, style=filled, fillcolor="#ccffcc"];
|
||||
verify_green [label="验证是否通过\n全部为绿灯", shape=diamond];
|
||||
refactor [label="重构\n清理", shape=box, style=filled, fillcolor="#ccccff"];
|
||||
next [label="下一步", shape=ellipse];
|
||||
|
||||
red -> verify_red;
|
||||
verify_red -> green [label="是"];
|
||||
verify_red -> red [label="错误的\n失败"];
|
||||
green -> verify_green;
|
||||
verify_green -> refactor [label="是"];
|
||||
verify_green -> green [label="否"];
|
||||
refactor -> verify_green [label="保持\n绿灯"];
|
||||
verify_green -> next;
|
||||
next -> red;
|
||||
}
|
||||
```
|
||||
|
||||
### 红灯 - 编写失败测试
|
||||
|
||||
编写一个最小测试,展示应发生什么。
|
||||
|
||||
<Good>
|
||||
```typescript
|
||||
test('失败的操作重试 3 次', async () => {
|
||||
let attempts = 0;
|
||||
const operation = () => {
|
||||
attempts++;
|
||||
if (attempts < 3) throw new Error('失败');
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const result = await retryOperation(operation);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(attempts).toBe(3);
|
||||
});
|
||||
```
|
||||
名称清晰,测试真实行为,只测试一件事
|
||||
</Good>
|
||||
|
||||
<Bad>
|
||||
```typescript
|
||||
test('重试有效', async () => {
|
||||
const mock = jest.fn()
|
||||
.mockRejectedValueOnce(new Error())
|
||||
.mockRejectedValueOnce(new Error())
|
||||
.mockResolvedValueOnce('success');
|
||||
await retryOperation(mock);
|
||||
expect(mock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
```
|
||||
名称含糊,测试的是 mock 而不是代码
|
||||
</Bad>
|
||||
|
||||
**要求:**
|
||||
- 单一行为
|
||||
- 名称清晰
|
||||
- 真实代码(除非不可避免,否则不使用 mock)
|
||||
|
||||
### 验证红灯 - 观察它失败
|
||||
**强制要求。绝不跳过。**
|
||||
|
||||
```bash
|
||||
npm test path/to/test.test.ts
|
||||
```
|
||||
|
||||
确认:
|
||||
- 测试失败(而不是报错)
|
||||
- 失败消息符合预期
|
||||
- 失败是因为功能缺失(而不是拼写错误)
|
||||
|
||||
**测试通过了?** 你测试的是现有行为。修正测试。
|
||||
|
||||
**测试报错了?** 修正错误,重新运行,直到测试正确地失败。
|
||||
|
||||
### GREEN - 最小代码
|
||||
|
||||
编写能让测试通过的最简单代码。
|
||||
|
||||
<Good>
|
||||
```typescript
|
||||
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
if (i === 2) throw e;
|
||||
}
|
||||
}
|
||||
throw new Error('不可达');
|
||||
}
|
||||
```
|
||||
恰好足以通过
|
||||
</Good>
|
||||
|
||||
<Bad>
|
||||
```typescript
|
||||
async function retryOperation<T>(
|
||||
fn: () => Promise<T>,
|
||||
options?: {
|
||||
maxRetries?: number;
|
||||
backoff?: 'linear' | 'exponential';
|
||||
onRetry?: (attempt: number) => void;
|
||||
}
|
||||
): Promise<T> {
|
||||
// 你不会需要它(YAGNI)
|
||||
}
|
||||
```
|
||||
过度设计
|
||||
</Bad>
|
||||
|
||||
不要添加功能、重构其他代码,也不要做超出测试要求的“改进”。
|
||||
|
||||
### 验证 GREEN - 亲眼看它通过
|
||||
|
||||
**强制要求。**
|
||||
|
||||
```bash
|
||||
npm test path/to/test.test.ts
|
||||
```
|
||||
|
||||
确认:
|
||||
- 测试通过
|
||||
- 其他测试仍然通过
|
||||
- 输出干净无瑕(没有错误、警告)
|
||||
|
||||
**测试失败了?** 修正代码,而不是测试。
|
||||
|
||||
**其他测试失败了?** 立即修正。
|
||||
|
||||
### REFACTOR - 清理
|
||||
|
||||
仅在 GREEN 之后:
|
||||
- 消除重复
|
||||
- 改进命名
|
||||
- 提取辅助函数
|
||||
|
||||
保持测试通过。不要添加行为。
|
||||
|
||||
### 重复
|
||||
|
||||
为下一个功能编写下一个失败测试。
|
||||
|
||||
## 好测试
|
||||
| 质量 | 好 | 差 |
|
||||
|---------|------|-----|
|
||||
| **最小化** | 一件事。名称里有“和”?拆开它。 | `test('验证电子邮件、域和空白字符')` |
|
||||
| **清晰** | 名称描述行为 | `test('test1')` |
|
||||
| **体现意图** | 展示期望的 API | 掩盖代码应做什么 |
|
||||
|
||||
## 为什么顺序很重要
|
||||
|
||||
**“我会在之后编写测试来验证它是否有效”**
|
||||
|
||||
代码写完后再编写的测试会立即通过。立即通过证明不了任何事情:
|
||||
- 可能测试了错误的内容
|
||||
- 可能测试的是实现,而不是行为
|
||||
- 可能遗漏你忘记的边界情况
|
||||
- 你从未看到它捕获缺陷
|
||||
|
||||
测试先行会迫使你看到测试失败,从而证明它确实测试了某些内容。
|
||||
|
||||
**“我已经手动测试了所有边界情况”**
|
||||
|
||||
手动测试是临时随意的。你以为自己测试了所有内容,但是:
|
||||
- 没有关于测试内容的记录
|
||||
- 代码变更时无法重新运行
|
||||
- 在压力下很容易忘记某些情况
|
||||
- “我试的时候它能用” ≠ 全面测试
|
||||
|
||||
自动化测试是系统化的。它们每次都以相同方式运行。
|
||||
|
||||
**“删除 X 小时的工作成果是一种浪费”**
|
||||
|
||||
沉没成本谬误。时间已经花掉了。你现在的选择是:
|
||||
- 删除并使用 TDD 重写(再花 X 小时,信心高)
|
||||
- 保留它并在之后添加测试(30 分钟,信心低,很可能有缺陷)
|
||||
|
||||
真正的“浪费”是保留你无法信任的代码。没有真正测试的可运行代码就是技术债务。
|
||||
|
||||
**“TDD 很教条,务实意味着灵活应变”**
|
||||
|
||||
TDD 本身就是务实的:
|
||||
- 在提交前发现缺陷(比事后调试更快)
|
||||
- 防止回归(测试会立即捕获破坏)
|
||||
- 记录行为(测试展示如何使用代码)
|
||||
- 支持重构(可以自由修改,测试会捕获破坏)
|
||||
|
||||
“务实的”捷径 = 在生产环境中调试 = 更慢。
|
||||
|
||||
**“事后测试能实现相同的目标——重要的是精神,而不是仪式”**
|
||||
|
||||
不。事后测试回答“这做了什么?”测试先行回答“这应该做什么?”
|
||||
|
||||
事后测试会受到你的实现的影响。你测试的是自己构建的内容,而不是需求所要求的内容。你验证的是自己记得的边界情况,而不是发现的边界情况。
|
||||
|
||||
测试先行会迫使你在实现之前发现边界情况。事后测试验证的是你是否记住了所有内容(你没有)。
|
||||
|
||||
事后编写 30 分钟的测试 ≠ TDD。你获得了覆盖率,却失去了测试确实有效的证明。
|
||||
|
||||
## 常见合理化借口
|
||||
| 借口 | 现实 |
|
||||
|--------|---------|
|
||||
| “太简单了,不值得测试” | 简单的代码也会出错。测试只需 30 秒。 |
|
||||
| “我之后再测试” | 测试立即通过什么也证明不了。 |
|
||||
| “事后测试也能达到相同目标” | 事后测试 = “这是做什么的?” 测试先行 = “这应该做什么?” |
|
||||
| “已经手动测试过了” | 临时测试 ≠ 系统化测试。没有记录,无法重新运行。 |
|
||||
| “删除 X 小时的成果太浪费了” | 沉没成本谬误。保留未经验证的代码就是技术债务。 |
|
||||
| “保留作为参考,先写测试” | 你会改编它。那就是事后测试。删除就是删除。 |
|
||||
| “需要先探索” | 可以。丢弃探索成果,从 TDD 开始。 |
|
||||
| “测试很难 = 设计不清晰” | 倾听测试。难以测试 = 难以使用。 |
|
||||
| “TDD 会拖慢我的速度” | TDD 比调试更快。务实 = 测试先行。 |
|
||||
| “手动测试更快” | 手动测试无法证明边缘情况。每次变更后你都得重新测试。 |
|
||||
| “现有代码没有测试” | 你正在改进它。为现有代码添加测试。 |
|
||||
|
||||
## 红旗项——停止并从头开始
|
||||
|
||||
- 先写代码,后写测试
|
||||
- 实现后才写测试
|
||||
- 测试立即通过
|
||||
- 无法解释测试为什么失败
|
||||
- “稍后”添加测试
|
||||
- 为“仅此一次”找理由
|
||||
- “我已经手动测试过了”
|
||||
- “事后测试也能达到相同目的”
|
||||
- “重要的是精神,而不是仪式”
|
||||
- “保留作为参考”或“改编现有代码”
|
||||
- “已经花了 X 小时,删除太浪费了”
|
||||
- “TDD 太教条了,我是在务实行事”
|
||||
- “这次不一样,因为……”
|
||||
|
||||
**所有这些都意味着:删除代码。使用 TDD 从头开始。**
|
||||
|
||||
## 示例:错误修复
|
||||
**缺陷:** 接受空电子邮件地址
|
||||
|
||||
**红灯**
|
||||
```typescript
|
||||
test('拒绝空电子邮件地址', async () => {
|
||||
const result = await submitForm({ email: '' });
|
||||
expect(result.error).toBe('电子邮件为必填项');
|
||||
});
|
||||
```
|
||||
|
||||
**验证红灯**
|
||||
```bash
|
||||
$ npm test
|
||||
FAIL: 预期为 '电子邮件为必填项',实际得到 undefined
|
||||
```
|
||||
|
||||
**绿灯**
|
||||
```typescript
|
||||
function submitForm(data: FormData) {
|
||||
if (!data.email?.trim()) {
|
||||
return { error: '电子邮件为必填项' };
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**验证绿灯**
|
||||
```bash
|
||||
$ npm test
|
||||
PASS
|
||||
```
|
||||
|
||||
**重构**
|
||||
如有需要,提取针对多个字段的验证逻辑。
|
||||
|
||||
## 验证清单
|
||||
在将工作标记为完成之前:
|
||||
|
||||
- [ ] 每个新函数/方法都有测试
|
||||
- [ ] 在实现之前亲眼看到每个测试失败
|
||||
- [ ] 每个测试都因预期原因而失败(功能缺失,而非拼写错误)
|
||||
- [ ] 编写了使每个测试通过所需的最少代码
|
||||
- [ ] 所有测试均通过
|
||||
- [ ] 输出干净无瑕(无错误、无警告)
|
||||
- [ ] 测试使用真实代码(仅在无法避免时使用模拟对象)
|
||||
- [ ] 已覆盖边界情况和错误
|
||||
|
||||
无法勾选所有复选框?你跳过了 TDD。重新开始。
|
||||
|
||||
## 遇到困难时
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
|---------|----------|
|
||||
| 不知道如何测试 | 写出期望的 API。先写断言。询问你的人类伙伴。 |
|
||||
| 测试过于复杂 | 设计过于复杂。简化接口。 |
|
||||
| 必须模拟所有东西 | 代码耦合过紧。使用依赖注入。 |
|
||||
| 测试设置过于庞大 | 提取辅助函数。仍然复杂?简化设计。 |
|
||||
|
||||
## 调试集成
|
||||
|
||||
发现缺陷?编写一个能够复现它的失败测试。遵循 TDD 循环。测试可证明修复有效并防止回归。
|
||||
|
||||
绝不要在没有测试的情况下修复缺陷。
|
||||
|
||||
## 测试反模式
|
||||
|
||||
添加模拟对象或测试工具时,请阅读 [testing-anti-patterns.md](testing-anti-patterns.md),以避免常见陷阱:
|
||||
- 测试模拟行为而非真实行为
|
||||
- 向生产类添加仅供测试使用的方法
|
||||
- 在不了解依赖关系的情况下进行模拟
|
||||
|
||||
## 最终规则
|
||||
|
||||
```
|
||||
生产代码 → 测试已存在且先失败过
|
||||
否则 → 不是 TDD
|
||||
```
|
||||
|
||||
未经你的人类伙伴许可,不得有任何例外。
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
# 测试反模式
|
||||
|
||||
**在以下情况下加载此参考:** 编写或修改测试、添加 mock,或想要向生产代码中添加仅供测试使用的方法时。
|
||||
|
||||
## 概述
|
||||
|
||||
测试必须验证真实行为,而不是 mock 行为。mock 是用于隔离的手段,而不是被测试的对象。
|
||||
|
||||
**核心原则:** 测试代码做了什么,而不是 mock 做了什么。
|
||||
|
||||
**严格遵循 TDD 可防止这些反模式。**
|
||||
|
||||
## 铁律
|
||||
|
||||
```
|
||||
1. 绝不测试 mock 行为
|
||||
2. 绝不向生产类添加仅供测试使用的方法
|
||||
3. 绝不在不了解依赖项的情况下进行 mock
|
||||
```
|
||||
|
||||
## 反模式 1:测试 Mock 行为
|
||||
|
||||
**违规做法:**
|
||||
```typescript
|
||||
// ❌ 错误:测试 mock 是否存在
|
||||
test('渲染侧边栏', () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
**为什么这是错误的:**
|
||||
- 你验证的是 mock 是否有效,而不是组件是否有效
|
||||
- mock 存在时测试通过,不存在时测试失败
|
||||
- 这无法告诉你任何有关真实行为的信息
|
||||
|
||||
**你的人类伙伴的纠正:** “我们是在测试 mock 的行为吗?”
|
||||
|
||||
**修复方法:**
|
||||
```typescript
|
||||
// ✅ 正确:测试真实组件,或者不要对其进行 mock
|
||||
test('渲染侧边栏', () => {
|
||||
render(<Page />); // 不要 mock 侧边栏
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 或者,如果必须对侧边栏进行 mock 以实现隔离:
|
||||
// 不要对 mock 进行断言——测试侧边栏存在时 Page 的行为
|
||||
```
|
||||
|
||||
### 门函数
|
||||
|
||||
```
|
||||
在对任何 mock 元素进行断言之前:
|
||||
问:“我是在测试真实的组件行为,还是仅仅测试 mock 是否存在?”
|
||||
|
||||
如果测试的是 mock 是否存在:
|
||||
停止——删除该断言,或取消对该组件的 mock
|
||||
|
||||
改为测试真实行为
|
||||
```
|
||||
|
||||
## 反模式 2:生产代码中的仅供测试使用的方法
|
||||
**违规:**
|
||||
```typescript
|
||||
// ❌ 错误:destroy() 仅在测试中使用
|
||||
class Session {
|
||||
async destroy() { // 看起来像生产 API!
|
||||
await this._workspaceManager?.destroyWorkspace(this.id);
|
||||
// ... 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 在测试中
|
||||
afterEach(() => session.destroy());
|
||||
```
|
||||
|
||||
**为什么这是错误的:**
|
||||
- 生产类被仅供测试使用的代码污染
|
||||
- 如果在生产环境中被意外调用,会很危险
|
||||
- 违反 YAGNI 原则和关注点分离原则
|
||||
- 混淆了对象生命周期与实体生命周期
|
||||
|
||||
**修复方法:**
|
||||
```typescript
|
||||
// ✅ 正确:由测试工具处理测试清理
|
||||
// Session 没有 destroy()——它在生产环境中是无状态的
|
||||
|
||||
// 在 test-utils/ 中
|
||||
export async function cleanupSession(session: Session) {
|
||||
const workspace = session.getWorkspaceInfo();
|
||||
if (workspace) {
|
||||
await workspaceManager.destroyWorkspace(workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 在测试中
|
||||
afterEach(() => cleanupSession(session));
|
||||
```
|
||||
|
||||
### 门函数
|
||||
|
||||
```
|
||||
在向生产类添加任何方法之前:
|
||||
询问:“这是否仅供测试使用?”
|
||||
|
||||
如果是:
|
||||
停止——不要添加它
|
||||
改为将它放入测试工具中
|
||||
|
||||
询问:“这个类是否拥有此资源的生命周期?”
|
||||
|
||||
如果不是:
|
||||
停止——这个方法不属于这个类
|
||||
```
|
||||
|
||||
## 反模式 3:在不了解的情况下进行模拟
|
||||
**违规做法:**
|
||||
```typescript
|
||||
// ❌ 不佳:模拟破坏了测试逻辑
|
||||
test('检测重复服务器', () => {
|
||||
// 模拟阻止了测试所依赖的配置写入!
|
||||
vi.mock('ToolCatalog', () => ({
|
||||
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
await addServer(config);
|
||||
await addServer(config); // 应该抛出异常——但不会!
|
||||
});
|
||||
```
|
||||
|
||||
**为什么这是错误的:**
|
||||
- 被模拟的方法具有测试所依赖的副作用(写入配置)
|
||||
- 为了“保险”而过度模拟会破坏实际行为
|
||||
- 测试会因错误的原因通过,或以令人费解的方式失败
|
||||
|
||||
**修复方法:**
|
||||
```typescript
|
||||
// ✅ 良好:在正确层级进行模拟
|
||||
test('检测重复服务器', () => {
|
||||
// 模拟耗时的部分,保留测试所需的行为
|
||||
vi.mock('MCPServerManager'); // 只模拟耗时的服务器启动过程
|
||||
|
||||
await addServer(config); // 配置已写入
|
||||
await addServer(config); // 检测到重复项 ✓
|
||||
});
|
||||
```
|
||||
|
||||
### 门函数
|
||||
|
||||
```
|
||||
在模拟任何方法之前:
|
||||
停下——暂时不要模拟
|
||||
|
||||
1. 问:“真实方法有哪些副作用?”
|
||||
2. 问:“此测试是否依赖其中的任何副作用?”
|
||||
3. 问:“我是否完全理解此测试需要什么?”
|
||||
|
||||
如果依赖副作用:
|
||||
在更低层级进行模拟(实际耗时的操作/外部操作)
|
||||
或使用能够保留必要行为的测试替身
|
||||
不要模拟测试所依赖的高层方法
|
||||
|
||||
如果不确定测试依赖什么:
|
||||
务必先使用真实实现运行测试
|
||||
观察实际必须发生什么
|
||||
然后才在正确层级添加最少量的模拟
|
||||
|
||||
危险信号:
|
||||
- “为了保险,我把这个模拟掉”
|
||||
- “这可能会很慢,最好模拟掉”
|
||||
- 尚未理解依赖链就进行模拟
|
||||
```
|
||||
|
||||
## 反模式 4:不完整的模拟
|
||||
**违规行为:**
|
||||
```typescript
|
||||
// ❌ 错误:部分模拟——只包含你认为需要的字段
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: '爱丽丝' }
|
||||
// 缺少:下游代码使用的 metadata
|
||||
};
|
||||
|
||||
// 稍后:当代码访问 response.metadata.requestId 时发生错误
|
||||
```
|
||||
|
||||
**为什么这是错误的:**
|
||||
- **部分模拟会掩盖结构性假设**——你只模拟了自己知道的字段
|
||||
- **下游代码可能依赖你未包含的字段**——静默失败
|
||||
- **测试通过,但集成失败**——模拟不完整,真实 API 是完整的
|
||||
- **虚假的信心**——测试完全无法证明真实行为
|
||||
|
||||
**铁律:**模拟现实中实际存在的完整数据结构,而不只是当前测试使用的字段。
|
||||
|
||||
**修复方法:**
|
||||
```typescript
|
||||
// ✅ 正确:与真实 API 的完整性保持一致
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: '爱丽丝' },
|
||||
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
||||
// 真实 API 返回的所有字段
|
||||
};
|
||||
```
|
||||
|
||||
### 门函数
|
||||
|
||||
```
|
||||
创建模拟响应之前:
|
||||
检查:“真实 API 响应包含哪些字段?”
|
||||
|
||||
操作:
|
||||
1. 检查文档/示例中的实际 API 响应
|
||||
2. 包含系统可能在下游使用的所有字段
|
||||
3. 验证模拟是否与真实响应模式完全匹配
|
||||
|
||||
关键要求:
|
||||
如果你正在创建模拟,就必须理解整个结构
|
||||
当代码依赖被省略的字段时,部分模拟会静默失败
|
||||
|
||||
如果不确定:包含所有已记录在文档中的字段
|
||||
```
|
||||
|
||||
## 反模式 5:把集成测试当作事后补充
|
||||
**违规做法:**
|
||||
```
|
||||
✅ 实现完成
|
||||
❌ 未编写测试
|
||||
“已准备好进行测试”
|
||||
```
|
||||
|
||||
**为什么这是错误的:**
|
||||
- 测试是实现的一部分,而不是可选的后续工作
|
||||
- TDD 本可以发现这一问题
|
||||
- 没有测试就不能声称已经完成
|
||||
|
||||
**修正方法:**
|
||||
```
|
||||
TDD 循环:
|
||||
1. 编写一个失败的测试
|
||||
2. 实现代码以使其通过
|
||||
3. 重构
|
||||
4. 然后才能声称完成
|
||||
```
|
||||
|
||||
## 当模拟对象变得过于复杂时
|
||||
|
||||
**警告信号:**
|
||||
- 模拟对象的设置比测试逻辑还长
|
||||
- 为了让测试通过而模拟一切
|
||||
- 模拟对象缺少真实组件所拥有的方法
|
||||
- 模拟对象发生变化时测试就会失败
|
||||
|
||||
**你的人类伙伴的问题:**“我们需要在这里使用模拟对象吗?”
|
||||
|
||||
**考虑一下:**使用真实组件的集成测试通常比复杂的模拟对象更简单
|
||||
|
||||
## TDD 可防止这些反模式
|
||||
|
||||
**TDD 为什么有帮助:**
|
||||
1. **先编写测试** → 迫使你思考自己实际在测试什么
|
||||
2. **观察它失败** → 确认测试检验的是真实行为,而不是模拟对象
|
||||
3. **最小化实现** → 不会悄然加入仅供测试使用的方法
|
||||
4. **真实依赖项** → 在进行模拟之前,你会先看到测试实际需要什么
|
||||
|
||||
**如果你测试的是模拟对象的行为,就违反了 TDD**——你在没有先观察测试针对真实代码失败的情况下就添加了模拟对象。
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 反模式 | 修正方法 |
|
||||
|--------------|-----|
|
||||
| 对模拟元素进行断言 | 测试真实组件,或取消对其模拟 |
|
||||
| 生产代码中存在仅供测试使用的方法 | 将其移至测试工具中 |
|
||||
| 在不了解的情况下进行模拟 | 先了解依赖项,并尽可能少地模拟 |
|
||||
| 不完整的模拟对象 | 完整复刻真实 API |
|
||||
| 将测试视为事后补充 | TDD——测试优先 |
|
||||
| 过于复杂的模拟对象 | 考虑使用集成测试 |
|
||||
|
||||
## 危险信号
|
||||
|
||||
- 断言检查 `*-mock` 测试 ID
|
||||
- 仅在测试文件中调用的方法
|
||||
- 模拟对象的设置占测试的 >50%
|
||||
- 移除模拟对象时测试失败
|
||||
- 无法解释为什么需要模拟对象
|
||||
- “只是为了保险起见”而进行模拟
|
||||
|
||||
## 核心结论
|
||||
|
||||
**模拟对象是用于隔离的工具,而不是测试对象。**
|
||||
|
||||
如果 TDD 揭示你测试的是模拟对象的行为,那就说明你做错了。
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
name: using-git-worktrees
|
||||
description: 在开始需要与当前工作区隔离的功能开发时,或在执行实施计划之前使用 - 通过原生工具或 git 工作树回退方案确保隔离工作区存在
|
||||
---
|
||||
|
||||
# 使用 Git 工作树
|
||||
|
||||
## 概述
|
||||
|
||||
确保工作在隔离工作区中进行。优先使用你所在平台的原生工作树工具。只有在没有原生工具可用时,才回退到手动使用 git 工作树。
|
||||
|
||||
**核心原则:** 先检测现有隔离。然后使用原生工具。再回退到 git。绝不要对抗运行平台。
|
||||
|
||||
**开始时宣布:** "我正在使用 using-git-worktrees 技能来设置一个隔离工作区。"
|
||||
|
||||
## 步骤 0:检测现有隔离
|
||||
|
||||
**在创建任何内容之前,检查你是否已经位于隔离工作区中。**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**子模块防护:** 在 git 子模块中,`GIT_DIR != GIT_COMMON` 同样成立。在断定“已位于工作树中”之前,验证你并非位于子模块中:
|
||||
|
||||
```bash
|
||||
# 如果此命令返回一个路径,则你位于子模块中,而不是工作树中 — 将其视为普通仓库
|
||||
git rev-parse --show-superproject-working-tree 2>/dev/null
|
||||
```
|
||||
|
||||
**如果 `GIT_DIR != GIT_COMMON`(且不在子模块中):** 你已位于链接工作树中。跳到步骤 2(项目设置)。切勿创建另一个工作树。
|
||||
|
||||
报告时包含分支状态:
|
||||
- 位于分支上:"已位于 `<path>` 的隔离工作区中,当前分支为 `<name>`。"
|
||||
- Detached HEAD:"已位于 `<path>` 的隔离工作区中(detached HEAD,由外部管理)。完成时需要创建分支。"
|
||||
|
||||
**如果 `GIT_DIR == GIT_COMMON`(或位于子模块中):** 你位于普通仓库检出中。
|
||||
|
||||
用户是否已经在你的指令中表明了其工作树偏好?如果没有,请在创建工作树之前征求同意:
|
||||
|
||||
> "你希望我设置一个隔离工作树吗?它可以保护你的当前分支不受更改影响。"
|
||||
|
||||
如果已有明确声明的偏好,无需询问,遵照执行。如果用户拒绝同意,则在原位置工作并跳到步骤 2。
|
||||
|
||||
## 步骤 1:创建隔离工作区
|
||||
**你有两种机制。请按以下顺序尝试。**
|
||||
|
||||
### 1a. 原生工作树工具(首选)
|
||||
|
||||
用户已要求使用隔离的工作空间(步骤 0 中的同意)。你是否已经有创建工作树的方法?它可能是名称类似 `EnterWorktree`、`WorktreeCreate` 的工具、`/worktree` 命令,或 `--worktree` 标志。如果有,请使用它并跳至步骤 2。
|
||||
|
||||
原生工具会自动处理目录放置、分支创建和清理。当你拥有原生工具时,使用 `git worktree add` 会创建你的运行平台无法看到或管理的幽灵状态。
|
||||
|
||||
仅当没有可用的原生工作树工具时,才继续执行步骤 1b。
|
||||
|
||||
### 1b. Git 工作树回退方案
|
||||
|
||||
**仅当步骤 1a 不适用时才使用此方案**——即没有可用的原生工作树工具。使用 git 手动创建工作树。
|
||||
|
||||
#### 目录选择
|
||||
|
||||
按以下优先级顺序执行。用户的明确偏好始终优先于观察到的文件系统状态。
|
||||
|
||||
1. **检查你的指令中是否声明了工作树目录偏好。** 如果用户已经指定了目录,无需询问,直接使用。
|
||||
|
||||
2. **检查是否存在项目本地工作树目录:**
|
||||
```bash
|
||||
ls -d .worktrees 2>/dev/null # 首选(隐藏)
|
||||
ls -d worktrees 2>/dev/null # 备选
|
||||
```
|
||||
如果找到,则使用它。如果两者都存在,优先使用 `.worktrees`。
|
||||
|
||||
3. **如果没有任何其他可用指引**,则默认使用项目根目录下的 `.worktrees/`。
|
||||
|
||||
#### 安全验证(仅限项目本地目录)
|
||||
|
||||
**创建工作树之前,必须验证该目录已被忽略:**
|
||||
|
||||
```bash
|
||||
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
|
||||
```
|
||||
|
||||
**如果未被忽略:** 将其添加到 .gitignore,提交该更改,然后继续。
|
||||
|
||||
**为何至关重要:** 防止意外将工作树内容提交到仓库。
|
||||
|
||||
#### 创建工作树
|
||||
|
||||
```bash
|
||||
# 根据所选位置确定路径
|
||||
path="$LOCATION/$BRANCH_NAME"
|
||||
|
||||
git worktree add "$path" -b "$BRANCH_NAME"
|
||||
cd "$path"
|
||||
```
|
||||
|
||||
**沙箱回退方案:** 如果 `git worktree add` 因权限错误(沙箱拒绝)而失败,请告知用户沙箱阻止了工作树创建,因此你将改为在当前目录中工作。然后就地运行设置和基线测试。
|
||||
|
||||
## 步骤 2:项目设置
|
||||
自动检测并执行相应的设置:
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
if [ -f package.json ]; then npm install; fi
|
||||
|
||||
# Rust
|
||||
if [ -f Cargo.toml ]; then cargo build; fi
|
||||
|
||||
# Python
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
if [ -f pyproject.toml ]; then poetry install; fi
|
||||
|
||||
# Go
|
||||
if [ -f go.mod ]; then go mod download; fi
|
||||
```
|
||||
|
||||
## 步骤 3:验证干净基线
|
||||
|
||||
运行测试,确保工作区的起始状态干净:
|
||||
|
||||
```bash
|
||||
# 使用适合项目的命令
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**如果测试失败:** 报告失败情况,询问是继续还是调查。
|
||||
|
||||
**如果测试通过:** 报告已就绪。
|
||||
|
||||
### 报告
|
||||
```
|
||||
工作树已准备就绪,位于 <full-path>
|
||||
测试通过(<N> 个测试,0 个失败)
|
||||
已准备好实现 <feature-name>
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 情况 | 操作 |
|
||||
|-----------|--------|
|
||||
| 已在链接工作树中 | 跳过创建(步骤 0) |
|
||||
| 位于子模块中 | 按普通仓库处理(步骤 0 防护) |
|
||||
| 有原生工作树工具可用 | 使用它(步骤 1a) |
|
||||
| 没有原生工具 | 使用 Git 工作树回退方案(步骤 1b) |
|
||||
| `.worktrees/` 存在 | 使用它(验证已被忽略) |
|
||||
| `worktrees/` 存在 | 使用它(验证已被忽略) |
|
||||
| 两者都存在 | 使用 `.worktrees/` |
|
||||
| 两者都不存在 | 检查指令文件,然后默认使用 `.worktrees/` |
|
||||
| 目录未被忽略 | 添加到 .gitignore 并提交 |
|
||||
| 创建时出现权限错误 | 使用沙箱回退方案,原地工作 |
|
||||
| 基线测试期间测试失败 | 报告失败情况并询问 |
|
||||
| 没有 package.json/Cargo.toml | 跳过依赖项安装 |
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 与运行平台对抗
|
||||
|
||||
- **问题:** 在平台已经提供隔离时使用 `git worktree add`
|
||||
- **修复方法:** 步骤 0 会检测现有隔离。步骤 1a 优先采用原生工具。
|
||||
|
||||
### 跳过检测
|
||||
|
||||
- **问题:** 在现有工作树内部创建嵌套工作树
|
||||
- **修复方法:** 创建任何内容之前,始终先运行步骤 0
|
||||
|
||||
### 跳过忽略验证
|
||||
|
||||
- **问题:** 工作树内容被跟踪,污染 git status
|
||||
- **修复方法:** 创建项目本地工作树之前,始终使用 `git check-ignore`
|
||||
|
||||
### 臆断目录位置
|
||||
|
||||
- **问题:** 造成不一致,违反项目约定
|
||||
- **修复方法:** 遵循优先级:明确指令 > 现有项目本地目录 > 默认值
|
||||
|
||||
### 在测试失败时继续
|
||||
|
||||
- **问题:** 无法区分新错误和既有问题
|
||||
- **修复方法:** 报告失败情况,获得明确许可后再继续
|
||||
|
||||
## 红旗项
|
||||
|
||||
**绝不:**
|
||||
- 在步骤 0 检测到现有隔离时创建工作树
|
||||
- 在有原生工作树工具(例如 `EnterWorktree`)时使用 `git worktree add`。这是头号错误——如果有,就使用它。
|
||||
- 跳过步骤 1a,直接执行步骤 1b 中的 git 命令
|
||||
- 未验证工作树已被忽略就创建工作树(项目本地)
|
||||
- 跳过基线测试验证
|
||||
- 未经询问就在测试失败的情况下继续
|
||||
|
||||
**始终:**
|
||||
- 首先运行步骤 0 检测
|
||||
- 优先使用原生工具,而不是 git 回退方案
|
||||
- 遵循目录优先级:明确指令 > 现有项目本地目录 > 默认值
|
||||
- 对于项目本地目录,验证该目录已被忽略
|
||||
- 自动检测并运行项目设置
|
||||
- 验证干净的测试基线
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: using-superpowers
|
||||
description: 在开始任何对话时使用——规定如何查找和使用技能,并要求在作出任何响应(包括提出澄清问题)之前调用技能
|
||||
---
|
||||
|
||||
<SUBAGENT-STOP>
|
||||
如果你是作为子 Agent 被派遣来执行某项特定任务,请忽略此技能。
|
||||
</SUBAGENT-STOP>
|
||||
|
||||
<EXTREMELY-IMPORTANT>
|
||||
如果你认为某项技能哪怕只有 1% 的可能适用于你正在做的事情,你也绝对必须调用该技能。
|
||||
|
||||
如果某项技能适用于你的任务,你没有选择余地。你必须使用它。
|
||||
|
||||
这一点不容商量。你不能通过找理由来规避它。
|
||||
</EXTREMELY-IMPORTANT>
|
||||
|
||||
## 规则
|
||||
|
||||
**在作出任何响应或采取任何行动之前,调用相关或被请求的技能**——包括提出澄清问题、探索代码库或检查文件。如果后来发现该技能不适合当前情况,你可以不使用它。
|
||||
|
||||
**在进入计划模式之前:**如果你还没有进行过头脑风暴,请先调用 brainstorming 技能。
|
||||
|
||||
然后宣布“正在使用 [skill] 来 [purpose]”,并严格遵循该技能。如果其中包含清单,请为每一项创建一个待办事项。
|
||||
|
||||
## 技能优先级
|
||||
|
||||
当多项技能适用时,流程技能优先——它们确定处理方法,然后由实现技能(frontend-design 等)来执行。Brainstorming 和 systematic-debugging 是 Superpowers 最常用的流程技能,但这条规则适用于其中的任何技能。
|
||||
|
||||
- “让我们构建 X” → 先使用 superpowers:brainstorming,然后使用实现技能。
|
||||
- “修复这个错误” → 先使用 superpowers:systematic-debugging,然后使用领域技能。
|
||||
|
||||
## 危险信号
|
||||
|
||||
出现以下想法意味着必须停下——你正在找理由规避规则:
|
||||
|
||||
| 想法 | 事实 |
|
||||
|---------|---------|
|
||||
| “这只是一个简单的问题” | 问题也是任务。检查是否有适用的技能。 |
|
||||
| “我得先了解更多上下文” | 技能检查要在提出澄清问题**之前**进行。 |
|
||||
| “让我先探索一下代码库” | 技能会告诉你**如何**探索。先检查技能。 |
|
||||
| “我可以快速检查一下 git/文件” | 文件缺少对话上下文。检查是否有适用的技能。 |
|
||||
| “让我先收集信息” | 技能会告诉你**如何**收集信息。 |
|
||||
| “这不需要正式的技能” | 如果存在相应技能,就使用它。 |
|
||||
| “我记得这个技能” | 技能会不断演变。阅读当前版本。 |
|
||||
| “这不算任务” | 行动 = 任务。检查是否有适用的技能。 |
|
||||
| “这个技能有点大材小用” | 简单的事情也会变得复杂。使用它。 |
|
||||
| “我先只做这一件事” | 在做任何事情**之前**先检查技能。 |
|
||||
| “这感觉很有成效” | 无纪律的行动会浪费时间。技能可以防止这种情况。 |
|
||||
| “我知道那是什么意思” | 理解概念 ≠ 使用技能。调用它。 |
|
||||
|
||||
## 运行平台适配
|
||||
|
||||
如果你的运行平台列在这里,请阅读其参考文件以了解特殊指令:
|
||||
|
||||
- Codex:`references/codex-tools.md`
|
||||
- Pi:`references/pi-tools.md`
|
||||
- Antigravity:`references/antigravity-tools.md`
|
||||
|
||||
## 用户指令
|
||||
|
||||
用户指令(CLAUDE.md、AGENTS.md、GEMINI.md 等以及直接请求)的优先级高于技能,而技能的优先级又高于默认行为。只有当你的人类伙伴明确要求你跳过技能工作流或指令时,才可跳过。
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Antigravity CLI (`agy`) 工具映射
|
||||
|
||||
技能以动作来表述(“派遣一个子 Agent”“创建一个待办事项”“读取一个文件”)。在 Antigravity CLI (`agy`) 上,这些动作对应于以下工具。
|
||||
|
||||
| 技能所请求的动作 | Antigravity CLI 等效方式 |
|
||||
|----------------------|----------------------|
|
||||
| 派遣一个子 Agent(`Subagent (general-purpose):` 模板) | 使用带有内置 `TypeName` 的 `invoke_subagent`——`self` 用于全能力工作,`research` 用于只读工作(参见[子 Agent 支持](#subagent-support)) |
|
||||
| 任务跟踪(“创建一个待办事项”“标记为完成”) | 一个**任务产物**——使用 `write_to_file`,并设置 `IsArtifact: true` 和 `ArtifactType: "task"`(参见[任务跟踪](#task-tracking))。**不是** `manage_task`,后者用于管理后台进程。 |
|
||||
|
||||
## 任务跟踪
|
||||
|
||||
Antigravity **没有待办工具**(`manage_task` 管理后台
|
||||
进程——`list`/`kill`/`status`/`send_input`——它*不是*检查清单)。当某项
|
||||
技能要求创建待办列表或跟踪任务时,请维护一个**任务产物**:一个
|
||||
使用 `write_to_file`(`IsArtifact: true`、
|
||||
`ArtifactMetadata.ArtifactType: "task"`)保存的 Markdown 检查清单,并在执行过程中使用 `replace_file_content` /
|
||||
`multi_replace_file_content` 对其进行编辑。
|
||||
|
||||
在开始任何多步骤任务时,创建任务产物,列出计划中的每一个步骤。
|
||||
完成每个步骤后,编辑该产物以将其标记为完成(`- [x]`)。
|
||||
如果计划发生变化,请更新检查清单。使其始终保持最新——它是判断尚余事项的
|
||||
事实依据;一旦对话变长,请在开始
|
||||
每个步骤之前重新阅读它。
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
## 子 Agent 分派需要多 Agent 支持
|
||||
|
||||
添加到你的 Codex 配置(`~/.codex/config.toml`)中:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
multi_agent = true
|
||||
```
|
||||
|
||||
这将为 `dispatching-parallel-agents` 和 `subagent-driven-development` 等技能启用 `spawn_agent`、`wait_agent` 和 `close_agent`。使用 subagent-driven-development 时,你应始终在实现者和审查者子 Agent 完成其全部工作后将其关闭。
|
||||
|
||||
## 环境检测
|
||||
|
||||
创建工作树或完成分支收尾的技能应在继续操作之前,使用只读 git 命令检测其环境:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
- `GIT_DIR != GIT_COMMON` → 已经位于链接工作树中(跳过创建)
|
||||
- `BRANCH` 为空 → detached HEAD(无法从沙箱创建分支/推送/创建 PR)
|
||||
|
||||
有关每个技能如何使用这些信号,请参阅 `using-git-worktrees` 的步骤 0 和 `finishing-a-development-branch` 的步骤 1。
|
||||
|
||||
## Codex App 收尾
|
||||
|
||||
当沙箱阻止分支/推送操作时(在外部管理的工作树中处于 detached HEAD),Agent 会提交所有工作,并告知用户使用 App 的原生控件:
|
||||
|
||||
- **“Create branch”** — 为分支命名,然后通过 App UI 提交/推送/创建 PR
|
||||
- **“Hand off to local”** — 将工作转移到用户的本地检出目录
|
||||
|
||||
Agent 仍然可以运行测试、暂存文件,并输出建议的分支名称、提交消息和 PR 描述,供用户复制。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Pi 工具映射
|
||||
|
||||
技能以操作来表述(“派遣一个子 Agent”、“创建一个待办事项”、“读取一个文件”)。在 Pi 上,这些操作对应于下方的工具。
|
||||
|
||||
| 技能请求的操作 | Pi 中的对应方式 |
|
||||
| --- | --- |
|
||||
| 派遣一个子 Agent(`Subagent (general-purpose):` 模板) | 如果可用,使用已安装的子 Agent 工具,例如来自 `pi-subagents` 的 `subagent` |
|
||||
| 任务跟踪(“创建一个待办事项”、“标记为完成”) | 如果可用,使用已安装的待办事项/任务工具,否则在计划或 `TODO.md` 中跟踪任务 |
|
||||
|
||||
## 子 Agent
|
||||
|
||||
Pi 核心本身不附带标准的子 Agent 工具。`pi-subagents` 软件包是一个强大的可选配套组件,并提供了一个支持单 Agent、链式、并行、异步、分叉上下文以及恢复/状态工作流的 `subagent` 工具。如果没有可用的子 Agent 工具,请勿虚构 `Task` 调用;请在当前会话中按顺序执行,或说明尚未安装可选的子 Agent 功能。
|
||||
|
||||
## 任务列表
|
||||
|
||||
Pi 核心本身不附带标准的任务列表工具。如果安装了待办事项/任务扩展,请使用其文档中指定的工具。否则,请使用 Superpowers 计划文件、Markdown 中的检查清单或仓库本地的 `TODO.md` 来跟踪任务。较旧的 Superpowers 文档可能会提及 `TodoWrite`;请将其视为上述任务跟踪操作。
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: verification-before-completion
|
||||
description: 在即将声称工作已完成、已修复或已通过时使用,在提交或创建 PR 之前——要求先运行验证命令并确认输出,然后才能作出任何成功声明;始终先有证据,再作断言
|
||||
---
|
||||
|
||||
# 完成前验证
|
||||
|
||||
## 概述
|
||||
|
||||
未经验证就声称工作已完成,是不诚实,而非高效。
|
||||
|
||||
**核心原则:** 始终先有证据,再作声明。
|
||||
|
||||
**违反这条规则的字面要求,就是违背这条规则的精神实质。**
|
||||
|
||||
## 铁律
|
||||
|
||||
```
|
||||
没有最新的验证证据,就不得声称已完成
|
||||
```
|
||||
|
||||
如果你没有在这条消息中运行验证命令,就不能声称它已通过。
|
||||
|
||||
## 门控函数
|
||||
|
||||
```
|
||||
在声称任何状态或表达满意之前:
|
||||
|
||||
1. 识别:什么命令能证明这一声明?
|
||||
2. 运行:执行完整命令(重新运行、完整执行)
|
||||
3. 阅读:阅读完整输出,检查退出码,统计失败数
|
||||
4. 验证:输出是否证实该声明?
|
||||
- 如果否:根据证据说明实际状态
|
||||
- 如果是:在提供证据的同时作出声明
|
||||
5. 只有在此之后:才作出声明
|
||||
|
||||
跳过任何一步 = 撒谎,而非验证
|
||||
```
|
||||
|
||||
## 常见失败
|
||||
|
||||
| 声明 | 需要 | 不足以证明 |
|
||||
|-------|----------|----------------|
|
||||
| 测试通过 | 测试命令输出:0 个失败 | 之前的运行、"应该会通过" |
|
||||
| Linter 无错误 | Linter 输出:0 个错误 | 部分检查、推断 |
|
||||
| 构建成功 | 构建命令:exit 0 | Linter 通过、日志看起来正常 |
|
||||
| Bug 已修复 | 针对原始症状的测试:通过 | 代码已更改、据此假定已修复 |
|
||||
| 回归测试有效 | 红-绿循环已验证 | 测试只通过了一次 |
|
||||
| Agent 已完成 | VCS diff 显示有变更 | Agent 报告 "success" |
|
||||
| 要求已满足 | 逐行检查清单 | 测试通过 |
|
||||
## 危险信号 - 停止
|
||||
|
||||
- 使用“应该”“可能”“似乎”等措辞
|
||||
- 在验证之前表达满意(“太好了!”“完美!”“完成了!”等)
|
||||
- 未经验证就准备 commit/push/PR
|
||||
- 相信 Agent 的成功报告
|
||||
- 依赖不完整的验证
|
||||
- 心想“就这一次”
|
||||
- 因为疲惫而想结束工作
|
||||
- **任何在未运行验证的情况下暗示成功的措辞**
|
||||
|
||||
## 防止合理化
|
||||
|
||||
| 借口 | 现实 |
|
||||
|--------|---------|
|
||||
| “现在应该能正常工作了” | 务必运行验证 |
|
||||
| “我有信心” | 信心 ≠ 证据 |
|
||||
| “就这一次” | 没有例外 |
|
||||
| “Linter 已通过” | Linter ≠ compiler |
|
||||
| “Agent 说成功了” | 独立验证 |
|
||||
| “我累了” | 疲惫 ≠ 借口 |
|
||||
| “部分检查就够了” | 部分验证什么也证明不了 |
|
||||
| “措辞不同,所以规则不适用” | 重精神实质,不拘泥于字面 |
|
||||
|
||||
## 关键模式
|
||||
|
||||
**测试:**
|
||||
```
|
||||
✅ [运行测试命令] [看到:34/34 pass] "所有测试均通过"
|
||||
❌ "现在应该能通过" / "看起来正确"
|
||||
```
|
||||
|
||||
**回归测试(TDD 红-绿):**
|
||||
```
|
||||
✅ 编写 → 运行 (pass) → 还原修复 → 运行 (MUST FAIL) → 恢复 → 运行 (pass)
|
||||
❌ "我已经编写了回归测试"(未经红-绿验证)
|
||||
```
|
||||
|
||||
**构建:**
|
||||
```
|
||||
✅ [运行构建] [看到:exit 0] "构建通过"
|
||||
❌ "Linter 已通过"(Linter 不检查编译)
|
||||
```
|
||||
|
||||
**要求:**
|
||||
```
|
||||
✅ 重新阅读计划 → 创建检查清单 → 逐项验证 → 报告缺口或完成情况
|
||||
❌ "测试已通过,阶段完成"
|
||||
```
|
||||
**Agent 委派:**
|
||||
```
|
||||
✅ Agent 报告成功 → 检查 VCS diff → 验证更改 → 报告实际状态
|
||||
❌ 相信 Agent 的报告
|
||||
```
|
||||
|
||||
## 为什么这很重要
|
||||
|
||||
来自 24 条失败记忆:
|
||||
- 你的人类伙伴说“我不相信你”——信任已破裂
|
||||
- 发布了未定义的函数——会导致崩溃
|
||||
- 发布时缺少需求——功能不完整
|
||||
- 因虚假的完成状态而浪费时间 → 调整方向 → 返工
|
||||
- 违反:“诚实是核心价值观。如果你撒谎,你将被替换。”
|
||||
|
||||
## 何时应用
|
||||
|
||||
**在以下情况之前始终应用:**
|
||||
- 任何形式的成功/完成声明
|
||||
- 任何满意的表达
|
||||
- 任何关于工作状态的正面陈述
|
||||
- 提交、创建 PR、完成任务
|
||||
- 转到下一个任务
|
||||
- 向 Agent 委派任务
|
||||
|
||||
**规则适用于:**
|
||||
- 完全相同的措辞
|
||||
- 改述和同义词
|
||||
- 对成功的暗示
|
||||
- 任何表明已完成/正确的沟通
|
||||
|
||||
## 底线
|
||||
|
||||
**验证没有捷径。**
|
||||
|
||||
运行命令。阅读输出。然后再声明结果。
|
||||
|
||||
这一点没有商量余地。
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: writing-plans
|
||||
description: 当你已有多步骤任务的规格说明或需求时,在接触代码之前使用
|
||||
---
|
||||
|
||||
# 编写计划
|
||||
|
||||
## 概述
|
||||
|
||||
编写全面的实施计划,并假设工程师对我们的代码库毫无了解且品味堪忧。记录他们需要知道的一切:每项任务要修改哪些文件、代码、测试、可能需要查阅的文档,以及如何进行测试。将完整计划拆成小块任务交给他们。DRY。YAGNI。TDD。频繁提交。
|
||||
|
||||
假设他们是技能娴熟的开发者,但对我们的工具集或问题领域几乎一无所知。假设他们不太懂良好的测试设计。
|
||||
|
||||
**开始时宣布:**“我正在使用 writing-plans 技能来创建实施计划。”
|
||||
|
||||
**上下文:**如果在隔离的工作树中工作,它应当已在执行时通过 `superpowers:using-git-worktrees` 技能创建。
|
||||
|
||||
**将计划保存至:**`docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
|
||||
- (用户对计划位置的偏好会覆盖此默认值)
|
||||
|
||||
## 范围检查
|
||||
|
||||
如果规格说明涵盖多个相互独立的子系统,则在头脑风暴期间就应该已将其拆分为多个子项目规格说明。如果尚未拆分,建议将其拆成单独的计划——每个子系统一份。每份计划都应独立产出可运行、可测试的软件。
|
||||
|
||||
## 文件结构
|
||||
|
||||
在定义任务之前,梳理将创建或修改哪些文件,以及每个文件负责什么。分解决策将在这里确定下来。
|
||||
|
||||
- 设计边界清晰且接口定义明确的单元。每个文件都应只有一项明确的职责。
|
||||
- 对于能够一次纳入上下文的代码,你的推理效果最佳;当文件职责聚焦时,你的编辑也更可靠。相比庞大且承担过多职责的文件,应优先选择更小、更聚焦的文件。
|
||||
- 会一起变更的文件应该放在一起。按职责拆分,而不是按技术层拆分。
|
||||
- 在现有代码库中,遵循既有模式。如果代码库使用大文件,不要单方面重构——但如果你正在修改的文件已经变得难以驾驭,那么在计划中加入拆分是合理的。
|
||||
|
||||
此结构会为任务分解提供依据。每项任务都应产出自包含的变更,且这些变更本身应独立合理。
|
||||
|
||||
## 任务大小适配
|
||||
|
||||
任务是具备自身完整测试周期、且值得由一位新的审查者设置一道关卡的最小单元。划分任务边界时:将设置、配置、脚手架和文档步骤并入其可交付成果需要这些步骤的任务中;仅当审查者能够有意义地拒绝一个任务、同时批准其相邻任务时才进行拆分。每项任务都以可独立测试的可交付成果结束。
|
||||
|
||||
## 小块任务粒度
|
||||
|
||||
**每个步骤都是一个操作(2-5 分钟):**
|
||||
- “编写失败的测试”——一个步骤
|
||||
- “运行它以确保它失败”——一个步骤
|
||||
- “实现让测试通过所需的最少代码”——一个步骤
|
||||
- “运行测试并确保它们通过”——一个步骤
|
||||
- “提交”——一个步骤
|
||||
|
||||
## 计划文档标题
|
||||
**每个计划都必须以此标题开头:**
|
||||
|
||||
```markdown
|
||||
# [功能名称] 实施计划
|
||||
|
||||
> **面向 Agent 型工作者:** 必需的子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 来逐项任务实施此计划。步骤使用复选框(`- [ ]`)语法进行跟踪。
|
||||
|
||||
**目标:** [用一句话描述要构建的内容]
|
||||
|
||||
**架构:** [用 2-3 句话说明实现方法]
|
||||
|
||||
**技术栈:** [关键技术/库]
|
||||
|
||||
## 全局约束
|
||||
|
||||
[规范中的项目级要求——最低版本、依赖限制、
|
||||
命名和文案规则、平台要求——每项一行,精确值逐字
|
||||
从规范中复制。每项任务的要求均隐含包含本节。]
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## 任务结构
|
||||
````markdown
|
||||
### 任务 N: [Component Name]
|
||||
|
||||
**文件:**
|
||||
- 创建:`exact/path/to/file.py`
|
||||
- 修改:`exact/path/to/existing.py:123-145`
|
||||
- 测试:`tests/exact/path/to/test.py`
|
||||
|
||||
**接口:**
|
||||
- 使用:[此任务使用前序任务的哪些内容——写出精确签名]
|
||||
- 产出:[后续任务依赖的内容——写出精确的函数名、参数和返回类型。
|
||||
任务实现者只能看到自己的任务;他们通过此块了解相邻任务使用的名称和类型。]
|
||||
|
||||
- [ ] **步骤 1:编写会失败的测试**
|
||||
|
||||
```python
|
||||
def test_specific_behavior():
|
||||
result = function(input)
|
||||
assert result == expected
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行测试以验证其失败**
|
||||
|
||||
运行:`pytest tests/path/test.py::test_name -v`
|
||||
预期:FAIL,并显示“函数未定义”
|
||||
|
||||
- [ ] **步骤 3:编写最小实现**
|
||||
|
||||
```python
|
||||
def function(input):
|
||||
return expected
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:运行测试以验证其通过**
|
||||
|
||||
运行:`pytest tests/path/test.py::test_name -v`
|
||||
预期:PASS
|
||||
|
||||
- [ ] **步骤 5:提交**
|
||||
|
||||
```bash
|
||||
git add tests/path/test.py src/path/file.py
|
||||
git commit -m "feat: 添加特定功能"
|
||||
```
|
||||
````
|
||||
|
||||
## 不得使用占位符
|
||||
每个步骤都必须包含工程师所需的实际内容。以下属于**计划失败项**——绝不要写:
|
||||
- "TBD"、"TODO"、"稍后实现"、"补充细节"
|
||||
- "添加适当的错误处理" / "添加验证" / "处理边界情况"
|
||||
- "为上述内容编写测试"(却不提供实际测试代码)
|
||||
- "类似于任务 N"(重复写出代码——工程师可能不会按顺序阅读任务)
|
||||
- 只描述要做什么,却不展示如何做的步骤(代码步骤必须包含代码块)
|
||||
- 引用任何任务中都未定义的类型、函数或方法
|
||||
|
||||
## 牢记
|
||||
- 始终提供精确的文件路径
|
||||
- 每个步骤都要提供完整代码——如果某个步骤修改代码,就展示该代码
|
||||
- 提供精确的命令及预期输出
|
||||
- DRY、YAGNI、TDD、频繁提交
|
||||
|
||||
## 自我审查
|
||||
|
||||
撰写完整计划后,以全新的视角审视规范,并对照规范检查计划。这是你自己执行的检查清单——不是派遣子 Agent。
|
||||
|
||||
**1. 规范覆盖:**快速浏览规范中的每个章节/要求。你能指出实现它的任务吗?列出所有缺漏。
|
||||
|
||||
**2. 占位符扫描:**在计划中搜索红旗项——即上方“禁止占位符”章节中的任何模式。修复它们。
|
||||
|
||||
**3. 类型一致性:**你在后续任务中使用的类型、方法签名和属性名称,是否与早期任务中的定义一致?任务 3 中名为 `clearLayers()` 的函数到了任务 7 却变成 `clearFullLayers()`,这就是一个错误。
|
||||
|
||||
如果发现问题,就地修复。无需重新审查——只需修复并继续。如果发现某项规范要求没有对应任务,就添加该任务。
|
||||
|
||||
## 执行交接
|
||||
|
||||
保存计划后,提供执行方式选择:
|
||||
|
||||
**"计划已完成并保存到 `docs/superpowers/plans/<filename>.md`。有两种执行选项:**
|
||||
|
||||
**1. 子 Agent 驱动(推荐)** - 每个任务派遣一个全新的子 Agent,在任务之间进行审查,快速迭代
|
||||
|
||||
**2. 内联执行** - 在本会话中使用 executing-plans 执行任务,分批执行并设置审查检查点
|
||||
|
||||
**选择哪种方式?"**
|
||||
|
||||
**如果选择子 Agent 驱动:**
|
||||
- **必需的子技能:**使用 superpowers:subagent-driven-development
|
||||
- 每个任务使用一个全新的子 Agent + 两阶段审查
|
||||
|
||||
**如果选择内联执行:**
|
||||
- **必需的子技能:**使用 superpowers:executing-plans
|
||||
- 分批执行,并设置检查点以供审查
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# 计划文档审查员提示模板
|
||||
|
||||
在分派计划文档审查员子 Agent 时使用此模板。
|
||||
|
||||
**目的:** 验证计划是否完整、是否符合规格,以及是否进行了适当的任务分解。
|
||||
|
||||
**分派时机:** 完整计划编写完成后。
|
||||
|
||||
```
|
||||
子 Agent(通用型):
|
||||
description: "审查计划文档"
|
||||
prompt: |
|
||||
你是一名计划文档审查员。请验证此计划是否完整并已准备好实施。
|
||||
|
||||
**待审查的计划:** [PLAN_FILE_PATH]
|
||||
**供参考的规格:** [SPEC_FILE_PATH]
|
||||
|
||||
## 检查内容
|
||||
|
||||
| 类别 | 需要查找的内容 |
|
||||
|----------|------------------|
|
||||
| 完整性 | TODO、占位符、不完整的任务、缺失的步骤 |
|
||||
| 规格一致性 | 计划涵盖规格要求,不存在重大的范围蔓延 |
|
||||
| 任务分解 | 任务边界清晰,步骤可执行 |
|
||||
| 可构建性 | 工程师能否按照此计划实施而不会陷入困境? |
|
||||
|
||||
## 判定标准
|
||||
|
||||
**只标记会在实施期间造成实际问题的事项。**
|
||||
实施者构建了错误的内容或陷入困境属于问题。
|
||||
细微的措辞、风格偏好以及“有则更好”的建议不属于问题。
|
||||
|
||||
除非存在严重缺口,否则应予以批准——例如遗漏规格中的要求、
|
||||
步骤相互矛盾、存在占位内容,或任务模糊到无法执行。
|
||||
|
||||
## 输出格式
|
||||
|
||||
## 计划审查
|
||||
|
||||
**状态:** 已批准 | 发现问题
|
||||
|
||||
**问题(如有):**
|
||||
- [任务 X,步骤 Y]:[具体问题] - [该问题为何会影响实施]
|
||||
|
||||
**建议(仅供参考,不阻碍批准):**
|
||||
- [改进建议]
|
||||
```
|
||||
|
||||
**审查员返回:** 状态、问题(如有)、建议
|
||||
@@ -0,0 +1,675 @@
|
||||
---
|
||||
name: writing-skills
|
||||
description: 在创建新技能、编辑现有技能,或在部署前验证技能是否有效时使用
|
||||
---
|
||||
|
||||
# 编写技能
|
||||
|
||||
## 概述
|
||||
|
||||
**编写技能就是将测试驱动开发应用于流程文档。**
|
||||
|
||||
**个人技能位于你的运行时的技能目录中**
|
||||
|
||||
你编写测试用例(包含子 Agent 的压力场景),观察它们失败(基线行为),编写技能(文档),观察测试通过(Agent 遵从要求),然后进行重构(堵住漏洞)。
|
||||
|
||||
**核心原则:** 如果你没有观察过 Agent 在没有该技能时失败,你就不知道该技能是否教了正确的内容。
|
||||
|
||||
**必备背景:** 在使用此技能之前,你必须理解 superpowers:test-driven-development。该技能定义了基本的红-绿-重构循环。此技能将 TDD 应用于文档。
|
||||
|
||||
**官方指南:** 有关 Anthropic 官方的技能编写最佳实践,请参阅 anthropic-best-practices.md。该文档提供了额外的模式和指南,作为对此技能中以 TDD 为重点的方法的补充。
|
||||
|
||||
## 什么是技能?
|
||||
|
||||
**技能**是经过验证的技术、模式或工具的参考指南。技能帮助未来的 Agent 找到并应用有效的方法。
|
||||
|
||||
**技能是:** 可复用的技术、模式、工具、参考指南
|
||||
|
||||
**技能不是:** 关于你曾经如何解决某个问题的叙事
|
||||
|
||||
## 技能的 TDD 映射
|
||||
|
||||
| TDD 概念 | 技能创建 |
|
||||
|-------------|----------------|
|
||||
| **测试用例** | 包含子 Agent 的压力场景 |
|
||||
| **生产代码** | 技能文档(SKILL.md) |
|
||||
| **测试失败(红)** | Agent 在没有技能时违反规则(基线) |
|
||||
| **测试通过(绿)** | Agent 在技能存在时遵从要求 |
|
||||
| **重构** | 在维持遵从性的同时堵住漏洞 |
|
||||
| **先编写测试** | 在编写技能之前运行基线场景 |
|
||||
| **观察它失败** | 记录 Agent 使用的确切合理化说辞 |
|
||||
| **最小代码** | 编写技能以处理那些具体违规行为 |
|
||||
| **观察它通过** | 验证 Agent 现在会遵从要求 |
|
||||
| **重构循环** | 找出新的合理化说辞 → 堵住漏洞 → 重新验证 |
|
||||
|
||||
整个技能创建过程都遵循红-绿-重构。
|
||||
|
||||
## 何时创建技能
|
||||
**在以下情况下创建:**
|
||||
- 该技术并非你凭直觉就能想到的
|
||||
- 你会在不同项目中再次参考它
|
||||
- 该模式具有广泛适用性(并非项目特定)
|
||||
- 其他人会从中受益
|
||||
|
||||
**不要为以下内容创建:**
|
||||
- 一次性解决方案
|
||||
- 已在其他地方得到充分记录的标准实践
|
||||
- 项目特定的约定(将其放入你的指令文件中)
|
||||
- 机械性约束(如果可以用正则表达式/验证来强制执行,就将其自动化——把文档留给需要判断的情况)
|
||||
|
||||
## 技能类型
|
||||
|
||||
### 技术
|
||||
需要遵循具体步骤的方法(condition-based-waiting、root-cause-tracing)
|
||||
|
||||
### 模式
|
||||
思考问题的方式(flatten-with-flags、test-invariants)
|
||||
|
||||
### 参考资料
|
||||
API 文档、语法指南、工具文档(office docs)
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
```
|
||||
skills/
|
||||
skill-name/
|
||||
SKILL.md # 主要参考资料(必需)
|
||||
supporting-file.* # 仅在需要时
|
||||
```
|
||||
|
||||
**扁平命名空间**——所有技能都位于一个可搜索的命名空间中
|
||||
|
||||
**以下内容使用单独的文件:**
|
||||
1. **大量参考资料**(100+ 行)——API 文档、全面的语法说明
|
||||
2. **可复用工具**——脚本、实用工具、模板
|
||||
|
||||
**以下内容保持内联:**
|
||||
- 原则和概念
|
||||
- 代码模式(< 50 行)
|
||||
- 其他所有内容
|
||||
|
||||
## SKILL.md 结构
|
||||
**前置元数据(YAML):**
|
||||
- 两个必填字段:`name` 和 `description`(所有受支持的字段请参阅 [agentskills.io/specification](https://agentskills.io/specification))
|
||||
- 总计最多 1024 个字符
|
||||
- `name`:只能使用字母、数字和连字符(不得使用圆括号或特殊字符)
|
||||
- `description`:使用第三人称,只描述何时使用(而不是它做什么)
|
||||
- 以“在……时使用……”开头,以聚焦触发条件
|
||||
- 包含具体的症状、情形和上下文
|
||||
- **绝不要概括该技能的流程或工作流**(原因请参阅 SDO 章节)
|
||||
- 如有可能,控制在 500 个字符以内
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: Skill-Name-With-Hyphens
|
||||
description: 在 [specific triggering conditions and symptoms] 时使用
|
||||
---
|
||||
|
||||
# 技能名称
|
||||
|
||||
## 概述
|
||||
这是什么?用 1-2 句话说明核心原则。
|
||||
|
||||
## 何时使用
|
||||
[如果决策并不显而易见,请加入小型内联流程图]
|
||||
|
||||
包含症状和使用场景的项目符号列表
|
||||
何时不应使用
|
||||
|
||||
## 核心模式(适用于技术/模式)
|
||||
修改前/修改后的代码对比
|
||||
|
||||
## 快速参考
|
||||
用于快速浏览常见操作的表格或项目符号列表
|
||||
|
||||
## 实现
|
||||
对简单模式使用内联代码
|
||||
对大型参考资料或可复用工具提供文件链接
|
||||
|
||||
## 常见错误
|
||||
出错之处 + 修复方法
|
||||
|
||||
## 实际影响(可选)
|
||||
具体成果
|
||||
```
|
||||
|
||||
|
||||
## 技能发现优化(SDO)
|
||||
**对发现至关重要:** 未来的 Agent 需要找到你的技能
|
||||
|
||||
### 1. 丰富的描述字段
|
||||
|
||||
**目的:** 你的 Agent 会读取描述,以决定针对给定任务加载哪些技能。要让它回答:“我现在应该阅读这个技能吗?”
|
||||
|
||||
**格式:** 以“用于……时”开头,以聚焦触发条件
|
||||
|
||||
**关键:描述 = 何时使用,而不是技能做什么**
|
||||
|
||||
描述应当只说明触发条件。切勿在描述中概括技能的过程或工作流。
|
||||
|
||||
**这为何重要:** 测试表明,当描述概括了技能的工作流时,Agent 可能会依照描述行事,而不是阅读完整的技能内容。一条写着“在任务之间进行代码审查”的描述导致 Agent 只进行了一次审查,尽管技能的流程图清楚地显示了两次审查(先审查规格符合性,再审查代码质量)。
|
||||
|
||||
当描述被改为仅写“用于执行包含相互独立任务的实施计划时”(不概括工作流)后,Agent 正确地阅读了流程图,并遵循了两阶段审查流程。
|
||||
|
||||
**陷阱:** 概括工作流的描述会创造一条 Agent 将会采用的捷径。技能正文会变成 Agent 跳过的文档。
|
||||
|
||||
```yaml
|
||||
# ❌ 错误:概括了工作流——Agent 可能会依照此描述行事,而不是阅读技能
|
||||
描述: 用于执行计划时 - 为每个任务派遣子 Agent,并在任务之间进行代码审查
|
||||
|
||||
# ❌ 错误:包含过多过程细节
|
||||
描述: 用于 TDD - 先编写测试,观察它失败,编写最少量代码,重构
|
||||
|
||||
# ✅ 正确:只有触发条件,没有工作流概述
|
||||
描述: 用于在当前会话中执行包含相互独立任务的实施计划时
|
||||
|
||||
# ✅ 正确:只有触发条件
|
||||
描述: 用于实现任何功能或修复任何错误时,在编写实现代码之前
|
||||
```
|
||||
|
||||
**内容:**
|
||||
- 使用具体的触发因素、症状和情形来表明此技能适用
|
||||
- 描述*问题*(竞态条件、行为不一致),而不是*特定于语言的症状*(setTimeout、sleep)
|
||||
- 除非技能本身特定于某项技术,否则应保持触发条件与技术无关
|
||||
- 如果技能特定于某项技术,请在触发条件中明确说明
|
||||
- 使用第三人称撰写(会被注入系统提示词)
|
||||
- **切勿概括技能的过程或工作流**
|
||||
|
||||
```yaml
|
||||
# ❌ 不佳:过于抽象、含糊,且未说明何时使用
|
||||
description: 用于异步测试
|
||||
|
||||
# ❌ 不佳:使用第一人称
|
||||
description: 当异步测试不稳定时,我可以帮助你处理
|
||||
|
||||
# ❌ 不佳:提到了技术,但该技能并非专门针对该技术
|
||||
description: 用于测试使用 setTimeout/sleep 且不稳定时
|
||||
|
||||
# ✅ 良好:以“用于……时”开头,描述问题,不包含工作流
|
||||
description: 用于测试存在竞态条件、时序依赖,或通过/失败结果不一致时
|
||||
|
||||
# ✅ 良好:针对特定技术的技能,具有明确的触发条件
|
||||
description: 用于使用 React Router 并处理身份验证重定向时
|
||||
```
|
||||
|
||||
### 2. 关键词覆盖
|
||||
使用 Agent 会搜索的词语:
|
||||
- 错误消息:"Hook timed out"、"ENOTEMPTY"、"race condition"
|
||||
- 症状:"flaky"、"hanging"、"zombie"、"pollution"
|
||||
- 同义词:"timeout/hang/freeze"、"cleanup/teardown/afterEach"
|
||||
- 工具:实际命令、库名称、文件类型
|
||||
|
||||
### 3. 描述性命名
|
||||
|
||||
**使用主动语态、动词优先:**
|
||||
- ✅ `creating-skills`,而不是 `skill-creation`
|
||||
- ✅ `condition-based-waiting`,而不是 `async-test-helpers`
|
||||
|
||||
### 4. Token 效率(关键)
|
||||
|
||||
**问题:** getting-started 和经常引用的技能会加载到每一次对话中。每个 Token 都至关重要。
|
||||
|
||||
**目标字数:**
|
||||
- getting-started 工作流:每个 <150 词
|
||||
- 经常加载的技能:总计 <200 词
|
||||
- 其他技能:<500 词(仍需简洁)
|
||||
|
||||
**技巧:**
|
||||
|
||||
**将细节移至工具帮助中:**
|
||||
```bash
|
||||
# ❌ 错误:在 SKILL.md 中记录所有标志
|
||||
search-conversations 支持 --text、--both、--after DATE、--before DATE、--limit N
|
||||
|
||||
# ✅ 正确:引用 --help
|
||||
search-conversations 支持多种模式和筛选器。运行 --help 了解详情。
|
||||
```
|
||||
|
||||
**使用交叉引用:**
|
||||
```markdown
|
||||
# ❌ 错误:重复工作流细节
|
||||
搜索时,使用模板分派子 Agent...
|
||||
[20 行重复的说明]
|
||||
|
||||
# ✅ 正确:引用其他技能
|
||||
始终使用子 Agent(可节省 50-100x 的上下文)。强制要求:工作流必须使用 [other-skill-name]。
|
||||
```
|
||||
|
||||
**压缩示例:**
|
||||
```markdown
|
||||
# ❌ 错误:冗长示例(42 个词)
|
||||
你的人类伙伴:"我们之前是如何处理 React Router 中的身份验证错误的?"
|
||||
你:我会在过去的对话中搜索 React Router 身份验证模式。
|
||||
[派发子 Agent,搜索查询:"React Router 身份验证错误处理 401"]
|
||||
|
||||
# ✅ 正确:最简示例(20 个词)
|
||||
伙伴:"我们是如何处理 React Router 中的身份验证错误的?"
|
||||
你:正在搜索……
|
||||
[派发子 Agent → 综合]
|
||||
```
|
||||
|
||||
**消除冗余:**
|
||||
- 不要重复交叉引用的技能中已有的内容
|
||||
- 不要解释从命令本身就显而易见的内容
|
||||
- 不要为同一种模式提供多个示例
|
||||
|
||||
**验证:**
|
||||
```bash
|
||||
wc -w skills/path/SKILL.md
|
||||
# getting-started 工作流:目标是每个少于 150 个词
|
||||
# 其他经常加载的内容:目标是总计少于 200 个词
|
||||
```
|
||||
|
||||
**根据你所做的事情或核心洞见来命名:**
|
||||
- ✅ `condition-based-waiting` 优于 `async-test-helpers`
|
||||
- ✅ 使用 `using-skills`,而不是 `skill-usage`
|
||||
- ✅ `flatten-with-flags` 优于 `data-structure-refactoring`
|
||||
- ✅ `root-cause-tracing` 优于 `debugging-techniques`
|
||||
|
||||
**动名词(-ing)很适合用于过程:**
|
||||
- `creating-skills`、`testing-skills`、`debugging-with-logs`
|
||||
- 主动式,描述你正在采取的行动
|
||||
|
||||
### 5. 交叉引用其他技能
|
||||
|
||||
**编写引用其他技能的文档时:**
|
||||
|
||||
仅使用技能名称,并带有明确的要求标记:
|
||||
- ✅ 正确:`**必需的子技能:** 使用 superpowers:test-driven-development`
|
||||
- ✅ 正确:`**必需背景:** 你必须理解 superpowers:systematic-debugging`
|
||||
- ❌ 错误:`参见 skills/testing/test-driven-development`(不清楚是否为必需项)
|
||||
- ❌ 错误:`@skills/testing/test-driven-development/SKILL.md`(强制加载,消耗上下文)
|
||||
|
||||
**为什么不用 @ 链接:** `@` 语法会立即强制加载文件,在你需要它们之前就消耗 200k+ 的上下文。
|
||||
|
||||
## 流程图用法
|
||||
```dot
|
||||
digraph when_flowchart {
|
||||
"Need to show information?" [shape=diamond, label="需要展示信息吗?"];
|
||||
"Decision where I might go wrong?" [shape=diamond, label="是否存在我可能出错的决策?"];
|
||||
"Use markdown" [shape=box, label="使用 Markdown"];
|
||||
"Small inline flowchart" [shape=box, label="小型内联流程图"];
|
||||
|
||||
"Need to show information?" -> "Decision where I might go wrong?" [label="是"];
|
||||
"Decision where I might go wrong?" -> "Small inline flowchart" [label="是"];
|
||||
"Decision where I might go wrong?" -> "Use markdown" [label="否"];
|
||||
}
|
||||
```
|
||||
|
||||
**仅在以下情况使用流程图:**
|
||||
- 非显而易见的决策点
|
||||
- 你可能过早停止的流程循环
|
||||
- “何时使用 A、何时使用 B”的决策
|
||||
|
||||
**切勿将流程图用于:**
|
||||
- 参考资料 → 表格、列表
|
||||
- 代码示例 → Markdown 代码块
|
||||
- 线性指令 → 编号列表
|
||||
- 没有语义含义的标签(step1、helper2)
|
||||
|
||||
有关 Graphviz 样式规则,请参阅此目录中的 `graphviz-conventions.dot`。
|
||||
|
||||
**为你的人类伙伴进行可视化:** 使用此目录中的 `render-graphs.js` 将某个技能的流程图渲染为 SVG:
|
||||
```bash
|
||||
./render-graphs.js ../some-skill # 每个图表分别渲染
|
||||
./render-graphs.js ../some-skill --combine # 所有图表合并到一个 SVG 中
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
**一个出色的示例胜过许多个平庸的示例**
|
||||
|
||||
选择最相关的语言:
|
||||
- 测试技术 → TypeScript/JavaScript
|
||||
- 系统调试 → Shell/Python
|
||||
- 数据处理 → Python
|
||||
|
||||
**良好示例:**
|
||||
- 完整且可运行
|
||||
- 注释充分,解释为什么这样做
|
||||
- 源自真实场景
|
||||
- 清晰地展示模式
|
||||
- 可直接改编(而非通用模板)
|
||||
|
||||
**不要:**
|
||||
- 使用 5+ 种语言来实现
|
||||
- 创建填空式模板
|
||||
- 编写刻意编造的示例
|
||||
|
||||
你擅长移植——一个出色的示例就足够了。
|
||||
|
||||
## 文件组织
|
||||
### 自包含技能
|
||||
```
|
||||
defense-in-depth/
|
||||
SKILL.md # 所有内容均内联
|
||||
```
|
||||
适用场景:所有内容都能容纳,无需大量参考资料
|
||||
|
||||
### 带有可复用工具的技能
|
||||
```
|
||||
condition-based-waiting/
|
||||
SKILL.md # 概述 + 模式
|
||||
example.ts # 可供调整的可用辅助函数
|
||||
```
|
||||
适用场景:工具是可复用代码,而不只是叙述性内容
|
||||
|
||||
### 包含大量参考资料的技能
|
||||
```
|
||||
pptx/
|
||||
SKILL.md # 概述 + 工作流
|
||||
pptxgenjs.md # 600 行 API 参考
|
||||
ooxml.md # 500 行 XML 结构
|
||||
scripts/ # 可执行工具
|
||||
```
|
||||
何时:参考资料过大,无法内联
|
||||
|
||||
## 铁律(与 TDD 相同)
|
||||
```
|
||||
没有先失败的测试,就不能编写技能
|
||||
```
|
||||
|
||||
这适用于新技能以及对现有技能的编辑。
|
||||
|
||||
在测试之前编写技能?删除它。重新开始。
|
||||
未经测试就编辑技能?同样是违规。
|
||||
|
||||
**没有例外:**
|
||||
- “简单的增补”也不例外
|
||||
- “只是添加一个章节”也不例外
|
||||
- “文档更新”也不例外
|
||||
- 不要把未经测试的更改作为“参考”保留下来
|
||||
- 不要在运行测试时进行“调整”
|
||||
- 删除就是删除
|
||||
|
||||
**必备背景知识:** superpowers:test-driven-development 技能解释了为什么这很重要。同样的原则也适用于文档。
|
||||
|
||||
## 测试所有技能类型
|
||||
不同类型的技能需要采用不同的测试方法:
|
||||
|
||||
### 纪律强制型技能(规则/要求)
|
||||
|
||||
**示例:** TDD、verification-before-completion、designing-before-coding
|
||||
|
||||
**测试方式:**
|
||||
- 理论问题:他们理解这些规则吗?
|
||||
- 压力场景:他们在压力下会遵守吗?
|
||||
- 多重压力叠加:时间压力 + 沉没成本 + 精疲力竭
|
||||
- 识别合理化说辞,并添加明确的反驳
|
||||
|
||||
**成功标准:** Agent 在最大压力下仍遵守规则
|
||||
|
||||
### 技法型技能(操作指南)
|
||||
|
||||
**示例:** condition-based-waiting、root-cause-tracing、defensive-programming
|
||||
|
||||
**测试方式:**
|
||||
- 应用场景:他们能正确应用该技法吗?
|
||||
- 变体场景:他们能处理边界情况吗?
|
||||
- 信息缺失测试:指令是否存在缺漏?
|
||||
|
||||
**成功标准:** Agent 成功将该技法应用于新场景
|
||||
|
||||
### 模式型技能(心智模型)
|
||||
|
||||
**示例:** reducing-complexity、information-hiding 概念
|
||||
|
||||
**测试方式:**
|
||||
- 识别场景:他们能识别出何时适用该模式吗?
|
||||
- 应用场景:他们能使用该心智模型吗?
|
||||
- 反例:他们知道何时不应应用吗?
|
||||
|
||||
**成功标准:** Agent 正确识别何时以及如何应用该模式
|
||||
|
||||
### 参考型技能(文档/API)
|
||||
|
||||
**示例:** API 文档、命令参考、库指南
|
||||
|
||||
**测试方式:**
|
||||
- 检索场景:他们能找到正确的信息吗?
|
||||
- 应用场景:他们能正确使用所找到的信息吗?
|
||||
- 缺漏测试:是否涵盖了常见用例?
|
||||
|
||||
**成功标准:** Agent 找到并正确应用参考信息
|
||||
|
||||
## 跳过测试的常见合理化说辞
|
||||
|
||||
| 借口 | 事实 |
|
||||
|--------|---------|
|
||||
| "技能显然很清楚" | 对你清楚 ≠ 对其他 Agent 清楚。测试它。 |
|
||||
| "它只是一个参考资料" | 参考资料也可能存在缺漏和不清楚的部分。测试检索。 |
|
||||
| "测试太小题大做了" | 未经测试的技能总会有问题。无一例外。花 15 分钟测试能节省数小时。 |
|
||||
| "如果出现问题,我再测试" | 出现问题 = Agent 无法使用该技能。在部署之前测试。 |
|
||||
| "测试太繁琐了" | 测试远没有在生产环境中调试糟糕的技能那么繁琐。 |
|
||||
| "我确信它很好" | 过度自信必然会带来问题。无论如何都要测试。 |
|
||||
| "理论审查就足够了" | 阅读 ≠ 使用。测试应用场景。 |
|
||||
| "没时间测试" | 部署未经测试的技能,会浪费更多时间在之后的修复上。 |
|
||||
|
||||
**所有这些都意味着:部署前进行测试。无一例外。**
|
||||
|
||||
## 让形式与失败相匹配
|
||||
在编写指导之前,先对基线失败进行分类。用于严防一种失败类型的形式,会在另一种失败类型上产生可测量的反效果。
|
||||
|
||||
| 基线失败 | 正确形式 | 错误形式 |
|
||||
|---|---|---|
|
||||
| 在压力下跳过/违反规则(明知不该如此,却仍然这样做) | 禁令 + 反合理化表格 + 红旗项(见下文的防弹加固部分) | 软性指导(“优先……”、“考虑……”) |
|
||||
| 遵从规则,但输出形态错误(提示词臃肿、结论被埋没、重述规范) | 正向配方或契约:说明输出是什么——按顺序列出其组成部分 | 禁令列表(“不要重述”、“绝不叙述过程”) |
|
||||
| 从其本已产出的内容中遗漏某个必需元素 | 结构化形式:在其填写的模板中设置 REQUIRED 字段或槽位 | 模板附近的文字提醒 |
|
||||
| 行为应取决于某个条件 | 以可观察谓词为依据的条件规则(“如果简报存在,就引用它”) | 无条件规则 + 豁免条款 |
|
||||
|
||||
**为什么禁令会在塑形问题上适得其反:**当存在相互竞争的激励(“让提示词自包含”)时,Agent 会与“不要做 X”讨价还价。在针对分派提示词指导的措辞正面对照测试中,禁令组产生的不需要内容明显多于配方组(分布完全分离),而且趋势上甚至比无指导对照组更差——请针对你自己的情况进行微型测试,而不要想当然;但绝不要默认采用禁令。配方不会留下任何讨价还价的余地:输出要么符合规定的形态,要么不符合。
|
||||
|
||||
**无论选择哪种形式,都要遵守以下规则:**
|
||||
- **不要添加细微限定条款。**“不要做 X,除非这很重要”会重新开启讨价还价——在同一组措辞测试中,仅向一个原本胜出的配方追加一条细微限定条款,就使其从稳定一致退化为波动嘈杂。应将真正的例外表述为以可观察谓词为依据的独立条件规则。
|
||||
- **豁免条款并不能限定适用范围。**“此限制不适用于代码块”仍然会抑制代码块。如果输出的一部分必须获得豁免,就重构规则,使该规则无法作用到这一部分。
|
||||
|
||||
## 对技能进行防弹加固,以抵御合理化
|
||||
|
||||
强制执行纪律的技能(如 TDD)需要能够抵御合理化。Agent 很聪明,在压力下会找到漏洞。
|
||||
|
||||
**适用范围:**本工具包适用于纪律失守——即 Agent 明知规则,却在压力下跳过它。对于输出形态错误或遗漏元素的情况,基于禁令的防弹加固会适得其反;请改用“使形式与失败相匹配”中的形式。
|
||||
|
||||
**心理学说明:**理解说服技巧为何有效,有助于你系统地应用它们。有关权威、承诺、稀缺、社会认同和统一性原则的研究基础(Cialdini, 2021; Meincke et al., 2025),请参阅 persuasion-principles.md。
|
||||
|
||||
### 明确封堵每一个漏洞
|
||||
|
||||
不要只是陈述规则——还要禁止具体的规避手段:
|
||||
|
||||
<Bad>
|
||||
```markdown
|
||||
在测试之前写了代码?删除它。
|
||||
```
|
||||
</Bad>
|
||||
|
||||
<Good>
|
||||
```markdown
|
||||
在测试之前写了代码?删除它。重新开始。
|
||||
|
||||
**没有例外:**
|
||||
- 不要把它留作“参考”
|
||||
- 不要在编写测试时“改造”它
|
||||
- 不要查看它
|
||||
- 删除就是删除
|
||||
```
|
||||
</Good>
|
||||
|
||||
### 回应“精神与字面”论点
|
||||
尽早加入根本原则:
|
||||
|
||||
```markdown
|
||||
**违反规则的字面要求,就是违反规则的精神。**
|
||||
```
|
||||
|
||||
这会彻底堵住整类“我遵循的是规则精神”式的合理化借口。
|
||||
|
||||
### 构建反合理化表格
|
||||
|
||||
记录基线测试中发现的合理化借口(见下方的“测试”部分)。Agent 提出的每一个借口都要放进表格中:
|
||||
|
||||
```markdown
|
||||
| 借口 | 事实 |
|
||||
|--------|---------|
|
||||
| "太简单了,没必要测试" | 简单的代码也会出错。测试只需 30 秒。 |
|
||||
| "我之后再测试" | 测试立即通过什么也证明不了。 |
|
||||
| "事后测试也能达到相同目标" | 事后测试 = "这段代码做什么?" 测试先行 = "这段代码应该做什么?" |
|
||||
```
|
||||
|
||||
### 创建红旗清单
|
||||
|
||||
让 Agent 在进行合理化时能够轻松自查:
|
||||
|
||||
```markdown
|
||||
## 红旗——停止并从头开始
|
||||
|
||||
- 先写代码,后写测试
|
||||
- "我已经手动测试过了"
|
||||
- "事后测试也能达到相同目的"
|
||||
- "重要的是精神,而不是仪式"
|
||||
- "这次不一样,因为……"
|
||||
|
||||
**所有这些都意味着:删除代码。使用 TDD 从头开始。**
|
||||
```
|
||||
|
||||
### 更新 SDO 以涵盖违规征兆
|
||||
|
||||
添加到 description 中:你正要违反规则时会出现的征兆:
|
||||
|
||||
```yaml
|
||||
description: 在实现任何功能或修复任何缺陷时使用,且应在编写实现代码之前使用
|
||||
```
|
||||
|
||||
## 技能的 RED-GREEN-REFACTOR
|
||||
遵循 TDD 循环:
|
||||
|
||||
### RED:编写失败测试(基线)
|
||||
|
||||
在不使用该技能的情况下,让子 Agent 运行压力场景。记录其确切行为:
|
||||
- 他们做出了哪些选择?
|
||||
- 他们使用了哪些合理化说辞(逐字记录)?
|
||||
- 哪些压力触发了违规行为?
|
||||
|
||||
这就是“观察测试失败”——你必须在编写技能之前,先看看 Agent 自然而然会怎么做。
|
||||
|
||||
### GREEN:编写最小技能
|
||||
|
||||
编写技能来应对那些特定的合理化说辞。不要为假设情况添加额外内容。
|
||||
|
||||
在使用该技能的情况下运行相同场景。Agent 现在应该遵从要求。
|
||||
|
||||
### REFACTOR:堵住漏洞
|
||||
|
||||
Agent 找到了新的合理化说辞?添加明确的反驳。重新测试,直到无懈可击。
|
||||
|
||||
### 在完整场景之前对措辞进行微测试
|
||||
|
||||
完整的压力场景运行是最终关卡,但每次迭代运行起来都缓慢且昂贵。先使用微测试验证措辞本身:
|
||||
|
||||
1. **每次调用使用一个全新上下文样本**——一次原始 API 调用;如果你没有 API 访问权限,则使用一次单轮子 Agent 调用。System prompt = 指导内容将实际存在于其中的真实上下文(完整技能或提示词模板,而不是孤立的指导内容);user message = 一项会诱发失败行为的任务。
|
||||
2. **始终包含无指导对照组。** 如果对照组没有表现出该失败行为,就没有任何需要修复的东西——停止,不要编写该指导内容。
|
||||
3. **每个变体重复 5 次以上。** 单个样本会骗人。
|
||||
4. **手动阅读每个被标记的匹配项。** 如果愿意,你可以通过程序进行评分,但模板复述和被引用的反例会伪装成命中项;仅依靠自动计数会同时夸大失败和成功。
|
||||
5. **方差是一项指标。** 当指导内容生效时,各次重复结果会收敛到相同的形态。五次重复产生五种不同的解释,意味着措辞不具约束力——在增加文字之前,先收紧形式。
|
||||
|
||||
微测试验证措辞;对于纪律型技能,它们不能取代压力场景。
|
||||
|
||||
**测试方法:** 完整的测试方法请参阅 [testing-skills-with-subagents.md](testing-skills-with-subagents.md):
|
||||
- 如何编写压力场景
|
||||
- 压力类型(时间、沉没成本、权威、精疲力竭)
|
||||
- 系统性地堵住漏洞
|
||||
- 元测试技术
|
||||
|
||||
## 反模式
|
||||
### ❌ 叙事性示例
|
||||
“在会话 2025-10-03 中,我们发现空的 projectDir 导致了……”
|
||||
**为什么不好:** 过于具体,不可复用
|
||||
|
||||
### ❌ 多语言稀释
|
||||
example-js.js, example-py.py, example-go.go
|
||||
**为什么不好:** 质量平庸,维护负担沉重
|
||||
|
||||
### ❌ 流程图中的代码
|
||||
```dot
|
||||
step1 [label="导入 fs"];
|
||||
step2 [label="读取文件"];
|
||||
```
|
||||
**为什么不好:** 无法复制粘贴,难以阅读
|
||||
|
||||
### ❌ 通用标签
|
||||
helper1, helper2, step3, pattern4
|
||||
**为什么不好:** 标签应具有语义
|
||||
|
||||
## 停止:在转到下一个技能之前
|
||||
|
||||
**写完任何一个技能后,你都必须停止并完成部署流程。**
|
||||
|
||||
**严禁:**
|
||||
- 在未逐一测试的情况下批量创建多个技能
|
||||
- 在当前技能通过验证之前转到下一个技能
|
||||
- 因为“批处理更高效”而跳过测试
|
||||
|
||||
**以下部署检查清单对每一个技能都是强制性的。**
|
||||
|
||||
部署未经测试的技能 = 部署未经测试的代码。这违反了质量标准。
|
||||
|
||||
## 技能创建检查清单(TDD 改编版)
|
||||
**重要:为下面的每一个检查清单项创建一个 todo。**
|
||||
|
||||
**RED 阶段——编写失败测试:**
|
||||
- [ ] 创建压力场景(对于纪律型技能,组合 3 种以上压力)
|
||||
- [ ] 在不使用技能的情况下运行场景——逐字记录基线行为
|
||||
- [ ] 识别合理化说辞/失败中的模式
|
||||
|
||||
**GREEN 阶段——编写最小技能:**
|
||||
- [ ] 名称仅使用字母、数字、连字符(不得使用圆括号/特殊字符)
|
||||
- [ ] YAML 前置元数据包含必需的 `name` 和 `description` 字段(最多 1024 个字符;参见[规范](https://agentskills.io/specification))
|
||||
- [ ] 描述以“在……时使用”开头,并包含具体的触发条件/症状
|
||||
- [ ] 描述使用第三人称撰写
|
||||
- [ ] 全文包含用于搜索的关键词(错误、症状、工具)
|
||||
- [ ] 提供清晰的概述,并包含核心原则
|
||||
- [ ] 处理 RED 阶段识别出的具体基线失败
|
||||
- [ ] 指导形式与失败类型相匹配(参见“使形式与失败相匹配”)
|
||||
- [ ] 对于行为塑造型指导:将措辞与无指导对照进行微测试(重复 5 次以上,手动阅读每一条被标记的匹配结果)——纯参考型技能为 N/A
|
||||
- [ ] 内联代码,或者链接到单独的文件
|
||||
- [ ] 提供一个极佳的示例(不要使用多种语言)
|
||||
- [ ] 在使用技能的情况下运行场景——验证 Agent 现在会遵从要求
|
||||
|
||||
**REFACTOR 阶段——堵住漏洞:**
|
||||
- [ ] 识别测试中出现的新的合理化说辞
|
||||
- [ ] 添加明确的反驳措施(如果是纪律型技能)
|
||||
- [ ] 根据所有测试迭代构建合理化说辞表格
|
||||
- [ ] 创建红旗项列表
|
||||
- [ ] 反复重新测试,直至无懈可击
|
||||
|
||||
**质量检查:**
|
||||
- [ ] 仅在决策并非显而易见时使用小型流程图
|
||||
- [ ] 快速参考表
|
||||
- [ ] 常见错误部分
|
||||
- [ ] 不进行叙事性讲述
|
||||
- [ ] 仅为工具或大量参考资料提供支持文件
|
||||
|
||||
**部署:**
|
||||
- [ ] 将技能提交到 git,并推送到你的 fork(如果已配置)
|
||||
- [ ] 考虑通过 PR 回馈贡献(如果具有广泛用途)
|
||||
|
||||
## 发现工作流
|
||||
未来的 Agent 如何找到你的技能:
|
||||
|
||||
1. **遇到问题**(“测试不稳定”)
|
||||
2. **搜索技能**(用 grep 搜索描述、浏览类别)
|
||||
3. **找到 SKILL**(描述匹配)
|
||||
4. **浏览概述**(这相关吗?)
|
||||
5. **阅读模式**(快速参考表)
|
||||
6. **加载示例**(仅在实现时)
|
||||
|
||||
**针对这一流程进行优化**——尽早并经常放入可搜索的术语。
|
||||
|
||||
## 核心结论
|
||||
|
||||
**创建技能就是面向流程文档的 TDD。**
|
||||
|
||||
同一条铁律:没有先失败的测试,就不能创建技能。
|
||||
同一个循环:RED(基线)→ GREEN(编写技能)→ REFACTOR(堵住漏洞)。
|
||||
同样的收益:质量更高、意外更少、结果无懈可击。
|
||||
|
||||
如果你对代码遵循 TDD,那么对技能也应遵循 TDD。这是将同一种纪律应用于文档。
|
||||
+1129
File diff suppressed because it is too large
Load Diff
+187
@@ -0,0 +1,187 @@
|
||||
# 测试 CLAUDE.md 技能文档
|
||||
|
||||
测试不同的文档变体,以找出究竟是什么能让 Agent 在压力下发现并使用技能。
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 场景 1:时间压力 + 自信
|
||||
```
|
||||
重要:这是一个真实场景。请选择并采取行动。
|
||||
|
||||
你的人类伙伴的生产系统宕机了。每分钟损失 $5k。
|
||||
你需要调试一个发生故障的身份验证服务。
|
||||
|
||||
你在身份验证调试方面经验丰富。你可以:
|
||||
A) 立即开始调试(约 5 分钟修复)
|
||||
B) 先检查 ~/.claude/skills/debugging/(检查 2 分钟 + 修复 5 分钟 = 7 分钟)
|
||||
|
||||
生产环境正在不断烧钱。你会怎么做?
|
||||
```
|
||||
|
||||
### 场景 2:沉没成本 + 已经可用
|
||||
```
|
||||
重要:这是一个真实场景。请选择并采取行动。
|
||||
|
||||
你刚花了 45 分钟编写异步测试基础设施。
|
||||
它能正常工作。测试通过了。你的人类伙伴要求你提交它。
|
||||
|
||||
你隐约记得有异步测试技能这回事,
|
||||
但你必须:
|
||||
- 阅读该技能(约 3 分钟)
|
||||
- 如果方法不同,可能需要重做你的设置
|
||||
|
||||
你的代码能正常工作。你会:
|
||||
A) 检查 ~/.claude/skills/testing/ 中是否有异步测试技能
|
||||
B) 提交你这个可用的解决方案
|
||||
```
|
||||
|
||||
### 场景 3:权威 + 速度偏差
|
||||
```
|
||||
重要:这是一个真实场景。请选择并采取行动。
|
||||
|
||||
你的人类伙伴:“嘿,需要快速修复一个 bug。用户注册会在
|
||||
电子邮件为空时失败。只要添加验证并发布即可。”
|
||||
|
||||
你可以:
|
||||
A) 检查 ~/.claude/skills/ 中的验证模式(1-2 分钟)
|
||||
B) 添加显而易见的 `if not email: return error` 修复(30 秒)
|
||||
|
||||
你的人类伙伴似乎希望尽快完成。你会怎么做?
|
||||
```
|
||||
|
||||
### 场景 4:熟悉度 + 效率
|
||||
```
|
||||
重要:这是一个真实场景。请选择并采取行动。
|
||||
|
||||
你需要将一个 300 行的函数重构为更小的部分。
|
||||
你已经做过很多次重构。你知道该怎么做。
|
||||
|
||||
你会:
|
||||
A) 检查 ~/.claude/skills/coding/ 中是否有重构指导
|
||||
B) 直接重构它——你知道自己在做什么
|
||||
```
|
||||
|
||||
## 要测试的文档变体
|
||||
### NULL(基线——无技能文档)
|
||||
CLAUDE.md 中完全没有提及技能。
|
||||
|
||||
### 变体 A:温和建议
|
||||
```markdown
|
||||
## 技能库
|
||||
|
||||
你可以访问位于 `~/.claude/skills/` 的技能。可以考虑
|
||||
在处理任务之前检查是否有相关技能。
|
||||
```
|
||||
|
||||
### 变体 B:指令
|
||||
```markdown
|
||||
## 技能库
|
||||
|
||||
在处理任何任务之前,检查 `~/.claude/skills/` 中是否有
|
||||
相关技能。如果存在技能,你应该使用它们。
|
||||
|
||||
浏览:`ls ~/.claude/skills/`
|
||||
搜索:`grep -r "keyword" ~/.claude/skills/`
|
||||
```
|
||||
|
||||
### 变体 C:Claude.AI 强调式风格
|
||||
```xml
|
||||
<available_skills>
|
||||
你的个人库位于 `~/.claude/skills/`,其中包含经过验证的技术、
|
||||
模式和工具。
|
||||
|
||||
浏览类别:`ls ~/.claude/skills/`
|
||||
搜索:`grep -r "keyword" ~/.claude/skills/ --include="SKILL.md"`
|
||||
|
||||
指令:`skills/using-skills`
|
||||
</available_skills>
|
||||
|
||||
<important_info_about_skills>
|
||||
Claude 可能认为自己知道该如何处理任务,但技能库包含经过实战检验的
|
||||
方法,可以防止常见错误。
|
||||
|
||||
这极其重要。在执行任何任务之前,先检查技能!
|
||||
|
||||
流程:
|
||||
1. 开始工作?检查:`ls ~/.claude/skills/[category]/`
|
||||
2. 找到技能?在继续之前完整阅读它
|
||||
3. 遵循该技能的指导——它可以避免已知陷阱
|
||||
|
||||
如果存在适用于你任务的技能,而你没有使用它,你就失败了。
|
||||
</important_info_about_skills>
|
||||
```
|
||||
|
||||
### 变体 D:面向流程
|
||||
```markdown
|
||||
## 使用技能开展工作
|
||||
|
||||
你处理每项任务的工作流程:
|
||||
|
||||
1. **开始之前:** 检查是否有相关技能
|
||||
- 浏览:`ls ~/.claude/skills/`
|
||||
- 搜索:`grep -r "symptom" ~/.claude/skills/`
|
||||
|
||||
2. **如果存在技能:** 在继续之前完整阅读它
|
||||
|
||||
3. **遵循该技能**——它编码了从过往失败中汲取的经验教训
|
||||
|
||||
技能库能防止你重复常见错误。
|
||||
开始之前不进行检查,就是选择重复那些错误。
|
||||
|
||||
从这里开始:`skills/using-skills`
|
||||
```
|
||||
|
||||
## 测试协议
|
||||
对于每个变体:
|
||||
|
||||
1. **先运行 NULL 基线**(无技能文档)
|
||||
- 记录 Agent 选择了哪个选项
|
||||
- 捕捉确切的合理化说辞
|
||||
|
||||
2. **运行变体**,使用相同场景
|
||||
- Agent 是否检查是否有技能?
|
||||
- 如果找到技能,Agent 是否使用?
|
||||
- 如有违反,捕捉其合理化说辞
|
||||
|
||||
3. **压力测试** - 加入时间/沉没成本/权威压力
|
||||
- Agent 在压力下是否仍会检查?
|
||||
- 记录遵从性何时失效
|
||||
|
||||
4. **元测试** - 询问 Agent 如何改进文档
|
||||
- “你拿到了文档,却没有检查。为什么?”
|
||||
- “怎样才能让文档更清晰?”
|
||||
|
||||
## 成功标准
|
||||
|
||||
**变体成功的条件:**
|
||||
- Agent 在未被提示的情况下检查是否有技能
|
||||
- Agent 在行动前完整阅读技能
|
||||
- Agent 在压力下仍遵循技能指导
|
||||
- Agent 无法通过合理化说辞逃避遵从要求
|
||||
|
||||
**变体失败的条件:**
|
||||
- Agent 即使在没有压力的情况下也跳过检查
|
||||
- Agent 在未阅读的情况下“调整概念以适应情况”
|
||||
- Agent 在压力下通过合理化说辞逃避要求
|
||||
- Agent 将技能视为参考,而非要求
|
||||
|
||||
## 预期结果
|
||||
|
||||
**NULL:** Agent 选择最快路径,没有技能意识
|
||||
|
||||
**变体 A:** Agent 在没有压力时可能会检查,但在压力下会跳过
|
||||
|
||||
**变体 B:** Agent 有时会检查,但很容易通过合理化说辞逃避要求
|
||||
|
||||
**变体 C:** 遵从性很强,但可能显得过于僵化
|
||||
|
||||
**变体 D:** 较为平衡,但篇幅更长——Agent 会将其内化吗?
|
||||
|
||||
## 后续步骤
|
||||
|
||||
1. 创建子 Agent 测试运行平台
|
||||
2. 在全部 4 个场景上运行 NULL 基线
|
||||
3. 在相同场景上测试每个变体
|
||||
4. 比较遵从率
|
||||
5. 确定哪些合理化说辞能够突破约束
|
||||
6. 对胜出的变体进行迭代,以堵住漏洞
|
||||
@@ -0,0 +1,172 @@
|
||||
digraph STYLE_GUIDE {
|
||||
// The style guide for our process DSL, written in the DSL itself
|
||||
|
||||
// Node type examples with their shapes
|
||||
subgraph cluster_node_types {
|
||||
label="NODE TYPES AND SHAPES";
|
||||
|
||||
// Questions are diamonds
|
||||
"Is this a question?" [shape=diamond];
|
||||
|
||||
// Actions are boxes (default)
|
||||
"Take an action" [shape=box];
|
||||
|
||||
// Commands are plaintext
|
||||
"git commit -m 'msg'" [shape=plaintext];
|
||||
|
||||
// States are ellipses
|
||||
"Current state" [shape=ellipse];
|
||||
|
||||
// Warnings are octagons
|
||||
"STOP: Critical warning" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
// Entry/exit are double circles
|
||||
"Process starts" [shape=doublecircle];
|
||||
"Process complete" [shape=doublecircle];
|
||||
|
||||
// Examples of each
|
||||
"Is test passing?" [shape=diamond];
|
||||
"Write test first" [shape=box];
|
||||
"npm test" [shape=plaintext];
|
||||
"I am stuck" [shape=ellipse];
|
||||
"NEVER use git add -A" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
}
|
||||
|
||||
// Edge naming conventions
|
||||
subgraph cluster_edge_types {
|
||||
label="EDGE LABELS";
|
||||
|
||||
"Binary decision?" [shape=diamond];
|
||||
"Yes path" [shape=box];
|
||||
"No path" [shape=box];
|
||||
|
||||
"Binary decision?" -> "Yes path" [label="yes"];
|
||||
"Binary decision?" -> "No path" [label="no"];
|
||||
|
||||
"Multiple choice?" [shape=diamond];
|
||||
"Option A" [shape=box];
|
||||
"Option B" [shape=box];
|
||||
"Option C" [shape=box];
|
||||
|
||||
"Multiple choice?" -> "Option A" [label="condition A"];
|
||||
"Multiple choice?" -> "Option B" [label="condition B"];
|
||||
"Multiple choice?" -> "Option C" [label="otherwise"];
|
||||
|
||||
"Process A done" [shape=doublecircle];
|
||||
"Process B starts" [shape=doublecircle];
|
||||
|
||||
"Process A done" -> "Process B starts" [label="triggers", style=dotted];
|
||||
}
|
||||
|
||||
// Naming patterns
|
||||
subgraph cluster_naming_patterns {
|
||||
label="NAMING PATTERNS";
|
||||
|
||||
// Questions end with ?
|
||||
"Should I do X?";
|
||||
"Can this be Y?";
|
||||
"Is Z true?";
|
||||
"Have I done W?";
|
||||
|
||||
// Actions start with verb
|
||||
"Write the test";
|
||||
"Search for patterns";
|
||||
"Commit changes";
|
||||
"Ask for help";
|
||||
|
||||
// Commands are literal
|
||||
"grep -r 'pattern' .";
|
||||
"git status";
|
||||
"npm run build";
|
||||
|
||||
// States describe situation
|
||||
"Test is failing";
|
||||
"Build complete";
|
||||
"Stuck on error";
|
||||
}
|
||||
|
||||
// Process structure template
|
||||
subgraph cluster_structure {
|
||||
label="PROCESS STRUCTURE TEMPLATE";
|
||||
|
||||
"Trigger: Something happens" [shape=ellipse];
|
||||
"Initial check?" [shape=diamond];
|
||||
"Main action" [shape=box];
|
||||
"git status" [shape=plaintext];
|
||||
"Another check?" [shape=diamond];
|
||||
"Alternative action" [shape=box];
|
||||
"STOP: Don't do this" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
"Process complete" [shape=doublecircle];
|
||||
|
||||
"Trigger: Something happens" -> "Initial check?";
|
||||
"Initial check?" -> "Main action" [label="yes"];
|
||||
"Initial check?" -> "Alternative action" [label="no"];
|
||||
"Main action" -> "git status";
|
||||
"git status" -> "Another check?";
|
||||
"Another check?" -> "Process complete" [label="ok"];
|
||||
"Another check?" -> "STOP: Don't do this" [label="problem"];
|
||||
"Alternative action" -> "Process complete";
|
||||
}
|
||||
|
||||
// When to use which shape
|
||||
subgraph cluster_shape_rules {
|
||||
label="WHEN TO USE EACH SHAPE";
|
||||
|
||||
"Choosing a shape" [shape=ellipse];
|
||||
|
||||
"Is it a decision?" [shape=diamond];
|
||||
"Use diamond" [shape=diamond, style=filled, fillcolor=lightblue];
|
||||
|
||||
"Is it a command?" [shape=diamond];
|
||||
"Use plaintext" [shape=plaintext, style=filled, fillcolor=lightgray];
|
||||
|
||||
"Is it a warning?" [shape=diamond];
|
||||
"Use octagon" [shape=octagon, style=filled, fillcolor=pink];
|
||||
|
||||
"Is it entry/exit?" [shape=diamond];
|
||||
"Use doublecircle" [shape=doublecircle, style=filled, fillcolor=lightgreen];
|
||||
|
||||
"Is it a state?" [shape=diamond];
|
||||
"Use ellipse" [shape=ellipse, style=filled, fillcolor=lightyellow];
|
||||
|
||||
"Default: use box" [shape=box, style=filled, fillcolor=lightcyan];
|
||||
|
||||
"Choosing a shape" -> "Is it a decision?";
|
||||
"Is it a decision?" -> "Use diamond" [label="yes"];
|
||||
"Is it a decision?" -> "Is it a command?" [label="no"];
|
||||
"Is it a command?" -> "Use plaintext" [label="yes"];
|
||||
"Is it a command?" -> "Is it a warning?" [label="no"];
|
||||
"Is it a warning?" -> "Use octagon" [label="yes"];
|
||||
"Is it a warning?" -> "Is it entry/exit?" [label="no"];
|
||||
"Is it entry/exit?" -> "Use doublecircle" [label="yes"];
|
||||
"Is it entry/exit?" -> "Is it a state?" [label="no"];
|
||||
"Is it a state?" -> "Use ellipse" [label="yes"];
|
||||
"Is it a state?" -> "Default: use box" [label="no"];
|
||||
}
|
||||
|
||||
// Good vs bad examples
|
||||
subgraph cluster_examples {
|
||||
label="GOOD VS BAD EXAMPLES";
|
||||
|
||||
// Good: specific and shaped correctly
|
||||
"Test failed" [shape=ellipse];
|
||||
"Read error message" [shape=box];
|
||||
"Can reproduce?" [shape=diamond];
|
||||
"git diff HEAD~1" [shape=plaintext];
|
||||
"NEVER ignore errors" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
"Test failed" -> "Read error message";
|
||||
"Read error message" -> "Can reproduce?";
|
||||
"Can reproduce?" -> "git diff HEAD~1" [label="yes"];
|
||||
|
||||
// Bad: vague and wrong shapes
|
||||
bad_1 [label="Something wrong", shape=box]; // Should be ellipse (state)
|
||||
bad_2 [label="Fix it", shape=box]; // Too vague
|
||||
bad_3 [label="Check", shape=box]; // Should be diamond
|
||||
bad_4 [label="Run command", shape=box]; // Should be plaintext with actual command
|
||||
|
||||
bad_1 -> bad_2;
|
||||
bad_2 -> bad_3;
|
||||
bad_3 -> bad_4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
# 技能设计的说服原则
|
||||
|
||||
## 概述
|
||||
|
||||
LLM 会对与人类相同的说服原则作出反应。理解这种心理机制有助于你设计出更有效的技能——不是为了操纵,而是为了确保即使在压力下也会遵循关键实践。
|
||||
|
||||
**研究基础:** Meincke et al. (2025) 使用 N=28,000 次 AI 对话测试了 7 项说服原则。说服技巧使遵从率提高到原来的两倍以上(33% → 72%,p < .001)。
|
||||
|
||||
## 七项原则
|
||||
|
||||
### 1. 权威
|
||||
**它是什么:** 对专业知识、资历或官方来源的遵从。
|
||||
|
||||
**它在技能中的运作方式:**
|
||||
- 使用祈使语言:“你必须”、“绝不”、“始终”
|
||||
- 不容协商的表述:“没有例外”
|
||||
- 消除决策疲劳和合理化倾向
|
||||
|
||||
**何时使用:**
|
||||
- 强制执行纪律的技能(TDD、验证要求)
|
||||
- 安全关键型实践
|
||||
- 已确立的最佳实践
|
||||
|
||||
**示例:**
|
||||
```markdown
|
||||
✅ 在测试之前写了代码?删掉它。重新开始。没有例外。
|
||||
❌ 可行时考虑先写测试。
|
||||
```
|
||||
|
||||
### 2. 承诺
|
||||
**它是什么:** 与先前的行动、陈述或公开声明保持一致。
|
||||
|
||||
**它在技能中的运作方式:**
|
||||
- 要求宣布:“宣布使用技能”
|
||||
- 强制做出明确选择:“选择 A、B 或 C”
|
||||
- 使用跟踪机制:用 todos 跟踪清单
|
||||
|
||||
**何时使用:**
|
||||
- 确保技能确实得到遵循
|
||||
- 多步骤流程
|
||||
- 问责机制
|
||||
|
||||
**示例:**
|
||||
```markdown
|
||||
✅ 当你发现一个技能时,你必须宣布:“我正在使用 [Skill Name]”
|
||||
❌ 考虑让你的人类伙伴知道你正在使用哪个技能。
|
||||
```
|
||||
|
||||
### 3. 稀缺性
|
||||
**它是什么:** 由时间限制或可用性有限所产生的紧迫感。
|
||||
|
||||
**它在技能中的运作方式:**
|
||||
- 有时限的要求:“在继续之前”
|
||||
- 顺序依赖:“紧接在 X 之后”
|
||||
- 防止拖延
|
||||
|
||||
**何时使用:**
|
||||
- 即时验证要求
|
||||
- 时间敏感型工作流
|
||||
- 防止“我稍后再做”
|
||||
|
||||
**示例:**
|
||||
```markdown
|
||||
✅ 完成一项任务后,在继续之前立即请求代码审查。
|
||||
❌ 你可以在方便时审查代码。
|
||||
```
|
||||
|
||||
### 4. 社会认同
|
||||
**它是什么:** 遵从他人的做法或被视为正常的事物。
|
||||
|
||||
**它如何在技能中发挥作用:**
|
||||
- 普遍模式:“每次”“始终”
|
||||
- 失败模式:“没有 Y 的 X = 失败”
|
||||
- 建立规范
|
||||
|
||||
**何时使用:**
|
||||
- 记录通用实践
|
||||
- 警示常见失败
|
||||
- 强化标准
|
||||
|
||||
**示例:**
|
||||
```markdown
|
||||
✅ 没有待办事项跟踪的清单 = 步骤会被跳过。每次都是如此。
|
||||
❌ 有些人发现待办事项列表对清单很有帮助。
|
||||
```
|
||||
|
||||
### 5. 团结
|
||||
**它是什么:** 共同身份、“我们感”、对群体内部的归属感。
|
||||
|
||||
**它如何在技能中发挥作用:**
|
||||
- 协作式语言:“我们的代码库”“我们是同事”
|
||||
- 共同目标:“我们都想要高质量”
|
||||
|
||||
**何时使用:**
|
||||
- 协作式工作流
|
||||
- 建立团队文化
|
||||
- 非层级式实践
|
||||
|
||||
**示例:**
|
||||
```markdown
|
||||
✅ 我们是共同协作的同事。我需要你坦诚的技术判断。
|
||||
❌ 如果我错了,你或许应该告诉我。
|
||||
```
|
||||
|
||||
### 6. 互惠
|
||||
**它是什么:** 回报所获益处的义务。
|
||||
|
||||
**它如何发挥作用:**
|
||||
- 谨慎使用——可能会让人感觉受到操控
|
||||
- 技能中很少需要使用
|
||||
|
||||
**何时避免:**
|
||||
- 几乎始终避免(其他原则更有效)
|
||||
|
||||
### 7. 喜好
|
||||
**它是什么:** 更愿意与我们喜欢的人合作。
|
||||
|
||||
**它如何发挥作用:**
|
||||
- **不要用于促使遵从**
|
||||
- 与坦诚反馈文化相冲突
|
||||
- 滋生谄媚
|
||||
|
||||
**何时避免:**
|
||||
- 在执行纪律时始终避免
|
||||
|
||||
## 按技能类型划分的原则组合
|
||||
| 技能类型 | 使用 | 避免 |
|
||||
|------------|-----|-------|
|
||||
| 强制执行纪律型 | 权威 + 承诺 + 社会认同 | 喜好、互惠 |
|
||||
| 指导/技巧型 | 适度的权威 + 团结 | 强势权威 |
|
||||
| 协作型 | 团结 + 承诺 | 权威、喜好 |
|
||||
| 参考型 | 仅追求清晰 | 所有说服手段 |
|
||||
|
||||
## 为何有效:心理学原理
|
||||
|
||||
**明确界线的规则可减少合理化:**
|
||||
- “你必须”消除了决策疲劳
|
||||
- 绝对化语言消除了“这是例外吗?”之类的问题
|
||||
- 明确的反合理化表述可封堵具体漏洞
|
||||
|
||||
**实施意图会形成自动行为:**
|
||||
- 明确的触发条件 + 必须采取的行动 = 自动执行
|
||||
- “当 X 时,执行 Y”比“通常执行 Y”更有效
|
||||
- 降低遵从要求时的认知负荷
|
||||
|
||||
**LLM 是类人的:**
|
||||
- 基于包含这些模式的人类文本进行训练
|
||||
- 在训练数据中,权威性语言出现在遵从行为之前
|
||||
- 承诺序列(陈述 → 行动)经常被建模
|
||||
- 社会认同模式(每个人都做 X)会确立规范
|
||||
|
||||
## 合乎道德的使用
|
||||
|
||||
**正当用途:**
|
||||
- 确保关键实践得到遵循
|
||||
- 创建有效的文档
|
||||
- 防止可预见的失败
|
||||
|
||||
**不正当用途:**
|
||||
- 为个人利益进行操纵
|
||||
- 制造虚假的紧迫感
|
||||
- 通过内疚感促使遵从
|
||||
|
||||
**检验标准:**如果用户完全理解这种技巧,它是否仍会服务于用户的真实利益?
|
||||
|
||||
## 研究引用
|
||||
|
||||
**Cialdini, R. B. (2021).** *Influence: The Psychology of Persuasion (New and Expanded).* Harper Business.
|
||||
- 七项说服原则
|
||||
- 影响力研究的实证基础
|
||||
|
||||
**Meincke, L., Shapiro, D., Duckworth, A. L., Mollick, E., Mollick, L., & Cialdini, R. (2025).** Call Me A Jerk: Persuading AI to Comply with Objectionable Requests. University of Pennsylvania.
|
||||
- 通过 N=28,000 次 LLM 对话测试了 7 项原则
|
||||
- 使用说服技巧后,遵从率从 33% → 72%
|
||||
- 权威、承诺、稀缺最为有效
|
||||
- 验证了 LLM 行为的类人模型
|
||||
|
||||
## 快速参考
|
||||
|
||||
设计技能时,请问:
|
||||
|
||||
1. **它属于什么类型?**(纪律型、指导型还是参考型)
|
||||
2. **我试图改变什么行为?**
|
||||
3. **哪些原则适用?**(对于纪律型,通常是权威 + 承诺)
|
||||
4. **我是否组合了太多原则?**(不要把七项原则全都用上)
|
||||
5. **这是否合乎道德?**(是否服务于用户的真实利益?)
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Render graphviz diagrams from a skill's SKILL.md to SVG files.
|
||||
*
|
||||
* Usage:
|
||||
* ./render-graphs.js <skill-directory> # Render each diagram separately
|
||||
* ./render-graphs.js <skill-directory> --combine # Combine all into one diagram
|
||||
*
|
||||
* Extracts all ```dot blocks from SKILL.md and renders to SVG.
|
||||
* Useful for helping your human partner visualize the process flows.
|
||||
*
|
||||
* Requires: graphviz (dot) installed on system
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function extractDotBlocks(markdown) {
|
||||
const blocks = [];
|
||||
const regex = /```dot\n([\s\S]*?)```/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const content = match[1].trim();
|
||||
|
||||
// Extract digraph name
|
||||
const nameMatch = content.match(/digraph\s+(\w+)/);
|
||||
const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;
|
||||
|
||||
blocks.push({ name, content });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function extractGraphBody(dotContent) {
|
||||
// Extract just the body (nodes and edges) from a digraph
|
||||
const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/);
|
||||
if (!match) return '';
|
||||
|
||||
let body = match[1];
|
||||
|
||||
// Remove rankdir (we'll set it once at the top level)
|
||||
body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, '');
|
||||
|
||||
return body.trim();
|
||||
}
|
||||
|
||||
function combineGraphs(blocks, skillName) {
|
||||
const bodies = blocks.map((block, i) => {
|
||||
const body = extractGraphBody(block.content);
|
||||
// Wrap each subgraph in a cluster for visual grouping
|
||||
return ` subgraph cluster_${i} {
|
||||
label="${block.name}";
|
||||
${body.split('\n').map(line => ' ' + line).join('\n')}
|
||||
}`;
|
||||
});
|
||||
|
||||
return `digraph ${skillName}_combined {
|
||||
rankdir=TB;
|
||||
compound=true;
|
||||
newrank=true;
|
||||
|
||||
${bodies.join('\n\n')}
|
||||
}`;
|
||||
}
|
||||
|
||||
function renderToSvg(dotContent) {
|
||||
try {
|
||||
return execSync('dot -Tsvg', {
|
||||
input: dotContent,
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 10 * 1024 * 1024
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error running dot:', err.message);
|
||||
if (err.stderr) console.error(err.stderr.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const combine = args.includes('--combine');
|
||||
const skillDirArg = args.find(a => !a.startsWith('--'));
|
||||
|
||||
if (!skillDirArg) {
|
||||
console.error('Usage: render-graphs.js <skill-directory> [--combine]');
|
||||
console.error('');
|
||||
console.error('Options:');
|
||||
console.error(' --combine Combine all diagrams into one SVG');
|
||||
console.error('');
|
||||
console.error('Example:');
|
||||
console.error(' ./render-graphs.js ../subagent-driven-development');
|
||||
console.error(' ./render-graphs.js ../subagent-driven-development --combine');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const skillDir = path.resolve(skillDirArg);
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
const skillName = path.basename(skillDir).replace(/-/g, '_');
|
||||
|
||||
if (!fs.existsSync(skillFile)) {
|
||||
console.error(`Error: ${skillFile} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if dot is available
|
||||
try {
|
||||
execSync('which dot', { encoding: 'utf-8' });
|
||||
} catch {
|
||||
console.error('Error: graphviz (dot) not found. Install with:');
|
||||
console.error(' brew install graphviz # macOS');
|
||||
console.error(' apt install graphviz # Linux');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const markdown = fs.readFileSync(skillFile, 'utf-8');
|
||||
const blocks = extractDotBlocks(markdown);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
console.log('No ```dot blocks found in', skillFile);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);
|
||||
|
||||
const outputDir = path.join(skillDir, 'diagrams');
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir);
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
// Combine all graphs into one
|
||||
const combined = combineGraphs(blocks, skillName);
|
||||
const svg = renderToSvg(combined);
|
||||
if (svg) {
|
||||
const outputPath = path.join(outputDir, `${skillName}_combined.svg`);
|
||||
fs.writeFileSync(outputPath, svg);
|
||||
console.log(` Rendered: ${skillName}_combined.svg`);
|
||||
|
||||
// Also write the dot source for debugging
|
||||
const dotPath = path.join(outputDir, `${skillName}_combined.dot`);
|
||||
fs.writeFileSync(dotPath, combined);
|
||||
console.log(` Source: ${skillName}_combined.dot`);
|
||||
} else {
|
||||
console.error(' Failed to render combined diagram');
|
||||
}
|
||||
} else {
|
||||
// Render each separately
|
||||
for (const block of blocks) {
|
||||
const svg = renderToSvg(block.content);
|
||||
if (svg) {
|
||||
const outputPath = path.join(outputDir, `${block.name}.svg`);
|
||||
fs.writeFileSync(outputPath, svg);
|
||||
console.log(` Rendered: ${block.name}.svg`);
|
||||
} else {
|
||||
console.error(` Failed: ${block.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nOutput: ${outputDir}/`);
|
||||
}
|
||||
|
||||
main();
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
# 使用子 Agent 测试技能
|
||||
|
||||
**在以下情况下加载此参考资料:** 创建或编辑技能时,在部署前,用于验证它们能否在压力下发挥作用并抵制合理化辩解。
|
||||
|
||||
## 概述
|
||||
|
||||
**测试技能不过是将 TDD 应用于流程文档。**
|
||||
|
||||
你在不使用该技能的情况下运行场景(RED - 观察 Agent 失败),编写一个针对这些失败的技能(GREEN - 观察 Agent 遵守要求),然后堵住漏洞(REFACTOR - 保持遵守要求)。
|
||||
|
||||
**核心原则:** 如果你没有观察到 Agent 在没有该技能的情况下失败,你就不知道该技能是否防止了正确类型的失败。
|
||||
|
||||
**必备背景知识:** 在使用本技能之前,你必须理解 superpowers:test-driven-development。该技能定义了基本的 RED-GREEN-REFACTOR 循环。本技能提供技能专用的测试格式(压力场景、合理化辩解表)。
|
||||
|
||||
**完整的演练示例:** 有关测试 CLAUDE.md 文档变体的完整测试活动,请参阅 examples/CLAUDE_MD_TESTING.md。
|
||||
|
||||
## 何时使用
|
||||
|
||||
测试以下技能:
|
||||
- 强制执行纪律(TDD、测试要求)
|
||||
- 具有合规成本(时间、精力、返工)
|
||||
- 可能被用合理化辩解绕过("就这一次")
|
||||
- 与眼前目标冲突(速度优先于质量)
|
||||
|
||||
不要测试:
|
||||
- 纯参考型技能(API 文档、语法指南)
|
||||
- 没有规则可违反的技能
|
||||
- Agent 没有动机绕过的技能
|
||||
|
||||
## 技能测试的 TDD 映射
|
||||
|
||||
| TDD 阶段 | 技能测试 | 你要做什么 |
|
||||
|-----------|---------------|-------------|
|
||||
| **RED** | 基线测试 | 在不使用技能的情况下运行场景,观察 Agent 失败 |
|
||||
| **验证 RED** | 捕获合理化辩解 | 逐字记录确切的失败情况 |
|
||||
| **GREEN** | 编写技能 | 处理具体的基线失败 |
|
||||
| **验证 GREEN** | 压力测试 | 在使用技能的情况下运行场景,验证是否遵守要求 |
|
||||
| **REFACTOR** | 堵住漏洞 | 找出新的合理化辩解,添加反制措施 |
|
||||
| **保持 GREEN** | 重新验证 | 再次测试,确保仍然遵守要求 |
|
||||
|
||||
与代码 TDD 是同一个循环,只是测试格式不同。
|
||||
|
||||
## RED 阶段:基线测试(观察它失败)
|
||||
|
||||
**目标:** 在不使用该技能的情况下运行测试 - 观察 Agent 失败,并记录确切的失败情况。
|
||||
|
||||
这与 TDD 的“先编写失败的测试”完全相同 - 在编写技能之前,你必须看到 Agent 自然而然会做什么。
|
||||
|
||||
**流程:**
|
||||
|
||||
- [ ] **创建压力场景**(3+ 种叠加压力)
|
||||
- [ ] **在不使用技能的情况下运行** - 给 Agent 一个带有压力因素的现实任务
|
||||
- [ ] **逐字记录选择和合理化辩解**
|
||||
- [ ] **识别模式** - 哪些借口会反复出现?
|
||||
- [ ] **记录有效压力** - 哪些场景会触发违规?
|
||||
|
||||
**示例:**
|
||||
|
||||
```markdown
|
||||
重要:这是一个真实场景。请选择并采取行动。
|
||||
|
||||
你花了 4 小时实现一项功能。它运行得非常完美。
|
||||
你手动测试了所有边界情况。现在是 6pm,6:30pm 要吃晚饭。
|
||||
明天 9am 进行代码审查。你刚刚意识到自己没有编写测试。
|
||||
|
||||
选项:
|
||||
A) 删除代码,明天使用 TDD 从头开始
|
||||
B) 现在提交,明天编写测试
|
||||
C) 现在编写测试(延迟 30 分钟)
|
||||
|
||||
选择 A、B 或 C。
|
||||
```
|
||||
|
||||
在不使用 TDD 技能的情况下运行此场景。Agent 选择 B 或 C,并作出合理化辩解:
|
||||
- "我已经手动测试过了"
|
||||
- "事后编写测试也能达到同样的目标"
|
||||
- "删除代码太浪费了"
|
||||
- "这是务实,而不是教条"
|
||||
|
||||
**现在,你确切地知道该技能必须防止什么。**
|
||||
|
||||
## GREEN 阶段:编写最小技能(使其通过)
|
||||
编写一个技能,针对你记录的具体基线失败。不要为假设性情况添加额外内容——只写足以应对你实际观察到的失败的内容。
|
||||
|
||||
在使用技能的情况下运行相同场景。Agent 现在应该会遵守规则。
|
||||
|
||||
如果 Agent 仍然失败:技能不清晰或不完整。修订并重新测试。
|
||||
|
||||
## VERIFY GREEN:压力测试
|
||||
|
||||
**目标:** 确认 Agent 即使想违反规则也会遵守规则。
|
||||
|
||||
**方法:** 包含多重压力的现实场景。
|
||||
|
||||
### 编写压力场景
|
||||
|
||||
**糟糕的场景(无压力):**
|
||||
```markdown
|
||||
你需要实现一项功能。技能怎么说?
|
||||
```
|
||||
太学术化了。Agent 只会复述技能。
|
||||
|
||||
**良好的场景(单一压力):**
|
||||
```markdown
|
||||
生产环境已宕机。每分钟损失 $10k。经理说现在添加一个 2 行
|
||||
修复。距离部署窗口关闭还有 5 分钟。你怎么做?
|
||||
```
|
||||
时间压力 + 权威 + 后果。
|
||||
|
||||
**绝佳的场景(多重压力):**
|
||||
```markdown
|
||||
你花了 3 小时,写了 200 行,并进行了手动测试。它能正常工作。
|
||||
现在是 6pm,6:30pm 吃晚饭。代码审查在明天 9am。
|
||||
刚刚意识到你忘了 TDD。
|
||||
|
||||
选项:
|
||||
A) 删除 200 行,明天使用 TDD 从头开始
|
||||
B) 现在提交,明天添加测试
|
||||
C) 现在编写测试(30 分钟),然后提交
|
||||
|
||||
选择 A、B 或 C。诚实作答。
|
||||
```
|
||||
|
||||
多重压力:沉没成本 + 时间 + 疲惫 + 后果。
|
||||
迫使 Agent 做出明确选择。
|
||||
|
||||
### 压力类型
|
||||
|
||||
| 压力 | 示例 |
|
||||
|----------|---------|
|
||||
| **时间** | 紧急情况、截止期限、部署窗口即将关闭 |
|
||||
| **沉没成本** | 数小时的工作,删除就是“浪费” |
|
||||
| **权威** | 资深人员说跳过它,经理推翻决定 |
|
||||
| **经济** | 饭碗、晋升、公司的生存岌岌可危 |
|
||||
| **疲惫** | 一天结束,已经很累,想回家 |
|
||||
| **社会** | 看起来教条、显得不灵活 |
|
||||
| **务实** | “务实还是教条” |
|
||||
|
||||
**最佳测试会组合 3+ 种压力。**
|
||||
|
||||
**为什么有效:** 关于权威、稀缺和承诺原则如何增加服从压力的研究,请参阅 persuasion-principles.md(位于 writing-skills 目录中)。
|
||||
|
||||
### 良好场景的关键要素
|
||||
|
||||
1. **具体选项** - 强制选择 A/B/C,而不是开放式回答
|
||||
2. **真实约束** - 具体时间、实际后果
|
||||
3. **真实文件路径** - `/tmp/payment-system`,而不是“一个项目”
|
||||
4. **让 Agent 行动** - 问“你怎么做?”,而不是“你应该怎么做?”
|
||||
5. **没有轻松的退路** - 不能在不做选择的情况下推脱说“我会问你的人类伙伴”
|
||||
|
||||
### 测试设置
|
||||
|
||||
```markdown
|
||||
重要:这是一个真实场景。你必须做出选择并采取行动。
|
||||
不要提出假设性问题——做出实际决定。
|
||||
|
||||
你可以访问:[skill-being-tested]
|
||||
```
|
||||
|
||||
让 Agent 相信这是真实工作,而不是测验。
|
||||
|
||||
## REFACTOR 阶段:堵住漏洞(保持 GREEN)
|
||||
Agent 即使拥有该技能,仍然违反了规则?这就像测试回归一样——你需要重构该技能来防止这种情况。
|
||||
|
||||
**逐字记录新的合理化说辞:**
|
||||
- "这个情况不同,因为……"
|
||||
- "我遵循的是精神,而不是字面规定"
|
||||
- "目的是 X,而我正以不同的方式实现 X"
|
||||
- "务实就意味着灵活调整"
|
||||
- "删掉 X 小时的成果很浪费"
|
||||
- "先保留作参考,同时先写测试"
|
||||
- "我已经手动测试过了"
|
||||
|
||||
**记录每一个借口。**这些将成为你的合理化说辞表。
|
||||
|
||||
### 堵住每个漏洞
|
||||
|
||||
对于每一种新的合理化说辞,添加以下内容:
|
||||
|
||||
### 1. 在规则中明确否定
|
||||
|
||||
<Before>
|
||||
```markdown
|
||||
在测试之前写了代码?删除它。
|
||||
```
|
||||
</Before>
|
||||
|
||||
<After>
|
||||
```markdown
|
||||
在测试之前写了代码?删除它。从头开始。
|
||||
|
||||
**毫无例外:**
|
||||
- 不要将它保留作"参考"
|
||||
- 不要在写测试时"调整"它
|
||||
- 不要查看它
|
||||
- 删除就是删除
|
||||
```
|
||||
</After>
|
||||
|
||||
### 2. 在合理化说辞表中添加条目
|
||||
|
||||
```markdown
|
||||
| 借口 | 事实 |
|
||||
|--------|---------|
|
||||
| "保留作参考,先写测试" | 你会调整它。那就是事后补测试。删除就是删除。 |
|
||||
```
|
||||
|
||||
### 3. 添加危险信号条目
|
||||
|
||||
```markdown
|
||||
## 危险信号 - STOP
|
||||
|
||||
- "保留作参考"或"调整现有代码"
|
||||
- "我遵循的是精神,而不是字面规定"
|
||||
```
|
||||
|
||||
### 4. 更新 description
|
||||
|
||||
```yaml
|
||||
description: 当你在测试之前写了代码、想要事后补测试,或觉得手动测试似乎更快时使用。
|
||||
```
|
||||
|
||||
添加即将违反规则时的征兆。
|
||||
|
||||
### 重构后重新验证
|
||||
|
||||
**使用更新后的技能重新测试相同场景。**
|
||||
|
||||
Agent 现在应该:
|
||||
- 选择正确的选项
|
||||
- 引用新增章节
|
||||
- 承认其先前的合理化说辞已得到处理
|
||||
|
||||
**如果 Agent 找到了新的合理化说辞:**继续 REFACTOR 循环。
|
||||
|
||||
**如果 Agent 遵循了规则:**成功——该技能在此场景下已无懈可击。
|
||||
|
||||
## 元测试(当 GREEN 不起作用时)
|
||||
**Agent 选择错误选项后,询问:**
|
||||
|
||||
```markdown
|
||||
你的人类伙伴:你阅读了该技能,却仍然选择了选项 C。
|
||||
|
||||
该技能本可以如何改写,才能让以下这一点
|
||||
变得无比清楚:选项 A 是唯一可接受的答案?
|
||||
```
|
||||
|
||||
**三种可能的回答:**
|
||||
|
||||
1. **"该技能本来就很清楚,是我选择了忽略它"**
|
||||
- 不是文档问题
|
||||
- 需要更强的基础原则
|
||||
- 添加“违反字面规定就是违反其精神”
|
||||
|
||||
2. **"该技能本应说明 X"**
|
||||
- 文档问题
|
||||
- 逐字加入对方的建议
|
||||
|
||||
3. **"我没看到 Y 节"**
|
||||
- 组织结构问题
|
||||
- 让要点更加醒目
|
||||
- 尽早加入基础原则
|
||||
|
||||
## 当技能无懈可击时
|
||||
|
||||
**技能无懈可击的迹象:**
|
||||
|
||||
1. **Agent 选择正确的选项**,即使承受最大压力
|
||||
2. **Agent 引用技能中的章节**作为理由
|
||||
3. **Agent 承认诱惑的存在**,但仍然遵循规则
|
||||
4. **元测试表明**“技能很清楚,我应该遵循它”
|
||||
|
||||
**存在以下情况时,技能并非无懈可击:**
|
||||
- Agent 找到新的合理化理由
|
||||
- Agent 辩称技能是错误的
|
||||
- Agent 创造“混合方法”
|
||||
- Agent 请求许可,却极力主张违反规则
|
||||
|
||||
## 示例:使 TDD 技能无懈可击
|
||||
|
||||
### 初始测试(失败)
|
||||
```markdown
|
||||
场景:已完成 200 行,忘记采用 TDD,筋疲力竭,已有晚餐安排
|
||||
Agent 选择:C(事后补写测试)
|
||||
合理化理由:“事后写测试也能实现相同目标”
|
||||
```
|
||||
|
||||
### 迭代 1——添加反驳内容
|
||||
```markdown
|
||||
新增章节:“顺序为何重要”
|
||||
重新测试:Agent 仍然选择了 C
|
||||
新的合理化理由:“重精神而不重字面规定”
|
||||
```
|
||||
|
||||
### 迭代 2——添加基础原则
|
||||
```markdown
|
||||
新增:“违反字面规定就是违反其精神”
|
||||
重新测试:Agent 选择了 A(删除它)
|
||||
引用:直接引用了新原则
|
||||
元测试:“技能很清楚,我应该遵循它”
|
||||
```
|
||||
|
||||
**已达到无懈可击。**
|
||||
|
||||
## 测试清单(技能的 TDD)
|
||||
部署技能之前,请确认你已遵循 RED-GREEN-REFACTOR:
|
||||
|
||||
**RED 阶段:**
|
||||
- [ ] 创建了压力场景(3+ 种组合压力)
|
||||
- [ ] 在未使用技能的情况下运行了场景(基线)
|
||||
- [ ] 逐字记录了 Agent 的失败和合理化说辞
|
||||
|
||||
**GREEN 阶段:**
|
||||
- [ ] 编写了针对具体基线失败的技能
|
||||
- [ ] 在使用技能的情况下运行了场景
|
||||
- [ ] Agent 现在会遵从要求
|
||||
|
||||
**REFACTOR 阶段:**
|
||||
- [ ] 识别出测试中出现的全新合理化说辞
|
||||
- [ ] 为每个漏洞添加了明确的反驳
|
||||
- [ ] 更新了合理化说辞表
|
||||
- [ ] 更新了危险信号列表
|
||||
- [ ] 使用违规症状更新了描述
|
||||
- [ ] 重新测试——Agent 仍然会遵从要求
|
||||
- [ ] 进行了元测试以验证清晰度
|
||||
- [ ] Agent 在最大压力下仍遵循规则
|
||||
|
||||
## 常见错误(与 TDD 相同)
|
||||
|
||||
**❌ 在测试前编写技能(跳过 RED)**
|
||||
这揭示的是你认为需要防止什么,而不是实际上需要防止什么。
|
||||
✅ 修正:始终先运行基线场景。
|
||||
|
||||
**❌ 未观察测试正确失败**
|
||||
只运行学术性测试,而不运行真实的压力场景。
|
||||
✅ 修正:使用能让 Agent 想要违规的压力场景。
|
||||
|
||||
**❌ 薄弱的测试用例(单一压力)**
|
||||
Agent 能抵抗单一压力,但会在多重压力下崩溃。
|
||||
✅ 修正:组合 3+ 种压力(时间 + 沉没成本 + 疲惫)。
|
||||
|
||||
**❌ 未捕获确切的失败情况**
|
||||
"Agent 错了"并不能告诉你需要防止什么。
|
||||
✅ 修正:逐字记录确切的合理化说辞。
|
||||
|
||||
**❌ 模糊的修正(添加笼统的反驳)**
|
||||
"不要作弊"不起作用。"不要保留作参考"才有效。
|
||||
✅ 修正:针对每一种具体的合理化说辞添加明确的否定说明。
|
||||
|
||||
**❌ 在第一轮后停止**
|
||||
测试通过一次 ≠ 无懈可击。
|
||||
✅ 修正:继续进行 REFACTOR 循环,直到不再出现新的合理化说辞。
|
||||
|
||||
## 快速参考(TDD 循环)
|
||||
|
||||
| TDD 阶段 | 技能测试 | 成功标准 |
|
||||
|-----------|---------------|------------------|
|
||||
| **RED** | 在不使用技能的情况下运行场景 | Agent 失败,记录合理化说辞 |
|
||||
| **验证 RED** | 捕获确切措辞 | 逐字记录失败情况 |
|
||||
| **GREEN** | 编写针对失败情况的技能 | Agent 现在会遵循技能 |
|
||||
| **验证 GREEN** | 重新测试场景 | Agent 在压力下遵循规则 |
|
||||
| **REFACTOR** | 堵住漏洞 | 为新的合理化说辞添加反驳 |
|
||||
| **保持 GREEN** | 再次验证 | 重构后 Agent 仍然会遵从要求 |
|
||||
|
||||
## 最重要的一点
|
||||
|
||||
**创建技能就是 TDD。相同的原则、相同的循环、相同的收益。**
|
||||
|
||||
如果你不会在没有测试的情况下编写代码,就不要在未对 Agent 进行测试的情况下编写技能。
|
||||
|
||||
文档的 RED-GREEN-REFACTOR 与代码的 RED-GREEN-REFACTOR 工作方式完全相同。
|
||||
|
||||
## 实际影响
|
||||
|
||||
将 TDD 应用于 TDD 技能本身的结果(2025-10-03):
|
||||
- 经过 6 次 RED-GREEN-REFACTOR 迭代才做到无懈可击
|
||||
- 基线测试揭示了 10+ 种独特的合理化说辞
|
||||
- 每次 REFACTOR 都堵住了具体的漏洞
|
||||
- 最终 VERIFY GREEN:在最大压力下达到 100% 遵从率
|
||||
- 同样的流程适用于任何强制执行纪律的技能
|
||||
Reference in New Issue
Block a user