@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.
Files changed (71) hide show
  1. package/.claude/hooks/route-edit.ps1 +58 -57
  2. package/INSTALLATION.md +122 -484
  3. package/README.md +124 -149
  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/execution-model.md +3 -1
  10. package/codecgc/reference/maintainer-guide.md +93 -0
  11. package/codecgc/reference/mcp-tool-surface.md +112 -0
  12. package/codecgc/reference/onboarding.md +69 -0
  13. package/codecgc/reference/operation-guide.md +29 -23
  14. package/codecgc/reference/path-contract.md +53 -0
  15. package/codecgc/reference/policy-routing.md +58 -0
  16. package/codecgc/reference/project-structure.md +87 -0
  17. package/codecgc/reference/quickstart.md +110 -0
  18. package/codecgc/reference/real-workflow-loop.md +123 -0
  19. package/codecgc/reference/recovery-loop.md +109 -0
  20. package/codecgc/reference/release-maintenance-playbook.md +4 -1
  21. package/codecgc/reference/troubleshooting.md +114 -0
  22. package/codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md +49 -0
  23. package/codecgc/roadmap/codecgc-release-maintenance/overview.md +41 -0
  24. package/codecgc/roadmap/codecgc-release-maintenance/phases.md +84 -0
  25. package/codecgc/templates/claude/settings.local.json +27 -0
  26. package/codecgc/templates/codex/codecgcrc.json +22 -0
  27. package/codecgc/templates/gemini/codecgc-policy.toml +47 -0
  28. package/codecgcmcp/README.md +57 -11
  29. package/codecgcmcp/src/codecgcmcp/server.py +164 -26
  30. package/codexmcp/src/codexmcp/server.py +45 -0
  31. package/geminimcp/src/geminimcp/server.py +106 -24
  32. package/model-routing.yaml +31 -6
  33. package/package.json +12 -4
  34. package/scripts/audit_codecgc_external_capabilities.py +83 -4
  35. package/scripts/audit_codecgc_historical_audits.py +42 -2
  36. package/scripts/audit_codecgc_package_runtime.py +76 -4
  37. package/scripts/audit_codecgc_release_readiness.py +55 -3
  38. package/scripts/audit_codecgc_workflow_history.py +8 -5
  39. package/scripts/build_codecgc_task.py +69 -45
  40. package/scripts/codecgc_artifact_roots.py +8 -40
  41. package/scripts/codecgc_console_io.py +3 -45
  42. package/scripts/codecgc_executor_registry.py +4 -54
  43. package/scripts/codecgc_path_contract.py +7 -0
  44. package/scripts/codecgc_policy.py +447 -0
  45. package/scripts/codecgc_routing_paths.py +3 -16
  46. package/scripts/codecgc_routing_template.py +11 -135
  47. package/scripts/codecgc_runtime/__init__.py +1 -0
  48. package/scripts/codecgc_runtime/artifacts.py +42 -0
  49. package/scripts/codecgc_runtime/console.py +45 -0
  50. package/scripts/codecgc_runtime/executor_registry.py +55 -0
  51. package/scripts/codecgc_runtime/mcp_config.py +72 -0
  52. package/scripts/codecgc_runtime/path_contract.py +123 -0
  53. package/scripts/codecgc_runtime/paths.py +22 -0
  54. package/scripts/codecgc_runtime/routing_paths.py +16 -0
  55. package/scripts/codecgc_runtime/routing_template.py +171 -0
  56. package/scripts/codecgc_runtime/workflow_runtime.py +72 -0
  57. package/scripts/codecgc_runtime_paths.py +3 -22
  58. package/scripts/codecgc_step_control.py +3 -2
  59. package/scripts/codecgc_workflow_runtime.py +3 -71
  60. package/scripts/entry_codecgc_workflow.py +4 -0
  61. package/scripts/install_codecgc.py +560 -32
  62. package/scripts/normalize_codecgc_audits.py +5 -3
  63. package/scripts/postinstall_codecgc.js +6 -56
  64. package/scripts/review_codecgc_workflow.py +6 -3
  65. package/scripts/route_codecgc_workflow.py +67 -36
  66. package/scripts/run_codecgc_build.py +28 -0
  67. package/scripts/run_codecgc_fix.py +28 -0
  68. package/scripts/run_codecgc_task.py +5 -2
  69. package/scripts/run_codecgc_test.py +28 -0
  70. package/scripts/sync_codecgc_mcp_config.py +4 -54
  71. package/scripts/write_codecgc_review.py +7 -3
