@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.
@@ -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**(因为只读,无副作用)
@@ -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
- const md = renderMarkdown(map, {
11180
- top: args.top,
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: md,
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.5";
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.5",
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