@hunyed15/codecgc 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude/hooks/route-edit.ps1 +58 -57
  2. package/INSTALLATION.md +117 -484
  3. package/README.md +118 -150
  4. package/bin/cgc-external-status.js +4 -0
  5. package/bin/cgc-start.js +4 -0
  6. package/bin/codecgc.js +141 -20
  7. package/codecgc/compound/codecgc-capability-matrix.md +37 -0
  8. package/codecgc/reference/README.md +69 -0
  9. package/codecgc/reference/maintainer-guide.md +93 -0
  10. package/codecgc/reference/mcp-tool-surface.md +112 -0
  11. package/codecgc/reference/onboarding.md +69 -0
  12. package/codecgc/reference/operation-guide.md +29 -23
  13. package/codecgc/reference/path-contract.md +53 -0
  14. package/codecgc/reference/policy-routing.md +57 -0
  15. package/codecgc/reference/project-structure.md +80 -0
  16. package/codecgc/reference/quickstart.md +108 -0
  17. package/codecgc/reference/real-workflow-loop.md +123 -0
  18. package/codecgc/reference/recovery-loop.md +109 -0
  19. package/codecgc/reference/release-maintenance-playbook.md +4 -1
  20. package/codecgc/reference/troubleshooting.md +112 -0
  21. package/codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md +49 -0
  22. package/codecgc/roadmap/codecgc-release-maintenance/overview.md +41 -0
  23. package/codecgc/roadmap/codecgc-release-maintenance/phases.md +84 -0
  24. package/codecgcmcp/README.md +57 -11
  25. package/codecgcmcp/src/codecgcmcp/server.py +164 -26
  26. package/model-routing.yaml +31 -6
  27. package/package.json +11 -4
  28. package/scripts/audit_codecgc_external_capabilities.py +83 -4
  29. package/scripts/audit_codecgc_historical_audits.py +42 -2
  30. package/scripts/audit_codecgc_package_runtime.py +73 -4
  31. package/scripts/audit_codecgc_release_readiness.py +55 -3
  32. package/scripts/audit_codecgc_workflow_history.py +8 -5
  33. package/scripts/build_codecgc_task.py +62 -45
  34. package/scripts/codecgc_artifact_roots.py +8 -40
  35. package/scripts/codecgc_console_io.py +3 -45
  36. package/scripts/codecgc_executor_registry.py +4 -54
  37. package/scripts/codecgc_path_contract.py +7 -0
  38. package/scripts/codecgc_policy.py +275 -0
  39. package/scripts/codecgc_routing_paths.py +3 -16
  40. package/scripts/codecgc_routing_template.py +11 -135
  41. package/scripts/codecgc_runtime/__init__.py +1 -0
  42. package/scripts/codecgc_runtime/artifacts.py +42 -0
  43. package/scripts/codecgc_runtime/console.py +45 -0
  44. package/scripts/codecgc_runtime/executor_registry.py +55 -0
  45. package/scripts/codecgc_runtime/mcp_config.py +72 -0
  46. package/scripts/codecgc_runtime/path_contract.py +123 -0
  47. package/scripts/codecgc_runtime/paths.py +22 -0
  48. package/scripts/codecgc_runtime/routing_paths.py +16 -0
  49. package/scripts/codecgc_runtime/routing_template.py +169 -0
  50. package/scripts/codecgc_runtime/workflow_runtime.py +72 -0
  51. package/scripts/codecgc_runtime_paths.py +3 -22
  52. package/scripts/codecgc_step_control.py +3 -2
  53. package/scripts/codecgc_workflow_runtime.py +3 -71
  54. package/scripts/entry_codecgc_workflow.py +4 -0
  55. package/scripts/install_codecgc.py +490 -21
  56. package/scripts/normalize_codecgc_audits.py +5 -3
  57. package/scripts/postinstall_codecgc.js +6 -56
  58. package/scripts/review_codecgc_workflow.py +6 -3
  59. package/scripts/route_codecgc_workflow.py +67 -36
  60. package/scripts/run_codecgc_build.py +28 -0
  61. package/scripts/run_codecgc_fix.py +28 -0
  62. package/scripts/run_codecgc_task.py +5 -2
  63. package/scripts/run_codecgc_test.py +28 -0
  64. package/scripts/sync_codecgc_mcp_config.py +4 -54
  65. package/scripts/write_codecgc_review.py +7 -3
@@ -10,6 +10,7 @@ from typing import Any
10
10
  from codecgc_console_io import render_summary_block
11
11
  from codecgc_executor_registry import build_executor_registry
12
12
  from codecgc_executor_registry import resolve_python_command
13
+ from codecgc_policy import load_policy
13
14
  from codecgc_routing_template import sync_workspace_routing_file
14
15
  from codecgc_runtime_paths import PACKAGE_ROOT
15
16
  from codecgc_runtime_paths import resolve_workspace_root
@@ -24,12 +25,16 @@ SETTINGS_PATH = CLAUDE_DIR / "settings.json"
24
25
  MCP_CONFIG_PATH = WORKSPACE / ".mcp.json"
25
26
  PROJECT_HOOK_PATH = HOOKS_DIR / "route-edit.ps1"
26
27
  PROJECT_ROUTING_PATH = WORKSPACE / "model-routing.yaml"
28
+ EDIT_GUARDRAIL_MATCHER = "Edit|Write|MultiEdit"
29
+ LEGACY_EDIT_GUARDRAIL_MATCHERS = {"Edit|Write"}
30
+ PROJECT_ONBOARDING_RELATIVE_PATH = "codecgc/START_HERE.md"
31
+ PROJECT_ONBOARDING_MARKER = "<!-- codecgc:onboarding:v1 -->"
27
32
 
28
33
 
