@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,149 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def render_bullet_list(items: list[str], indent: str, fallback: str) -> str:
5
+ values = [item.strip() for item in items if item.strip()]
6
+ if not values:
7
+ return f"{indent}- {fallback}"
8
+ return "\n".join(f"{indent}- {item}" for item in values)
9
+
10
+
11
+ def render_overview(
12
+ *,
13
+ initiative: str,
14
+ summary: str,
15
+ user_story: str,
16
+ goal: str,
17
+ context: list[str],
18
+ scope: list[str],
19
+ risks: list[str],
20
+ reasons: list[str],
21
+ artifact_class: str,
22
+ ) -> str:
23
+ return f"""---
24
+ doc_type: roadmap-overview
25
+ artifact_class: {artifact_class}
26
+ initiative: {initiative}
27
+ status: draft
28
+ summary: {summary}
29
+ tags: []
30
+ ---
31
+
32
+ # {summary}
33
+
34
+ ## 1. 目标
35
+
36
+ - 摘要: {summary}
37
+ - 目标: {goal or '待补充'}
38
+ - 用户故事或操作者诉求: {user_story or '待补充'}
39
+
40
+ ## 2. 背景
41
+
42
+ {render_bullet_list(context, '', '待补充')}
43
+
44
+ ## 3. 为什么需要走 Roadmap
45
+
46
+ {render_bullet_list(reasons, '', '待补充')}
47
+
48
+ ## 4. 范围
49
+
50
+ {render_bullet_list(scope, '', '待补充')}
51
+
52
+ ## 5. 风险
53
+
54
+ {render_bullet_list(risks, '', '待补充')}
55
+ """
56
+
57
+
58
+ def render_phases(
59
+ *,
60
+ initiative: str,
61
+ grouped_paths: dict[str, list[str]],
62
+ artifact_class: str,
63
+ ) -> str:
64
+ frontend_paths = grouped_paths.get("frontend", [])
65
+ backend_paths = grouped_paths.get("backend", [])
66
+ shared_paths = grouped_paths.get("shared", [])
67
+ unknown_paths = grouped_paths.get("unknown", [])
68
+ return f"""---
69
+ doc_type: roadmap-phases
70
+ artifact_class: {artifact_class}
71
+ initiative: {initiative}
72
+ status: draft
73
+ tags: []
74
+ ---
75
+
76
+ # {initiative} 阶段拆分
77
+
78
+ ## 1. 阶段说明
79
+
80
+ - 第 1 阶段:澄清范围、依赖和成功标准
81
+ - 第 2 阶段:把 initiative 拆成可执行的 feature 或 issue track
82
+ - 第 3 阶段:按正常 CodeCGC 流程交付前端和后端 track
83
+
84
+ ## 2. 候选前端 Track
85
+
86
+ {render_bullet_list(frontend_paths, '', '暂未识别。')}
87
+
88
+ ## 3. 候选后端 Track
89
+
90
+ {render_bullet_list(backend_paths, '', '暂未识别。')}
91
+
92
+ ## 4. 共享或未知范围
93
+
94
+ 共享范围:
95
+ {render_bullet_list(shared_paths, ' ', '无。')}
96
+
97
+ 未知范围:
98
+ {render_bullet_list(unknown_paths, ' ', '无。')}
99
+
100
+ ## 5. 已初始化的子工作流
101
+
102
+ - 暂无。
103
+ """
104
+
105
+
106
+ def render_delivery_plan(
107
+ *,
108
+ initiative: str,
109
+ dependencies: list[str],
110
+ assumptions: list[str],
111
+ validation: list[str],
112
+ rollback: list[str],
113
+ open_questions: list[str],
114
+ artifact_class: str,
115
+ ) -> str:
116
+ return f"""---
117
+ doc_type: roadmap-delivery-plan
118
+ artifact_class: {artifact_class}
119
+ initiative: {initiative}
120
+ status: draft
121
+ tags: []
122
+ ---
123
+
124
+ # {initiative} 交付计划
125
+
126
+ ## 1. 依赖
127
+
128
+ {render_bullet_list(dependencies, '', '待补充')}
129
+
130
+ ## 2. 前提假设
131
+
132
+ {render_bullet_list(assumptions, '', '待补充')}
133
+
134
+ ## 3. 验证策略
135
+
136
+ {render_bullet_list(validation, '', '待补充')}
137
+
138
+ ## 4. 回退策略
139
+
140
+ {render_bullet_list(rollback, '', '待补充')}
141
+
142
+ ## 5. 开放问题
143
+
144
+ {render_bullet_list(open_questions, '', '当前无。')}
145
+
146
+ ## 6. 工作流跟踪
147
+
148
+ - 目前还没有登记子工作流。
149
+ """
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,135 @@
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
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def resolve_workspace_root(override_workspace: str = "") -> Path:
11
+ explicit = str(override_workspace or "").strip()
12
+ if explicit:
13
+ return Path(explicit).expanduser().resolve()
14
+
15
+ configured = os.environ.get("CODECGC_WORKSPACE_ROOT", "").strip()
16
+ if configured:
17
+ return Path(configured).expanduser().resolve()
18
+
19
+ return Path.cwd().resolve()
20
+
21
+
22
+ PROJECT_ROOT = resolve_workspace_root()
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from codecgc_artifact_roots import execution_root
8
+
9
+
10
+ def load_audit_json(path: Path) -> dict[str, Any] | None:
11
+ if not path.exists():
12
+ return None
13
+ try:
14
+ data = json.loads(path.read_text(encoding="utf-8"))
15
+ except Exception:
16
+ return None
17
+ return data if isinstance(data, dict) else None
18
+
19
+
20
+ def extract_reusable_session_id(audit: dict[str, Any] | None) -> str:
21
+ if not isinstance(audit, dict):
22
+ return ""
23
+
24
+ result = audit.get("result", {})
25
+ if isinstance(result, dict):
26
+ session_id = str(result.get("session_id", "")).strip()
27
+ if session_id:
28
+ return session_id
29
+
30
+ return str(audit.get("requested_session_id", "")).strip()
31
+
32
+
33
+ def resolve_task_audit_path(task_id: str, artifact_class: str) -> Path | None:
34
+ cleaned_task_id = str(task_id).strip()
35
+ if not cleaned_task_id:
36
+ return None
37
+ return execution_root(artifact_class) / f"{cleaned_task_id}.json"
38
+
39
+
40
+ def resolve_session_id_from_task(task_id: str, artifact_class: str) -> str:
41
+ audit_path = resolve_task_audit_path(task_id, artifact_class)
42
+ if audit_path is None:
43
+ return ""
44
+ return extract_reusable_session_id(load_audit_json(audit_path))
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from build_codecgc_task import load_checklist_yaml
7
+
8
+
9
+ def resolve_artifact_type(data: dict[str, Any]) -> str:
10
+ if data.get("feature"):
11
+ return "feature"
12
+ if data.get("issue"):
13
+ return "issue"
14
+ return "checklist"
15
+
16
+
17
+ def is_placeholder_path(path_text: str) -> bool:
18
+ normalized = str(path_text or "").strip().replace("\\", "/").lower()
19
+ return not normalized or normalized == "todo/path" or normalized.startswith("todo/")
20
+
21
+
22
+ def is_executable_codecgc_block(codecgc: Any) -> bool:
23
+ if not isinstance(codecgc, dict):
24
+ return False
25
+
26
+ kind = str(codecgc.get("kind", "")).strip().lower()
27
+ if kind not in {"frontend", "backend"}:
28
+ return False
29
+
30
+ target_paths = codecgc.get("target_paths", [])
31
+ if not isinstance(target_paths, list) or not target_paths:
32
+ return False
33
+
34
+ if any(is_placeholder_path(path) for path in target_paths):
35
+ return False
36
+
37
+ task_id = str(codecgc.get("task_id", "")).strip()
38
+ task_summary = str(codecgc.get("task_summary", "")).strip()
39
+ return bool(task_id and task_summary)
40
+
41
+
42
+ def is_test_codecgc_block(codecgc: Any) -> bool:
43
+ if not is_executable_codecgc_block(codecgc):
44
+ return False
45
+ return str(codecgc.get("step_type", "")).strip().lower() == "test"
46
+
47
+
48
+ def select_next_executable_step(checklist_path: Path) -> dict[str, Any]:
49
+ data = load_checklist_yaml(checklist_path)
50
+ steps = data.get("steps", [])
51
+ if not isinstance(steps, list) or not steps:
52
+ raise ValueError("Checklist does not contain any steps.")
53
+
54
+ first_planning_index: int | None = None
55
+ first_executable_index: int | None = None
56
+
57
+ for index, step in enumerate(steps, start=1):
58
+ if not isinstance(step, dict):
59
+ continue
60
+ status = str(step.get("status", "pending")).strip().lower()
61
+ if status not in {"pending", ""}:
62
+ continue
63
+
64
+ codecgc = step.get("codecgc")
65
+ if is_executable_codecgc_block(codecgc):
66
+ if first_executable_index is None:
67
+ first_executable_index = index
68
+ continue
69
+
70
+ if first_planning_index is None:
71
+ first_planning_index = index
72
+
73
+ if first_planning_index is not None:
74
+ step = steps[first_planning_index - 1]
75
+ raise ValueError(
76
+ f"Planning-only step {first_planning_index} must be resolved before execution: "
77
+ f"{step.get('action', 'unknown action')}"
78
+ )
79
+
80
+ if first_executable_index is None:
81
+ raise ValueError("No pending executable step remains in this checklist.")
82
+
83
+ step = steps[first_executable_index - 1]
84
+ codecgc = step.get("codecgc", {})
85
+ return {
86
+ "step_number": first_executable_index,
87
+ "task_id": str(codecgc.get("task_id", "")),
88
+ "action": str(step.get("action", "")),
89
+ "kind": str(codecgc.get("kind", "")),
90
+ "target_paths": codecgc.get("target_paths", []),
91
+ "task_summary": str(codecgc.get("task_summary", "")),
92
+ "artifact_type": resolve_artifact_type(data),
93
+ "timeout_seconds": int(codecgc.get("timeout_seconds", 0)) or 0,
94
+ }
95
+
96
+
97
+ def get_step_metadata(checklist_path: Path, step_number: int) -> dict[str, Any]:
98
+ data = load_checklist_yaml(checklist_path)
99
+ steps = data.get("steps", [])
100
+ if not isinstance(steps, list) or step_number < 1 or step_number > len(steps):
101
+ raise ValueError(f"Step number must be between 1 and {len(steps)}.")
102
+ step = steps[step_number - 1]
103
+ codecgc = step.get("codecgc", {}) if isinstance(step, dict) else {}
104
+ executable = is_executable_codecgc_block(codecgc)
105
+ return {
106
+ "step_number": step_number,
107
+ "task_id": str(codecgc.get("task_id", "")),
108
+ "action": str(step.get("action", "")) if isinstance(step, dict) else "",
109
+ "kind": str(codecgc.get("kind", "")),
110
+ "target_paths": codecgc.get("target_paths", []),
111
+ "task_summary": str(codecgc.get("task_summary", "")),
112
+ "artifact_type": resolve_artifact_type(data),
113
+ "executable": executable,
114
+ "timeout_seconds": int(codecgc.get("timeout_seconds", 0)) or 0,
115
+ }
116
+
117
+
118
+ def replace_step_status(text: str, step_number: int, new_status: str) -> str:
119
+ lines = text.splitlines()
120
+ current_step = 0
121
+ inside_steps = False
122
+ current_step_indent = ""
123
+
124
+ for index, line in enumerate(lines):
125
+ stripped = line.strip()
126
+ if stripped == "steps:":
127
+ inside_steps = True
128
+ continue
129
+
130
+ if inside_steps and not line.startswith(" "):
131
+ break
132
+
133
+ if inside_steps and line.startswith(" - "):
134
+ current_step += 1
135
+ current_step_indent = line[: len(line) - len(line.lstrip(" "))]
136
+ continue
137
+
138
+ if (
139
+ inside_steps
140
+ and current_step == step_number
141
+ and stripped.startswith("status:")
142
+ and line.startswith(f"{current_step_indent} ")
143
+ ):
144
+ indent = line[: len(line) - len(line.lstrip(" "))]
145
+ lines[index] = f"{indent}status: {new_status}"
146
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
147
+
148
+ raise ValueError(f"Could not find status field for step {step_number}.")
149
+
150
+
151
+ def update_step_status(checklist_path: Path, step_number: int, new_status: str) -> None:
152
+ original = checklist_path.read_text(encoding="utf-8")
153
+ updated = replace_step_status(original, step_number, new_status)
154
+ checklist_path.write_text(updated, encoding="utf-8")
@@ -0,0 +1,63 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from codecgc_runtime_paths import PACKAGE_ROOT
8
+ from codecgc_runtime_paths import PROJECT_ROOT
9
+
10
+ WORKSPACE = PACKAGE_ROOT
11
+ PROJECT_WORKSPACE = PROJECT_ROOT
12
+ SCRIPTS_DIR = WORKSPACE / "scripts"
13
+
14
+
15
+ def build_script_command(script_name: str, *args: str) -> list[str]:
16
+ return [sys.executable, str(SCRIPTS_DIR / script_name), *args]
17
+
18
+
19
+ def parse_json_text(text: str) -> dict[str, Any]:
20
+ stripped = text.strip()
21
+ if not stripped:
22
+ raise ValueError("Command produced no JSON output.")
23
+ return json.loads(stripped)
24
+
25
+
26
+ def run_json_script(script_name: str, *args: str) -> dict[str, Any]:
27
+ command = build_script_command(script_name, *args)
28
+ completed = subprocess.run(
29
+ command,
30
+ cwd=PROJECT_WORKSPACE,
31
+ capture_output=True,
32
+ text=True,
33
+ encoding="utf-8",
34
+ errors="replace",
35
+ )
36
+
37
+ stdout = completed.stdout.strip()
38
+ stderr = completed.stderr.strip()
39
+
40
+ if completed.returncode == 0:
41
+ if not stdout:
42
+ return {"success": True}
43
+ return parse_json_text(stdout)
44
+
45
+ for candidate in (stdout, stderr):
46
+ if not candidate:
47
+ continue
48
+ try:
49
+ parsed = parse_json_text(candidate)
50
+ except Exception:
51
+ continue
52
+ if isinstance(parsed, dict):
53
+ parsed.setdefault("success", False)
54
+ parsed.setdefault("returncode", completed.returncode)
55
+ return parsed
56
+
57
+ return {
58
+ "success": False,
59
+ "returncode": completed.returncode,
60
+ "stdout": stdout,
61
+ "stderr": stderr,
62
+ "error": f"{script_name} failed with exit code {completed.returncode}.",
63
+ }