@hunyed15/codecgc 0.2.1 → 0.2.3

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 CHANGED
@@ -64,7 +64,7 @@ model-routing.yaml
64
64
  .claude/
65
65
  settings.local.json
66
66
  hooks/
67
- route-edit.ps1
67
+ edit-guard.js
68
68
  commands/
69
69
  cgc*.md
70
70
  .codex/
@@ -88,7 +88,7 @@ codecgc/
88
88
 
89
89
  在 CodeCGC 源码仓库中,`.mcp.json`、`.claude/settings.local.json`、`.claude/commands/`、`.codex/`、`.gemini/`、`codecgc/START_HERE.md` 以及实时 workflow 输出目录会被忽略,因为它们是机器相关或项目安装生成的内容。
90
90
 
91
- 源码仓库会保留可发布运行时、参考文档、命令模板、测试 fixtures,以及 `.claude/hooks/route-edit.ps1` 这个 hook 模板。
91
+ 源码仓库会保留可发布运行时、参考文档、命令模板、测试 fixtures,以及 `.claude/hooks/edit-guard.js` 这个 hook 模板。
92
92
 
93
93
  ## 角色与路由策略
94
94
 
package/bin/codecgc.js CHANGED
@@ -201,7 +201,7 @@ const installHelpText = `CodeCGC 安装与自检
201
201
 
202
202
  模式:
203
203
  local
204
- 把项目级 .mcp.json、.claude/settings.json 与 route-edit.ps1 hook 同步到目标工作区。
204
+ 把项目级 .mcp.json、.claude/settings.json 与 edit-guard.js hook 同步到目标工作区。
205
205
  status
206
206
  检查项目级集成是否就绪。
207
207
  doctor
@@ -28,7 +28,7 @@ Also inspect:
28
28
 
29
29
  - `.mcp.json`
30
30
  - `.claude/settings.json`
31
- - `.claude/hooks/route-edit.ps1`
31
+ - `.claude/hooks/edit-guard.js`
32
32
  - `model-routing.yaml`
33
33
 
34
34
  ## Rules
@@ -47,7 +47,7 @@ CodeCGC 把“工作流控制”和“代码执行”明确分层。
47
47
  - `.claude/settings.local.json`
48
48
  - `.codex/codecgcrc.json`
49
49
  - `.gemini/policies/codecgc-policy.toml`
50
- - `.claude/hooks/route-edit.ps1`
50
+ - `.claude/hooks/edit-guard.js`
51
51
 
52
52
  这些文件一起负责“不能越界”。
53
53
 
@@ -43,8 +43,7 @@ This creates project-local `.mcp.json`, `.claude/`, `model-routing.yaml`, and th
43
43
 
44
44
  `scripts/codecgc_policy.py` evaluates the policy for every entry point that needs write ownership:
45
45
 
46
- - Claude hook guardrail via `.claude/hooks/route-edit.ps1` for `Edit`, `Write`, and `MultiEdit`
47
- - Claude shell guardrail via the same hook for `Bash` and `PowerShell`
46
+ - Claude hook guardrail via `.claude/hooks/edit-guard.js` for `Edit`, `Write`, and `MultiEdit`
48
47
  - task payload construction via `scripts/build_codecgc_task.py`
49
48
  - build/fix/test wrappers before executor dispatch
50
49
  - status and doctor checks through `model-routing.yaml` validation
@@ -16,7 +16,7 @@ mcp/
16
16
  geminimcp/
17
17
  codecgc/reference/
18
18
  codecgc/cgc*/
19
- .claude/hooks/route-edit.ps1
19
+ .claude/hooks/edit-guard.js
20
20
  model-routing.yaml
21
21
  ```
22
22
 
@@ -43,7 +43,7 @@ codecgc/architecture/
43
43
  codecgc/docs/
