@hunyed15/codecgc 0.1.7 → 0.1.8

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 (65) hide show
  1. package/.claude/hooks/route-edit.ps1 +58 -57
  2. package/INSTALLATION.md +117 -484
  3. package/README.md +118 -150
  4. package/bin/cgc-external-status.js +4 -0
  5. package/bin/cgc-start.js +4 -0
  6. package/bin/codecgc.js +141 -20
  7. package/codecgc/compound/codecgc-capability-matrix.md +37 -0
  8. package/codecgc/reference/README.md +69 -0
  9. package/codecgc/reference/maintainer-guide.md +93 -0
  10. package/codecgc/reference/mcp-tool-surface.md +112 -0
  11. package/codecgc/reference/onboarding.md +69 -0
  12. package/codecgc/reference/operation-guide.md +29 -23
  13. package/codecgc/reference/path-contract.md +53 -0
  14. package/codecgc/reference/policy-routing.md +57 -0
  15. package/codecgc/reference/project-structure.md +80 -0
  16. package/codecgc/reference/quickstart.md +108 -0
  17. package/codecgc/reference/real-workflow-loop.md +123 -0
  18. package/codecgc/reference/recovery-loop.md +109 -0
  19. package/codecgc/reference/release-maintenance-playbook.md +4 -1
  20. package/codecgc/reference/troubleshooting.md +112 -0
  21. package/codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md +49 -0
  22. package/codecgc/roadmap/codecgc-release-maintenance/overview.md +41 -0
  23. package/codecgc/roadmap/codecgc-release-maintenance/phases.md +84 -0
  24. package/codecgcmcp/README.md +57 -11
  25. package/codecgcmcp/src/codecgcmcp/server.py +164 -26
  26. package/model-routing.yaml +31 -6
  27. package/package.json +11 -4
  28. package/scripts/audit_codecgc_external_capabilities.py +83 -4
  29. package/scripts/audit_codecgc_historical_audits.py +42 -2
  30. package/scripts/audit_codecgc_package_runtime.py +73 -4
  31. package/scripts/audit_codecgc_release_readiness.py +55 -3
  32. package/scripts/audit_codecgc_workflow_history.py +8 -5
  33. package/scripts/build_codecgc_task.py +62 -45
  34. package/scripts/codecgc_artifact_roots.py +8 -40
  35. package/scripts/codecgc_console_io.py +3 -45
  36. package/scripts/codecgc_executor_registry.py +4 -54
  37. package/scripts/codecgc_path_contract.py +7 -0
  38. package/scripts/codecgc_policy.py +275 -0
  39. package/scripts/codecgc_routing_paths.py +3 -16
  40. package/scripts/codecgc_routing_template.py +11 -135
  41. package/scripts/codecgc_runtime/__init__.py +1 -0
  42. package/scripts/codecgc_runtime/artifacts.py +42 -0
  43. package/scripts/codecgc_runtime/console.py +45 -0
  44. package/scripts/codecgc_runtime/executor_registry.py +55 -0
  45. package/scripts/codecgc_runtime/mcp_config.py +72 -0
  46. package/scripts/codecgc_runtime/path_contract.py +123 -0
  47. package/scripts/codecgc_runtime/paths.py +22 -0
  48. package/scripts/codecgc_runtime/routing_paths.py +16 -0
  49. package/scripts/codecgc_runtime/routing_template.py +169 -0
  50. package/scripts/codecgc_runtime/workflow_runtime.py +72 -0
  51. package/scripts/codecgc_runtime_paths.py +3 -22
  52. package/scripts/codecgc_step_control.py +3 -2
  53. package/scripts/codecgc_workflow_runtime.py +3 -71
  54. package/scripts/entry_codecgc_workflow.py +4 -0
  55. package/scripts/install_codecgc.py +490 -21
  56. package/scripts/normalize_codecgc_audits.py +5 -3
  57. package/scripts/postinstall_codecgc.js +6 -56
  58. package/scripts/review_codecgc_workflow.py +6 -3
  59. package/scripts/route_codecgc_workflow.py +67 -36
  60. package/scripts/run_codecgc_build.py +28 -0
  61. package/scripts/run_codecgc_fix.py +28 -0
  62. package/scripts/run_codecgc_task.py +5 -2
  63. package/scripts/run_codecgc_test.py +28 -0
  64. package/scripts/sync_codecgc_mcp_config.py +4 -54
  65. package/scripts/write_codecgc_review.py +7 -3
@@ -6,6 +6,8 @@ from typing import Any
6
6
  from codecgc_artifact_roots import discover_flow_directory
