add-coder 0.3.27 → 0.3.29
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 +252 -150
- 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,440 @@
|
|
|
1
|
+
// lib/doc-format-guard.ts — doc-format-guard 守卫服务(治理逻辑层,Task 2.1 类化收敛)
|
|
2
|
+
// 规则真源: hook-doc-format-rules.toml([doc.token_rules] / [doc.content_rules] /
|
|
3
|
+
// [doc.handoff] / [doc.incremental] / [doc.anti_cheat])
|
|
4
|
+
// 行为等价红线: 识别序(token → 内容探测 → 增量豁免 → 拒绝)与 bash 逐字一致
|
|
5
|
+
//
|
|
6
|
+
// 设计范式: OOP 守卫服务(模板识别 + schema 校验 + 算法规则聚合)+ 纯函数判定。
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync, readdirSync, unlinkSync } from "node:fs"
|
|
9
|
+
import { join, basename, dirname } from "node:path"
|
|
10
|
+
import { spawnSync } from "node:child_process"
|
|
11
|
+
import { detectActiveAdd, localIsoSeconds } from "./common.js"
|
|
12
|
+
import { writeHookEvent } from "./notify.js"
|
|
13
|
+
import { doc } from "./rules.js"
|
|
14
|
+
|
|
15
|
+
interface SchemaSection {
|
|
16
|
+
id?: string
|
|
17
|
+
heading?: string
|
|
18
|
+
required?: boolean
|
|
19
|
+
anchor?: string
|
|
20
|
+
within?: string
|
|
21
|
+
subsections?: Array<{ heading?: string }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SchemaFile {
|
|
25
|
+
sections: SchemaSection[]
|
|
26
|
+
placeholders?: string[]
|
|
27
|
+
forbidden_terms?: string[]
|
|
28
|
+
groupColumn?: number | string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** token 规则条目(rules.doc.token_rules 结构) */
|
|
32
|
+
interface TokenRule {
|
|
33
|
+
template: string
|
|
34
|
+
tokens: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 内容探测条目(rules.doc.content_rules / adapter_content_rules 结构) */
|
|
38
|
+
interface ContentRule {
|
|
39
|
+
adapter?: string
|
|
40
|
+
marker: string
|
|
41
|
+
template: string
|
|
42
|
+
sub_markers?: Array<{ marker: string; template: string }>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 纯函数:提取内容(对齐 bash jq 的 if/elif 链) */
|
|
46
|
+
function extractContent(input: Record<string, unknown>): string {
|
|
47
|
+
const ti = input.tool_input as Record<string, unknown> | undefined
|
|
48
|
+
if (!ti) return ""
|
|
49
|
+
if (typeof ti.file_content === "string") return ti.file_content
|
|
50
|
+
if (typeof ti.content === "string") return ti.content
|
|
51
|
+
if (typeof ti.new_string === "string") return ti.new_string
|
|
52
|
+
const reps = ti.replacements as Array<{ new_text?: unknown }> | undefined
|
|
53
|
+
if (reps && reps.length > 0 && typeof reps[0].new_text === "string") return reps[0].new_text
|
|
54
|
+
return ""
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 纯函数:模板名 token 匹配(数据驱动,特异性优先,对齐 bash case 链) */
|
|
58
|
+
function templateNameByToken(filePath: string): string {
|
|
59
|
+
const base = basename(filePath).replace(/-v\d.*$/, "")
|
|
60
|
+
for (const rule of (doc.token_rules as unknown as TokenRule[]) ?? []) {
|
|
61
|
+
if (rule.tokens.every((t) => base.includes(t))) return rule.template
|
|
62
|
+
}
|
|
63
|
+
return ""
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 纯函数:内容特征探测(数据驱动,命中即返回;无法识别返回 null)
|
|
67
|
+
* adapterName: 缺省 core 基线链;adapter 子类可通过构造参数加载 [doc.adapter_content_rules] 独立链 */
|
|
68
|
+
function templateNameByContent(content: string, filePath: string, adapterName: string): string | null {
|
|
69
|
+
const rules = adapterName === "core"
|
|
70
|
+
? (doc.content_rules as unknown as ContentRule[]) ?? []
|
|
71
|
+
: ((doc.adapter_content_rules as unknown as ContentRule[]) ?? []).filter((r) => r.adapter === adapterName)
|
|
72
|
+
for (const rule of rules) {
|
|
73
|
+
if (content.includes(rule.marker)) {
|
|
74
|
+
for (const sub of rule.sub_markers ?? []) {
|
|
75
|
+
if (content.includes(sub.marker)) return sub.template
|
|
76
|
+
}
|
|
77
|
+
return rule.template
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// handoff 内容识别(真源: [doc.handoff])
|
|
81
|
+
if (filePath.includes("handoff")) {
|
|
82
|
+
const handoffRules = doc.handoff as { marker_multi: string; marker_single: string }
|
|
83
|
+
if (content.includes(handoffRules.marker_multi)) return "handoff-multi-round-template.md"
|
|
84
|
+
if (content.includes(handoffRules.marker_single)) return "handoff-single-round-template.md"
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
// 文件名 fallback(真源: [doc.fallback_rules],顺序与 token 表不同——plan 优先)
|
|
88
|
+
for (const rule of (doc.fallback_rules as unknown as TokenRule[]) ?? []) {
|
|
89
|
+
if (rule.tokens.every((t) => filePath.includes(t))) return rule.template
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 增量修订识别(真源: [doc.incremental.regex],对齐 bash:~~删除线~~ / → / [修订日期]) */
|
|
95
|
+
function isIncrementalEdit(content: string): boolean {
|
|
96
|
+
const regex = (doc.incremental as { regex: string }).regex
|
|
97
|
+
return new RegExp(regex).test(content)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* doc-format-guard 守卫服务(export: adapter 子类可继承 override 探测链):
|
|
102
|
+
* - 模板识别(token → 内容特征 → 增量修订豁免)
|
|
103
|
+
* - schema 校验(章节/子章节/占位符/结构位禁词 + 锚定)
|
|
104
|
+
* - 算法化规则(精简版反作弊 / HITL 表非空 / handoff 冲突)
|
|
105
|
+
* - struct_score 精确语义(缺失规则数 / 适用规则数)
|
|
106
|
+
*/
|
|
107
|
+
export class DocFormatGuard {
|
|
108
|
+
private readonly projectDir: string
|
|
109
|
+
private readonly magicDir: string
|
|
110
|
+
private readonly planKeyword: string
|
|
111
|
+
private readonly planStatus: string
|
|
112
|
+
/** adapter 名(缺省 core;adapter 子类传名加载独立内容探测链) */
|
|
113
|
+
protected readonly adapterName: string
|
|
114
|
+
private readonly issues: string[] = []
|
|
115
|
+
|
|
116
|
+
constructor(projectDir: string, magicDir: string, adapterName: string = "core") {
|
|
117
|
+
this.projectDir = projectDir
|
|
118
|
+
this.magicDir = magicDir
|
|
119
|
+
this.adapterName = adapterName
|
|
120
|
+
const active = detectActiveAdd()
|
|
121
|
+
if (active !== null) {
|
|
122
|
+
this.planKeyword = active.split("::")[0] ?? ""
|
|
123
|
+
this.planStatus = "active"
|
|
124
|
+
} else {
|
|
125
|
+
this.planKeyword = "no-active-plan"
|
|
126
|
+
this.planStatus = "none"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 识别模板:token → 内容 → null(无法识别);handoff 内容无法识别时由调用方阻断
|
|
131
|
+
* protected: adapter 子类可 override 探测链(如 claude 版 simple-standard-plan 分支) */
|
|
132
|
+
protected identifyTemplate(filePath: string, content: string): { name: string } | { handoffUnrecognized: true } | null {
|
|
133
|
+
let name = templateNameByToken(filePath)
|
|
134
|
+
if (name === "") {
|
|
135
|
+
const byContent = templateNameByContent(content, filePath, this.adapterName)
|
|
136
|
+
if (byContent === null && filePath.includes("handoff")) {
|
|
137
|
+
return { handoffUnrecognized: true }
|
|
138
|
+
}
|
|
139
|
+
if (byContent === null) {
|
|
140
|
+
// 增量修订识别
|
|
141
|
+
if (isIncrementalEdit(content)) {
|
|
142
|
+
return null // 放行(增量修订跳过完整校验)
|
|
143
|
+
}
|
|
144
|
+
return null // 无法识别(调用方阻断)
|
|
145
|
+
}
|
|
146
|
+
name = byContent
|
|
147
|
+
}
|
|
148
|
+
return { name }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 读取 schema.json(含默认空结构) */
|
|
152
|
+
private loadSchema(templateName: string): SchemaFile | null {
|
|
153
|
+
const schemaFile = join(this.projectDir, this.magicDir, "templates", templateName.replace(/\.md$/, ".schema.json"))
|
|
154
|
+
if (!existsSync(schemaFile)) return null
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(readFileSync(schemaFile, "utf-8")) as SchemaFile
|
|
157
|
+
} catch {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 章节/锚定/占位符/禁词校验,返回 struct 统计 */
|
|
163
|
+
private runSchemaChecks(schema: SchemaFile, templateName: string, content: string, isSearchReplace: boolean): { applied: number; missed: number; anchorHit: boolean } {
|
|
164
|
+
const templatesDir = join(this.projectDir, this.magicDir, "templates")
|
|
165
|
+
let applied = 0
|
|
166
|
+
let missed = 0
|
|
167
|
+
let anchorHit = true
|
|
168
|
+
|
|
169
|
+
const headings = schema.sections.filter((s) => s.heading).map((s) => s.heading as string)
|
|
170
|
+
const requiredHeadings = schema.sections.filter((s) => s.required === true && s.heading).map((s) => s.heading as string)
|
|
171
|
+
const subs = schema.sections.flatMap((s) => s.subsections ?? []).filter((s) => s.heading).map((s) => s.heading as string)
|
|
172
|
+
const placeholders = schema.placeholders ?? []
|
|
173
|
+
const terms = schema.forbidden_terms ?? []
|
|
174
|
+
|
|
175
|
+
if (!isSearchReplace) {
|
|
176
|
+
// 锚定校验
|
|
177
|
+
for (const section of schema.sections) {
|
|
178
|
+
if (!section.anchor) continue
|
|
179
|
+
applied++
|
|
180
|
+
const templateContent = existsSync(join(templatesDir, templateName))
|
|
181
|
+
? readFileSync(join(templatesDir, templateName), "utf-8")
|
|
182
|
+
: ""
|
|
183
|
+
const refLine = templateContent.split("\n").find((l) => l.includes(section.anchor as string))
|
|
184
|
+
if (!refLine) {
|
|
185
|
+
process.stderr.write(`[doc-format-guard] anchor_miss: schema ${section.id} 声明的 anchor '${section.anchor}' 在 ${templateName} 中未定位,跳过该规则(冒烟巡检兑底)\n`)
|
|
186
|
+
applied--
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
const tokens = [...new Set(refLine.replace(/[#*`|(){]/g, " ").split(/\s+/).filter((t) => t !== "" && !t.includes("{")))]
|
|
190
|
+
if (tokens.length === 0) {
|
|
191
|
+
applied--
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
let scope = content
|
|
195
|
+
if (section.within) {
|
|
196
|
+
if (content.includes(section.within)) {
|
|
197
|
+
const startIdx = content.indexOf(section.within)
|
|
198
|
+
const endIdx = content.indexOf("\n## ", startIdx + 1)
|
|
199
|
+
scope = content.slice(startIdx, endIdx === -1 ? undefined : endIdx)
|
|
200
|
+
} else {
|
|
201
|
+
process.stderr.write(`[doc-format-guard] within_miss: schema ${section.id} 的 within '${section.within}' 在文档中未定位,跳过该规则\n`)
|
|
202
|
+
applied--
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const missTokens = tokens.filter((tok) => !scope.includes(tok))
|
|
207
|
+
if (missTokens.length > 0) {
|
|
208
|
+
this.issues.push(` 缺锚点(${section.id}): ${missTokens.join(" ")}`)
|
|
209
|
+
missed++
|
|
210
|
+
anchorHit = false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 必选章节
|
|
215
|
+
for (const heading of requiredHeadings) {
|
|
216
|
+
applied++
|
|
217
|
+
if (!content.includes(heading)) {
|
|
218
|
+
this.issues.push(` 缺章节: ${heading}`)
|
|
219
|
+
missed++
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// 子章节
|
|
223
|
+
for (const sub of subs) {
|
|
224
|
+
applied++
|
|
225
|
+
if (!content.includes(sub)) {
|
|
226
|
+
this.issues.push(` 缺子章节: ${sub}`)
|
|
227
|
+
missed++
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 占位符
|
|
233
|
+
for (const ph of placeholders) {
|
|
234
|
+
if (content.includes(ph)) {
|
|
235
|
+
this.issues.push(` 未替换占位符: ${ph}`)
|
|
236
|
+
missed++
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 结构位禁词(标题行 + groupColumn)
|
|
241
|
+
let structText = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n")
|
|
242
|
+
const col = typeof schema.groupColumn === "number" ? schema.groupColumn : Number(schema.groupColumn)
|
|
243
|
+
if (!Number.isNaN(col) && col > 0) {
|
|
244
|
+
const colLines = content
|
|
245
|
+
.split("\n")
|
|
246
|
+
.map((l) => {
|
|
247
|
+
const cells = l.split("|")
|
|
248
|
+
return cells.length > col ? (cells[col + 1] ?? "").trim() : ""
|
|
249
|
+
})
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
structText += "\n" + colLines.join("\n")
|
|
252
|
+
}
|
|
253
|
+
for (const term of terms) {
|
|
254
|
+
applied++
|
|
255
|
+
if (structText.includes(term)) {
|
|
256
|
+
this.issues.push(` 结构位禁词: ${term}`)
|
|
257
|
+
missed++
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { applied, missed, anchorHit }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 算法化规则校验(真源: [doc.anti_cheat] + HITL 表非空 + handoff 冲突)
|
|
265
|
+
* protected: adapter 子类可 override(如 claude 版无算法规则段) */
|
|
266
|
+
protected runAlgoChecks(templateName: string, filePath: string, content: string): void {
|
|
267
|
+
const antiCheat = doc.anti_cheat as {
|
|
268
|
+
max_file_count: number
|
|
269
|
+
fuzzy_file_regex: string
|
|
270
|
+
fuzzy_decision_regex: string
|
|
271
|
+
forbidden_heading: string
|
|
272
|
+
}
|
|
273
|
+
if (templateName.includes("simple-plan")) {
|
|
274
|
+
const fileCount = (content.match(/^\|\s*`[^`]+`/gm) || []).length
|
|
275
|
+
if (fileCount > antiCheat.max_file_count) {
|
|
276
|
+
this.issues.push(` ❌ 精简版反作弊: 涉及 ${fileCount} 个文件(超过 ${antiCheat.max_file_count} 个限制),应改用 standard-plan-template.md`)
|
|
277
|
+
}
|
|
278
|
+
if (new RegExp(antiCheat.fuzzy_file_regex).test(content)) {
|
|
279
|
+
this.issues.push(" ❌ 精简版反作弊: HITL 表文件清单使用模糊描述('等 N 个文件'),必须列出实际完整路径")
|
|
280
|
+
}
|
|
281
|
+
if (new RegExp(antiCheat.fuzzy_decision_regex).test(content)) {
|
|
282
|
+
this.issues.push(" ❌ 精简版反作弊: HITL 表方案/设计决策使用模糊描述('等若干决策'),必须逐条列出")
|
|
283
|
+
}
|
|
284
|
+
if (content.includes(antiCheat.forbidden_heading)) {
|
|
285
|
+
this.issues.push(" ❌ 精简版反作弊: 包含 '## 三、架构设计' 章节,精简版不应有架构设计——应改用 standard-plan-template.md")
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (/plan|review/.test(templateName)) {
|
|
290
|
+
if (content.includes("## HITL")) {
|
|
291
|
+
const hitlSection = content.slice(content.indexOf("## HITL"))
|
|
292
|
+
const hitlRows = (hitlSection.match(/^\|\s*[^|{]*\s*\|/gm) || []).length
|
|
293
|
+
const hitlData = hitlRows - 2
|
|
294
|
+
if (hitlData < 1) {
|
|
295
|
+
this.issues.push(" ⚠️ HITL 表为空——必须填写至少 1 行实际内容后再提交审核")
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (templateName.includes("simple-plan")) {
|
|
301
|
+
const planBase = basename(filePath).replace(/-plan-v.*/, "")
|
|
302
|
+
const planDir = dirname(filePath)
|
|
303
|
+
const pattern = `${planBase}-handoff`
|
|
304
|
+
const found = (() => {
|
|
305
|
+
try {
|
|
306
|
+
return readdirSync(planDir).some((f) => f.startsWith(pattern) && f.endsWith(".md"))
|
|
307
|
+
} catch {
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
})()
|
|
311
|
+
if (found) {
|
|
312
|
+
this.issues.push(` ❌ 精简版 Handoff 冲突: 检测到独立 handoff 文件(${pattern}*.md)。精简版 Plan 的 Handoff 已融合在 §四,不应生成独立文件。请删除独立 handoff 文件或改用 standard-plan-template.md`)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 主入口:返回 exit code(0 放行 / 2 阻断) */
|
|
318
|
+
run(inputRaw: string): number {
|
|
319
|
+
const input: Record<string, unknown> = (() => {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(inputRaw) as Record<string, unknown>
|
|
322
|
+
} catch {
|
|
323
|
+
return {}
|
|
324
|
+
}
|
|
325
|
+
})()
|
|
326
|
+
|
|
327
|
+
// DEBUG dump(对齐 bash L24-38;非空输入且 tool_input 为 null 时 jq 报错泄漏到 stderr;
|
|
328
|
+
// 空输入 jq 无输出不报错——2026-08-14 Task 5.1 修正: 原缺空输入判定,被 refresh-fixed 掩盖)
|
|
329
|
+
const ti = input.tool_input as Record<string, unknown> | undefined
|
|
330
|
+
if (inputRaw.trim() !== "" && !ti) {
|
|
331
|
+
process.stderr.write("jq: error (at <stdin>:1): null (null) has no keys\n")
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const debugDir = join(this.projectDir, this.magicDir, "debug-dump")
|
|
335
|
+
mkdirSync(debugDir, { recursive: true })
|
|
336
|
+
const log = [
|
|
337
|
+
`=== ${localIsoSeconds()} ===`,
|
|
338
|
+
`file_path: ${typeof ti?.file_path === "string" ? ti.file_path : "EMPTY"}`,
|
|
339
|
+
`has_file_content: ${ti && typeof ti.file_content === "string"}`,
|
|
340
|
+
`has_replacements: ${ti && Array.isArray(ti.replacements)}`,
|
|
341
|
+
]
|
|
342
|
+
if (ti && typeof ti.file_content === "string") {
|
|
343
|
+
log.push(`[file_content[500]]: ${ti.file_content.slice(0, 500)}`)
|
|
344
|
+
}
|
|
345
|
+
if (ti && Array.isArray(ti.replacements) && (ti.replacements[0] as { new_text?: string })?.new_text) {
|
|
346
|
+
log.push(`[replacement_new_text[500]]: ${(ti.replacements[0] as { new_text: string }).new_text.slice(0, 500)}`)
|
|
347
|
+
}
|
|
348
|
+
log.push(`top_keys: ${Object.keys(input).join(", ")}`)
|
|
349
|
+
log.push(`tool_input_keys: ${ti ? Object.keys(ti).join(", ") : "NO_TOOL_INPUT"}`)
|
|
350
|
+
log.push("=== DONE ===")
|
|
351
|
+
appendFileSync(join(debugDir, "stdin.log"), log.join("\n") + "\n")
|
|
352
|
+
} catch {
|
|
353
|
+
/* ignore */
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const filePath = typeof ti?.file_path === "string" ? ti.file_path : ""
|
|
357
|
+
if (filePath === "") return 0
|
|
358
|
+
|
|
359
|
+
if (!new RegExp(`${this.magicDir}/(plans|specs)/`).test(filePath)) return 0
|
|
360
|
+
|
|
361
|
+
const CONTENT = extractContent(input)
|
|
362
|
+
if (CONTENT === "") {
|
|
363
|
+
process.stderr.write("⛔ 拒绝:Write 工具未传 file_content,无法校验手写文档格式\n")
|
|
364
|
+
process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Write 工具未传 file_content,无法校验手写文档。请用 SearchReplace 改写已有文件,或用 Write 工具重试。"}}\n')
|
|
365
|
+
writeHookEvent("doc-format-guard", "deny", "Write", "Write 工具未传 file_content", this.planKeyword, this.planStatus)
|
|
366
|
+
return 2
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const identified = this.identifyTemplate(filePath, CONTENT)
|
|
370
|
+
let TEMPLATE_NAME = ""
|
|
371
|
+
if (identified && "handoffUnrecognized" in identified) {
|
|
372
|
+
process.stderr.write("⛔ handoff 文件内容无法识别模板类型(缺 '## 全局元信息' 或 '## 1. 交接前状态'),拒绝写入\n")
|
|
373
|
+
process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"handoff 内容不符合 single/multi 模板规范"}}\n')
|
|
374
|
+
writeHookEvent("doc-format-guard", "deny", filePath, "handoff 模板类型无法识别", this.planKeyword, this.planStatus)
|
|
375
|
+
return 2
|
|
376
|
+
}
|
|
377
|
+
if (identified && "name" in identified) {
|
|
378
|
+
TEMPLATE_NAME = identified.name
|
|
379
|
+
} else {
|
|
380
|
+
// 增量修订豁免
|
|
381
|
+
if (isIncrementalEdit(CONTENT)) {
|
|
382
|
+
process.stderr.write("[doc-format-guard] 检测到增量修订格式,跳过完整章节校验\n")
|
|
383
|
+
return 0
|
|
384
|
+
}
|
|
385
|
+
process.stderr.write(`⛔ 拒绝:无法识别文档类型 (file_path: ${filePath}),缺少模板匹配规则\n`)
|
|
386
|
+
process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"无法识别 ADD 文档类型,请联系管理员更新 doc-format-guard"}}\n')
|
|
387
|
+
writeHookEvent("doc-format-guard", "deny", filePath, "无法识别文档类型", this.planKeyword, this.planStatus)
|
|
388
|
+
return 2
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const schema = this.loadSchema(TEMPLATE_NAME)
|
|
392
|
+
if (!schema) {
|
|
393
|
+
process.stderr.write(`⛔ 阻断:模板 ${TEMPLATE_NAME} 缺少对应的 .schema.json 校验规则\n`)
|
|
394
|
+
process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"缺少 .schema.json 校验规则文件,禁止无规则放行"}}\n')
|
|
395
|
+
writeHookEvent("doc-format-guard", "deny", filePath, "缺少 .schema.json 校验规则", this.planKeyword, this.planStatus)
|
|
396
|
+
return 2
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// SearchReplace 只传 patch → 跳过章节校验
|
|
400
|
+
const isSearchReplace = ti && Array.isArray(ti.replacements) && ti.replacements.length > 0
|
|
401
|
+
? true
|
|
402
|
+
: (ti && typeof ti.new_string === "string")
|
|
403
|
+
|
|
404
|
+
const { applied, missed, anchorHit } = this.runSchemaChecks(schema, TEMPLATE_NAME, CONTENT, Boolean(isSearchReplace))
|
|
405
|
+
this.runAlgoChecks(TEMPLATE_NAME, filePath, CONTENT)
|
|
406
|
+
|
|
407
|
+
const termTotal = (schema.forbidden_terms ?? []).length
|
|
408
|
+
const finalApplied = isSearchReplace ? termTotal : applied
|
|
409
|
+
const finalMissed = this.issues.length
|
|
410
|
+
let structScore = 100
|
|
411
|
+
if (finalApplied > 0 && finalMissed > 0) {
|
|
412
|
+
structScore = Math.max(0, Math.floor(((finalApplied - finalMissed) * 100) / finalApplied))
|
|
413
|
+
}
|
|
414
|
+
const BACKFLOW_EXTRA = `"anchor_hit":${anchorHit},"struct_score":${structScore}`
|
|
415
|
+
|
|
416
|
+
if (this.issues.length > 0) {
|
|
417
|
+
process.stderr.write(`⛔ ${TEMPLATE_NAME} 校验不通过:\n${this.issues.join("\n")}\n`)
|
|
418
|
+
const brief = this.issues.join(" ").replace(/"/g, "").replace(/\s+/g, " ").slice(0, 180)
|
|
419
|
+
process.stdout.write(`{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"文档格式校验不通过: ${brief}"}}\n`)
|
|
420
|
+
writeHookEvent("doc-format-guard", "deny", filePath, "文档格式校验不通过", this.planKeyword, this.planStatus, BACKFLOW_EXTRA)
|
|
421
|
+
return 2
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── 自动更新 index.md ──
|
|
425
|
+
if (new RegExp(`${this.magicDir}/plans/`).test(filePath)) {
|
|
426
|
+
const genIndex = join(this.projectDir, "scripts", "gen-plan-index.sh")
|
|
427
|
+
if (existsSync(genIndex)) {
|
|
428
|
+
try {
|
|
429
|
+
spawnSync("bash", [genIndex], { stdio: "ignore" })
|
|
430
|
+
} catch {
|
|
431
|
+
/* ignore */
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 放行回流
|
|
437
|
+
writeHookEvent("doc-format-guard", "allow", filePath, "校验通过", this.planKeyword, this.planStatus, BACKFLOW_EXTRA)
|
|
438
|
+
return 0
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// notification-router.ts — Notification Review 提醒路由(治理逻辑层,Task 3.1 继承体系)
|
|
2
|
+
// 治理卡位 #12: 开发提醒/Token 预警
|
|
3
|
+
// 行为对齐注: MAGIC_DIR 空时跨端 for 循环探测(.claude/.qoder/.vscode/.add)——
|
|
4
|
+
// 兜底值因端而异(core: .add;claude: .claude),构造参数注入。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
import { detectActiveAdd, jsonGet } from "./common.js"
|
|
9
|
+
import { protocol } from "./rules.js"
|
|
10
|
+
|
|
11
|
+
/** Notification 路由(探测循环 + reviews 提醒);adapter 差异 = fallbackMagicDir 构造参数 + emitReminder 文本 */
|
|
12
|
+
export class NotificationRouter {
|
|
13
|
+
protected readonly projectDir: string
|
|
14
|
+
protected readonly fallbackMagicDir: string
|
|
15
|
+
|
|
16
|
+
constructor(projectDir: string, fallbackMagicDir: string) {
|
|
17
|
+
this.projectDir = projectDir
|
|
18
|
+
this.fallbackMagicDir = fallbackMagicDir
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 提醒输出(core 默认: 含 Plan 前缀——bash 原文逐字;qoder 子类 override 无前缀) */
|
|
22
|
+
protected emitReminder(plan: string, reviewsDir: string): void {
|
|
23
|
+
process.stdout.write(`[ADD Notification] Plan: ${plan} — 请检查 Review 文档: ${reviewsDir}\n`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* magicDir 解析(core 默认: env 优先 → 跨端探测循环 → fallback;bash 原文逐字):
|
|
28
|
+
* 扩展点: qoder 子类 override 为固定值(qoder bash 原文硬编码 ${PROJECT_DIR}/.qoder/reviews,无探测循环)。
|
|
29
|
+
* Task 9.4 中立化: 探测列表从 hook-protocol-rules.toml [protocol.adapter_defaults.probe_magic_dirs] 读取——
|
|
30
|
+
* 原硬编码 [".claude", ".qoder", ".vscode", ".add"] 缺 .trae/.codex(新增端需改代码);声明化后随端演进。
|
|
31
|
+
*/
|
|
32
|
+
protected resolveMagicDir(): string {
|
|
33
|
+
const env = process.env.MAGIC_DIR
|
|
34
|
+
if (env) return env
|
|
35
|
+
const defaults = protocol.adapter_defaults as { probe_magic_dirs?: readonly string[] } | undefined
|
|
36
|
+
const probeList = defaults?.probe_magic_dirs ?? [".claude", ".qoder", ".vscode", ".add"]
|
|
37
|
+
for (const m of probeList) {
|
|
38
|
+
if (existsSync(join(this.projectDir, m))) return m
|
|
39
|
+
}
|
|
40
|
+
return this.fallbackMagicDir
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 主路由:返回 exit code(0) */
|
|
44
|
+
run(input: string): number {
|
|
45
|
+
const ntype = jsonGet(input, "notification_type")
|
|
46
|
+
if (ntype !== "result") return 0
|
|
47
|
+
|
|
48
|
+
const state = detectActiveAdd()
|
|
49
|
+
if (state === null) return 0
|
|
50
|
+
|
|
51
|
+
const plan = state.split("::")[0] ?? ""
|
|
52
|
+
|
|
53
|
+
// 动态探测(扩展点: qoder override 固定值)
|
|
54
|
+
const magicDir = this.resolveMagicDir()
|
|
55
|
+
|
|
56
|
+
const reviewsDir = join(this.projectDir, magicDir, "reviews")
|
|
57
|
+
if (existsSync(reviewsDir) && readdirSync(reviewsDir).some((f) => f.endsWith(".md"))) {
|
|
58
|
+
this.emitReminder(plan, reviewsDir)
|
|
59
|
+
}
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// notify.ts — Hook 拦截事件写入 jsonl(bash 版 notify.sh 的 TS 同语义实现,零外部依赖)
|
|
2
|
+
// 用法: import 后在 exit 2 前调用 writeHookEvent
|
|
3
|
+
//
|
|
4
|
+
// 写入格式(7 字段 jsonl,真源 = hook-event-rules.toml):
|
|
5
|
+
// {"ts":"ISO8601","hook":"...","decision":"deny","cmd":"...","reason":"...","planKeyword":"...","planStatus":"..."}
|
|
6
|
+
//
|
|
7
|
+
// 磁盘管理(真源 = hook-event-rules.toml [event.file]):
|
|
8
|
+
// - 单文件 ≤256KB,超限轮转为 .old(覆盖,不累积多份)
|
|
9
|
+
// - 总量 ≤512KB(当前 + 一个 .old)
|
|
10
|
+
// - MCP Server 宕机不丢事件,重启后从文件恢复消费
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, renameSync, statSync, appendFileSync } from "node:fs"
|
|
13
|
+
import { join } from "node:path"
|
|
14
|
+
import { event, protocol } from "./rules.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 写入 hook 事件(与 bash write_hook_event 逐字对齐)。
|
|
18
|
+
* extra 为可选 JSON 片段(anchor_hit/struct_score/override 等),非空时以逗号拼接。
|
|
19
|
+
* magicDirOverride 为可选参数(Task 8.1 AuditBridge 传入——默认 env.MAGIC_DIR 兜底声明真源,
|
|
20
|
+
* Task 9.4 中立化: 兜底值从 hook-protocol-rules.toml [protocol.adapter_defaults] 读取,不锁死 .qoder)。
|
|
21
|
+
*/
|
|
22
|
+
export function writeHookEvent(
|
|
23
|
+
hook: string,
|
|
24
|
+
decision: string,
|
|
25
|
+
cmd: string,
|
|
26
|
+
reason: string,
|
|
27
|
+
plan = "unknown",
|
|
28
|
+
status = "none",
|
|
29
|
+
extra = "",
|
|
30
|
+
magicDirOverride?: string
|
|
31
|
+
): void {
|
|
32
|
+
const defaults = protocol.adapter_defaults as { magic_dir_fallback?: string } | undefined
|
|
33
|
+
const fallback = defaults?.magic_dir_fallback ?? ".qoder"
|
|
34
|
+
const dir = join(magicDirOverride ?? (process.env.MAGIC_DIR || fallback), "reports")
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(dir, { recursive: true })
|
|
37
|
+
} catch {
|
|
38
|
+
/* mkdir 失败不阻断 */
|
|
39
|
+
}
|
|
40
|
+
const file = join(dir, "hook-events.jsonl")
|
|
41
|
+
|
|
42
|
+
// 超过 rotate_bytes 自动轮转(真源: hook-event-rules.toml [event.file])
|
|
43
|
+
if (existsSync(file)) {
|
|
44
|
+
let sz = 0
|
|
45
|
+
try {
|
|
46
|
+
sz = statSync(file).size
|
|
47
|
+
} catch {
|
|
48
|
+
sz = 0
|
|
49
|
+
}
|
|
50
|
+
if (sz > (event.file.rotate_bytes as number)) {
|
|
51
|
+
try {
|
|
52
|
+
renameSync(file, `${file}.old`)
|
|
53
|
+
} catch {
|
|
54
|
+
/* 轮转失败不阻断 */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 时间格式对齐 bash: date -u +%Y-%m-%dT%H:%M:%SZ
|
|
60
|
+
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z")
|
|
61
|
+
const extraPart = extra ? `,${extra}` : ""
|
|
62
|
+
const line =
|
|
63
|
+
`{"ts":"${ts}","hook":"${hook}","decision":"${decision}","cmd":"${cmd}",` +
|
|
64
|
+
`"reason":"${reason}","planKeyword":"${plan}","planStatus":"${status}"${extraPart}}\n`
|
|
65
|
+
appendFileSync(file, line)
|
|
66
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// permission-gate-router.ts — PermissionRequest 权限请求门禁路由(治理逻辑层,Task 5.1 继承体系)
|
|
2
|
+
// 治理卡位: 权限请求提示(Review 卡位)
|
|
3
|
+
// 设计范式: 模板方法基类——tool_name 解析流程固化,emit() 输出形态/文本 + 缺失回退值由 adapter 子类 override。
|
|
4
|
+
// core 默认: 仅高风险工具(Bash/Write/Edit)stdout 二次确认提示;缺失回退 ""(bash `// empty` 语义)
|
|
5
|
+
// qoder 子类: 全量工具 stderr 日志(qoder 权限弹窗自行处理 Review 卡位);缺失回退 "unknown"(bash `// "unknown"`)
|
|
6
|
+
|
|
7
|
+
import { jsonGet, readHookInput } from "./common.js"
|
|
8
|
+
|
|
9
|
+
/** 字段提取(bash jq `// fallback` 精确语义:字段存在——含空串——取值;缺失才回退) */
|
|
10
|
+
function fieldOr(json: string, field: string, fallback: string): string {
|
|
11
|
+
const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`)
|
|
12
|
+
const m = re.exec(json)
|
|
13
|
+
return m ? m[1] : fallback
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** PermissionRequest 门禁路由(tool_name 解析 → emit 输出) */
|
|
17
|
+
export class PermissionGateRouter {
|
|
18
|
+
/** 主路由:返回 exit code(0) */
|
|
19
|
+
run(input: string): number {
|
|
20
|
+
// 空输入 → 空串(bash jq 无输出);非空 → fieldOr(字段存在取值,缺失回退扩展点值)
|
|
21
|
+
const toolName = input.trim() === "" ? "" : fieldOr(input, "tool_name", this.fallbackToolName())
|
|
22
|
+
this.emit(toolName)
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** tool_name 缺失回退(core: ""——bash `// empty` 语义;qoder 子类 override: "unknown") */
|
|
27
|
+
protected fallbackToolName(): string {
|
|
28
|
+
return ""
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 输出(core 默认: 仅高风险工具 stdout 二次确认提示;qoder 子类 override: 全量 stderr 日志) */
|
|
32
|
+
protected emit(toolName: string): void {
|
|
33
|
+
if (toolName === "Bash" || toolName === "Write" || toolName === "Edit") {
|
|
34
|
+
process.stdout.write(`[ADD PermissionGate] 高风险工具: ${toolName},请确认操作。\n`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 入口便捷函数(core 薄壳用) */
|
|
40
|
+
export function runPermissionGate(): void {
|
|
41
|
+
process.exit(new PermissionGateRouter().run(readHookInput()))
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// post-tool-failure-router.ts — PostToolUseFailure 错误分类路由(治理逻辑层,Task 5.1 继承体系)
|
|
2
|
+
// 设计范式: 模板方法基类——tool_name/error 解析流程固化,emit() 输出 + 缺失回退值由 adapter 子类 override。
|
|
3
|
+
// core 默认: stdout 固定提示(不依赖参数)
|
|
4
|
+
// qoder 子类: stderr 回退建议;缺失回退 "unknown"(bash `// "unknown"` 语义)
|
|
5
|
+
// vscode 子类: 429 限流降级(串行模式建议);error 缺失回退 "未知错误"(bash grep || echo 语义)
|
|
6
|
+
|
|
7
|
+
import { jsonGet, readHookInput } from "./common.js"
|
|
8
|
+
|
|
9
|
+
/** 字段提取(bash jq `// fallback` 精确语义:字段存在——含空串——取值;缺失才回退) */
|
|
10
|
+
function fieldOr(json: string, field: string, fallback: string): string {
|
|
11
|
+
const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`)
|
|
12
|
+
const m = re.exec(json)
|
|
13
|
+
return m ? m[1] : fallback
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** PostToolUseFailure 错误分类路由(tool_name/error 解析 → emit 输出) */
|
|
17
|
+
export class PostToolFailureRouter {
|
|
18
|
+
/** 主路由:返回 exit code(0) */
|
|
19
|
+
run(input: string): number {
|
|
20
|
+
// 空输入语义: 默认 → 空串(bash jq 无输出);vscode 子类 → fallback(bash grep || echo)
|
|
21
|
+
const empty = input.trim() === ""
|
|
22
|
+
const toolName = empty
|
|
23
|
+
? (this.emptyUsesFallback() ? this.fallbackToolName() : "")
|
|
24
|
+
: fieldOr(input, "tool_name", this.fallbackToolName())
|
|
25
|
+
const error = empty
|
|
26
|
+
? (this.emptyUsesFallback() ? this.fallbackError() : "")
|
|
27
|
+
: fieldOr(input, "error", this.fallbackError())
|
|
28
|
+
this.emit(toolName, error)
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 空输入是否走 fallback(core/qoder: false——空串;vscode 子类 override: true——bash grep || echo 语义) */
|
|
33
|
+
protected emptyUsesFallback(): boolean {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** tool_name 缺失回退(core: "";qoder 子类 override: "unknown") */
|
|
38
|
+
protected fallbackToolName(): string {
|
|
39
|
+
return ""
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** error 缺失回退(core: "";qoder 子类 override: "unknown";vscode 子类 override: "未知错误") */
|
|
43
|
+
protected fallbackError(): string {
|
|
44
|
+
return ""
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 输出(core 默认: stdout 固定文本——bash 原文逐字;adapter 子类 override 用 toolName/error) */
|
|
48
|
+
protected emit(_toolName: string, _error: string): void {
|
|
49
|
+
process.stdout.write("[ADD PostToolFailure] 工具调用失败,请检查错误信息并修复。\n")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 入口便捷函数(core 薄壳用) */
|
|
54
|
+
export function runPostToolFailure(): void {
|
|
55
|
+
process.exit(new PostToolFailureRouter().run(readHookInput()))
|
|
56
|
+
}
|