@@ -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,447 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ import shlex
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from fnmatch import fnmatch
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import yaml
14
+
15
+ from codecgc_console_io import configure_utf8_stdio
16
+ from codecgc_console_io import print_json
17
+ from codecgc_path_contract import to_project_relative_path
18
+ from codecgc_routing_paths import resolve_active_routing_file
19
+
20
+
21
+ OWNERS = {"orchestration", "docs", "frontend", "backend", "frontend-test", "backend-test", "shared", "unknown"}
22
+ POLICY_PROJECT_ROOT_KEY = "_codecgc_project_root"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PathDecision:
27
+ path: str
28
+ owner: str
29
+ allowed: bool
30
+ reason: str
31
+ recommended_action: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class HookRequest:
36
+ tool_name: str
37
+ file_path: str
38
+ command: str
39
+
40
+
41
+ EDIT_TOOL_NAMES = {"Edit", "Write", "MultiEdit"}
42
+ SHELL_TOOL_NAMES = {"Bash", "PowerShell"}
43
+ SHELL_DENIED_COMMAND_PATTERNS = (
44
+ r"\brm\s+-[^\n\r]*[rf]",
45
+ r"\bdel\s+",
46
+ r"\brmdir\s+",
47
+ r"\bremove-item\b",
48
+ r"\bset-content\b",
49
+ r"\badd-content\b",
50
+ r"\bout-file\b",
51
+ r"\bnew-item\b",
52
+ r"\bcopy-item\b",
53
+ r"\bmove-item\b",
54
+ r"\bgit\s+reset\s+--hard\b",
55
+ r"\bgit\s+clean\b",
56
+ r"\bpython\s+-c\b",
57
+ r"\bnode\s+-e\b",
58
+ r"\bpowershell(\.exe)?\s+-(command|encodedcommand)\b",
59
+ r"\bcmd(\.exe)?\s+/c\b",
60
+ )
61
+ SHELL_ALLOWED_COMMAND_PREFIXES = (
62
+ "git status",
63
+ "git diff",
64
+ "git log",
65
+ "git show",
66
+ "git branch",
67
+ "git rev-parse",
68
+ "git ls-files",
69
+ "git remote",
70
+ "rg",
71
+ "ls",
72
+ "dir",
73
+ "pwd",
74
+ "get-content",
75
+ "get-childitem",
76
+ "select-string",
77
+ "test-path",
78
+ "resolve-path",
79
+ "pytest",
80
+ "python -m pytest",
81
+ "python -m compileall",
82
+ "npm test",
83
+ "npm run test",
84
+ "npm run lint",
85
+ "npm run typecheck",
86
+ "npm run check",
87
+ "pnpm test",
88
+ "pnpm run test",
89
+ "pnpm run lint",
90
+ "pnpm run typecheck",
91
+ "yarn test",
92
+ "yarn lint",
93
+ "codex --help",
94
+ "gemini --help",
95
+ "gemini --version",
96
+ )
97
+
98
+
99
+ def normalize_path_text(path_text: str) -> str:
100
+ normalized = str(path_text or "").replace("\\", "/").strip()
101
+ while normalized.startswith("./"):
102
+ normalized = normalized[2:]
103
+ return normalized
104
+
105
+
106
+ def _as_string_list(value: Any) -> list[str]:
107
+ if value is None:
108
+ return []
109
+ if isinstance(value, list):
110
+ return [str(item).strip() for item in value if str(item).strip()]
111
+ return [str(value).strip()] if str(value).strip() else []
112
+
113
+
114
+ def _load_yaml(path: Path) -> dict[str, Any]:
115
+ if not path.exists():
116
+ raise FileNotFoundError(f"Routing policy file not found: {path}")
117
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
118
+ if not isinstance(data, dict):
119
+ raise ValueError(f"Routing policy must be a mapping: {path}")
120
+ return data
121
+
122
+
123
+ def load_policy(path: Path | None = None) -> dict[str, Any]:
124
+ policy_path = path or resolve_active_routing_file()
125
+ policy = _load_yaml(policy_path)
126
+ if int(policy.get("version", 0) or 0) != 2:
127
+ raise ValueError("model-routing.yaml must use version: 2.")
128
+ validate_policy(policy)
129
+ policy[POLICY_PROJECT_ROOT_KEY] = str(policy_path.parent.resolve())
130
+ return policy
131
+
132
+
133
+ def validate_policy(policy: dict[str, Any]) -> None:
134
+ required_lists = [
135
+ "orchestration_paths",
136
+ "docs_paths",
137
+ "frontend_paths",
138
+ "backend_paths",
139
+ "shared_paths",
140
+ ]
141
+ missing = [name for name in required_lists if not isinstance(policy.get(name), list)]
142
+ test_paths = policy.get("test_paths")
143
+ rules = policy.get("rules")
144
+ if not isinstance(test_paths, dict):
145
+ missing.append("test_paths")
146
+ if not isinstance(rules, dict):
147
+ missing.append("rules")
148
+ if missing:
149
+ raise ValueError(f"model-routing.yaml is missing required policy sections: {', '.join(missing)}")
150
+
151
+ for name in ("frontend", "backend"):
152
+ if not isinstance(test_paths.get(name), list):
153
+ raise ValueError(f"model-routing.yaml test_paths.{name} must be a list.")
154
+
155
+ allowed = set(_as_string_list(rules.get("claude_allowed_owners")))
156
+ invalid_allowed = sorted(allowed - OWNERS)
157
+ if invalid_allowed:
158
+ raise ValueError(f"Invalid claude_allowed_owners entries: {', '.join(invalid_allowed)}")
159
+
160
+ if str(rules.get("shared_policy", "")).strip() != "split-first":
161
+ raise ValueError("Only shared_policy: split-first is supported.")
162
+
163
+
164
+ def _matches_any(path_text: str, patterns: list[str]) -> bool:
165
+ return any(fnmatch(path_text, pattern) for pattern in patterns)
166
+
167
+
168
+ def normalize_policy_path(path_text: str, policy: dict[str, Any]) -> str:
169
+ project_root = str(policy.get(POLICY_PROJECT_ROOT_KEY, "")).strip()
170
+ if project_root:
171
+ return normalize_path_text(
172
+ to_project_relative_path(path_text, Path(project_root), allow_legacy_suffix=False)
173
+ )
174
+ return normalize_path_text(path_text)
175
+
176
+
177
+ def classify_path(path_text: str, policy: dict[str, Any]) -> str:
178
+ normalized = normalize_policy_path(path_text, policy)
179
+ test_paths = policy.get("test_paths", {})
180
+
181
+ ordered_groups: list[tuple[str, list[str]]] = [
182
+ ("shared", _as_string_list(policy.get("shared_paths"))),
183
+ ("orchestration", _as_string_list(policy.get("orchestration_paths"))),
184
+ ("docs", _as_string_list(policy.get("docs_paths"))),
185
+ ("frontend-test", _as_string_list(test_paths.get("frontend"))),
186
+ ("backend-test", _as_string_list(test_paths.get("backend"))),
187
+ ("frontend", _as_string_list(policy.get("frontend_paths"))),
188
+ ("backend", _as_string_list(policy.get("backend_paths"))),
189
+ ]
190
+ for owner, patterns in ordered_groups:
191
+ if _matches_any(normalized, patterns):
192
+ return owner
193
+ return "unknown"
194
+
195
+
196
+ def allowed_owners_for_actor(actor: str, policy: dict[str, Any]) -> set[str]:
197
+ normalized_actor = str(actor or "").strip().lower()
198
+ rules = policy.get("rules", {})
199
+ if normalized_actor == "claude":
200
+ return set(_as_string_list(rules.get("claude_allowed_owners")))
201
+ if normalized_actor in {"codex", "codexmcp", "backend"}:
202
+ return {"backend", "backend-test"}
203
+ if normalized_actor in {"gemini", "geminimcp", "frontend"}:
204
+ return {"frontend", "frontend-test"}
205
+ return set()
206
+
207
+
208
+ def recommended_action_for(owner: str, actor: str) -> str:
209
+ if owner == "backend":
210
+ return "route through cgc-build or cgc-fix with a backend step"
211
+ if owner == "backend-test":
212
+ return "route through cgc-test with a backend test step"
213
+ if owner == "frontend":
214
+ return "route through cgc-build or cgc-fix with a frontend step"
215
+ if owner == "frontend-test":
216
+ return "route through cgc-test with a frontend test step"
217
+ if owner == "shared":
218
+ return "split the shared change before execution"
219
+ if owner == "unknown":
220
+ return "add the path to model-routing.yaml or narrow the target path"
221
+ return f"request a policy route for actor {actor}"
222
+
223
+
224
+ def decide_path(path_text: str, actor: str, operation: str, policy: dict[str, Any]) -> PathDecision:
225
+ owner = classify_path(path_text, policy)
226
+ allowed_owners = allowed_owners_for_actor(actor, policy)
227
+ normalized_actor = str(actor or "").strip().lower()
228
+ normalized_operation = str(operation or "").strip().lower() or "write"
229
+
230
+ allowed = normalized_operation in {"read", "inspect"} or owner in allowed_owners
231
+ if owner == "unknown":
232
+ allowed = False
233
+ if owner == "shared" and normalized_operation not in {"read", "inspect"}:
234
+ allowed = False
235
+
236
+ if allowed:
237
+ reason = f"{normalized_actor} may {normalized_operation} {owner} paths"
238
+ recommended = ""
239
+ elif owner == "shared":
240
+ reason = "shared paths require split-first routing"
241
+ recommended = recommended_action_for(owner, normalized_actor)
242
+ elif owner == "unknown":
243
+ reason = "path is not covered by model-routing.yaml"
244
+ recommended = recommended_action_for(owner, normalized_actor)
245
+ else:
246
+ reason = f"{owner} paths are not owned by {normalized_actor}"
247
+ recommended = recommended_action_for(owner, normalized_actor)
248
+
249
+ return PathDecision(
250
+ path=normalize_policy_path(path_text, policy),
251
+ owner=owner,
252
+ allowed=allowed,
253
+ reason=reason,
254
+ recommended_action=recommended,
255
+ )
256
+
257
+
258
+ def evaluate_paths(paths: list[str], actor: str, operation: str, policy: dict[str, Any]) -> dict[str, Any]:
259
+ decisions = [decide_path(path, actor, operation, policy) for path in paths]
260
+ return {
261
+ "success": all(decision.allowed for decision in decisions),
262
+ "allowed": all(decision.allowed for decision in decisions),
263
+ "actor": str(actor or "").strip().lower(),
264
+ "operation": str(operation or "").strip().lower() or "write",
265
+ "decisions": [decision.__dict__ for decision in decisions],
266
+ "owners": sorted({decision.owner for decision in decisions}),
267
+ }
268
+
269
+
270
+ def validate_executor_target(kind: str, target_paths: list[str], policy: dict[str, Any]) -> dict[str, Any]:
271
+ actor = "codex" if str(kind).strip().lower() == "backend" else "gemini" if str(kind).strip().lower() == "frontend" else str(kind)
272
+ result = evaluate_paths(target_paths, actor=actor, operation="write", policy=policy)
273
+ result["kind"] = str(kind).strip().lower()
274
+ return result
275
+
276
+
277
+ def parse_hook_request(text: str) -> HookRequest:
278
+ if not text.strip():
279
+ return HookRequest(tool_name="", file_path="", command="")
280
+ payload = json.loads(text)
281
+ if not isinstance(payload, dict):
282
+ return HookRequest(tool_name="", file_path="", command="")
283
+ tool_name = str(payload.get("tool_name", "")).strip()
284
+ tool_input = payload.get("tool_input", {})
285
+ if not isinstance(tool_input, dict):
286
+ return HookRequest(tool_name=tool_name, file_path="", command="")
287
+ file_path = str(tool_input.get("file_path") or tool_input.get("path") or "").strip()
288
+ command = str(tool_input.get("command") or tool_input.get("script") or "").strip()
289
+ return HookRequest(tool_name=tool_name, file_path=file_path, command=command)
290
+
291
+
292
+ def parse_hook_payload(text: str) -> tuple[str, str]:
293
+ request = parse_hook_request(text)
294
+ return request.tool_name, request.file_path
295
+
296
+
297
+ def has_unquoted_shell_control_operator(command: str) -> bool:
298
+ in_single = False
299
+ in_double = False
300
+ escaped = False
301
+ for index, char in enumerate(command):
302
+ if escaped:
303
+ escaped = False
304
+ continue
305
+ if char == "\\":
306
+ escaped = True
307
+ continue
308
+ if char == "'" and not in_double:
309
+ in_single = not in_single
310
+ continue
311
+ if char == '"' and not in_single:
312
+ in_double = not in_double
313
+ continue
314
+ if in_single or in_double:
315
+ continue
316
+ if char in {"|", ";", ">", "<", "`", "&"}:
317
+ return True
318
+ if char == "$" and index + 1 < len(command) and command[index + 1] == "(":
319
+ return True
320
+ return False
321
+
322
+
323
+ def normalize_shell_command_for_prefix(command: str) -> str:
324
+ try:
325
+ parts = shlex.split(command, posix=False)
326
+ except ValueError:
327
+ parts = command.split()
328
+ if not parts:
329
+ return ""
330
+
331
+ first = parts[0].strip().strip("'\"")
332
+ first_name = Path(first.replace("\\", "/")).name.lower()
333
+ for suffix in (".cmd", ".ps1", ".exe", ".bat"):
334
+ if first_name.endswith(suffix):
335
+ first_name = first_name[: -len(suffix)]
336
+ break
337
+ rest = " ".join(str(item).strip().strip("'\"").lower() for item in parts[1:])
338
+ return f"{first_name} {rest}".strip()
339
+
340
+
341
+ def shell_prefix_matches(command: str, prefix: str) -> bool:
342
+ return command == prefix or command.startswith(f"{prefix} ")
343
+
344
+
345
+ def is_codecgc_cli_shell_command(normalized_command: str) -> bool:
346
+ first = normalized_command.split(" ", 1)[0]
347
+ return first == "cgc" or first == "codecgc" or first.startswith("cgc-")
348
+
349
+
350
+ def evaluate_shell_command(command: str, tool_name: str = "Bash") -> dict[str, Any]:
351
+ normalized_tool_name = str(tool_name or "").strip()
352
+ if not str(command or "").strip():
353
+ return {"allowed": True, "reason": "", "recommended_action": ""}
354
+
355
+ if has_unquoted_shell_control_operator(command):
356
+ return {
357
+ "allowed": False,
358
+ "reason": f"{normalized_tool_name} commands with shell control operators are not allowed for Claude",
359
+ "recommended_action": "route write work through /cgc or run a single read-only check",
360
+ }
361
+
362
+ lowered = command.lower()
363
+ if any(re.search(pattern, lowered) for pattern in SHELL_DENIED_COMMAND_PATTERNS):
364
+ return {
365
+ "allowed": False,
366
+ "reason": f"{normalized_tool_name} command looks like a direct write or destructive shell operation",
367
+ "recommended_action": "route implementation through /cgc so CodeCGC can dispatch Codex or Gemini",
368
+ }
369
+
370
+ normalized_command = normalize_shell_command_for_prefix(command)
371
+ if is_codecgc_cli_shell_command(normalized_command):
372
+ return {"allowed": True, "reason": "", "recommended_action": ""}
373
+
374
+ if any(shell_prefix_matches(normalized_command, prefix) for prefix in SHELL_ALLOWED_COMMAND_PREFIXES):
375
+ return {"allowed": True, "reason": "", "recommended_action": ""}
376
+
377
+ return {
378
+ "allowed": False,
379
+ "reason": f"{normalized_tool_name} command is outside the CodeCGC Claude shell allowlist",
380
+ "recommended_action": "use /cgc for write work, or use an allowed read-only/test command",
381
+ }
382
+
383
+
384
+ def build_hook_response(allowed: bool, reason: str) -> dict[str, Any]:
385
+ if allowed:
386
+ return {"decision": "approve"}
387
+ return {"decision": "deny", "reason": reason}
388
+
389
+
390
+ def build_parser() -> argparse.ArgumentParser:
391
+ parser = argparse.ArgumentParser(description="Evaluate CodeCGC routing policy decisions.")
392
+ parser.add_argument("--routing-file", default="", help="Optional explicit model-routing.yaml path.")
393
+ parser.add_argument("--actor", default="claude", help="Actor requesting the operation.")
394
+ parser.add_argument("--operation", default="write", help="Operation to evaluate.")
395
+ parser.add_argument("--path", action="append", default=[], help="Path to evaluate. Repeatable.")
396
+ parser.add_argument("--hook-check", action="store_true", help="Read Claude hook JSON from stdin and return hook JSON.")
397
+ return parser
398
+
399
+
400
+ def main() -> int:
401
+ configure_utf8_stdio()
402
+ parser = build_parser()
403
+ args = parser.parse_args()
404
+
405
+ try:
406
+ policy_path = Path(args.routing_file).resolve() if args.routing_file else resolve_active_routing_file()
407
+ policy = load_policy(policy_path)
408
+
409
+ if args.hook_check:
410
+ request = parse_hook_request(sys.stdin.read())
411
+ if request.tool_name in SHELL_TOOL_NAMES:
412
+ result = evaluate_shell_command(request.command, request.tool_name)
413
+ if result["allowed"]:
414
+ print_json(build_hook_response(True, ""))
415
+ return 0
416
+ hook_reason = f"CodeCGC: {result['reason']}."
417
+ recommended = str(result.get("recommended_action", "")).strip()
418
+ if recommended:
419
+ hook_reason += f" {recommended}."
420
+ print_json(build_hook_response(False, hook_reason))
421
+ return 0
422
+
423
+ if request.tool_name not in EDIT_TOOL_NAMES or not request.file_path:
424
+ print_json(build_hook_response(True, ""))
425
+ return 0
426
+ result = evaluate_paths([request.file_path], actor=args.actor, operation=args.operation, policy=policy)
427
+ decision = result["decisions"][0]
428
+ reason = decision.get("reason", "")
429
+ recommended = decision.get("recommended_action", "")
430
+ hook_reason = f"CodeCGC: {reason}."
431
+ if recommended:
432
+ hook_reason += f" {recommended}."
433
+ print_json(build_hook_response(bool(result["allowed"]), hook_reason))
434
+ return 0
435
+
436
+ if not args.path:
437
+ raise ValueError("At least one --path is required unless --hook-check is used.")
438
+
439
+ print_json(evaluate_paths(args.path, actor=args.actor, operation=args.operation, policy=policy))
440
+ return 0
441
+ except Exception as error:
442
+ print_json({"success": False, "allowed": False, "error": str(error)}, file=sys.stderr)
443
+ return 1
444
+
445
+
446
+ if __name__ == "__main__":
447
+ 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