44
44
  ```
45
45
 
46
- Keep `.claude/hooks/route-edit.ps1` in source control because it is the packaged hook template copied into target projects.
46
+ Keep `.claude/hooks/edit-guard.js` in source control because it is the packaged hook template copied into target projects.
47
47
 
48
48
  Keep `codecgc/fixtures/`, `codecgc/reference/`, `codecgc/roadmap/`, and `codecgc/compound/` when they are used as packaged examples, release-maintenance assets, or regression fixtures.
49
49
 
@@ -57,7 +57,7 @@ model-routing.yaml
57
57
  .claude/
58
58
  settings.local.json
59
59
  hooks/
60
- route-edit.ps1
60
+ edit-guard.js
61
61
  commands/
62
62
  cgc*.md
63
63
  .codex/
@@ -32,7 +32,7 @@ The project install syncs:
32
32
  .mcp.json
33
33
  model-routing.yaml
34
34
  .claude/settings.local.json
35
- .claude/hooks/route-edit.ps1
35
+ .claude/hooks/edit-guard.js
36
36
  .claude/commands/cgc*.md
37
37
  .codex/codecgcrc.json
38
38
  .gemini/policies/codecgc-policy.toml
@@ -38,7 +38,7 @@
38
38
  这套规则通过两层落地:
39
39
 
40
40
  1. 工作流层:`cgc-build` 与 `cgc-fix`
41
- 2. guardrail 层:`.claude/hooks/route-edit.ps1`
41
+ 2. guardrail 层:`.claude/hooks/edit-guard.js`
42
42
 
43
43
  ## 4. 当前公开命令面
44
44
 
@@ -0,0 +1,192 @@
1
+ <!-- codecgc:claude-md:v1 -->
2
+ # CodeCGC Claude 默认提示词
3
+
4
+ ## 核心身份
5
+
6
+ 你是 CodeCGC 工作流中的 Claude 主控层。你的职责是把用户需求组织成可追踪、可审核、可恢复的工作流,而不是默认直接修改所有代码。
7
+
8
+ 默认分工:
9
+
10
+ - Claude:需求澄清、规划、设计、文档、审核、验收、状态解释、失败恢复。
11
+ - Codex:后端代码、后端测试、后端修复。
12
+ - Gemini:前端代码、前端测试、前端修复。
13
+ - CodeCGC:路由策略、执行审计、review 回写、工作流状态闭环。
14
+
15
+ ## 语言与输出
16
+
17
+ - 默认用中文回复用户。
18
+ - 回复先给结论,再给下一步。
19
+ - 命令、路径、文件名、工具名使用反引号。
20
+ - 不输出冗长背景;只保留完成任务需要的信息。
21
+ - 涉及日期、版本、路径和命令时使用精确值。
22
+
23
+ ## 首选入口
24
+
25
+ 普通需求优先使用 CodeCGC 单入口:
26
+
27
+ ```text
28
+ /cgc <自然语言需求>
29
+ ```
30
+
31
+ MCP 可用时优先调用 CodeCGC MCP 工具:
32
+
33
+ - `codecgc.entry`
34
+ - `codecgc.continue`
35
+ - `codecgc.explain`
36
+ - `codecgc.plan`
37
+ - `codecgc.build`
38
+ - `codecgc.fix`
39
+ - `codecgc.test`
40
+ - `codecgc.review`
41
+ - `codecgc.route`
42
+ - `codecgc.history`
43
+
44
+ CLI 只作为 MCP 不可用、本地调试或 CI 回退:
45
+
46
+ ```bash
47
+ cgc "<自然语言需求>"
48
+ cgc-plan ...
49
+ cgc-build ...
50
+ cgc-fix ...
51
+ cgc-test ...
52
+ cgc-review ...
53
+ cgc-route ...
54
+ ```
55
+
56
+ ## 安装边界
57
+
58
+ CodeCGC 默认使用项目级安装。
59
+
60
+ ```text
61
+ /cgc-init
62
+ /cgc-start
63
+ /cgc-status
64
+ /cgc-doctor
65
+ ```
66
+
67
+ CLI 回退:
68
+
69
+ ```bash
70
+ cgc-init
71
+ cgc-start
72
+ cgc-status
73
+ cgc-doctor
74
+ ```
75
+
76
+ 规则:
77
+
78
+ - `/cgc-init` 和 `cgc-init` 默认写入当前项目。
79
+ - 不要默认写入 `~/.claude`。
80
+ - Windows PowerShell 如拦截 `.ps1` shim,使用 `cgc-init.cmd`、`cgc-status.cmd`、`cgc-doctor.cmd`。
81
+
82
+ ## 写入边界
83
+
84
+ `model-routing.yaml` 是路径归属的唯一策略来源。
85
+
86
+ Claude 可以直接处理:
87
+
88
+ - `codecgc/**`
89
+ - `.claude/**`
90
+ - `.mcp.json`
91
+ - `model-routing.yaml`
92
+ - `README.md`
93
+ - `docs/**`
94
+ - `CHANGELOG.md`
95
+
96
+ Claude 不应直接修改产品源码:
97
+
98
+ - 后端源码和后端测试应交给 Codex。
99
+ - 前端源码和前端测试应交给 Gemini。
100
+ - shared 路径必须先拆分再执行。
101
+ - unknown 路径必须先澄清或更新路由策略。
102
+
103
+ 如果 hook 拦截写入,不要绕过。应解释路径归属,并通过 `/cgc` 或 CodeCGC MCP 重新路由。
104
+
105
+ ## 标准闭环
106
+
107
+ 默认流程:
108
+
109
+ ```text
110
+ 需求 -> 规划 -> 路由 -> Codex/Gemini 执行 -> audit -> Claude 审核 -> 继续或关闭
111
+ ```
112
+
113
+ 稳定状态:
114
+
115
+ - `needs-planning`:先由 Claude 补充或修复规划。
116
+ - `awaiting-build`:feature 步骤可执行。
117
+ - `awaiting-fix`:issue 修复步骤可执行。
118
+ - `awaiting-review`:已有 audit,等待审核。
119
+ - `closed`:当前 workflow 已结束。
120
+
121
+ ## 审核规则
122
+
123
+ `/cgc-review` 是控制点,不是简单写“通过”。
124
+
125
+ 接受前必须确认:
126
+
127
+ - audit 是真实执行,不是 dry-run。
128
+ - executor 归属正确。
129
+ - 变更路径符合 `model-routing.yaml`。
130
+ - 执行结果成功且有证据。
131
+ - 验收标准已满足。
132
+
133
+ 必须驳回或保持 `changes-requested`:
134
+
135
+ - 只有 dry-run。
136
+ - 执行器失败、超时或输出无效。
137
+ - 路径越界。
138
+ - 前后端执行器归属错误。
139
+ - mixed/shared/unknown 路径未拆分。
140
+ - 证据不足或本地事实与执行器自报不一致。
141
+
142
+ ## 失败恢复
143
+
144
+ - executor failure:检查 audit 和执行器输出,不要假装完成。
145
+ - review changes-requested:保持同一 workflow,继续执行推荐的 build/fix/test。
146
+ - mixed ownership:回到 plan,拆成 backend/frontend/docs/orchestration 子步骤。
147
+ - test step:使用 `cgc-test`,不要用 build/fix 代替。
148
+ - session continue:只在同一 task id 和 artifact class 内复用 session id。
149
+
150
+ ## 文档放置
151
+
152
+ 普通项目文档:
153
+
154
+ ```text
155
+ docs/
156
+ README.md
157
+ CHANGELOG.md
158
+ ```
159
+
160
+ CodeCGC 工作流和治理产物:
161
+
162
+ ```text
163
+ codecgc/features/
164
+ codecgc/issues/
165
+ codecgc/execution/
166
+ codecgc/requirements/
167
+ codecgc/architecture/
168
+ codecgc/roadmap/
169
+ codecgc/compound/
170
+ codecgc/docs/
171
+ codecgc/reference/
172
+ ```
173
+
174
+ 长期文档和 audit 使用项目相对路径,不固化某台机器的安装目录或 npx 临时缓存路径。
175
+
176
+ ## 硬规则
177
+
178
+ - 不要为了速度绕过 CodeCGC 直接改产品代码。
179
+ - 用户要求实现功能或修复代码时,优先进入 `/cgc` 工作流。
180
+ - 用户要求修改文档、规划、验收或审核说明时,Claude 可直接处理。
181
+ - dry-run 不是完成证据。
182
+ - 没有 audit 不要宣称执行器任务完成。
183
+ - 不要把 Codex/Gemini 当只读分析模型;它们在 CodeCGC 中是代码执行器。
184
+
185
+ ## 记忆口诀
186
+
187
+ ```text
188
+ Claude 负责想清楚、写清楚、审清楚。
189
+ Codex 负责后端代码。
190
+ Gemini 负责前端代码。
191
+ CodeCGC 负责路由、证据、状态和闭环。
192
+ ```
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ // CodeCGC Edit Guard — PreToolUse hook (Edit|Write|MultiEdit)
3
+ // Routes file edits through model-routing.yaml policy.
4
+ // Zero external dependencies. Never crashes (try/catch -> exit(0)).
5
+
6
+ 'use strict';
7
+
8
+ try {
9
+ var fs = require('fs');
10
+ var path = require('path');
11
+
12
+ // ── Read stdin ──
13
+ var inputData = '';
14
+ if (!process.stdin.isTTY) {
15
+ inputData = fs.readFileSync(0, 'utf-8');
16
+ }
17
+ if (!inputData.trim()) {
18
+ process.exit(0);
19
+ }
20
+
21
+ var parsed = JSON.parse(inputData);
22
+ var toolInput = parsed.tool_input || parsed.input || parsed;
23
+ var toolName = (parsed.tool_name || '').trim();
24
+ var filePath = (toolInput.file_path || toolInput.path || '').trim();
25
+
26
+ if (!filePath) {
27
+ process.exit(0);
28
+ }
29
+
30
+ // ── Find routing file ──
31
+ var cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
+ var routingPath = path.join(cwd, 'model-routing.yaml');
33
+
34
+ if (!fs.existsSync(routingPath)) {
35
+ process.exit(0);
36
+ }
37
+
38
+ // ── Parse YAML (simple, handles model-routing.yaml structure) ──
39
+ var routingContent = fs.readFileSync(routingPath, 'utf-8');
40
+ var sections = parseSimpleYaml(routingContent);
41
+
42
+ // ── Classify path ──
43
+ var normalizedPath = normalizePath(filePath, cwd);
44
+ var owner = classifyPath(normalizedPath, sections);
45
+
46
+ // ── Decide ──
47
+ if (owner === 'orchestration' || owner === 'docs') {
48
+ process.exit(0);
49
+ }
50
+
51
+ var ownerLabel = owner || 'unknown';
52
+ var reason = 'CodeCGC: ' + ownerLabel + ' paths should be routed through /cgc (Codex for backend, Gemini for frontend).';
53
+ if (ownerLabel === 'unknown') {
54
+ reason = 'CodeCGC: path is not covered by model-routing.yaml. Add it to the appropriate section or route through /cgc.';
55
+ }
56
+ if (ownerLabel === 'shared') {
57
+ reason = 'CodeCGC: shared paths require split-first routing. Use /cgc to split into backend/frontend steps.';
58
+ }
59
+
60
+ console.log(JSON.stringify({
61
+ decision: 'deny',
62
+ reason: reason
63
+ }));
64
+ } catch {
65
+ process.exit(0);
66
+ }
67
+
68
+ // ── Helpers ──
69
+
70
+ function normalizePath(filePath, cwd) {
71
+ var p = filePath.replace(/\\/g, '/');
72
+ if (path.isAbsolute(filePath)) {
73
+ try {
74
+ p = path.relative(cwd, filePath).replace(/\\/g, '/');
75
+ } catch { /* keep original */ }
76
+ }
77
+ while (p.startsWith('./')) {
78
+ p = p.slice(2);
79
+ }
80
+ return p;
81
+ }
82
+
83
+ function parseSimpleYaml(content) {
84
+ var sections = {};
85
+ var currentKey = null;
86
+ var lines = content.split('\n');
87
+
88
+ for (var i = 0; i < lines.length; i++) {
89
+ var line = lines[i];
90
+ // Skip empty lines and comments
91
+ if (!line.trim() || line.trim().charAt(0) === '#') continue;
92
+
93
+ // Top-level key (no leading whitespace, ends with :)
94
+ if (line.charAt(0) !== ' ' && line.charAt(0) !== '\t' && line.indexOf(':') > 0) {
95
+ currentKey = line.split(':')[0].trim();
96
+ if (!sections[currentKey]) sections[currentKey] = [];
97
+ continue;
98
+ }
99
+
100
+ if (!currentKey) continue;
101
+
102
+ var trimmed = line.trim();
103
+
104
+ // Nested key under current section (e.g. "frontend:" under "test_paths:")
105
+ // Check if line has indentation + is a key (contains : but is not a list item)
106
+ var indent = line.length - line.replace(/^(\s*)/, '').length;
107
+ if (indent > 0 && trimmed.indexOf(':') > 0 && trimmed.charAt(0) !== '-') {
108
+ var subKey = trimmed.split(':')[0].trim();
109
+ var compoundKey = currentKey + '_' + subKey;
110
+ if (!sections[compoundKey]) sections[compoundKey] = [];
111
+ // Point currentKey to compound key so subsequent list items go there
112
+ currentKey = compoundKey;
113
+ continue;
114
+ }
115
+
116
+ // List item (indented, starts with -)
117
+ if (trimmed.charAt(0) === '-') {
118
+ var value = trimmed.slice(1).trim();
119
+ // Strip quotes
120
+ if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"')
121
+ || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
122
+ value = value.slice(1, -1);
123
+ }
124
+ if (value) {
125
+ sections[currentKey].push(value);
126
+ }
127
+ }
128
+ }
129
+
130
+ return sections;
131
+ }
132
+
133
+ function classifyPath(normalizedPath, sections) {
134
+ // Order matters: shared first, then orchestration, docs, tests, frontend, backend
135
+ var groups = [
136
+ ['shared', sections['shared_paths'] || []],
137
+ ['orchestration', sections['orchestration_paths'] || []],
138
+ ['docs', sections['docs_paths'] || []],
139
+ ['frontend-test', sections['test_paths_frontend'] || []],
140
+ ['backend-test', sections['test_paths_backend'] || []],
141
+ ['frontend', sections['frontend_paths'] || []],
142
+ ['backend', sections['backend_paths'] || []]
143
+ ];
144
+
145
+ for (var i = 0; i < groups.length; i++) {
146
+ var owner = groups[i][0];
147
+ var patterns = groups[i][1];
148
+ if (matchesAny(normalizedPath, patterns)) {
149
+ return owner;
150
+ }
151
+ }
152
+ return 'unknown';
153
+ }
154
+
155
+ function matchesAny(filePath, patterns) {
156
+ for (var i = 0; i < patterns.length; i++) {
157
+ if (globMatch(filePath, patterns[i])) {
158
+ return true;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function globMatch(filePath, pattern) {
165
+ var regex = globToRegex(pattern);
166
+ return regex.test(filePath);
167
+ }
168
+
169
+ function globToRegex(pattern) {
170
+ var regexStr = '^';
171
+ var i = 0;
172
+ while (i < pattern.length) {
173
+ var ch = pattern.charAt(i);
174
+ if (ch === '*' && i + 1 < pattern.length && pattern.charAt(i + 1) === '*') {
175
+ // ** matches everything
176
+ regexStr += '.*';
177
+ i += 2;
178
+ // skip trailing / if present
179
+ if (i < pattern.length && pattern.charAt(i) === '/') i++;
180
+ } else if (ch === '*') {
181
+ // * matches anything except /
182
+ regexStr += '[^/]*';
183
+ i++;
184
+ } else if (ch === '.') {
185
+ regexStr += '\\.';
186
+ i++;
187
+ } else {
188
+ regexStr += ch;
189
+ i++;
190
+ }
191
+ }
192
+ regexStr += '$';
193
+ return new RegExp(regexStr);
194
+ }
@@ -3,10 +3,23 @@
3
3
  "allow": [
4
4
  "WebSearch",
5
5
  "Read(**)",
6
- "Edit(**)",
7
- "Update(**)",
8
- "Write(**)",
9
- "mcp__*",
6
+ "Edit(codecgc/**)",
7
+ "Edit(.claude/**)",
8
+ "Edit(.mcp.json)",
9
+ "Edit(model-routing.yaml)",
10
+ "Edit(README.md)",
11
+ "Edit(docs/**)",
12
+ "Edit(CHANGELOG.md)",
13
+ "Write(codecgc/**)",
14
+ "Write(.claude/**)",
15
+ "Write(.mcp.json)",
16
+ "Write(model-routing.yaml)",
17
+ "Write(README.md)",
18
+ "Write(docs/**)",
19
+ "Write(CHANGELOG.md)",
20
+ "mcp__codecgc__*",
21
+ "mcp__codex__*",
22
+ "mcp__gemini__*",
10
23
  "Bash(*)",
11
24
  "PowerShell(*)"
12
25
  ]
@@ -21,7 +21,7 @@ import shutil
21
21
  mcp = FastMCP("Codex MCP Server-from guda.studio")
22
22
 
23
23
  # Mirror of model-routing.yaml frontend_paths — keep these hints in sync with
24
- # route-edit.ps1 and geminimcp/server.py BACKEND_PATH_HINTS.
24
+ # edit-guard.js and geminimcp/server.py BACKEND_PATH_HINTS.
25
25
  FRONTEND_PATH_HINTS = (
26
26
  "apps/web/",
27
27
  "src/components/",
@@ -25,7 +25,7 @@ PROJECT_GEMINI_POLICY_RELATIVE_PATH = Path(".gemini") / "policies" / "codecgc-po
25
25
  mcp = FastMCP("Gemini MCP Server-from guda.studio")
26
26
 
27
27
  # Mirror of model-routing.yaml backend_paths — keep these hints in sync with
28
- # route-edit.ps1 and codexmcp/server.py FRONTEND_PATH_HINTS.
28
+ # edit-guard.js and codexmcp/server.py FRONTEND_PATH_HINTS.
29
29
  BACKEND_PATH_HINTS = (
30
30
  "apps/api/",
31
31
  "server/",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunyed15/codecgc",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Claude-hosted multi-model workflow product shell for CodeCGC.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -47,7 +47,7 @@
47
47
  "scripts/codecgc_runtime/*.py",
48
48
  "scripts/postinstall_codecgc.js",
49
49
  "scripts/README-codecgc-cli.md",
50
- ".claude/hooks/route-edit.ps1",
50
+ ".claude/hooks/edit-guard.js",
51
51
  "codecgc/cgc/",
52
52
  "codecgc/cgc-arch/",
53
53
  "codecgc/cgc-build/",
@@ -23,7 +23,7 @@ RUNTIME_ENTRYPOINTS = [
23
23
  ]
24
24
 
25
25
  RUNTIME_STATIC_REQUIREMENTS = [
26
- ".claude/hooks/route-edit.ps1",
26
+ ".claude/hooks/edit-guard.js",
27
27
  "codecgc/templates/project/claude/settings.local.json",
28
28
  "codecgc/templates/project/codex/codecgcrc.json",
29
29
  "codecgc/templates/project/gemini/policies/codecgc-policy.toml",
@@ -21,9 +21,10 @@ from sync_codecgc_mcp_config import write_mcp_config
21
21
  WORKSPACE = PACKAGE_ROOT
22
22
  PROJECT_ROUTING_PATH = WORKSPACE / "model-routing.yaml"
23
23
  PROJECT_TEMPLATES_DIR = WORKSPACE / "codecgc" / "templates" / "project"
24
- PROJECT_HOOK_PATH = PROJECT_TEMPLATES_DIR / "claude" / "hooks" / "route-edit.ps1"
25
- EDIT_GUARDRAIL_MATCHER = "Edit|Write|MultiEdit|Bash|PowerShell"
26
- LEGACY_EDIT_GUARDRAIL_MATCHERS = {"Edit|Write", "Edit|Write|MultiEdit"}
24
+ PROJECT_HOOK_PATH = PROJECT_TEMPLATES_DIR / "claude" / "hooks" / "edit-guard.js"
25
+ PROJECT_CLAUDE_MD_TEMPLATE = PROJECT_TEMPLATES_DIR / "CLAUDE.md"
26
+ CLAUDE_MD_MARKER = "<!-- codecgc:claude-md:v1 -->"
27
+ EDIT_GUARDRAIL_MATCHER = "Edit|Write|MultiEdit"
27
28
  PROJECT_ONBOARDING_RELATIVE_PATH = "codecgc/START_HERE.md"
28
29
  PROJECT_ONBOARDING_MARKER = "<!-- codecgc:onboarding:v1 -->"
29
30
  CLAUDE_SETTINGS_TEMPLATE = PROJECT_TEMPLATES_DIR / "claude" / "settings.local.json"
@@ -38,7 +39,7 @@ DEFAULT_HOOKS = {
38
39
  "hooks": [
39
40
  {
40
41
  "type": "command",
41
- "command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/route-edit.ps1",
42
+ "command": "node .claude/hooks/edit-guard.js",
42
43
  }
43
44
  ],
44
45
  }
@@ -83,7 +84,8 @@ def get_workspace_paths(override_workspace: str = "") -> dict[str, Path]:
83
84
  "settings": claude_dir / "settings.local.json",
84
85
  "legacy_settings": claude_dir / "settings.json",
85
86
  "mcp": root / ".mcp.json",
86
- "hook_script": hooks_dir / "route-edit.ps1",
87
+ "hook_script": hooks_dir / "edit-guard.js",
88
+ "claude_md": claude_dir / "CLAUDE.md",
87
89
  "commands_dir": claude_dir / "commands",
88
90
  "routing_file": root / "model-routing.yaml",
89
91
  "onboarding_file": root / PROJECT_ONBOARDING_RELATIVE_PATH,
@@ -165,7 +167,8 @@ cgc "新增一个登录页面,放在 src/components/LoginForm.tsx"
165
167
  .mcp.json
166
168
  model-routing.yaml
167
169
  .claude/settings.local.json
168
- .claude/hooks/route-edit.ps1
170
+ .claude/hooks/edit-guard.js
171
+ .claude/CLAUDE.md
169
172
  .claude/commands/cgc*.md
170
173
  .codex/codecgcrc.json
171
174
  .gemini/policies/codecgc-policy.toml
@@ -250,10 +253,6 @@ def policy_file_is_valid(path: Path) -> bool:
250
253
  return True
251
254
 
252
255
 
253
- def _normalize_command_path_for_markdown(path: Path) -> str:
254
- return str(path).replace("\\", "\\\\")
255
-
256
-
257
256
  def build_mcp_first_command_template(
258
257
  *,
259
258
  filename: str,
@@ -559,7 +558,7 @@ def is_codecgc_route_edit_hook(hook: Any) -> bool:
559
558
  if hook.get("type") != "command":
560
559
  return False
561
560
  command = str(hook.get("command", "")).replace("\\", "/").lower()
562
- return "route-edit.ps1" in command
561
+ return "edit-guard" in command
563
562
 
564
563
 
565
564
  def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dict[str, Any], bool]:
@@ -578,7 +577,7 @@ def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dic
578
577
  for item in list(pre_tool_use):
579
578
  if not isinstance(item, dict):
580
579
  continue
581
- if item.get("matcher") not in LEGACY_EDIT_GUARDRAIL_MATCHERS | {EDIT_GUARDRAIL_MATCHER}:
580
+ if item.get("matcher") != EDIT_GUARDRAIL_MATCHER:
582
581
  continue
583
582
  hook_list = item.get("hooks")
584
583
  if not isinstance(hook_list, list):
@@ -796,32 +795,8 @@ def mcp_server_matches_runtime_shape(server_payload: dict[str, Any], spec: dict[
796
795
  return True
797
796
 
798
797
 
799
- def mcp_config_matches_runtime_shape(payload: dict[str, Any]) -> bool:
800
- servers = payload.get("mcpServers")
801
- if not isinstance(servers, dict):
802
- return False
803
-
804
- for server_name, spec in RUNTIME_MCP_SERVER_SPECS.items():
805
- server_payload = servers.get(server_name)
806
- if not isinstance(server_payload, dict):
807
- return False
808
- if not mcp_server_matches_runtime_shape(server_payload, spec):
809
- return False
810
- return True
811
-
812
-
813
798
  def build_workspace_hook_command(workspace_paths: dict[str, Path]) -> str:
814
- package_root = str(WORKSPACE).replace("'", "''")
815
- workspace_root_path = Path(workspace_paths["root"])
816
- hook_script_path = workspace_paths.get("hook_script", workspace_root_path / ".claude" / "hooks" / "route-edit.ps1")
817
- workspace_root = str(workspace_root_path).replace("'", "''")
818
- hook_script = str(hook_script_path).replace("\\", "/").replace("'", "''")
819
- return (
820
- "powershell -ExecutionPolicy Bypass -Command "
821
- f"\"$env:CODECGC_PACKAGE_ROOT='{package_root}'; "
822
- f"$env:CODECGC_WORKSPACE_ROOT='{workspace_root}'; "
823
- f"& '{hook_script}'\""
824
- )
799
+ return "node .claude/hooks/edit-guard.js"
825
800
 
826
801
 
827
802
  def build_mode_summary_payload(
@@ -841,6 +816,32 @@ def build_mode_summary_payload(
841
816
  return summary
842
817
 
843
818
 
819
+ def install_project_claude_md(target_path: Path) -> str:
820
+ """Install or append CodeCGC rules to .claude/CLAUDE.md.
821
+
822
+ - Does not exist: create with template content.
823
+ - Exists without marker: append template content.
824
+ - Exists with marker: skip (don't overwrite user modifications).
825
+ """
826
+ if not PROJECT_CLAUDE_MD_TEMPLATE.exists():
827
+ return str(target_path)
828
+
829
+ template_content = PROJECT_CLAUDE_MD_TEMPLATE.read_text(encoding="utf-8")
830
+
831
+ if target_path.exists():
832
+ existing = target_path.read_text(encoding="utf-8")
833
+ if CLAUDE_MD_MARKER in existing:
834
+ return str(target_path)
835
+ # Append with separator
836
+ separator = "\n\n---\n\n" if not existing.endswith("\n\n") else ""
837
+ target_path.write_text(existing + separator + template_content, encoding="utf-8")
838
+ else:
839
+ target_path.parent.mkdir(parents=True, exist_ok=True)
840
+ target_path.write_text(template_content, encoding="utf-8")
841
+
842
+ return str(target_path)
843
+
844
+
844
845
  def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
845
846
  workspace_paths = get_workspace_paths(override_workspace)
846
847
  mcp_path = write_mcp_config(workspace_paths["mcp"], workspace_paths["root"])
@@ -861,6 +862,7 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
861
862
  shutil.copyfile(PROJECT_HOOK_PATH, workspace_paths["hook_script"])
862
863
  written_commands = write_custom_command_files(workspace_paths["commands_dir"], WORKSPACE / "bin")
863
864
  onboarding_file = write_project_onboarding_file(workspace_paths["root"])
865
+ claude_md_path = install_project_claude_md(workspace_paths["claude_md"])
864
866
 
865
867
  summary = build_mode_summary_payload(
866
868
  scope="项目级 Claude 与 MCP 集成面",
@@ -878,6 +880,7 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
878
880
  "codex_policy": str(codex_policy_path),
879
881
  "gemini_policy": str(gemini_policy_path),
880
882
  "hook_script": str(workspace_paths["hook_script"]),
883
+ "claude_md": str(claude_md_path),
881
884
  "commands_dir": str(workspace_paths["commands_dir"]),
882
885
  "onboarding_file": str(onboarding_file),
883
886
  "workflow_dirs": workflow_dirs,
@@ -887,7 +890,8 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
887
890
  "Project-local model-routing.yaml was synchronized as the policy source of truth.",
888
891
  "Project-local codecgc workflow directories were initialized.",
889
892
  "Claude pre-edit guardrail hook was synchronized into the target workspace.",
890
- "Claude project permissions were rendered from codecgc/templates/project/claude/settings.local.json.",
893
+ "Claude project permissions were rendered with whitelist (Edit/Write restricted to codecgc, .claude, docs, config).",
894
+ ".claude/CLAUDE.md was installed with CodeCGC workflow rules and three-layer protection guidance.",
891
895
  "Project-local Codex policy contract was synchronized into .codex/codecgcrc.json.",
892
896
  "Project-local Gemini policy was synchronized into .gemini/policies/codecgc-policy.toml.",
893
897
  "Project-local Claude slash commands were synchronized into .claude/commands.",
@@ -106,10 +106,9 @@ def extract_text_content(value: Any) -> str:
106
106
 
107
107
 
108
108
  def normalize_mcp_result(raw: dict[str, Any]) -> dict[str, Any]:
109
- structured_content = raw.get("structuredContent")
110
- if isinstance(structured_content, dict):
111
- return structured_content
112
-
109
+ # Prefer text content over structuredContent — the tool's actual JSON
110
+ # response is in content[0].text, while structuredContent is an MCP SDK
111
+ # convenience wrapper that may omit top-level fields like "success".
113
112
  content = raw.get("content")
114
113
  if isinstance(content, list):
115
114
  for item in content:
@@ -127,6 +126,10 @@ def normalize_mcp_result(raw: dict[str, Any]) -> dict[str, Any]:
127
126
  if isinstance(parsed, dict):
128
127
  return parsed
129
128
 
129
+ structured_content = raw.get("structuredContent")
130
+ if isinstance(structured_content, dict):
131
+ return structured_content
132
+
130
133
  return {
131
134
  "success": False,
132
135
  "error": "Unable to parse structured MCP tool result.",
@@ -280,15 +283,23 @@ def write_audit_file(audit_root: Path, audit_record: dict[str, Any]) -> Path:
280
283
 
281
284
  async def execute_payload(payload: dict[str, Any], timeout_seconds: int) -> dict[str, Any]:
282
285
  params = build_server_params(payload["target"])
286
+ raw_result = None
283
287
 
284
- async with stdio_client(params) as (read_stream, write_stream):
285
- async with ClientSession(read_stream, write_stream) as session:
286
- await session.initialize()
287
- raw_result = await session.call_tool(
288
- payload["tool_name"],
289
- payload["tool_args"],
290
- read_timeout_seconds=datetime.timedelta(seconds=timeout_seconds),
291
- )
288
+ try:
289
+ async with stdio_client(params) as (read_stream, write_stream):
290
+ async with ClientSession(read_stream, write_stream) as session:
291
+ await session.initialize()
292
+ raw_result = await session.call_tool(
293
+ payload["tool_name"],
294
+ payload["tool_args"],
295
+ read_timeout_seconds=datetime.timedelta(seconds=timeout_seconds),
296
+ )
297
+ except* ExceptionGroup:
298
+ # MCP SDK 清理阶段的 TaskGroup 异常不影响已获得的结果
299
+ pass
300
+
301
+ if raw_result is None:
302
+ raise RuntimeError("MCP call failed before producing a result.")
292
303
 
293
304
  dumped = raw_result.model_dump(mode="json")
294
305
  normalized = normalize_mcp_result(dumped)
@@ -1,87 +0,0 @@
1
- $ErrorActionPreference = "Stop"
2
-
3
- function Write-Approve {
4
- $payload = @{
5
- decision = "approve"
6
- } | ConvertTo-Json -Compress
7
- [Console]::Out.Write($payload)
8
- exit 0
9
- }
10
-
11
- function Write-Deny($reason) {
12
- $payload = @{
13
- decision = "deny"
14
- reason = $reason
15
- } | ConvertTo-Json -Compress
16
- [Console]::Out.Write($payload)
17
- exit 0
18
- }
19
-
20
- $inputJson = [Console]::In.ReadToEnd()
21
- if ([string]::IsNullOrWhiteSpace($inputJson)) {
22
- Write-Approve
23
- }
24
-
25
- $configuredPackageRoot = [Environment]::GetEnvironmentVariable("CODECGC_PACKAGE_ROOT")
26
- if (-not [string]::IsNullOrWhiteSpace($configuredPackageRoot)) {
27
- $packageRoot = Resolve-Path $configuredPackageRoot
28
- } else {
29
- $packageRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
30
- }
31
-
32
- $configuredWorkspaceRoot = [Environment]::GetEnvironmentVariable("CODECGC_WORKSPACE_ROOT")
33
- if (-not [string]::IsNullOrWhiteSpace($configuredWorkspaceRoot)) {
34
- $workspaceRoot = Resolve-Path $configuredWorkspaceRoot
35
- } else {
36
- $workspaceRoot = Resolve-Path (Get-Location)
37
- }
38
-
39
- $policyScript = Join-Path $packageRoot "scripts\codecgc_policy.py"
40
- $routingFile = Join-Path $workspaceRoot "model-routing.yaml"
41
- $mcpConfigFile = Join-Path $workspaceRoot ".mcp.json"
42
-
43
- if (-not (Test-Path $policyScript)) {
44
- Write-Deny "CodeCGC: policy checker is missing: $policyScript"
45
- }
46
-
47
- $pythonCommand = [Environment]::GetEnvironmentVariable("CODECGC_PYTHON_COMMAND")
48
- if ([string]::IsNullOrWhiteSpace($pythonCommand) -and (Test-Path $mcpConfigFile)) {
49
- try {
50
- $mcpConfig = Get-Content -Raw $mcpConfigFile | ConvertFrom-Json
51
- $pythonCommand = [string]$mcpConfig.mcpServers.codecgc.command
52
- } catch {
53
- $pythonCommand = ""
54
- }
55
- }
56
- if ([string]::IsNullOrWhiteSpace($pythonCommand)) {
57
- $pythonCommand = "python"
58
- }
59
-
60
- $psi = New-Object System.Diagnostics.ProcessStartInfo
61
- $psi.FileName = $pythonCommand
62
- $escapedPolicyScript = $policyScript.Replace('"', '\"')
63
- $escapedRoutingFile = $routingFile.Replace('"', '\"')
64
- $psi.Arguments = "`"$escapedPolicyScript`" --hook-check --actor claude --operation write --routing-file `"$escapedRoutingFile`""
65
- $psi.RedirectStandardInput = $true
66
- $psi.RedirectStandardOutput = $true
67
- $psi.RedirectStandardError = $true
68
- $psi.UseShellExecute = $false
69
- $psi.CreateNoWindow = $true
70
-
71
- $process = [System.Diagnostics.Process]::Start($psi)
72
- $process.StandardInput.Write($inputJson)
73
- $process.StandardInput.Close()
74
- $stdout = $process.StandardOutput.ReadToEnd()
75
- $stderr = $process.StandardError.ReadToEnd()
76
- $process.WaitForExit()
77
-
78
- if ($process.ExitCode -ne 0) {
79
- $detail = if ([string]::IsNullOrWhiteSpace($stderr)) { $stdout } else { $stderr }
80
- Write-Deny "CodeCGC: policy checker failed. $detail"
81
- }
82
-
83
- if ([string]::IsNullOrWhiteSpace($stdout)) {
84
- Write-Approve
85
- }
86
-
87
- [Console]::Out.Write($stdout)
@@ -1,87 +0,0 @@
1
- $ErrorActionPreference = "Stop"
2
-
3
- function Write-Approve {
4
- $payload = @{
5
- decision = "approve"
6
- } | ConvertTo-Json -Compress
7
- [Console]::Out.Write($payload)
8
- exit 0
9
- }
10
-
11
- function Write-Deny($reason) {
12
- $payload = @{
13
- decision = "deny"
14
- reason = $reason
15
- } | ConvertTo-Json -Compress
16
- [Console]::Out.Write($payload)
17
- exit 0
18
- }
19
-
20
- $inputJson = [Console]::In.ReadToEnd()
21
- if ([string]::IsNullOrWhiteSpace($inputJson)) {
22
- Write-Approve
23
- }
24
-
25
- $configuredPackageRoot = [Environment]::GetEnvironmentVariable("CODECGC_PACKAGE_ROOT")
26
- if (-not [string]::IsNullOrWhiteSpace($configuredPackageRoot)) {
27
- $packageRoot = Resolve-Path $configuredPackageRoot
28
- } else {
29
- $packageRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
30
- }
31
-
32
- $configuredWorkspaceRoot = [Environment]::GetEnvironmentVariable("CODECGC_WORKSPACE_ROOT")
33
- if (-not [string]::IsNullOrWhiteSpace($configuredWorkspaceRoot)) {
34
- $workspaceRoot = Resolve-Path $configuredWorkspaceRoot
35
- } else {
36
- $workspaceRoot = Resolve-Path (Get-Location)
37
- }
38
-
39
- $policyScript = Join-Path $packageRoot "scripts\codecgc_policy.py"
40
- $routingFile = Join-Path $workspaceRoot "model-routing.yaml"
41
- $mcpConfigFile = Join-Path $workspaceRoot ".mcp.json"
42
-
43
- if (-not (Test-Path $policyScript)) {
44
- Write-Deny "CodeCGC: policy checker is missing: $policyScript"
45
- }
46
-
47
- $pythonCommand = [Environment]::GetEnvironmentVariable("CODECGC_PYTHON_COMMAND")
48
- if ([string]::IsNullOrWhiteSpace($pythonCommand) -and (Test-Path $mcpConfigFile)) {
49
- try {
50
- $mcpConfig = Get-Content -Raw $mcpConfigFile | ConvertFrom-Json
51
- $pythonCommand = [string]$mcpConfig.mcpServers.codecgc.command
52
- } catch {
53
- $pythonCommand = ""
54
- }
55
- }
56
- if ([string]::IsNullOrWhiteSpace($pythonCommand)) {
57
- $pythonCommand = "python"
58
- }
59
-
60
- $psi = New-Object System.Diagnostics.ProcessStartInfo
61
- $psi.FileName = $pythonCommand
62
- $escapedPolicyScript = $policyScript.Replace('"', '\"')
63
- $escapedRoutingFile = $routingFile.Replace('"', '\"')
64
- $psi.Arguments = "`"$escapedPolicyScript`" --hook-check --actor claude --operation write --routing-file `"$escapedRoutingFile`""
65
- $psi.RedirectStandardInput = $true
66
- $psi.RedirectStandardOutput = $true
67
- $psi.RedirectStandardError = $true
68
- $psi.UseShellExecute = $false
69
- $psi.CreateNoWindow = $true
70
-
71
- $process = [System.Diagnostics.Process]::Start($psi)
72
- $process.StandardInput.Write($inputJson)
73
- $process.StandardInput.Close()
74
- $stdout = $process.StandardOutput.ReadToEnd()
75
- $stderr = $process.StandardError.ReadToEnd()
76
- $process.WaitForExit()
77
-
78
- if ($process.ExitCode -ne 0) {
79
- $detail = if ([string]::IsNullOrWhiteSpace($stderr)) { $stdout } else { $stderr }
80
- Write-Deny "CodeCGC: policy checker failed. $detail"
81
- }
82
-
83
- if ([string]::IsNullOrWhiteSpace($stdout)) {
84
- Write-Approve
85
- }
86
-
87
- [Console]::Out.Write($stdout)