@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
@@ -1,54 +1,4 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import sys
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
@@ -0,0 +1,7 @@
1
+ from codecgc_runtime.path_contract import is_project_relative_path
2
+ from codecgc_runtime.path_contract import normalize_audit_path_fields
3
+ from codecgc_runtime.path_contract import normalize_path_text
4
+ from codecgc_runtime.path_contract import normalize_persisted_project_path
5
+ from codecgc_runtime.path_contract import normalize_source_contract
6
+ from codecgc_runtime.path_contract import resolve_project_path
7
+ from codecgc_runtime.path_contract import to_project_relative_path
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from fnmatch import fnmatch
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from codecgc_console_io import configure_utf8_stdio
14
+ from codecgc_console_io import print_json
15
+ from codecgc_path_contract import to_project_relative_path
16
+ from codecgc_routing_paths import resolve_active_routing_file
17
+
18
+
19
+ OWNERS = {"orchestration", "docs", "frontend", "backend", "frontend-test", "backend-test", "shared", "unknown"}
20
+ POLICY_PROJECT_ROOT_KEY = "_codecgc_project_root"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class PathDecision:
25
+ path: str
26
+ owner: str
27
+ allowed: bool
28
+ reason: str
29
+ recommended_action: str
30
+
31
+
32
+ def normalize_path_text(path_text: str) -> str:
33
+ normalized = str(path_text or "").replace("\\", "/").strip()
34
+ while normalized.startswith("./"):
35
+ normalized = normalized[2:]
36
+ return normalized
37
+
38
+
39
+ def _as_string_list(value: Any) -> list[str]:
40
+ if value is None:
41
+ return []
42
+ if isinstance(value, list):
43
+ return [str(item).strip() for item in value if str(item).strip()]
44
+ return [str(value).strip()] if str(value).strip() else []
45
+
46
+
47
+ def _load_yaml(path: Path) -> dict[str, Any]:
48
+ if not path.exists():
49
+ raise FileNotFoundError(f"Routing policy file not found: {path}")
50
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
51
+ if not isinstance(data, dict):
52
+ raise ValueError(f"Routing policy must be a mapping: {path}")
53
+ return data
54
+
55
+
56
+ def load_policy(path: Path | None = None) -> dict[str, Any]:
57
+ policy_path = path or resolve_active_routing_file()
58
+ policy = _load_yaml(policy_path)
59
+ if int(policy.get("version", 0) or 0) != 2:
60
+ raise ValueError("model-routing.yaml must use version: 2.")
61
+ validate_policy(policy)
62
+ policy[POLICY_PROJECT_ROOT_KEY] = str(policy_path.parent.resolve())
63
+ return policy
64
+
65
+
66
+ def validate_policy(policy: dict[str, Any]) -> None:
67
+ required_lists = [
68
+ "orchestration_paths",
69
+ "docs_paths",
70
+ "frontend_paths",
71
+ "backend_paths",
72
+ "shared_paths",
73
+ ]
74
+ missing = [name for name in required_lists if not isinstance(policy.get(name), list)]
75
+ test_paths = policy.get("test_paths")
76
+ rules = policy.get("rules")
77
+ if not isinstance(test_paths, dict):
78
+ missing.append("test_paths")
79
+ if not isinstance(rules, dict):
80
+ missing.append("rules")
81
+ if missing:
82
+ raise ValueError(f"model-routing.yaml is missing required policy sections: {', '.join(missing)}")
83
+
84
+ for name in ("frontend", "backend"):
85
+ if not isinstance(test_paths.get(name), list):
86
+ raise ValueError(f"model-routing.yaml test_paths.{name} must be a list.")
87
+
88
+ allowed = set(_as_string_list(rules.get("claude_allowed_owners")))
89
+ invalid_allowed = sorted(allowed - OWNERS)
90
+ if invalid_allowed:
91
+ raise ValueError(f"Invalid claude_allowed_owners entries: {', '.join(invalid_allowed)}")
92
+
93
+ if str(rules.get("shared_policy", "")).strip() != "split-first":
94
+ raise ValueError("Only shared_policy: split-first is supported.")
95
+
96
+
97
+ def _matches_any(path_text: str, patterns: list[str]) -> bool:
98
+ return any(fnmatch(path_text, pattern) for pattern in patterns)
99
+
100
+
101
+ def normalize_policy_path(path_text: str, policy: dict[str, Any]) -> str:
102
+ project_root = str(policy.get(POLICY_PROJECT_ROOT_KEY, "")).strip()
103
+ if project_root:
104
+ return normalize_path_text(
105
+ to_project_relative_path(path_text, Path(project_root), allow_legacy_suffix=False)
106
+ )
107
+ return normalize_path_text(path_text)
108
+
109
+
110
+ def classify_path(path_text: str, policy: dict[str, Any]) -> str:
111
+ normalized = normalize_policy_path(path_text, policy)
112
+ test_paths = policy.get("test_paths", {})
113
+
114
+ ordered_groups: list[tuple[str, list[str]]] = [
115
+ ("shared", _as_string_list(policy.get("shared_paths"))),
116
+ ("orchestration", _as_string_list(policy.get("orchestration_paths"))),
117
+ ("docs", _as_string_list(policy.get("docs_paths"))),
118
+ ("frontend-test", _as_string_list(test_paths.get("frontend"))),
119
+ ("backend-test", _as_string_list(test_paths.get("backend"))),
120
+ ("frontend", _as_string_list(policy.get("frontend_paths"))),
121
+ ("backend", _as_string_list(policy.get("backend_paths"))),
122
+ ]
123
+ for owner, patterns in ordered_groups:
124
+ if _matches_any(normalized, patterns):
125
+ return owner
126
+ return "unknown"
127
+
128
+
129
+ def allowed_owners_for_actor(actor: str, policy: dict[str, Any]) -> set[str]:
130
+ normalized_actor = str(actor or "").strip().lower()
131
+ rules = policy.get("rules", {})
132
+ if normalized_actor == "claude":
133
+ return set(_as_string_list(rules.get("claude_allowed_owners")))
134
+ if normalized_actor in {"codex", "codexmcp", "backend"}:
135
+ return {"backend", "backend-test"}
136
+ if normalized_actor in {"gemini", "geminimcp", "frontend"}:
137
+ return {"frontend", "frontend-test"}
138
+ return set()
139
+
140
+
141
+ def recommended_action_for(owner: str, actor: str) -> str:
142
+ if owner == "backend":
143
+ return "route through cgc-build or cgc-fix with a backend step"
144
+ if owner == "backend-test":
145
+ return "route through cgc-test with a backend test step"
146
+ if owner == "frontend":
147
+ return "route through cgc-build or cgc-fix with a frontend step"
148
+ if owner == "frontend-test":
149
+ return "route through cgc-test with a frontend test step"
150
+ if owner == "shared":
151
+ return "split the shared change before execution"
152
+ if owner == "unknown":
153
+ return "add the path to model-routing.yaml or narrow the target path"
154
+ return f"request a policy route for actor {actor}"
155
+
156
+
157
+ def decide_path(path_text: str, actor: str, operation: str, policy: dict[str, Any]) -> PathDecision:
158
+ owner = classify_path(path_text, policy)
159
+ allowed_owners = allowed_owners_for_actor(actor, policy)
160
+ normalized_actor = str(actor or "").strip().lower()
161
+ normalized_operation = str(operation or "").strip().lower() or "write"
162
+
163
+ allowed = normalized_operation in {"read", "inspect"} or owner in allowed_owners
164
+ if owner == "unknown":
165
+ allowed = False
166
+ if owner == "shared" and normalized_operation not in {"read", "inspect"}:
167
+ allowed = False
168
+
169
+ if allowed:
170
+ reason = f"{normalized_actor} may {normalized_operation} {owner} paths"
171
+ recommended = ""
172
+ elif owner == "shared":
173
+ reason = "shared paths require split-first routing"
174
+ recommended = recommended_action_for(owner, normalized_actor)
175
+ elif owner == "unknown":
176
+ reason = "path is not covered by model-routing.yaml"
177
+ recommended = recommended_action_for(owner, normalized_actor)
178
+ else:
179
+ reason = f"{owner} paths are not owned by {normalized_actor}"
180
+ recommended = recommended_action_for(owner, normalized_actor)
181
+
182
+ return PathDecision(
183
+ path=normalize_policy_path(path_text, policy),
184
+ owner=owner,
185
+ allowed=allowed,
186
+ reason=reason,
187
+ recommended_action=recommended,
188
+ )
189
+
190
+
191
+ def evaluate_paths(paths: list[str], actor: str, operation: str, policy: dict[str, Any]) -> dict[str, Any]:
192
+ decisions = [decide_path(path, actor, operation, policy) for path in paths]
193
+ return {
194
+ "success": all(decision.allowed for decision in decisions),
195
+ "allowed": all(decision.allowed for decision in decisions),
196
+ "actor": str(actor or "").strip().lower(),
197
+ "operation": str(operation or "").strip().lower() or "write",
198
+ "decisions": [decision.__dict__ for decision in decisions],
199
+ "owners": sorted({decision.owner for decision in decisions}),
200
+ }
201
+
202
+
203
+ def validate_executor_target(kind: str, target_paths: list[str], policy: dict[str, Any]) -> dict[str, Any]:
204
+ actor = "codex" if str(kind).strip().lower() == "backend" else "gemini" if str(kind).strip().lower() == "frontend" else str(kind)
205
+ result = evaluate_paths(target_paths, actor=actor, operation="write", policy=policy)
206
+ result["kind"] = str(kind).strip().lower()
207
+ return result
208
+
209
+
210
+ def parse_hook_payload(text: str) -> tuple[str, str]:
211
+ if not text.strip():
212
+ return "", ""
213
+ payload = json.loads(text)
214
+ if not isinstance(payload, dict):
215
+ return "", ""
216
+ tool_name = str(payload.get("tool_name", "")).strip()
217
+ tool_input = payload.get("tool_input", {})
218
+ if not isinstance(tool_input, dict):
219
+ return tool_name, ""
220
+ file_path = str(tool_input.get("file_path") or tool_input.get("path") or "").strip()
221
+ return tool_name, file_path
222
+
223
+
224
+ def build_hook_response(allowed: bool, reason: str) -> dict[str, Any]:
225
+ if allowed:
226
+ return {"decision": "approve"}
227
+ return {"decision": "deny", "reason": reason}
228
+
229
+
230
+ def build_parser() -> argparse.ArgumentParser:
231
+ parser = argparse.ArgumentParser(description="Evaluate CodeCGC routing policy decisions.")
232
+ parser.add_argument("--routing-file", default="", help="Optional explicit model-routing.yaml path.")
233
+ parser.add_argument("--actor", default="claude", help="Actor requesting the operation.")
234
+ parser.add_argument("--operation", default="write", help="Operation to evaluate.")
235
+ parser.add_argument("--path", action="append", default=[], help="Path to evaluate. Repeatable.")
236
+ parser.add_argument("--hook-check", action="store_true", help="Read Claude hook JSON from stdin and return hook JSON.")
237
+ return parser
238
+
239
+
240
+ def main() -> int:
241
+ configure_utf8_stdio()
242
+ parser = build_parser()
243
+ args = parser.parse_args()
244
+
245
+ try:
246
+ policy_path = Path(args.routing_file).resolve() if args.routing_file else resolve_active_routing_file()
247
+ policy = load_policy(policy_path)
248
+
249
+ if args.hook_check:
250
+ tool_name, file_path = parse_hook_payload(sys.stdin.read())
251
+ if tool_name not in {"Edit", "Write", "MultiEdit"} or not file_path:
252
+ print_json(build_hook_response(True, ""))
253
+ return 0
254
+ result = evaluate_paths([file_path], actor=args.actor, operation=args.operation, policy=policy)
255
+ decision = result["decisions"][0]
256
+ reason = decision.get("reason", "")
257
+ recommended = decision.get("recommended_action", "")
258
+ hook_reason = f"CodeCGC: {reason}."
259
+ if recommended:
260
+ hook_reason += f" {recommended}."
261
+ print_json(build_hook_response(bool(result["allowed"]), hook_reason))
262
+ return 0
263
+
264
+ if not args.path:
265
+ raise ValueError("At least one --path is required unless --hook-check is used.")
266
+
267
+ print_json(evaluate_paths(args.path, actor=args.actor, operation=args.operation, policy=policy))
268
+ return 0
269
+ except Exception as error:
270
+ print_json({"success": False, "allowed": False, "error": str(error)}, file=sys.stderr)
271
+ return 1
272
+
273
+
274
+ if __name__ == "__main__":
275
+ raise SystemExit(main())
@@ -1,16 +1,3 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
- from codecgc_runtime_paths import PACKAGE_ROOT
6
- from codecgc_runtime_paths import PROJECT_ROOT
7
-
8
-
9
- PACKAGE_ROUTING_FILE = PACKAGE_ROOT / "model-routing.yaml"
10
- PROJECT_ROUTING_FILE = PROJECT_ROOT / "model-routing.yaml"
11
-
12
-
13
- def resolve_active_routing_file() -> Path:
14
- if PROJECT_ROUTING_FILE.exists():
15
- return PROJECT_ROUTING_FILE
16
- return PACKAGE_ROUTING_FILE
1
+ from codecgc_runtime.routing_paths import PACKAGE_ROUTING_FILE
2
+ from codecgc_runtime.routing_paths import PROJECT_ROUTING_FILE
3
+ from codecgc_runtime.routing_paths import resolve_active_routing_file
@@ -1,135 +1,11 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
-
6
- DEFAULT_FRONTEND_PATHS = [
7
- "apps/web/**",
8
- "src/components/**",
9
- "src/pages/**",
10
- "src/app/**",
11
- "src/styles/**",
12
- "web/**",
13
- "frontend/**",
14
- ]
15
-
16
- DEFAULT_BACKEND_PATHS = [
17
- "apps/api/**",
18
- "server/**",
19
- "src/server/**",
20
- "src/services/**",
21
- "src/repositories/**",
22
- "backend/**",
23
- ]
24
-
25
- DEFAULT_SHARED_PATHS = [
26
- "packages/shared/**",
27
- "src/shared/**",
28
- "src/lib/**",
29
- "src/types/**",
30
- ]
31
-
32
- DEFAULT_RULES = {
33
- "frontend_executor": "geminimcp",
34
- "backend_executor": "codexmcp",
35
- "shared_policy": "split-first",
36
- "claude_role": "plan-review-accept-only",
37
- }
38
-
39
-
40
- def _render_list_block(name: str, items: list[str]) -> list[str]:
41
- lines = [f"{name}:"]
42
- for item in items:
43
- lines.append(f' - "{item}"')
44
- return lines
45
-
46
-
47
- def _render_rules_block() -> list[str]:
48
- lines = ["rules:"]
49
- for key, value in DEFAULT_RULES.items():
50
- lines.append(f' {key}: "{value}"')
51
- return lines
52
-
53
-
54
- def render_default_routing_yaml() -> str:
55
- lines: list[str] = [
56
- "version: 1",
57
- "",
58
- *_render_list_block("frontend_paths", DEFAULT_FRONTEND_PATHS),
59
- "",
60
- *_render_list_block("custom_frontend_paths", []),
61
- "",
62
- *_render_list_block("backend_paths", DEFAULT_BACKEND_PATHS),
63
- "",
64
- *_render_list_block("custom_backend_paths", []),
65
- "",
66
- *_render_list_block("shared_paths", DEFAULT_SHARED_PATHS),
67
- "",
68
- *_render_rules_block(),
69
- "",
70
- ]
71
- return "\n".join(lines)
72
-
73
-
74
- def _normalize_line_endings(text: str) -> str:
75
- return text.replace("\r\n", "\n").replace("\r", "\n")
76
-
77
-
78
- def _extract_list_block(lines: list[str], block_name: str) -> list[str]:
79
- items: list[str] = []
80
- inside = False
81
- for line in lines:
82
- stripped = line.strip()
83
- if not inside:
84
- if stripped == f"{block_name}:":
85
- inside = True
86
- continue
87
-
88
- if line and not line.startswith(" "):
89
- break
90
- if stripped.startswith("- "):
91
- value = stripped[2:].strip()
92
- if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
93
- value = value[1:-1]
94
- if value:
95
- items.append(value)
96
- return items
97
-
98
-
99
- def merge_routing_template(existing_text: str) -> str:
100
- if not existing_text.strip():
101
- return render_default_routing_yaml()
102
-
103
- lines = _normalize_line_endings(existing_text).split("\n")
104
- custom_frontend = _extract_list_block(lines, "custom_frontend_paths")
105
- custom_backend = _extract_list_block(lines, "custom_backend_paths")
106
-
107
- merged = render_default_routing_yaml().split("\n")
108
- output: list[str] = []
109
- current_block = ""
110
-
111
- for line in merged:
112
- stripped = line.strip()
113
- output.append(line)
114
- if stripped == "custom_frontend_paths:":
115
- current_block = "custom_frontend_paths"
116
- for item in custom_frontend:
117
- output.append(f' - "{item}"')
118
- continue
119
- if stripped == "custom_backend_paths:":
120
- current_block = "custom_backend_paths"
121
- for item in custom_backend:
122
- output.append(f' - "{item}"')
123
- continue
124
- if stripped.endswith(":") and stripped not in {"custom_frontend_paths:", "custom_backend_paths:"}:
125
- current_block = stripped[:-1]
126
-
127
- return "\n".join(output).rstrip() + "\n"
128
-
129
-
130
- def sync_workspace_routing_file(target_path: Path) -> Path:
131
- existing_text = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
132
- merged_text = merge_routing_template(existing_text)
133
- target_path.parent.mkdir(parents=True, exist_ok=True)
134
- target_path.write_text(merged_text, encoding="utf-8")
135
- return target_path
1
+ from codecgc_runtime.routing_template import DEFAULT_BACKEND_PATHS
2
+ from codecgc_runtime.routing_template import DEFAULT_BACKEND_TEST_PATHS
3
+ from codecgc_runtime.routing_template import DEFAULT_DOCS_PATHS
4
+ from codecgc_runtime.routing_template import DEFAULT_FRONTEND_PATHS
5
+ from codecgc_runtime.routing_template import DEFAULT_FRONTEND_TEST_PATHS
6
+ from codecgc_runtime.routing_template import DEFAULT_ORCHESTRATION_PATHS
7
+ from codecgc_runtime.routing_template import DEFAULT_RULES
8
+ from codecgc_runtime.routing_template import DEFAULT_SHARED_PATHS
9
+ from codecgc_runtime.routing_template import merge_routing_template
10
+ from codecgc_runtime.routing_template import render_default_routing_yaml
11
+ from codecgc_runtime.routing_template import sync_workspace_routing_file
@@ -0,0 +1 @@
1
+ """Shared CodeCGC runtime helpers."""
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .paths import PROJECT_ROOT
6
+
7
+
8
+ CODECGC_ROOT = PROJECT_ROOT / "codecgc"
9
+ PRODUCT_ROOT = CODECGC_ROOT
10
+ FIXTURE_ROOT = CODECGC_ROOT / "fixtures"
11
+
12
+
13
+ def normalize_artifact_class(value: str | None) -> str:
14
+ cleaned = str(value or "product").strip().lower()
15
+ return "fixture" if cleaned == "fixture" else "product"
16
+
17
+
18
+ def flow_root(flow: str, artifact_class: str) -> Path:
19
+ base = FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
20
+ return base / ("features" if flow == "feature" else "issues")
21
+
22
+
23
+ def execution_root(artifact_class: str) -> Path:
24
+ base = FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
25
+ return base / "execution"
26
+
27
+
28
+ def discover_flow_directory(flow: str, slug: str, artifact_class: str = "auto") -> tuple[str, Path] | None:
29
+ classes = (
30
+ [normalize_artifact_class(artifact_class)]
31
+ if artifact_class in {"product", "fixture"}
32
+ else ["product", "fixture"]
33
+ )
34
+ for candidate_class in classes:
35
+ directory = flow_root(flow, candidate_class) / slug
36
+ if directory.exists():
37
+ return candidate_class, directory
38
+ return None
39
+
40
+
41
+ def artifact_output_root(artifact_class: str) -> Path:
42
+ return FIXTURE_ROOT if normalize_artifact_class(artifact_class) == "fixture" else PRODUCT_ROOT
@@ -0,0 +1,45 @@
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)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Any
6
+
7
+ from .paths import PACKAGE_ROOT
8
+
9
+
10
+ WORKSPACE = PACKAGE_ROOT
11
+
12
+
13
+ def resolve_python_command() -> str:
14
+ override = os.environ.get("CODECGC_PYTHON_COMMAND", "").strip()
15
+ if override:
16
+ return override
17
+ return sys.executable
18
+
19
+
20
+ def build_executor_registry() -> dict[str, dict[str, Any]]:
21
+ python_command = resolve_python_command()
22
+ return {
23
+ "backend": {
24
+ "target": "backend",
25
+ "mcp_server_name": "codex",
26
+ "routing_executor": "codexmcp",
27
+ "tool_name": "implement_backend_task",
28
+ "python_module": "codexmcp.cli",
29
+ "pythonpath": str(WORKSPACE / "codexmcp" / "src"),
30
+ "python_command": python_command,
31
+ },
32
+ "frontend": {
33
+ "target": "frontend",
34
+ "mcp_server_name": "gemini",
35
+ "routing_executor": "geminimcp",
36
+ "tool_name": "implement_frontend_task",
37
+ "python_module": "geminimcp.cli",
38
+ "pythonpath": str(WORKSPACE / "geminimcp" / "src"),
39
+ "python_command": python_command,
40
+ },
41
+ }
42
+
43
+
44
+ def get_executor_config(target: str) -> dict[str, Any]:
45
+ registry = build_executor_registry()
46
+ if target not in registry:
47
+ raise ValueError(f"Unsupported target: {target}")
48
+ return registry[target]
49
+
50
+
51
+ def build_executor_env(target: str) -> dict[str, str]:
52
+ env = dict(os.environ)
53
+ config = get_executor_config(target)
54
+ env["PYTHONPATH"] = str(config["pythonpath"])
55
+ return env