7
7
  from codecgc_artifact_roots import execution_root
8
8
  from codecgc_artifact_roots import normalize_artifact_class
9
+ from codecgc_path_contract import is_project_relative_path
10
+ from codecgc_path_contract import resolve_project_path
9
11
 
10
12
 
11
13
  WORKSPACE = Path(__file__).resolve().parents[1]
@@ -33,6 +35,19 @@ def contains_old_repo_name(value: Any) -> bool:
33
35
  return isinstance(value, str) and OLD_REPO_NAME in value
34
36
 
35
37
 
38
+ def contains_persisted_absolute_project_path(value: Any) -> bool:
39
+ if not isinstance(value, str) or not value.strip():
40
+ return False
41
+ if is_project_relative_path(value):
42
+ return False
43
+ resolved = resolve_project_path(value)
44
+ try:
45
+ resolved.relative_to(WORKSPACE.resolve())
46
+ return True
47
+ except ValueError:
48
+ return False
49
+
50
+
36
51
  def expected_artifact_filename(artifact_type: str, artifact_slug: str) -> str:
37
52
  base_slug = artifact_slug[11:] if len(artifact_slug) > 11 and artifact_slug[4] == "-" else artifact_slug
38
53
  if artifact_type == "feature":
@@ -97,7 +112,7 @@ def validate_source_contract(source: dict[str, Any]) -> list[dict[str, str]]:
97
112
  )
98
113
 
99
114
  if artifact_file:
100
- artifact_file_path = Path(artifact_file)
115
+ artifact_file_path = resolve_project_path(artifact_file)
101
116
  if not artifact_file_path.exists():
102
117
  issues.append(
103
118
  {
@@ -105,7 +120,7 @@ def validate_source_contract(source: dict[str, Any]) -> list[dict[str, str]]:
105
120
  "detail": artifact_file,
106
121
  }
107
122
  )
108
- elif directory not in artifact_file_path.parents:
123
+ elif directory.resolve() not in artifact_file_path.resolve().parents:
109
124
  issues.append(
110
125
  {
111
126
  "problem": "source-artifact-file-directory-mismatch",
@@ -174,6 +189,31 @@ def inspect_audit(path: Path) -> list[dict[str, str]]:
174
189
  }
175
190
  )
176
191
 
192
+ if contains_persisted_absolute_project_path(data.get("routing_file")):
193
+ issues.append(
194
+ {
195
+ "path": str(path),
196
+ "problem": "absolute-project-routing-file",
197
+ "detail": str(data.get("routing_file", "")),
198
+ }
199
+ )
200
+ if contains_persisted_absolute_project_path(data.get("cd")):
201
+ issues.append(
202
+ {
203
+ "path": str(path),
204
+ "problem": "absolute-project-cd",
205
+ "detail": str(data.get("cd", "")),
206
+ }
207
+ )
208
+ if contains_persisted_absolute_project_path(source.get("artifact_file")):
209
+ issues.append(
210
+ {
211
+ "path": str(path),
212
+ "problem": "absolute-project-artifact-file",
213
+ "detail": str(source.get("artifact_file", "")),
214
+ }
215
+ )
216
+
177
217
  for item in validate_source_contract(source):
