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,212 @@
|
|
|
1
|
+
// lib/post-tool-router.ts — PostToolUse 路由(治理逻辑层,Task 2.1 类化收敛)
|
|
2
|
+
// 治理卡位 #5: 格式化 + ADD文档守卫 + 审计落库 + 结果增强 + 哨兵自动化
|
|
3
|
+
//
|
|
4
|
+
// 设计范式: OOP 路由类(§1 DPS 哨兵自动化 / §2 Edit·Write 守卫 / §3 Bash 增强)。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { join, basename, dirname } from "node:path"
|
|
8
|
+
import { jsonGet } from "./common.js"
|
|
9
|
+
import { AuditBridge } from "./audit-bridge.js"
|
|
10
|
+
|
|
11
|
+
/** 纯函数:从 input 中提取 output/content 拼接文本(对齐 bash grep -o 组合) */
|
|
12
|
+
function extractOutputText(raw: string): string {
|
|
13
|
+
const m1 = raw.match(/"output"\s*:\s*"([^"]*)"/)
|
|
14
|
+
const m2 = raw.match(/"content"\s*:\s*"([^"]*)"/)
|
|
15
|
+
return [m1?.[1] ?? "", m2?.[1] ?? ""].filter(Boolean).join(" ")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* PostToolUse 路由(§1 DPS 自动化 → §2 Edit/Write 守卫 → §3 Bash 增强):
|
|
20
|
+
* 行为对齐注: bash 版文件含两段实现(L1-103 有效 + L104-139 死代码)——仅翻译有效段。
|
|
21
|
+
*/
|
|
22
|
+
export class PostToolRouter {
|
|
23
|
+
private readonly projectDir: string
|
|
24
|
+
private readonly magicDir: string
|
|
25
|
+
/** 输出聚合(qoder feedback JSON 模式收集;core 直出 stderr) */
|
|
26
|
+
protected readonly lines: string[] = []
|
|
27
|
+
/** Task 8.1 审计桥接: 文件写入事件面扩展(jsonl 主路径 → MCP 常驻消费落库) */
|
|
28
|
+
private readonly auditBridge: AuditBridge
|
|
29
|
+
|
|
30
|
+
constructor(projectDir: string, magicDir: string) {
|
|
31
|
+
this.projectDir = projectDir
|
|
32
|
+
this.magicDir = magicDir
|
|
33
|
+
this.auditBridge = new AuditBridge(projectDir, magicDir)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
37
|
+
|
|
38
|
+
/** §1 DPS 自动化跳过(core: false;claude: true——claude 版未实现哨兵自动建) */
|
|
39
|
+
protected shouldSkipDps(): boolean {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** §2a 文档守卫文本(core: 章节完整) */
|
|
44
|
+
protected docGuardText(filePath: string): string {
|
|
45
|
+
return `[ADD PostToolUse] ADD 文档已写入: ${filePath}。请确保章节完整、双向链接齐全。\n`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** §2 后置段(core: plan_track 自动触发 + devlog 提醒 + schema regen) */
|
|
49
|
+
protected postDocSections(filePath: string): void {
|
|
50
|
+
// §2b: plan_track 自动触发 — specs/ 或 add-route 写入后自动落库
|
|
51
|
+
if (/\/(specs|plans)\/.*add-route/.test(filePath)) {
|
|
52
|
+
let planName = basename(filePath).replace(/-add-route.*/, "").replace(/-plan-v\d*$/, "")
|
|
53
|
+
// 从路径提取完整 planName(含 -plan-vN 后缀)
|
|
54
|
+
if (filePath.includes("add-route")) {
|
|
55
|
+
// add-route 写入:从文件名反推 planName
|
|
56
|
+
const planDir = dirname(filePath)
|
|
57
|
+
if (existsSync(planDir)) {
|
|
58
|
+
try {
|
|
59
|
+
const planFile = readdirSync(planDir).find((f) => f.includes("-plan-v") && f.endsWith(".md"))
|
|
60
|
+
if (planFile) planName = planFile.replace(/\.md$/, "")
|
|
61
|
+
} catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (planName !== "") {
|
|
67
|
+
this.emitLine(`[ADD PostToolUse] 📊 自动同步 PlanRecord: plan_track({ planName: "${planName}" }) — 请执行\n`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// §2c: devlog 自动提醒 — add-route Step 8 全 [x] 时提醒
|
|
72
|
+
if (/add-route.*\.md$/.test(filePath)) {
|
|
73
|
+
if (existsSync(filePath)) {
|
|
74
|
+
const content = readFileSync(filePath, "utf-8")
|
|
75
|
+
const step8 = content.match(/## Step 8[\s\S]*?(?=^## |\Z)/m)?.[0] ?? ""
|
|
76
|
+
const unchecked = (step8.match(/^\- \[ \]/gm) || []).length
|
|
77
|
+
const checked = (step8.match(/^\- \[x\]/gm) || []).length
|
|
78
|
+
if (unchecked === 0 && checked > 0) {
|
|
79
|
+
this.emitLine("[ADD PostToolUse] ⚠️ Step 8 全部收敛完成!请写 devlog日志(走mcp) → 更新 handoff\n")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// §2d: schema.json 自动 regen — 模板 .md 修改后更新对应 schema
|
|
85
|
+
if (/templates\/.*\.md$/.test(filePath)) {
|
|
86
|
+
const schemaFile = filePath.replace(/\.md$/, ".schema.json")
|
|
87
|
+
if (existsSync(schemaFile)) {
|
|
88
|
+
const headings = (readFileSync(filePath, "utf-8").match(/^## (.+)$/gm) ?? []).slice(0, 20)
|
|
89
|
+
if (headings.length > 0) {
|
|
90
|
+
this.emitLine(`[ADD PostToolUse] 🔄 模板已修改,请检查 ${schemaFile} 是否需更新(§ sections)\n`)
|
|
91
|
+
this.emitLine(`[ADD PostToolUse] 模板标题: ${headings.join(" ")}\n`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 输出通道(core: stderr 直出;qoder: 收集后 feedback JSON flush) */
|
|
98
|
+
protected collectJson(): boolean {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 输出一行(core: stderr 直出;qoder: 收集聚合) */
|
|
103
|
+
protected emitLine(text: string): void {
|
|
104
|
+
if (this.collectJson()) {
|
|
105
|
+
this.lines.push(text.endsWith("\n") ? text.trimEnd() : text)
|
|
106
|
+
} else {
|
|
107
|
+
process.stderr.write(text)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 刷新收集的输出(qoder: hookSpecificOutput.feedback JSON——Qoder 文档实证 PostToolUse 专属字段) */
|
|
112
|
+
protected flushLines(): void {
|
|
113
|
+
if (this.collectJson() && this.lines.length > 0) {
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", feedback: this.lines.join("\n") } }) + "\n"
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 主路由:返回 exit code(0) */
|
|
121
|
+
run(input: string, toolName: string): number {
|
|
122
|
+
// ═══════════════ §1: HITL DPS 自动化 — check_dps ≥ 80 → 自动建哨兵 ═══════════════
|
|
123
|
+
if (!this.shouldSkipDps() && input.includes('"check_dps"')) {
|
|
124
|
+
const toolOutput = extractOutputText(input)
|
|
125
|
+
const dpsScore = toolOutput.match(/DPS\s*=\s*(\d+)/)?.[1] ?? ""
|
|
126
|
+
const planKeyword = input.match(/"planKeyword"\s*:\s*"([^"]+)"/)?.[1] ?? ""
|
|
127
|
+
|
|
128
|
+
if (dpsScore !== "" && planKeyword !== "") {
|
|
129
|
+
const score = Number(dpsScore)
|
|
130
|
+
if (!Number.isNaN(score) && score >= 80) {
|
|
131
|
+
const sentinel = join(this.projectDir, this.magicDir, "hitl", `.tongyi-${planKeyword}`)
|
|
132
|
+
if (!existsSync(sentinel)) {
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(sentinel, "")
|
|
135
|
+
this.emitLine(`[ADD PostToolUse] ✅ DPS=${dpsScore} ≥80, HITL 自动通过 → ${sentinel}\n`)
|
|
136
|
+
} catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
this.emitLine(`[ADD PostToolUse] DPS=${dpsScore} ≥80, 已自动建哨兵 ${sentinel}\n`)
|
|
140
|
+
} else {
|
|
141
|
+
this.emitLine(`[ADD PostToolUse] DPS=${dpsScore} ≥80, 哨兵已存在 → ${sentinel}\n`)
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
this.emitLine(`[ADD PostToolUse] ⚠️ DPS=${dpsScore} <80, 需 Review 后手动建哨兵 .tongyi-${planKeyword}\n`)
|
|
145
|
+
this.emitLine("[ADD PostToolUse] 修复文档后重新 check_dps,通过即自动放行。\n")
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ═══════════════ §2: 文件写工具 matcher(Edit/Write 等: 格式化 + 文档守卫 + 自动 plan_track)═══
|
|
151
|
+
if (this.isFileTool(toolName)) {
|
|
152
|
+
const filePath = this.extractFilePath(input)
|
|
153
|
+
if (filePath === "") return 0
|
|
154
|
+
|
|
155
|
+
// §2a: ADD 文档结构守卫
|
|
156
|
+
if (/\.(qoder|claude|add)\/(plans|specs|reviews)\//.test(filePath)) {
|
|
157
|
+
this.emitLine(this.docGuardText(filePath))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// §2b/§2c/§2d 后置段(扩展点)
|
|
161
|
+
this.postDocSections(filePath)
|
|
162
|
+
|
|
163
|
+
// §2e: 审计提醒(扩展点: codex 简化文本)
|
|
164
|
+
this.emitLine(this.emitAuditReminder(filePath))
|
|
165
|
+
|
|
166
|
+
// §2f: 审计桥接(Task 8.1,ADD-7 自动化)——文件写入事件 → jsonl → MCP 常驻消费落库
|
|
167
|
+
this.emitWriteEvent(filePath)
|
|
168
|
+
} else if (toolName === "Bash") {
|
|
169
|
+
// ═══════════════ §3: Bash matcher: 结果增强(扩展点: codex 文本)═══════════════
|
|
170
|
+
this.emitLine(this.emitBashDone())
|
|
171
|
+
} else {
|
|
172
|
+
// 附加 matcher(扩展点: codex apply_patch——解析 patch 路径逐文件 reportFile)
|
|
173
|
+
this.applyPatchMatcher(input)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.flushLines()
|
|
177
|
+
return 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────── 扩展点(续)───────────────────────────
|
|
181
|
+
|
|
182
|
+
/** 文件写工具判定(core: Edit/Write;codex 子类: +SearchReplace + apply_patch 走附加 matcher) */
|
|
183
|
+
protected isFileTool(toolName: string): boolean {
|
|
184
|
+
return toolName === "Edit" || toolName === "Write"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** filePath 提取(core: file_path 字段;codex 子类: file_path ?? path) */
|
|
188
|
+
protected extractFilePath(input: string): string {
|
|
189
|
+
return jsonGet(input, "file_path")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** §2e 审计提醒(core: 含 ADD-7;codex 子类: 简化文本) */
|
|
193
|
+
protected emitAuditReminder(filePath: string): string {
|
|
194
|
+
return `[ADD PostToolUse] 文件已写入: ${filePath}。请执行 record_dev_operation 落库审计(ADD-7)。\n`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** §2f 审计桥接触发(Task 8.1,扩展点: 默认 AuditBridge.emit;
|
|
198
|
+
* 如有端需关闭/改造事件面在此 override——本轮 5 端全接入,无关闭端) */
|
|
199
|
+
protected emitWriteEvent(filePath: string): void {
|
|
200
|
+
this.auditBridge.emit(filePath)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** §3 Bash 增强(core: lint/tsc;codex 子类: lint/typecheck/test) */
|
|
204
|
+
protected emitBashDone(): string {
|
|
205
|
+
return "[ADD PostToolUse] 命令执行完成。如有 lint/tsc 错误请修复。\n"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 附加 matcher(core: 无;codex 子类: apply_patch 解析 `*** Add|Update|Delete File:` 逐文件 report) */
|
|
209
|
+
protected applyPatchMatcher(_input: string): void {
|
|
210
|
+
// core 协议: 无 apply_patch 工具
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// pre-compact-guard.ts — 上下文压缩前 ADD 状态保存(治理逻辑层,Task 3.1 继承体系)
|
|
2
|
+
// 治理卡位 #9: ADD状态保存 + 恢复清单导出 + tpl标记清理
|
|
3
|
+
// adapter 差异仅 PROJECT_DIR 注入(依赖注入,无 override 需求)。
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, unlinkSync } from "node:fs"
|
|
6
|
+
import { detectActiveAdd, projectHash } from "./common.js"
|
|
7
|
+
|
|
8
|
+
/** PreCompact 守卫(① 状态保存到恢复标记 → ② tpl 标记清理) */
|
|
9
|
+
export class PreCompactGuard {
|
|
10
|
+
/** 主入口:返回 exit code(0) */
|
|
11
|
+
run(): number {
|
|
12
|
+
const hash = projectHash()
|
|
13
|
+
|
|
14
|
+
// ── ① 获取 ADD 状态并保存到标记文件 ──
|
|
15
|
+
const state = detectActiveAdd()
|
|
16
|
+
if (state !== null) {
|
|
17
|
+
const [plan, step, rounds, handoff, addRoute] = state.split("::")
|
|
18
|
+
|
|
19
|
+
// 写入恢复标记文件(SessionStart 恢复时读取)
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(
|
|
22
|
+
`/tmp/add_recovery_${hash}`,
|
|
23
|
+
`plan=${plan}\nstep=${step}\nrounds=${rounds}\nhandoff=${handoff}\nadd_route=${addRoute}\n`
|
|
24
|
+
)
|
|
25
|
+
} catch {
|
|
26
|
+
/* ignore */
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.stderr.write(`[ADD PreCompact] ADD 状态已保存: Plan=${plan}, Step=${step}\n`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── ② 清理 tpl-injected 标记(compact 后上下文丢失,下次需重注) ──
|
|
33
|
+
try {
|
|
34
|
+
unlinkSync(`/tmp/add_tpl_${hash}`)
|
|
35
|
+
} catch {
|
|
36
|
+
/* ignore(对齐 rm -f || true) */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// pre-tool-guard.ts — PreToolUse 守卫基类(治理逻辑层,Task 3.1 继承体系)
|
|
2
|
+
// 治理卡位 #4: 危险命令拦截 / 模板路径兜底 / 写入前置守卫 / 敏感文件保护
|
|
3
|
+
// 规则真源: hook-guard-rules.toml([guard.detectors] 基线链 / [guard.adapter_detectors] 独立链 /
|
|
4
|
+
// [guard.sensitive_files] / [guard.thresholds] / [guard.template_hints] / [guard.hitl_exemptions])
|
|
5
|
+
//
|
|
6
|
+
// 设计范式: 模板方法基类——治理流程(链加载 → 判定 → §B/§C 分流)在基类固化,
|
|
7
|
+
// 协议差异(输出形态 / exit 语义 / 阻断日志 / 附加 matcher)由 adapter 子类
|
|
8
|
+
// override protected 扩展点。adapter 入口 = 薄壳(环境注入)+ 子类(协议差异),治理流程 0 复制。
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, appendFileSync, statSync } from "node:fs"
|
|
11
|
+
import { join, basename } from "node:path"
|
|
12
|
+
import { detectActiveAdd, jsonGet, localIsoSeconds, markDevAction } from "./common.js"
|
|
13
|
+
import { writeHookEvent } from "./notify.js"
|
|
14
|
+
import { guard } from "./rules.js"
|
|
15
|
+
|
|
16
|
+
/** 检测器条目(rules.guard.detectors / adapter_detectors 结构) */
|
|
17
|
+
interface Detector {
|
|
18
|
+
id: string
|
|
19
|
+
regex: string
|
|
20
|
+
flags?: string
|
|
21
|
+
reason: string
|
|
22
|
+
stderr: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 模板提示条目(rules.guard.template_hints 结构) */
|
|
26
|
+
interface TemplateHint {
|
|
27
|
+
pattern: string
|
|
28
|
+
message: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 拦截原因与输出文本 */
|
|
32
|
+
interface BlockResult {
|
|
33
|
+
reason: string
|
|
34
|
+
stderr: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 纯函数:构造 hookSpecificOutput JSON(对齐 bash echo 格式) */
|
|
38
|
+
function askJson(reason: string): string {
|
|
39
|
+
return `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"${reason}"}}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function allowJson(reason: string): string {
|
|
43
|
+
return `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"${reason}"}}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function denyJson(reason: string): string {
|
|
47
|
+
return `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"${reason}"}}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PreToolUse 守卫基类(模板方法):
|
|
52
|
+
* 流程固化(不可 override): runSectionA / runSectionB / runSectionC
|
|
53
|
+
* 扩展点(protected,adapter 子类 override 协议差异):
|
|
54
|
+
* - detectorChain(): 检测链加载(core 基线 / adapter 独立链,按 adapterName)
|
|
55
|
+
* - onBlock(): §A 阻断输出(core: stderr+askJson+logBlock+2;claude: 纯 stderr+2)
|
|
56
|
+
* - onNoPlanAllow(): §B 无 Plan 放行(core: allowJson+0;claude: allowJson+2)
|
|
57
|
+
* - onSensitiveDeny(): 敏感文件阻断(core: stderr+denyJson+2;claude: 纯 stderr+2)
|
|
58
|
+
* - onOtherTool(): 附加 matcher(core: 无;claude: ③ Read 模板提示)
|
|
59
|
+
*/
|
|
60
|
+
export class PreToolUseGuard {
|
|
61
|
+
protected readonly projectDir: string
|
|
62
|
+
protected readonly magicDir: string
|
|
63
|
+
protected readonly planKeyword: string
|
|
64
|
+
protected readonly planStatus: string
|
|
65
|
+
protected readonly adapterName: string
|
|
66
|
+
|
|
67
|
+
constructor(projectDir: string, magicDir: string, adapterName: string = "core") {
|
|
68
|
+
this.projectDir = projectDir
|
|
69
|
+
this.magicDir = magicDir
|
|
70
|
+
this.adapterName = adapterName
|
|
71
|
+
const active = detectActiveAdd()
|
|
72
|
+
if (active !== null) {
|
|
73
|
+
this.planKeyword = active.split("::")[0] ?? ""
|
|
74
|
+
this.planStatus = "active"
|
|
75
|
+
} else {
|
|
76
|
+
this.planKeyword = "no-active-plan"
|
|
77
|
+
this.planStatus = "none"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
82
|
+
|
|
83
|
+
/** 检测链加载(真源: [guard.detectors] 基线 / [guard.adapter_detectors] 独立链) */
|
|
84
|
+
protected detectorChain(): Detector[] {
|
|
85
|
+
if (this.adapterName === "core") {
|
|
86
|
+
return (guard.detectors as unknown as Detector[]) ?? []
|
|
87
|
+
}
|
|
88
|
+
const all = (guard.adapter_detectors as unknown as Array<Detector & { adapter: string }>) ?? []
|
|
89
|
+
return all.filter((d) => d.adapter === this.adapterName)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** §A 阻断输出(core 协议: stderr + askJson + 阻断日志 + 事件 + 2) */
|
|
93
|
+
protected onBlock(blocked: BlockResult, command: string): number {
|
|
94
|
+
process.stderr.write(blocked.stderr)
|
|
95
|
+
process.stdout.write(askJson(blocked.reason) + "\n")
|
|
96
|
+
this.logBlock("检测器链", command)
|
|
97
|
+
writeHookEvent("pre-tool-use", "deny", command, blocked.reason, this.planKeyword, this.planStatus)
|
|
98
|
+
return 2
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** §A 放行后处理(core: 无操作;claude: bash 原文 mark_dev_action) */
|
|
102
|
+
protected onSectionAPass(_command: string): void {
|
|
103
|
+
// core 协议: 构建工具/版本控制/只读操作放行,无标记
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** §B 无 Plan 放行(core 协议: 提示 + allowJson + 事件 + 0) */
|
|
107
|
+
protected onNoPlanAllow(toolName: string, filePath: string): number {
|
|
108
|
+
process.stderr.write("[ADD 提示] 正在写入 Plan/Spec/Review 文档但无活跃 ADD Plan——首次创建场景放行,建议先执行 add-paradigm 生成 Plan+add-route\n")
|
|
109
|
+
process.stdout.write(allowJson("无活跃 ADD Plan 但为 Plan/Spec/Review 写入(首次创建场景),提示而非拦截") + "\n")
|
|
110
|
+
writeHookEvent("pre-tool-use", "info", `${toolName} ${filePath}`, "无活跃 ADD Plan 下写入 Plan/Spec/Review(首次创建放行)", this.planKeyword, this.planStatus)
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** §B 敏感文件阻断(core 协议: stderr + denyJson + 2) */
|
|
115
|
+
protected onSensitiveDeny(filePath: string): number {
|
|
116
|
+
const sensReason = (guard.sensitive_files as { regex: string; deny_reason: string }).deny_reason
|
|
117
|
+
process.stderr.write(`⛔ 敏感文件受保护,禁止写入: ${filePath}\n`)
|
|
118
|
+
process.stdout.write(denyJson(sensReason) + "\n")
|
|
119
|
+
return 2
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 大文件适配提示文本(core: payload 限制;qoder: Qoder 40500 错误码) */
|
|
123
|
+
protected largeFileText(fsize: number): string {
|
|
124
|
+
return `⚠️ [ADD PreToolUse] 文件已有 ${fsize} 字节,Write 全量覆盖可能触发工具 payload 限制。建议用 SearchReplace 分块追加。\n`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** HITL 未 tongyi 输出(core 协议: stderr 3 行 + JSON deny + 事件 + exit 0;qoder: exit 2 + event ask) */
|
|
128
|
+
protected onHitlDeny(toolName: string, filePath: string, tongyiMarker: string): number {
|
|
129
|
+
process.stderr.write(`⛔ [ADD PreToolUse §C] HITL 未 tongyi: ${filePath}\n`)
|
|
130
|
+
process.stderr.write(` 原因: 哨兵文件 ${tongyiMarker} 不存在\n`)
|
|
131
|
+
process.stderr.write(' 操作: 请先调用 create_hitl 创建审批,再 update_hitl({ status: "TONGYI" })\n')
|
|
132
|
+
const reason = `HITL 未 tongyi: 哨兵 ${tongyiMarker} 不存在。请先 create_hitl → 人工 tongyi → update_hitl 后再写入`
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
`{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"${reason}","additionalContext":"${reason}"}}\n`
|
|
135
|
+
)
|
|
136
|
+
writeHookEvent("pre-tool-use", "deny", `${toolName} ${filePath}`, `HITL 未 tongyi: ${tongyiMarker}`, this.planKeyword, this.planStatus)
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 附加 matcher(core: 无;claude: ③ Read 模板提示) */
|
|
141
|
+
protected onOtherTool(_input: string, _toolName: string): number {
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─────────────────────────── 流程固化 ───────────────────────────
|
|
146
|
+
|
|
147
|
+
/** 阻断日志(对齐 bash _log_block:追加到 debug-dump/stdin.log) */
|
|
148
|
+
protected logBlock(rule: string, cmd: string): void {
|
|
149
|
+
try {
|
|
150
|
+
const dir = join(this.projectDir, this.magicDir, "debug-dump")
|
|
151
|
+
mkdirSync(dir, { recursive: true })
|
|
152
|
+
appendFileSync(
|
|
153
|
+
join(dir, "stdin.log"),
|
|
154
|
+
`=== ${localIsoSeconds()} [BLOCKED by §A: ${rule}] ===\ncommand: ${cmd.slice(0, 300)}\n=== DONE ===\n`
|
|
155
|
+
)
|
|
156
|
+
} catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** §A 检测器链判定:返回阻断结果,放行返回 null */
|
|
162
|
+
private checkBashCommand(command: string): BlockResult | null {
|
|
163
|
+
for (const d of this.detectorChain()) {
|
|
164
|
+
if (new RegExp(d.regex, d.flags ?? "").test(command)) {
|
|
165
|
+
return { reason: d.reason, stderr: d.stderr }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** §A 入口:Bash 工具写入保护(模板方法) */
|
|
172
|
+
runSectionA(input: string): number {
|
|
173
|
+
const command = jsonGet(input, "command")
|
|
174
|
+
if (command === "") return 0 // 无命令字段 → 走 §B
|
|
175
|
+
|
|
176
|
+
const blocked = this.checkBashCommand(command)
|
|
177
|
+
if (blocked) {
|
|
178
|
+
return this.onBlock(blocked, command)
|
|
179
|
+
}
|
|
180
|
+
// 放行: 构建工具/版本控制/只读操作
|
|
181
|
+
this.onSectionAPass(command)
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** §B 入口:Write/Edit 文件写入前置守卫(模板方法) */
|
|
186
|
+
runSectionB(input: string, toolName: string): number {
|
|
187
|
+
if (toolName !== "Write" && toolName !== "Edit" && toolName !== "SearchReplace") return 0
|
|
188
|
+
|
|
189
|
+
const filePath = jsonGet(input, "file_path")
|
|
190
|
+
if (filePath === "") return 0
|
|
191
|
+
|
|
192
|
+
// §B: 写入守卫按当前 magicDir 限域(无 Plan 分支: core return / codex 提示继续)
|
|
193
|
+
if (new RegExp(`${this.magicDir}/(plans|specs|reviews)/`).test(filePath)) {
|
|
194
|
+
const state = detectActiveAdd()
|
|
195
|
+
if (state === null) {
|
|
196
|
+
if (this.noPlanHint()) {
|
|
197
|
+
process.stderr.write(`[ADD PreToolUse] 正在修改 ADD 文档但未检测到活跃 Plan: ${filePath}\n`)
|
|
198
|
+
} else {
|
|
199
|
+
return this.onNoPlanAllow(toolName, filePath)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 敏感文件保护 + HITL + 模板提示(guard 核心——codex apply_patch 复用)
|
|
205
|
+
const guardCode = this.guardFilePath(filePath, toolName)
|
|
206
|
+
if (guardCode !== 0) return guardCode
|
|
207
|
+
|
|
208
|
+
markDevAction()
|
|
209
|
+
return 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* guard 核心(敏感文件 → HITL → 模板提示;codex apply_patch 路径逐文件复用):
|
|
214
|
+
* 敏感 → onSensitiveDeny;HITL → onHitlDeny;模板提示/大文件 → stderr
|
|
215
|
+
* 返回阻断码(非 0 = 阻断,调用方必须透传 exit)——
|
|
216
|
+
* 2026-08-14 Task 9.4 修复: 原返回 void 丢弃 onSensitiveDeny/onHitlDeny 的 exit 2(敏感文件拦截形同虚设)
|
|
217
|
+
*/
|
|
218
|
+
protected guardFilePath(filePath: string, toolName: string): number {
|
|
219
|
+
// 敏感文件保护(扩展点: codex 自定义锚定正则)
|
|
220
|
+
const sensRegex = this.sensitiveFileRegex()
|
|
221
|
+
if (new RegExp(sensRegex).test(filePath)) {
|
|
222
|
+
return this.onSensitiveDeny(filePath)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// §C: 模板类型前置注入(真源: [guard.template_hints])
|
|
226
|
+
if (new RegExp(`${this.magicDir}/(plans)/`).test(filePath)) {
|
|
227
|
+
const fname = basename(filePath)
|
|
228
|
+
for (const hint of (guard.template_hints as unknown as TemplateHint[]) ?? []) {
|
|
229
|
+
if (new RegExp(hint.pattern).test(fname)) {
|
|
230
|
+
process.stderr.write(hint.message + "\n")
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Write 大文件适配(真源: [guard.thresholds.large_file_bytes])
|
|
234
|
+
if (toolName === "Write" && existsSync(filePath)) {
|
|
235
|
+
const fsize = statSync(filePath).size
|
|
236
|
+
const limit = (guard.thresholds as { large_file_bytes: number }).large_file_bytes
|
|
237
|
+
if (fsize > limit) {
|
|
238
|
+
process.stderr.write(this.largeFileText(fsize))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// §C: HITL tongyi 检查(扩展点: codex 双哨兵 + reviews 豁免差异)
|
|
244
|
+
let doHitl = false
|
|
245
|
+
if (new RegExp(`${this.magicDir}/(plans)/`).test(filePath)) {
|
|
246
|
+
doHitl = !filePath.includes("-handoff")
|
|
247
|
+
} else if (new RegExp(`${this.magicDir}/(reviews)/`).test(filePath)) {
|
|
248
|
+
doHitl = !this.hitlExemptReviews().test(filePath)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (doHitl) {
|
|
252
|
+
// 限域: 仅当前 magicDir 下的 plans/reviews 触发 HITL 门禁
|
|
253
|
+
if (!new RegExp(`${this.magicDir}/(plans|reviews)/`).test(filePath)) {
|
|
254
|
+
doHitl = false
|
|
255
|
+
} else {
|
|
256
|
+
const relative = filePath.replace(/.*\/\.(qoder|claude|add|vscode|trae|codex)\/(plans|reviews)\//, "")
|
|
257
|
+
const planName = basename(relative, ".md")
|
|
258
|
+
.replace(/\.hitl$/, "")
|
|
259
|
+
.replace(/-plan-v\d*$/, "")
|
|
260
|
+
.replace(/-add-route-v\d*$/, "")
|
|
261
|
+
.replace(/-review-v\d*$/, "")
|
|
262
|
+
.replace(/-review-implementation$/, "")
|
|
263
|
+
.replace(/-review-runtime$/, "")
|
|
264
|
+
if (planName !== "") {
|
|
265
|
+
const markers = this.hitlMarkers(planName)
|
|
266
|
+
if (!markers.some((m) => existsSync(join(this.projectDir, this.magicDir, "hitl", m)))) {
|
|
267
|
+
const tongyiMarker = markers[0]
|
|
268
|
+
return this.onHitlDeny(toolName, filePath, tongyiMarker)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─────────────────────────── 扩展点(续)───────────────────────────
|
|
277
|
+
|
|
278
|
+
/** 敏感文件正则(core: TOML 真源;codex 子类: 锚定版 `(^|\/)(...)$|credentials|secrets`) */
|
|
279
|
+
protected sensitiveFileRegex(): string {
|
|
280
|
+
return (guard.sensitive_files as { regex: string; deny_reason: string }).regex
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Reviews HITL 豁免(仅 -runtime;review-implementation-* 也走 HITL——2026-08-14 Task 9.4.4③ 上提,回流: I2) */
|
|
284
|
+
protected hitlExemptReviews(): RegExp {
|
|
285
|
+
return /-runtime/
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** HITL 哨兵([full, base] 双哨兵——2026-08-14 Task 9.4.4② 上提,回流: I2;
|
|
289
|
+
* 与 MCP update_hitl 双命名哨兵(原始 planName + 剥后缀推导名)对齐) */
|
|
290
|
+
protected hitlMarkers(planName: string): string[] {
|
|
291
|
+
const base = planName
|
|
292
|
+
.replace(/-plan-v\d*$/, "")
|
|
293
|
+
.replace(/-add-route-v\d*$/, "")
|
|
294
|
+
.replace(/-review-v\d*$/, "")
|
|
295
|
+
.replace(/-review-implementation$/, "")
|
|
296
|
+
.replace(/-review-runtime$/, "")
|
|
297
|
+
return [`.tongyi-${planName}`, `.tongyi-${base}`]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 无 Plan 分支语义(core: onNoPlanAllow return;codex 子类: 仅 stderr 提示后继续) */
|
|
301
|
+
protected noPlanHint(): boolean {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** §C 入口:其他工具 matcher(模板方法,默认委托扩展点) */
|
|
306
|
+
runSectionC(input: string, toolName: string): number {
|
|
307
|
+
return this.onOtherTool(input, toolName)
|
|
308
|
+
}
|
|
309
|
+
}
|