29
34
  DEFAULT_HOOKS = {
30
35
  "PreToolUse": [
31
36
  {
32
- "matcher": "Edit|Write",
37
+ "matcher": EDIT_GUARDRAIL_MATCHER,
33
38
  "hooks": [
34
39
  {
35
40
  "type": "command",
@@ -48,6 +53,22 @@ DEFAULT_ALLOWED_TOOLS = [
48
53
  "mcp__gemini__*",
49
54
  ]
50
55
 
56
+ PROJECT_WORKFLOW_DIRS = [
57
+ "codecgc/features",
58
+ "codecgc/issues",
59
+ "codecgc/execution",
60
+ "codecgc/requirements",
61
+ "codecgc/architecture",
62
+ "codecgc/roadmap",
63
+ "codecgc/compound",
64
+ "codecgc/docs",
65
+ "codecgc/reference",
66
+ "codecgc/fixtures/features",
67
+ "codecgc/fixtures/issues",
68
+ "codecgc/fixtures/execution",
69
+ "codecgc/fixtures/roadmap",
70
+ ]
71
+
51
72
 
52
73
  def get_user_claude_root(override_root: str = "") -> Path:
53
74
  override = override_root.strip() or os.environ.get("CODECGC_USER_CLAUDE_DIR", "").strip()
@@ -86,6 +107,7 @@ def get_workspace_paths(override_workspace: str = "") -> dict[str, Path]:
86
107
  "hook_script": hooks_dir / "route-edit.ps1",
87
108
  "commands_dir": claude_dir / "commands",
88
109
  "routing_file": root / "model-routing.yaml",
110
+ "onboarding_file": root / PROJECT_ONBOARDING_RELATIVE_PATH,
89
111
  }
90
112
 
91
113
 
@@ -101,11 +123,122 @@ def load_text_file(path: Path) -> str:
101
123
  return path.read_text(encoding="utf-8")
102
124
 
103
125
 
126
+ def ensure_workspace_workflow_dirs(workspace_root: Path) -> list[str]:
127
+ created_or_existing: list[str] = []
128
+ for relative_path in PROJECT_WORKFLOW_DIRS:
129
+ directory = workspace_root / relative_path
130
+ directory.mkdir(parents=True, exist_ok=True)
131
+ created_or_existing.append(str(directory))
132
+ return created_or_existing
133
+
134
+
135
+ def workspace_workflow_dirs_ready(workspace_root: Path) -> bool:
136
+ return all((workspace_root / relative_path).is_dir() for relative_path in PROJECT_WORKFLOW_DIRS)
137
+
138
+
139
+ def build_project_onboarding_text() -> str:
140
+ return f"""{PROJECT_ONBOARDING_MARKER}
141
+ # CodeCGC Start Here
142
+
143
+ This file is generated by `cgc-install` for this project. It is intentionally project-relative and should not contain machine-specific install paths.
144
+
145
+ ## First Run
146
+
147
+ Inside Claude:
148
+
149
+ ```text
150
+ /cgc-start
151
+ /cgc-status
152
+ /cgc-doctor
153
+ /cgc 新增一个登录页面,放在 src/components/LoginForm.tsx
154
+ ```
155
+
156
+ Outside Claude:
157
+
158
+ ```bash
159
+ cgc-start
160
+ cgc-status
161
+ cgc-doctor
162
+ cgc "新增一个登录页面,放在 src/components/LoginForm.tsx"
163
+ ```
164
+
165
+ ## What CodeCGC Owns
166
+
167
+ - Claude owns orchestration, requirements, design notes, docs, review, acceptance, and workflow state.
168
+ - Codex owns backend implementation and backend tests.
169
+ - Gemini owns frontend implementation and frontend tests.
170
+ - Mixed backend/frontend work should be split before execution.
171
+
172
+ ## Installed Project Surface
173
+
174
+ ```text
175
+ .mcp.json
176
+ model-routing.yaml
177
+ .claude/settings.json
178
+ .claude/hooks/route-edit.ps1
179
+ .claude/commands/cgc*.md
180
+ codecgc/features/
181
+ codecgc/issues/
182
+ codecgc/execution/
183
+ codecgc/requirements/
184
+ codecgc/architecture/
185
+ codecgc/roadmap/
186
+ codecgc/compound/
187
+ codecgc/docs/
188
+ codecgc/reference/
189
+ ```
190
+
191
+ ## Normal Loop
192
+
193
+ 1. Start with `/cgc <your request>` or `cgc "<your request>"`.
194
+ 2. Let CodeCGC decide whether the next step is planning, execution, review, or closure.
195
+ 3. Review execution audits with `/cgc-review` or `cgc-review`.
196
+ 4. Use `/cgc-history` or `cgc-history` when you need to find open work.
197
+
198
+ ## Recovery
199
+
200
+ - If project integration looks wrong, run `cgc-install`, then `cgc-status`.
201
+ - If runtime or executor startup fails, run `cgc-doctor`.
202
+ - If a write is blocked, inspect `model-routing.yaml`; it is the project-local policy source of truth.
203
+ - If an execution was rejected, use `/cgc` or `cgc` to continue the same workflow rather than starting a new one.
204
+
205
+ ## Stable References
206
+
207
+ - `codecgc/reference/quickstart.md`
208
+ - `codecgc/reference/onboarding.md`
209
+ - `codecgc/reference/operation-guide.md`
210
+ - `codecgc/reference/troubleshooting.md`
211
+ - `codecgc/reference/path-contract.md`
212
+ """
213
+
214
+
215
+ def write_project_onboarding_file(workspace_root: Path) -> Path:
216
+ path = workspace_root / PROJECT_ONBOARDING_RELATIVE_PATH
217
+ path.parent.mkdir(parents=True, exist_ok=True)
218
+ path.write_text(build_project_onboarding_text(), encoding="utf-8")
219
+ return path
220
+
221
+
222
+ def onboarding_file_is_valid(path: Path) -> bool:
223
+ text = load_text_file(path)
224
+ if not text:
225
+ return False
226
+ required_markers = [
227
+ PROJECT_ONBOARDING_MARKER,
228
+ "/cgc-start",
229
+ "cgc-start",
230
+ "/cgc-status",
231
+ "cgc-status",
232
+ "model-routing.yaml",
233
+ ]
234
+ return all(marker in text for marker in required_markers)
235
+
236
+
104
237
  def build_hook_payload(command_text: str) -> dict[str, Any]:
105
238
  return {
106
239
  "PreToolUse": [
107
240
  {
108
- "matcher": "Edit|Write",
241
+ "matcher": EDIT_GUARDRAIL_MATCHER,
109
242
  "hooks": [
110
243
  {
111
244
  "type": "command",
@@ -117,6 +250,14 @@ def build_hook_payload(command_text: str) -> dict[str, Any]:
117
250
  }
118
251
 
119
252
 
253
+ def policy_file_is_valid(path: Path) -> bool:
254
+ try:
255
+ load_policy(path)
256
+ except Exception:
257
+ return False
258
+ return True
259
+
260
+
120
261
  def _normalize_command_path_for_markdown(path: Path) -> str:
121
262
  return str(path).replace("\\", "\\\\")
122
263
 
@@ -323,6 +464,17 @@ def build_custom_command_templates(bin_dir: Path) -> dict[str, str]:
323
464
  ],
324
465
  fallback_command="cgc-external-audit",
325
466
  ),
467
+ build_mcp_first_command_template(
468
+ filename="cgc-external-status.md",
469
+ description="查看外部 MCP 能力状态面板",
470
+ argument_hint="[参数]",
471
+ primary_tool="codecgc.external_status",
472
+ direct_rules=[
473
+ "映射可选字段 `workspace` 和 `format`。",
474
+ "该命令用于日常快速查看外部能力登记状态与本地 MCP 观测结果。",
475
+ ],
476
+ fallback_command="cgc-external-status",
477
+ ),
326
478
  build_mcp_first_command_template(
327
479
  filename="cgc-release-readiness.md",
328
480
  description="运行 CodeCGC 发布就绪检查",
@@ -347,6 +499,57 @@ def build_custom_command_templates(bin_dir: Path) -> dict[str, str]:
347
499
  ),
348
500
  ]
349
501
  )
502
+ templates["cgc-install.md"] = """---
503
+ description: Install or repair CodeCGC project-local integration
504
+ argument-hint: "[optional install arguments]"
505
+ ---
506
+
507
+ Use the `codecgc.install` MCP tool as the primary path.
508
+
509
+ Default behavior:
510
+
511
+ - Run project-local install for the current workspace.
512
+ - Do not install user-level Claude files unless the user explicitly asks for `--mode user`.
513
+ - Treat `model-routing.yaml` as the single routing policy source.
514
+
515
+ Argument mapping:
516
+
517
+ - No arguments: call `codecgc.install` with `mode: "local"`.
518
+ - `status`: call `codecgc.install` with `mode: "status"`.
519
+ - `doctor`: call `codecgc.install` with `mode: "doctor"`.
520
+ - `--workspace <dir>`: pass the workspace through.
521
+ - `--mode user-dry-run`: preview user-level files only.
522
+ - `--mode user`: only run if the user explicitly asks to write user-level Claude integration.
523
+
524
+ Fallback:
525
+
526
+ - If the MCP tool is unavailable, run `cgc-install` with the same arguments from the target project root.
527
+ - After install, run or suggest `cgc-start`, `cgc-status`, and `cgc-doctor`.
528
+ """
529
+ templates["cgc-start.md"] = """---
530
+ description: Show the CodeCGC first-run entry guide for this project
531
+ argument-hint: "[optional workspace]"
532
+ ---
533
+
534
+ Use `codecgc.start` as the primary path.
535
+
536
+ Default behavior:
537
+
538
+ - Treat this as a read-only onboarding/status entry.
539
+ - Summarize the project-local `codecgc/START_HERE.md` guide if present.
540
+ - If the guide is missing, tell the user to run `/cgc-install`.
541
+ - Keep the answer short and in Chinese.
542
+
543
+ Argument mapping:
544
+
545
+ - No arguments: call `codecgc.start`.
546
+ - `--workspace <dir>`: pass the workspace through.
547
+
548
+ Fallback:
549
+
550
+ - If the MCP tool is unavailable, run `cgc-start` from the target project root.
551
+ - If `cgc-start` reports missing onboarding, suggest `cgc-install`.
552
+ """
350
553
  return templates
351
554
 
352
555
 
@@ -361,6 +564,15 @@ def write_custom_command_files(target_dir: Path, bin_dir: Path) -> list[str]:
361
564
  return written
362
565
 
363
566
 
567
+ def is_codecgc_route_edit_hook(hook: Any) -> bool:
568
+ if not isinstance(hook, dict):
569
+ return False
570
+ if hook.get("type") != "command":
571
+ return False
572
+ command = str(hook.get("command", "")).replace("\\", "/").lower()
573
+ return "route-edit.ps1" in command
574
+
575
+
364
576
  def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dict[str, Any], bool]:
365
577
  hooks = current.get("hooks")
366
578
  expected_hooks = build_hook_payload(command_text)
@@ -373,6 +585,23 @@ def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dic
373
585
  hooks["PreToolUse"] = expected_hooks["PreToolUse"]
374
586
  return current, True
375
587
 
588
+ changed = False
589
+ for item in list(pre_tool_use):
590
+ if not isinstance(item, dict):
591
+ continue
592
+ if item.get("matcher") not in LEGACY_EDIT_GUARDRAIL_MATCHERS | {EDIT_GUARDRAIL_MATCHER}:
593
+ continue
594
+ hook_list = item.get("hooks")
595
+ if not isinstance(hook_list, list):
596
+ continue
597
+ filtered_hooks = [hook for hook in hook_list if not is_codecgc_route_edit_hook(hook)]
598
+ if len(filtered_hooks) != len(hook_list):
599
+ changed = True
600
+ if filtered_hooks:
601
+ item["hooks"] = filtered_hooks
602
+ else:
603
+ pre_tool_use.remove(item)
604
+
376
605
  expected = expected_hooks["PreToolUse"][0]
377
606
  for item in pre_tool_use:
378
607
  if not isinstance(item, dict):
@@ -387,7 +616,7 @@ def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dic
387
616
  if not isinstance(hook, dict):
388
617
  continue
389
618
  if hook.get("type") == "command" and hook.get("command") == expected["hooks"][0]["command"]:
390
- return current, False
619
+ return current, changed
391
620
  hook_list.append(expected["hooks"][0])
392
621
  return current, True
393
622
 
@@ -466,7 +695,7 @@ def settings_have_hook_command(settings: dict[str, Any], command_text: str) -> b
466
695
  for item in pre_tool_use:
467
696
  if not isinstance(item, dict):
468
697
  continue
469
- if item.get("matcher") != "Edit|Write":
698
+ if item.get("matcher") != EDIT_GUARDRAIL_MATCHER:
470
699
  continue
471
700
  hook_list = item.get("hooks")
472
701
  if not isinstance(hook_list, list):
@@ -490,8 +719,110 @@ def settings_have_allowed_tools(settings: dict[str, Any], allow_rules: list[str]
490
719
  return all(str(rule).strip() in existing for rule in allow_rules)
491
720
 
492
721
 
722
+ RUNTIME_MCP_SERVER_SPECS: dict[str, dict[str, Any]] = {
723
+ "codecgc": {
724
+ "module": "codecgcmcp.cli",
725
+ "pythonpath_suffixes": ["scripts", "codecgcmcp/src"],
726
+ },
727
+ "codex": {
728
+ "module": "codexmcp.cli",
729
+ "pythonpath_suffixes": ["scripts", "codecgcmcp/src", "codexmcp/src"],
730
+ },
731
+ "gemini": {
732
+ "module": "geminimcp.cli",
733
+ "pythonpath_suffixes": ["scripts", "codecgcmcp/src", "geminimcp/src"],
734
+ },
735
+ }
736
+
737
+
738
+ def _normalize_path_parts(path_text: str) -> tuple[str, ...]:
739
+ text = str(path_text).replace("\\", "/").strip().rstrip("/")
740
+ if not text:
741
+ return ()
742
+ return tuple(part.lower() for part in text.split("/") if part)
743
+
744
+
745
+ def _match_runtime_root(path_text: str, suffix: str) -> tuple[str, ...] | None:
746
+ parts = _normalize_path_parts(path_text)
747
+ suffix_parts = _normalize_path_parts(suffix)
748
+ if not parts or not suffix_parts or len(parts) < len(suffix_parts):
749
+ return None
750
+ if parts[-len(suffix_parts) :] != suffix_parts:
751
+ return None
752
+ return parts[: -len(suffix_parts)]
753
+
754
+
755
+ def _collect_runtime_roots(entries: list[str], suffix: str) -> set[tuple[str, ...]]:
756
+ roots: set[tuple[str, ...]] = set()
757
+ for entry in entries:
758
+ root = _match_runtime_root(entry, suffix)
759
+ if root is not None:
760
+ roots.add(root)
761
+ return roots
762
+
763
+
764
+ def mcp_server_matches_runtime_shape(server_payload: dict[str, Any], spec: dict[str, Any]) -> bool:
765
+ command_text = str(server_payload.get("command", "")).strip()
766
+ if not command_text:
767
+ return False
768
+
769
+ args = server_payload.get("args")
770
+ if args != ["-m", spec["module"]]:
771
+ return False
772
+
773
+ env = server_payload.get("env")
774
+ if not isinstance(env, dict):
775
+ return False
776
+
777
+ workspace_text = str(env.get("CODECGC_WORKSPACE_ROOT", "")).strip()
778
+ if not workspace_text:
779
+ return False
780
+
781
+ pythonpath_text = str(env.get("PYTHONPATH", "")).strip()
782
+ if not pythonpath_text:
783
+ return False
784
+
785
+ entries = [item.strip() for item in pythonpath_text.split(os.pathsep) if item.strip()]
786
+ if not entries:
787
+ return False
788
+
789
+ candidate_roots: set[tuple[str, ...]] | None = None
790
+ for suffix in spec["pythonpath_suffixes"]:
791
+ matching_roots = _collect_runtime_roots(entries, str(suffix))
792
+ if not matching_roots:
793
+ return False
794
+ candidate_roots = matching_roots if candidate_roots is None else candidate_roots & matching_roots
795
+ if not candidate_roots:
796
+ return False
797
+ return True
798
+
799
+
800
+ def mcp_config_matches_runtime_shape(payload: dict[str, Any]) -> bool:
801
+ servers = payload.get("mcpServers")
802
+ if not isinstance(servers, dict):
803
+ return False
804
+
805
+ for server_name, spec in RUNTIME_MCP_SERVER_SPECS.items():
806
+ server_payload = servers.get(server_name)
807
+ if not isinstance(server_payload, dict):
808
+ return False
809
+ if not mcp_server_matches_runtime_shape(server_payload, spec):
810
+ return False
811
+ return True
812
+
813
+
493
814
  def build_workspace_hook_command(workspace_paths: dict[str, Path]) -> str:
494
- return "powershell -ExecutionPolicy Bypass -File .claude/hooks/route-edit.ps1"
815
+ package_root = str(WORKSPACE).replace("'", "''")
816
+ workspace_root_path = Path(workspace_paths["root"])
817
+ hook_script_path = workspace_paths.get("hook_script", workspace_root_path / ".claude" / "hooks" / "route-edit.ps1")
818
+ workspace_root = str(workspace_root_path).replace("'", "''")
819
+ hook_script = str(hook_script_path).replace("\\", "/").replace("'", "''")
820
+ return (
821
+ "powershell -ExecutionPolicy Bypass -Command "
822
+ f"\"$env:CODECGC_PACKAGE_ROOT='{package_root}'; "
823
+ f"$env:CODECGC_WORKSPACE_ROOT='{workspace_root}'; "
824
+ f"& '{hook_script}'\""
825
+ )
495
826
 
496
827
 
497
828
  def build_mode_summary_payload(
@@ -513,11 +844,12 @@ def build_mode_summary_payload(
513
844
 
514
845
  def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
515
846
  workspace_paths = get_workspace_paths(override_workspace)
516
- mcp_path = write_mcp_config(workspace_paths["mcp"])
847
+ mcp_path = write_mcp_config(workspace_paths["mcp"], workspace_paths["root"])
517
848
  routing_path = sync_workspace_routing_file(workspace_paths["routing_file"])
518
849
 
519
850
  workspace_paths["claude_dir"].mkdir(parents=True, exist_ok=True)
520
851
  workspace_paths["hooks_dir"].mkdir(parents=True, exist_ok=True)
852
+ workflow_dirs = ensure_workspace_workflow_dirs(workspace_paths["root"])
521
853
 
522
854
  settings = load_json_file(workspace_paths["settings"])
523
855
  merged_settings, settings_changed = merge_hook_settings(
@@ -533,11 +865,12 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
533
865
  if PROJECT_HOOK_PATH.resolve() != workspace_paths["hook_script"].resolve():
534
866
  shutil.copyfile(PROJECT_HOOK_PATH, workspace_paths["hook_script"])
535
867
  written_commands = write_custom_command_files(workspace_paths["commands_dir"], WORKSPACE / "bin")
868
+ onboarding_file = write_project_onboarding_file(workspace_paths["root"])
536
869
 
537
870
  summary = build_mode_summary_payload(
538
871
  scope="项目级 Claude 与 MCP 集成面",
539
872
  human_summary="项目级 CodeCGC 集成文件已同步。",
540
- recommended_next_action="cgc-status",
873
+ recommended_next_action="cgc-start",
541
874
  )
542
875
 
543
876
  return {
@@ -549,13 +882,17 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
549
882
  "claude_settings": str(workspace_paths["settings"]),
550
883
  "hook_script": str(workspace_paths["hook_script"]),
551
884
  "commands_dir": str(workspace_paths["commands_dir"]),
885
+ "onboarding_file": str(onboarding_file),
886
+ "workflow_dirs": workflow_dirs,
552
887
  "command_files": written_commands,
553
888
  "notes": [
554
889
  "Repository-local MCP config was synced from the executor registry.",
555
- "Project-local model-routing.yaml was synchronized and preserves custom path blocks.",
890
+ "Project-local model-routing.yaml was synchronized as the policy source of truth.",
891
+ "Project-local codecgc workflow directories were initialized.",
556
892
  "Claude pre-edit guardrail hook was synchronized into the target workspace.",
557
893
  "Claude MCP tool permissions were merged into project .claude/settings.json.",
558
894
  "Project-local Claude slash commands were synchronized into .claude/commands.",
895
+ "Project-local codecgc/START_HERE.md was written as the first-run entry guide.",
559
896
  "This mode prepares project-level integration surfaces for the selected workspace.",
560
897
  ],
561
898
  "summary": summary,
@@ -563,7 +900,15 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
563
900
 
564
901
 
565
902
  def build_user_hook_command(user_paths: dict[str, Path]) -> str:
566
- return f"powershell -ExecutionPolicy Bypass -File {user_paths['hook_script']}"
903
+ package_root = str(WORKSPACE).replace("'", "''")
904
+ workspace_root = str(WORKSPACE).replace("'", "''")
905
+ hook_script = str(user_paths["hook_script"]).replace("'", "''")
906
+ return (
907
+ "powershell -ExecutionPolicy Bypass -Command "
908
+ f"\"$env:CODECGC_PACKAGE_ROOT='{package_root}'; "
909
+ f"$env:CODECGC_WORKSPACE_ROOT='{workspace_root}'; "
910
+ f"& '{hook_script}'\""
911
+ )
567
912
 
568
913
 
569
914
  def preview_user_install(override_root: str = "") -> dict[str, Any]:
@@ -571,8 +916,8 @@ def preview_user_install(override_root: str = "") -> dict[str, Any]:
571
916
  user_settings = load_json_file(user_paths["settings"])
572
917
  merged_settings, settings_changed = merge_hook_settings(user_settings, build_user_hook_command(user_paths))
573
918
  merged_settings, permissions_changed = merge_permission_settings(merged_settings, DEFAULT_ALLOWED_TOOLS)
574
- mcp_config = build_mcp_config()
575
- recommended_next_action = f"cgc-install --mode user --user-root {shell_quote(str(user_paths['root']))}"
919
+ mcp_config = build_mcp_config(WORKSPACE)
920
+ recommended_next_action = "cgc-install"
576
921
  summary = build_mode_summary_payload(
577
922
  scope="用户级 Claude 集成预演",
578
923
  human_summary="已完成用户级 Claude 集成预演,未写入任何文件。",
@@ -602,7 +947,7 @@ def preview_user_install(override_root: str = "") -> dict[str, Any]:
602
947
  },
603
948
  "notes": [
604
949
  "This mode does not modify user-level Claude files.",
605
- "Use this preview to inspect the future user-level integration surface.",
950
+ "Use this preview only when intentionally inspecting a user-level integration surface.",
606
951
  "The preview includes MCP tool allow rules for codecgc, codex, and gemini servers.",
607
952
  "Current CodeCGC product policy still defaults to project-local installation.",
608
953
  ],
@@ -620,7 +965,7 @@ def install_user_runtime(override_root: str = "") -> dict[str, Any]:
620
965
  merged_settings, settings_changed = merge_hook_settings(settings, build_user_hook_command(user_paths))
621
966
  merged_settings, permissions_changed = merge_permission_settings(merged_settings, DEFAULT_ALLOWED_TOOLS)
622
967
  write_json_file(user_paths["settings"], merged_settings)
623
- write_json_file(user_paths["mcp"], build_mcp_config())
968
+ write_json_file(user_paths["mcp"], build_mcp_config(WORKSPACE))
624
969
  shutil.copyfile(PROJECT_HOOK_PATH, user_paths["hook_script"])
625
970
  written_commands = write_custom_command_files(user_paths["commands_dir"], WORKSPACE / "bin")
626
971
  summary = build_mode_summary_payload(
@@ -683,9 +1028,11 @@ def build_install_mode_summary(result: dict[str, Any]) -> str:
683
1028
  f"- Claude 设置: {result.get('claude_settings', '')}",
684
1029
  f"- Hook 脚本: {result.get('hook_script', '')}",
685
1030
  f"- Slash Commands: {result.get('commands_dir', '')}",
1031
+ f"- 新手入口: {result.get('onboarding_file', '')}",
686
1032
  "- 说明: 可选外部能力如 MemOS 不由 cgc-install 自动写入;如需启用,请在 Claude 中单独配置官方 MCP。",
687
1033
  ]
688
1034
  next_actions = [
1035
+ "cgc-start",
689
1036
  "cgc-status",
690
1037
  "cgc-doctor",
691
1038
  ]
@@ -707,7 +1054,7 @@ def build_install_mode_summary(result: dict[str, Any]) -> str:
707
1054
  next_actions = []
708
1055
  user_root = str(result.get("user_claude_root", "")).strip()
709
1056
  if user_root:
710
- next_actions.append(f"cgc-install --mode user --user-root {shell_quote(user_root)}")
1057
+ next_actions.append("cgc-install")
711
1058
  next_actions.append("cgc-install --mode status")
712
1059
  return render_summary_block("CodeCGC 用户级预演", lines, next_actions)
713
1060
 
@@ -734,13 +1081,16 @@ def build_install_mode_summary(result: dict[str, Any]) -> str:
734
1081
 
735
1082
 
736
1083
  def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
737
- expected_mcp = build_mcp_config()
1084
+ expected_mcp = build_mcp_config(workspace_paths["root"])
738
1085
  expected_hook_command = build_workspace_hook_command(workspace_paths)
739
1086
  expected_hook_text = load_text_file(PROJECT_HOOK_PATH)
740
1087
  current_settings = load_json_file(workspace_paths["settings"])
741
1088
  current_mcp = load_json_file(workspace_paths["mcp"])
742
1089
  current_hook_text = load_text_file(workspace_paths["hook_script"])
743
1090
  routing_exists = workspace_paths["routing_file"].exists()
1091
+ policy_valid = policy_file_is_valid(workspace_paths["routing_file"]) if routing_exists else False
1092
+ workflow_dirs_ready = workspace_workflow_dirs_ready(workspace_paths["root"])
1093
+ onboarding_ready = onboarding_file_is_valid(workspace_paths["onboarding_file"])
744
1094
 
745
1095
  hook_registered = settings_have_hook_command(current_settings, expected_hook_command)
746
1096
  permissions_registered = settings_have_allowed_tools(current_settings, DEFAULT_ALLOWED_TOOLS)
@@ -750,6 +1100,10 @@ def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
750
1100
  missing = []
751
1101
  if not routing_exists:
752
1102
  missing.append("routing_file")
1103
+ if routing_exists and not policy_valid:
1104
+ missing.append("routing_policy")
1105
+ if not workflow_dirs_ready:
1106
+ missing.append("workflow_dirs")
753
1107
  if not mcp_matches:
754
1108
  missing.append("mcp_json")
755
1109
  if not hook_registered:
@@ -758,6 +1112,8 @@ def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
758
1112
  missing.append("claude_settings_permissions")
759
1113
  if not hook_file_matches:
760
1114
  missing.append("hook_script")
1115
+ if not onboarding_ready:
1116
+ missing.append("onboarding_file")
761
1117
 
762
1118
  ready = not missing
763
1119
  return {
@@ -765,10 +1121,16 @@ def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
765
1121
  "routing_file_path": str(workspace_paths["routing_file"]),
766
1122
  "claude_settings_path": str(workspace_paths["settings"]),
767
1123
  "hook_script_path": str(workspace_paths["hook_script"]),
1124
+ "onboarding_file_path": str(workspace_paths["onboarding_file"]),
768
1125
  "mcp_json_exists": workspace_paths["mcp"].exists(),
769
1126
  "routing_file_exists": routing_exists,
1127
+ "routing_policy_valid": policy_valid,
1128
+ "workflow_dirs_ready": workflow_dirs_ready,
1129
+ "onboarding_ready": onboarding_ready,
1130
+ "workflow_dirs_expected": [str(workspace_paths["root"] / item) for item in PROJECT_WORKFLOW_DIRS],
770
1131
  "claude_settings_exists": workspace_paths["settings"].exists(),
771
1132
  "hook_exists": workspace_paths["hook_script"].exists(),
1133
+ "onboarding_exists": workspace_paths["onboarding_file"].exists(),
772
1134
  "mcp_matches_expected": mcp_matches,
773
1135
  "hook_registered": hook_registered,
774
1136
  "permissions_registered": permissions_registered,
@@ -782,7 +1144,7 @@ def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
782
1144
 
783
1145
 
784
1146
  def collect_user_status(user_paths: dict[str, Path]) -> dict[str, Any]:
785
- expected_mcp = build_mcp_config()
1147
+ expected_mcp = build_mcp_config(WORKSPACE)
786
1148
  expected_hook_command = build_user_hook_command(user_paths)
787
1149
  expected_hook_text = load_text_file(PROJECT_HOOK_PATH)
788
1150
  current_settings = load_json_file(user_paths["settings"])
@@ -791,7 +1153,9 @@ def collect_user_status(user_paths: dict[str, Path]) -> dict[str, Any]:
791
1153
 
792
1154
  hook_registered = settings_have_hook_command(current_settings, expected_hook_command)
793
1155
  permissions_registered = settings_have_allowed_tools(current_settings, DEFAULT_ALLOWED_TOOLS)
794
- mcp_matches = current_mcp == expected_mcp if user_paths["mcp"].exists() else False
1156
+ mcp_matches_exact = current_mcp == expected_mcp if user_paths["mcp"].exists() else False
1157
+ mcp_matches_runtime = mcp_config_matches_runtime_shape(current_mcp) if user_paths["mcp"].exists() else False
1158
+ mcp_matches = mcp_matches_exact or mcp_matches_runtime
795
1159
  hook_file_matches = current_hook_text == expected_hook_text if user_paths["hook_script"].exists() else False
796
1160
 
797
1161
  missing = []
@@ -814,6 +1178,8 @@ def collect_user_status(user_paths: dict[str, Path]) -> dict[str, Any]:
814
1178
  "mcp_exists": user_paths["mcp"].exists(),
815
1179
  "hook_exists": user_paths["hook_script"].exists(),
816
1180
  "mcp_matches_expected": mcp_matches,
1181
+ "mcp_matches_expected_exact": mcp_matches_exact,
1182
+ "mcp_matches_runtime_shape": mcp_matches_runtime,
817
1183
  "hook_registered": hook_registered,
818
1184
  "permissions_registered": permissions_registered,
819
1185
  "hook_file_matches_expected": hook_file_matches,
@@ -845,6 +1211,20 @@ def collect_install_status(override_workspace: str = "") -> dict[str, Any]:
845
1211
  "scope": "项目级集成就绪状态,以及用户级 Claude 集成预演状态",
846
1212
  }
847
1213
 
1214
+ status_summary.update(
1215
+ {
1216
+ "default_policy": "project-local-first",
1217
+ "recommended_next_command": project_status["recommended_command"],
1218
+ "recommended_user_command": "",
1219
+ "human_summary": (
1220
+ "Project-local CodeCGC integration is ready."
1221
+ if project_status["ready"]
1222
+ else "Project-local CodeCGC integration is missing or outdated."
1223
+ ),
1224
+ "scope": "project-local integration status; user-level config is optional preview only",
1225
+ }
1226
+ )
1227
+
848
1228
  return {
849
1229
  "success": True,
850
1230
  "mode": "status",
@@ -856,6 +1236,51 @@ def collect_install_status(override_workspace: str = "") -> dict[str, Any]:
856
1236
  }
857
1237
 
858
1238
 
1239
+ def collect_start_status(override_workspace: str = "") -> dict[str, Any]:
1240
+ workspace_paths = get_workspace_paths(override_workspace)
1241
+ project_status = collect_project_status(workspace_paths)
1242
+ onboarding_path = workspace_paths["onboarding_file"]
1243
+ onboarding_ready = onboarding_file_is_valid(onboarding_path)
1244
+ guide_text = load_text_file(onboarding_path) if onboarding_ready else ""
1245
+ recommended_next_action = "cgc-status" if onboarding_ready else build_workspace_install_command(workspace_paths["root"])
1246
+ human_summary = (
1247
+ "CodeCGC first-run guide is ready. Start with /cgc or cgc after status/doctor are green."
1248
+ if onboarding_ready
1249
+ else "CodeCGC first-run guide is missing or outdated. Run project-local install first."
1250
+ )
1251
+ quick_actions = [
1252
+ "/cgc-status",
1253
+ "/cgc-doctor",
1254
+ "/cgc <你的需求>",
1255
+ "cgc-status",
1256
+ "cgc-doctor",
1257
+ "cgc \"你的需求\"",
1258
+ ]
1259
+ if not onboarding_ready:
1260
+ quick_actions = ["/cgc-install", build_workspace_install_command(workspace_paths["root"])]
1261
+
1262
+ return {
1263
+ "success": True,
1264
+ "mode": "start",
1265
+ "workspace": str(workspace_paths["root"]),
1266
+ "summary": {
1267
+ "ready": onboarding_ready,
1268
+ "human_summary": human_summary,
1269
+ "scope": "project-local first-run guide and next actions",
1270
+ "recommended_next_action": recommended_next_action,
1271
+ "quick_actions": quick_actions,
1272
+ },
1273
+ "onboarding": {
1274
+ "path": str(onboarding_path),
1275
+ "exists": onboarding_path.exists(),
1276
+ "ready": onboarding_ready,
1277
+ "relative_path": PROJECT_ONBOARDING_RELATIVE_PATH,
1278
+ "guide_excerpt": guide_text[:1200],
1279
+ },
1280
+ "project": project_status,
1281
+ }
1282
+
1283
+
859
1284
  def find_python_command() -> str:
860
1285
  candidates = ["python", "py"] if os.name == "nt" else ["python3", "python"]
861
1286
  for candidate in candidates:
@@ -1065,11 +1490,20 @@ def classify_doctor_failures(
1065
1490
  )
1066
1491
  continue
1067
1492
 
1068
- if name in {"routing_file_exists", "project_hook_source_exists"}:
1493
+ if name == "onboarding_file_ready":
1494
+ add_failure(
1495
+ "onboarding-guide-missing",
1496
+ "项目级新手入口文件缺失或已过期。",
1497
+ "重新执行项目级安装以同步 `codecgc/START_HERE.md` 与 `/cgc-start` 入口。",
1498
+ install_command,
1499
+ )
1500
+ continue
1501
+
1502
+ if name in {"routing_file_exists", "routing_policy_valid", "project_hook_source_exists"}:
1069
1503
  add_failure(
1070
1504
  "packaged-runtime-missing-files",
1071
- "运行时所需的路由文件或 hook 源文件缺失。",
1072
- "检查当前安装包是否完整,必要时重新安装或重新打包后再试。",
1505
+ "运行时所需的 policy、路由文件或 hook 源文件缺失/无效。",
1506
+ "重新执行项目级安装以同步 model-routing.yaml、policy-backed hook 与 Claude settings。",
1073
1507
  )
1074
1508
  continue
1075
1509
 
@@ -1104,6 +1538,21 @@ def collect_doctor_status(override_workspace: str = "") -> dict[str, Any]:
1104
1538
  "ok": workspace_paths["routing_file"].exists(),
1105
1539
  "detail": str(workspace_paths["routing_file"]),
1106
1540
  },
1541
+ {
1542
+ "name": "routing_policy_valid",
1543
+ "ok": policy_file_is_valid(workspace_paths["routing_file"]) if workspace_paths["routing_file"].exists() else False,
1544
+ "detail": str(workspace_paths["routing_file"]),
1545
+ },
1546
+ {
1547
+ "name": "workflow_dirs_ready",
1548
+ "ok": workspace_workflow_dirs_ready(workspace_paths["root"]),
1549
+ "detail": str(workspace_paths["root"] / "codecgc"),
1550
+ },
1551
+ {
1552
+ "name": "onboarding_file_ready",
1553
+ "ok": onboarding_file_is_valid(workspace_paths["onboarding_file"]),
1554
+ "detail": str(workspace_paths["onboarding_file"]),
1555
+ },
1107
1556
  {
1108
1557
  "name": "project_hook_source_exists",
1109
1558
  "ok": PROJECT_HOOK_PATH.exists(),
@@ -1204,7 +1653,7 @@ def collect_doctor_status(override_workspace: str = "") -> dict[str, Any]:
1204
1653
 
1205
1654
  def build_parser() -> argparse.ArgumentParser:
1206
1655
  parser = argparse.ArgumentParser(description="Install or inspect CodeCGC integration surfaces.")
1207
- parser.add_argument("--mode", choices=["local", "user-dry-run", "user", "status", "doctor"], default="local")
1656
+ parser.add_argument("--mode", choices=["local", "user-dry-run", "user", "status", "doctor", "start"], default="local")
1208
1657
  parser.add_argument(
1209
1658
  "--format",
1210
1659
  choices=["json", "summary"],
@@ -1234,9 +1683,28 @@ def main() -> int:
1234
1683
  result = collect_install_status(args.workspace)
1235
1684
  elif args.mode == "doctor":
1236
1685
  result = collect_doctor_status(args.workspace)
1686
+ elif args.mode == "start":
1687
+ result = collect_start_status(args.workspace)
1237
1688
  else:
1238
1689
  raise ValueError(f"Unsupported install mode: {args.mode}")
1239
1690
 
1691
+ if args.mode == "start" and args.format == "summary":
1692
+ summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
1693
+ onboarding = result.get("onboarding", {}) if isinstance(result.get("onboarding"), dict) else {}
1694
+ quick_actions = summary.get("quick_actions", []) if isinstance(summary.get("quick_actions"), list) else []
1695
+ lines = [
1696
+ f"- 工作区: {result.get('workspace', '')}",
1697
+ f"- 范围: {summary.get('scope', '')}",
1698
+ f"- 新手入口就绪: {format_bool_zh(summary.get('ready'))}",
1699
+ f"- 入口文件: {onboarding.get('path', '')}",
1700
+ f"- 摘要: {summary.get('human_summary', '')}",
1701
+ f"- 快速动作: {format_list_zh(quick_actions)}",
1702
+ ]
1703
+ next_action = str(summary.get("recommended_next_action", "")).strip()
1704
+ next_actions = [next_action] if next_action else []
1705
+ print(render_summary_block("CodeCGC Start", lines, next_actions))
1706
+ return 0 if result.get("success") else 1
1707
+
1240
1708
  if args.mode == "status" and args.format == "summary":
1241
1709
  summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
1242
1710
  project = result.get("project", {}) if isinstance(result.get("project"), dict) else {}
@@ -1249,6 +1717,7 @@ def main() -> int:
1249
1717
  f"- 策略: {summary.get('default_policy', '')}",
1250
1718
  f"- 摘要: {summary.get('human_summary', '')}",
1251
1719
  f"- 项目级缺失项: {format_list_zh(project.get('missing_or_outdated', []))}",
1720
+ f"- 新手入口: {project.get('onboarding_file_path', '')}",
1252
1721
  f"- 用户级缺失项: {format_list_zh(user.get('missing_or_outdated', []))}",
1253
1722
  ]
1254
1723
  recommended_project = str(summary.get("recommended_project_command", "")).strip()