178
218
  issues.append(
179
219
  {
@@ -13,14 +13,18 @@ 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",
22
25
  "model-routing.yaml",
23
26
  "requirements.txt",
27
+ "scripts/codecgc_runtime/__init__.py",
24
28
  "scripts/audit_codecgc_external_capabilities.py",
25
29
  "scripts/audit_codecgc_lifecycle.py",
26
30
  "codexmcp/pyproject.toml",
@@ -45,10 +49,25 @@ DOC_RUNTIME_PATHS = [
45
49
  "codecgc/cgc-onboard/SKILL.md",
46
50
  "codecgc/cgc-plan/SKILL.md",
47
51
  "codecgc/cgc-review/SKILL.md",
52
+ "codecgc/compound/codecgc-capability-matrix.md",
53
+ "codecgc/reference/README.md",
48
54
  "codecgc/reference/external-capability-registry.json",
49
55
  "codecgc/reference/lifecycle-playbook.md",
56
+ "codecgc/reference/maintainer-guide.md",
57
+ "codecgc/reference/mcp-tool-surface.md",
50
58
  "codecgc/reference/operation-guide.md",
59
+ "codecgc/reference/path-contract.md",
60
+ "codecgc/reference/policy-routing.md",
61
+ "codecgc/reference/project-structure.md",
62
+ "codecgc/reference/quickstart.md",
63
+ "codecgc/reference/onboarding.md",
64
+ "codecgc/reference/recovery-loop.md",
65
+ "codecgc/reference/real-workflow-loop.md",
51
66
  "codecgc/reference/release-maintenance-playbook.md",
67
+ "codecgc/reference/troubleshooting.md",
68
+ "codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md",
69
+ "codecgc/roadmap/codecgc-release-maintenance/overview.md",
70
+ "codecgc/roadmap/codecgc-release-maintenance/phases.md",
52
71
  ]
53
72
 
54
73
  PLACEHOLDER_METADATA_MARKERS = (
@@ -148,19 +167,64 @@ def path_matches_package_files(path_text: str, file_rules: list[str]) -> bool:
148
167
  continue
149
168
  if normalized == normalized_rule:
150
169
  return True
170
+ if (WORKSPACE / normalized_rule).is_dir() and normalized.startswith(f"{normalized_rule}/"):
171
+ return True
151
172
  return False
152
173
 
153
174
 
154
175
  def resolve_local_python_module(module_name: str) -> str:
155
176
  relative = normalize_path_text(module_name.replace(".", "/") + ".py")
156
- script_candidate = f"scripts/{relative.split('/')[-1]}"
157
- if (WORKSPACE / script_candidate).exists():
158
- return script_candidate
177
+ package_candidates = [
178
+ f"{package}/src/{relative}"
179
+ for package in ("codecgcmcp", "codexmcp", "geminimcp")
180
+ if module_name == package or module_name.startswith(f"{package}.")
181
+ ]
182
+ candidates = [
183
+ *package_candidates,
184
+ f"scripts/{relative}",
185
+ f"scripts/{relative.split('/')[-1]}",
186
+ relative,
187
+ ]
188
+ for candidate in candidates:
189
+ if (WORKSPACE / candidate).exists():
190
+ return candidate
191
+ package_init = f"scripts/{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
192
+ if (WORKSPACE / package_init).exists():
193
+ return package_init
194
+ for package in ("codecgcmcp", "codexmcp", "geminimcp"):
195
+ if module_name == package or module_name.startswith(f"{package}."):
196
+ package_init = f"{package}/src/{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
197
+ if (WORKSPACE / package_init).exists():
198
+ return package_init
199
+ root_package_init = f"{normalize_path_text(module_name.replace('.', '/'))}/__init__.py"
200
+ if (WORKSPACE / root_package_init).exists():
201
+ return root_package_init
159
202
  if (WORKSPACE / relative).exists():
160
203
  return relative
161
204
  return ""
162
205
 
163
206
 
207
+ def resolve_relative_python_module(current_path: str, level: int, module_name: str | None) -> str:
208
+ current = Path(normalize_path_text(current_path))
209
+ package_dir = current.parent
210
+ for _ in range(max(level - 1, 0)):
211
+ package_dir = package_dir.parent
212
+
213
+ if module_name:
214
+ candidate = package_dir / normalize_path_text(module_name.replace(".", "/") + ".py")
215
+ if (WORKSPACE / candidate).exists():
216
+ return normalize_path_text(str(candidate))
217
+
218
+ package_init = package_dir / normalize_path_text(module_name.replace(".", "/")) / "__init__.py"
219
+ if (WORKSPACE / package_init).exists():
220
+ return normalize_path_text(str(package_init))
221
+
222
+ init_candidate = package_dir / "__init__.py"
223
+ if (WORKSPACE / init_candidate).exists():
224
+ return normalize_path_text(str(init_candidate))
225
+ return ""
226
+
227
+
164
228
  def parse_local_python_dependencies(relative_path: str) -> tuple[list[str], list[str]]:
165
229
  path = WORKSPACE / relative_path
166
230
  tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
@@ -174,7 +238,12 @@ def parse_local_python_dependencies(relative_path: str) -> tuple[list[str], list
174
238
  if resolved:
175
239
  imports.append(resolved)
176
240
  elif isinstance(node, ast.ImportFrom):
177
- if node.level != 0 or not node.module:
241
+ if node.level != 0:
242
+ resolved_relative = resolve_relative_python_module(relative_path, node.level, node.module)
243
+ if resolved_relative:
244
+ imports.append(resolved_relative)
245
+ continue
246
+ if not node.module:
178
247
  continue
179
248
  resolved = resolve_local_python_module(node.module)
180
249
  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
- install_status = collect_install_status(workspace_override)
88
- doctor_status = collect_doctor_status(workspace_override)
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(install_status.get("workspace", "")),
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 str(matches[0].resolve())
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
- audit_time = parse_iso_like_timestamp(Path(audit_path).stat().st_mtime_ns and datetime.fromtimestamp(Path(audit_path).stat().st_mtime).isoformat())
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": str(directory.resolve()),
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": str(artifact_root(artifact_class).resolve()),
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
- if not path.exists():
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
- return [str(item) for item in value]
66
- return [str(value)]
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": str(Path(str(args.routing_file)).resolve()),
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": str(checklist_path.resolve()),
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,7 +202,7 @@ 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": str(Path(str(args.routing_file)).resolve()),
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 "",
@@ -190,26 +213,6 @@ def load_explicit_task_payload(args: argparse.Namespace) -> dict[str, Any]:
190
213
  }
191
214
 
192
215
 
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
216
  def classify_paths(paths: list[str], routing: dict[str, Any]) -> dict[str, str]:
214
217
  return {path: classify_path(path, routing) for path in paths}
215
218
 
@@ -218,6 +221,10 @@ def split_paths_by_category(classifications: dict[str, str]) -> dict[str, list[s
218
221
  grouped = {
219
222
  "frontend": [],
220
223
  "backend": [],
224
+ "frontend-test": [],
225
+ "backend-test": [],
226
+ "docs": [],
227
+ "orchestration": [],
221
228
  "shared": [],
222
229
  "unknown": [],
223
230
  }
@@ -275,13 +282,15 @@ def detect_target_kind(paths: list[str], routing: dict[str, Any]) -> tuple[str,
275
282
  classifications = classify_paths(paths, routing)
276
283
  categories = set(classifications.values())
277
284
 
278
- if "shared" in categories:
285
+ if categories & {"shared", "docs", "orchestration"}:
279
286
  details = [
280
- f"{path} -> shared" for path, category in classifications.items() if category == "shared"
287
+ f"{path} -> {category}"
288
+ for path, category in classifications.items()
289
+ if category in {"shared", "docs", "orchestration"}
281
290
  ]
282
291
  split_payload = build_split_required_payload(paths, routing)
283
292
  raise ValueError(
284
- "Detected shared paths. Split the task first.\n"
293
+ "Detected non-executor-owned paths. Route them through Claude workflow first.\n"
285
294
  + "\n".join(details)
286
295
  + "\nSPLIT_PAYLOAD: "
287
296
  + json.dumps(split_payload, ensure_ascii=False)
@@ -295,10 +304,10 @@ def detect_target_kind(paths: list[str], routing: dict[str, Any]) -> tuple[str,
295
304
  "Some target paths are not covered by model-routing.yaml.\n" + "\n".join(details)
296
305
  )
297
306
 
298
- if categories == {"frontend"}:
307
+ if categories <= {"frontend", "frontend-test"}:
299
308
  return "frontend", [f"{path} -> frontend" for path in paths]
300
309
 
301
- if categories == {"backend"}:
310
+ if categories <= {"backend", "backend-test"}:
302
311
  return "backend", [f"{path} -> backend" for path in paths]
303
312
 
304
313
  details = [f"{path} -> {classifications[path]}" for path in paths]
@@ -329,6 +338,13 @@ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[s
329
338
  kind = requested_kind
330
339
  route_notes = [f"{path} -> forced:{kind}" for path in target_paths]
331
340
 
341
+ policy_result = validate_executor_target(kind, target_paths, routing)
342
+ if not policy_result.get("allowed"):
343
+ raise ValueError(
344
+ "Target paths violate CodeCGC routing policy.\n"
345
+ + json.dumps(policy_result, ensure_ascii=False)
346
+ )
347
+
332
348
  if kind == "frontend":
333
349
  tool_name = "implement_frontend_task"
334
350
  tool_args: dict[str, Any] = {
@@ -367,6 +383,7 @@ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[s
367
383
  "tool_name": tool_name,
368
384
  "tool_args": tool_args,
369
385
  "route_notes": route_notes,
386
+ "policy": policy_result,
370
387
  "routing_file": payload_inputs["routing_file"],
371
388
  }
372
389
  if payload_inputs["source"] is not None:
@@ -1,40 +1,8 @@
1
- from __future__ import annotations
2
-
3
- from codecgc_runtime_paths import PROJECT_ROOT
4
-
5
-
6
- CODECGC_ROOT = PROJECT_ROOT / "codecgc"
7
- PRODUCT_ROOT = CODECGC_ROOT
8
- FIXTURE_ROOT = CODECGC_ROOT / "fixtures"
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 __future__ import annotations
2
-
3
- import json
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