@hunyed15/codecgc 0.2.2 → 0.2.5

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.
Files changed (43) hide show
  1. package/README.md +32 -10
  2. package/bin/cgc-build.js +1 -1
  3. package/bin/cgc-doctor.js +1 -1
  4. package/bin/cgc-entry.js +1 -1
  5. package/bin/cgc-explain.js +4 -0
  6. package/bin/cgc-external-audit.js +1 -1
  7. package/bin/cgc-external-status.js +1 -1
  8. package/bin/cgc-fix.js +1 -1
  9. package/bin/cgc-history.js +1 -1
  10. package/bin/cgc-init.js +1 -1
  11. package/bin/cgc-lifecycle.js +1 -1
  12. package/bin/cgc-package-audit.js +1 -1
  13. package/bin/cgc-plan.js +1 -1
  14. package/bin/cgc-release-readiness.js +1 -1
  15. package/bin/cgc-review.js +1 -1
  16. package/bin/cgc-route.js +1 -1
  17. package/bin/cgc-start.js +1 -1
  18. package/bin/cgc-status.js +1 -1
  19. package/bin/cgc-test.js +1 -1
  20. package/bin/codecgc.js +45 -2
  21. package/codecgc/cgc-onboard/SKILL.md +1 -1
  22. package/codecgc/reference/execution-model.md +1 -1
  23. package/codecgc/reference/policy-routing.md +1 -2
  24. package/codecgc/reference/project-structure.md +3 -3
  25. package/codecgc/reference/quickstart.md +1 -1
  26. package/codecgc/reference/shared-conventions.md +1 -1
  27. package/codecgc/templates/project/CLAUDE.md +214 -0
  28. package/codecgc/templates/project/claude/hooks/edit-guard.js +194 -0
  29. package/codecgc/templates/project/claude/settings.local.json +17 -4
  30. package/mcp/codexmcp/src/codexmcp/server.py +38 -4
  31. package/mcp/geminimcp/src/geminimcp/server.py +1 -1
  32. package/package.json +3 -2
  33. package/scripts/audit_codecgc_package_runtime.py +1 -1
  34. package/scripts/codecgc_error_catalog.py +172 -0
  35. package/scripts/codecgc_error_formatter.py +124 -0
  36. package/scripts/codecgc_flow_control.py +11 -0
  37. package/scripts/codecgc_runtime/console.py +9 -0
  38. package/scripts/codecgc_runtime/workflow_runtime.py +18 -0
  39. package/scripts/explain_codecgc_error.py +71 -0
  40. package/scripts/install_codecgc.py +92 -39
  41. package/scripts/postinstall_codecgc.js +23 -5
  42. package/.claude/hooks/route-edit.ps1 +0 -87
  43. package/codecgc/templates/project/claude/hooks/route-edit.ps1 +0 -87
@@ -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
  ]
@@ -20,8 +20,10 @@ import shutil
20
20
 
21
21
  mcp = FastMCP("Codex MCP Server-from guda.studio")
22
22
 
23
+ DEFAULT_CODEX_TIMEOUT_SECONDS = 600
24
+
23
25
  # Mirror of model-routing.yaml frontend_paths — keep these hints in sync with
