@brightliu/ai-control 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/addons/export-adapters.js +132 -0
- package/bin/ai.js +58 -0
- package/lib/change.js +62 -0
- package/lib/core.js +94 -0
- package/lib/doctor.js +68 -0
- package/lib/gate.js +193 -0
- package/lib/init.js +88 -0
- package/lib/junit.js +76 -0
- package/lib/testrun.js +136 -0
- package/package.json +37 -0
- package/payload/AGENTS.md +62 -0
- package/payload/agents/agent-dba.md +104 -0
- package/payload/agents/agent-dev.md +100 -0
- package/payload/agents/agent-spec.md +209 -0
- package/payload/agents/agent-test.md +74 -0
- package/payload/agents/dev/go.md +23 -0
- package/payload/agents/dev/java.md +71 -0
- package/payload/agents/dev/php.md +23 -0
- package/payload/agents/dev/web.md +62 -0
- package/payload/hooks/guard-bash.js +37 -0
- package/payload/hooks/guard-write.js +92 -0
- package/payload/rules/00-agent-base.md +56 -0
- package/payload/rules/01-code-change.md +27 -0
- package/payload/rules/02-product-ux.md +141 -0
- package/payload/rules/10-db-schema.md +84 -0
- package/payload/rules/20-api.md +160 -0
- package/payload/rules/21-jwt.md +25 -0
- package/payload/rules/22-rbac.md +38 -0
- package/payload/rules/24-openapi.md +14 -0
- package/payload/rules/30-frontend.md +58 -0
- package/payload/rules/31-vue3.md +20 -0
- package/payload/rules/32-react.md +20 -0
- package/payload/rules/40-backend.md +63 -0
- package/payload/rules/41-spring-boot.md +184 -0
- package/payload/rules/42-go-gin.md +20 -0
- package/payload/rules/43-php.md +19 -0
- package/payload/rules/44-java-enum.md +108 -0
- package/payload/rules/50-testing.md +39 -0
- package/payload/rules/51-security.md +50 -0
- package/payload/rules/52-performance.md +45 -0
- package/payload/rules/53-release.md +141 -0
- package/payload/rules/README.md +12 -0
- package/payload/templates/design.md +15 -0
- package/payload/templates/proposal-lite.md +21 -0
- package/payload/templates/proposal.md +28 -0
- package/payload/templates/review-prompt.md +20 -0
- package/payload/templates/spec.md +13 -0
- package/payload/templates/test-cases.md +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BrightLiu4917
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ai-control
|
|
2
|
+
|
|
3
|
+
**让 AI 编程助手在你的项目里守规矩的工具。**
|
|
4
|
+
|
|
5
|
+
你在用 Cursor、Claude Code、Codex、通义 Qoder 这些 AI 写代码时,是不是遇到过:
|
|
6
|
+
|
|
7
|
+
- AI **编造**了一个数据库里根本不存在的字段?
|
|
8
|
+
- AI 说"测试通过了",其实**根本没跑**测试?
|
|
9
|
+
- AI **顺手执行**了一条危险 SQL,或者把不该动的表改了?
|
|
10
|
+
- 想管住它,结果流程太重,改个文案也要走一堆步骤,最后**自己都不想守**?
|
|
11
|
+
|
|
12
|
+
ai-control 就是治这四件事的:装进你的项目后,AI 会**先写变更单给你确认、动数据库前必须等你点头、测试结果以测试报告为准(说了不算)**——而改文案这类小事走 30 秒的快速通道,不折腾你。
|
|
13
|
+
|
|
14
|
+
## 效果对比
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
❌ 没装之前
|
|
18
|
+
你:加个订单导出功能
|
|
19
|
+
AI:好的!(编了个 status=5 的枚举值,顺手改了表结构,最后说"测试全部通过")
|
|
20
|
+
你:……上线炸了。
|
|
21
|
+
|
|
22
|
+
✅ 装了之后
|
|
23
|
+
你:加个订单导出功能
|
|
24
|
+
AI:我先看了代码。这个需求要动 t_order 表,属于高风险变更。
|
|
25
|
+
这是变更单:改什么、动哪张表、验收标准是这 4 条测试用例。确认吗?
|
|
26
|
+
你:确认
|
|
27
|
+
AI:(实现 → 跑测试)测试报告 4 条全绿,ai ship 发布检查通过。表结构变更的回滚 SQL 在这里。
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
你全程只做两件事:**确认变更单、确认数据库改动**。其他检查(用例覆盖没覆盖、测试报告在不在、有没有失败)由内置的自动检查完成——它查的是测试框架生成的原始报告,AI 想蒙混就得伪造报告文件,成本高得多、也容易被发现。
|
|
31
|
+
|
|
32
|
+
## 3 分钟上手
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 1. 装工具(一次性)
|
|
36
|
+
npm install -g github:BrightLiu4917/ai-control
|
|
37
|
+
|
|
38
|
+
# 2. 装进你的项目
|
|
39
|
+
cd 你的项目
|
|
40
|
+
ai init # 自动识别技术栈;也可指定 --stack java|go|php|vue|react|node|mixed
|
|
41
|
+
ai sync # 只有用 Claude Code / WorkBuddy 才需要:为它们生成各自格式的配置文件
|
|
42
|
+
|
|
43
|
+
# 3. 开始干活——两种用法任选
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**用法 A(推荐):在 AI 工具里说人话。** 直接提需求,AI 会自己执行下面这 5 个命令并替你补全变更单——你只负责在确认点点头。用法 B 是同一套流程的手动入口,两者随时混用。
|
|
47
|
+
|
|
48
|
+
**用法 B:终端命令。** 一共 5 个:
|
|
49
|
+
|
|
50
|
+
| 命令 | 干什么 | 人话 |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `ai new 名字` | 生成变更单骨架(需求说明 + 验收用例两个文件,由 AI 补全内容、你来确认) | "我要开工了"(小事加 `--lite`) |
|
|
53
|
+
| `ai check 名字` | 检查变更单写全没有 | "能给用户确认了吗" |
|
|
54
|
+
| `ai confirm 名字` | **由你本人运行**,留下确认记录(谁、何时、哪个提交);AI 代跑会被钩子拦下 | "我确认了"(ship 的前置) |
|
|
55
|
+
| `ai test 名字` | 跑你项目自己的测试,报告自动按变更隔离存放 | "跑测试" |
|
|
56
|
+
| `ai ship 名字` | 核对测试报告:确认过没有、报告新不新、全绿没有 | "能交付了吗"(秒级出结果) |
|
|
57
|
+
|
|
58
|
+
## 它靠什么管住 AI(三层,由软到硬)
|
|
59
|
+
|
|
60
|
+
1. **规矩**(`AGENTS.md` + `.ai/rules/`):63 行契约每次对话自动生效 + 20 份中文工程规则按需加载——禁止编造字段、SQL 必须带 WHERE、枚举必须 code+desc 禁止魔法值、JOIN 不许先连后分页……**这些是踩过生产事故总结的具体规则,不是"请写好代码"式的空话**。
|
|
61
|
+
2. **确认点**(必须等你点头的两处):变更单确认之前 AI 不许写码——你点头后 `ai confirm` 会记下确认人/时间/提交号,之后变更单再被改动确认即失效;数据库改动确认之前 AI 不许执行 SQL。
|
|
62
|
+
3. **自动检查**(内置在 check/confirm/ship 命令里的程序):变更单声明动表却想走快速通道?拦。碰表/接口却全是手动用例?拦。没确认就想交付、或确认后偷改了变更单?拦。测试报告缺失、有失败、或比确认时间还旧(拿旧报告顶包)?拦。(数据库确认属于第 2 层——靠契约约束 AI 停下等你,ship 时会提示做独立审查,不是机械拦截。)
|
|
63
|
+
|
|
64
|
+
## 支持哪些 AI 工具
|
|
65
|
+
|
|
66
|
+
| 工具 | 需要做什么 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Codex / Cursor / Kimi Code / Qoder | **装完即用**(它们自动读 AGENTS.md) |
|
|
69
|
+
| Claude Code | `ai sync` 一次——生成它专用的 CLAUDE.md、角色配置,以及 **PreToolUse 钩子**:未确认就写业务代码、写影响范围外的文件、执行危险 SQL,会在动手的一瞬间被拦下(其他工具是事后检查,Claude Code 是当场按住手) |
|
|
70
|
+
| WorkBuddy(腾讯 AI 办公助手) | `ai sync` 写入项目级 `.workbuddy/skills/`,重启 WorkBuddy 即生效(无需复制);不用它可忽略 |
|
|
71
|
+
|
|
72
|
+
多语言混合项目(如 Java 后端 + Vue 前端)装一次即可:AI 按任务碰到的文件自动选对应规则;测试命令在 `.ai/config.json` 里配一条串联命令。
|
|
73
|
+
|
|
74
|
+
## 装进项目后长什么样
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
你的项目/
|
|
78
|
+
├── AGENTS.md 规矩总纲——AI 工具自动读取
|
|
79
|
+
└── .ai/ 规则库、角色手册 + changes/(变更单记录,每个需求一个文件夹)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
已有 AGENTS.md 的项目不会被覆盖;升级用 `ai init --update`(只更新框架文件,先自动备份,绝不碰你的变更记录)。
|
|
83
|
+
|
|
84
|
+
## 高风险变更想要第二意见?
|
|
85
|
+
|
|
86
|
+
`ai ship` 检测到动数据库/支付/状态流时会提示:新开一个 AI 会话,用 `.ai/templates/review-prompt.md` + git diff 让它做一次独立审查(审查者不能是写代码的那个会话)。零配置、零 API 费用。
|
|
87
|
+
|
|
88
|
+
## 常见问题
|
|
89
|
+
|
|
90
|
+
**Q:改个错别字也要走流程吗?**
|
|
91
|
+
不用。纯文档/注释/typo 直接改;小功能走 `--lite`(两个文件、30 秒确认);只有动数据库、接口、权限才走完整流程。管控力度和风险成正比。
|
|
92
|
+
|
|
93
|
+
**Q:AI 不守规矩怎么办?**
|
|
94
|
+
说一句"按控制系统流程来"即可拽回;关键环节(越级偷渡、报告缺失或有失败)有自动检查兜底。诚实说明边界:AI 理论上可以伪造报告文件绕过检查,门禁的作用是把"张嘴谎报"变成"必须留下可查的假证据"——成本和暴露风险完全不是一个量级。
|
|
95
|
+
|
|
96
|
+
**Q:测试报告是什么格式?**
|
|
97
|
+
JUnit XML——`ai test` 会按栈自动搞定:Maven 自带;Go 的输出自动转成报告(零配置,无需 gotestsum);Vitest 自动注入内置 junit reporter;PHPUnit 自动加 `--log-junit`;仅 Jest 需要 `npm i -D jest-junit`(会提示)。测试方法名里带上用例编号(如 `test_TC01_xxx`)即可被核对。多变更并行时把报告输出到 `test-results/<变更名>/` 可互相隔离;报告必须比确认时间新——旧报告顶包会被拦下。
|
|
98
|
+
|
|
99
|
+
## 被脚本 / Agent 调用?机器契约在这
|
|
100
|
+
|
|
101
|
+
所有命令遵守统一约定,无需解析人类文案:**退出码** 0=通过、1=用法或环境错误、2=门禁拦截;**结果信号**固定为输出中的单行大写标记:`CHECK_PASSED` / `CONFIRM_OK` / `TEST_PASSED` / `SHIP_GATES_PASSED` / `DOCTOR_OK`(失败时为 `[FAIL] ...` 行 + 非零退出码)。这两样是稳定接口,不会随文案调整而变。
|
|
102
|
+
|
|
103
|
+
## 团队用?把门禁挂到 PR 上
|
|
104
|
+
|
|
105
|
+
个人用时门禁跑在本地;团队用时加一个 GitHub Action,PR 不合规直接挂红叉合不了:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
# .github/workflows/ai-gates.yml
|
|
109
|
+
name: ai-gates
|
|
110
|
+
on: pull_request
|
|
111
|
+
jobs:
|
|
112
|
+
gates:
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
- uses: BrightLiu4917/ai-control@main # 契约门禁(check)
|
|
117
|
+
# 需要证据门禁时:先跑你的测试产出 JUnit 报告,再加一步
|
|
118
|
+
# - uses: BrightLiu4917/ai-control@main
|
|
119
|
+
# with: { mode: ship }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 工程质量
|
|
123
|
+
|
|
124
|
+
零运行时依赖(只需 Node ≥18.17 + git);22 个端到端验收测试 + 8 个单元测试 + 规则库引用自检(防悬空引用);代码量硬预算写进 CI(内核 ≤1500 行、契约 ≤100 行、文档 1 份),超支即红——防止工具本身变臃肿。
|
|
125
|
+
|
|
126
|
+
前身 [ai-coding-fun](https://github.com/BrightLiu4917/ai-coding-fun)(v1)经 15 批真实项目迭代后彻底重构:5000 行 bash → 600 行 JS,五份文档的流程 → 两份,学习成本压缩到本 README 一页。
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// ai sync:导出多工具适配物(Claude Code + WorkBuddy)。
|
|
2
|
+
// Codex / Cursor / Kimi / Qoder 原生读 AGENTS.md,无需导出。
|
|
3
|
+
// 幂等:已存在跳过;--force 覆盖。WorkBuddy 技能自包含(内嵌被引用的规则快照)。
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
function sync(root, args) {
|
|
8
|
+
const force = args.includes("--force");
|
|
9
|
+
const ai = path.join(root, ".ai");
|
|
10
|
+
const agentsDir = path.join(ai, "agents");
|
|
11
|
+
const written = [];
|
|
12
|
+
const skipped = [];
|
|
13
|
+
|
|
14
|
+
function write(p, content) {
|
|
15
|
+
if (fs.existsSync(p) && !force) { skipped.push(path.relative(root, p)); return; }
|
|
16
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
17
|
+
fs.writeFileSync(p, content);
|
|
18
|
+
written.push(path.relative(root, p));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sec = (s, title) => (s.match(new RegExp(`^## ${title}\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, "m")) || [])[1]?.trim() || "";
|
|
22
|
+
const firstLine = (s) => (s.trim().split("\n")[0] || "").replace(/。$/, "") + "。";
|
|
23
|
+
|
|
24
|
+
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.startsWith("agent-") && f.endsWith(".md"));
|
|
25
|
+
|
|
26
|
+
// 1. .claude/settings.json:PreToolUse 钩子。项目可能已有自己的 settings.json——
|
|
27
|
+
// 只做 JSON 合并(缺哪条补哪条),绝不整文件覆盖用户的 permissions/其他 hooks;写后读回校验。
|
|
28
|
+
const hooksOk = mergeClaudeSettings(root, written, skipped);
|
|
29
|
+
|
|
30
|
+
// 2. CLAUDE.md(钩子状态按合并的实际结果措辞,不许说大话)
|
|
31
|
+
write(path.join(root, "CLAUDE.md"),
|
|
32
|
+
`# 项目契约入口(Claude Code)
|
|
33
|
+
|
|
34
|
+
@AGENTS.md
|
|
35
|
+
|
|
36
|
+
- 角色手册已导出为 \`.claude/agents/\` 原生 subagent,匹配任务自动委派。
|
|
37
|
+
- 变更流程可通过 \`.claude/skills/\` 技能触发。
|
|
38
|
+
${hooksOk
|
|
39
|
+
? "- PreToolUse 钩子已启用:未确认变更时写业务代码、写影响范围外文件、执行危险 SQL、代跑 ai confirm 会被当场拦截(临时停用:AI_CONTROL_HOOKS=off)。"
|
|
40
|
+
: "- 注意:PreToolUse 钩子未安装(.claude/settings.json 无法合并);上述拦截不生效,仅靠契约约束。"}
|
|
41
|
+
`);
|
|
42
|
+
|
|
43
|
+
// 2. .claude/agents/
|
|
44
|
+
for (const f of agentFiles) {
|
|
45
|
+
const content = fs.readFileSync(path.join(agentsDir, f), "utf8");
|
|
46
|
+
const id = f.replace(/\.md$/, "");
|
|
47
|
+
const role = (content.match(/^# ([^(\n]+)/) || [, id])[1].trim();
|
|
48
|
+
const duty = firstLine(sec(content, "职责"));
|
|
49
|
+
const desc = `${role}。${duty}涉及对应场景时必须主动使用(use proactively)。`.replace(/\n/g, " ");
|
|
50
|
+
write(path.join(root, ".claude", "agents", f), `---\nname: ${id}\ndescription: ${desc}\n---\n\n${content}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. .claude/skills/
|
|
54
|
+
const skills = {
|
|
55
|
+
"new-feature": ["为新功能创建 change 骨架。用户说“加个XX”“帮我做XX”“改一下XX”等提出需求时使用;先影响探测判级(lite/完整)。",
|
|
56
|
+
"# 新功能流程\n\n1. 与用户确认 change-id(小写中横线)。\n2. 运行 `ai new <id>`(小需求 `--lite`)。\n3. 按产品规格工程师手册补全 proposal 与验收用例,答掉待确认问题。\n4. `ai check <id>` 通过后,输出带级别与判级理由的确认单,等用户确认。\n5. 把 `ai confirm <id>` 交给用户本人运行(AI 禁止代跑;ship 的前置)。\n"],
|
|
57
|
+
"ready-check": ["校验 change 是否可请求确认。用户说“检查一下”“可以确认了吗”时使用。",
|
|
58
|
+
"# 就绪校验\n\n运行 `ai check <id>`;失败逐项修复后重跑,通过后向用户输出确认单;把 `ai confirm <id>` 交给用户本人运行。\n"],
|
|
59
|
+
"release-ship": ["发布门禁。用户说“测一下”“能上线吗”“发布”时使用。",
|
|
60
|
+
"# 发布流程\n\n1. `ai test <change-id>` 跑测试(报告自动写入 test-results/<change-id>/,与其他变更隔离)。\n2. `ai ship <id>` 过证据门禁。\n3. 按 `.ai/rules/53-release.md` 清单过与本次变更相关的项。\n4. 高风险变更建议在新会话用 `.ai/templates/review-prompt.md` 做独立审查。\n"],
|
|
61
|
+
};
|
|
62
|
+
for (const [name, [desc, body]] of Object.entries(skills)) {
|
|
63
|
+
write(path.join(root, ".claude", "skills", name, "SKILL.md"), `---\nname: ${name}\ndescription: ${desc}\n---\n\n${body}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. workbuddy-skills/(自包含:内嵌被引用的 rules 与 dev 栈手册快照)
|
|
67
|
+
const NOTE = "\n\n---\n\n# 附录:内嵌快照\n\n> 本技能自包含;框架更新后需重跑 `ai sync --force` (项目级技能就地生效)。\n> 正文中的命令仅在安装了控制系统的项目内可执行。\n\n";
|
|
68
|
+
const embed = (content) => {
|
|
69
|
+
const refs = [...new Set([...content.matchAll(/\.ai\/(rules\/[0-9A-Za-z._-]+\.md|agents\/dev\/[a-z]+\.md)/g)].map((m) => m[1]))];
|
|
70
|
+
const parts = refs
|
|
71
|
+
.map((r) => path.join(ai, r))
|
|
72
|
+
.filter((p) => fs.existsSync(p))
|
|
73
|
+
.map((p) => `## 规则快照:${path.relative(ai, p)}\n\n${fs.readFileSync(p, "utf8").trim()}\n`);
|
|
74
|
+
return parts.length ? content + NOTE + parts.join("\n") : content;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const contract = fs.readFileSync(path.join(root, "AGENTS.md"), "utf8");
|
|
78
|
+
write(path.join(root, ".workbuddy", "skills", "ai-control-contract", "SKILL.md"),
|
|
79
|
+
`---\nname: ai-control-contract\ndescription: AI 全栈控制系统总契约:红线、两个确认点、任务分级、验收契约。在装有本控制系统的项目内开发时必须先应用。\nversion: 2.0.0\ntags: contract, workflow, gates\n---\n\n${embed(contract)}`);
|
|
80
|
+
for (const f of agentFiles) {
|
|
81
|
+
const content = fs.readFileSync(path.join(agentsDir, f), "utf8");
|
|
82
|
+
const id = f.replace(/\.md$/, "");
|
|
83
|
+
const role = (content.match(/^# ([^(\n]+)/) || [, id])[1].trim();
|
|
84
|
+
const duty = firstLine(sec(content, "职责"));
|
|
85
|
+
write(path.join(root, ".workbuddy", "skills", id, "SKILL.md"),
|
|
86
|
+
`---\nname: ${id}\ndescription: ${role}。${duty}\nversion: 2.0.0\ntags: ai-control, ${id.replace("agent-", "")}\n---\n\n${embed(content)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`SYNC_OK written=${written.length} skipped=${skipped.length}`);
|
|
90
|
+
written.forEach((p) => console.log(` + ${p}`));
|
|
91
|
+
console.log("WorkBuddy:项目级技能已写入 .workbuddy/skills/,重启 WorkBuddy 生效(无需复制;若曾装过 v1 全局技能,请从 ~/.workbuddy/skills/ 删除 agent-architect/agent-release 等残留)。Codex/Cursor/Kimi/Qoder 原生读 AGENTS.md 无需操作。");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 合并 PreToolUse 钩子进 .claude/settings.json:只追加缺失项,保留用户已有内容。
|
|
95
|
+
// 返回“钩子确实已就位”(写后读回校验);settings.json 非法 JSON 时不动它并返回 false。
|
|
96
|
+
function mergeClaudeSettings(root, written, skipped) {
|
|
97
|
+
const p = path.join(root, ".claude", "settings.json");
|
|
98
|
+
const OURS = [
|
|
99
|
+
{ matcher: "Write|Edit|MultiEdit|NotebookEdit",
|
|
100
|
+
hooks: [{ type: "command", command: "node .ai/hooks/guard-write.js" }] },
|
|
101
|
+
{ matcher: "Bash",
|
|
102
|
+
hooks: [{ type: "command", command: "node .ai/hooks/guard-bash.js" }] },
|
|
103
|
+
];
|
|
104
|
+
let obj = {};
|
|
105
|
+
if (fs.existsSync(p)) {
|
|
106
|
+
try { obj = JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
107
|
+
catch {
|
|
108
|
+
console.error("警告:.claude/settings.json 不是合法 JSON,已跳过 hooks 注入(未改动该文件)。");
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
obj.hooks = obj.hooks || {};
|
|
113
|
+
obj.hooks.PreToolUse = obj.hooks.PreToolUse || [];
|
|
114
|
+
const have = JSON.stringify(obj.hooks.PreToolUse);
|
|
115
|
+
let added = 0;
|
|
116
|
+
for (const entry of OURS) {
|
|
117
|
+
if (!have.includes(entry.hooks[0].command)) { obj.hooks.PreToolUse.push(entry); added++; }
|
|
118
|
+
}
|
|
119
|
+
if (added) {
|
|
120
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
121
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
|
|
122
|
+
written.push(path.relative(root, p) + `(合并 ${added} 条钩子,用户配置保留)`);
|
|
123
|
+
} else {
|
|
124
|
+
skipped.push(path.relative(root, p) + "(钩子已在)");
|
|
125
|
+
}
|
|
126
|
+
// 写后读回校验
|
|
127
|
+
try {
|
|
128
|
+
return JSON.stringify(JSON.parse(fs.readFileSync(p, "utf8"))).includes("guard-write.js");
|
|
129
|
+
} catch { return false; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { sync };
|
package/bin/ai.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ai —— 控制系统统一入口(npm 全局命令)
|
|
3
|
+
//
|
|
4
|
+
// ai init [--stack <java|go|php|vue|react|node|mixed>] [--update|--force] 安装/更新到当前项目
|
|
5
|
+
// ai new <id> [--lite|--upgrade] 建变更骨架(lite 两件套)
|
|
6
|
+
// ai check <id> 契约门禁:影响范围/lite越界/用例覆盖/待确认
|
|
7
|
+
// ai confirm <id> 用户确认后留痕(by/at/sha),ship 的前置
|
|
8
|
+
// ai test [<id>] 跑项目测试(带 id 时报告按变更隔离)
|
|
9
|
+
// ai ship <id> [报告目录...] 证据门禁:报告直查 + 高风险提示
|
|
10
|
+
// ai sync [--force] 导出 Claude/WorkBuddy 适配物
|
|
11
|
+
// ai doctor 环境体检
|
|
12
|
+
|
|
13
|
+
const { die, requireProjectRoot, PKG_ROOT } = require("../lib/core");
|
|
14
|
+
|
|
15
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
switch (cmd) {
|
|
18
|
+
case "init":
|
|
19
|
+
require("../lib/init").init(args);
|
|
20
|
+
break;
|
|
21
|
+
case "new":
|
|
22
|
+
require("../lib/change").cmdNew(args);
|
|
23
|
+
break;
|
|
24
|
+
case "confirm":
|
|
25
|
+
require("../lib/gate").cmdConfirm(args);
|
|
26
|
+
break;
|
|
27
|
+
case "check":
|
|
28
|
+
require("../lib/gate").cmdCheck(args);
|
|
29
|
+
break;
|
|
30
|
+
case "test":
|
|
31
|
+
require("../lib/testrun").cmdTest(args);
|
|
32
|
+
break;
|
|
33
|
+
case "ship":
|
|
34
|
+
require("../lib/gate").cmdShip(args);
|
|
35
|
+
break;
|
|
36
|
+
case "sync":
|
|
37
|
+
require("../addons/export-adapters").sync(requireProjectRoot(), args);
|
|
38
|
+
break;
|
|
39
|
+
case "doctor":
|
|
40
|
+
require("../lib/doctor").cmdDoctor();
|
|
41
|
+
break;
|
|
42
|
+
case "version":
|
|
43
|
+
case "--version":
|
|
44
|
+
case "-v":
|
|
45
|
+
console.log(require(`${PKG_ROOT}/package.json`).version);
|
|
46
|
+
break;
|
|
47
|
+
case "help":
|
|
48
|
+
case "--help":
|
|
49
|
+
case "-h":
|
|
50
|
+
case undefined: {
|
|
51
|
+
const fs = require("fs");
|
|
52
|
+
const self = fs.readFileSync(__filename, "utf8");
|
|
53
|
+
console.log(self.split("\n").slice(1, 11).map((l) => l.replace(/^\/\/ ?/, "")).join("\n"));
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
die(`未知命令: ${cmd}(ai help 查看用法)`);
|
|
58
|
+
}
|
package/lib/change.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ai new:建变更骨架(lite 两件套 / 完整三件;--upgrade 平滑升级保留内容)
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { requireProjectRoot, changeDir, die, exists, read, renderTemplate } = require("./core");
|
|
5
|
+
|
|
6
|
+
function writeIfAbsent(p, content) {
|
|
7
|
+
if (exists(p)) {
|
|
8
|
+
console.log(`已存在,跳过: ${p}`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
12
|
+
fs.writeFileSync(p, content);
|
|
13
|
+
console.log(`[WRITE] ${p}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cmdNew(args) {
|
|
17
|
+
const id = args.find((a) => !a.startsWith("--"));
|
|
18
|
+
if (!id || !/^[a-z0-9][a-z0-9-]*$/.test(id)) die("用法: ai new <change-id>(小写中横线)[--lite|--upgrade]");
|
|
19
|
+
const root = requireProjectRoot();
|
|
20
|
+
const dir = changeDir(root, id);
|
|
21
|
+
const vars = { CHANGE_ID: id };
|
|
22
|
+
|
|
23
|
+
const lite = args.includes("--lite");
|
|
24
|
+
if (args.includes("--upgrade")) return upgrade(root, dir, id, vars);
|
|
25
|
+
writeIfAbsent(path.join(dir, "proposal.md"), renderTemplate(lite ? "proposal-lite.md" : "proposal.md", vars));
|
|
26
|
+
let casesTpl = renderTemplate("test-cases.md", vars);
|
|
27
|
+
if (lite) casesTpl = casesTpl.replace("| 单测 |", "| 手动 |"); // lite 常态是无自动化测试的小改动
|
|
28
|
+
writeIfAbsent(path.join(dir, "test-cases.md"), casesTpl);
|
|
29
|
+
if (!lite) {
|
|
30
|
+
writeIfAbsent(path.join(dir, "specs", id, "spec.md"), renderTemplate("spec.md", vars));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("复制给 AI 助手:");
|
|
35
|
+
console.log(`请读取 .ai/changes/${id}${lite ? "(lite 变更)" : ""}。`);
|
|
36
|
+
if (lite) {
|
|
37
|
+
console.log("补全需求说明、影响文件和验收用例,答掉待确认问题。");
|
|
38
|
+
console.log(`本变更不涉及数据库和 API 契约;如发现需要涉及,停止并运行 ai new ${id} --upgrade 升级(保留已写内容),升级后重新请求确认。`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log("先做影响探测(检索代码列出触碰的文件/表/接口),据此补全影响范围、规格场景和验收用例,答掉待确认问题。");
|
|
41
|
+
console.log("涉及数据库先走数据库工程师两阶段确认(.ai/agents/agent-dba.md);涉及跨模块/接口兼容按模板补建 design.md。");
|
|
42
|
+
}
|
|
43
|
+
console.log("不要直接写代码;确认单需标注级别与判级理由,请用户确认后由用户本人运行 ai confirm " + id + "(AI 不代跑)。");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function upgrade(root, dir, id, vars) {
|
|
47
|
+
if (!exists(dir)) die(`change 不存在,无法升级: ${id}`);
|
|
48
|
+
const proposalPath = path.join(dir, "proposal.md");
|
|
49
|
+
if (exists(proposalPath)) {
|
|
50
|
+
const s = read(proposalPath);
|
|
51
|
+
if (/^变更级别:\s*lite\s*\n?/m.test(s)) {
|
|
52
|
+
fs.writeFileSync(proposalPath, s.replace(/^变更级别:\s*lite\s*\n?/m, ""));
|
|
53
|
+
console.log("已移除 lite 标记。");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
writeIfAbsent(path.join(dir, "specs", id, "spec.md"), renderTemplate("spec.md", vars));
|
|
57
|
+
console.log("已升级为完整流程:既有 proposal/test-cases 全部保留。");
|
|
58
|
+
console.log("注意:影响范围已变化,必须重新经用户确认(ai confirm);涉及数据库先走两阶段确认。");
|
|
59
|
+
console.log("升级即意味着涉及数据库/API:请把关键用例改为自动化(非手动),否则 check 会拦——全手动会让证据门禁失效。");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { cmdNew };
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 公共函数:项目根定位、change 目录、YAML 段解析
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
6
|
+
const PAYLOAD = path.join(PKG_ROOT, "payload");
|
|
7
|
+
|
|
8
|
+
// 从 cwd 向上找 .ai/ 定位项目根
|
|
9
|
+
function findProjectRoot(from = process.cwd()) {
|
|
10
|
+
let dir = from;
|
|
11
|
+
while (true) {
|
|
12
|
+
if (fs.existsSync(path.join(dir, ".ai"))) return dir;
|
|
13
|
+
const parent = path.dirname(dir);
|
|
14
|
+
if (parent === dir) return null;
|
|
15
|
+
dir = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireProjectRoot() {
|
|
20
|
+
const root = findProjectRoot();
|
|
21
|
+
if (!root) die("当前目录不在已安装项目内(未找到 .ai/)。先运行: ai init --stack <java|vue|react|go|php>");
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function changeDir(root, id) {
|
|
26
|
+
return path.join(root, ".ai", "changes", id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 读取 .ai/config.json(缺失或解析失败返回空对象)
|
|
30
|
+
function loadConfig(root) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(path.join(root, ".ai", "config.json"), "utf8"));
|
|
33
|
+
} catch { return {}; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function die(msg, code = 1) {
|
|
37
|
+
console.error(`[FAIL] ${msg}`);
|
|
38
|
+
process.exit(code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function read(p) {
|
|
42
|
+
return fs.readFileSync(p, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function exists(p) {
|
|
46
|
+
return fs.existsSync(p);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 解析 proposal 影响范围里某个 key 的条目;返回非 none 的条目数组。
|
|
50
|
+
// 兼容两种 YAML 写法:行内标量(key: none / key: t_order)与块列表(key:\n - xxx)。
|
|
51
|
+
// "无内容"的写法宽容匹配:none/None/无/暂无/N/A(P1-1:格式误伤会不断制造假失败)
|
|
52
|
+
const NONE_RE = /^[-~]?$|^(none|无|暂无|n\/a)$/i;
|
|
53
|
+
function scopeItems(proposalText, key) {
|
|
54
|
+
const re = new RegExp(`^[ \\t]*${key}:[ \\t]*([^\\n]*)\\n?((?:[ \\t]*-[ \\t]*[^\\n]*\\n?)*)`, "m");
|
|
55
|
+
const m = proposalText.match(re);
|
|
56
|
+
if (!m) return null; // 字段缺失
|
|
57
|
+
const items = [];
|
|
58
|
+
if (m[1] && m[1].trim()) items.push(m[1].trim());
|
|
59
|
+
for (const l of (m[2] || "").split("\n")) {
|
|
60
|
+
const v = l.replace(/^[ \t]*-[ \t]*/, "").trim();
|
|
61
|
+
if (v) items.push(v);
|
|
62
|
+
}
|
|
63
|
+
return items.filter((v) => !NONE_RE.test(v.replace(/[`'"]/g, "").trim()));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isLite(proposalText) {
|
|
67
|
+
return /^变更级别:\s*lite/m.test(proposalText);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 渲染模板:{{KEY}} 替换
|
|
71
|
+
function renderTemplate(name, vars) {
|
|
72
|
+
let s = read(path.join(PAYLOAD, "templates", name));
|
|
73
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
74
|
+
s = s.split(`{{${k}}}`).join(v);
|
|
75
|
+
}
|
|
76
|
+
return s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 递归拷贝目录
|
|
80
|
+
function copyDir(src, dst) {
|
|
81
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
82
|
+
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
83
|
+
const s = path.join(src, e.name);
|
|
84
|
+
const d = path.join(dst, e.name);
|
|
85
|
+
if (e.isDirectory()) copyDir(s, d);
|
|
86
|
+
else fs.copyFileSync(s, d);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
PKG_ROOT, PAYLOAD,
|
|
92
|
+
findProjectRoot, requireProjectRoot, changeDir, loadConfig,
|
|
93
|
+
die, read, exists, scopeItems, isLite, renderTemplate, copyDir,
|
|
94
|
+
};
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ai doctor:环境与安装体检。原则"信任但验证"——sync 说装好了不算数,逐项实测。
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const { findProjectRoot, loadConfig, exists } = require("./core");
|
|
6
|
+
|
|
7
|
+
function cmdDoctor() {
|
|
8
|
+
const ok = (b, label, fix) => console.log(`${b ? "OK " : "FAIL"} ${label}${b || !fix ? "" : `——${fix}`}`);
|
|
9
|
+
let bad = 0;
|
|
10
|
+
const check = (b, label, fix) => { ok(b, label, fix); if (!b) bad++; };
|
|
11
|
+
|
|
12
|
+
// 环境
|
|
13
|
+
const [maj, min] = process.versions.node.split(".").map(Number);
|
|
14
|
+
check(maj > 18 || (maj === 18 && min >= 17), `node ${process.version}(需 ≥18.17)`, "升级 Node");
|
|
15
|
+
let hasGit = true;
|
|
16
|
+
try { execSync("command -v git", { stdio: "ignore", shell: true }); } catch { hasGit = false; }
|
|
17
|
+
check(hasGit, "git", "安装 git(confirm 留痕与独立审查需要)");
|
|
18
|
+
|
|
19
|
+
// 安装
|
|
20
|
+
const root = findProjectRoot();
|
|
21
|
+
if (!root) {
|
|
22
|
+
console.log("FAIL 未安装(未找到 .ai/)——运行 ai init");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
console.log(`OK 项目根: ${root}`);
|
|
26
|
+
const cfg = loadConfig(root);
|
|
27
|
+
check(exists(path.join(root, "AGENTS.md")), "AGENTS.md 契约", "ai init --update 恢复");
|
|
28
|
+
console.log(`OK stack=${cfg.stack || "unknown"} testCommand=${cfg.testCommand || "(自动探测)"}`);
|
|
29
|
+
|
|
30
|
+
// 钩子体检:文件在不在、settings.json 里是不是真挂上了(防"文案说启用、实际没装")
|
|
31
|
+
const hookFiles = ["guard-write.js", "guard-bash.js"].every((f) => exists(path.join(root, ".ai", "hooks", f)));
|
|
32
|
+
check(hookFiles, ".ai/hooks/ 守卫脚本", "ai init --update 恢复");
|
|
33
|
+
const settingsPath = path.join(root, ".claude", "settings.json");
|
|
34
|
+
if (exists(settingsPath)) {
|
|
35
|
+
let wired = false, legal = true;
|
|
36
|
+
try { wired = JSON.stringify(JSON.parse(fs.readFileSync(settingsPath, "utf8"))).includes("guard-write.js"); }
|
|
37
|
+
catch { legal = false; }
|
|
38
|
+
check(legal, ".claude/settings.json 是合法 JSON", "修复语法后重跑 ai sync");
|
|
39
|
+
if (legal) check(wired, "PreToolUse 钩子已挂载(Claude Code 写入/执行瞬间拦截)", "运行 ai sync 合并钩子");
|
|
40
|
+
} else {
|
|
41
|
+
console.log("提示 未生成 Claude Code 适配(不用 Claude Code 可忽略;否则运行 ai sync)");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 变更概览:确认状态与失效检测
|
|
45
|
+
const changesDir = path.join(root, ".ai", "changes");
|
|
46
|
+
const ids = exists(changesDir)
|
|
47
|
+
? fs.readdirSync(changesDir).filter((d) => { try { return fs.statSync(path.join(changesDir, d)).isDirectory(); } catch { return false; } })
|
|
48
|
+
: [];
|
|
49
|
+
let confirmed = 0, stale = 0;
|
|
50
|
+
for (const id of ids) {
|
|
51
|
+
const cp = path.join(changesDir, id, "confirmed.json");
|
|
52
|
+
if (!exists(cp)) continue;
|
|
53
|
+
confirmed++;
|
|
54
|
+
try {
|
|
55
|
+
const at = Date.parse(JSON.parse(fs.readFileSync(cp, "utf8")).at) || 0;
|
|
56
|
+
for (const f of ["proposal.md", "test-cases.md"]) {
|
|
57
|
+
const p = path.join(changesDir, id, f);
|
|
58
|
+
if (exists(p) && fs.statSync(p).mtimeMs > at) { stale++; console.log(`FAIL 变更 ${id}: 确认后 ${f} 被修改——确认已失效,需重新 ai confirm`); bad++; break; }
|
|
59
|
+
}
|
|
60
|
+
} catch { console.log(`FAIL 变更 ${id}: confirmed.json 无效`); bad++; }
|
|
61
|
+
}
|
|
62
|
+
console.log(`OK 变更: ${ids.length} 个(已确认 ${confirmed},确认失效 ${stale})`);
|
|
63
|
+
|
|
64
|
+
console.log(bad ? `\nDOCTOR_FAIL:${bad} 项需处理。` : "\nDOCTOR_OK:全部体检通过。");
|
|
65
|
+
if (bad) process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { cmdDoctor };
|