@hunyed15/codecgc 0.1.7 → 0.1.9
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/.claude/hooks/route-edit.ps1 +58 -57
- package/INSTALLATION.md +122 -484
- package/README.md +124 -149
- package/bin/cgc-external-status.js +4 -0
- package/bin/cgc-start.js +4 -0
- package/bin/codecgc.js +141 -20
- package/codecgc/compound/codecgc-capability-matrix.md +37 -0
- package/codecgc/reference/README.md +69 -0
- package/codecgc/reference/execution-model.md +3 -1
- package/codecgc/reference/maintainer-guide.md +93 -0
- package/codecgc/reference/mcp-tool-surface.md +112 -0
- package/codecgc/reference/onboarding.md +69 -0
- package/codecgc/reference/operation-guide.md +29 -23
- package/codecgc/reference/path-contract.md +53 -0
- package/codecgc/reference/policy-routing.md +58 -0
- package/codecgc/reference/project-structure.md +87 -0
- package/codecgc/reference/quickstart.md +110 -0
- package/codecgc/reference/real-workflow-loop.md +123 -0
- package/codecgc/reference/recovery-loop.md +109 -0
- package/codecgc/reference/release-maintenance-playbook.md +4 -1
- package/codecgc/reference/troubleshooting.md +114 -0
- package/codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md +49 -0
- package/codecgc/roadmap/codecgc-release-maintenance/overview.md +41 -0
- package/codecgc/roadmap/codecgc-release-maintenance/phases.md +84 -0
- package/codecgc/templates/claude/settings.local.json +27 -0
- package/codecgc/templates/codex/codecgcrc.json +22 -0
- package/codecgc/templates/gemini/codecgc-policy.toml +47 -0
- package/codecgcmcp/README.md +57 -11
- package/codecgcmcp/src/codecgcmcp/server.py +164 -26
- package/codexmcp/src/codexmcp/server.py +45 -0
- package/geminimcp/src/geminimcp/server.py +106 -24
- package/model-routing.yaml +31 -6
- package/package.json +12 -4
- package/scripts/audit_codecgc_external_capabilities.py +83 -4
- package/scripts/audit_codecgc_historical_audits.py +42 -2
- package/scripts/audit_codecgc_package_runtime.py +76 -4
- package/scripts/audit_codecgc_release_readiness.py +55 -3
- package/scripts/audit_codecgc_workflow_history.py +8 -5
- package/scripts/build_codecgc_task.py +69 -45
- package/scripts/codecgc_artifact_roots.py +8 -40
- package/scripts/codecgc_console_io.py +3 -45
- package/scripts/codecgc_executor_registry.py +4 -54
- package/scripts/codecgc_path_contract.py +7 -0
- package/scripts/codecgc_policy.py +447 -0
- package/scripts/codecgc_routing_paths.py +3 -16
- package/scripts/codecgc_routing_template.py +11 -135
- package/scripts/codecgc_runtime/__init__.py +1 -0
- package/scripts/codecgc_runtime/artifacts.py +42 -0
- package/scripts/codecgc_runtime/console.py +45 -0
- package/scripts/codecgc_runtime/executor_registry.py +55 -0
- package/scripts/codecgc_runtime/mcp_config.py +72 -0
- package/scripts/codecgc_runtime/path_contract.py +123 -0
- package/scripts/codecgc_runtime/paths.py +22 -0
- package/scripts/codecgc_runtime/routing_paths.py +16 -0
- package/scripts/codecgc_runtime/routing_template.py +171 -0
- package/scripts/codecgc_runtime/workflow_runtime.py +72 -0
- package/scripts/codecgc_runtime_paths.py +3 -22
- package/scripts/codecgc_step_control.py +3 -2
- package/scripts/codecgc_workflow_runtime.py +3 -71
- package/scripts/entry_codecgc_workflow.py +4 -0
- package/scripts/install_codecgc.py +560 -32
- package/scripts/normalize_codecgc_audits.py +5 -3
- package/scripts/postinstall_codecgc.js +6 -56
- package/scripts/review_codecgc_workflow.py +6 -3
- package/scripts/route_codecgc_workflow.py +67 -36
- package/scripts/run_codecgc_build.py +28 -0
- package/scripts/run_codecgc_fix.py +28 -0
- package/scripts/run_codecgc_task.py +5 -2
- package/scripts/run_codecgc_test.py +28 -0
- package/scripts/sync_codecgc_mcp_config.py +4 -54
- package/scripts/write_codecgc_review.py +7 -3
|
@@ -13,14 +13,21 @@ PACKAGE_JSON_PATH = WORKSPACE / "package.json"
|
|
|
13
13
|
|
|
14
14
|
RUNTIME_ENTRYPOINTS = [
|
|
15
15
|
"bin/codecgc.js",
|
|
16
|
+
"bin/cgc-start.js",
|
|
17
|
+
"codecgcmcp/src/codecgcmcp/cli.py",
|
|
16
18
|
"scripts/install_codecgc.py",
|
|
17
19
|
"scripts/codecgc_cli.py",
|
|
20
|
+
"scripts/codecgc_policy.py",
|
|
18
21
|
]
|
|
19
22
|
|
|
20
23
|
RUNTIME_STATIC_REQUIREMENTS = [
|
|
21
24
|
".claude/hooks/route-edit.ps1",
|
|
25
|
+
"codecgc/templates/claude/settings.local.json",
|
|
26
|
+
"codecgc/templates/codex/codecgcrc.json",
|
|
27
|
+
"codecgc/templates/gemini/codecgc-policy.toml",
|
|
22
28
|
"model-routing.yaml",
|
|
23
29
|
"requirements.txt",
|
|
30
|
+
"scripts/codecgc_runtime/__init__.py",
|
|
24
31
|
"scripts/audit_codecgc_external_capabilities.py",
|
|
25
32
|
"scripts/audit_codecgc_lifecycle.py",
|
|
26
33
|
"codexmcp/pyproject.toml",
|
|
@@ -45,10 +52,25 @@ DOC_RUNTIME_PATHS = [
|
|
|
45
52
|
"codecgc/cgc-onboard/SKILL.md",
|
|
46
53
|
"codecgc/cgc-plan/SKILL.md",
|
|
47
54
|
"codecgc/cgc-review/SKILL.md",
|
|
55
|
+
"codecgc/compound/codecgc-capability-matrix.md",
|
|
56
|
+
"codecgc/reference/README.md",
|
|
48
57
|
"codecgc/reference/external-capability-registry.json",
|
|
49
58
|
"codecgc/reference/lifecycle-playbook.md",
|
|
59
|
+
"codecgc/reference/maintainer-guide.md",
|
|
60
|
+
"codecgc/reference/mcp-tool-surface.md",
|
|
50
61
|
"codecgc/reference/operation-guide.md",
|
|
62
|
+
"codecgc/reference/path-contract.md",
|
|
63
|
+
"codecgc/reference/policy-routing.md",
|
|
64
|
+
"codecgc/reference/project-structure.md",
|
|
65
|
+
"codecgc/reference/quickstart.md",
|
|
66
|
+
"codecgc/reference/onboarding.md",
|
|
67
|
+
"codecgc/reference/recovery-loop.md",
|
|
68
|
+
"codecgc/reference/real-workflow-loop.md",
|
|
51
69
|
"codecgc/reference/release-maintenance-playbook.md",
|
|
70
|
+
"codecgc/reference/troubleshooting.md",
|
|
71
|
+
"codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md",
|
|
72
|
+
"codecgc/roadmap/codecgc-release-maintenance/overview.md",
|
|
73
|
+
"codecgc/roadmap/codecgc-release-maintenance/phases.md",
|
|
52
74
|
]
|
|
53
75
|
|
|
54
76
|
PLACEHOLDER_METADATA_MARKERS = (
|
|
@@ -148,19 +170,64 @@ def path_matches_package_files(path_text: str, file_rules: list[str]) -> bool:
|
|
|
148
170
|
continue
|
|
149
171
|
if normalized == normalized_rule:
|
|
150
172
|
return True
|
|
173
|
+
if (WORKSPACE / normalized_rule).is_dir() and normalized.startswith(f"{normalized_rule}/"):
|
|
174
|
+
return True
|
|
151
175
|
return False
|
|
152
176
|
|
|
153
177
|
|
|
154
178
|
def resolve_local_python_module(module_name: str) -> str:
|
|
155
179
|
relative = normalize_path_text(module_name.replace(".", "/") + ".py")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
package_candidates = [
|
|
181
|
+
f"{package}/src/{relative}"
|
|
182
|
+
for package in ("codecgcmcp", "codexmcp", "geminimcp")
|
|
183
|
+
if module_name == package or module_name.startswith(f"{package}.")
|
|
184
|
+
]
|
|
185
|
+
candidates = [
|
|
186
|
+
*package_candidates,
|
|
187
|
+
f"scripts/{relative}",
|
|
188
|
+
f"scripts/{relative.split('/')[-1]}",
|
|
189
|
+
relative,
|
|
190
|
+
]
|
|
191
|
+
for candidate in candidates:
|
|
192
|
+
if (WORKSPACE / candidate).exists():
|
|
193
|
+
return candidate
|
|
194
|
+
package_init = f"scripts/{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
|
|
195
|
+
if (WORKSPACE / package_init).exists():
|
|
196
|
+
return package_init
|
|
197
|
+
for package in ("codecgcmcp", "codexmcp", "geminimcp"):
|
|
198
|
+
if module_name == package or module_name.startswith(f"{package}."):
|
|
199
|
+
package_init = f"{package}/src/{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
|
|
200
|
+
if (WORKSPACE / package_init).exists():
|
|
201
|
+
return package_init
|
|
202
|
+
root_package_init = f"{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
|
|
203
|
+
if (WORKSPACE / root_package_init).exists():
|
|
204
|
+
return root_package_init
|
|
159
205
|
if (WORKSPACE / relative).exists():
|
|
160
206
|
return relative
|
|
161
207
|
return ""
|
|
162
208
|
|
|
163
209
|
|
|
210
|
+
def resolve_relative_python_module(current_path: str, level: int, module_name: str | None) -> str:
|
|
211
|
+
current = Path(normalize_path_text(current_path))
|
|
212
|
+
package_dir = current.parent
|
|
213
|
+
for _ in range(max(level - 1, 0)):
|
|
214
|
+
package_dir = package_dir.parent
|
|
215
|
+
|
|
216
|
+
if module_name:
|
|
217
|
+
candidate = package_dir / normalize_path_text(module_name.replace(".", "/") + ".py")
|
|
218
|
+
if (WORKSPACE / candidate).exists():
|
|
219
|
+
return normalize_path_text(str(candidate))
|
|
220
|
+
|
|
221
|
+
package_init = package_dir / normalize_path_text(module_name.replace(".", "/")) / "__init__.py"
|
|
222
|
+
if (WORKSPACE / package_init).exists():
|
|
223
|
+
return normalize_path_text(str(package_init))
|
|
224
|
+
|
|
225
|
+
init_candidate = package_dir / "__init__.py"
|
|
226
|
+
if (WORKSPACE / init_candidate).exists():
|
|
227
|
+
return normalize_path_text(str(init_candidate))
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
|
|
164
231
|
def parse_local_python_dependencies(relative_path: str) -> tuple[list[str], list[str]]:
|
|
165
232
|
path = WORKSPACE / relative_path
|
|
166
233
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
@@ -174,7 +241,12 @@ def parse_local_python_dependencies(relative_path: str) -> tuple[list[str], list
|
|
|
174
241
|
if resolved:
|
|
175
242
|
imports.append(resolved)
|
|
176
243
|
elif isinstance(node, ast.ImportFrom):
|
|
177
|
-
if node.level != 0
|
|
244
|
+
if node.level != 0:
|
|
245
|
+
resolved_relative = resolve_relative_python_module(relative_path, node.level, node.module)
|
|
246
|
+
if resolved_relative:
|
|
247
|
+
imports.append(resolved_relative)
|
|
248
|
+
continue
|
|
249
|
+
if not node.module:
|
|
178
250
|
continue
|
|
179
251
|
resolved = resolve_local_python_module(node.module)
|
|
180
252
|
if resolved:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
@@ -8,14 +10,25 @@ from audit_codecgc_package_runtime import audit_package_runtime
|
|
|
8
10
|
from codecgc_console_io import render_summary_block
|
|
9
11
|
from install_codecgc import collect_doctor_status
|
|
10
12
|
from install_codecgc import collect_install_status
|
|
13
|
+
from install_codecgc import install_local_runtime
|
|
11
14
|
|
|
12
15
|
WORKSPACE = Path(__file__).resolve().parents[1]
|
|
16
|
+
RELEASE_PROBE_ROOT_ENV = "CODECGC_RELEASE_PROBE_ROOT"
|
|
13
17
|
|
|
14
18
|
LIFECYCLE_REQUIRED_PATHS = [
|
|
19
|
+
"codecgc/reference/README.md",
|
|
15
20
|
"codecgc/reference/lifecycle-map.md",
|
|
16
21
|
"codecgc/reference/lifecycle-playbook.md",
|
|
22
|
+
"codecgc/reference/maintainer-guide.md",
|
|
23
|
+
"codecgc/reference/mcp-tool-surface.md",
|
|
17
24
|
"codecgc/reference/operation-guide.md",
|
|
25
|
+
"codecgc/reference/onboarding.md",
|
|
26
|
+
"codecgc/reference/path-contract.md",
|
|
27
|
+
"codecgc/reference/quickstart.md",
|
|
28
|
+
"codecgc/reference/recovery-loop.md",
|
|
29
|
+
"codecgc/reference/real-workflow-loop.md",
|
|
18
30
|
"codecgc/reference/release-maintenance-playbook.md",
|
|
31
|
+
"codecgc/reference/troubleshooting.md",
|
|
19
32
|
"codecgc/reference/external-capability-registry.json",
|
|
20
33
|
"codecgc/compound/codecgc-capability-matrix.md",
|
|
21
34
|
]
|
|
@@ -83,9 +96,44 @@ def collect_deploy_signals() -> dict[str, Any]:
|
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
|
|
99
|
+
def collect_install_probe(workspace_override: str = "") -> dict[str, Any]:
|
|
100
|
+
if str(workspace_override or "").strip():
|
|
101
|
+
install_status = collect_install_status(workspace_override)
|
|
102
|
+
doctor_status = collect_doctor_status(workspace_override)
|
|
103
|
+
return {
|
|
104
|
+
"mode": "target-workspace",
|
|
105
|
+
"workspace": str(install_status.get("workspace", "")),
|
|
106
|
+
"install_result": {},
|
|
107
|
+
"install_status": install_status,
|
|
108
|
+
"doctor_status": doctor_status,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
probe_root_value = os.environ.get(RELEASE_PROBE_ROOT_ENV, "").strip()
|
|
112
|
+
probe_root = Path(probe_root_value).expanduser().resolve() if probe_root_value else None
|
|
113
|
+
if probe_root is not None:
|
|
114
|
+
probe_root.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
with tempfile.TemporaryDirectory(
|
|
117
|
+
prefix="codecgc-release-check-",
|
|
118
|
+
dir=str(probe_root) if probe_root is not None else None,
|
|
119
|
+
ignore_cleanup_errors=True,
|
|
120
|
+
) as temp_dir:
|
|
121
|
+
install_result = install_local_runtime(temp_dir)
|
|
122
|
+
install_status = collect_install_status(temp_dir)
|
|
123
|
+
doctor_status = collect_doctor_status(temp_dir)
|
|
124
|
+
return {
|
|
125
|
+
"mode": "temporary-project-install",
|
|
126
|
+
"workspace": temp_dir,
|
|
127
|
+
"install_result": install_result,
|
|
128
|
+
"install_status": install_status,
|
|
129
|
+
"doctor_status": doctor_status,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
86
133
|
def audit_release_readiness(workspace_override: str = "") -> dict[str, Any]:
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
install_probe = collect_install_probe(workspace_override)
|
|
135
|
+
install_status = install_probe["install_status"]
|
|
136
|
+
doctor_status = install_probe["doctor_status"]
|
|
89
137
|
package_audit = audit_package_runtime()
|
|
90
138
|
external_audit = audit_external_capabilities(workspace_override)
|
|
91
139
|
lifecycle_docs = audit_document_set(LIFECYCLE_REQUIRED_PATHS)
|
|
@@ -119,12 +167,14 @@ def audit_release_readiness(workspace_override: str = "") -> dict[str, Any]:
|
|
|
119
167
|
return {
|
|
120
168
|
"success": ready,
|
|
121
169
|
"mode": "release-readiness-audit",
|
|
122
|
-
"workspace": str(
|
|
170
|
+
"workspace": str(WORKSPACE),
|
|
123
171
|
"summary": {
|
|
124
172
|
"ready": ready,
|
|
125
173
|
"scope": "release / maintenance / ops 就绪状态",
|
|
126
174
|
"human_summary": human_summary,
|
|
127
175
|
"recommended_next_action": recommended_next_action,
|
|
176
|
+
"install_probe_mode": install_probe["mode"],
|
|
177
|
+
"install_probe_workspace": install_probe["workspace"],
|
|
128
178
|
"install_ready": install_ready,
|
|
129
179
|
"doctor_ready": doctor_ready,
|
|
130
180
|
"package_ready": package_ready,
|
|
@@ -134,6 +184,7 @@ def audit_release_readiness(workspace_override: str = "") -> dict[str, Any]:
|
|
|
134
184
|
"deploy_signals_detected": deploy_signals["deploy_signals_detected"],
|
|
135
185
|
"deploy_readiness_stage": deploy_signals["deploy_readiness_stage"],
|
|
136
186
|
},
|
|
187
|
+
"install_probe": install_probe,
|
|
137
188
|
"install_status": install_status,
|
|
138
189
|
"doctor_status": doctor_status,
|
|
139
190
|
"package_audit": package_audit,
|
|
@@ -152,6 +203,7 @@ def build_summary(result: dict[str, Any]) -> str:
|
|
|
152
203
|
lines = [
|
|
153
204
|
f"- 工作区: {result.get('workspace', '')}",
|
|
154
205
|
f"- 范围: {summary.get('scope', '')}",
|
|
206
|
+
f"- 安装探针: {summary.get('install_probe_mode', '')} ({summary.get('install_probe_workspace', '')})",
|
|
155
207
|
f"- 就绪: {'是' if summary.get('ready') else '否'}",
|
|
156
208
|
f"- 摘要: {summary.get('human_summary', '')}",
|
|
157
209
|
f"- 项目级集成: {'就绪' if summary.get('install_ready') else '未就绪'}",
|
|
@@ -9,6 +9,8 @@ from typing import Any
|
|
|
9
9
|
from codecgc_artifact_roots import FIXTURE_ROOT
|
|
10
10
|
from codecgc_artifact_roots import PRODUCT_ROOT
|
|
11
11
|
from codecgc_console_io import render_summary_block
|
|
12
|
+
from codecgc_path_contract import normalize_persisted_project_path
|
|
13
|
+
from codecgc_path_contract import resolve_project_path
|
|
12
14
|
from codecgc_runtime_paths import PROJECT_ROOT
|
|
13
15
|
from route_codecgc_workflow import attach_route_summary
|
|
14
16
|
from route_codecgc_workflow import route_feature
|
|
@@ -84,7 +86,7 @@ def locate_summary_file(flow: str, directory: Path) -> str:
|
|
|
84
86
|
matches = sorted(directory.glob(f"*{suffix}"))
|
|
85
87
|
if not matches:
|
|
86
88
|
return ""
|
|
87
|
-
return
|
|
89
|
+
return normalize_persisted_project_path(matches[0])
|
|
88
90
|
|
|
89
91
|
|
|
90
92
|
def collect_history_record(flow: str, artifact_class: str, directory: Path) -> dict[str, Any]:
|
|
@@ -97,7 +99,8 @@ def collect_history_record(flow: str, artifact_class: str, directory: Path) -> d
|
|
|
97
99
|
audit_path = str(route.get("audit_path", "")).strip()
|
|
98
100
|
audit_timestamp = ""
|
|
99
101
|
if audit_path:
|
|
100
|
-
|
|
102
|
+
resolved_audit_path = resolve_project_path(audit_path)
|
|
103
|
+
audit_time = parse_iso_like_timestamp(resolved_audit_path.stat().st_mtime_ns and datetime.fromtimestamp(resolved_audit_path.stat().st_mtime).isoformat())
|
|
101
104
|
if audit_time:
|
|
102
105
|
audit_timestamp = audit_time.isoformat()
|
|
103
106
|
|
|
@@ -106,7 +109,7 @@ def collect_history_record(flow: str, artifact_class: str, directory: Path) -> d
|
|
|
106
109
|
"artifact_class": artifact_class,
|
|
107
110
|
"slug": slug,
|
|
108
111
|
"created": extract_created_from_slug(slug),
|
|
109
|
-
"directory":
|
|
112
|
+
"directory": normalize_persisted_project_path(directory),
|
|
110
113
|
"workflow_state": workflow_state,
|
|
111
114
|
"state_label": STATE_LABELS.get(workflow_state, workflow_state or "未知"),
|
|
112
115
|
"recommended_command": str(route.get("recommended_command", "")).strip(),
|
|
@@ -116,10 +119,10 @@ def collect_history_record(flow: str, artifact_class: str, directory: Path) -> d
|
|
|
116
119
|
"current_task_id": str(summary.get("current_task_id", "")).strip(),
|
|
117
120
|
"review_decision": str(summary.get("review_decision", "")).strip(),
|
|
118
121
|
"is_closed": bool(summary.get("is_closed")),
|
|
119
|
-
"audit_path": audit_path,
|
|
122
|
+
"audit_path": normalize_persisted_project_path(audit_path) if audit_path else "",
|
|
120
123
|
"audit_timestamp": audit_timestamp,
|
|
121
124
|
"summary_file": locate_summary_file(flow, directory),
|
|
122
|
-
"root":
|
|
125
|
+
"root": normalize_persisted_project_path(artifact_root(artifact_class)),
|
|
123
126
|
}
|
|
124
127
|
if current_step:
|
|
125
128
|
record["current_step"] = {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
|
-
import yaml
|
|
5
|
-
from fnmatch import fnmatch
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Any
|
|
8
6
|
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
9
|
from codecgc_console_io import configure_utf8_stdio
|
|
10
10
|
from codecgc_console_io import print_json
|
|
11
|
+
from codecgc_path_contract import normalize_persisted_project_path
|
|
12
|
+
from codecgc_policy import classify_path
|
|
13
|
+
from codecgc_policy import load_policy
|
|
14
|
+
from codecgc_policy import validate_executor_target
|
|
11
15
|
from codecgc_routing_paths import resolve_active_routing_file
|
|
12
16
|
from codecgc_runtime_paths import PACKAGE_ROOT
|
|
13
17
|
from codecgc_runtime_paths import PROJECT_ROOT
|
|
@@ -26,19 +30,7 @@ def normalize_path_text(path_text: str) -> str:
|
|
|
26
30
|
|
|
27
31
|
def load_simple_routing_config(path: Path) -> dict[str, Any]:
|
|
28
32
|
"""Load routing configuration YAML file using standard yaml library."""
|
|
29
|
-
|
|
30
|
-
raise FileNotFoundError(f"Routing file not found: {path}")
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
34
|
-
data = yaml.safe_load(f)
|
|
35
|
-
|
|
36
|
-
if not isinstance(data, dict):
|
|
37
|
-
raise ValueError(f"Routing config must be a dictionary, got {type(data).__name__}")
|
|
38
|
-
|
|
39
|
-
return data
|
|
40
|
-
except yaml.YAMLError as e:
|
|
41
|
-
raise ValueError(f"Failed to parse routing YAML file {path}: {e}") from e
|
|
33
|
+
return load_policy(path)
|
|
42
34
|
|
|
43
35
|
|
|
44
36
|
def load_checklist_yaml(path: Path) -> dict[str, Any]:
|
|
@@ -61,9 +53,40 @@ def load_checklist_yaml(path: Path) -> dict[str, Any]:
|
|
|
61
53
|
def normalize_string_list(value: Any) -> list[str]:
|
|
62
54
|
if value is None:
|
|
63
55
|
return []
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
64
58
|
if isinstance(value, list):
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
normalized: list[str] = []
|
|
60
|
+
for item in value:
|
|
61
|
+
if item is None:
|
|
62
|
+
continue
|
|
63
|
+
text = str(item).strip()
|
|
64
|
+
if text:
|
|
65
|
+
normalized.append(text)
|
|
66
|
+
return normalized
|
|
67
|
+
text = str(value).strip()
|
|
68
|
+
return [text] if text else []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_executable_codecgc_block(block: Any) -> bool:
|
|
72
|
+
"""Compatibility helper for older tests and scripts.
|
|
73
|
+
|
|
74
|
+
Runtime step selection uses codecgc_step_control.is_executable_codecgc_block,
|
|
75
|
+
which is intentionally stricter. This helper preserves the historical task
|
|
76
|
+
builder contract: explicit frontend/backend kind plus at least one target.
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(block, dict):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
kind = str(block.get("kind", "")).strip().lower()
|
|
82
|
+
if not kind or kind == "auto":
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
target_paths = block.get("target_paths", [])
|
|
86
|
+
if not isinstance(target_paths, list):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
return any(str(path).strip() for path in target_paths)
|
|
67
90
|
|
|
68
91
|
|
|
69
92
|
def resolve_optional_value(cli_value: Any, spec_value: Any, default_value: Any) -> Any:
|
|
@@ -140,7 +163,7 @@ def load_checklist_step_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
140
163
|
"constraint": constraints,
|
|
141
164
|
"acceptance": acceptance,
|
|
142
165
|
"cd": resolve_cd_value(str(resolve_optional_value(args.cd, step_spec.get("cd"), None))),
|
|
143
|
-
"routing_file":
|
|
166
|
+
"routing_file": normalize_persisted_project_path(args.routing_file),
|
|
144
167
|
"session_id": str(resolve_optional_value(args.session_id, step_spec.get("session_id"), "")),
|
|
145
168
|
"model": str(resolve_optional_value(args.model, step_spec.get("model"), "")),
|
|
146
169
|
"profile": str(resolve_optional_value(args.profile, step_spec.get("profile"), "")),
|
|
@@ -160,7 +183,7 @@ def load_checklist_step_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
160
183
|
"timeout_seconds": int(step_spec.get("timeout_seconds", 0)) or 0,
|
|
161
184
|
"source": {
|
|
162
185
|
"type": "workflow-step",
|
|
163
|
-
"artifact_file":
|
|
186
|
+
"artifact_file": normalize_persisted_project_path(checklist_path),
|
|
164
187
|
"artifact_type": artifact_type,
|
|
165
188
|
"artifact_class": artifact_class,
|
|
166
189
|
"artifact_slug": artifact_slug,
|
|
@@ -179,37 +202,18 @@ def load_explicit_task_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
179
202
|
"constraint": args.constraint,
|
|
180
203
|
"acceptance": args.acceptance,
|
|
181
204
|
"cd": resolve_cd_value(args.cd),
|
|
182
|
-
"routing_file":
|
|
205
|
+
"routing_file": normalize_persisted_project_path(args.routing_file),
|
|
183
206
|
"session_id": args.session_id or "",
|
|
184
207
|
"model": args.model or "",
|
|
185
208
|
"profile": args.profile or "",
|
|
186
209
|
"codex_sandbox": args.codex_sandbox or "workspace-write",
|
|
187
210
|
"gemini_sandbox": bool(args.gemini_sandbox),
|
|
188
211
|
"return_all_messages": bool(args.return_all_messages),
|
|
212
|
+
"timeout_seconds": int(getattr(args, "timeout_seconds", 0) or 0),
|
|
189
213
|
"source": None,
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
|
|
193
|
-
def classify_path(path_text: str, routing: dict[str, Any]) -> str:
|
|
194
|
-
normalized = normalize_path_text(path_text)
|
|
195
|
-
|
|
196
|
-
for pattern in routing.get("shared_paths", []):
|
|
197
|
-
if fnmatch(normalized, pattern):
|
|
198
|
-
return "shared"
|
|
199
|
-
|
|
200
|
-
frontend_patterns = list(routing.get("frontend_paths", [])) + list(routing.get("custom_frontend_paths", []))
|
|
201
|
-
for pattern in frontend_patterns:
|
|
202
|
-
if fnmatch(normalized, pattern):
|
|
203
|
-
return "frontend"
|
|
204
|
-
|
|
205
|
-
backend_patterns = list(routing.get("backend_paths", [])) + list(routing.get("custom_backend_paths", []))
|
|
206
|
-
for pattern in backend_patterns:
|
|
207
|
-
if fnmatch(normalized, pattern):
|
|
208
|
-
return "backend"
|
|
209
|
-
|
|
210
|
-
return "unknown"
|
|
211
|
-
|
|
212
|
-
|
|
213
217
|
def classify_paths(paths: list[str], routing: dict[str, Any]) -> dict[str, str]:
|
|
214
218
|
return {path: classify_path(path, routing) for path in paths}
|
|
215
219
|
|
|
@@ -218,6 +222,10 @@ def split_paths_by_category(classifications: dict[str, str]) -> dict[str, list[s
|
|
|
218
222
|
grouped = {
|
|
219
223
|
"frontend": [],
|
|
220
224
|
"backend": [],
|
|
225
|
+
"frontend-test": [],
|
|
226
|
+
"backend-test": [],
|
|
227
|
+
"docs": [],
|
|
228
|
+
"orchestration": [],
|
|
221
229
|
"shared": [],
|
|
222
230
|
"unknown": [],
|
|
223
231
|
}
|
|
@@ -275,13 +283,15 @@ def detect_target_kind(paths: list[str], routing: dict[str, Any]) -> tuple[str,
|
|
|
275
283
|
classifications = classify_paths(paths, routing)
|
|
276
284
|
categories = set(classifications.values())
|
|
277
285
|
|
|
278
|
-
if "shared"
|
|
286
|
+
if categories & {"shared", "docs", "orchestration"}:
|
|
279
287
|
details = [
|
|
280
|
-
f"{path} ->
|
|
288
|
+
f"{path} -> {category}"
|
|
289
|
+
for path, category in classifications.items()
|
|
290
|
+
if category in {"shared", "docs", "orchestration"}
|
|
281
291
|
]
|
|
282
292
|
split_payload = build_split_required_payload(paths, routing)
|
|
283
293
|
raise ValueError(
|
|
284
|
-
"Detected
|
|
294
|
+
"Detected non-executor-owned paths. Route them through Claude workflow first.\n"
|
|
285
295
|
+ "\n".join(details)
|
|
286
296
|
+ "\nSPLIT_PAYLOAD: "
|
|
287
297
|
+ json.dumps(split_payload, ensure_ascii=False)
|
|
@@ -295,10 +305,10 @@ def detect_target_kind(paths: list[str], routing: dict[str, Any]) -> tuple[str,
|
|
|
295
305
|
"Some target paths are not covered by model-routing.yaml.\n" + "\n".join(details)
|
|
296
306
|
)
|
|
297
307
|
|
|
298
|
-
if categories
|
|
308
|
+
if categories <= {"frontend", "frontend-test"}:
|
|
299
309
|
return "frontend", [f"{path} -> frontend" for path in paths]
|
|
300
310
|
|
|
301
|
-
if categories
|
|
311
|
+
if categories <= {"backend", "backend-test"}:
|
|
302
312
|
return "backend", [f"{path} -> backend" for path in paths]
|
|
303
313
|
|
|
304
314
|
details = [f"{path} -> {classifications[path]}" for path in paths]
|
|
@@ -329,6 +339,17 @@ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[s
|
|
|
329
339
|
kind = requested_kind
|
|
330
340
|
route_notes = [f"{path} -> forced:{kind}" for path in target_paths]
|
|
331
341
|
|
|
342
|
+
policy_result = validate_executor_target(kind, target_paths, routing)
|
|
343
|
+
if not policy_result.get("allowed"):
|
|
344
|
+
raise ValueError(
|
|
345
|
+
"Target paths violate CodeCGC routing policy.\n"
|
|
346
|
+
+ json.dumps(policy_result, ensure_ascii=False)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
effective_timeout_seconds = int(payload_inputs.get("timeout_seconds", 0)) or int(
|
|
350
|
+
getattr(args, "timeout_seconds", 0) or 0
|
|
351
|
+
)
|
|
352
|
+
|
|
332
353
|
if kind == "frontend":
|
|
333
354
|
tool_name = "implement_frontend_task"
|
|
334
355
|
tool_args: dict[str, Any] = {
|
|
@@ -342,6 +363,7 @@ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[s
|
|
|
342
363
|
"sandbox": payload_inputs["gemini_sandbox"],
|
|
343
364
|
"return_all_messages": payload_inputs["return_all_messages"],
|
|
344
365
|
"model": payload_inputs["model"],
|
|
366
|
+
"timeout_seconds": effective_timeout_seconds,
|
|
345
367
|
}
|
|
346
368
|
elif kind == "backend":
|
|
347
369
|
tool_name = "implement_backend_task"
|
|
@@ -366,7 +388,9 @@ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[s
|
|
|
366
388
|
"target": kind,
|
|
367
389
|
"tool_name": tool_name,
|
|
368
390
|
"tool_args": tool_args,
|
|
391
|
+
"timeout_seconds": effective_timeout_seconds,
|
|
369
392
|
"route_notes": route_notes,
|
|
393
|
+
"policy": policy_result,
|
|
370
394
|
"routing_file": payload_inputs["routing_file"],
|
|
371
395
|
}
|
|
372
396
|
if payload_inputs["source"] is not None:
|
|
@@ -1,40 +1,8 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def normalize_artifact_class(value: str | None) -> str:
|
|
12
|
-
cleaned = str(value or "product").strip().lower()
|
|
13
|
-
return "fixture" if cleaned == "fixture" else "product"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def flow_root(flow: str, artifact_class: str) -> Path:
|
|
17
|
-
base = FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
|
|
18
|
-
return base / ("features" if flow == "feature" else "issues")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def execution_root(artifact_class: str) -> Path:
|
|
22
|
-
base = FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
|
|
23
|
-
return base / "execution"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def discover_flow_directory(flow: str, slug: str, artifact_class: str = "auto") -> tuple[str, Path] | None:
|
|
27
|
-
classes = (
|
|
28
|
-
[normalize_artifact_class(artifact_class)]
|
|
29
|
-
if artifact_class in {"product", "fixture"}
|
|
30
|
-
else ["product", "fixture"]
|
|
31
|
-
)
|
|
32
|
-
for candidate_class in classes:
|
|
33
|
-
directory = flow_root(flow, candidate_class) / slug
|
|
34
|
-
if directory.exists():
|
|
35
|
-
return candidate_class, directory
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def artifact_output_root(artifact_class: str) -> Path:
|
|
40
|
-
return FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
|
|
1
|
+
from codecgc_runtime.artifacts import CODECGC_ROOT
|
|
2
|
+
from codecgc_runtime.artifacts import FIXTURE_ROOT
|
|
3
|
+
from codecgc_runtime.artifacts import PRODUCT_ROOT
|
|
4
|
+
from codecgc_runtime.artifacts import artifact_output_root
|
|
5
|
+
from codecgc_runtime.artifacts import discover_flow_directory
|
|
6
|
+
from codecgc_runtime.artifacts import execution_root
|
|
7
|
+
from codecgc_runtime.artifacts import flow_root
|
|
8
|
+
from codecgc_runtime.artifacts import normalize_artifact_class
|
|
@@ -1,45 +1,3 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import sys
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def configure_utf8_stdio() -> None:
|
|
9
|
-
for stream_name in ("stdout", "stderr"):
|
|
10
|
-
stream = getattr(sys, stream_name, None)
|
|
11
|
-
if stream is None:
|
|
12
|
-
continue
|
|
13
|
-
reconfigure = getattr(stream, "reconfigure", None)
|
|
14
|
-
if callable(reconfigure):
|
|
15
|
-
try:
|
|
16
|
-
reconfigure(encoding="utf-8", errors="replace")
|
|
17
|
-
except Exception:
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def print_json(payload: dict[str, Any], *, file: Any | None = None) -> None:
|
|
22
|
-
target = file or sys.stdout
|
|
23
|
-
text = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
24
|
-
target.write(text)
|
|
25
|
-
target.write("\n")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def render_summary_block(title: str, base_lines: list[str], next_actions: list[str]) -> str:
|
|
29
|
-
lines = [title, *base_lines]
|
|
30
|
-
unique_actions: list[str] = []
|
|
31
|
-
seen: set[str] = set()
|
|
32
|
-
for item in next_actions:
|
|
33
|
-
normalized = str(item).strip()
|
|
34
|
-
if not normalized or normalized in seen:
|
|
35
|
-
continue
|
|
36
|
-
seen.add(normalized)
|
|
37
|
-
unique_actions.append(normalized)
|
|
38
|
-
|
|
39
|
-
if unique_actions:
|
|
40
|
-
lines.append(f"- 下一步: {unique_actions[0]}")
|
|
41
|
-
for item in unique_actions[1:]:
|
|
42
|
-
lines.append(f"- 备选动作: {item}")
|
|
43
|
-
else:
|
|
44
|
-
lines.append("- 下一步: 无")
|
|
45
|
-
return "\n".join(lines)
|
|
1
|
+
from codecgc_runtime.console import configure_utf8_stdio
|
|
2
|
+
from codecgc_runtime.console import print_json
|
|
3
|
+
from codecgc_runtime.console import render_summary_block
|
|
@@ -1,54 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
WORKSPACE = Path(__file__).resolve().parents[1]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def resolve_python_command() -> str:
|
|
13
|
-
override = os.environ.get("CODECGC_PYTHON_COMMAND", "").strip()
|
|
14
|
-
if override:
|
|
15
|
-
return override
|
|
16
|
-
return sys.executable
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def build_executor_registry() -> dict[str, dict[str, Any]]:
|
|
20
|
-
python_command = resolve_python_command()
|
|
21
|
-
return {
|
|
22
|
-
"backend": {
|
|
23
|
-
"target": "backend",
|
|
24
|
-
"mcp_server_name": "codex",
|
|
25
|
-
"routing_executor": "codexmcp",
|
|
26
|
-
"tool_name": "implement_backend_task",
|
|
27
|
-
"python_module": "codexmcp.cli",
|
|
28
|
-
"pythonpath": str(WORKSPACE / "codexmcp" / "src"),
|
|
29
|
-
"python_command": python_command,
|
|
30
|
-
},
|
|
31
|
-
"frontend": {
|
|
32
|
-
"target": "frontend",
|
|
33
|
-
"mcp_server_name": "gemini",
|
|
34
|
-
"routing_executor": "geminimcp",
|
|
35
|
-
"tool_name": "implement_frontend_task",
|
|
36
|
-
"python_module": "geminimcp.cli",
|
|
37
|
-
"pythonpath": str(WORKSPACE / "geminimcp" / "src"),
|
|
38
|
-
"python_command": python_command,
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_executor_config(target: str) -> dict[str, Any]:
|
|
44
|
-
registry = build_executor_registry()
|
|
45
|
-
if target not in registry:
|
|
46
|
-
raise ValueError(f"Unsupported target: {target}")
|
|
47
|
-
return registry[target]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def build_executor_env(target: str) -> dict[str, str]:
|
|
51
|
-
env = dict(os.environ)
|
|
52
|
-
config = get_executor_config(target)
|
|
53
|
-
env["PYTHONPATH"] = str(config["pythonpath"])
|
|
54
|
-
return env
|
|
1
|
+
from codecgc_runtime.executor_registry import build_executor_env
|
|
2
|
+
from codecgc_runtime.executor_registry import build_executor_registry
|
|
3
|
+
from codecgc_runtime.executor_registry import get_executor_config
|
|
4
|
+
from codecgc_runtime.executor_registry import resolve_python_command
|