@hunyed15/codecgc 0.1.0

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 (128) hide show
  1. package/.claude/hooks/route-edit.ps1 +86 -0
  2. package/INSTALLATION.md +550 -0
  3. package/LICENSE +21 -0
  4. package/README.md +171 -0
  5. package/bin/cgc-build.js +4 -0
  6. package/bin/cgc-doctor.js +4 -0
  7. package/bin/cgc-entry.js +4 -0
  8. package/bin/cgc-external-audit.js +4 -0
  9. package/bin/cgc-fix.js +4 -0
  10. package/bin/cgc-history.js +4 -0
  11. package/bin/cgc-install.js +4 -0
  12. package/bin/cgc-lifecycle.js +4 -0
  13. package/bin/cgc-package-audit.js +4 -0
  14. package/bin/cgc-plan.js +4 -0
  15. package/bin/cgc-release-readiness.js +4 -0
  16. package/bin/cgc-review.js +4 -0
  17. package/bin/cgc-route.js +4 -0
  18. package/bin/cgc-status.js +4 -0
  19. package/bin/cgc-test.js +4 -0
  20. package/bin/cgc.js +4 -0
  21. package/bin/codecgc.js +1284 -0
  22. package/codecgc/cgc/SKILL.md +46 -0
  23. package/codecgc/cgc-arch/SKILL.md +61 -0
  24. package/codecgc/cgc-build/SKILL.md +53 -0
  25. package/codecgc/cgc-decide/SKILL.md +55 -0
  26. package/codecgc/cgc-fix/SKILL.md +47 -0
  27. package/codecgc/cgc-learn/SKILL.md +46 -0
  28. package/codecgc/cgc-onboard/SKILL.md +52 -0
  29. package/codecgc/cgc-plan/SKILL.md +48 -0
  30. package/codecgc/cgc-refactor/SKILL.md +46 -0
  31. package/codecgc/cgc-req/SKILL.md +61 -0
  32. package/codecgc/cgc-review/SKILL.md +57 -0
  33. package/codecgc/cgc-roadmap/SKILL.md +55 -0
  34. package/codecgc/cgc-test/SKILL.md +21 -0
  35. package/codecgc/reference/api-cgc-review-libdoc.md +13 -0
  36. package/codecgc/reference/artifact-class-policy.md +81 -0
  37. package/codecgc/reference/build-flow.md +95 -0
  38. package/codecgc/reference/checklist-contract.md +103 -0
  39. package/codecgc/reference/execution-audit.md +121 -0
  40. package/codecgc/reference/execution-model.md +118 -0
  41. package/codecgc/reference/execution-routing.md +130 -0
  42. package/codecgc/reference/executor-contract.md +87 -0
  43. package/codecgc/reference/external-capability-registry.json +104 -0
  44. package/codecgc/reference/fix-flow.md +94 -0
  45. package/codecgc/reference/fixture-governance.md +60 -0
  46. package/codecgc/reference/flow-execution.md +65 -0
  47. package/codecgc/reference/lifecycle-map.md +172 -0
  48. package/codecgc/reference/lifecycle-playbook.md +104 -0
  49. package/codecgc/reference/long-lived-artifacts.md +98 -0
  50. package/codecgc/reference/operation-guide.md +242 -0
  51. package/codecgc/reference/release-maintenance-playbook.md +150 -0
  52. package/codecgc/reference/review-writeback.md +141 -0
  53. package/codecgc/reference/role-model.md +128 -0
  54. package/codecgc/reference/runtime-boundary.md +72 -0
  55. package/codecgc/reference/shared-conventions.md +93 -0
  56. package/codecgc/reference/workflow-scaffold.md +57 -0
  57. package/codexmcp/LICENSE +21 -0
  58. package/codexmcp/README.md +294 -0
  59. package/codexmcp/pyproject.toml +37 -0
  60. package/codexmcp/src/codexmcp/__init__.py +4 -0
  61. package/codexmcp/src/codexmcp/cli.py +12 -0
  62. package/codexmcp/src/codexmcp/server.py +529 -0
  63. package/geminimcp/README.md +258 -0
  64. package/geminimcp/pyproject.toml +15 -0
  65. package/geminimcp/src/geminimcp/__init__.py +4 -0
  66. package/geminimcp/src/geminimcp/cli.py +12 -0
  67. package/geminimcp/src/geminimcp/server.py +465 -0
  68. package/model-routing.yaml +30 -0
  69. package/package.json +90 -0
  70. package/requirements.txt +1 -0
  71. package/scripts/README-codecgc-cli.md +89 -0
  72. package/scripts/audit_codecgc_external_capabilities.py +276 -0
  73. package/scripts/audit_codecgc_historical_audits.py +242 -0
  74. package/scripts/audit_codecgc_lifecycle.py +241 -0
  75. package/scripts/audit_codecgc_package_runtime.py +445 -0
  76. package/scripts/audit_codecgc_release_readiness.py +202 -0
  77. package/scripts/audit_codecgc_review_policy.py +82 -0
  78. package/scripts/audit_codecgc_workflow_history.py +317 -0
  79. package/scripts/build_codecgc_task.py +487 -0
  80. package/scripts/codecgc_artifact_roots.py +40 -0
  81. package/scripts/codecgc_cli.py +843 -0
  82. package/scripts/codecgc_command_surface.py +28 -0
  83. package/scripts/codecgc_console_io.py +45 -0
  84. package/scripts/codecgc_executor_registry.py +54 -0
  85. package/scripts/codecgc_file_evidence.py +349 -0
  86. package/scripts/codecgc_flow_control.py +233 -0
  87. package/scripts/codecgc_governance_dedupe.py +161 -0
  88. package/scripts/codecgc_plan_decision.py +103 -0
  89. package/scripts/codecgc_review_control.py +588 -0
  90. package/scripts/codecgc_roadmap_templates.py +149 -0
  91. package/scripts/codecgc_routing_paths.py +16 -0
  92. package/scripts/codecgc_routing_template.py +135 -0
  93. package/scripts/codecgc_runtime_paths.py +22 -0
  94. package/scripts/codecgc_session_recovery.py +44 -0
  95. package/scripts/codecgc_step_control.py +154 -0
  96. package/scripts/codecgc_workflow_runtime.py +63 -0
  97. package/scripts/codecgc_workflow_templates.py +437 -0
  98. package/scripts/entry_codecgc_workflow.py +3419 -0
  99. package/scripts/exercise_mcp_tools.py +109 -0
  100. package/scripts/expand_codecgc_roadmap.py +664 -0
  101. package/scripts/init_codecgc_roadmap.py +134 -0
  102. package/scripts/init_codecgc_workflow.py +207 -0
  103. package/scripts/install_codecgc.py +938 -0
  104. package/scripts/migrate_demo_workflows_to_fixtures.py +128 -0
  105. package/scripts/normalize_codecgc_audits.py +114 -0
  106. package/scripts/normalize_codecgc_governance_docs.py +79 -0
  107. package/scripts/normalize_codecgc_workflow_docs.py +269 -0
  108. package/scripts/plan_codecgc_workflow.py +970 -0
  109. package/scripts/refresh_codecgc_review_policy.py +223 -0
  110. package/scripts/review_codecgc_workflow.py +88 -0
  111. package/scripts/route_codecgc_workflow.py +671 -0
  112. package/scripts/run_codecgc_build.py +104 -0
  113. package/scripts/run_codecgc_fix.py +104 -0
  114. package/scripts/run_codecgc_flow_step.py +165 -0
  115. package/scripts/run_codecgc_task.py +410 -0
  116. package/scripts/run_codecgc_test.py +105 -0
  117. package/scripts/sync_codecgc_mcp_config.py +41 -0
  118. package/scripts/write_codecgc_architecture.py +78 -0
  119. package/scripts/write_codecgc_decision.py +83 -0
  120. package/scripts/write_codecgc_explore.py +118 -0
  121. package/scripts/write_codecgc_guide.py +141 -0
  122. package/scripts/write_codecgc_learning.py +87 -0
  123. package/scripts/write_codecgc_libdoc.py +140 -0
  124. package/scripts/write_codecgc_refactor.py +78 -0
  125. package/scripts/write_codecgc_requirement.py +78 -0
  126. package/scripts/write_codecgc_review.py +291 -0
  127. package/scripts/write_codecgc_roadmap.py +122 -0
  128. package/scripts/write_codecgc_trick.py +123 -0
