add-coder 0.3.27 → 0.3.28
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/README.md +246 -145
- package/package.json +2 -1
- package/templates/.add-coder-src-hash.json +126 -148
- package/templates/adapters/claude/hooks/doc-format-guard.ts +32 -0
- package/templates/adapters/claude/hooks/lib/claude-env.ts +7 -0
- package/templates/adapters/claude/hooks/notification.ts +24 -0
- package/templates/adapters/claude/hooks/permission-denied.ts +33 -0
- package/templates/adapters/claude/hooks/permission-gate.ts +12 -0
- package/templates/adapters/claude/hooks/post-tool-failure.ts +4 -0
- package/templates/adapters/claude/hooks/post-tool-use.ts +17 -0
- package/templates/adapters/claude/hooks/pre-compact.ts +10 -0
- package/templates/adapters/claude/hooks/pre-tool-use.ts +81 -0
- package/templates/adapters/claude/hooks/prompt-submit.ts +82 -0
- package/templates/adapters/claude/hooks/review-checklist.ts +4 -0
- package/templates/adapters/claude/hooks/session-end.ts +10 -0
- package/templates/adapters/claude/hooks/session-start.ts +24 -0
- package/templates/adapters/claude/hooks/stop-check.ts +15 -0
- package/templates/adapters/claude/hooks/stop-failure.ts +34 -0
- package/templates/adapters/claude/hooks/subagent-guard.ts +25 -0
- package/templates/adapters/claude/hooks/subagent-stop.ts +12 -0
- package/templates/adapters/claude/settings.json +13 -13
- package/templates/adapters/codex/hooks/doc-format-guard.ts +30 -0
- package/templates/adapters/codex/hooks/notification.ts +26 -0
- package/templates/adapters/codex/hooks/permission-gate.ts +15 -0
- package/templates/adapters/codex/hooks/post-tool-failure.ts +14 -0
- package/templates/adapters/codex/hooks/post-tool-use.ts +85 -0
- package/templates/adapters/codex/hooks/pre-compact.ts +20 -0
- package/templates/adapters/codex/hooks/pre-tool-use.ts +141 -0
- package/templates/adapters/codex/hooks/prompt-submit.ts +38 -0
- package/templates/adapters/codex/hooks/review-checklist.ts +5 -0
- package/templates/adapters/codex/hooks/session-end.ts +19 -0
- package/templates/adapters/codex/hooks/session-start.ts +45 -0
- package/templates/adapters/codex/hooks/stop-check.ts +59 -0
- package/templates/adapters/codex/hooks/subagent-guard.ts +22 -0
- package/templates/adapters/codex/hooks/subagent-stop.ts +20 -0
- package/templates/adapters/codex/hooks.json +5 -5
- package/templates/adapters/qoder/hooks/doc-format-guard.ts +34 -0
- package/templates/adapters/qoder/hooks/lib/context-inject.ts +61 -0
- package/templates/adapters/qoder/hooks/lib/qoder-env.ts +23 -0
- package/templates/adapters/qoder/hooks/lib/review-checklist.ts +5 -0
- package/templates/adapters/qoder/hooks/notification.ts +34 -0
- package/templates/adapters/qoder/hooks/permission-gate.ts +26 -0
- package/templates/adapters/qoder/hooks/post-tool-failure.ts +30 -0
- package/templates/adapters/qoder/hooks/post-tool-use.ts +32 -0
- package/templates/adapters/qoder/hooks/pre-compact.ts +16 -0
- package/templates/adapters/qoder/hooks/pre-tool-use.ts +112 -0
- package/templates/adapters/qoder/hooks/prompt-submit.ts +66 -0
- package/templates/adapters/qoder/hooks/review-checklist.ts +24 -0
- package/templates/adapters/qoder/hooks/session-end.ts +27 -0
- package/templates/adapters/qoder/hooks/session-start.ts +83 -0
- package/templates/adapters/qoder/hooks/stop-check.ts +69 -0
- package/templates/adapters/qoder/hooks/subagent-guard.ts +16 -0
- package/templates/adapters/qoder/hooks/subagent-stop.ts +33 -0
- package/templates/adapters/qoder/settings.json +13 -13
- package/templates/adapters/trae/hooks/doc-format-guard.ts +30 -0
- package/templates/adapters/trae/hooks/notification.ts +22 -0
- package/templates/adapters/trae/hooks/permission-gate.ts +15 -0
- package/templates/adapters/trae/hooks/post-tool-failure.ts +14 -0
- package/templates/adapters/trae/hooks/post-tool-use.ts +24 -0
- package/templates/adapters/trae/hooks/pre-compact.ts +15 -0
- package/templates/adapters/trae/hooks/pre-tool-use.ts +40 -0
- package/templates/adapters/trae/hooks/prompt-submit.ts +29 -0
- package/templates/adapters/trae/hooks/review-checklist.ts +5 -0
- package/templates/adapters/trae/hooks/session-end.ts +16 -0
- package/templates/adapters/trae/hooks/session-start.ts +15 -0
- package/templates/adapters/trae/hooks/stop-check.ts +20 -0
- package/templates/adapters/trae/hooks/subagent-guard.ts +22 -0
- package/templates/adapters/trae/hooks/subagent-stop.ts +15 -0
- package/templates/adapters/trae/hooks.json +1 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.ts +27 -0
- package/templates/adapters/vscode/hooks/notification.ts +22 -0
- package/templates/adapters/vscode/hooks/permission-gate.ts +15 -0
- package/templates/adapters/vscode/hooks/post-tool-failure.ts +32 -0
- package/templates/adapters/vscode/hooks/post-tool-use.ts +25 -0
- package/templates/adapters/vscode/hooks/pre-compact.ts +15 -0
- package/templates/adapters/vscode/hooks/pre-tool-use.ts +78 -0
- package/templates/adapters/vscode/hooks/prompt-submit.ts +74 -0
- package/templates/adapters/vscode/hooks/review-checklist.ts +5 -0
- package/templates/adapters/vscode/hooks/session-end.ts +16 -0
- package/templates/adapters/vscode/hooks/session-start.ts +24 -0
- package/templates/adapters/vscode/hooks/stop-check.ts +21 -0
- package/templates/adapters/vscode/hooks/subagent-guard.ts +21 -0
- package/templates/adapters/vscode/hooks/subagent-stop.ts +15 -0
- package/templates/core/docs/ADD-governance-claude-code.md +3 -3
- package/templates/core/docs/ADD-governance-codex.md +5 -5
- package/templates/core/docs/ADD-governance-qoder-cn.md +4 -4
- package/templates/core/docs/ADD-governance-trae.md +2 -2
- package/templates/core/docs/ADD-governance-vscode-copilot.md +21 -22
- package/templates/core/governance/audit-bridge.ts +47 -0
- package/templates/core/governance/common.ts +299 -0
- package/templates/core/governance/context-inject.ts +53 -0
- package/templates/core/governance/doc-format-guard.ts +440 -0
- package/templates/core/governance/notification-router.ts +62 -0
- package/templates/core/governance/notify.ts +66 -0
- package/templates/core/governance/permission-gate-router.ts +42 -0
- package/templates/core/governance/post-tool-failure-router.ts +56 -0
- package/templates/core/governance/post-tool-router.ts +212 -0
- package/templates/core/governance/pre-compact-guard.ts +41 -0
- package/templates/core/governance/pre-tool-guard.ts +309 -0
- package/templates/core/governance/preload-templates.ts +241 -0
- package/templates/core/governance/prompt-router.ts +222 -0
- package/templates/core/governance/review-checklist-guard.ts +186 -0
- package/templates/core/governance/rules.ts +720 -0
- package/templates/core/governance/session-end.ts +88 -0
- package/templates/core/governance/session-start-guard.ts +99 -0
- package/templates/core/governance/state-detect.ts +6 -0
- package/templates/core/governance/stop-router.ts +140 -0
- package/templates/core/governance/subagent-guard-router.ts +35 -0
- package/templates/core/governance/subagent-stop-router.ts +93 -0
- package/templates/core/governance/subagent-stop.ts +75 -0
- package/templates/core/governance/vocabulary.ts +109 -0
- package/templates/core/hooks/doc-format-guard.ts +21 -0
- package/templates/core/hooks/notification.ts +11 -0
- package/templates/core/hooks/permission-gate.ts +7 -0
- package/templates/core/hooks/post-tool-failure.ts +7 -0
- package/templates/core/hooks/post-tool-use.ts +23 -0
- package/templates/core/hooks/pre-compact.ts +9 -0
- package/templates/core/hooks/pre-tool-use.ts +30 -0
- package/templates/core/hooks/prompt-submit.ts +11 -0
- package/templates/core/hooks/review-checklist.ts +4 -0
- package/templates/core/hooks/session-end.ts +10 -0
- package/templates/core/hooks/session-start.ts +9 -0
- package/templates/core/hooks/stop-check.ts +14 -0
- package/templates/core/hooks/subagent-guard.ts +8 -0
- package/templates/core/hooks/subagent-stop.ts +9 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +12 -1
- package/templates/core/scripts/mcp-server/tools/plan.ts +12 -2
- package/templates/adapters/claude/hooks/doc-format-guard.sh +0 -283
- package/templates/adapters/claude/hooks/lib/common.sh +0 -220
- package/templates/adapters/claude/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/claude/hooks/lib/notify.sh +0 -36
- package/templates/adapters/claude/hooks/lib/preload-templates.sh +0 -212
- package/templates/adapters/claude/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/claude/hooks/lib/state-detect.sh +0 -13
- package/templates/adapters/claude/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/claude/hooks/lib/vocabulary.sh +0 -62
- package/templates/adapters/claude/hooks/notification.sh +0 -45
- package/templates/adapters/claude/hooks/permission-denied.sh +0 -42
- package/templates/adapters/claude/hooks/permission-gate.sh +0 -18
- package/templates/adapters/claude/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/claude/hooks/post-tool-use.sh +0 -35
- package/templates/adapters/claude/hooks/pre-compact.sh +0 -37
- package/templates/adapters/claude/hooks/pre-tool-use.sh +0 -153
- package/templates/adapters/claude/hooks/prompt-submit.sh +0 -99
- package/templates/adapters/claude/hooks/review-checklist.sh +0 -10
- package/templates/adapters/claude/hooks/session-end.sh +0 -21
- package/templates/adapters/claude/hooks/session-start.sh +0 -36
- package/templates/adapters/claude/hooks/stop-check.sh +0 -54
- package/templates/adapters/claude/hooks/stop-failure.sh +0 -44
- package/templates/adapters/claude/hooks/subagent-guard.sh +0 -36
- package/templates/adapters/claude/hooks/subagent-stop.sh +0 -20
- package/templates/adapters/codex/hooks/doc-format-guard.sh +0 -343
- package/templates/adapters/codex/hooks/lib/common.sh +0 -209
- package/templates/adapters/codex/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/codex/hooks/lib/notify.sh +0 -36
- package/templates/adapters/codex/hooks/lib/preload-templates.sh +0 -208
- package/templates/adapters/codex/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/codex/hooks/lib/state-detect.sh +0 -8
- package/templates/adapters/codex/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/codex/hooks/lib/vocabulary.sh +0 -51
- package/templates/adapters/codex/hooks/notification.sh +0 -30
- package/templates/adapters/codex/hooks/permission-gate.sh +0 -18
- package/templates/adapters/codex/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/codex/hooks/post-tool-use.sh +0 -43
- package/templates/adapters/codex/hooks/pre-compact.sh +0 -37
- package/templates/adapters/codex/hooks/pre-tool-use.sh +0 -130
- package/templates/adapters/codex/hooks/prompt-submit.sh +0 -92
- package/templates/adapters/codex/hooks/review-checklist.sh +0 -10
- package/templates/adapters/codex/hooks/session-end.sh +0 -38
- package/templates/adapters/codex/hooks/session-start.sh +0 -63
- package/templates/adapters/codex/hooks/stop-check.sh +0 -71
- package/templates/adapters/codex/hooks/subagent-guard.sh +0 -15
- package/templates/adapters/codex/hooks/subagent-stop.sh +0 -47
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +0 -341
- package/templates/adapters/qoder/hooks/lib/common.sh +0 -223
- package/templates/adapters/qoder/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/qoder/hooks/lib/notify.sh +0 -36
- package/templates/adapters/qoder/hooks/lib/preload-templates.sh +0 -212
- package/templates/adapters/qoder/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/qoder/hooks/lib/state-detect.sh +0 -13
- package/templates/adapters/qoder/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/qoder/hooks/lib/vocabulary.sh +0 -62
- package/templates/adapters/qoder/hooks/notification.sh +0 -25
- package/templates/adapters/qoder/hooks/permission-gate.sh +0 -8
- package/templates/adapters/qoder/hooks/post-tool-failure.sh +0 -8
- package/templates/adapters/qoder/hooks/post-tool-use.sh +0 -121
- package/templates/adapters/qoder/hooks/pre-compact.sh +0 -37
- package/templates/adapters/qoder/hooks/pre-tool-use.sh +0 -154
- package/templates/adapters/qoder/hooks/prompt-submit.sh +0 -96
- package/templates/adapters/qoder/hooks/review-checklist.sh +0 -157
- package/templates/adapters/qoder/hooks/session-end.sh +0 -24
- package/templates/adapters/qoder/hooks/session-start.sh +0 -45
- package/templates/adapters/qoder/hooks/stop-check.sh +0 -59
- package/templates/adapters/qoder/hooks/subagent-guard.sh +0 -27
- package/templates/adapters/qoder/hooks/subagent-stop.sh +0 -26
- package/templates/adapters/trae/hooks/doc-format-guard.sh +0 -343
- package/templates/adapters/trae/hooks/lib/common.sh +0 -220
- package/templates/adapters/trae/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/trae/hooks/lib/notify.sh +0 -36
- package/templates/adapters/trae/hooks/lib/preload-templates.sh +0 -212
- package/templates/adapters/trae/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/trae/hooks/lib/state-detect.sh +0 -10
- package/templates/adapters/trae/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/trae/hooks/lib/vocabulary.sh +0 -62
- package/templates/adapters/trae/hooks/notification.sh +0 -45
- package/templates/adapters/trae/hooks/permission-gate.sh +0 -18
- package/templates/adapters/trae/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/trae/hooks/post-tool-use.sh +0 -138
- package/templates/adapters/trae/hooks/pre-compact.sh +0 -37
- package/templates/adapters/trae/hooks/pre-tool-use.sh +0 -223
- package/templates/adapters/trae/hooks/prompt-submit.sh +0 -87
- package/templates/adapters/trae/hooks/review-checklist.sh +0 -10
- package/templates/adapters/trae/hooks/session-end.sh +0 -38
- package/templates/adapters/trae/hooks/session-start.sh +0 -53
- package/templates/adapters/trae/hooks/stop-check.sh +0 -54
- package/templates/adapters/trae/hooks/subagent-guard.sh +0 -15
- package/templates/adapters/trae/hooks/subagent-stop.sh +0 -47
- package/templates/adapters/vscode/hooks/doc-format-guard.sh +0 -282
- package/templates/adapters/vscode/hooks/lib/common.sh +0 -220
- package/templates/adapters/vscode/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/vscode/hooks/lib/notify.sh +0 -36
- package/templates/adapters/vscode/hooks/lib/preload-templates.sh +0 -212
- package/templates/adapters/vscode/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/vscode/hooks/lib/state-detect.sh +0 -13
- package/templates/adapters/vscode/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/vscode/hooks/lib/vocabulary.sh +0 -62
- package/templates/adapters/vscode/hooks/notification.sh +0 -43
- package/templates/adapters/vscode/hooks/permission-gate.sh +0 -18
- package/templates/adapters/vscode/hooks/post-tool-failure.sh +0 -28
- package/templates/adapters/vscode/hooks/post-tool-use.sh +0 -35
- package/templates/adapters/vscode/hooks/pre-compact.sh +0 -37
- package/templates/adapters/vscode/hooks/pre-tool-use.sh +0 -153
- package/templates/adapters/vscode/hooks/prompt-submit.sh +0 -99
- package/templates/adapters/vscode/hooks/review-checklist.sh +0 -10
- package/templates/adapters/vscode/hooks/session-end.sh +0 -13
- package/templates/adapters/vscode/hooks/session-start.sh +0 -35
- package/templates/adapters/vscode/hooks/stop-check.sh +0 -62
- package/templates/adapters/vscode/hooks/subagent-guard.sh +0 -37
- package/templates/adapters/vscode/hooks/subagent-stop.sh +0 -12
- package/templates/core/hooks/doc-format-guard.sh +0 -343
- package/templates/core/hooks/lib/common.sh +0 -220
- package/templates/core/hooks/lib/context-inject.sh +0 -96
- package/templates/core/hooks/lib/notify.sh +0 -36
- package/templates/core/hooks/lib/preload-templates.sh +0 -212
- package/templates/core/hooks/lib/session-end.sh +0 -76
- package/templates/core/hooks/lib/state-detect.sh +0 -10
- package/templates/core/hooks/lib/subagent-stop.sh +0 -90
- package/templates/core/hooks/lib/vocabulary.sh +0 -62
- package/templates/core/hooks/notification.sh +0 -45
- package/templates/core/hooks/permission-gate.sh +0 -18
- package/templates/core/hooks/post-tool-failure.sh +0 -10
- package/templates/core/hooks/post-tool-use.sh +0 -138
- package/templates/core/hooks/pre-compact.sh +0 -37
- package/templates/core/hooks/pre-tool-use.sh +0 -223
- package/templates/core/hooks/prompt-submit.sh +0 -87
- package/templates/core/hooks/review-checklist.sh +0 -10
- package/templates/core/hooks/session-end.sh +0 -38
- package/templates/core/hooks/session-start.sh +0 -53
- package/templates/core/hooks/stop-check.sh +0 -54
- package/templates/core/hooks/subagent-guard.sh +0 -15
- package/templates/core/hooks/subagent-stop.sh +0 -47
- package/templates/shared/hooks-lib/common.sh +0 -22
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// preload-templates.ts — ADD 模板预读(bash 版 preload-templates.sh 的 TS 同语义实现)
|
|
2
|
+
// 用法:
|
|
3
|
+
// preload-templates.ts --index # 输出模板清单(文件名 + 用途)
|
|
4
|
+
// preload-templates.ts --full # 输出全部模板全文
|
|
5
|
+
// preload-templates.ts --full --top 5 # 输出前 5 个最常用模板全文
|
|
6
|
+
// preload-templates.ts --full --mark # 全文输出 + 落 tpl-injected 标记
|
|
7
|
+
//
|
|
8
|
+
// 被 SessionStart(--index)和 UserPromptSubmit(--full)调用。
|
|
9
|
+
// tpl-injected 标记文件用于去重——同会话二次命中时不重复注入。
|
|
10
|
+
// 协议层契约(模板预载): 读 ${magicDir}/templates 本地物化副本;目录/标准模板缺失 fail-fast
|
|
11
|
+
//
|
|
12
|
+
// 设计范式: OOP 服务类封装(模板表 + 优先级序 + 校验 + 输出模式 + 标记管理单一职责聚合),
|
|
13
|
+
// 纯函数负责文本变换(frontmatter 剥离),CLI 入口仅做参数解析与进程语义。
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { join, dirname } from "node:path"
|
|
17
|
+
import { createHash } from "node:crypto"
|
|
18
|
+
import { fileURLToPath } from "node:url"
|
|
19
|
+
import { findUpSync } from "find-up"
|
|
20
|
+
|
|
21
|
+
/** 模板元信息(泛型 Record 收敛:文件名 → 用途描述) */
|
|
22
|
+
type TemplateRegistry = Readonly<Record<string, string>>
|
|
23
|
+
|
|
24
|
+
/** 输出模式(--index / --full) */
|
|
25
|
+
type OutputMode = "index" | "full"
|
|
26
|
+
|
|
27
|
+
/** CLI 参数解析结果(结构化,避免散落参数) */
|
|
28
|
+
interface CliArgs {
|
|
29
|
+
mode: OutputMode
|
|
30
|
+
top: number
|
|
31
|
+
mark: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 纯函数:剥离 YAML frontmatter(对齐 bash awk:首行 --- 至下一个 --- 之间的内容跳过) */
|
|
35
|
+
function stripFrontmatter(lines: readonly string[]): string[] {
|
|
36
|
+
const out: string[] = []
|
|
37
|
+
let inFm = false
|
|
38
|
+
lines.forEach((line, idx) => {
|
|
39
|
+
if (idx === 0 && line === "---") {
|
|
40
|
+
inFm = true
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (inFm && line === "---") {
|
|
44
|
+
inFm = false
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (!inFm) out.push(line)
|
|
48
|
+
})
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 纯函数:项目 hash(对齐 bash md5sum | cut -c1-8;失败回退 "default") */
|
|
53
|
+
function projectHash(projectDir: string): string {
|
|
54
|
+
try {
|
|
55
|
+
return createHash("md5").update(projectDir).digest("hex").slice(0, 8)
|
|
56
|
+
} catch {
|
|
57
|
+
return "default"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 纯函数:解析 CLI 参数为结构化 CliArgs */
|
|
62
|
+
function parseArgs(argv: readonly string[]): CliArgs {
|
|
63
|
+
const args: CliArgs = { mode: "index", top: 0, mark: false }
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
switch (argv[i]) {
|
|
66
|
+
case "--index":
|
|
67
|
+
args.mode = "index"
|
|
68
|
+
break
|
|
69
|
+
case "--full":
|
|
70
|
+
args.mode = "full"
|
|
71
|
+
break
|
|
72
|
+
case "--top":
|
|
73
|
+
args.top = Number(argv[++i] ?? "0") || 0
|
|
74
|
+
break
|
|
75
|
+
case "--mark":
|
|
76
|
+
args.mark = true
|
|
77
|
+
break
|
|
78
|
+
default:
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return args
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 模板预读服务(OOP 聚合):
|
|
87
|
+
* - 模板注册表 + 优先级序(顺序即 --top N 的优先级)
|
|
88
|
+
* - 目录校验(fail-fast)
|
|
89
|
+
* - index/full 两种输出模式
|
|
90
|
+
* - tpl-injected 标记管理(去重)
|
|
91
|
+
*/
|
|
92
|
+
export class PreloadTemplates {
|
|
93
|
+
private readonly templatesDir: string
|
|
94
|
+
private readonly tplFlag: string
|
|
95
|
+
|
|
96
|
+
private static readonly TEMPLATES: TemplateRegistry = Object.freeze({
|
|
97
|
+
"simple-plan-template.md": "需求方案(简单版):六节结构,元信息+背景+方案+架构+实施+验收",
|
|
98
|
+
"spec-template.md": "功能规格:Why/What Changes/Impact/WHEN-THEN Requirements",
|
|
99
|
+
"tasks-template.md": "任务拆分:Phase→Task→SubTask层级",
|
|
100
|
+
"checklist-template.md": "验收清单:[T]编译期+[R]运行时+ADD规则合规",
|
|
101
|
+
"review-template.md": "方案审查(ADD-9):问题复现+方案对比+决策结论+影响评估",
|
|
102
|
+
"standard-plan-template.md": "需求方案(标准版):PLAN元信息+背景+方案+架构+实施Task+验收+关联文档",
|
|
103
|
+
"add-route-template-heavyweight.md": "ADD执行路线图(重型):每Step验证并更新状态+spec_sync交叉校验",
|
|
104
|
+
"add-route-template.md": "ADD执行路线图(轻量):标准Step产出检查",
|
|
105
|
+
"handoff-single-round-template.md": "单轮交接:9章节(含恢复上下文审计查询)",
|
|
106
|
+
"handoff-multi-round-template.md": "多轮交接:全局拓扑+每轮13子章节+收敛规则+启动模板",
|
|
107
|
+
"review-implementation-template.md": "实现审查(ADD-10):格式契约+框架版本+数据模型+E2E curl",
|
|
108
|
+
"review-runtime-template.md": "运行时纠偏(ADD-11):发现列表+根因分析+流程改进项",
|
|
109
|
+
"prd-standard-template.md": "产品需求文档(新建):背景目标+用户场景+功能需求+验收标准",
|
|
110
|
+
"prd-incremental-template.md": "产品需求文档(增量):变更摘要+diff式记录",
|
|
111
|
+
"fix-verification-template.md": "修复验证模板",
|
|
112
|
+
"report-template.md": "代码审查报告模板",
|
|
113
|
+
"runtime-report-template.md": "运行时报告模板",
|
|
114
|
+
"TERMINOLOGY.md": "模板术语速查",
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
private static readonly PRIORITY_ORDER: readonly string[] = Object.freeze([
|
|
118
|
+
"simple-plan-template.md",
|
|
119
|
+
"spec-template.md",
|
|
120
|
+
"tasks-template.md",
|
|
121
|
+
"checklist-template.md",
|
|
122
|
+
"review-template.md",
|
|
123
|
+
"standard-plan-template.md",
|
|
124
|
+
"add-route-template-heavyweight.md",
|
|
125
|
+
"add-route-template.md",
|
|
126
|
+
"handoff-single-round-template.md",
|
|
127
|
+
"handoff-multi-round-template.md",
|
|
128
|
+
"review-implementation-template.md",
|
|
129
|
+
"review-runtime-template.md",
|
|
130
|
+
"prd-standard-template.md",
|
|
131
|
+
"prd-incremental-template.md",
|
|
132
|
+
"fix-verification-template.md",
|
|
133
|
+
"report-template.md",
|
|
134
|
+
"runtime-report-template.md",
|
|
135
|
+
"TERMINOLOGY.md",
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 模板目录 find-up 解析(复用 find-up 包,与 src/shared/paths.ts projectRoot 同范式):
|
|
140
|
+
* 从 startDir 向上找第一个 templates 目录(esbuild bundle 内联,产物零依赖):
|
|
141
|
+
* - 产物 <magicDir>/hooks/xxx.mjs → <magicDir>/templates(分发物化)
|
|
142
|
+
* - 源码 templates/core/governance/preload-templates.ts → templates/core/templates(源码模板)
|
|
143
|
+
* 命中条件:目录含至少一个标准模板(防命中仓库根 templates/ 空壳);
|
|
144
|
+
* 目录存在但标准模板缺失 = 物化异常 → 返回 null,由 validate() fail-fast(不继续向上找)。
|
|
145
|
+
* 缺陷修复(2026-08-14 Task 5.1): 原写死 join(dirname, "..", "..", "templates") 固定层级,
|
|
146
|
+
* 产物从 hooks/ 上溯两级到仓库根 templates/(无标准模板)——被 refresh-fixed 反写掩盖,
|
|
147
|
+
* golden 重抓暴露;改 find-up 锚点查找,层级零漂移。
|
|
148
|
+
*/
|
|
149
|
+
private static findTemplatesDir(startDir: string): string | null {
|
|
150
|
+
const hit = findUpSync("templates", { cwd: startDir, type: "directory" })
|
|
151
|
+
if (!hit) return null
|
|
152
|
+
return PreloadTemplates.PRIORITY_ORDER.some((t) => existsSync(join(hit, t))) ? hit : null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
constructor(templatesDir?: string, tplFlag?: string) {
|
|
156
|
+
const startDir = dirname(fileURLToPath(import.meta.url))
|
|
157
|
+
this.templatesDir = templatesDir ?? PreloadTemplates.findTemplatesDir(startDir) ?? join(startDir, "..", "templates")
|
|
158
|
+
this.tplFlag = tplFlag ?? `/tmp/add_tpl_${projectHash(process.env.PROJECT_DIR || process.cwd())}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 模板目录存在性校验(fail-fast:目录缺失/标准模板全缺 → 抛错,由 CLI 层转 exit 1) */
|
|
162
|
+
validate(): void {
|
|
163
|
+
if (!existsSync(this.templatesDir)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`[ADD preload] 模板目录不存在: ${this.templatesDir}(生成态物化缺失,请执行 add-coder sync 后重试)`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const available = PreloadTemplates.PRIORITY_ORDER.filter((t) =>
|
|
169
|
+
existsSync(join(this.templatesDir, t))
|
|
170
|
+
).length
|
|
171
|
+
if (available === 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[ADD preload] 模板目录中未找到 ADD 标准模板: ${this.templatesDir}(缺失清单: ${PreloadTemplates.PRIORITY_ORDER.join(" ")})`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 读取模板内容(strip frontmatter) */
|
|
179
|
+
readTemplate(file: string): string {
|
|
180
|
+
const path = join(this.templatesDir, file)
|
|
181
|
+
if (!existsSync(path)) return ""
|
|
182
|
+
return stripFrontmatter(readFileSync(path, "utf-8").split("\n")).join("\n")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** --index 模式输出(对齐 bash output_index 逐字) */
|
|
186
|
+
index(): string {
|
|
187
|
+
const lines: string[] = ["## ADD 可用模板清单", "", "| # | 模板文件 | 用途 |", "|---|---------|------|"]
|
|
188
|
+
let i = 1
|
|
189
|
+
for (const tmpl of PreloadTemplates.PRIORITY_ORDER) {
|
|
190
|
+
if (existsSync(join(this.templatesDir, tmpl))) {
|
|
191
|
+
lines.push(`| ${i} | ${tmpl} | ${PreloadTemplates.TEMPLATES[tmpl] ?? "模板文件"} |`)
|
|
192
|
+
i++
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return lines.join("\n") + "\n"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** --full 模式输出(对齐 bash output_full 逐字) */
|
|
199
|
+
full(top: number): string {
|
|
200
|
+
const lines: string[] = ["## ADD 模板全文内容", ""]
|
|
201
|
+
let count = 0
|
|
202
|
+
for (const tmpl of PreloadTemplates.PRIORITY_ORDER) {
|
|
203
|
+
if (!existsSync(join(this.templatesDir, tmpl))) continue
|
|
204
|
+
count++
|
|
205
|
+
if (top > 0 && count > top) break
|
|
206
|
+
lines.push("---", `### ${tmpl}`, "", this.readTemplate(tmpl), "")
|
|
207
|
+
}
|
|
208
|
+
return lines.join("\n") + "\n"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
isInjected(): boolean {
|
|
212
|
+
return existsSync(this.tplFlag)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 标记模板已注入(tpl-injected 去重:同会话二次命中不重复注入) */
|
|
216
|
+
markInjected(): void {
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(this.tplFlag, "")
|
|
219
|
+
} catch {
|
|
220
|
+
/* ignore(对齐 bash touch || true) */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** CLI 入口:解析参数 → 校验 → 输出;去重命中时 stderr 提示并 exit 0 */
|
|
225
|
+
run(argv: readonly string[]): number {
|
|
226
|
+
const { mode, top, mark } = parseArgs(argv)
|
|
227
|
+
this.validate()
|
|
228
|
+
if (mode === "index") {
|
|
229
|
+
process.stdout.write(this.index())
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
if (this.isInjected() && !mark) {
|
|
233
|
+
process.stderr.write("[ADD preload] 模板已在本会话注入,跳过重复注入(tpl-injected 标记存在)\n")
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
236
|
+
process.stdout.write(this.full(top))
|
|
237
|
+
this.markInjected()
|
|
238
|
+
return 0
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// prompt-router.ts — UserPromptSubmit 触发词路由基类(治理逻辑层,Task 3.1 继承体系)
|
|
2
|
+
// 治理卡位 #3: Layer 1 精准触发 → Layer 2 阻断 → Layer 3 状态注入
|
|
3
|
+
// 规则真源: hook-event-rules.toml([event.daily.warn_threshold] 日报阈值)
|
|
4
|
+
//
|
|
5
|
+
// 设计范式: 模板方法基类——L1/L2/L3 分流流程固化,
|
|
6
|
+
// 协议差异(提取方式/文本形态/输出通道/附加注入)由 adapter 子类 override。
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
9
|
+
import { join } from "node:path"
|
|
10
|
+
import { spawnSync } from "node:child_process"
|
|
11
|
+
import {
|
|
12
|
+
detectActiveAdd,
|
|
13
|
+
isAlreadyAccepted,
|
|
14
|
+
jsonGet,
|
|
15
|
+
localIsoSeconds,
|
|
16
|
+
} from "./common.js"
|
|
17
|
+
import { loadDevKeywords, matchTrigger } from "./vocabulary.js"
|
|
18
|
+
import { writeHookEvent } from "./notify.js"
|
|
19
|
+
import { event } from "./rules.js"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* UserPromptSubmit 触发词路由基类(模板方法):
|
|
23
|
+
* 流程固化(不可 override): run()
|
|
24
|
+
* 扩展点(protected,adapter 子类 override 协议差异):
|
|
25
|
+
* - extractPrompt(): prompt 提取(core: jsonGet;claude: grep 正则)
|
|
26
|
+
* - acceptedText(): 验收幂等文本(core: 6 行含 ★;claude: 4 行)
|
|
27
|
+
* - onAccepted(): 验收后置(core: spawn review-checklist;claude: 无)
|
|
28
|
+
* - onDevKwMatched(): 开发关键词命中提示(claude: stderr;core: 无)
|
|
29
|
+
* - layer2Channel: Layer2 输出通道(core: stderr;claude: stdout)
|
|
30
|
+
* - layer3Text(): Layer3 状态文本(core: 单行;claude: 多行块)
|
|
31
|
+
* - dailyWarnText(): 日报告警文本(core: 含"或检查 hooks 误报";claude: 无)
|
|
32
|
+
* - afterLayer3(): Layer3 附加注入(claude: 模板全文 full(5);core: 无)
|
|
33
|
+
*/
|
|
34
|
+
export class PromptRouter {
|
|
35
|
+
protected readonly magicDir: string
|
|
36
|
+
|
|
37
|
+
constructor(magicDir: string) {
|
|
38
|
+
this.magicDir = magicDir
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
42
|
+
|
|
43
|
+
/** prompt 提取(core: jsonGet) */
|
|
44
|
+
protected extractPrompt(input: string): string {
|
|
45
|
+
return jsonGet(input, "prompt")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 验收幂等文本(core 协议: 6 行含 ★ 同步检查) */
|
|
49
|
+
protected acceptedText(): string {
|
|
50
|
+
return `[ADD 验收] ⚠️ 已验收。进入 Review 模式:
|
|
51
|
+
① 重新检查 checklist [T]/[R] 项
|
|
52
|
+
② 审查 audit 记录完整性
|
|
53
|
+
③ 如有差异 → Review 回流至 handoff(增量更新,不覆盖已有结论)
|
|
54
|
+
④ 无差异 → 记录 'Review 已确认,无新发现'
|
|
55
|
+
★ 同步检查: 如 checklist 有新 cuid 但 handoff 审计表缺失 → 更新 handoff ADD-7 表 + query_audit_logs 命令
|
|
56
|
+
`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 验收后置处理(core: spawn review-checklist 子进程,输出丢弃) */
|
|
60
|
+
protected onAccepted(handoff: string, addRoute: string): void {
|
|
61
|
+
const reviewScript = join(this.magicDir, "hooks", "review-checklist.mjs")
|
|
62
|
+
if (existsSync(reviewScript)) {
|
|
63
|
+
spawnSync(process.execPath, [reviewScript, handoff, addRoute], { stdio: "ignore" })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 开发关键词命中提示(core: 无) */
|
|
68
|
+
protected onDevKwMatched(): void {
|
|
69
|
+
// core 协议: 无额外输出
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 开发关键词匹配(core: join("|") 单正则;qoder: 逐个正则 some) */
|
|
73
|
+
protected devKwMatched(prompt: string, devKw: string[]): boolean {
|
|
74
|
+
try {
|
|
75
|
+
return new RegExp(devKw.join("|"), "i").test(prompt)
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Layer 2 输出通道(core: stderr) */
|
|
82
|
+
protected layer2ToStderr(): boolean {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Layer 3 状态文本(core: 单行) */
|
|
87
|
+
protected layer3Text(plan: string, rounds: string, step: string, handoff: string): string {
|
|
88
|
+
return `[ADD 状态] Plan: ${plan}, 轮次: ${rounds}, Step: ${step}, handoff: ${handoff}\n`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 日报告警文本(core: 含"或检查 hooks 误报") */
|
|
92
|
+
protected dailyWarnText(noPlan: number, threshold: number): string {
|
|
93
|
+
return `[Hook ⚠️] 无 Plan 提示已达 ${noPlan} 次(≥${threshold}),建议创建 Plan 或检查 hooks 误报\n`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Layer 3 附加注入(core: 无) */
|
|
97
|
+
protected afterLayer3(): void {
|
|
98
|
+
// core 协议: 无附加注入
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 日报跳过条件(core: 不跳过;claude: MAGIC_DIR 未设置时跳过) */
|
|
102
|
+
protected shouldSkipDaily(): boolean {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 前置注入(core: 无;qoder: 无条件 "ADD workflow active" JSON) */
|
|
107
|
+
protected preamble(_input: string): void {
|
|
108
|
+
// core 协议: 无前置注入
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Layer 3 输出形态(core: 纯文本逐行;qoder: hookSpecificOutput JSON 包) */
|
|
112
|
+
protected layer3Json(): boolean {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 日报并入 Layer3 上下文(core: 独立行输出;qoder: 并入 additionalContext) */
|
|
117
|
+
protected dailyInContext(): boolean {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────── 流程固化 ───────────────────────────
|
|
122
|
+
|
|
123
|
+
/** 主路由(模板方法):返回 exit code(0 放行) */
|
|
124
|
+
run(input: string): number {
|
|
125
|
+
const prompt = this.extractPrompt(input)
|
|
126
|
+
if (prompt === "") return 0
|
|
127
|
+
|
|
128
|
+
// 前置注入(qoder: 无条件 JSON——bash 原文位于空 prompt 检查之后)
|
|
129
|
+
this.preamble(input)
|
|
130
|
+
|
|
131
|
+
// ─── Layer 1: 精准 P0 触发词 ───
|
|
132
|
+
const matched = matchTrigger(prompt)
|
|
133
|
+
if (matched.length > 0) {
|
|
134
|
+
// 验收幂等保护: 如果已验收,提示不重复
|
|
135
|
+
if (/验收|收敛/i.test(prompt)) {
|
|
136
|
+
const addState = detectActiveAdd()
|
|
137
|
+
if (addState !== null) {
|
|
138
|
+
const handoff = addState.split("::")[3] ?? ""
|
|
139
|
+
const addRoute = addState.split("::")[4] ?? ""
|
|
140
|
+
if (isAlreadyAccepted(addRoute, handoff)) {
|
|
141
|
+
process.stdout.write(this.acceptedText())
|
|
142
|
+
this.onAccepted(handoff, addRoute)
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const m of matched) process.stdout.write(m + "\n")
|
|
148
|
+
return 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── 开发关键词检测(动态加载) ───
|
|
152
|
+
const devKw = loadDevKeywords()
|
|
153
|
+
if (devKw.length === 0) return 0
|
|
154
|
+
|
|
155
|
+
if (!this.devKwMatched(prompt, devKw)) return 0
|
|
156
|
+
|
|
157
|
+
this.onDevKwMatched()
|
|
158
|
+
|
|
159
|
+
// ─── Layer 2/3: 按活跃 ADD 分流 ───
|
|
160
|
+
const state = detectActiveAdd()
|
|
161
|
+
if (state === null) {
|
|
162
|
+
// Layer 2: 无活跃 ADD → 提示启动(不阻断)
|
|
163
|
+
const text = `[ADD 提示] 检测到开发任务,但无活跃 ADD Plan。建议先执行 add-paradigm SKILL:
|
|
164
|
+
Step 0: 文档先行 (Plan → Review → Specs)
|
|
165
|
+
Step 3: 代码实现 + 审计植入
|
|
166
|
+
Step 8: 收敛判断
|
|
167
|
+
`
|
|
168
|
+
if (this.layer2ToStderr()) {
|
|
169
|
+
process.stderr.write(text)
|
|
170
|
+
} else {
|
|
171
|
+
process.stdout.write(text)
|
|
172
|
+
}
|
|
173
|
+
writeHookEvent("prompt-submit", "info", prompt, "无活跃 ADD Plan 下检测到开发任务", "no-active-plan", "none")
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Layer 3: 有活跃 ADD → 注入状态
|
|
178
|
+
const plan = state.split("::")[0] ?? ""
|
|
179
|
+
const step = state.split("::")[1] ?? ""
|
|
180
|
+
const rounds = state.split("::")[2] ?? ""
|
|
181
|
+
const handoff = state.split("::")[3] ?? ""
|
|
182
|
+
let block = this.layer3Text(plan, rounds, step, handoff)
|
|
183
|
+
|
|
184
|
+
// ─── Hook 治理日报(阈值真源: hook-event-rules.toml [event.daily.warn_threshold])───
|
|
185
|
+
if (!this.shouldSkipDaily()) {
|
|
186
|
+
const HOOK_JSONL = join(this.magicDir, "reports", "hook-events.jsonl")
|
|
187
|
+
if (existsSync(HOOK_JSONL)) {
|
|
188
|
+
const today = localIsoSeconds().slice(0, 10) // 对齐 bash date +%Y-%m-%d(本地日期)
|
|
189
|
+
const content = readFileSync(HOOK_JSONL, "utf-8")
|
|
190
|
+
const todayLines = content.split("\n").filter((l) => l.includes(`"ts":"${today}`))
|
|
191
|
+
const total = todayLines.length
|
|
192
|
+
const noPlan = todayLines.filter((l) => l.includes('"planKeyword":"no-active-plan"')).length
|
|
193
|
+
if (total > 0) {
|
|
194
|
+
const dailyLine = `[Hook 治理] 今日提示: ${total} 次 | 无 Plan 提示: ${noPlan} 次`
|
|
195
|
+
const warnThreshold = (event.daily as { warn_threshold: number }).warn_threshold
|
|
196
|
+
const warnLine = noPlan >= warnThreshold ? this.dailyWarnText(noPlan, warnThreshold) : ""
|
|
197
|
+
if (this.dailyInContext()) {
|
|
198
|
+
// qoder 协议: 日报并入 additionalContext(\n 前缀 + 警告尾行)
|
|
199
|
+
block += `\n${dailyLine}`
|
|
200
|
+
if (warnLine) block += `\n${warnLine.trimEnd()}`
|
|
201
|
+
} else {
|
|
202
|
+
// core/claude 协议: 日报独立 stdout 行(警告随层3文本后输出)
|
|
203
|
+
process.stdout.write(dailyLine + "\n")
|
|
204
|
+
if (warnLine) process.stdout.write(warnLine)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.layer3Json()) {
|
|
211
|
+
// qoder 协议: hookSpecificOutput JSON 包
|
|
212
|
+
process.stdout.write(
|
|
213
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: block.trimEnd() } }) + "\n"
|
|
214
|
+
)
|
|
215
|
+
} else {
|
|
216
|
+
process.stdout.write(block)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.afterLayer3()
|
|
220
|
+
return 0
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// review-checklist-guard.ts — 验收 Review 模式:checklist 质量检查(治理逻辑层,Task 4.1 收敛)
|
|
2
|
+
// bash 版 review-checklist.sh 的 TS 同语义实现(源自 qoder 私有 lib,R2 回流收敛 core);
|
|
3
|
+
// 输入: handoff 路径 + add_route 路径;输出: 质量问题清单文本(逐字对齐 bash)。
|
|
4
|
+
// 参数化: specRoot(qoder: .qoder/specs;其余端按协议注入)+ PROJECT_DIR。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
7
|
+
import { join, dirname, basename } from "node:path"
|
|
8
|
+
import { tryResolveMagicDir } from "./common.js"
|
|
9
|
+
|
|
10
|
+
/** 从 checklist 提取全部 [x] 行(对齐 bash grep '\[x\]') */
|
|
11
|
+
function checkedLines(content: string): string[] {
|
|
12
|
+
return content.split("\n").filter((l) => l.includes("[x]"))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 定位 spec 目录下的 checklist/tasks(对齐 bash 目录优先 + find fallback) */
|
|
16
|
+
function locateSpecFile(specRoot: string, planKw: string, name: string): string {
|
|
17
|
+
const specDir = join(specRoot, planKw)
|
|
18
|
+
const direct = join(specDir, name)
|
|
19
|
+
if (existsSync(direct)) return direct
|
|
20
|
+
// fallback: 搜索(对齐 bash find .qoder/specs -name ... -path "*${plan_kw}*")
|
|
21
|
+
if (existsSync(specRoot)) {
|
|
22
|
+
for (const dir of readdirSync(specRoot)) {
|
|
23
|
+
if (!dir.includes(planKw)) continue
|
|
24
|
+
const cand = join(specRoot, dir, name)
|
|
25
|
+
if (existsSync(cand)) return cand
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return direct
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* spec 目录默认定位(治理层中立——禁止偏爱任何 adapter):
|
|
33
|
+
* <projectDir>/<magicDir>/specs(magicDir 唯一解析链推导);推导失败 → ""(fail-closed)。
|
|
34
|
+
* adapter 协议差异(如 qoder 显式 .qoder/specs)由调用方显式传参,不下沉到治理层默认值。
|
|
35
|
+
*/
|
|
36
|
+
function defaultSpecRoot(): string {
|
|
37
|
+
const projectDir = process.env.PROJECT_DIR || process.cwd()
|
|
38
|
+
const magicDir = tryResolveMagicDir()
|
|
39
|
+
return magicDir ? join(projectDir, magicDir, "specs") : ""
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* checklist 质量检查(逐字对齐 bash review-checklist.sh)。
|
|
44
|
+
* 返回: 问题清单文本(无问题时为 " ✅ Review: checklist 质量检查通过")。
|
|
45
|
+
* Step 0 准入失败时返回 " ❌ Step 0 未完成:..."(调用方 exit 1 语义由 CLI 层执行)。
|
|
46
|
+
*/
|
|
47
|
+
export function checkReviewQuality(handoff: string, addRoute: string, specRoot?: string): string {
|
|
48
|
+
if (handoff === "") return ""
|
|
49
|
+
const projectDir = process.env.PROJECT_DIR || process.cwd()
|
|
50
|
+
const planKw = basename(handoff).replace(/-handoff.*$/, "")
|
|
51
|
+
const root = specRoot ?? defaultSpecRoot()
|
|
52
|
+
const checklist = locateSpecFile(root, planKw, "checklist.md")
|
|
53
|
+
const tasksFile = locateSpecFile(root, planKw, "tasks.md")
|
|
54
|
+
|
|
55
|
+
let issues = ""
|
|
56
|
+
|
|
57
|
+
// ── 0. Step 0 准入: ADD 核心文档存在性(BLOCKING)──
|
|
58
|
+
let missingDocs = ""
|
|
59
|
+
if (!existsSync(handoff)) {
|
|
60
|
+
missingDocs += " Handoff"
|
|
61
|
+
} else {
|
|
62
|
+
const h = readFileSync(handoff, "utf-8")
|
|
63
|
+
let hSections = 0
|
|
64
|
+
if (h.includes("spec 文件")) hSections++
|
|
65
|
+
if (h.includes("你要改的文件")) hSections++
|
|
66
|
+
if (h.includes("验证标准")) hSections++
|
|
67
|
+
if (h.includes("完成后记录 ADD-7 审计")) hSections++
|
|
68
|
+
if (hSections < 4) missingDocs += ` Handoff(缺章节:${hSections}/4)`
|
|
69
|
+
}
|
|
70
|
+
if (!existsSync(addRoute)) {
|
|
71
|
+
missingDocs += " add-route"
|
|
72
|
+
} else {
|
|
73
|
+
const ar = readFileSync(addRoute, "utf-8")
|
|
74
|
+
if (!ar.includes("Task 映射表") && !ar.includes("文件清单")) {
|
|
75
|
+
missingDocs += " add-route(缺Task映射/文件清单)"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!existsSync(checklist)) {
|
|
79
|
+
missingDocs += " checklist"
|
|
80
|
+
} else {
|
|
81
|
+
const cl = readFileSync(checklist, "utf-8")
|
|
82
|
+
if (!cl.includes("[T]")) missingDocs += " checklist(无[T]项)"
|
|
83
|
+
}
|
|
84
|
+
if (!existsSync(tasksFile)) {
|
|
85
|
+
missingDocs += " tasks"
|
|
86
|
+
} else {
|
|
87
|
+
const tk = readFileSync(tasksFile, "utf-8")
|
|
88
|
+
if (!/Task|\[ \]/.test(tk)) missingDocs += " tasks(无Task项)"
|
|
89
|
+
}
|
|
90
|
+
if (missingDocs !== "") {
|
|
91
|
+
return ` ❌ Step 0 未完成:${missingDocs.trim().replace(/ /g, ", ")} 文件缺失。回退 Step 0.5/Step 1 补建后再进入代码实现。`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 1-4. checklist 质量 ──
|
|
95
|
+
if (existsSync(checklist) && existsSync(tasksFile)) {
|
|
96
|
+
const cl = readFileSync(checklist, "utf-8")
|
|
97
|
+
const clChecked = checkedLines(cl).length
|
|
98
|
+
const clOpen = (cl.match(/\[ \]/g) || []).length
|
|
99
|
+
const tkChecked = (readFileSync(tasksFile, "utf-8").match(/\[x\]/g) || []).length
|
|
100
|
+
|
|
101
|
+
if (clOpen > 0) {
|
|
102
|
+
issues += ` ⚠️ checklist 有 ${clOpen} 项未勾选\n`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. [T] 项是否都有 [x]
|
|
106
|
+
const tItems = (cl.match(/\[T\]/g) || []).length
|
|
107
|
+
const tChecked = checkedLines(cl).filter((l) => l.includes("[T]")).length
|
|
108
|
+
if (tItems > tChecked) {
|
|
109
|
+
issues += ` ⚠️ [T] 编译期验证: ${tChecked}/${tItems} 通过\n`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. 证据缺失检测:[x] 但缺 — 证据: 标记
|
|
113
|
+
let noEvidence = 0
|
|
114
|
+
for (const line of checkedLines(cl)) {
|
|
115
|
+
if (/—\s*证据:\s*\S/.test(line)) continue
|
|
116
|
+
if (/npx|tsc|vitest|grep|✅|cmq[0-9a-z]{10}|18\/18|exit.*0/.test(line)) continue
|
|
117
|
+
noEvidence++
|
|
118
|
+
}
|
|
119
|
+
if (noEvidence > 0) {
|
|
120
|
+
issues += ` ❌ ${noEvidence} 项 [x] 缺少验收证据(需附 — 证据: tsc/vitest/grep/审计ID 等)\n`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. 审计链
|
|
124
|
+
const withAudit = checkedLines(cl).filter((l) => /cmq[a-z0-9]{10,}/.test(l)).length
|
|
125
|
+
const fakeAudit = checkedLines(cl).filter((l) => /cmq\.\.\.|cmqxxx|审计.*cmq\.\./.test(l)).length
|
|
126
|
+
if (fakeAudit > 0) {
|
|
127
|
+
issues += ` ❌ ${fakeAudit} 项 [x] 使用了占位符审计ID(cmq.../cmqxxx),必须调 record_dev_operation 获取真实 cuid 后替换\n`
|
|
128
|
+
}
|
|
129
|
+
const withEvidence = checkedLines(cl).filter((l) =>
|
|
130
|
+
/tsc|vitest|npx|grep|✅|验证|确认|compgen|审计.*cmq[a-z0-9]{10}/.test(l)
|
|
131
|
+
).length
|
|
132
|
+
if (withEvidence > 0 && withAudit === 0) {
|
|
133
|
+
issues += ` 📎 初验: ${withEvidence}/${clChecked} 项有证据但未写审计 ID(需调 record_dev_operation 落库)\n`
|
|
134
|
+
} else if (withAudit > 0 && withAudit < clChecked) {
|
|
135
|
+
issues += ` 📎 复验: ${withAudit}/${clChecked} 项引用审计 ID。${withEvidence}/${clChecked} 项有证据。证据一致则不需追写 devlog日志(走mcp)\n`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 5. add-route Step 闭环
|
|
140
|
+
if (existsSync(addRoute)) {
|
|
141
|
+
const ar = readFileSync(addRoute, "utf-8")
|
|
142
|
+
const arOpen = (ar.match(/\[ \]/g) || []).length
|
|
143
|
+
if (arOpen > 0) {
|
|
144
|
+
issues += ` ⚠️ add-route ${arOpen} Step 未闭环\n`
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 6. handoff 审计表同步
|
|
149
|
+
if (existsSync(handoff) && existsSync(checklist)) {
|
|
150
|
+
const h = readFileSync(handoff, "utf-8")
|
|
151
|
+
const cl = readFileSync(checklist, "utf-8")
|
|
152
|
+
const cuids = [...new Set(cl.match(/cmq[a-z0-9]{10,}/g) || [])]
|
|
153
|
+
const newCuids = cuids.filter((c) => !h.includes(c))
|
|
154
|
+
if (newCuids.length > 0) {
|
|
155
|
+
issues += ` ❌ handoff 审计表未同步: ${newCuids.length} 个 cuid 在 checklist 中存在但 handoff 中缺失(需更新 handoff ADD-7 表 + query_audit_logs 命令)\n`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (issues === "") {
|
|
160
|
+
return " ✅ Review: checklist 质量检查通过"
|
|
161
|
+
}
|
|
162
|
+
return ` 📋 Review 发现问题:\n${issues}`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* ReviewChecklist 服务类(OOP 封装,Task 5.1 继承体系):
|
|
167
|
+
* run() = 检查 + 输出 + exit 语义(Step 0 准入失败 → exit 1,对齐 bash 原文)。
|
|
168
|
+
* specRoot 缺省 = 治理层中立推导(<projectDir>/<magicDir>/specs);adapter 显式传参覆盖。
|
|
169
|
+
*/
|
|
170
|
+
export class ReviewChecklistGuard {
|
|
171
|
+
private readonly specRoot: string | undefined
|
|
172
|
+
|
|
173
|
+
constructor(specRoot?: string) {
|
|
174
|
+
this.specRoot = specRoot
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 主入口:返回 exit code(0 放行 / 1 Step 0 准入失败) */
|
|
178
|
+
run(handoff: string, addRoute: string): number {
|
|
179
|
+
if (handoff === "") return 0
|
|
180
|
+
const out = checkReviewQuality(handoff, addRoute, this.specRoot)
|
|
181
|
+
process.stdout.write(out + "\n")
|
|
182
|
+
// Step 0 准入失败 → exit 1(对齐 bash)
|
|
183
|
+
if (out.startsWith(" ❌ Step 0 未完成")) return 1
|
|
184
|
+
return 0
|
|
185
|
+
}
|
|
186
|
+
}
|