24
- # route-edit.ps1 and geminimcp/server.py BACKEND_PATH_HINTS.
26
+ # edit-guard.js and geminimcp/server.py BACKEND_PATH_HINTS.
25
27
  FRONTEND_PATH_HINTS = (
26
28
  "apps/web/",
27
29
  "src/components/",
@@ -177,7 +179,25 @@ def _validate_backend_target_paths(target_paths: List[Path]) -> tuple[bool, List
177
179
  return True, policy_checks, ""
178
180
 
179
181
 
180
- def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
182
+ def _terminate_process_tree(process: subprocess.Popen[str]) -> None:
183
+ """Terminate a process and its children best-effort."""
184
+ if process.poll() is not None:
185
+ return
186
+
187
+ if os.name == "nt":
188
+ subprocess.run(
189
+ ["taskkill", "/PID", str(process.pid), "/T", "/F"],
190
+ stdin=subprocess.DEVNULL,
191
+ stdout=subprocess.DEVNULL,
192
+ stderr=subprocess.DEVNULL,
193
+ check=False,
194
+ )
195
+ return
196
+
197
+ process.kill()
198
+
199
+
200
+ def run_shell_command(cmd: list[str], timeout_seconds: int = DEFAULT_CODEX_TIMEOUT_SECONDS) -> Generator[str, None, None]:
181
201
  """Execute a command and stream its output line-by-line.
182
202
 
183
203
  Args:
@@ -204,6 +224,8 @@ def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
204
224
 
205
225
  output_queue: queue.Queue[str | None] = queue.Queue()
206
226
  GRACEFUL_SHUTDOWN_DELAY = 0.3
227
+ started_at = time.monotonic()
228
+ timed_out = False
207
229
 
208
230
  def is_turn_completed(line: str) -> bool:
209
231
  """Check if the line indicates turn completion via JSON parsing."""
@@ -237,13 +259,17 @@ def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
237
259
  break
238
260
  yield line
239
261
  except queue.Empty:
262
+ if timeout_seconds > 0 and time.monotonic() - started_at > timeout_seconds:
263
+ timed_out = True
264
+ _terminate_process_tree(process)
265
+ break
240
266
  if process.poll() is not None and not thread.is_alive():
241
267
  break
242
268
 
243
269
  try:
244
270
  process.wait(timeout=5)
245
271
  except subprocess.TimeoutExpired:
246
- process.kill()
272
+ _terminate_process_tree(process)
247
273
  process.wait()
248
274
  thread.join(timeout=5)
249
275
 
@@ -255,6 +281,13 @@ def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
255
281
  except queue.Empty:
256
282
  break
257
283
 
284
+ if timed_out:
285
+ raise TimeoutError(
286
+ f"Codex CLI timed out after {timeout_seconds} seconds. "
287
+ "This usually means the CLI was waiting for interactive approval, "
288
+ "network/authentication, or a long-running tool call."
289
+ )
290
+
258
291
 
259
292
  def _execute_codex_session(
260
293
  *,
@@ -268,6 +301,7 @@ def _execute_codex_session(
268
301
  model: str,
269
302
  yolo: bool,
270
303
  profile: str,
304
+ timeout_seconds: int = DEFAULT_CODEX_TIMEOUT_SECONDS,
271
305
  ) -> Dict[str, Any]:
272
306
  """Execute Codex CLI and return the parsed MCP response payload."""
273
307
  if not cd.exists():
@@ -310,7 +344,7 @@ def _execute_codex_session(
310
344
  err_message = ""
311
345
  thread_id: Optional[str] = None
312
346
 
313
- for line in run_shell_command(cmd):
347
+ for line in run_shell_command(cmd, timeout_seconds):
314
348
  try:
315
349
  line_dict = json.loads(line.strip())
316
350
  all_messages.append(line_dict)
@@ -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.2",
3
+ "version": "0.2.5",
4
4
  "description": "Claude-hosted multi-model workflow product shell for CodeCGC.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -10,6 +10,7 @@
10
10
  "cgc-init": "bin/cgc-init.js",
11
11
  "cgc-status": "bin/cgc-status.js",
12
12
  "cgc-doctor": "bin/cgc-doctor.js",
13
+ "cgc-explain": "bin/cgc-explain.js",
13
14
  "cgc-package-audit": "bin/cgc-package-audit.js",
14
15
  "cgc-external-audit": "bin/cgc-external-audit.js",
15
16
  "cgc-external-status": "bin/cgc-external-status.js",
@@ -47,7 +48,7 @@
47
48
  "scripts/codecgc_runtime/*.py",
48
49
  "scripts/postinstall_codecgc.js",
49
50
  "scripts/README-codecgc-cli.md",
50
- ".claude/hooks/route-edit.ps1",
51
+ ".claude/hooks/edit-guard.js",
51
52
  "codecgc/cgc/",
52
53
  "codecgc/cgc-arch/",
53
54
  "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",
@@ -0,0 +1,172 @@
1
+ """Error code classification and explanation for CodeCGC.
2
+
3
+ Provides human-readable explanations and actionable suggestions for common error scenarios.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+
10
+ # Error code to explanation mapping
11
+ ERROR_CATALOG: dict[str, dict[str, str]] = {
12
+ "executor-crash": {
13
+ "title": "执行器内部异常",
14
+ "description": "执行器脚本在运行过程中异常退出,未能完成任务。",
15
+ "common_causes": [
16
+ "执行器环境配置不完整(缺少依赖、路径错误)",
17
+ "执行器超时或资源不足",
18
+ "执行器内部代码错误",
19
+ ],
20
+ "suggestions": [
21
+ "运行 cgc-doctor 检查执行器环境配置",
22
+ "查看审计文件中的详细错误日志",
23
+ "如果持续失败,尝试重新安装:cgc-init",
24
+ ],
25
+ },
26
+ "executor-failure": {
27
+ "title": "执行器任务失败",
28
+ "description": "执行器正常运行,但任务执行失败(如代码生成失败、测试未通过)。",
29
+ "common_causes": [
30
+ "目标路径不存在或不可访问",
31
+ "任务契约与实际代码不匹配",
32
+ "执行器返回的结果格式不正确",
33
+ ],
34
+ "suggestions": [
35
+ "检查审计文件中的执行器输出",
36
+ "确认目标路径在工作区中存在",
37
+ "如果是路径问题,回到 cgc-plan 修正步骤契约",
38
+ ],
39
+ },
40
+ "scope-error": {
41
+ "title": "任务范围错误",
42
+ "description": "当前步骤包含多个执行器归属的路径,需要拆分。",
43
+ "common_causes": [
44
+ "一个步骤同时包含前端和后端路径",
45
+ "一个步骤包含 shared 或 unknown 路径",
46
+ ],
47
+ "suggestions": [
48
+ "运行 cgc-plan 查看拆分建议",
49
+ "按执行器归属(frontend/backend)拆分成独立步骤",
50
+ "shared 路径需要先明确归属或单独处理",
51
+ ],
52
+ },
53
+ "design-gap": {
54
+ "title": "设计缺口",
55
+ "description": "当前步骤引用的路径或配置在路由策略中未覆盖,或目标文件不存在。",
56
+ "common_causes": [
57
+ "目标路径在 model-routing.yaml 中未定义",
58
+ "目标文件在工作区中不存在",
59
+ "路由规则与实际项目结构不匹配",
60
+ ],
61
+ "suggestions": [
62
+ "检查 model-routing.yaml 是否覆盖目标路径",
63
+ "确认目标文件在工作区中存在",
64
+ "回到 cgc-plan 修正目标路径或步骤契约",
65
+ ],
66
+ },
67
+ "environment-or-tooling": {
68
+ "title": "环境或工具问题",
69
+ "description": "执行器环境、依赖或外部工具不可用。",
70
+ "common_causes": [
71
+ "执行器 CLI 未安装或不在 PATH 中",
72
+ "执行器超时(网络、认证、长时间运行)",
73
+ "缺少必需的依赖或配置文件",
74
+ ],
75
+ "suggestions": [
76
+ "运行 cgc-doctor 检查执行器可用性",
77
+ "检查网络连接和认证状态",
78
+ "确认执行器 CLI 已正确安装",
79
+ ],
80
+ },
81
+ "workflow-state": {
82
+ "title": "工作流状态不满足",
83
+ "description": "当前工作流状态不允许执行请求的操作。",
84
+ "common_causes": [
85
+ "尝试执行 build/fix/test,但工作流还在 needs-planning 状态",
86
+ "尝试 review,但没有可审核的执行结果",
87
+ "工作流已关闭,但尝试继续执行",
88
+ ],
89
+ "suggestions": [
90
+ "运行 cgc-route 查看当前工作流状态和推荐命令",
91
+ "按推荐命令顺序执行(plan → build/fix → review)",
92
+ "如果工作流已关闭,使用 /cgc 开始新的工作流",
93
+ ],
94
+ },
95
+ "returned-to-planning": {
96
+ "title": "返回规划阶段",
97
+ "description": "执行器发现问题,需要回到规划阶段修正。",
98
+ "common_causes": [
99
+ "任务范围需要拆分(scope-error)",
100
+ "目标路径或契约有设计缺口(design-gap)",
101
+ ],
102
+ "suggestions": [
103
+ "运行 cgc-plan 查看问题详情和修正建议",
104
+ "根据建议修正步骤契约或拆分步骤",
105
+ "修正后重新执行 build/fix",
106
+ ],
107
+ },
108
+ }
109
+
110
+
111
+ def explain_error(error_code: str) -> dict[str, Any]:
112
+ """Get explanation for an error code.
113
+
114
+ Args:
115
+ error_code: Error code from failure_type field
116
+
117
+ Returns:
118
+ Dict with title, description, common_causes, and suggestions
119
+ """
120
+ if error_code not in ERROR_CATALOG:
121
+ return {
122
+ "error_code": error_code,
123
+ "title": "未知错误类型",
124
+ "description": f"错误代码 '{error_code}' 未在错误分类表中定义。",
125
+ "common_causes": [],
126
+ "suggestions": [
127
+ "查看完整错误信息中的 error 和 next 字段",
128
+ "运行 cgc-doctor 检查环境配置",
129
+ "如果问题持续,请报告此错误代码",
130
+ ],
131
+ }
132
+
133
+ explanation = ERROR_CATALOG[error_code].copy()
134
+ explanation["error_code"] = error_code
135
+ return explanation
136
+
137
+
138
+ def list_error_codes() -> list[str]:
139
+ """List all available error codes."""
140
+ return sorted(ERROR_CATALOG.keys())
141
+
142
+
143
+ def format_explanation(explanation: dict[str, Any]) -> str:
144
+ """Format explanation as human-readable text.
145
+
146
+ Args:
147
+ explanation: Result from explain_error
148
+
149
+ Returns:
150
+ Formatted text block
151
+ """
152
+ lines = [
153
+ f"错误代码: {explanation['error_code']}",
154
+ f"标题: {explanation['title']}",
155
+ "",
156
+ "说明:",
157
+ explanation['description'],
158
+ "",
159
+ ]
160
+
161
+ if explanation.get("common_causes"):
162
+ lines.append("常见原因:")
163
+ for cause in explanation["common_causes"]:
164
+ lines.append(f" - {cause}")
165
+ lines.append("")
166
+
167
+ if explanation.get("suggestions"):
168
+ lines.append("建议操作:")
169
+ for suggestion in explanation["suggestions"]:
170
+ lines.append(f" - {suggestion}")
171
+
172
+ return "\n".join(lines)
@@ -0,0 +1,124 @@
1
+ """Error formatting and leveling for CodeCGC output.
2
+
3
+ Provides three-level error display:
4
+ - summary: User-friendly Chinese message for non-technical users
5
+ - detail: Technical context for developers
6
+ - debug: Full logs and tracebacks for troubleshooting
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ def format_error_output(
14
+ result: dict[str, Any],
15
+ *,
16
+ level: str = "summary",
17
+ ) -> dict[str, Any]:
18
+ """Format error output with appropriate detail level.
19
+
20
+ Args:
21
+ result: Raw result dict from workflow execution
22
+ level: Display level - "summary", "detail", or "debug"
23
+
24
+ Returns:
25
+ Formatted result with error_display field
26
+ """
27
+ if result.get("success"):
28
+ return result
29
+
30
+ error_display = _build_error_display(result, level)
31
+ formatted = result.copy()
32
+ formatted["error_display"] = error_display
33
+ formatted["error_level"] = level
34
+
35
+ return formatted
36
+
37
+
38
+ def _build_error_display(result: dict[str, Any], level: str) -> dict[str, Any]:
39
+ """Build error display structure based on level."""
40
+ display: dict[str, Any] = {
41
+ "level": level,
42
+ }
43
+
44
+ # Summary level: user-friendly Chinese only
45
+ if level == "summary":
46
+ display["message"] = _extract_user_message(result)
47
+ display["suggestion"] = _extract_user_suggestion(result)
48
+ return display
49
+
50
+ # Detail level: add technical context
51
+ if level == "detail":
52
+ display["message"] = _extract_user_message(result)
53
+ display["suggestion"] = _extract_user_suggestion(result)
54
+ display["technical_error"] = result.get("error", "")
55
+ display["failure_type"] = result.get("failure_type", "")
56
+ display["state"] = result.get("state", "")
57
+ display["audit_path"] = result.get("audit_path", "")
58
+ return display
59
+
60
+ # Debug level: everything
61
+ if level == "debug":
62
+ display["message"] = _extract_user_message(result)
63
+ display["suggestion"] = _extract_user_suggestion(result)
64
+ display["technical_error"] = result.get("error", "")
65
+ display["failure_type"] = result.get("failure_type", "")
66
+ display["state"] = result.get("state", "")
67
+ display["audit_path"] = result.get("audit_path", "")
68
+ display["full_result"] = result
69
+ return display
70
+
71
+ # Fallback
72
+ display["message"] = _extract_user_message(result)
73
+ return display
74
+
75
+
76
+ def _extract_user_message(result: dict[str, Any]) -> str:
77
+ """Extract user-friendly error message."""
78
+ # Check for human_error first (from Phase 1)
79
+ execution = result.get("execution", {})
80
+ if isinstance(execution, dict):
81
+ exec_result = execution.get("result", {})
82
+ if isinstance(exec_result, dict):
83
+ human_error = exec_result.get("human_error", "").strip()
84
+ if human_error:
85
+ return human_error
86
+
87
+ # Check for next field (from flow control)
88
+ next_msg = result.get("next", "").strip()
89
+ if next_msg:
90
+ return next_msg
91
+
92
+ # Check for error field
93
+ error_msg = result.get("error", "").strip()
94
+ if error_msg:
95
+ # If it looks technical, provide generic message
96
+ if "failed with exit code" in error_msg.lower() or "traceback" in error_msg.lower():
97
+ return "执行步骤失败。请查看详细信息或运行 cgc-doctor 检查环境。"
98
+ return error_msg
99
+
100
+ return "执行失败,未提供详细信息。"
101
+
102
+
103
+ def _extract_user_suggestion(result: dict[str, Any]) -> str:
104
+ """Extract user-friendly suggestion."""
105
+ # Check for suggestion field (from Phase 1)
106
+ execution = result.get("execution", {})
107
+ if isinstance(execution, dict):
108
+ exec_result = execution.get("result", {})
109
+ if isinstance(exec_result, dict):
110
+ suggestion = exec_result.get("suggestion", "").strip()
111
+ if suggestion:
112
+ return suggestion
113
+
114
+ # Check for recommended_command
115
+ recommended = result.get("recommended_command", "").strip()
116
+ if recommended:
117
+ return f"建议运行: {recommended}"
118
+
119
+ # Check for next field that contains suggestions
120
+ next_msg = result.get("next", "").strip()
121
+ if next_msg and ("建议" in next_msg or "请" in next_msg or "运行" in next_msg):
122
+ return next_msg
123
+
124
+ return "请稍后重试,或运行 cgc-doctor 检查环境配置。"
@@ -108,6 +108,17 @@ def classify_execution_failure(execution: dict[str, Any]) -> tuple[str, str, str
108
108
  "当前步骤引用的目标路径在工作区中不存在,请先回到 cgc-plan 修正目标路径或步骤契约。",
109
109
  )
110
110
 
111
+ if "failed with exit code" in combined:
112
+ human = str(result.get("human_error", "")).strip()
113
+ suggestion = str(result.get("suggestion", "")).strip()
114
+ next_msg = human + "\n" + suggestion if human else "执行器内部异常,请稍后重试。如果持续失败,运行 cgc-doctor 检查环境。"
115
+ return (
116
+ "blocked",
117
+ "executor-crash",
118
+ "",
119
+ next_msg,
120
+ )
121
+
111
122
  if outcome == "blocked" or "timeout" in combined or "does not exist" in combined:
112
123
  return (
113
124
  "blocked",