@@ -0,0 +1,487 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ import yaml
5
+ from fnmatch import fnmatch
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from codecgc_console_io import configure_utf8_stdio
10
+ from codecgc_console_io import print_json
11
+ from codecgc_routing_paths import resolve_active_routing_file
12
+ from codecgc_runtime_paths import PACKAGE_ROOT
13
+ from codecgc_runtime_paths import PROJECT_ROOT
14
+
15
+ WORKSPACE = PACKAGE_ROOT
16
+ PROJECT_WORKSPACE = PROJECT_ROOT
17
+ DEFAULT_ROUTING_FILE = resolve_active_routing_file()
18
+
19
+
20
+ def normalize_path_text(path_text: str) -> str:
21
+ normalized = path_text.replace("\\", "/").strip()
22
+ while normalized.startswith("./"):
23
+ normalized = normalized[2:]
24
+ return normalized
25
+
26
+
27
+ def load_simple_routing_config(path: Path) -> dict[str, Any]:
28
+ """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
42
+
43
+
44
+ def load_checklist_yaml(path: Path) -> dict[str, Any]:
45
+ """Load and parse a checklist YAML file using standard yaml library."""
46
+ if not path.exists():
47
+ raise FileNotFoundError(f"Checklist file not found: {path}")
48
+
49
+ try:
50
+ with open(path, "r", encoding="utf-8") as f:
51
+ data = yaml.safe_load(f)
52
+
53
+ if not isinstance(data, dict):
54
+ raise ValueError(f"Checklist YAML must be a dictionary, got {type(data).__name__}")
55
+
56
+ return data
57
+ except yaml.YAMLError as e:
58
+ raise ValueError(f"Failed to parse YAML file {path}: {e}") from e
59
+
60
+
61
+ def normalize_string_list(value: Any) -> list[str]:
62
+ if value is None:
63
+ return []
64
+ if isinstance(value, list):
65
+ return [str(item) for item in value]
66
+ return [str(value)]
67
+
68
+
69
+ def resolve_optional_value(cli_value: Any, spec_value: Any, default_value: Any) -> Any:
70
+ if cli_value not in (None, "", []):
71
+ return cli_value
72
+ if spec_value not in (None, "", []):
73
+ return spec_value
74
+ return default_value
75
+
76
+
77
+ def resolve_cd_value(cd_value: str | None) -> str:
78
+ if not cd_value:
79
+ return str(PROJECT_WORKSPACE)
80
+
81
+ path = Path(str(cd_value))
82
+ if path.is_absolute():
83
+ return str(path)
84
+
85
+ return str((PROJECT_WORKSPACE / path).resolve())
86
+
87
+
88
+ def load_checklist_step_payload(args: argparse.Namespace) -> dict[str, Any]:
89
+ checklist_path = Path(str(args.checklist_file))
90
+ checklist = load_checklist_yaml(checklist_path)
91
+ steps = checklist.get("steps", [])
92
+
93
+ if not isinstance(steps, list) or not steps:
94
+ raise ValueError("Checklist does not contain any steps.")
95
+
96
+ step_number = args.step_number
97
+ if step_number is None or step_number < 1 or step_number > len(steps):
98
+ raise ValueError(f"Step number must be between 1 and {len(steps)}.")
99
+
100
+ step = steps[step_number - 1]
101
+ step_spec = step.get("codecgc")
102
+ if not isinstance(step_spec, dict):
103
+ raise ValueError(
104
+ f"Checklist step {step_number} does not define a codecgc block."
105
+ )
106
+
107
+ feature_value = checklist.get("feature")
108
+ issue_value = checklist.get("issue")
109
+ artifact_class = str(checklist.get("artifact_class", "product") or "product")
110
+ artifact_type = "feature" if feature_value else "issue" if issue_value else "checklist"
111
+ fallback_slug = checklist_path.stem.replace("-checklist", "").replace("-fix", "")
112
+ artifact_slug = str(feature_value or issue_value or fallback_slug)
113
+ kind_from_spec = step_spec.get("kind", "auto")
114
+ requested_kind = args.kind if args.kind != "auto" else str(kind_from_spec)
115
+ task_summary = resolve_optional_value(args.task_summary, step_spec.get("task_summary"), step.get("action"))
116
+ target_paths = normalize_string_list(
117
+ resolve_optional_value(args.target_path, step_spec.get("target_paths"), None)
118
+ )
119
+ constraints = normalize_string_list(
120
+ resolve_optional_value(args.constraint, step_spec.get("constraints"), [])
121
+ )
122
+ acceptance = normalize_string_list(
123
+ resolve_optional_value(
124
+ args.acceptance,
125
+ step_spec.get("acceptance"),
126
+ [step.get("exit_signal")] if step.get("exit_signal") else [],
127
+ )
128
+ )
129
+ task_id = resolve_optional_value(
130
+ args.task_id,
131
+ step_spec.get("task_id"),
132
+ f"{artifact_slug}-step-{step_number}",
133
+ )
134
+
135
+ return {
136
+ "kind": requested_kind,
137
+ "task_id": str(task_id),
138
+ "task_summary": str(task_summary),
139
+ "target_path": target_paths,
140
+ "constraint": constraints,
141
+ "acceptance": acceptance,
142
+ "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()),
144
+ "session_id": str(resolve_optional_value(args.session_id, step_spec.get("session_id"), "")),
145
+ "model": str(resolve_optional_value(args.model, step_spec.get("model"), "")),
146
+ "profile": str(resolve_optional_value(args.profile, step_spec.get("profile"), "")),
147
+ "codex_sandbox": str(
148
+ resolve_optional_value(args.codex_sandbox, step_spec.get("codex_sandbox"), "workspace-write")
149
+ ),
150
+ "gemini_sandbox": bool(
151
+ resolve_optional_value(args.gemini_sandbox, step_spec.get("gemini_sandbox"), False)
152
+ ),
153
+ "return_all_messages": bool(
154
+ resolve_optional_value(
155
+ args.return_all_messages,
156
+ step_spec.get("return_all_messages"),
157
+ False,
158
+ )
159
+ ),
160
+ "timeout_seconds": int(step_spec.get("timeout_seconds", 0)) or 0,
161
+ "source": {
162
+ "type": "workflow-step",
163
+ "artifact_file": str(checklist_path.resolve()),
164
+ "artifact_type": artifact_type,
165
+ "artifact_class": artifact_class,
166
+ "artifact_slug": artifact_slug,
167
+ "step_number": step_number,
168
+ "step_action": str(step.get("action", "")),
169
+ },
170
+ }
171
+
172
+
173
+ def load_explicit_task_payload(args: argparse.Namespace) -> dict[str, Any]:
174
+ return {
175
+ "kind": args.kind,
176
+ "task_id": args.task_id,
177
+ "task_summary": args.task_summary,
178
+ "target_path": args.target_path,
179
+ "constraint": args.constraint,
180
+ "acceptance": args.acceptance,
181
+ "cd": resolve_cd_value(args.cd),
182
+ "routing_file": str(Path(str(args.routing_file)).resolve()),
183
+ "session_id": args.session_id or "",
184
+ "model": args.model or "",
185
+ "profile": args.profile or "",
186
+ "codex_sandbox": args.codex_sandbox or "workspace-write",
187
+ "gemini_sandbox": bool(args.gemini_sandbox),
188
+ "return_all_messages": bool(args.return_all_messages),
189
+ "source": None,
190
+ }
191
+
192
+
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
+ def classify_paths(paths: list[str], routing: dict[str, Any]) -> dict[str, str]:
214
+ return {path: classify_path(path, routing) for path in paths}
215
+
216
+
217
+ def split_paths_by_category(classifications: dict[str, str]) -> dict[str, list[str]]:
218
+ grouped = {
219
+ "frontend": [],
220
+ "backend": [],
221
+ "shared": [],
222
+ "unknown": [],
223
+ }
224
+ for path, category in classifications.items():
225
+ grouped.setdefault(category, []).append(path)
226
+ return grouped
227
+
228
+
229
+ def build_split_required_payload(paths: list[str], routing: dict[str, Any]) -> dict[str, Any]:
230
+ classifications = classify_paths(paths, routing)
231
+ grouped = split_paths_by_category(classifications)
232
+ suggestions: list[dict[str, Any]] = []
233
+
234
+ if grouped.get("backend"):
235
+ suggestions.append(
236
+ {
237
+ "step": f"step-{len(suggestions) + 1}",
238
+ "kind": "backend",
239
+ "executor": "Codex",
240
+ "target_paths": grouped["backend"],
241
+ "reason": "这些路径已明确属于后端范围,应拆成独立后端步骤。",
242
+ }
243
+ )
244
+
245
+ if grouped.get("frontend"):
246
+ suggestions.append(
247
+ {
248
+ "step": f"step-{len(suggestions) + 1}",
249
+ "kind": "frontend",
250
+ "executor": "Gemini",
251
+ "target_paths": grouped["frontend"],
252
+ "reason": "这些路径已明确属于前端范围,应拆成独立前端步骤。",
253
+ }
254
+ )
255
+
256
+ if grouped.get("shared"):
257
+ suggestions.append(
258
+ {
259
+ "step": f"step-{len(suggestions) + 1}",
260
+ "kind": "planning",
261
+ "executor": "Claude",
262
+ "target_paths": grouped["shared"],
263
+ "reason": "这些路径属于 shared 范围,必须先重新拆分到纯前端或纯后端步骤后才能执行。",
264
+ }
265
+ )
266
+
267
+ return {
268
+ "path_classification": classifications,
269
+ "grouped_paths": grouped,
270
+ "suggested_split_steps": suggestions,
271
+ }
272
+
273
+
274
+ def detect_target_kind(paths: list[str], routing: dict[str, Any]) -> tuple[str, list[str]]:
275
+ classifications = classify_paths(paths, routing)
276
+ categories = set(classifications.values())
277
+
278
+ if "shared" in categories:
279
+ details = [
280
+ f"{path} -> shared" for path, category in classifications.items() if category == "shared"
281
+ ]
282
+ split_payload = build_split_required_payload(paths, routing)
283
+ raise ValueError(
284
+ "Detected shared paths. Split the task first.\n"
285
+ + "\n".join(details)
286
+ + "\nSPLIT_PAYLOAD: "
287
+ + json.dumps(split_payload, ensure_ascii=False)
288
+ )
289
+
290
+ if "unknown" in categories:
291
+ details = [
292
+ f"{path} -> unknown" for path, category in classifications.items() if category == "unknown"
293
+ ]
294
+ raise ValueError(
295
+ "Some target paths are not covered by model-routing.yaml.\n" + "\n".join(details)
296
+ )
297
+
298
+ if categories == {"frontend"}:
299
+ return "frontend", [f"{path} -> frontend" for path in paths]
300
+
301
+ if categories == {"backend"}:
302
+ return "backend", [f"{path} -> backend" for path in paths]
303
+
304
+ details = [f"{path} -> {classifications[path]}" for path in paths]
305
+ split_payload = build_split_required_payload(paths, routing)
306
+ raise ValueError(
307
+ "Detected mixed frontend/backend paths. Split the task first.\n"
308
+ + "\n".join(details)
309
+ + "\nSPLIT_PAYLOAD: "
310
+ + json.dumps(split_payload, ensure_ascii=False)
311
+ )
312
+
313
+
314
+ def build_tool_call(args: argparse.Namespace, routing: dict[str, Any]) -> dict[str, Any]:
315
+ payload_inputs = (
316
+ load_checklist_step_payload(args)
317
+ if args.checklist_file
318
+ else load_explicit_task_payload(args)
319
+ )
320
+ target_paths = [normalize_path_text(path) for path in payload_inputs["target_path"]]
321
+
322
+ if not target_paths:
323
+ raise ValueError("At least one --target-path is required.")
324
+
325
+ requested_kind = str(payload_inputs["kind"])
326
+ if requested_kind == "auto":
327
+ kind, route_notes = detect_target_kind(target_paths, routing)
328
+ else:
329
+ kind = requested_kind
330
+ route_notes = [f"{path} -> forced:{kind}" for path in target_paths]
331
+
332
+ if kind == "frontend":
333
+ tool_name = "implement_frontend_task"
334
+ tool_args: dict[str, Any] = {
335
+ "task_id": payload_inputs["task_id"],
336
+ "task_summary": payload_inputs["task_summary"],
337
+ "target_paths": target_paths,
338
+ "constraints": payload_inputs["constraint"],
339
+ "acceptance_criteria": payload_inputs["acceptance"],
340
+ "cd": payload_inputs["cd"],
341
+ "SESSION_ID": payload_inputs["session_id"],
342
+ "sandbox": payload_inputs["gemini_sandbox"],
343
+ "return_all_messages": payload_inputs["return_all_messages"],
344
+ "model": payload_inputs["model"],
345
+ }
346
+ elif kind == "backend":
347
+ tool_name = "implement_backend_task"
348
+ tool_args = {
349
+ "task_id": payload_inputs["task_id"],
350
+ "task_summary": payload_inputs["task_summary"],
351
+ "target_paths": target_paths,
352
+ "constraints": payload_inputs["constraint"],
353
+ "acceptance_criteria": payload_inputs["acceptance"],
354
+ "cd": payload_inputs["cd"],
355
+ "SESSION_ID": payload_inputs["session_id"],
356
+ "sandbox": payload_inputs["codex_sandbox"],
357
+ "return_all_messages": payload_inputs["return_all_messages"],
358
+ "model": payload_inputs["model"],
359
+ }
360
+ if payload_inputs["profile"]:
361
+ tool_args["profile"] = payload_inputs["profile"]
362
+ else:
363
+ raise ValueError(f"Unsupported kind: {kind}")
364
+
365
+ result = {
366
+ "target": kind,
367
+ "tool_name": tool_name,
368
+ "tool_args": tool_args,
369
+ "route_notes": route_notes,
370
+ "routing_file": payload_inputs["routing_file"],
371
+ }
372
+ if payload_inputs["source"] is not None:
373
+ result["source"] = payload_inputs["source"]
374
+ return result
375
+
376
+
377
+ def build_parser() -> argparse.ArgumentParser:
378
+ parser = argparse.ArgumentParser(
379
+ description="Build a standard CodeCGC MCP task payload."
380
+ )
381
+ parser.add_argument(
382
+ "--kind",
383
+ choices=["auto", "frontend", "backend"],
384
+ default="auto",
385
+ help="Task target kind. Use auto to classify from model-routing.yaml.",
386
+ )
387
+ parser.add_argument("--task-id", help="Stable task identifier.")
388
+ parser.add_argument(
389
+ "--task-summary",
390
+ help="Current step or fix summary only.",
391
+ )
392
+ parser.add_argument(
393
+ "--target-path",
394
+ action="append",
395
+ default=[],
396
+ help="Allowed target path. Repeat for multiple files.",
397
+ )
398
+ parser.add_argument(
399
+ "--constraint",
400
+ action="append",
401
+ default=[],
402
+ help="Non-negotiable task constraint. Repeat for multiple lines.",
403
+ )
404
+ parser.add_argument(
405
+ "--acceptance",
406
+ action="append",
407
+ default=[],
408
+ help="Acceptance criterion. Repeat for multiple lines.",
409
+ )
410
+ parser.add_argument(
411
+ "--cd",
412
+ help="Workspace root that the executor should use.",
413
+ )
414
+ parser.add_argument(
415
+ "--checklist-file",
416
+ help="Optional checklist YAML file that contains a step-level codecgc block.",
417
+ )
418
+ parser.add_argument(
419
+ "--step-number",
420
+ type=int,
421
+ help="1-based checklist step number. Requires --checklist-file.",
422
+ )
423
+ parser.add_argument(
424
+ "--routing-file",
425
+ default=str(DEFAULT_ROUTING_FILE),
426
+ help="Path to model-routing.yaml.",
427
+ )
428
+ parser.add_argument(
429
+ "--session-id",
430
+ default=None,
431
+ help="Optional MCP session id for resume.",
432
+ )
433
+ parser.add_argument(
434
+ "--model",
435
+ default=None,
436
+ help="Optional explicit model override.",
437
+ )
438
+ parser.add_argument(
439
+ "--profile",
440
+ default=None,
441
+ help="Optional Codex profile. Backend only.",
442
+ )
443
+ parser.add_argument(
444
+ "--codex-sandbox",
445
+ choices=["read-only", "workspace-write", "danger-full-access"],
446
+ default=None,
447
+ help="Backend sandbox value.",
448
+ )
449
+ parser.add_argument(
450
+ "--gemini-sandbox",
451
+ action=argparse.BooleanOptionalAction,
452
+ default=None,
453
+ help="Frontend sandbox flag.",
454
+ )
455
+ parser.add_argument(
456
+ "--return-all-messages",
457
+ action=argparse.BooleanOptionalAction,
458
+ default=None,
459
+ help="Request full event logs from the executor.",
460
+ )
461
+ return parser
462
+
463
+
464
+ def main() -> int:
465
+ configure_utf8_stdio()
466
+ parser = build_parser()
467
+ args = parser.parse_args()
468
+
469
+ try:
470
+ routing = load_simple_routing_config(Path(args.routing_file))
471
+ payload = build_tool_call(args, routing)
472
+ except Exception as error:
473
+ print_json(
474
+ {
475
+ "success": False,
476
+ "error": str(error),
477
+ },
478
+ file=sys.stderr,
479
+ )
480
+ return 1
481
+
482
+ print_json(payload)
483
+ return 0
484
+
485
+
486
+ if __name__ == "__main__":
487
+ raise SystemExit(main())
@@ -0,0 +1,40 @@
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