@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,4 @@
1
+ """Codex MCP server package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,12 @@
1
+ """Console entry point for the Codex MCP server."""
2
+
3
+ from codexmcp.server import run
4
+
5
+
6
+ def main() -> None:
7
+ """Start the Codex MCP server."""
8
+ run()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,529 @@
1
+ """FastMCP server implementation for the Codex MCP project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import queue
8
+ import subprocess
9
+ import threading
10
+ import time
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Annotated, Any, Dict, Generator, List, Literal, Optional
14
+
15
+ from mcp.server.fastmcp import FastMCP
16
+ from pydantic import BeforeValidator, Field
17
+ import shutil
18
+
19
+ mcp = FastMCP("Codex MCP Server-from guda.studio")
20
+
21
+ # Mirror of model-routing.yaml frontend_paths — keep these hints in sync with
22
+ # route-edit.ps1 and geminimcp/server.py BACKEND_PATH_HINTS.
23
+ FRONTEND_PATH_HINTS = (
24
+ "apps/web/",
25
+ "src/components/",
26
+ "src/pages/",
27
+ "src/app/",
28
+ "src/styles/",
29
+ "src/ui/",
30
+ "components/",
31
+ "pages/",
32
+ "app/",
33
+ "styles/",
34
+ "ui/",
35
+ "web/",
36
+ "frontend/",
37
+ )
38
+
39
+ FRONTEND_FILE_SUFFIXES = (
40
+ ".tsx",
41
+ ".jsx",
42
+ ".css",
43
+ ".scss",
44
+ ".sass",
45
+ ".less",
46
+ ".styl",
47
+ ".html",
48
+ ".vue",
49
+ ".svelte",
50
+ )
51
+
52
+
53
+ def _empty_str_to_none(value: str | None) -> str | None:
54
+ """Convert empty strings to None for optional UUID parameters."""
55
+ if isinstance(value, str) and not value.strip():
56
+ return None
57
+ return value
58
+
59
+
60
+ def _normalize_path_text(path_value: Path | str) -> str:
61
+ """Normalize a path-like value to a forward-slash string."""
62
+ return str(path_value).replace("\\", "/").strip()
63
+
64
+
65
+ def _is_probably_frontend_path(path_value: Path | str) -> bool:
66
+ """Best-effort check to keep backend-only Codex tasks away from frontend files."""
67
+ normalized = _normalize_path_text(path_value).lower().lstrip("./")
68
+ if any(hint in normalized for hint in FRONTEND_PATH_HINTS):
69
+ return True
70
+ suffix = Path(normalized).suffix.lower()
71
+ return suffix in FRONTEND_FILE_SUFFIXES
72
+
73
+
74
+ def _build_backend_task_prompt(
75
+ task_summary: str,
76
+ target_paths: List[Path],
77
+ constraints: List[str],
78
+ acceptance_criteria: List[str],
79
+ ) -> str:
80
+ """Build a constrained prompt for backend implementation tasks."""
81
+ lines = [
82
+ "You are implementing a backend-only coding task.",
83
+ "Stay strictly within the provided target paths.",
84
+ "Do not edit frontend files, styling files, or UI components.",
85
+ "Return a concise implementation summary, followed by risks if any remain.",
86
+ "",
87
+ "Task summary:",
88
+ task_summary.strip(),
89
+ "",
90
+ "Target paths:",
91
+ ]
92
+
93
+ lines.extend(f"- {_normalize_path_text(path)}" for path in target_paths)
94
+
95
+ if constraints:
96
+ lines.append("")
97
+ lines.append("Constraints:")
98
+ lines.extend(f"- {item.strip()}" for item in constraints if item.strip())
99
+
100
+ if acceptance_criteria:
101
+ lines.append("")
102
+ lines.append("Acceptance criteria:")
103
+ lines.extend(f"- {item.strip()}" for item in acceptance_criteria if item.strip())
104
+
105
+ return "\n".join(lines).strip()
106
+
107
+
108
+ def _validate_backend_target_paths(target_paths: List[Path]) -> tuple[bool, List[str], str]:
109
+ """Validate backend task target paths and return policy check notes."""
110
+ if not target_paths:
111
+ return False, [], "The `target_paths` field must contain at least one file or directory."
112
+
113
+ normalized_paths = [_normalize_path_text(path) for path in target_paths]
114
+ frontend_hits = [
115
+ path_text
116
+ for path_text in normalized_paths
117
+ if _is_probably_frontend_path(path_text)
118
+ ]
119
+
120
+ policy_checks = [
121
+ "target_paths_present",
122
+ "backend_scope_requested",
123
+ ]
124
+
125
+ if frontend_hits:
126
+ return (
127
+ False,
128
+ policy_checks,
129
+ "The backend executor refused frontend-like paths: "
130
+ + ", ".join(frontend_hits),
131
+ )
132
+
133
+ policy_checks.append("frontend_boundary_check_passed")
134
+ return True, policy_checks, ""
135
+
136
+
137
+ def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
138
+ """Execute a command and stream its output line-by-line.
139
+
140
+ Args:
141
+ cmd: Command and arguments as a list (e.g., ["codex", "exec", "prompt"])
142
+
143
+ Yields:
144
+ Output lines from the command
145
+ """
146
+ # On Windows, codex is exposed via a *.cmd shim. Use cmd.exe with /s so
147
+ # user prompts containing quotes/newlines aren't reinterpreted as shell syntax.
148
+ popen_cmd = cmd.copy()
149
+ codex_path = shutil.which('codex') or cmd[0]
150
+ popen_cmd[0] = codex_path
151
+
152
+ process = subprocess.Popen(
153
+ popen_cmd,
154
+ shell=False,
155
+ stdin=subprocess.DEVNULL,
156
+ stdout=subprocess.PIPE,
157
+ stderr=subprocess.STDOUT,
158
+ universal_newlines=True,
159
+ encoding='utf-8',
160
+ )
161
+
162
+ output_queue: queue.Queue[str | None] = queue.Queue()
163
+ GRACEFUL_SHUTDOWN_DELAY = 0.3
164
+
165
+ def is_turn_completed(line: str) -> bool:
166
+ """Check if the line indicates turn completion via JSON parsing."""
167
+ try:
168
+ data = json.loads(line)
169
+ return data.get("type") == "turn.completed"
170
+ except (json.JSONDecodeError, AttributeError, TypeError):
171
+ return False
172
+
173
+ def read_output() -> None:
174
+ """Read process output in a separate thread."""
175
+ if process.stdout:
176
+ for line in iter(process.stdout.readline, ""):
177
+ stripped = line.strip()
178
+ output_queue.put(stripped)
179
+ if is_turn_completed(stripped):
180
+ time.sleep(GRACEFUL_SHUTDOWN_DELAY)
181
+ process.terminate()
182
+ break
183
+ process.stdout.close()
184
+ output_queue.put(None)
185
+
186
+ thread = threading.Thread(target=read_output)
187
+ thread.start()
188
+
189
+ # Yield lines while process is running
190
+ while True:
191
+ try:
192
+ line = output_queue.get(timeout=0.5)
193
+ if line is None:
194
+ break
195
+ yield line
196
+ except queue.Empty:
197
+ if process.poll() is not None and not thread.is_alive():
198
+ break
199
+
200
+ try:
201
+ process.wait(timeout=5)
202
+ except subprocess.TimeoutExpired:
203
+ process.kill()
204
+ process.wait()
205
+ thread.join(timeout=5)
206
+
207
+ while not output_queue.empty():
208
+ try:
209
+ line = output_queue.get_nowait()
210
+ if line is not None:
211
+ yield line
212
+ except queue.Empty:
213
+ break
214
+
215
+
216
+ def _execute_codex_session(
217
+ *,
218
+ prompt: str,
219
+ cd: Path,
220
+ sandbox: Literal["read-only", "workspace-write", "danger-full-access"],
221
+ session_id: str,
222
+ skip_git_repo_check: bool,
223
+ return_all_messages: bool,
224
+ image: List[Path],
225
+ model: str,
226
+ yolo: bool,
227
+ profile: str,
228
+ ) -> Dict[str, Any]:
229
+ """Execute Codex CLI and return the parsed MCP response payload."""
230
+ if not cd.exists():
231
+ return {
232
+ "success": False,
233
+ "error": f"The workspace root directory `{cd.absolute().as_posix()}` does not exist.",
234
+ }
235
+
236
+ cmd = ["codex", "exec", "--sandbox", sandbox, "--cd", str(cd), "--json"]
237
+
238
+ if len(image):
239
+ cmd.extend(["--image", ",".join(image)])
240
+
241
+ if model:
242
+ cmd.extend(["--model", model])
243
+
244
+ if profile:
245
+ cmd.extend(["--profile", profile])
246
+
247
+ if yolo:
248
+ cmd.append("--yolo")
249
+
250
+ if skip_git_repo_check:
251
+ cmd.append("--skip-git-repo-check")
252
+
253
+ if session_id:
254
+ cmd.extend(["resume", str(session_id)])
255
+
256
+ if os.name == "nt":
257
+ prompt = windows_escape(prompt)
258
+ cmd += ["--", prompt]
259
+
260
+ all_messages: list[Dict[str, Any]] = []
261
+ agent_messages = ""
262
+ success = True
263
+ err_message = ""
264
+ thread_id: Optional[str] = None
265
+
266
+ for line in run_shell_command(cmd):
267
+ try:
268
+ line_dict = json.loads(line.strip())
269
+ all_messages.append(line_dict)
270
+ item = line_dict.get("item", {})
271
+ item_type = item.get("type", "")
272
+ if item_type == "agent_message":
273
+ agent_messages = agent_messages + item.get("text", "")
274
+ if line_dict.get("thread_id") is not None:
275
+ thread_id = line_dict.get("thread_id")
276
+ if "fail" in line_dict.get("type", ""):
277
+ success = False if len(agent_messages) == 0 else success
278
+ err_message += "\n\n[codex error] " + line_dict.get("error", {}).get("message", "")
279
+ if "error" in line_dict.get("type", ""):
280
+ error_msg = line_dict.get("message", "")
281
+ import re
282
+
283
+ is_reconnecting = bool(re.match(r"^Reconnecting\.\.\.\s+\d+/\d+", error_msg))
284
+ if not is_reconnecting:
285
+ success = False if len(agent_messages) == 0 else success
286
+ err_message += "\n\n[codex error] " + error_msg
287
+
288
+ except json.JSONDecodeError:
289
+ err_message += "\n\n[json decode error] " + line
290
+ continue
291
+ except Exception as error:
292
+ err_message += "\n\n[unexpected error] " + f"Unexpected error: {error}. Line: {line!r}"
293
+ success = False
294
+ break
295
+
296
+ if thread_id is None:
297
+ success = False
298
+ err_message = "Failed to get `SESSION_ID` from the codex session. \n\n" + err_message
299
+
300
+ if len(agent_messages) == 0:
301
+ success = False
302
+ err_message = (
303
+ "Failed to get `agent_messages` from the codex session. "
304
+ "\n\n You can try to set `return_all_messages` to `True` to get the full reasoning information. "
305
+ + err_message
306
+ )
307
+
308
+ if success:
309
+ result: Dict[str, Any] = {
310
+ "success": True,
311
+ "SESSION_ID": thread_id,
312
+ "agent_messages": agent_messages,
313
+ }
314
+ else:
315
+ result = {"success": False, "error": err_message}
316
+
317
+ if return_all_messages:
318
+ result["all_messages"] = all_messages
319
+
320
+ return result
321
+
322
+ def windows_escape(prompt):
323
+ """
324
+ Windows 风格的字符串转义函数。
325
+ 把常见特殊字符转义成 \\ 形式,适合命令行、JSON 或路径使用。
326
+ 比如:\n 变成 \\n," 变成 \\"。
327
+ """
328
+ # 先处理反斜杠,避免它干扰其他替换
329
+ result = prompt.replace('\\', '\\\\')
330
+ # 双引号,转义成 \",防止字符串边界乱套
331
+ result = result.replace('"', '\\"')
332
+ # 换行符,Windows 常用 \r\n,但我们分开转义
333
+ result = result.replace('\n', '\\n')
334
+ result = result.replace('\r', '\\r')
335
+ # 制表符,空格的“超级版”
336
+ result = result.replace('\t', '\\t')
337
+ # 其他常见:退格符(像按了后退键)、换页符(打印机跳页用)
338
+ result = result.replace('\b', '\\b')
339
+ result = result.replace('\f', '\\f')
340
+ # 如果有单引号,也转义下(不过 Windows 命令行不那么严格,但保险起见)
341
+ result = result.replace("'", "\\'")
342
+
343
+ return result
344
+
345
+ @mcp.tool(
346
+ name="codex",
347
+ description="""
348
+ Executes a non-interactive Codex session via CLI to perform AI-assisted coding tasks in a secure workspace.
349
+ This tool wraps the `codex exec` command, enabling model-driven code generation, debugging, or automation based on natural language prompts.
350
+ It supports resuming ongoing sessions for continuity and enforces sandbox policies to prevent unsafe operations. Ideal for integrating Codex into MCP servers for agentic workflows, such as code reviews or repo modifications.
351
+
352
+ **Key Features:**
353
+ - **Prompt-Driven Execution:** Send task instructions to Codex for step-by-step code handling.
354
+ - **Workspace Isolation:** Operate within a specified directory, with optional Git repo skipping.
355
+ - **Security Controls:** Three sandbox levels balance functionality and safety.
356
+ - **Session Persistence:** Resume prior conversations via `SESSION_ID` for iterative tasks.
357
+
358
+ **Edge Cases & Best Practices:**
359
+ - Ensure `cd` exists and is accessible; tool fails silently on invalid paths.
360
+ - For most repos, prefer "read-only" to avoid accidental changes.
361
+ - If needed, set `return_all_messages` to `True` to parse "all_messages" for detailed tracing (e.g., reasoning, tool calls, etc.).
362
+ """,
363
+ meta={"version": "0.0.0", "author": "guda.studio"},
364
+ )
365
+ async def codex(
366
+ PROMPT: Annotated[str, "Instruction for the task to send to codex."],
367
+ cd: Annotated[Path, "Set the workspace root for codex before executing the task."],
368
+ sandbox: Annotated[
369
+ Literal["read-only", "workspace-write", "danger-full-access"],
370
+ Field(
371
+ description="Sandbox policy for model-generated commands. Defaults to `read-only`."
372
+ ),
373
+ ] = "read-only",
374
+ SESSION_ID: Annotated[
375
+ str,
376
+ "Resume the specified session of the codex. Defaults to `None`, start a new session.",
377
+ ] = "",
378
+ skip_git_repo_check: Annotated[
379
+ bool,
380
+ "Allow codex running outside a Git repository (useful for one-off directories).",
381
+ ] = True,
382
+ return_all_messages: Annotated[
383
+ bool,
384
+ "Return all messages (e.g. reasoning, tool calls, etc.) from the codex session. Set to `False` by default, only the agent's final reply message is returned.",
385
+ ] = False,
386
+ image: Annotated[
387
+ List[Path],
388
+ Field(
389
+ description="Attach one or more image files to the initial prompt. Separate multiple paths with commas or repeat the flag.",
390
+ ),
391
+ ] = [],
392
+ model: Annotated[
393
+ str,
394
+ Field(
395
+ description="The model to use for the codex session. This parameter is strictly prohibited unless explicitly specified by the user.",
396
+ ),
397
+ ] = "",
398
+ yolo: Annotated[
399
+ bool,
400
+ Field(
401
+ description="Run every command without approvals or sandboxing. Only use when `sandbox` couldn't be applied.",
402
+ ),
403
+ ] = False,
404
+ profile: Annotated[
405
+ str,
406
+ "Configuration profile name to load from `~/.codex/config.toml`. This parameter is strictly prohibited unless explicitly specified by the user.",
407
+ ] = "",
408
+ ) -> Dict[str, Any]:
409
+ """Execute a Codex CLI session and return the results."""
410
+ return _execute_codex_session(
411
+ prompt=PROMPT,
412
+ cd=cd,
413
+ sandbox=sandbox,
414
+ session_id=SESSION_ID,
415
+ skip_git_repo_check=skip_git_repo_check,
416
+ return_all_messages=return_all_messages,
417
+ image=image,
418
+ model=model,
419
+ yolo=yolo,
420
+ profile=profile,
421
+ )
422
+
423
+
424
+ @mcp.tool(
425
+ name="implement_backend_task",
426
+ description="""
427
+ Executes a backend-only implementation task via Codex with extra policy checks.
428
+ Use this tool when Claude has already completed planning/design and needs Codex to
429
+ perform the actual backend code changes inside a constrained path set.
430
+ """,
431
+ meta={"version": "0.1.0", "author": "CodeCGC"},
432
+ )
433
+ async def implement_backend_task(
434
+ task_id: Annotated[str, "Stable task identifier for audit and review."],
435
+ task_summary: Annotated[str, "Backend implementation summary prepared by the orchestrator."],
436
+ target_paths: Annotated[
437
+ List[Path],
438
+ Field(
439
+ description="Backend paths Codex is allowed to touch for this task.",
440
+ ),
441
+ ],
442
+ constraints: Annotated[
443
+ List[str],
444
+ Field(
445
+ description="Non-negotiable implementation constraints for this backend task.",
446
+ ),
447
+ ] = [],
448
+ acceptance_criteria: Annotated[
449
+ List[str],
450
+ Field(
451
+ description="Acceptance criteria the implementation should satisfy.",
452
+ ),
453
+ ] = [],
454
+ cd: Annotated[Path, "Workspace root for the backend task." ] = Path("."),
455
+ SESSION_ID: Annotated[
456
+ str,
457
+ "Resume the specified Codex session. Empty string starts a new session.",
458
+ ] = "",
459
+ sandbox: Annotated[
460
+ Literal["read-only", "workspace-write", "danger-full-access"],
461
+ Field(
462
+ description="Sandbox policy for the backend task. Defaults to `workspace-write`."
463
+ ),
464
+ ] = "workspace-write",
465
+ return_all_messages: Annotated[
466
+ bool,
467
+ "Return full Codex event logs for debugging. Defaults to `False`.",
468
+ ] = False,
469
+ model: Annotated[
470
+ str,
471
+ Field(
472
+ description="Optional model override. Only use when explicitly requested by the user.",
473
+ ),
474
+ ] = "",
475
+ profile: Annotated[
476
+ str,
477
+ "Optional Codex profile from `~/.codex/config.toml`.",
478
+ ] = "",
479
+ ) -> Dict[str, Any]:
480
+ """Execute a backend-only Codex task with CodeCGC policy checks."""
481
+ valid, policy_checks, validation_error = _validate_backend_target_paths(target_paths)
482
+ if not valid:
483
+ return {
484
+ "success": False,
485
+ "task_id": task_id,
486
+ "policy_checks": policy_checks,
487
+ "error": validation_error,
488
+ }
489
+
490
+ prompt = _build_backend_task_prompt(
491
+ task_summary=task_summary,
492
+ target_paths=target_paths,
493
+ constraints=constraints,
494
+ acceptance_criteria=acceptance_criteria,
495
+ )
496
+ result = _execute_codex_session(
497
+ prompt=prompt,
498
+ cd=cd,
499
+ sandbox=sandbox,
500
+ session_id=SESSION_ID,
501
+ skip_git_repo_check=True,
502
+ return_all_messages=return_all_messages,
503
+ image=[],
504
+ model=model,
505
+ yolo=False,
506
+ profile=profile,
507
+ )
508
+
509
+ if not result.get("success"):
510
+ result["task_id"] = task_id
511
+ result["policy_checks"] = policy_checks
512
+ return result
513
+
514
+ return {
515
+ "success": True,
516
+ "task_id": task_id,
517
+ "SESSION_ID": result["SESSION_ID"],
518
+ "summary": result["agent_messages"],
519
+ "agent_messages": result["agent_messages"],
520
+ "changed_files": [_normalize_path_text(path) for path in target_paths],
521
+ "policy_checks": policy_checks + ["backend_executor_completed"],
522
+ "risks": [],
523
+ **({"all_messages": result["all_messages"]} if return_all_messages and "all_messages" in result else {}),
524
+ }
525
+
526
+
527
+ def run() -> None:
528
+ """Start the MCP server over stdio transport."""
529
+ mcp.run(transport="stdio")