@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.
- package/README.md +32 -10
- package/bin/cgc-build.js +1 -1
- package/bin/cgc-doctor.js +1 -1
- package/bin/cgc-entry.js +1 -1
- package/bin/cgc-explain.js +4 -0
- package/bin/cgc-external-audit.js +1 -1
- package/bin/cgc-external-status.js +1 -1
- package/bin/cgc-fix.js +1 -1
- package/bin/cgc-history.js +1 -1
- package/bin/cgc-init.js +1 -1
- package/bin/cgc-lifecycle.js +1 -1
- package/bin/cgc-package-audit.js +1 -1
- package/bin/cgc-plan.js +1 -1
- package/bin/cgc-release-readiness.js +1 -1
- package/bin/cgc-review.js +1 -1
- package/bin/cgc-route.js +1 -1
- package/bin/cgc-start.js +1 -1
- package/bin/cgc-status.js +1 -1
- package/bin/cgc-test.js +1 -1
- package/bin/codecgc.js +45 -2
- package/codecgc/cgc-onboard/SKILL.md +1 -1
- package/codecgc/reference/execution-model.md +1 -1
- package/codecgc/reference/policy-routing.md +1 -2
- package/codecgc/reference/project-structure.md +3 -3
- package/codecgc/reference/quickstart.md +1 -1
- package/codecgc/reference/shared-conventions.md +1 -1
- package/codecgc/templates/project/CLAUDE.md +214 -0
- package/codecgc/templates/project/claude/hooks/edit-guard.js +194 -0
- package/codecgc/templates/project/claude/settings.local.json +17 -4
- package/mcp/codexmcp/src/codexmcp/server.py +38 -4
- package/mcp/geminimcp/src/geminimcp/server.py +1 -1
- package/package.json +3 -2
- package/scripts/audit_codecgc_package_runtime.py +1 -1
- package/scripts/codecgc_error_catalog.py +172 -0
- package/scripts/codecgc_error_formatter.py +124 -0
- package/scripts/codecgc_flow_control.py +11 -0
- package/scripts/codecgc_runtime/console.py +9 -0
- package/scripts/codecgc_runtime/workflow_runtime.py +18 -0
- package/scripts/explain_codecgc_error.py +71 -0
- package/scripts/install_codecgc.py +92 -39
- package/scripts/postinstall_codecgc.js +23 -5
- package/.claude/hooks/route-edit.ps1 +0 -87
- 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
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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/
|
|
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/
|
|
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",
|