@andyqiu/codeforge 0.3.5 → 0.3.7
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/commands/refactor.md +79 -0
- package/commands/review.md +66 -0
- package/commands/tdd.md +91 -0
- package/dist/index.js +289 -6
- package/package.json +5 -1
- package/workflows/code-review.yaml +66 -0
- package/workflows/refactor.yaml +106 -0
- package/workflows/tdd.yaml +99 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 安全重构工作流,先补特征测试再重构,保证行为不变
|
|
3
|
+
agent: planner
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
codeforge 元数据(opencode 不读,由 plugins / workflow-engine 解析):
|
|
8
|
+
name: refactor
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
trigger_workflow: refactor
|
|
11
|
+
requires_human_approval: true
|
|
12
|
+
allowed_tools: smart_search, save_chat_insight, pending-changes, repo-map, nav-find, nav-goto, bash
|
|
13
|
+
说明:workflow-engine 看到 trigger_workflow=refactor 拦截 /refactor 触发对应 YAML
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# /refactor — 安全重构工作流
|
|
18
|
+
|
|
19
|
+
触发 `workflows/refactor.yaml`。核心原则:**先补 characterization test 锁定现状行为,再重构**(参考 Working Effectively with Legacy Code)。
|
|
20
|
+
|
|
21
|
+
## 输入
|
|
22
|
+
|
|
23
|
+
用户需求:$ARGUMENTS
|
|
24
|
+
|
|
25
|
+
## 当前仓库改动概览(自动注入)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
!`git diff --stat`
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 用法
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
/refactor lib/foo.ts 里的 doBar 函数
|
|
35
|
+
/refactor 把 user-service 缓存层抽出来
|
|
36
|
+
/refactor #234 提到的 N+1 query
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 流程(7 步,由 refactor.yaml 编排)
|
|
40
|
+
|
|
41
|
+
| 步骤 | Agent | 动作 | 是否需审批 |
|
|
42
|
+
|------|------|------|-----------|
|
|
43
|
+
| 1. 规划 | planner | 出重构方案 + smart_search 历史经验 | 否 |
|
|
44
|
+
| 2. 补特征测试 | coder | 先写 characterization tests(锁定现状) | 否,auto_feedback 自纠 |
|
|
45
|
+
| 3. 确认覆盖 | reviewer | 确认测试覆盖待重构代码路径 | 否 |
|
|
46
|
+
| 4. 重构 | coder | 实施重构,每步保证测试仍绿 | 否,auto_feedback 自纠 |
|
|
47
|
+
| 5. 审阅 | reviewer | 确认行为不变 + 代码更清晰 | 否 |
|
|
48
|
+
| 6. 落地 | coder | `pending-changes apply_all` | **必审批**(YAML 写死) |
|
|
49
|
+
| 7. 沉淀 | reviewer | `save_chat_insight` 把经验回写 KH | 否(写失败 skip) |
|
|
50
|
+
|
|
51
|
+
## 与 /tdd 的区别
|
|
52
|
+
|
|
53
|
+
| 维度 | /refactor | /tdd |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| 起点 | 已有可工作代码(可能很烂) | 新功能从零开始 |
|
|
56
|
+
| 测试角色 | 锁定现状(characterization test) | 驱动设计(先红再绿) |
|
|
57
|
+
| 终极目标 | 行为不变 + 代码更清晰 | 实现新功能 + 测试覆盖 |
|
|
58
|
+
|
|
59
|
+
## 自主度(AGENTS.md C18)
|
|
60
|
+
|
|
61
|
+
`/refactor` 默认 `semi`:风险动作(apply / bash)弹审批,但 `auto_feedback` 在测试反复失败时**自动 escalate 给 reviewer**(避免 coder 死循环)。
|
|
62
|
+
|
|
63
|
+
## 失败处理
|
|
64
|
+
|
|
65
|
+
| 失败位置 | 行为 |
|
|
66
|
+
|---------|------|
|
|
67
|
+
| step 1 (规划) | abort |
|
|
68
|
+
| step 2 (补测试) | auto_feedback 3 轮重试,仍败 escalate reviewer |
|
|
69
|
+
| step 3 (覆盖不足) | REQUEST_CHANGES → 回 step 2 重补,max_loops=3 兜底 |
|
|
70
|
+
| step 4 (重构) | auto_feedback 5 轮重试,仍败 escalate reviewer |
|
|
71
|
+
| step 5 (审阅 REQUEST_CHANGES) | 回 step 4 重做 |
|
|
72
|
+
| step 6 (apply) | hash 漂移则拒(防脏覆盖) |
|
|
73
|
+
| step 7 (沉淀) | skip |
|
|
74
|
+
|
|
75
|
+
## 元数据
|
|
76
|
+
|
|
77
|
+
- `agent`: `planner` —— 起手让 planner 出方案
|
|
78
|
+
- `trigger_workflow`: `refactor`
|
|
79
|
+
- `requires_human_approval`: `true` —— 高影响命令,CLI/UI 额外确认
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 审阅 PR / diff / 本地未提交改动,输出 APPROVE/REQUEST_CHANGES/BLOCK
|
|
3
|
+
agent: reviewer
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
codeforge 元数据(opencode 不读,由 plugins / workflow-engine 解析):
|
|
8
|
+
name: review
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
trigger_workflow: code-review
|
|
11
|
+
allowed_tools: smart_search, save_chat_insight, pending-changes, bash, repo-map, nav-find
|
|
12
|
+
说明:workflow-engine 看到 trigger_workflow=code-review 拦截 /review 触发对应 YAML
|
|
13
|
+
-->
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# /review — 代码审阅工作流
|
|
17
|
+
|
|
18
|
+
触发 `workflows/code-review.yaml`。**只读流程**:不 stage 任何改动,不 apply 任何 pending。
|
|
19
|
+
|
|
20
|
+
## 输入
|
|
21
|
+
|
|
22
|
+
用户需求:$ARGUMENTS
|
|
23
|
+
|
|
24
|
+
## 当前仓库改动概览(自动注入)
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
!`git diff --stat`
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 用法
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/review # 审本地未提交改动(默认)
|
|
34
|
+
/review <PR URL> # 审指定 PR
|
|
35
|
+
/review --base main # 审当前分支与 main 的 diff
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 流程(4 步,由 code-review.yaml 编排)
|
|
39
|
+
|
|
40
|
+
| 步骤 | Agent | 动作 | 是否需审批 |
|
|
41
|
+
|------|------|------|-----------|
|
|
42
|
+
| 1. 准备 diff | reviewer | 取 diff 上下文 + smart_search 历史经验 | 否 |
|
|
43
|
+
| 2. 出审阅维度 | planner | 列出本次必查维度(安全/性能/可维护性/测试/风格) | 否 |
|
|
44
|
+
| 3. 审阅 | reviewer | 按维度逐项审 + 跑测试 + 给 APPROVE/REQUEST_CHANGES/BLOCK | 否 |
|
|
45
|
+
| 4. 沉淀 | reviewer | 把发现的 gotcha 沉淀回 KH | 否(写失败 skip) |
|
|
46
|
+
|
|
47
|
+
## 与 /plan / /ship 的关系
|
|
48
|
+
|
|
49
|
+
- `/plan` = 规划(不写代码)
|
|
50
|
+
- `/ship` = 全套(规划 → coder → review → apply → 沉淀)
|
|
51
|
+
- `/review` = **只审,不动手**:用于 PR review、code review 例会、入职新人代码巡检
|
|
52
|
+
|
|
53
|
+
## 失败处理
|
|
54
|
+
|
|
55
|
+
| 失败位置 | 行为 |
|
|
56
|
+
|---------|------|
|
|
57
|
+
| step 1 (准备 diff) | abort:取不到 diff 后续无意义 |
|
|
58
|
+
| step 2 (出维度) | abort:planner 出不来维度则无可执行清单 |
|
|
59
|
+
| step 3 (审阅 BLOCK) | abort:严重问题,用户决定是否继续 |
|
|
60
|
+
| step 4 (沉淀) | skip:KH 写失败不能让审阅结果"看起来失败" |
|
|
61
|
+
|
|
62
|
+
## 元数据
|
|
63
|
+
|
|
64
|
+
- `agent`: `reviewer` —— 起手就切到 reviewer
|
|
65
|
+
- `trigger_workflow`: `code-review`
|
|
66
|
+
- 不像 `/ship`,本命令**不需要 human_approval**(因为只读,无副作用)
|
package/commands/tdd.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TDD 严格流程 RED → GREEN → REFACTOR,测试驱动新功能
|
|
3
|
+
agent: planner
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
codeforge 元数据(opencode 不读,由 plugins / workflow-engine 解析):
|
|
8
|
+
name: tdd
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
trigger_workflow: tdd
|
|
11
|
+
requires_human_approval: true
|
|
12
|
+
allowed_tools: smart_search, save_chat_insight, pending-changes, repo-map, nav-find, nav-goto, bash
|
|
13
|
+
说明:workflow-engine 看到 trigger_workflow=tdd 拦截 /tdd 触发对应 YAML
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# /tdd — Test-Driven Development 工作流
|
|
18
|
+
|
|
19
|
+
触发 `workflows/tdd.yaml`。严格 **RED → GREEN → REFACTOR** 三步循环,避免"先写实现再补测试"反 TDD。
|
|
20
|
+
|
|
21
|
+
## 输入
|
|
22
|
+
|
|
23
|
+
用户需求:$ARGUMENTS
|
|
24
|
+
|
|
25
|
+
## 当前仓库改动概览(自动注入)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
!`git diff --stat`
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 用法
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
/tdd 给 lib/utils 加一个 deepMerge 函数
|
|
35
|
+
/tdd 实现 #345:用户登录 24h 内失败 5 次自动锁定
|
|
36
|
+
/tdd 给 Channel 加 LarkChannel 子类型
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 流程(7 步,由 tdd.yaml 编排,max_loops=5)
|
|
40
|
+
|
|
41
|
+
| 步骤 | Agent | 动作 | 是否需审批 |
|
|
42
|
+
|------|------|------|-----------|
|
|
43
|
+
| 1. 拆需求 | planner | 把需求拆成可测的小步骤清单 | 否 |
|
|
44
|
+
| 2. 写测试-RED | coder | 先写测试(不写实现) | 否 |
|
|
45
|
+
| 3. 验证 RED | reviewer | 跑 npm test 确认测试**确实红了**(绿则 BLOCK) | 否 |
|
|
46
|
+
| 4. 最小实现-GREEN | coder | 写最小实现让测试变绿(不超出需要) | 否,auto_feedback 自纠 |
|
|
47
|
+
| 5. 重构-REFACTOR | coder | 可选 refactor,测试必须仍绿 | 否,可 skip |
|
|
48
|
+
| 6. 审阅 | reviewer | 确认三步全做 + 测试覆盖核心 | 否 |
|
|
49
|
+
| 7. 落地 | coder | `pending-changes apply_all` | **必审批**(YAML 写死) |
|
|
50
|
+
|
|
51
|
+
## TDD 三色循环
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌─────────────────────────────────────────────────────┐
|
|
55
|
+
│ RED → 写一个失败的测试(描述期望行为) │
|
|
56
|
+
│ GREEN → 写最简实现让测试通过(不超出测试需要) │
|
|
57
|
+
│ REFACT → 在测试保护下重构(可选;测试仍绿才能进下一轮) │
|
|
58
|
+
└─────────────────────────────────────────────────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`max_loops=5` 表示 reviewer REQUEST_CHANGES 时最多回到 step 2 重做 5 轮,防止死循环。
|
|
62
|
+
|
|
63
|
+
## 与 /ship / /refactor 的区别
|
|
64
|
+
|
|
65
|
+
| 维度 | /ship | /refactor | /tdd |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| 起点 | 任何需求 | 已有代码(改善结构) | 新功能(从零) |
|
|
68
|
+
| 测试态度 | 写完后测 | 改前先锁定 | **先红再绿** |
|
|
69
|
+
| 适用 | 大部分日常 | 技术债清理 | 新功能 + 高质量要求 |
|
|
70
|
+
|
|
71
|
+
## 自主度
|
|
72
|
+
|
|
73
|
+
`/tdd` 默认 `semi`:apply 必审批;其余步骤 auto_feedback 自纠。
|
|
74
|
+
|
|
75
|
+
## 失败处理
|
|
76
|
+
|
|
77
|
+
| 失败位置 | 行为 |
|
|
78
|
+
|---------|------|
|
|
79
|
+
| step 1 (拆需求) | abort |
|
|
80
|
+
| step 2 (写测试) | retry 1 次 |
|
|
81
|
+
| step 3 (验证 RED 失败,测试是绿的) | BLOCK:测试无效,必须重写 |
|
|
82
|
+
| step 4 (最小实现) | auto_feedback 5 轮 |
|
|
83
|
+
| step 5 (REFACTOR 失败) | skip:refactor 是可选 |
|
|
84
|
+
| step 6 (审阅 REQUEST_CHANGES) | 回 step 2,max_loops=5 |
|
|
85
|
+
| step 7 (apply) | hash 漂移则拒 |
|
|
86
|
+
|
|
87
|
+
## 元数据
|
|
88
|
+
|
|
89
|
+
- `agent`: `planner` —— 起手让 planner 拆需求
|
|
90
|
+
- `trigger_workflow`: `tdd`
|
|
91
|
+
- `requires_human_approval`: `true`
|
package/dist/index.js
CHANGED
|
@@ -8965,6 +8965,7 @@ import { promises as fs2 } from "node:fs";
|
|
|
8965
8965
|
import * as path4 from "node:path";
|
|
8966
8966
|
|
|
8967
8967
|
// lib/channels.ts
|
|
8968
|
+
import { createHmac } from "node:crypto";
|
|
8968
8969
|
var TEMPLATE_RE = /\$\{([a-zA-Z_][a-zA-Z0-9_.]*)(?:\|([^}]*))?\}/g;
|
|
8969
8970
|
function renderChannelTemplate(template, ev) {
|
|
8970
8971
|
const missing = new Set;
|
|
@@ -9078,6 +9079,10 @@ async function sendOne(ev, ch, deps) {
|
|
|
9078
9079
|
return await sendKh(ev, ch, deps);
|
|
9079
9080
|
case "mcp":
|
|
9080
9081
|
return await sendMcp(ev, ch, deps);
|
|
9082
|
+
case "slack":
|
|
9083
|
+
return await sendSlack(ev, ch, deps);
|
|
9084
|
+
case "lark":
|
|
9085
|
+
return await sendLark(ev, ch, deps);
|
|
9081
9086
|
}
|
|
9082
9087
|
}
|
|
9083
9088
|
async function sendWebhook(ev, ch, deps) {
|
|
@@ -9217,6 +9222,224 @@ async function sendMcp(ev, ch, deps) {
|
|
|
9217
9222
|
error: r.ok ? undefined : r.error ?? "mcp 调用失败"
|
|
9218
9223
|
};
|
|
9219
9224
|
}
|
|
9225
|
+
function transformSlackToWebhook(ch, ev) {
|
|
9226
|
+
const missing = new Set;
|
|
9227
|
+
const titleTpl = ch.title_template ?? "${title|}";
|
|
9228
|
+
const msgTpl = ch.message_template ?? "${message|}";
|
|
9229
|
+
const title = renderChannelTemplate(titleTpl, ev);
|
|
9230
|
+
const message = renderChannelTemplate(msgTpl, ev);
|
|
9231
|
+
title.missing.forEach((k) => missing.add(k));
|
|
9232
|
+
message.missing.forEach((k) => missing.add(k));
|
|
9233
|
+
const titleText = title.rendered.trim() ? title.rendered : ev.event;
|
|
9234
|
+
const mentionText = (ch.mentions ?? []).map((id) => id.startsWith("@") || id.startsWith("<") ? id : `<@${id}>`).join(" ");
|
|
9235
|
+
const color = severityToSlackColor(ev.severity);
|
|
9236
|
+
const blocks = [
|
|
9237
|
+
{
|
|
9238
|
+
type: "header",
|
|
9239
|
+
text: { type: "plain_text", text: titleText.slice(0, 150), emoji: true }
|
|
9240
|
+
}
|
|
9241
|
+
];
|
|
9242
|
+
if (message.rendered.trim()) {
|
|
9243
|
+
blocks.push({
|
|
9244
|
+
type: "section",
|
|
9245
|
+
text: { type: "mrkdwn", text: message.rendered.slice(0, 3000) }
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
9248
|
+
if (mentionText) {
|
|
9249
|
+
blocks.push({
|
|
9250
|
+
type: "context",
|
|
9251
|
+
elements: [{ type: "mrkdwn", text: mentionText }]
|
|
9252
|
+
});
|
|
9253
|
+
}
|
|
9254
|
+
const footerParts = [`event=\`${ev.event}\``];
|
|
9255
|
+
if (ev.session_id)
|
|
9256
|
+
footerParts.push(`session=\`${ev.session_id.slice(0, 8)}\``);
|
|
9257
|
+
footerParts.push(`ts=<!date^${Math.floor(ev.timestamp / 1000)}^{date_short_pretty} {time}|${new Date(ev.timestamp).toISOString()}>`);
|
|
9258
|
+
blocks.push({
|
|
9259
|
+
type: "context",
|
|
9260
|
+
elements: [{ type: "mrkdwn", text: footerParts.join(" · ") }]
|
|
9261
|
+
});
|
|
9262
|
+
const payload = {
|
|
9263
|
+
attachments: [{ color, blocks }]
|
|
9264
|
+
};
|
|
9265
|
+
if (ch.channel)
|
|
9266
|
+
payload.channel = ch.channel;
|
|
9267
|
+
if (ch.username)
|
|
9268
|
+
payload.username = ch.username;
|
|
9269
|
+
if (ch.icon_emoji)
|
|
9270
|
+
payload.icon_emoji = ch.icon_emoji;
|
|
9271
|
+
return { body: JSON.stringify(payload), missing: [...missing] };
|
|
9272
|
+
}
|
|
9273
|
+
function severityToSlackColor(sev) {
|
|
9274
|
+
if (sev === undefined)
|
|
9275
|
+
return "#cccccc";
|
|
9276
|
+
if (sev >= 40)
|
|
9277
|
+
return "#d50000";
|
|
9278
|
+
if (sev >= 30)
|
|
9279
|
+
return "#e91e63";
|
|
9280
|
+
if (sev >= 20)
|
|
9281
|
+
return "#ff9800";
|
|
9282
|
+
if (sev >= 10)
|
|
9283
|
+
return "#36a64f";
|
|
9284
|
+
return "#cccccc";
|
|
9285
|
+
}
|
|
9286
|
+
async function sendSlack(ev, ch, deps) {
|
|
9287
|
+
if (!deps.fetch) {
|
|
9288
|
+
return { name: ch.name, type: "slack", status: "error", error: "deps.fetch 未注入" };
|
|
9289
|
+
}
|
|
9290
|
+
const { body } = transformSlackToWebhook(ch, ev);
|
|
9291
|
+
try {
|
|
9292
|
+
const resp = await deps.fetch(ch.webhook_url, {
|
|
9293
|
+
method: "POST",
|
|
9294
|
+
headers: { "Content-Type": "application/json" },
|
|
9295
|
+
body
|
|
9296
|
+
});
|
|
9297
|
+
const ok = resp.status >= 200 && resp.status < 300;
|
|
9298
|
+
if (ok) {
|
|
9299
|
+
return {
|
|
9300
|
+
name: ch.name,
|
|
9301
|
+
type: "slack",
|
|
9302
|
+
status: "sent",
|
|
9303
|
+
http_status: resp.status,
|
|
9304
|
+
rendered: body
|
|
9305
|
+
};
|
|
9306
|
+
}
|
|
9307
|
+
return {
|
|
9308
|
+
name: ch.name,
|
|
9309
|
+
type: "slack",
|
|
9310
|
+
status: "error",
|
|
9311
|
+
http_status: resp.status,
|
|
9312
|
+
error: `slack returned ${resp.status}: ${resp.body?.slice(0, 200) ?? ""}`,
|
|
9313
|
+
rendered: body
|
|
9314
|
+
};
|
|
9315
|
+
} catch (err) {
|
|
9316
|
+
return {
|
|
9317
|
+
name: ch.name,
|
|
9318
|
+
type: "slack",
|
|
9319
|
+
status: "error",
|
|
9320
|
+
error: describe2(err),
|
|
9321
|
+
rendered: body
|
|
9322
|
+
};
|
|
9323
|
+
}
|
|
9324
|
+
}
|
|
9325
|
+
function transformLarkToWebhook(ch, ev) {
|
|
9326
|
+
const missing = new Set;
|
|
9327
|
+
const titleTpl = ch.title_template ?? "${title|}";
|
|
9328
|
+
const msgTpl = ch.message_template ?? "${message|}";
|
|
9329
|
+
const titleR = renderChannelTemplate(titleTpl, ev);
|
|
9330
|
+
const msgR = renderChannelTemplate(msgTpl, ev);
|
|
9331
|
+
titleR.missing.forEach((k) => missing.add(k));
|
|
9332
|
+
msgR.missing.forEach((k) => missing.add(k));
|
|
9333
|
+
const titleText = titleR.rendered.trim() || ev.event;
|
|
9334
|
+
const messageText = msgR.rendered.trim();
|
|
9335
|
+
const mentionMarkdown = (ch.mentions ?? []).map((id) => {
|
|
9336
|
+
if (id === "@all" || id === "all")
|
|
9337
|
+
return '<at user_id="all"></at>';
|
|
9338
|
+
if (id.startsWith("<at"))
|
|
9339
|
+
return id;
|
|
9340
|
+
return `<at user_id="${id}"></at>`;
|
|
9341
|
+
}).join(" ");
|
|
9342
|
+
const headerTemplate = severityToLarkHeader(ev.severity);
|
|
9343
|
+
const elements = [];
|
|
9344
|
+
if (messageText || mentionMarkdown) {
|
|
9345
|
+
const fullMsg = [messageText, mentionMarkdown].filter(Boolean).join(`
|
|
9346
|
+
|
|
9347
|
+
`);
|
|
9348
|
+
elements.push({
|
|
9349
|
+
tag: "div",
|
|
9350
|
+
text: { tag: "lark_md", content: fullMsg.slice(0, 3000) }
|
|
9351
|
+
});
|
|
9352
|
+
}
|
|
9353
|
+
const footer = [
|
|
9354
|
+
`**event**: \`${ev.event}\``,
|
|
9355
|
+
ev.session_id ? `**session**: \`${ev.session_id.slice(0, 8)}\`` : null,
|
|
9356
|
+
`**ts**: ${new Date(ev.timestamp).toISOString()}`
|
|
9357
|
+
].filter(Boolean).join(" · ");
|
|
9358
|
+
elements.push({
|
|
9359
|
+
tag: "note",
|
|
9360
|
+
elements: [{ tag: "lark_md", content: footer }]
|
|
9361
|
+
});
|
|
9362
|
+
const payload = {
|
|
9363
|
+
msg_type: "interactive",
|
|
9364
|
+
card: {
|
|
9365
|
+
config: { wide_screen_mode: true },
|
|
9366
|
+
header: {
|
|
9367
|
+
template: headerTemplate,
|
|
9368
|
+
title: { tag: "plain_text", content: titleText.slice(0, 150) }
|
|
9369
|
+
},
|
|
9370
|
+
elements
|
|
9371
|
+
}
|
|
9372
|
+
};
|
|
9373
|
+
return { body: JSON.stringify(payload), missing: [...missing] };
|
|
9374
|
+
}
|
|
9375
|
+
function severityToLarkHeader(sev) {
|
|
9376
|
+
if (sev === undefined)
|
|
9377
|
+
return "grey";
|
|
9378
|
+
if (sev >= 40)
|
|
9379
|
+
return "carmine";
|
|
9380
|
+
if (sev >= 30)
|
|
9381
|
+
return "red";
|
|
9382
|
+
if (sev >= 20)
|
|
9383
|
+
return "orange";
|
|
9384
|
+
if (sev >= 10)
|
|
9385
|
+
return "blue";
|
|
9386
|
+
return "grey";
|
|
9387
|
+
}
|
|
9388
|
+
function computeLarkSign(secret, timestampSec) {
|
|
9389
|
+
const stringToSign = `${timestampSec}
|
|
9390
|
+
${secret}`;
|
|
9391
|
+
const hmac = createHmac("sha256", stringToSign);
|
|
9392
|
+
hmac.update("");
|
|
9393
|
+
return hmac.digest("base64");
|
|
9394
|
+
}
|
|
9395
|
+
async function sendLark(ev, ch, deps) {
|
|
9396
|
+
if (!deps.fetch) {
|
|
9397
|
+
return { name: ch.name, type: "lark", status: "error", error: "deps.fetch 未注入" };
|
|
9398
|
+
}
|
|
9399
|
+
const { body: cardBody } = transformLarkToWebhook(ch, ev);
|
|
9400
|
+
let body = cardBody;
|
|
9401
|
+
if (ch.secret) {
|
|
9402
|
+
const tsSec = Math.floor((deps.now ? deps.now() : Date.now()) / 1000);
|
|
9403
|
+
const sign = computeLarkSign(ch.secret, tsSec);
|
|
9404
|
+
const parsed = JSON.parse(cardBody);
|
|
9405
|
+
parsed.timestamp = String(tsSec);
|
|
9406
|
+
parsed.sign = sign;
|
|
9407
|
+
body = JSON.stringify(parsed);
|
|
9408
|
+
}
|
|
9409
|
+
try {
|
|
9410
|
+
const resp = await deps.fetch(ch.webhook_url, {
|
|
9411
|
+
method: "POST",
|
|
9412
|
+
headers: { "Content-Type": "application/json" },
|
|
9413
|
+
body
|
|
9414
|
+
});
|
|
9415
|
+
const ok = resp.status >= 200 && resp.status < 300;
|
|
9416
|
+
if (ok) {
|
|
9417
|
+
return {
|
|
9418
|
+
name: ch.name,
|
|
9419
|
+
type: "lark",
|
|
9420
|
+
status: "sent",
|
|
9421
|
+
http_status: resp.status,
|
|
9422
|
+
rendered: body
|
|
9423
|
+
};
|
|
9424
|
+
}
|
|
9425
|
+
return {
|
|
9426
|
+
name: ch.name,
|
|
9427
|
+
type: "lark",
|
|
9428
|
+
status: "error",
|
|
9429
|
+
http_status: resp.status,
|
|
9430
|
+
error: `lark returned ${resp.status}: ${resp.body?.slice(0, 200) ?? ""}`,
|
|
9431
|
+
rendered: body
|
|
9432
|
+
};
|
|
9433
|
+
} catch (err) {
|
|
9434
|
+
return {
|
|
9435
|
+
name: ch.name,
|
|
9436
|
+
type: "lark",
|
|
9437
|
+
status: "error",
|
|
9438
|
+
error: describe2(err),
|
|
9439
|
+
rendered: body
|
|
9440
|
+
};
|
|
9441
|
+
}
|
|
9442
|
+
}
|
|
9220
9443
|
function dedupeTags(defaults, evTags) {
|
|
9221
9444
|
const set = new Set;
|
|
9222
9445
|
if (defaults) {
|
|
@@ -11141,6 +11364,51 @@ function applyFocus(ranked, focus) {
|
|
|
11141
11364
|
const rest = others.filter((n) => !isAdj(n.rel));
|
|
11142
11365
|
return [center, ...adj, ...rest];
|
|
11143
11366
|
}
|
|
11367
|
+
function renderMermaid(map, opts = {}) {
|
|
11368
|
+
const top = opts.top ?? 20;
|
|
11369
|
+
const dir = opts.direction ?? "LR";
|
|
11370
|
+
const focus = opts.focus ? toPosix(opts.focus.replace(/^\.\//, "")) : undefined;
|
|
11371
|
+
const sorted = [...map.ranked].sort((a, b) => b.score - a.score).slice(0, top);
|
|
11372
|
+
const idMap = new Map;
|
|
11373
|
+
for (const f of sorted) {
|
|
11374
|
+
idMap.set(f.rel, "n" + sha1Short(f.rel));
|
|
11375
|
+
}
|
|
11376
|
+
const lines = [];
|
|
11377
|
+
lines.push(`flowchart ${dir}`);
|
|
11378
|
+
for (const f of sorted) {
|
|
11379
|
+
const id = idMap.get(f.rel);
|
|
11380
|
+
const basename = f.rel.split(/[\\/]/).pop() ?? f.rel;
|
|
11381
|
+
const label = `${escapeLabel(basename)}<br/><small>${f.score.toFixed(2)}</small>`;
|
|
11382
|
+
lines.push(` ${id}["${label}"]`);
|
|
11383
|
+
}
|
|
11384
|
+
const topSet = new Set(sorted.map((f) => f.rel));
|
|
11385
|
+
for (const f of sorted) {
|
|
11386
|
+
const from = idMap.get(f.rel);
|
|
11387
|
+
for (const dep of f.deps ?? []) {
|
|
11388
|
+
if (topSet.has(dep)) {
|
|
11389
|
+
const to = idMap.get(dep);
|
|
11390
|
+
lines.push(` ${from} --> ${to}`);
|
|
11391
|
+
}
|
|
11392
|
+
}
|
|
11393
|
+
}
|
|
11394
|
+
if (focus && idMap.has(focus)) {
|
|
11395
|
+
lines.push(` classDef focus fill:#ff9800,stroke:#e65100,color:#fff`);
|
|
11396
|
+
lines.push(` ${idMap.get(focus)}:::focus`);
|
|
11397
|
+
}
|
|
11398
|
+
return lines.join(`
|
|
11399
|
+
`);
|
|
11400
|
+
}
|
|
11401
|
+
function sha1Short(s) {
|
|
11402
|
+
let h = 2166136261 >>> 0;
|
|
11403
|
+
for (let i = 0;i < s.length; i++) {
|
|
11404
|
+
h ^= s.charCodeAt(i);
|
|
11405
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
11406
|
+
}
|
|
11407
|
+
return h.toString(16).padStart(8, "0");
|
|
11408
|
+
}
|
|
11409
|
+
function escapeLabel(s) {
|
|
11410
|
+
return s.replace(/["`]/g, "'");
|
|
11411
|
+
}
|
|
11144
11412
|
|
|
11145
11413
|
// tools/repo-map.ts
|
|
11146
11414
|
var description17 = [
|
|
@@ -11149,6 +11417,7 @@ var description17 = [
|
|
|
11149
11417
|
"- planner agent 接到新需求 → 先 smart_search → 再 repo-map 找代码入口",
|
|
11150
11418
|
"- 用户问「这个项目怎么组织的 / 入口在哪 / 哪些是核心模块」",
|
|
11151
11419
|
"- 跨多个文件的重构前,先用 focus= 看清依赖关系",
|
|
11420
|
+
'- 想要可视化依赖图时传 format="mermaid"(Markdown 渲染器会自动出图)',
|
|
11152
11421
|
"**何时不需要**:",
|
|
11153
11422
|
"- 用户已指明确切文件,且只在该文件内改动",
|
|
11154
11423
|
"- 项目地图本会话已生成且未发生大改"
|
|
@@ -11159,6 +11428,7 @@ var ArgsSchema17 = z18.object({
|
|
|
11159
11428
|
top: z18.number().int().min(1).max(100).optional().describe("展示 top N 文件,默认 20;想要全图可传 100"),
|
|
11160
11429
|
focus: z18.string().optional().describe("聚焦文件(仓内 POSIX 相对路径):把它和它的直接依赖 / 反向依赖排在前面"),
|
|
11161
11430
|
max_files: z18.number().int().min(10).max(5000).optional().describe("扫描文件数上限,默认 500;超过自动截断"),
|
|
11431
|
+
format: z18.enum(["markdown", "mermaid", "both"]).optional().describe("输出格式:markdown(默认,文字 + 星级评分)/mermaid(flowchart 流程图,可贴 mermaid.live 渲染)/both(两者,方便对照)"),
|
|
11162
11432
|
_raw: z18.boolean().optional()
|
|
11163
11433
|
});
|
|
11164
11434
|
async function execute17(input) {
|
|
@@ -11171,19 +11441,32 @@ async function execute17(input) {
|
|
|
11171
11441
|
};
|
|
11172
11442
|
}
|
|
11173
11443
|
const args = parsed.data;
|
|
11444
|
+
const format = args.format ?? "markdown";
|
|
11174
11445
|
try {
|
|
11175
11446
|
const map = await buildRepoMap({
|
|
11176
11447
|
root: args.root,
|
|
11177
11448
|
maxFiles: args.max_files
|
|
11178
11449
|
});
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
focus: args.focus
|
|
11182
|
-
})
|
|
11450
|
+
let body;
|
|
11451
|
+
if (format === "markdown") {
|
|
11452
|
+
body = renderMarkdown(map, { top: args.top, focus: args.focus });
|
|
11453
|
+
} else if (format === "mermaid") {
|
|
11454
|
+
const mmd = renderMermaid(map, { top: args.top, focus: args.focus });
|
|
11455
|
+
body = "```mermaid\n" + mmd + "\n```";
|
|
11456
|
+
} else {
|
|
11457
|
+
const md = renderMarkdown(map, { top: args.top, focus: args.focus });
|
|
11458
|
+
const mmd = renderMermaid(map, { top: args.top, focus: args.focus });
|
|
11459
|
+
body = md + `
|
|
11460
|
+
|
|
11461
|
+
## Dependency Graph
|
|
11462
|
+
|
|
11463
|
+
\`\`\`mermaid
|
|
11464
|
+
` + mmd + "\n```";
|
|
11465
|
+
}
|
|
11183
11466
|
const truncated = args.max_files ? map.totalFiles >= args.max_files : map.totalFiles >= 500;
|
|
11184
11467
|
return {
|
|
11185
11468
|
ok: true,
|
|
11186
|
-
markdown:
|
|
11469
|
+
markdown: body,
|
|
11187
11470
|
raw: args._raw ? map : undefined,
|
|
11188
11471
|
stats: {
|
|
11189
11472
|
totalFiles: map.totalFiles,
|
|
@@ -16600,7 +16883,7 @@ import * as zlib from "node:zlib";
|
|
|
16600
16883
|
// lib/version-injected.ts
|
|
16601
16884
|
function getInjectedVersion() {
|
|
16602
16885
|
try {
|
|
16603
|
-
const v = "0.3.
|
|
16886
|
+
const v = "0.3.7";
|
|
16604
16887
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
16605
16888
|
return v;
|
|
16606
16889
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andyqiu/codeforge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "CodeForge — opencode 的零侵入扩展包",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
42
|
+
"bench": "node --experimental-strip-types --no-warnings ./scripts/bench-repo-map.mjs",
|
|
43
|
+
"bench:json": "node --experimental-strip-types --no-warnings ./scripts/bench-repo-map.mjs --json",
|
|
41
44
|
"lint": "tsc --noEmit",
|
|
42
45
|
"check:bun": "node ./scripts/check-bun.mjs",
|
|
43
46
|
"install:local": "bash ./install.sh",
|
|
@@ -79,6 +82,7 @@
|
|
|
79
82
|
"@opencode-ai/plugin": "^1.15.0",
|
|
80
83
|
"@opencode-ai/sdk": "^1.15.0",
|
|
81
84
|
"@types/node": "^22.0.0",
|
|
85
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
82
86
|
"husky": "^9.1.7",
|
|
83
87
|
"typescript": "^5.6.0",
|
|
84
88
|
"vitest": "^2.1.0"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────────────────────
|
|
2
|
+
# code-review.yaml — 审阅别人的 PR / diff(最常用 workflow)
|
|
3
|
+
# trigger: /review
|
|
4
|
+
# 流程:取 diff → planner 出审阅维度 → reviewer 多维度审 → 沉淀
|
|
5
|
+
# 注意:本流程只读,不 stage / apply 任何改动
|
|
6
|
+
# ──────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
name: code-review
|
|
9
|
+
max_loops: 1
|
|
10
|
+
version: 1.0.0
|
|
11
|
+
description: |
|
|
12
|
+
审阅 PR / diff / 本地未提交改动的工作流:
|
|
13
|
+
1. 取 diff(默认 git diff,支持指定 base 分支或 PR URL)
|
|
14
|
+
2. planner 看 diff 内容,列出审阅维度(安全 / 性能 / 可维护性 / 测试 / 风格)
|
|
15
|
+
3. reviewer 按维度逐项审,跑测试,输出 APPROVE / REQUEST_CHANGES / BLOCK
|
|
16
|
+
4. 沉淀关键发现到 KH(gotcha / convention 类)
|
|
17
|
+
|
|
18
|
+
trigger: /review
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: 准备 diff
|
|
22
|
+
agent: reviewer
|
|
23
|
+
description: 取 diff 上下文(git diff / PR)
|
|
24
|
+
actions:
|
|
25
|
+
- tool: smart_search
|
|
26
|
+
args:
|
|
27
|
+
query: "code review checklist ${user_request}"
|
|
28
|
+
limit: 5
|
|
29
|
+
on_error: skip
|
|
30
|
+
on_error: abort
|
|
31
|
+
|
|
32
|
+
- name: 出审阅维度
|
|
33
|
+
agent: planner
|
|
34
|
+
description: 基于 diff 内容 + 项目特点,列出本次审阅必查的维度清单
|
|
35
|
+
inject_context:
|
|
36
|
+
role_hint: 你只列审阅维度,不评审具体代码
|
|
37
|
+
on_error: abort
|
|
38
|
+
|
|
39
|
+
- name: 审阅
|
|
40
|
+
agent: reviewer
|
|
41
|
+
description: 按维度清单逐项审,跑测试,输出 APPROVE/REQUEST_CHANGES/BLOCK
|
|
42
|
+
actions:
|
|
43
|
+
- tool: pending-changes
|
|
44
|
+
args:
|
|
45
|
+
action: list
|
|
46
|
+
status: pending
|
|
47
|
+
on_error: skip
|
|
48
|
+
on_decision:
|
|
49
|
+
APPROVE: continue
|
|
50
|
+
REQUEST_CHANGES: continue # 仍 continue 进入沉淀,REQUEST_CHANGES 是给提交者看的
|
|
51
|
+
BLOCK: abort
|
|
52
|
+
on_error: abort
|
|
53
|
+
|
|
54
|
+
- name: 沉淀
|
|
55
|
+
agent: reviewer
|
|
56
|
+
description: 把发现的 gotcha / convention 沉淀到 KH
|
|
57
|
+
actions:
|
|
58
|
+
- tool: save_chat_insight
|
|
59
|
+
args:
|
|
60
|
+
insight: "${session_summary}"
|
|
61
|
+
category: gotcha
|
|
62
|
+
tags:
|
|
63
|
+
- "trigger:/review"
|
|
64
|
+
- "workflow:code-review"
|
|
65
|
+
on_error: skip
|
|
66
|
+
on_error: skip
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────────────────────
|
|
2
|
+
# refactor.yaml — 安全重构工作流
|
|
3
|
+
# trigger: /refactor
|
|
4
|
+
# 流程:planner → 补 characterization test → 重构 → 测试仍绿 → apply
|
|
5
|
+
# 参考:Working Effectively with Legacy Code(Michael Feathers)
|
|
6
|
+
# ──────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
name: refactor
|
|
9
|
+
max_loops: 3
|
|
10
|
+
version: 1.0.0
|
|
11
|
+
description: |
|
|
12
|
+
安全重构流程(参考 Working Effectively with Legacy Code):
|
|
13
|
+
1. planner 分析现状、列出重构方案
|
|
14
|
+
2. coder A 阶段:先补 characterization test(描述现状行为,可能很难看)
|
|
15
|
+
3. reviewer 确认 test 覆盖待重构代码的现有行为
|
|
16
|
+
4. coder B 阶段:开始重构,每步必须保证 test 仍绿
|
|
17
|
+
5. reviewer 最终确认:行为不变 + 代码更清晰
|
|
18
|
+
6. apply 落地
|
|
19
|
+
|
|
20
|
+
trigger: /refactor
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: 规划
|
|
24
|
+
agent: planner
|
|
25
|
+
description: 列出重构目标 / 涉及文件 / 拆步计划
|
|
26
|
+
actions:
|
|
27
|
+
- tool: smart_search
|
|
28
|
+
args:
|
|
29
|
+
query: "${user_request} refactor"
|
|
30
|
+
limit: 5
|
|
31
|
+
on_error: skip
|
|
32
|
+
on_error: abort
|
|
33
|
+
|
|
34
|
+
- name: 补特征测试
|
|
35
|
+
agent: coder
|
|
36
|
+
description: 先写 characterization tests,覆盖现状行为(不为重构,只为锁定行为)
|
|
37
|
+
auto_feedback:
|
|
38
|
+
test_cmd: "npm test"
|
|
39
|
+
max_retries: 3
|
|
40
|
+
error_excerpt_lines: 5
|
|
41
|
+
escalate_to: reviewer
|
|
42
|
+
on_error: retry
|
|
43
|
+
max_retries: 1
|
|
44
|
+
|
|
45
|
+
- name: 确认覆盖
|
|
46
|
+
agent: reviewer
|
|
47
|
+
description: 确认 characterization tests 覆盖了所有待重构代码路径
|
|
48
|
+
on_decision:
|
|
49
|
+
APPROVE: continue
|
|
50
|
+
REQUEST_CHANGES:
|
|
51
|
+
action: goto
|
|
52
|
+
target: 补特征测试
|
|
53
|
+
BLOCK: abort
|
|
54
|
+
on_error: abort
|
|
55
|
+
|
|
56
|
+
- name: 重构
|
|
57
|
+
agent: coder
|
|
58
|
+
description: 实施重构,每步必须保证已写的 tests 全绿
|
|
59
|
+
auto_feedback:
|
|
60
|
+
test_cmd: "npm test"
|
|
61
|
+
max_retries: 5
|
|
62
|
+
error_excerpt_lines: 5
|
|
63
|
+
escalate_to: reviewer
|
|
64
|
+
on_error: retry
|
|
65
|
+
max_retries: 1
|
|
66
|
+
|
|
67
|
+
- name: 审阅
|
|
68
|
+
agent: reviewer
|
|
69
|
+
description: 确认行为不变(tests 绿)+ 代码更清晰
|
|
70
|
+
actions:
|
|
71
|
+
- tool: pending-changes
|
|
72
|
+
args:
|
|
73
|
+
action: list
|
|
74
|
+
status: pending
|
|
75
|
+
on_decision:
|
|
76
|
+
APPROVE: continue
|
|
77
|
+
REQUEST_CHANGES:
|
|
78
|
+
action: goto
|
|
79
|
+
target: 重构
|
|
80
|
+
BLOCK: abort
|
|
81
|
+
on_error: abort
|
|
82
|
+
|
|
83
|
+
- name: 落地
|
|
84
|
+
agent: coder
|
|
85
|
+
description: 用户审批通过后 apply 全部 pending
|
|
86
|
+
requires_human_approval: true
|
|
87
|
+
actions:
|
|
88
|
+
- tool: pending-changes
|
|
89
|
+
args:
|
|
90
|
+
action: apply_all
|
|
91
|
+
on_error: abort
|
|
92
|
+
on_error: abort
|
|
93
|
+
|
|
94
|
+
- name: 沉淀
|
|
95
|
+
agent: reviewer
|
|
96
|
+
description: 把这次重构的经验沉淀回 KH
|
|
97
|
+
actions:
|
|
98
|
+
- tool: save_chat_insight
|
|
99
|
+
args:
|
|
100
|
+
insight: "${session_summary}"
|
|
101
|
+
category: workflow
|
|
102
|
+
tags:
|
|
103
|
+
- "trigger:/refactor"
|
|
104
|
+
- "workflow:refactor"
|
|
105
|
+
on_error: skip
|
|
106
|
+
on_error: skip
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────────────────────
|
|
2
|
+
# tdd.yaml — Test-Driven Development 严格流程
|
|
3
|
+
# trigger: /tdd
|
|
4
|
+
# 流程:拆需求 → RED 测试 → 验证红 → GREEN 实现 → REFACTOR → 审阅 → apply
|
|
5
|
+
# ──────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
name: tdd
|
|
8
|
+
max_loops: 5
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
description: |
|
|
11
|
+
Test-Driven Development 严格流程:
|
|
12
|
+
1. planner 拆需求成可测的小步骤
|
|
13
|
+
2. 循环 N 次(每个小步骤):
|
|
14
|
+
a. coder 写 RED 测试(必须先红,否则 BLOCK)
|
|
15
|
+
b. coder 写最小实现(必须变绿)
|
|
16
|
+
c. coder refactor(仍绿)
|
|
17
|
+
d. reviewer APPROVE 进下一循环
|
|
18
|
+
3. 全部完成后 apply
|
|
19
|
+
|
|
20
|
+
trigger: /tdd
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: 拆需求
|
|
24
|
+
agent: planner
|
|
25
|
+
description: 把需求拆成可测的小步骤清单
|
|
26
|
+
actions:
|
|
27
|
+
- tool: smart_search
|
|
28
|
+
args:
|
|
29
|
+
query: "${user_request} tdd"
|
|
30
|
+
limit: 5
|
|
31
|
+
on_error: skip
|
|
32
|
+
on_error: abort
|
|
33
|
+
|
|
34
|
+
- name: 写测试-RED
|
|
35
|
+
agent: coder
|
|
36
|
+
description: 先写测试。必须先跑红(无实现→失败),否则 BLOCK
|
|
37
|
+
inject_context:
|
|
38
|
+
role_hint: 你只写测试,不写实现。测试必须先失败再继续
|
|
39
|
+
on_error: retry
|
|
40
|
+
max_retries: 1
|
|
41
|
+
|
|
42
|
+
- name: 验证 RED
|
|
43
|
+
agent: reviewer
|
|
44
|
+
description: 跑 npm test,确认新测试是红的(如果绿了说明测试无效)
|
|
45
|
+
on_decision:
|
|
46
|
+
APPROVE: continue # APPROVE 意味着"测试确实红了"
|
|
47
|
+
REQUEST_CHANGES:
|
|
48
|
+
action: goto
|
|
49
|
+
target: 写测试-RED
|
|
50
|
+
BLOCK: abort
|
|
51
|
+
on_error: abort
|
|
52
|
+
|
|
53
|
+
- name: 最小实现-GREEN
|
|
54
|
+
agent: coder
|
|
55
|
+
description: 写最小实现,让测试变绿。不要超出测试需要
|
|
56
|
+
auto_feedback:
|
|
57
|
+
test_cmd: "npm test"
|
|
58
|
+
max_retries: 5
|
|
59
|
+
error_excerpt_lines: 5
|
|
60
|
+
escalate_to: reviewer
|
|
61
|
+
on_error: retry
|
|
62
|
+
max_retries: 1
|
|
63
|
+
|
|
64
|
+
- name: 重构-REFACTOR
|
|
65
|
+
agent: coder
|
|
66
|
+
description: 测试绿了之后可选 refactor;任何时刻测试必须仍绿
|
|
67
|
+
auto_feedback:
|
|
68
|
+
test_cmd: "npm test"
|
|
69
|
+
max_retries: 3
|
|
70
|
+
error_excerpt_lines: 5
|
|
71
|
+
escalate_to: reviewer
|
|
72
|
+
on_error: skip # refactor 是可选步骤,跳过也行
|
|
73
|
+
|
|
74
|
+
- name: 审阅
|
|
75
|
+
agent: reviewer
|
|
76
|
+
description: 确认 RED→GREEN→REFACTOR 三步全做,测试覆盖核心
|
|
77
|
+
actions:
|
|
78
|
+
- tool: pending-changes
|
|
79
|
+
args:
|
|
80
|
+
action: list
|
|
81
|
+
status: pending
|
|
82
|
+
on_decision:
|
|
83
|
+
APPROVE: continue
|
|
84
|
+
REQUEST_CHANGES:
|
|
85
|
+
action: goto
|
|
86
|
+
target: 写测试-RED
|
|
87
|
+
BLOCK: abort
|
|
88
|
+
on_error: abort
|
|
89
|
+
|
|
90
|
+
- name: 落地
|
|
91
|
+
agent: coder
|
|
92
|
+
description: 用户审批通过后 apply 全部 pending
|
|
93
|
+
requires_human_approval: true
|
|
94
|
+
actions:
|
|
95
|
+
- tool: pending-changes
|
|
96
|
+
args:
|
|
97
|
+
action: apply_all
|
|
98
|
+
on_error: abort
|
|
99
|
+
on_error: abort
|