@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,3419 @@
1
+ import argparse
2
+ import base64
3
+ import hashlib
4
+ import json
5
+ import re
6
+ import sys
7
+ from typing import Any
8
+ from pathlib import Path
9
+
10
+ from build_codecgc_task import classify_path
11
+ from build_codecgc_task import load_checklist_yaml
12
+ from build_codecgc_task import load_simple_routing_config
13
+ from codecgc_artifact_roots import discover_flow_directory
14
+ from codecgc_artifact_roots import flow_root
15
+ from codecgc_command_surface import matches_command
16
+ from codecgc_command_surface import to_internal_command
17
+ from codecgc_command_surface import to_public_command
18
+ from codecgc_console_io import configure_utf8_stdio
19
+ from codecgc_console_io import print_json
20
+ from codecgc_routing_paths import resolve_active_routing_file
21
+ from codecgc_runtime_paths import PACKAGE_ROOT
22
+ from codecgc_session_recovery import resolve_session_id_from_task
23
+ from codecgc_workflow_runtime import run_json_script
24
+
25
+ WORKSPACE = PACKAGE_ROOT
26
+ ROUTING_FILE = resolve_active_routing_file()
27
+
28
+
29
+ def build_parser() -> argparse.ArgumentParser:
30
+ parser = argparse.ArgumentParser(
31
+ description="Single-entry orchestration layer for CodeCGC."
32
+ )
33
+ parser.add_argument("--payload-json", default="")
34
+ parser.add_argument("--payload-base64", default="")
35
+ parser.add_argument("--payload-file", default="")
36
+ parser.add_argument("--mode", choices=["auto", "new", "continue", "explain"], default="auto")
37
+ parser.add_argument("--flow", choices=["feature", "issue"], default="")
38
+ parser.add_argument("--slug", default="")
39
+ parser.add_argument("--summary", default="")
40
+ parser.add_argument("--request", default="")
41
+ parser.add_argument("--date", default="")
42
+ parser.add_argument("--target-path", action="append", default=[])
43
+ parser.add_argument("--kind", choices=["auto", "frontend", "backend"], default="auto")
44
+ parser.add_argument("--goal", default="")
45
+ parser.add_argument("--context", action="append", default=[])
46
+ parser.add_argument("--user-story", default="")
47
+ parser.add_argument("--in-scope", action="append", default=[])
48
+ parser.add_argument("--out-of-scope", action="append", default=[])
49
+ parser.add_argument("--acceptance", action="append", default=[])
50
+ parser.add_argument("--risk", action="append", default=[])
51
+ parser.add_argument("--dependency", action="append", default=[])
52
+ parser.add_argument("--assumption", action="append", default=[])
53
+ parser.add_argument("--open-question", action="append", default=[])
54
+ parser.add_argument("--validation", action="append", default=[])
55
+ parser.add_argument("--rollback", action="append", default=[])
56
+ parser.add_argument("--symptom", default="")
57
+ parser.add_argument("--reproduction", default="")
58
+ parser.add_argument("--expected", default="")
59
+ parser.add_argument("--actual", default="")
60
+ parser.add_argument("--root-cause", default="")
61
+ parser.add_argument("--preferred-fix", default="")
62
+ parser.add_argument("--rejected-fix", default="")
63
+ parser.add_argument("--artifact-class", choices=["product", "fixture"], default="product")
64
+ parser.add_argument("--latest", action="store_true")
65
+ parser.add_argument("--include-fixtures", action="store_true")
66
+ parser.add_argument("--step-number", type=int)
67
+ parser.add_argument("--checklist-file", default="")
68
+ parser.add_argument("--audit-root", default="")
69
+ parser.add_argument("--timeout-seconds", type=int, default=120)
70
+ parser.add_argument("--dry-run", action="store_true")
71
+ parser.add_argument("--return-all-messages", action="store_true")
72
+ parser.add_argument("--auto-dispatch", action="store_true")
73
+ parser.add_argument("--decision", choices=["accepted", "changes-requested"], default="")
74
+ parser.add_argument("--audit-file", default="")
75
+ parser.add_argument("--next-step", default="")
76
+ parser.add_argument("--force", action="store_true")
77
+ return parser
78
+
79
+
80
+ def load_payload_input(args: argparse.Namespace) -> dict[str, Any]:
81
+ if args.payload_json.strip():
82
+ payload = json.loads(args.payload_json)
83
+ elif args.payload_base64.strip():
84
+ decoded = base64.b64decode(args.payload_base64).decode("utf-8")
85
+ payload = json.loads(decoded)
86
+ elif args.payload_file.strip():
87
+ payload = json.loads(Path(args.payload_file).read_text(encoding="utf-8-sig"))
88
+ else:
89
+ return {}
90
+
91
+ if not isinstance(payload, dict):
92
+ raise ValueError("Entry payload must be a JSON object.")
93
+ return payload
94
+
95
+
96
+ def merge_scalar_arg(args: argparse.Namespace, payload: dict[str, Any], field: str, default_value: str = "") -> None:
97
+ current = getattr(args, field)
98
+ if current != default_value:
99
+ return
100
+ value = payload.get(field)
101
+ if isinstance(value, str) and value.strip():
102
+ setattr(args, field, value.strip())
103
+
104
+
105
+ def merge_list_arg(args: argparse.Namespace, payload: dict[str, Any], field: str) -> None:
106
+ current = getattr(args, field)
107
+ if current:
108
+ return
109
+ value = payload.get(field)
110
+ if isinstance(value, list):
111
+ merged = [str(item).strip() for item in value if str(item).strip()]
112
+ if merged:
113
+ setattr(args, field, merged)
114
+ elif isinstance(value, str) and value.strip():
115
+ setattr(args, field, [value.strip()])
116
+
117
+
118
+ def merge_list_arg_alias(args: argparse.Namespace, payload: dict[str, Any], field: str, alias: str) -> None:
119
+ current = getattr(args, field)
120
+ if current:
121
+ return
122
+ value = payload.get(alias)
123
+ if isinstance(value, list):
124
+ merged = [str(item).strip() for item in value if str(item).strip()]
125
+ if merged:
126
+ setattr(args, field, merged)
127
+ elif isinstance(value, str) and value.strip():
128
+ setattr(args, field, [value.strip()])
129
+
130
+
131
+ def apply_entry_payload(args: argparse.Namespace) -> argparse.Namespace:
132
+ payload = load_payload_input(args)
133
+ if not payload:
134
+ return args
135
+
136
+ merge_scalar_arg(args, payload, "mode", "auto")
137
+ merge_scalar_arg(args, payload, "flow", "")
138
+ merge_scalar_arg(args, payload, "slug", "")
139
+ merge_scalar_arg(args, payload, "summary", "")
140
+ merge_scalar_arg(args, payload, "request", "")
141
+ merge_scalar_arg(args, payload, "date", "")
142
+ merge_scalar_arg(args, payload, "kind", "auto")
143
+ merge_scalar_arg(args, payload, "goal", "")
144
+ merge_scalar_arg(args, payload, "user_story", "")
145
+ merge_scalar_arg(args, payload, "symptom", "")
146
+ merge_scalar_arg(args, payload, "reproduction", "")
147
+ merge_scalar_arg(args, payload, "expected", "")
148
+ merge_scalar_arg(args, payload, "actual", "")
149
+ merge_scalar_arg(args, payload, "root_cause", "")
150
+ merge_scalar_arg(args, payload, "preferred_fix", "")
151
+ merge_scalar_arg(args, payload, "rejected_fix", "")
152
+ merge_scalar_arg(args, payload, "artifact_class", "product")
153
+ merge_scalar_arg(args, payload, "audit_root", "")
154
+ merge_scalar_arg(args, payload, "audit_file", "")
155
+ merge_scalar_arg(args, payload, "next_step", "")
156
+ merge_scalar_arg(args, payload, "decision", "")
157
+
158
+ merge_list_arg(args, payload, "target_path")
159
+ merge_list_arg_alias(args, payload, "target_path", "target_paths")
160
+ merge_list_arg(args, payload, "context")
161
+ merge_list_arg(args, payload, "in_scope")
162
+ merge_list_arg(args, payload, "out_of_scope")
163
+ merge_list_arg(args, payload, "acceptance")
164
+ merge_list_arg(args, payload, "risk")
165
+ merge_list_arg(args, payload, "dependency")
166
+ merge_list_arg(args, payload, "assumption")
167
+ merge_list_arg(args, payload, "open_question")
168
+ merge_list_arg(args, payload, "validation")
169
+ merge_list_arg(args, payload, "rollback")
170
+
171
+ if not args.latest and bool(payload.get("latest")):
172
+ args.latest = True
173
+ if not args.include_fixtures and bool(payload.get("include_fixtures")):
174
+ args.include_fixtures = True
175
+ if args.step_number is None and payload.get("step_number") is not None:
176
+ args.step_number = int(payload["step_number"])
177
+ if args.timeout_seconds == 120 and payload.get("timeout_seconds") is not None:
178
+ args.timeout_seconds = int(payload["timeout_seconds"])
179
+ if not args.auto_dispatch and bool(payload.get("auto_dispatch")):
180
+ args.auto_dispatch = True
181
+ if not args.dry_run and bool(payload.get("dry_run")):
182
+ args.dry_run = True
183
+ if not args.return_all_messages and bool(payload.get("return_all_messages")):
184
+ args.return_all_messages = True
185
+ if not args.force and bool(payload.get("force")):
186
+ args.force = True
187
+
188
+ return args
189
+
190
+
191
+ def resolve_governance_followup(payload: dict[str, Any], request: str) -> dict[str, Any]:
192
+ governance_type = str(payload.get("governance_type", "")).strip()
193
+ artifact_path = str(payload.get("artifact_path", "")).strip()
194
+ if not governance_type or not artifact_path:
195
+ return {}
196
+
197
+ request_text = request.strip()
198
+ if not request_text:
199
+ return {}
200
+
201
+ lowered = request_text.lower()
202
+ original_summary = str(payload.get("artifact_summary", "")).strip()
203
+
204
+ def pick_clause(*patterns: str) -> str:
205
+ return extract_clause(request_text, list(patterns))
206
+
207
+ def split_labeled_fields(mapping: dict[str, list[str]]) -> dict[str, str]:
208
+ labels: list[tuple[int, int, str]] = []
209
+ for key, aliases in mapping.items():
210
+ for alias in aliases:
211
+ for match in re.finditer(rf"{re.escape(alias)}[::]\s*", request_text, flags=re.IGNORECASE):
212
+ labels.append((match.start(), match.end(), key))
213
+ labels.sort(key=lambda item: item[0])
214
+ values: dict[str, str] = {key: "" for key in mapping}
215
+ for index, (_, content_start, key) in enumerate(labels):
216
+ next_start = labels[index + 1][0] if index + 1 < len(labels) else len(request_text)
217
+ chunk = request_text[content_start:next_start].strip(" 。;;,,\n")
218
+ if chunk and not values.get(key):
219
+ values[key] = chunk
220
+ return values
221
+
222
+ def build_flag_args(pairs: list[tuple[str, str]], *, required_pairs: list[tuple[str, str]] | None = None) -> list[str]:
223
+ args: list[str] = []
224
+ for flag, value in required_pairs or []:
225
+ args.extend([flag, value])
226
+ for flag, value in pairs:
227
+ if value.strip():
228
+ args.extend([flag, value.strip()])
229
+ return args
230
+
231
+ if governance_type == "guide":
232
+ details = split_labeled_fields(
233
+ {
234
+ "purpose": ["目的", "purpose"],
235
+ "steps": ["步骤", "steps", "step"],
236
+ "boundary": ["边界", "boundary"],
237
+ }
238
+ )
239
+ details["append_note"] = request_text if not any(details.values()) else ""
240
+ return {
241
+ "governance_type": governance_type,
242
+ "script": "write_codecgc_guide.py",
243
+ "args": build_flag_args(
244
+ [
245
+ ("--purpose", details["purpose"]),
246
+ ("--steps", details["steps"]),
247
+ ("--boundary", details["boundary"]),
248
+ ("--append-note", details["append_note"]),
249
+ ],
250
+ required_pairs=[
251
+ ("--summary", original_summary or request_text),
252
+ ("--artifact-path", artifact_path),
253
+ ],
254
+ ),
255
+ "artifact_summary": original_summary or request_text,
256
+ "artifact_path": artifact_path,
257
+ }
258
+
259
+ if governance_type == "libdoc":
260
+ details = split_labeled_fields(
261
+ {
262
+ "entry": ["入口", "entry"],
263
+ "contract": ["契约", "contract"],
264
+ "example": ["示例", "example"],
265
+ "boundary": ["边界", "boundary"],
266
+ }
267
+ )
268
+ details["append_note"] = request_text if not any(details.values()) else ""
269
+ return {
270
+ "governance_type": governance_type,
271
+ "script": "write_codecgc_libdoc.py",
272
+ "args": build_flag_args(
273
+ [
274
+ ("--entry", details["entry"]),
275
+ ("--contract", details["contract"]),
276
+ ("--example", details["example"]),
277
+ ("--boundary", details["boundary"]),
278
+ ("--append-note", details["append_note"]),
279
+ ],
280
+ required_pairs=[
281
+ ("--summary", original_summary or request_text),
282
+ ("--artifact-path", artifact_path),
283
+ ],
284
+ ),
285
+ "artifact_summary": original_summary or request_text,
286
+ "artifact_path": artifact_path,
287
+ }
288
+
289
+ if governance_type == "trick":
290
+ details = split_labeled_fields(
291
+ {
292
+ "practice": ["做法", "practice"],
293
+ "scope": ["范围", "scope"],
294
+ "counterexample": ["反例", "误用", "counterexample"],
295
+ }
296
+ )
297
+ details["append_note"] = request_text if not any(details.values()) else ""
298
+ return {
299
+ "governance_type": governance_type,
300
+ "script": "write_codecgc_trick.py",
301
+ "args": build_flag_args(
302
+ [
303
+ ("--practice", details["practice"]),
304
+ ("--scope", details["scope"]),
305
+ ("--counterexample", details["counterexample"]),
306
+ ("--append-note", details["append_note"]),
307
+ ],
308
+ required_pairs=[
309
+ ("--summary", original_summary or request_text),
310
+ ("--artifact-path", artifact_path),
311
+ ],
312
+ ),
313
+ "artifact_summary": original_summary or request_text,
314
+ "artifact_path": artifact_path,
315
+ }
316
+
317
+ if governance_type == "explore":
318
+ details = split_labeled_fields(
319
+ {
320
+ "question_detail": ["问题", "question"],
321
+ "target_modules": ["模块", "目录", "文件", "module"],
322
+ "expected_output": ["输出", "结论", "output"],
323
+ }
324
+ )
325
+ details["append_note"] = request_text if not any(details.values()) else ""
326
+ return {
327
+ "governance_type": governance_type,
328
+ "script": "write_codecgc_explore.py",
329
+ "args": build_flag_args(
330
+ [
331
+ ("--question-detail", details["question_detail"]),
332
+ ("--target-modules", details["target_modules"]),
333
+ ("--expected-output", details["expected_output"]),
334
+ ("--append-note", details["append_note"]),
335
+ ],
336
+ required_pairs=[
337
+ ("--summary", original_summary or request_text),
338
+ ("--artifact-path", artifact_path),
339
+ ],
340
+ ),
341
+ "artifact_summary": original_summary or request_text,
342
+ "artifact_path": artifact_path,
343
+ }
344
+
345
+ return {}
346
+
347
+
348
+ def summarize_new_work_seed(args: argparse.Namespace, flow: str) -> str:
349
+ if args.summary.strip():
350
+ return args.summary.strip()
351
+ if args.request.strip():
352
+ return args.request.strip()
353
+ if flow == "feature":
354
+ for value in (args.goal, args.user_story):
355
+ if value.strip():
356
+ return value.strip()
357
+ else:
358
+ for value in (args.symptom, args.expected, args.actual):
359
+ if value.strip():
360
+ return value.strip()
361
+ target_paths = [item.strip() for item in args.target_path if item.strip()]
362
+ if target_paths:
363
+ return f"{flow} for {target_paths[0]}"
364
+ return ""
365
+
366
+
367
+ def infer_flow_for_new_work(args: argparse.Namespace) -> str:
368
+ if args.flow:
369
+ return args.flow
370
+ request = args.request.strip().lower()
371
+ issue_signals = [
372
+ args.symptom,
373
+ args.reproduction,
374
+ args.expected,
375
+ args.actual,
376
+ args.root_cause,
377
+ args.preferred_fix,
378
+ args.rejected_fix,
379
+ ]
380
+ issue_keywords = [
381
+ "bug",
382
+ "fix",
383
+ "issue",
384
+ "error",
385
+ "failure",
386
+ "broken",
387
+ "problem",
388
+ "修复",
389
+ "问题",
390
+ "报错",
391
+ "异常",
392
+ "故障",
393
+ ]
394
+ if any(keyword in request for keyword in issue_keywords):
395
+ return "issue"
396
+ return "issue" if any(value.strip() for value in issue_signals) else "feature"
397
+
398
+
399
+ def request_implies_continue(request: str) -> bool:
400
+ lowered = request.lower()
401
+ keywords = [
402
+ "continue",
403
+ "resume",
404
+ "pick up",
405
+ "go on",
406
+ "继续",
407
+ "接着",
408
+ "继续做",
409
+ "继续推进",
410
+ ]
411
+ return any(keyword in lowered for keyword in keywords)
412
+
413
+
414
+ def request_implies_explain(request: str) -> bool:
415
+ lowered = request.lower()
416
+ keywords = [
417
+ "what next",
418
+ "next step",
419
+ "status",
420
+ "explain",
421
+ "why",
422
+ "state",
423
+ "下一步",
424
+ "现在该做什么",
425
+ "状态",
426
+ "解释",
427
+ "看看进度",
428
+ ]
429
+ return any(keyword in lowered for keyword in keywords)
430
+
431
+
432
+ def request_implies_execute(request: str) -> bool:
433
+ lowered = request.lower()
434
+ keywords = [
435
+ "implement",
436
+ "build it",
437
+ "fix it",
438
+ "execute",
439
+ "run it",
440
+ "go ahead",
441
+ "start now",
442
+ "处理掉",
443
+ "解决掉",
444
+ "直接做",
445
+ "开始做",
446
+ "开工",
447
+ "执行",
448
+ "继续处理",
449
+ "继续执行",
450
+ "直接修复",
451
+ "马上修复",
452
+ "立即修复",
453
+ "直接实现",
454
+ "马上实现",
455
+ "立即实现",
456
+ "直接开始",
457
+ "马上开始",
458
+ "立即开始",
459
+ "测试",
460
+ "补测试",
461
+ "写测试",
462
+ "加测试",
463
+ "生成测试",
464
+ ]
465
+ return any(keyword in lowered for keyword in keywords)
466
+
467
+
468
+ def request_implies_review(request: str) -> bool:
469
+ lowered = request.lower()
470
+ keywords = [
471
+ "review",
472
+ "acceptance",
473
+ "approve",
474
+ "验收",
475
+ "审核",
476
+ "审查",
477
+ "检查一下",
478
+ "看下结果",
479
+ "过一下",
480
+ "通过",
481
+ "不通过",
482
+ ]
483
+ return any(keyword in lowered for keyword in keywords)
484
+
485
+
486
+ def normalize_request_shortcut(request: str) -> str:
487
+ normalized = request.strip().lower()
488
+ normalized = re.sub(r"\s+", "", normalized)
489
+ normalized = normalized.strip("`\"',,。;;::!?!?()()[]{}")
490
+ return normalized
491
+
492
+
493
+ def classify_existing_workflow_shortcut(request: str) -> str:
494
+ normalized = normalize_request_shortcut(request)
495
+ if not normalized:
496
+ return ""
497
+
498
+ explain_shortcuts = {
499
+ "下一步",
500
+ "现在下一步该做什么",
501
+ "当前下一步",
502
+ "看看进度",
503
+ "当前状态",
504
+ "解释一下当前状态",
505
+ "解释当前状态",
506
+ "看看状态",
507
+ }
508
+ continue_shortcuts = {
509
+ "继续",
510
+ "继续做",
511
+ "继续处理",
512
+ "继续推进",
513
+ "继续执行",
514
+ "继续刚刚的工作",
515
+ "接着做",
516
+ "接着处理",
517
+ "直接做",
518
+ "直接开始",
519
+ "开始做",
520
+ "开始吧",
521
+ "开工",
522
+ "马上开始",
523
+ "立即开始",
524
+ "处理掉",
525
+ "解决掉",
526
+ "审核",
527
+ "审核一下",
528
+ "审查",
529
+ "审查一下",
530
+ "验收",
531
+ "验收一下",
532
+ "看下结果",
533
+ "看结果",
534
+ "过一下",
535
+ "通过",
536
+ "不通过",
537
+ "需修改",
538
+ "需要修改",
539
+ "测试",
540
+ "补测试",
541
+ "写测试",
542
+ "加测试",
543
+ "生成测试",
544
+ }
545
+
546
+ if normalized in explain_shortcuts:
547
+ return "explain"
548
+ if normalized in continue_shortcuts:
549
+ return "continue"
550
+ return ""
551
+
552
+
553
+ def classify_governance_request(request: str) -> dict[str, str]:
554
+ lowered = request.lower().strip()
555
+ if not lowered:
556
+ return {}
557
+
558
+ governance_patterns = [
559
+ (
560
+ "cgc-arch",
561
+ "arch",
562
+ [
563
+ "架构文档",
564
+ "更新架构",
565
+ "架构图",
566
+ "系统图",
567
+ "architecture",
568
+ "system map",
569
+ "integration map",
570
+ ],
571
+ "当前请求更适合回写长期架构现状,而不是进入 feature/issue 执行流。",
572
+ "下一步进入 cgc-arch,更新 codecgc/architecture/ 下的当前态文档。",
573
+ ),
574
+ (
575
+ "cgc-req",
576
+ "req",
577
+ [
578
+ "需求文档",
579
+ "requirements",
580
+ "回写需求",
581
+ "补需求",
582
+ "当前能力说明",
583
+ "稳定需求",
584
+ "需求边界",
585
+ "稳定需求边界",
586
+ "能力边界",
587
+ "产品边界",
588
+ ],
589
+ "当前请求更适合回写稳定需求边界,而不是进入一次性执行流。",
590
+ "下一步进入 cgc-req,更新 codecgc/requirements/ 下的稳定能力说明。",
591
+ ),
592
+ (
593
+ "cgc-explore",
594
+ "explore",
595
+ [
596
+ "explore",
597
+ "探索一下",
598
+ "快速熟悉",
599
+ "这个仓库里",
600
+ "怎么实现",
601
+ "module overview",
602
+ "spike",
603
+ ],
604
+ "当前请求更适合登记为定向代码探索,而不是直接进入执行流。",
605
+ "下一步进入 cgc-explore,沉淀这条探索问题或模块概览请求。",
606
+ ),
607
+ (
608
+ "cgc-roadmap",
609
+ "roadmap",
610
+ [
611
+ "roadmap",
612
+ "路线图",
613
+ "分阶段",
614
+ "阶段拆解",
615
+ "大需求拆解",
616
+ "长期规划",
617
+ "生命周期蓝图",
618
+ "发布规划",
619
+ "运维规划",
620
+ "maintenance",
621
+ "release plan",
622
+ "release roadmap",
623
+ "maintenance roadmap",
624
+ "ops roadmap",
625
+ "lifecycle",
626
+ ],
627
+ "当前请求更像 roadmap 级分解,不适合直接落成单个 feature/issue step。",
628
+ "下一步进入 cgc-roadmap,把需求拆成 phases、tracks 或 child workflows。",
629
+ ),
630
+ (
631
+ "cgc-decide",
632
+ "decide",
633
+ [
634
+ "记录决定",
635
+ "记录这个决定",
636
+ "决定记录下来",
637
+ "把这条决定记录下来",
638
+ "记成决定",
639
+ "长期决定",
640
+ "记录为约束",
641
+ "作为长期约束",
642
+ "技术选型",
643
+ "架构决策",
644
+ "adr",
645
+ "decision",
646
+ "约束",
647
+ "编码规约",
648
+ ],
649
+ "当前请求是长期决策归档动作,不应误走 feature/issue 执行流。",
650
+ "下一步进入 cgc-decide,记录已确认的约束、选型或长期规则。",
651
+ ),
652
+ (
653
+ "cgc-learn",
654
+ "learn",
655
+ [
656
+ "沉淀经验",
657
+ "记录经验",
658
+ "沉淀成经验",
659
+ "值得沉淀",
660
+ "值得记录",
661
+ "踩坑",
662
+ "教训",
663
+ "pitfall",
664
+ "learning",
665
+ "最佳实践",
666
+ "经验",
667
+ ],
668
+ "当前请求是经验沉淀动作,不应误走一次性执行流。",
669
+ "下一步进入 cgc-learn,记录可复用的经验、坑点或默认做法。",
670
+ ),
671
+ (
672
+ "cgc-refactor",
673
+ "refactor",
674
+ [
675
+ "重构",
676
+ "refactor",
677
+ "代码优化",
678
+ "结构优化",
679
+ "可读性优化",
680
+ "性能优化",
681
+ ],
682
+ "当前请求更像受控重构,而不是新 feature 或 issue 修复。",
683
+ "下一步进入 cgc-refactor,先确认行为不变边界,再进入受控执行。",
684
+ ),
685
+ (
686
+ "cgc-guide",
687
+ "guide",
688
+ [
689
+ "用户指南",
690
+ "开发者指南",
691
+ "操作指南",
692
+ "使用指南",
693
+ "guide",
694
+ "how to",
695
+ "使用文档",
696
+ ],
697
+ "当前请求更适合沉淀为面向用户或开发者的任务导向指南。",
698
+ "下一步进入 cgc-guide,把这条使用说明沉淀为长期 guide 资产。",
699
+ ),
700
+ (
701
+ "cgc-libdoc",
702
+ "libdoc",
703
+ [
704
+ "api 文档",
705
+ "组件文档",
706
+ "命令文档",
707
+ "参考文档",
708
+ "libdoc",
709
+ "reference doc",
710
+ ],
711
+ "当前请求更适合沉淀为公开表面的参考文档,而不是执行流。",
712
+ "下一步进入 cgc-libdoc,生成对应 API、组件或命令参考资产。",
713
+ ),
714
+ (
715
+ "cgc-trick",
716
+ "trick",
717
+ [
718
+ "技巧",
719
+ "trick",
720
+ "pattern",
721
+ "技术技巧",
722
+ "记录这个用法",
723
+ "记录库用法",
724
+ "最佳用法",
725
+ ],
726
+ "当前请求更适合沉淀为可复用的模式、库用法或技巧处方。",
727
+ "下一步进入 cgc-trick,记录这条可复用技巧资产。",
728
+ ),
729
+ ]
730
+
731
+ for skill, governance_type, keywords, summary, next_step in governance_patterns:
732
+ if any(keyword in lowered for keyword in keywords):
733
+ return {
734
+ "skill": skill,
735
+ "governance_type": governance_type,
736
+ "human_summary": summary,
737
+ "next": next_step,
738
+ }
739
+ return {}
740
+
741
+
742
+ def infer_review_decision_from_request(request: str) -> str:
743
+ lowered = request.lower()
744
+ accepted_keywords = [
745
+ "accept",
746
+ "approve",
747
+ "passed",
748
+ "通过",
749
+ "通过验收",
750
+ "没问题就通过",
751
+ "可以通过",
752
+ ]
753
+ rejected_keywords = [
754
+ "changes requested",
755
+ "request changes",
756
+ "reject",
757
+ "不通过",
758
+ "驳回",
759
+ "需要修改",
760
+ "有问题就退回",
761
+ ]
762
+ if any(keyword in lowered for keyword in rejected_keywords):
763
+ return "changes-requested"
764
+ if any(keyword in lowered for keyword in accepted_keywords):
765
+ return "accepted"
766
+ return ""
767
+
768
+
769
+ def build_request_slug(flow: str, request: str) -> str:
770
+ normalized = re.sub(r"[^a-z0-9]+", "-", request.lower()).strip("-")
771
+ if not normalized:
772
+ normalized = "request"
773
+ normalized = re.sub(r"-{2,}", "-", normalized)
774
+ normalized = "-".join(normalized.split("-")[:6]).strip("-") or "request"
775
+ digest = hashlib.md5(request.encode("utf-8")).hexdigest()[:8]
776
+ return f"{flow}-{normalized}-{digest}"
777
+
778
+
779
+ def resolve_new_slug(args: argparse.Namespace, flow: str) -> str:
780
+ if args.slug.strip():
781
+ return args.slug
782
+ seed = summarize_new_work_seed(args, flow)
783
+ if seed:
784
+ return build_request_slug(flow, seed)
785
+ raise ValueError("New work requires --slug, --request, or enough payload fields to infer a summary.")
786
+
787
+
788
+ def resolve_new_summary(args: argparse.Namespace) -> str:
789
+ if args.summary.strip():
790
+ return args.summary
791
+ if args.request.strip():
792
+ return args.request.strip()
793
+ flow = infer_flow_for_new_work(args)
794
+ seed = summarize_new_work_seed(args, flow)
795
+ if seed:
796
+ return seed
797
+ raise ValueError("New work requires --summary, --request, or enough payload fields to infer a summary.")
798
+
799
+
800
+ def normalize_request_path(path_text: str) -> str:
801
+ normalized = path_text.strip().strip("\"'`")
802
+ normalized = normalized.replace("\\", "/")
803
+ normalized = re.sub(r"/{2,}", "/", normalized)
804
+ return normalized.rstrip(".,;:!?)]}")
805
+
806
+
807
+ def _is_likely_false_positive_path(normalized: str) -> bool:
808
+ segments = normalized.split("/")
809
+ if all(seg.isdigit() for seg in segments if seg):
810
+ return True
811
+ if re.match(r"^\d{4}/\d{2}(/\d{2})?$", normalized):
812
+ return True
813
+ if re.match(r"^v?\d+\.\d+", segments[0]):
814
+ return True
815
+ if normalized.count("/") == 1 and "." not in normalized:
816
+ first, second = segments[0], segments[1] if len(segments) > 1 else ""
817
+ if first.isdigit() or second.isdigit():
818
+ return True
819
+ return False
820
+
821
+
822
+ def extract_target_paths_from_request(request: str) -> list[str]:
823
+ if not request.strip():
824
+ return []
825
+
826
+ matches = re.findall(r"(?:(?:[A-Za-z]:)?[\\/])?(?:[\w.\-]+[\\/])+[\w.\-]+", request)
827
+ extracted: list[str] = []
828
+ seen: set[str] = set()
829
+
830
+ for raw in matches:
831
+ normalized = normalize_request_path(raw)
832
+ if "/" not in normalized:
833
+ continue
834
+ lowered = normalized.lower()
835
+ if lowered.startswith("http/") or lowered.startswith("https/"):
836
+ continue
837
+ if _is_likely_false_positive_path(lowered):
838
+ continue
839
+ if lowered in seen:
840
+ continue
841
+ seen.add(lowered)
842
+ extracted.append(normalized)
843
+
844
+ return extracted
845
+
846
+
847
+ def classify_request_paths(paths: list[str]) -> str:
848
+ if not paths:
849
+ return "auto"
850
+
851
+ routing = load_simple_routing_config(ROUTING_FILE)
852
+ categories = {classify_path(path, routing) for path in paths}
853
+
854
+ if categories == {"frontend"}:
855
+ return "frontend"
856
+ if categories == {"backend"}:
857
+ return "backend"
858
+ return "auto"
859
+
860
+
861
+ def resolve_target_paths(args: argparse.Namespace) -> list[str]:
862
+ explicit = [item.strip() for item in args.target_path if item.strip()]
863
+ if explicit:
864
+ return explicit
865
+ return extract_target_paths_from_request(args.request)
866
+
867
+
868
+ def resolve_kind(args: argparse.Namespace, target_paths: list[str]) -> str:
869
+ if args.kind != "auto":
870
+ return args.kind
871
+ return classify_request_paths(target_paths)
872
+
873
+
874
+ def cleaned_items(items: list[str]) -> list[str]:
875
+ return [item.strip() for item in items if item.strip()]
876
+
877
+
878
+ def extract_clause(request: str, patterns: list[str]) -> str:
879
+ if not request.strip():
880
+ return ""
881
+ for pattern in patterns:
882
+ match = re.search(pattern, request, flags=re.IGNORECASE)
883
+ if not match:
884
+ continue
885
+ value = str(match.group(1)).strip(" ,,;;。\"'")
886
+ if value:
887
+ return value
888
+ return ""
889
+
890
+
891
+ def infer_feature_goal(args: argparse.Namespace) -> str:
892
+ if args.goal.strip():
893
+ return args.goal
894
+ request = args.request.strip()
895
+ if not request:
896
+ return ""
897
+ trimmed = re.split(r"[,,。;;]", request, maxsplit=1)[0].strip()
898
+ return trimmed or request
899
+
900
+
901
+ def infer_feature_user_story(args: argparse.Namespace) -> str:
902
+ if args.user_story.strip():
903
+ return args.user_story
904
+ return extract_clause(
905
+ args.request,
906
+ [
907
+ r"(作为[^,,。;;]*?[我你他她它]?(?:想|希望|需要)[^,,。;;]*)",
908
+ r"(As\s+an?[^,.;\n]*?(?:I want|I need)[^,.;\n]*)",
909
+ ],
910
+ )
911
+
912
+
913
+ def infer_in_scope(args: argparse.Namespace, flow: str, target_paths: list[str]) -> list[str]:
914
+ explicit = [item.strip() for item in args.in_scope if item.strip()]
915
+ if explicit:
916
+ return explicit
917
+ inferred = extract_clause(
918
+ args.request,
919
+ [
920
+ r"范围(?:只)?包括(.+?)(?:[,,;;。]|$)",
921
+ r"只包括(.+?)(?:[,,;;。]|$)",
922
+ r"in scope(?: is|:)?\s*(.+?)(?:[,.;\n]|$)",
923
+ ],
924
+ )
925
+ if inferred:
926
+ return [inferred]
927
+
928
+ if target_paths:
929
+ if flow == "feature":
930
+ return [f"Implement the requested feature only in {', '.join(target_paths)}."]
931
+ if flow == "issue":
932
+ return [f"Fix the reported issue only in {', '.join(target_paths)}."]
933
+ return []
934
+
935
+
936
+ def infer_acceptance(args: argparse.Namespace, flow: str) -> list[str]:
937
+ explicit = [item.strip() for item in args.acceptance if item.strip()]
938
+ if explicit:
939
+ return explicit
940
+
941
+ inferred = extract_clause(
942
+ args.request,
943
+ [
944
+ r"验收(?:标准)?(?:是|为|包括)?(.+?)(?:[,,;;。]|$)",
945
+ r"acceptance(?: criteria)?(?: is| are|:)?\s*(.+?)(?:[,.;\n]|$)",
946
+ ],
947
+ )
948
+ if inferred:
949
+ return [inferred]
950
+
951
+ if flow == "issue":
952
+ expected = infer_issue_expected(args)
953
+ if expected:
954
+ return [expected]
955
+ return []
956
+
957
+
958
+ def infer_issue_symptom(args: argparse.Namespace) -> str:
959
+ if args.symptom.strip():
960
+ return args.symptom
961
+ request = args.request.strip()
962
+ if not request:
963
+ return ""
964
+ before_expected = re.split(r"预期|expected", request, maxsplit=1, flags=re.IGNORECASE)[0].strip(" ,,;;。")
965
+ return before_expected
966
+
967
+
968
+ def infer_issue_expected(args: argparse.Namespace) -> str:
969
+ if args.expected.strip():
970
+ return args.expected
971
+ return extract_clause(
972
+ args.request,
973
+ [
974
+ r"预期(?:是|为)?(.+?)(?:[,,;;。]|$)",
975
+ r"expected(?: is|:)?\s*(.+?)(?:[,.;\n]|$)",
976
+ ],
977
+ )
978
+
979
+
980
+ def infer_issue_actual(args: argparse.Namespace) -> str:
981
+ if args.actual.strip():
982
+ return args.actual
983
+ return extract_clause(
984
+ args.request,
985
+ [
986
+ r"实际(?:是|为)?(.+?)(?:[,,;;。]|$)",
987
+ r"actual(?: is|:)?\s*(.+?)(?:[,.;\n]|$)",
988
+ ],
989
+ )
990
+
991
+
992
+ def clean_placeholder_text(value: str) -> str:
993
+ cleaned = value.strip()
994
+ placeholders = {
995
+ "",
996
+ "TODO",
997
+ "todo",
998
+ "待补充",
999
+ "当前无。",
1000
+ "当前无",
1001
+ "无",
1002
+ }
1003
+ return "" if cleaned in placeholders else cleaned
1004
+
1005
+
1006
+ def read_text_if_exists(path: Path | None) -> str:
1007
+ if not path or not path.exists():
1008
+ return ""
1009
+ return path.read_text(encoding="utf-8-sig")
1010
+
1011
+
1012
+ def find_first_artifact_file(directory: Path, pattern: str) -> Path | None:
1013
+ matches = sorted(directory.glob(pattern))
1014
+ return matches[0] if matches else None
1015
+
1016
+
1017
+ def extract_frontmatter_value(text: str, key: str) -> str:
1018
+ if not text.strip():
1019
+ return ""
1020
+ match = re.search(rf"(?m)^{re.escape(key)}:\s*(.+?)\s*$", text)
1021
+ if not match:
1022
+ return ""
1023
+ return clean_placeholder_text(str(match.group(1)).strip().strip("\"'"))
1024
+
1025
+
1026
+ def extract_markdown_bullet_value(text: str, label: str) -> str:
1027
+ if not text.strip():
1028
+ return ""
1029
+ match = re.search(rf"(?m)^\s*-\s*{re.escape(label)}:\s*(.+?)\s*$", text)
1030
+ if not match:
1031
+ return ""
1032
+ return clean_placeholder_text(str(match.group(1)).strip())
1033
+
1034
+
1035
+ def extract_markdown_section_bullets(text: str, heading: str) -> list[str]:
1036
+ if not text.strip():
1037
+ return []
1038
+ pattern = rf"{re.escape(heading)}\s*\n(?P<body>.*?)(?=\n##\s|\Z)"
1039
+ match = re.search(pattern, text, flags=re.DOTALL)
1040
+ if not match:
1041
+ return []
1042
+
1043
+ bullets: list[str] = []
1044
+ for raw_line in str(match.group("body")).splitlines():
1045
+ stripped = raw_line.strip()
1046
+ if not stripped.startswith("- "):
1047
+ continue
1048
+ value = clean_placeholder_text(stripped[2:].strip())
1049
+ if value:
1050
+ bullets.append(value)
1051
+ return bullets
1052
+
1053
+
1054
+ def unique_nonempty(items: list[str]) -> list[str]:
1055
+ result: list[str] = []
1056
+ seen: set[str] = set()
1057
+ for item in items:
1058
+ cleaned = clean_placeholder_text(str(item))
1059
+ if not cleaned:
1060
+ continue
1061
+ key = cleaned.lower()
1062
+ if key in seen:
1063
+ continue
1064
+ seen.add(key)
1065
+ result.append(cleaned)
1066
+ return result
1067
+
1068
+
1069
+ def load_step_contract_context(checklist_path: Path | None) -> dict[str, Any]:
1070
+ if not checklist_path or not checklist_path.exists():
1071
+ return {}
1072
+
1073
+ data = load_checklist_yaml(checklist_path)
1074
+ steps = data.get("steps", [])
1075
+ if not isinstance(steps, list):
1076
+ return {}
1077
+
1078
+ target_paths: list[str] = []
1079
+ acceptance: list[str] = []
1080
+ kinds: list[str] = []
1081
+
1082
+ for step in steps:
1083
+ if not isinstance(step, dict):
1084
+ continue
1085
+ codecgc = step.get("codecgc")
1086
+ if not isinstance(codecgc, dict):
1087
+ continue
1088
+ kinds.append(str(codecgc.get("kind", "")).strip())
1089
+ target_paths.extend(str(item).strip() for item in codecgc.get("target_paths", []) if str(item).strip())
1090
+ acceptance.extend(str(item).strip() for item in codecgc.get("acceptance", []) if str(item).strip())
1091
+
1092
+ distinct_kinds = {kind for kind in kinds if kind}
1093
+ resolved_kind = ""
1094
+ if len(distinct_kinds) == 1:
1095
+ resolved_kind = next(iter(distinct_kinds))
1096
+ elif len(distinct_kinds) > 1:
1097
+ resolved_kind = "auto"
1098
+
1099
+ return {
1100
+ "kind": resolved_kind,
1101
+ "target_paths": unique_nonempty(target_paths),
1102
+ "acceptance": unique_nonempty(acceptance),
1103
+ }
1104
+
1105
+
1106
+ def build_existing_workflow_replan_payload(flow: str, slug: str) -> dict[str, Any]:
1107
+ discovered = discover_flow_directory(flow, slug, "auto")
1108
+ if not discovered:
1109
+ return {}
1110
+
1111
+ _, directory = discovered
1112
+ payload: dict[str, Any] = {
1113
+ "flow": flow,
1114
+ "slug": slug,
1115
+ }
1116
+
1117
+ if flow == "issue":
1118
+ report_text = read_text_if_exists(find_first_artifact_file(directory, "*-report.md"))
1119
+ analysis_text = read_text_if_exists(find_first_artifact_file(directory, "*-analysis.md"))
1120
+ step_context = load_step_contract_context(find_first_artifact_file(directory, "*-fix.yaml"))
1121
+ payload["summary"] = extract_frontmatter_value(report_text, "summary")
1122
+ payload["user_story"] = extract_markdown_bullet_value(report_text, "用户影响")
1123
+ payload["symptom"] = extract_markdown_bullet_value(report_text, "现象")
1124
+ payload["expected"] = extract_markdown_bullet_value(report_text, "预期")
1125
+ payload["actual"] = extract_markdown_bullet_value(report_text, "实际")
1126
+ payload["in_scope"] = extract_markdown_section_bullets(analysis_text, "## 2. 范围")
1127
+ else:
1128
+ design_text = read_text_if_exists(find_first_artifact_file(directory, "*-design.md"))
1129
+ step_context = load_step_contract_context(find_first_artifact_file(directory, "*-checklist.yaml"))
1130
+ payload["summary"] = extract_frontmatter_value(design_text, "summary")
1131
+ payload["goal"] = extract_markdown_bullet_value(design_text, "用户目标")
1132
+ payload["user_story"] = extract_markdown_bullet_value(design_text, "用户故事")
1133
+ payload["in_scope"] = extract_markdown_section_bullets(design_text, "## 3. 范围内")
1134
+
1135
+ payload.update(step_context)
1136
+ task_id = str(step_context.get("task_id", "")).strip()
1137
+ reusable_session_id = resolve_session_id_from_task(task_id, discovered[0]) if task_id else ""
1138
+ if reusable_session_id:
1139
+ payload["session_id"] = reusable_session_id
1140
+
1141
+ if not payload.get("summary"):
1142
+ payload["summary"] = slug
1143
+
1144
+ return {
1145
+ key: value
1146
+ for key, value in payload.items()
1147
+ if has_planning_value(value) and not (key == "kind" and value == "auto")
1148
+ }
1149
+
1150
+
1151
+ def has_planning_value(value: Any) -> bool:
1152
+ if isinstance(value, list):
1153
+ return any(str(item).strip() for item in value)
1154
+ return bool(str(value).strip()) if isinstance(value, str) else bool(value)
1155
+
1156
+
1157
+ def build_planning_snapshot(flow: str, args: argparse.Namespace, target_paths: list[str], resolved_kind: str) -> dict[str, Any]:
1158
+ snapshot: dict[str, Any] = {
1159
+ "kind": resolved_kind,
1160
+ "target_paths": target_paths,
1161
+ "context": cleaned_items(args.context),
1162
+ }
1163
+
1164
+ if flow == "feature":
1165
+ snapshot.update(
1166
+ {
1167
+ "goal": infer_feature_goal(args),
1168
+ "user_story": infer_feature_user_story(args),
1169
+ "in_scope": infer_in_scope(args, flow, target_paths),
1170
+ "acceptance": infer_acceptance(args, flow),
1171
+ }
1172
+ )
1173
+ else:
1174
+ snapshot.update(
1175
+ {
1176
+ "symptom": infer_issue_symptom(args),
1177
+ "expected": infer_issue_expected(args),
1178
+ "actual": infer_issue_actual(args),
1179
+ "in_scope": infer_in_scope(args, flow, target_paths),
1180
+ "acceptance": infer_acceptance(args, flow),
1181
+ }
1182
+ )
1183
+ return snapshot
1184
+
1185
+
1186
+ def build_captured_fields(snapshot: dict[str, Any]) -> dict[str, Any]:
1187
+ return {
1188
+ key: value
1189
+ for key, value in snapshot.items()
1190
+ if has_planning_value(value) and not (key == "kind" and value == "auto")
1191
+ }
1192
+
1193
+
1194
+ def build_clarification_mode(flow: str, missing_fields: list[str], resolved_kind: str) -> str:
1195
+ missing = set(missing_fields)
1196
+ if not missing:
1197
+ return ""
1198
+ if missing == {"target_paths"}:
1199
+ return "path-discovery"
1200
+ if "routing_coverage" in missing or resolved_kind == "auto":
1201
+ return f"{flow}-routing-clarification"
1202
+ return f"{flow}-beginner"
1203
+
1204
+
1205
+ def build_clarification_prompts(flow: str, missing_fields: list[str]) -> list[dict[str, str]]:
1206
+ prompts: list[dict[str, str]] = []
1207
+ mapping = {
1208
+ "target_paths": {
1209
+ "field": "target_paths",
1210
+ "question": "这次改动具体落在哪些文件或目录?",
1211
+ "example": "例如:src/components/LoginForm.tsx 或 backend/src/sync.py",
1212
+ },
1213
+ "user_story": {
1214
+ "field": "user_story",
1215
+ "question": "这个需求从用户视角想解决什么问题?",
1216
+ "example": "例如:作为用户,我想快速登录,以便进入系统。",
1217
+ },
1218
+ "goal": {
1219
+ "field": "goal",
1220
+ "question": "这次需求的目标是什么?",
1221
+ "example": "例如:提供一个独立登录入口页面。",
1222
+ },
1223
+ "in_scope": {
1224
+ "field": "in_scope",
1225
+ "question": "这次明确要做的范围是什么?",
1226
+ "example": "例如:只新增登录表单,不改后端接口。",
1227
+ },
1228
+ "acceptance": {
1229
+ "field": "acceptance",
1230
+ "question": "怎么才算完成?",
1231
+ "example": "例如:页面展示邮箱和密码输入框,并可提交。",
1232
+ },
1233
+ "symptom": {
1234
+ "field": "symptom",
1235
+ "question": "这个问题的现象是什么?",
1236
+ "example": "例如:批量同步时报错,中途停止。",
1237
+ },
1238
+ "expected": {
1239
+ "field": "expected",
1240
+ "question": "你预期正确行为应该是什么?",
1241
+ "example": "例如:坏记录被跳过,其余记录继续处理。",
1242
+ },
1243
+ "actual": {
1244
+ "field": "actual",
1245
+ "question": "现在实际发生了什么?",
1246
+ "example": "例如:遇到一条坏记录后整批停止。",
1247
+ },
1248
+ "routing_coverage": {
1249
+ "field": "routing_coverage",
1250
+ "question": "这些路径应该归前端、后端,还是需要先拆分?",
1251
+ "example": "例如:src/components/** 归前端,backend/** 归后端。",
1252
+ },
1253
+ }
1254
+
1255
+ preferred_order = (
1256
+ ["target_paths", "goal", "user_story", "in_scope", "acceptance"]
1257
+ if flow == "feature"
1258
+ else ["target_paths", "symptom", "expected", "actual", "in_scope", "acceptance"]
1259
+ )
1260
+ ordered_fields = preferred_order + [field for field in missing_fields if field not in preferred_order]
1261
+
1262
+ seen: set[str] = set()
1263
+ for field in ordered_fields:
1264
+ if field not in missing_fields or field in seen:
1265
+ continue
1266
+ seen.add(field)
1267
+ prompt = mapping.get(field)
1268
+ if prompt:
1269
+ prompts.append(prompt)
1270
+ return prompts
1271
+
1272
+
1273
+ def build_suggested_reply_payload(flow: str, snapshot: dict[str, Any], missing_fields: list[str]) -> dict[str, Any]:
1274
+ field_order = (
1275
+ ["target_paths", "goal", "user_story", "in_scope", "acceptance"]
1276
+ if flow == "feature"
1277
+ else ["target_paths", "symptom", "expected", "actual", "in_scope", "acceptance"]
1278
+ )
1279
+ list_fields = {"target_paths", "in_scope", "acceptance"}
1280
+ payload: dict[str, Any] = {"flow": flow}
1281
+
1282
+ kind = str(snapshot.get("kind", "")).strip()
1283
+ if kind and kind != "auto":
1284
+ payload["kind"] = kind
1285
+
1286
+ missing = set(missing_fields)
1287
+ for field in field_order:
1288
+ value = snapshot.get(field)
1289
+ if field in missing:
1290
+ payload[field] = [] if field in list_fields else ""
1291
+ elif has_planning_value(value):
1292
+ payload[field] = value
1293
+ return payload
1294
+
1295
+
1296
+ def build_suggested_reply_template(flow: str, prompts: list[dict[str, str]]) -> str:
1297
+ if not prompts:
1298
+ return ""
1299
+
1300
+ lines: list[str] = []
1301
+ if flow == "feature":
1302
+ lines.append("请补充下面这些信息后我再继续推进:")
1303
+ else:
1304
+ lines.append("请补充下面这些问题信息后我再继续修复:")
1305
+
1306
+ for prompt in prompts:
1307
+ question = str(prompt.get("question", "")).strip()
1308
+ example = str(prompt.get("example", "")).strip()
1309
+ if not question:
1310
+ continue
1311
+ lines.append(f"- {question}")
1312
+ if example:
1313
+ lines.append(f" 参考:{example}")
1314
+ return "\n".join(lines)
1315
+
1316
+
1317
+ def resolve_effective_auto_dispatch(
1318
+ args: argparse.Namespace,
1319
+ recommended_command: str,
1320
+ explain_only: bool = False,
1321
+ ) -> tuple[bool, str]:
1322
+ if explain_only:
1323
+ return False, "explain-mode"
1324
+ if args.auto_dispatch:
1325
+ return True, "explicit-flag"
1326
+
1327
+ request = args.request.strip()
1328
+ if not request:
1329
+ return False, ""
1330
+
1331
+ if matches_command(recommended_command, "cgc-build", "cgc-fix") and (
1332
+ request_implies_execute(request) or request_implies_continue(request)
1333
+ ):
1334
+ return True, "request-implies-execution"
1335
+
1336
+ if matches_command(recommended_command, "cgc-review") and request_implies_review(request):
1337
+ inferred_decision = infer_review_decision_from_request(request)
1338
+ if inferred_decision and not args.decision:
1339
+ args.decision = inferred_decision
1340
+ if args.decision:
1341
+ return True, "request-implies-review"
1342
+ return False, "review-decision-missing"
1343
+
1344
+ return False, ""
1345
+
1346
+
1347
+ def build_route_status_summary(route: dict[str, Any]) -> dict[str, Any]:
1348
+ review = route.get("review", {}) if isinstance(route.get("review"), dict) else {}
1349
+ current_step = route.get("current_step", {}) if isinstance(route.get("current_step"), dict) else {}
1350
+ recommended_command = str(route.get("recommended_command", "")).strip()
1351
+ audit_path = str(route.get("audit_path", "")).strip()
1352
+ task_id = str(current_step.get("task_id", "")).strip()
1353
+ artifact_class = str(route.get("artifact_class", "")).strip() or "product"
1354
+ reusable_session_id = resolve_session_id_from_task(task_id, artifact_class) if task_id else ""
1355
+
1356
+ workflow_state = "closed"
1357
+ if matches_command(recommended_command, "cgc-plan"):
1358
+ workflow_state = "needs-planning"
1359
+ elif matches_command(recommended_command, "cgc-build"):
1360
+ workflow_state = "awaiting-build"
1361
+ elif matches_command(recommended_command, "cgc-fix"):
1362
+ workflow_state = "awaiting-fix"
1363
+ elif matches_command(recommended_command, "cgc-review"):
1364
+ workflow_state = "awaiting-review"
1365
+ elif current_step:
1366
+ workflow_state = "step-selected"
1367
+
1368
+ flow_step_labels = {
1369
+ "cgc-build": "功能开发步骤",
1370
+ "cgc-fix": "问题修复步骤",
1371
+ "cgc-test": "测试步骤",
1372
+ }
1373
+
1374
+ human_status_summary = "当前工作流已关闭。"
1375
+ operator_action_summary = "如需继续,先新增后续步骤,或回到规划阶段。"
1376
+ if matches_command(recommended_command, "cgc-plan"):
1377
+ human_status_summary = "当前工作流还需要回到规划阶段补齐可执行信息。"
1378
+ operator_action_summary = "进入规划阶段,补齐范围、归属或可执行步骤。"
1379
+ elif matches_command(recommended_command, "cgc-build"):
1380
+ human_status_summary = "当前工作流正在等待前端或功能实现执行。"
1381
+ operator_action_summary = f"执行当前{flow_step_labels['cgc-build']}。"
1382
+ elif matches_command(recommended_command, "cgc-fix"):
1383
+ human_status_summary = "当前工作流正在等待后端或问题修复执行。"
1384
+ operator_action_summary = f"执行当前{flow_step_labels['cgc-fix']}。"
1385
+ elif matches_command(recommended_command, "cgc-test"):
1386
+ human_status_summary = "当前工作流正在等待测试步骤执行。"
1387
+ operator_action_summary = f"执行当前{flow_step_labels['cgc-test']}。"
1388
+ elif matches_command(recommended_command, "cgc-review"):
1389
+ human_status_summary = "当前工作流已有执行证据,正在等待审核决策。"
1390
+ operator_action_summary = "基于最新审计产物执行审核,并写回“通过”或“需修改”。"
1391
+ elif current_step:
1392
+ human_status_summary = "当前工作流已选中步骤,但还没有明确的后续命令。"
1393
+ operator_action_summary = "检查 route 原因,并确认是否需要回到规划阶段。"
1394
+
1395
+ review_fallback_stage = str(review.get("fallback_stage", "")).strip()
1396
+ review_action_kind = str(review.get("action_kind", "")).strip()
1397
+ review_policy_reason = str(review.get("policy_reason", "")).strip()
1398
+ if recommended_command == "" and review_fallback_stage == "closed":
1399
+ human_status_summary = "当前工作流已关闭,最近一次审核已确认该步骤可以结束。"
1400
+ operator_action_summary = "如需继续,新增后续步骤;否则当前结果已可视为通过。"
1401
+
1402
+ return {
1403
+ "workflow_state": workflow_state,
1404
+ "recommended_command": recommended_command,
1405
+ "current_step_number": int(current_step.get("step_number", 0) or 0),
1406
+ "current_task_id": str(current_step.get("task_id", "")),
1407
+ "current_kind": str(current_step.get("kind", "")),
1408
+ "current_target_paths": current_step.get("target_paths", []),
1409
+ "review_decision": str(review.get("decision", "")),
1410
+ "review_step_number": int(review.get("step_number", 0) or 0),
1411
+ "review_fallback_stage": review_fallback_stage,
1412
+ "review_policy_reason": review_policy_reason,
1413
+ "review_action_kind": review_action_kind,
1414
+ "audit_path": audit_path,
1415
+ "reusable_session_id": reusable_session_id,
1416
+ "human_status_summary": human_status_summary,
1417
+ "operator_action_summary": operator_action_summary,
1418
+ "is_closed": recommended_command == "",
1419
+ "needs_review_decision": matches_command(recommended_command, "cgc-review"),
1420
+ "needs_execution": matches_command(recommended_command, "cgc-build", "cgc-fix", "cgc-test"),
1421
+ }
1422
+
1423
+
1424
+ def build_review_policy_status(dispatch_result: dict[str, Any]) -> dict[str, str]:
1425
+ fallback_stage = str(dispatch_result.get("fallback_stage", "")).strip()
1426
+ action_kind = str(dispatch_result.get("recommended_action_kind", "")).strip()
1427
+ policy_reason = str(dispatch_result.get("policy_reason", "")).strip()
1428
+ action_kind_labels = {
1429
+ "execute-for-real": "执行一次真实运行",
1430
+ }
1431
+
1432
+ human_status_summary = ""
1433
+ operator_action_summary = ""
1434
+
1435
+ if fallback_stage == "closed":
1436
+ human_status_summary = "当前审核已完成,并确认该步骤可以关闭。"
1437
+ operator_action_summary = "结束当前步骤,或继续处理新的后续工作。"
1438
+ elif fallback_stage == "planning":
1439
+ human_status_summary = "当前审核已把工作退回规划阶段。"
1440
+ operator_action_summary = "回到 cgc-plan,修正范围、归属或拆分策略。"
1441
+ elif fallback_stage == "execution":
1442
+ human_status_summary = "当前审核认为还需要重新执行或补做实现。"
1443
+ if action_kind == "execute-for-real":
1444
+ operator_action_summary = "执行一次非 dry-run 的真实执行。"
1445
+ else:
1446
+ operator_action_summary = "修正实现后重新执行当前步骤。"
1447
+ elif fallback_stage == "review":
1448
+ human_status_summary = "当前审核结果仍需进一步确认。"
1449
+ operator_action_summary = "重新检查审核输入与决策依据。"
1450
+
1451
+ return {
1452
+ "fallback_stage": fallback_stage,
1453
+ "recommended_action_kind": action_kind,
1454
+ "policy_reason": policy_reason,
1455
+ "human_status_summary": human_status_summary,
1456
+ "operator_action_summary": operator_action_summary,
1457
+ }
1458
+
1459
+
1460
+ def infer_dispatch_recovery(command: str, failure_type: str, state: str, next_step: str) -> dict[str, str]:
1461
+ normalized_failure = failure_type.strip()
1462
+ normalized_state = state.strip()
1463
+ normalized_next = next_step.strip()
1464
+ public_command = to_public_command(command)
1465
+
1466
+ if normalized_failure == "workflow-state":
1467
+ recovery_command = public_command or to_public_command("cgc-build")
1468
+ retry_kind = "rerun-without-dry-run" if normalized_state == "executed-dry-run" else "workflow-repair"
1469
+ retry_hint = normalized_next or "先修复当前工作流就绪状态,再重新执行同一个 step。"
1470
+ return {
1471
+ "recovery_command": recovery_command,
1472
+ "recovery_kind": retry_kind,
1473
+ "retry_hint": retry_hint,
1474
+ }
1475
+
1476
+ if normalized_failure in {"scope-error", "design-gap"}:
1477
+ retry_hint = normalized_next or "先回到规划阶段,收窄范围,并重新生成可执行 step。"
1478
+ return {
1479
+ "recovery_command": to_public_command("cgc-plan"),
1480
+ "recovery_kind": "returned-to-planning",
1481
+ "retry_hint": retry_hint,
1482
+ }
1483
+
1484
+ if normalized_failure == "environment-or-tooling":
1485
+ retry_hint = normalized_next or "先修复本地环境、缺失文件或超时条件,再重试当前 step。"
1486
+ return {
1487
+ "recovery_command": "",
1488
+ "recovery_kind": "repair-environment",
1489
+ "retry_hint": retry_hint,
1490
+ }
1491
+
1492
+ if normalized_failure == "executor-failure":
1493
+ retry_hint = normalized_next or "先检查执行器输出和审计产物,修复执行器侧失败后再重试。"
1494
+ return {
1495
+ "recovery_command": public_command,
1496
+ "recovery_kind": "inspect-executor",
1497
+ "retry_hint": retry_hint,
1498
+ }
1499
+
1500
+ return {
1501
+ "recovery_command": public_command,
1502
+ "recovery_kind": "",
1503
+ "retry_hint": normalized_next,
1504
+ }
1505
+
1506
+
1507
+ def build_dispatch_failure_user_summary(dispatch_result: dict[str, Any], command: str) -> tuple[str, str]:
1508
+ failure_type = str(dispatch_result.get("failure_type", "")).strip()
1509
+ dispatch_state = str(dispatch_result.get("state", "")).strip()
1510
+ recovery = infer_dispatch_recovery(command, failure_type, dispatch_state, str(dispatch_result.get("next", "")).strip())
1511
+ recovery_command = str(recovery.get("recovery_command", "")).strip()
1512
+ split_suggestion = dispatch_result.get("split_suggestion", {}) if isinstance(dispatch_result.get("split_suggestion"), dict) else {}
1513
+
1514
+ if failure_type == "workflow-state":
1515
+ if dispatch_state == "executed-dry-run":
1516
+ return "本次只完成了预演执行,尚未真正完成代码改动。", "去掉 dry-run 后重新执行当前步骤。"
1517
+ return "当前步骤还不满足执行条件,执行尚未完成。", f"先修复工作流状态,再通过 {recovery_command or '当前执行命令'} 重试。"
1518
+
1519
+ if failure_type == "environment-or-tooling":
1520
+ return "当前卡在本地环境或工具前置条件,执行尚未完成。", "先修复缺失目录、依赖、权限或超时条件,再重试当前步骤。"
1521
+
1522
+ if failure_type == "executor-failure":
1523
+ return "当前卡在执行器返回异常,需要检查执行日志与审计产物。", f"先检查执行器输出,修复后再通过 {recovery_command or '当前执行命令'} 重试。"
1524
+
1525
+ if failure_type in {"scope-error", "design-gap"}:
1526
+ if failure_type == "scope-error" and split_suggestion:
1527
+ suggested_steps = split_suggestion.get("suggested_split_steps", [])
1528
+ if isinstance(suggested_steps, list):
1529
+ parts: list[str] = []
1530
+ for item in suggested_steps:
1531
+ if not isinstance(item, dict):
1532
+ continue
1533
+ kind = str(item.get("kind", "")).strip()
1534
+ executor = str(item.get("executor", "")).strip()
1535
+ paths = [str(path).strip() for path in item.get("target_paths", []) if str(path).strip()]
1536
+ if not paths:
1537
+ continue
1538
+ parts.append(f"{kind}->{executor}: {', '.join(paths)}")
1539
+ if parts:
1540
+ return (
1541
+ "当前步骤混合了前后端或 shared 范围,执行已返回规划阶段。",
1542
+ "建议按以下方式拆分后回到 cgc-plan:" + ";".join(parts),
1543
+ )
1544
+ return "当前步骤的规划、范围或目标路径还不够清晰,执行已返回规划阶段。", "先回到 cgc-plan 补齐规划、修正目标路径或重新拆分步骤,再继续执行。"
1545
+
1546
+ fallback_summary = str(dispatch_result.get("summary", "")).strip()
1547
+ if fallback_summary:
1548
+ return "自动调度未完成,需要先处理当前阻塞。", fallback_summary
1549
+ return "自动调度未完成,需要先处理当前阻塞。", "先检查当前步骤状态和执行日志,再决定是否重试。"
1550
+
1551
+
1552
+ def build_dispatch_recovery_user_hint(dispatch_result: dict[str, Any], command: str) -> str:
1553
+ _, next_step = build_dispatch_failure_user_summary(dispatch_result, command)
1554
+ return next_step
1555
+
1556
+
1557
+ def build_command_user_next(command: str) -> str:
1558
+ if matches_command(command, "cgc-build"):
1559
+ return "执行当前功能开发步骤。"
1560
+ if matches_command(command, "cgc-fix"):
1561
+ return "执行当前问题修复步骤。"
1562
+ if matches_command(command, "cgc-plan"):
1563
+ return "回到规划阶段,补齐可执行信息。"
1564
+ if matches_command(command, "cgc-review"):
1565
+ return "基于最新审计产物完成审核,并写回“通过”或“需修改”。"
1566
+ return ""
1567
+
1568
+
1569
+ def compose_user_reply(summary: str, next_text: str, prefix: str = "下一步:") -> str:
1570
+ normalized_summary = summary.strip()
1571
+ normalized_next = next_text.strip()
1572
+ if normalized_summary and normalized_next:
1573
+ if normalized_next in normalized_summary:
1574
+ return normalized_summary
1575
+ return f"{normalized_summary}\n\n{prefix}{normalized_next}"
1576
+ return normalized_summary or normalized_next
1577
+
1578
+
1579
+ def apply_dispatch_failure_context(result: dict[str, Any], fallback_command: str) -> None:
1580
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
1581
+ if not dispatch_result or dispatch_result.get("success"):
1582
+ return
1583
+
1584
+ failure_command = str(dispatch_result.get("recommended_command", "")).strip() or fallback_command
1585
+ recovery = infer_dispatch_recovery(
1586
+ failure_command,
1587
+ str(dispatch_result.get("failure_type", "")).strip(),
1588
+ str(dispatch_result.get("state", "")).strip(),
1589
+ str(dispatch_result.get("next", "")).strip(),
1590
+ )
1591
+ retry_hint = str(recovery.get("retry_hint", "")).strip()
1592
+ user_summary, user_next = build_dispatch_failure_user_summary(dispatch_result, failure_command)
1593
+
1594
+ result["recommended_command"] = failure_command
1595
+ result["next"] = user_next or retry_hint or user_summary
1596
+
1597
+
1598
+ def describe_missing_fields(missing_fields: list[str]) -> str:
1599
+ labels = {
1600
+ "target_paths": "目标文件或目录",
1601
+ "goal": "需求目标",
1602
+ "user_story": "用户故事",
1603
+ "in_scope": "本次范围",
1604
+ "acceptance": "验收标准",
1605
+ "symptom": "问题现象",
1606
+ "expected": "预期行为",
1607
+ "actual": "实际行为",
1608
+ "routing_coverage": "前后端归属",
1609
+ "planning_blockers": "规划阻塞项",
1610
+ "executable_step": "可执行步骤定义",
1611
+ }
1612
+ described = [labels.get(str(item), str(item)) for item in missing_fields]
1613
+ return "、".join(item for item in described if item)
1614
+
1615
+
1616
+ def build_user_facing_next(result: dict[str, Any]) -> str:
1617
+ if str(result.get("entry_mode", "")).strip() == "governance":
1618
+ return str(result.get("next", "")).strip()
1619
+
1620
+ entry_mode = str(result.get("entry_mode", "")).strip()
1621
+ if entry_mode == "new":
1622
+ planning_status = str(result.get("planning_status", "")).strip()
1623
+ recommended_command = str(result.get("recommended_command", "")).strip()
1624
+ if planning_status == "needs-clarification":
1625
+ return "请先补充缺失信息,我再继续完成规划和路由。"
1626
+ command_next = build_command_user_next(recommended_command)
1627
+ if command_next:
1628
+ return command_next
1629
+ return str(result.get("next", "")).strip()
1630
+
1631
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
1632
+ recommended_command = str(result.get("recommended_command", "")).strip()
1633
+ workflow_state = str(route_status.get("workflow_state", "")).strip()
1634
+ if workflow_state == "needs-new-workflow":
1635
+ return "先直接描述一个新需求,或显式提供已有工作流的 slug。"
1636
+ if workflow_state == "needs-review-target":
1637
+ return "先定位一个待审核工作流,或先完成执行步骤后再回来审核。"
1638
+ if bool(route_status.get("is_closed")):
1639
+ return str(route_status.get("operator_action_summary", "")).strip() or "如需继续,先新增后续步骤,或回到规划阶段。"
1640
+ if bool(route_status.get("needs_review_decision")):
1641
+ return "基于最新审计产物执行审核,并写回“通过”或“需修改”。"
1642
+ command_next = build_command_user_next(recommended_command)
1643
+ if command_next:
1644
+ return command_next
1645
+ return str(result.get("next", "")).strip()
1646
+
1647
+
1648
+ def build_new_mode_human_summary(result: dict[str, Any]) -> str:
1649
+ flow = str(result.get("flow", "")).strip() or "workflow"
1650
+ status = str(result.get("planning_status", "")).strip()
1651
+ recommended_command = str(result.get("recommended_command", "")).strip()
1652
+ dispatched = bool(result.get("dispatched"))
1653
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
1654
+ missing_fields = result.get("planning_missing_fields", [])
1655
+ resolved_kind = str(result.get("resolved_kind", "")).strip() or "auto"
1656
+ flow_labels = {
1657
+ "feature": "功能开发",
1658
+ "issue": "问题修复",
1659
+ "workflow": "工作流",
1660
+ }
1661
+ kind_labels = {
1662
+ "frontend": "前端",
1663
+ "backend": "后端",
1664
+ "shared": "共享范围(需先拆分)",
1665
+ "auto": "待路由",
1666
+ }
1667
+ flow_label = flow_labels.get(flow, flow)
1668
+ kind_label = kind_labels.get(resolved_kind, resolved_kind)
1669
+
1670
+ if status == "needs-clarification":
1671
+ if missing_fields:
1672
+ return f"已识别为{flow_label}请求,但信息还不完整,当前缺少:{describe_missing_fields(missing_fields)}。"
1673
+ return f"已识别为{flow_label}请求,但还需要进一步澄清。"
1674
+ if dispatched:
1675
+ if not dispatch_result.get("success"):
1676
+ failure_summary, next_step = build_dispatch_failure_user_summary(dispatch_result, recommended_command)
1677
+ if failure_summary and next_step:
1678
+ return f"已完成{flow_label}规划,并尝试自动调度,但当前未完成:{failure_summary} 下一步建议:{next_step}"
1679
+ if next_step:
1680
+ return f"已完成{flow_label}规划,并尝试自动调度,但当前未完成。下一步建议:{next_step}"
1681
+ return f"已完成{flow_label}规划,但自动调度尚未完成:{failure_summary}"
1682
+ command_next = build_command_user_next(recommended_command)
1683
+ if command_next and recommended_command:
1684
+ return f"已完成{flow_label}规划,并已自动调度到 {recommended_command},当前应继续{command_next}"
1685
+ return f"已完成{flow_label}规划,并已自动调度到 {recommended_command or '后续执行阶段'}。"
1686
+ if recommended_command:
1687
+ command_next = build_command_user_next(recommended_command)
1688
+ if command_next:
1689
+ return f"已完成{flow_label}规划,当前归属为{kind_label},下一步建议进入 {recommended_command},继续{command_next}"
1690
+ return f"已完成{flow_label}规划,当前归属为{kind_label},下一步建议进入 {recommended_command}。"
1691
+ return f"已完成{flow_label}规划,当前没有进一步命令需要执行。"
1692
+
1693
+
1694
+ def build_existing_mode_human_summary(result: dict[str, Any]) -> str:
1695
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
1696
+ summary = str(route_status.get("human_status_summary", "")).strip()
1697
+ action = str(route_status.get("operator_action_summary", "")).strip()
1698
+ recommended_command = str(result.get("recommended_command", "")).strip()
1699
+ dispatched = bool(result.get("dispatched"))
1700
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
1701
+ decision_labels = {
1702
+ "accepted": "通过",
1703
+ "changes-requested": "需修改",
1704
+ }
1705
+ fallback_stage_labels = {
1706
+ "closed": "关闭",
1707
+ "planning": "规划阶段",
1708
+ "execution": "执行阶段",
1709
+ "review": "审核阶段",
1710
+ }
1711
+ action_kind_labels = {
1712
+ "close-step": "关闭当前步骤",
1713
+ "repair-plan": "回到规划修正",
1714
+ "execute-for-real": "执行一次真实运行",
1715
+ "refine-and-rerun": "修正实现后重新执行",
1716
+ "re-evaluate": "重新评估当前步骤",
1717
+ }
1718
+
1719
+ if dispatched and dispatch_result.get("success"):
1720
+ final_decision = str(dispatch_result.get("final_decision", "")).strip()
1721
+ fallback_stage = str(dispatch_result.get("fallback_stage", "")).strip()
1722
+ action_kind = str(dispatch_result.get("recommended_action_kind", "")).strip()
1723
+ if final_decision:
1724
+ policy_suffix = ""
1725
+ if fallback_stage:
1726
+ policy_suffix = f" 当前回退阶段为{fallback_stage_labels.get(fallback_stage, fallback_stage)}。"
1727
+ if action_kind:
1728
+ policy_suffix += f" 建议动作类型为{action_kind_labels.get(action_kind, action_kind)}。"
1729
+ return f"{summary} 已自动完成审核,最终决策为{decision_labels.get(final_decision, final_decision)}。{policy_suffix}".strip()
1730
+ if recommended_command:
1731
+ command_next = build_command_user_next(recommended_command)
1732
+ if command_next:
1733
+ return f"{summary} 已自动调度到 {recommended_command},当前应继续{command_next}"
1734
+ return f"{summary} 已自动调度到 {recommended_command}。"
1735
+ return f"{summary} 已自动完成当前建议动作。"
1736
+ if dispatched and not dispatch_result.get("success"):
1737
+ failure_summary, next_step = build_dispatch_failure_user_summary(dispatch_result, recommended_command)
1738
+ dispatch_state = str(dispatch_result.get("state", "")).strip()
1739
+ dispatch_failure_type = str(dispatch_result.get("failure_type", "")).strip()
1740
+ if dispatch_failure_type in {"scope-error", "design-gap"} or dispatch_state == "returned-to-planning":
1741
+ if failure_summary and next_step:
1742
+ return f"{failure_summary} 下一步建议:{next_step}"
1743
+ return failure_summary or next_step or "当前步骤已返回规划阶段。"
1744
+ if dispatch_failure_type == "environment-or-tooling":
1745
+ if failure_summary and next_step:
1746
+ return f"{failure_summary} 下一步建议:{next_step}"
1747
+ return failure_summary or next_step or "当前执行被环境或工具前置条件阻塞。"
1748
+ if dispatch_failure_type == "executor-failure":
1749
+ if failure_summary and next_step:
1750
+ return f"{failure_summary} 下一步建议:{next_step}"
1751
+ return failure_summary or next_step or "当前执行器返回异常,需要先处理阻塞。"
1752
+ if summary and failure_summary and next_step:
1753
+ return f"{summary} 自动调度未完成:{failure_summary} 下一步建议:{next_step}"
1754
+ if summary and next_step:
1755
+ return f"{summary} 自动调度未完成。下一步建议:{next_step}"
1756
+ if summary and failure_summary:
1757
+ return f"{summary} 自动调度未完成:{failure_summary}"
1758
+ if failure_summary and next_step:
1759
+ return f"自动调度未完成:{failure_summary} 下一步建议:{next_step}"
1760
+ if next_step:
1761
+ return f"自动调度未完成。下一步建议:{next_step}"
1762
+ if failure_summary:
1763
+ return f"自动调度未完成:{failure_summary}"
1764
+ if summary and action:
1765
+ if action in summary:
1766
+ return summary
1767
+ return f"{summary} 下一步建议:{action}"
1768
+ if summary:
1769
+ return summary
1770
+ if action:
1771
+ return action
1772
+ return "当前已有工作流状态暂时无法判断,请检查输入的 slug 或最近工作流记录。"
1773
+
1774
+
1775
+ def build_missing_existing_workflow_result(
1776
+ *,
1777
+ request: str,
1778
+ explain_only: bool,
1779
+ include_fixtures: bool,
1780
+ human_summary: str = "当前没有可继续的 CodeCGC 工作流。",
1781
+ next_text: str = "先直接描述一个新需求,或显式提供已有 workflow 的 slug。",
1782
+ action_reason: str = "当前仓库里还没有可用于 continue / explain 的最近工作流。",
1783
+ workflow_state: str = "needs-new-workflow",
1784
+ reply_kind: str = "start-new-workflow",
1785
+ action_type: str = "wait-new-request",
1786
+ dispatch_blocker: str = "missing-existing-workflow",
1787
+ recovery_hint: str = "直接描述一个新需求,或显式提供已有 workflow slug。",
1788
+ recommended_command: str = "",
1789
+ command_args: list[str] | None = None,
1790
+ ) -> dict[str, Any]:
1791
+ command_args = [str(item).strip() for item in (command_args or []) if str(item).strip()]
1792
+ result = {
1793
+ "success": True,
1794
+ "entry_mode": "explain" if explain_only else "continue",
1795
+ "flow": "",
1796
+ "slug": "",
1797
+ "latest": True,
1798
+ "request": request,
1799
+ "auto_dispatch": False,
1800
+ "auto_dispatch_reason": "missing-existing-workflow",
1801
+ "recommended_command": recommended_command,
1802
+ "next": next_text,
1803
+ "dispatched": False,
1804
+ "dispatch_result": None,
1805
+ "route": {},
1806
+ "route_status": {
1807
+ "workflow_state": workflow_state,
1808
+ "recommended_command": recommended_command,
1809
+ "current_step_number": 0,
1810
+ "current_task_id": "",
1811
+ "current_kind": "",
1812
+ "current_target_paths": [],
1813
+ "review_decision": "",
1814
+ "review_step_number": 0,
1815
+ "review_fallback_stage": "",
1816
+ "review_policy_reason": "",
1817
+ "review_action_kind": "",
1818
+ "audit_path": "",
1819
+ "human_status_summary": human_summary,
1820
+ "operator_action_summary": next_text,
1821
+ "is_closed": False,
1822
+ "needs_review_decision": False,
1823
+ "needs_execution": False,
1824
+ "include_fixtures": include_fixtures,
1825
+ },
1826
+ "human_summary": human_summary,
1827
+ "assistant_reply": compose_user_reply(human_summary, next_text, prefix=""),
1828
+ }
1829
+ result["operator_brief"] = {
1830
+ "entry_mode": result["entry_mode"],
1831
+ "flow": "",
1832
+ "slug": "",
1833
+ "recommended_command": recommended_command,
1834
+ "next": next_text,
1835
+ "human_summary": human_summary,
1836
+ "assistant_reply": result["assistant_reply"],
1837
+ "user_message": result["assistant_reply"],
1838
+ "dispatched": False,
1839
+ "needs_user_reply": True,
1840
+ "needs_execution": False,
1841
+ "needs_review": False,
1842
+ "is_closed": False,
1843
+ "reply_kind": reply_kind,
1844
+ "workflow_state": workflow_state,
1845
+ "current_step_number": 0,
1846
+ "current_task_id": "",
1847
+ "current_kind": "",
1848
+ "current_target_paths": [],
1849
+ "review_decision": "",
1850
+ "audit_path": "",
1851
+ "machine_next_action": {
1852
+ "type": action_type,
1853
+ "command": recommended_command,
1854
+ "reason": action_reason,
1855
+ "can_auto_dispatch": False,
1856
+ "auto_dispatch_reason": dispatch_blocker,
1857
+ "command_args": command_args,
1858
+ "requires_user_reply": True,
1859
+ "requires_decision": False,
1860
+ "requires_audit_file": False,
1861
+ "requires_slug": False,
1862
+ "requires_step_number": False,
1863
+ "missing_fields": [],
1864
+ "workflow_state": workflow_state,
1865
+ "current_step_number": 0,
1866
+ "current_task_id": "",
1867
+ "current_kind": "",
1868
+ "audit_path": "",
1869
+ "review_decision": "",
1870
+ "review_fallback_stage": "",
1871
+ "review_policy_reason": "",
1872
+ "review_action_kind": "",
1873
+ "dispatch_attempted": False,
1874
+ "dispatch_success": None,
1875
+ "dispatch_state": "",
1876
+ "dispatch_failure_type": "",
1877
+ "dispatch_error": "",
1878
+ "dispatch_next": "",
1879
+ "dispatch_summary": "",
1880
+ "recovery_command": "",
1881
+ "recovery_kind": "",
1882
+ "retry_hint": "",
1883
+ "followup_payload": {},
1884
+ "dispatch_blocker": dispatch_blocker,
1885
+ "user_action": {
1886
+ "summary": human_summary,
1887
+ "next_step": next_text,
1888
+ "recovery_hint": recovery_hint,
1889
+ "followup_payload": {},
1890
+ },
1891
+ "diagnostics": {
1892
+ "dispatch_attempted": False,
1893
+ "dispatch_success": None,
1894
+ "dispatch_state": "",
1895
+ "dispatch_failure_type": "",
1896
+ "dispatch_blocker": dispatch_blocker,
1897
+ "dispatch_error": "",
1898
+ "dispatch_next": "",
1899
+ "dispatch_summary": "",
1900
+ "recovery_command": "",
1901
+ "recovery_kind": "",
1902
+ "retry_hint": recovery_hint,
1903
+ "audit_path": "",
1904
+ "review_fallback_stage": "",
1905
+ "review_policy_reason": "",
1906
+ "review_action_kind": "",
1907
+ },
1908
+ "execution": {
1909
+ "command": recommended_command,
1910
+ "command_args": command_args,
1911
+ "can_auto_dispatch": False,
1912
+ "auto_dispatch_reason": dispatch_blocker,
1913
+ "requires_user_reply": True,
1914
+ "requires_decision": False,
1915
+ "requires_audit_file": False,
1916
+ "requires_slug": False,
1917
+ "requires_step_number": False,
1918
+ "missing_fields": [],
1919
+ "workflow_state": workflow_state,
1920
+ "current_step_number": 0,
1921
+ "current_task_id": "",
1922
+ "current_kind": "",
1923
+ "review_decision": "",
1924
+ "review_fallback_stage": "",
1925
+ "review_policy_reason": "",
1926
+ "review_action_kind": "",
1927
+ },
1928
+ "preferred_fields": {
1929
+ "user_message": "user_action.summary",
1930
+ "next_step": "user_action.next_step",
1931
+ "recovery_hint": "user_action.recovery_hint",
1932
+ },
1933
+ "compatibility_fields": {
1934
+ "reason": "请优先改用 user_action.summary。",
1935
+ "dispatch_blocker": "请优先改用 diagnostics.dispatch_blocker。",
1936
+ },
1937
+ },
1938
+ "reply_payload": {},
1939
+ "reply_prompts": [],
1940
+ }
1941
+ return attach_entry_summary(result)
1942
+ return "已读取当前工作流状态。"
1943
+
1944
+
1945
+ def build_assistant_reply(result: dict[str, Any]) -> str:
1946
+ entry_mode = str(result.get("entry_mode", "")).strip()
1947
+ if entry_mode == "governance":
1948
+ summary = str(result.get("human_summary", "")).strip()
1949
+ next_text = str(result.get("next", "")).strip()
1950
+ return compose_user_reply(summary, next_text, prefix="")
1951
+
1952
+ dispatched = bool(result.get("dispatched"))
1953
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
1954
+ user_next = build_user_facing_next(result)
1955
+ if entry_mode == "new":
1956
+ summary = build_new_mode_human_summary(result)
1957
+ suggested = str(result.get("suggested_reply_template", "")).strip()
1958
+ if dispatched and not dispatch_result.get("success"):
1959
+ return summary
1960
+ if suggested:
1961
+ return f"{summary}\n\n{suggested}"
1962
+ return compose_user_reply(summary, user_next)
1963
+
1964
+ summary = build_existing_mode_human_summary(result)
1965
+ if dispatched and not dispatch_result.get("success"):
1966
+ return summary
1967
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
1968
+ if str(route_status.get("operator_action_summary", "")).strip():
1969
+ return compose_user_reply(summary, user_next)
1970
+ return compose_user_reply(summary, user_next)
1971
+
1972
+
1973
+ def build_beginner_action_pack(result: dict[str, Any]) -> list[str]:
1974
+ operator_brief = result.get("operator_brief", {}) if isinstance(result.get("operator_brief"), dict) else {}
1975
+ machine_next_action = operator_brief.get("machine_next_action", {}) if isinstance(operator_brief.get("machine_next_action"), dict) else {}
1976
+ execution = machine_next_action.get("execution", {}) if isinstance(machine_next_action.get("execution"), dict) else {}
1977
+ user_action = machine_next_action.get("user_action", {}) if isinstance(machine_next_action.get("user_action"), dict) else {}
1978
+ command = str(execution.get("command", "") or machine_next_action.get("command", "")).strip()
1979
+ command_args = execution.get("command_args", []) if isinstance(execution.get("command_args"), list) else []
1980
+ prompts = operator_brief.get("reply_prompts", []) if isinstance(operator_brief.get("reply_prompts"), list) else []
1981
+ next_step = str(user_action.get("next_step", "")).strip()
1982
+
1983
+ actions: list[str] = []
1984
+ if command:
1985
+ rendered = " ".join([command, *[str(item).strip() for item in command_args if str(item).strip()]]).strip()
1986
+ if rendered:
1987
+ actions.append(f"直接执行:{rendered}")
1988
+ if next_step:
1989
+ actions.append(f"你现在要做:{next_step}")
1990
+ for item in prompts[:3]:
1991
+ if not isinstance(item, dict):
1992
+ continue
1993
+ label = str(item.get("label", "")).strip()
1994
+ question = str(item.get("question", "")).strip()
1995
+ if label and question:
1996
+ actions.append(f"先补充 {label}:{question}")
1997
+ return actions[:3]
1998
+
1999
+
2000
+ def attach_entry_summary(result: dict[str, Any]) -> dict[str, Any]:
2001
+ operator_brief = result.get("operator_brief", {}) if isinstance(result.get("operator_brief"), dict) else {}
2002
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
2003
+ machine_next_action = operator_brief.get("machine_next_action", {}) if isinstance(operator_brief.get("machine_next_action"), dict) else {}
2004
+ execution = machine_next_action.get("execution", {}) if isinstance(machine_next_action.get("execution"), dict) else {}
2005
+ summary = {
2006
+ "human_summary": str(result.get("human_summary", "")).strip(),
2007
+ "assistant_reply": str(result.get("assistant_reply", "")).strip(),
2008
+ "recommended_command": str(result.get("recommended_command", "")).strip(),
2009
+ "next": str(result.get("next", "")).strip(),
2010
+ "beginner_actions": build_beginner_action_pack(result),
2011
+ "workflow_state": str(
2012
+ execution.get("workflow_state", "")
2013
+ or machine_next_action.get("workflow_state", "")
2014
+ or route_status.get("workflow_state", "")
2015
+ ).strip(),
2016
+ "reply_kind": str(operator_brief.get("reply_kind", "")).strip(),
2017
+ "action_type": str(machine_next_action.get("type", "")).strip(),
2018
+ "governance_type": str(operator_brief.get("governance_type", "")).strip(),
2019
+ "is_closed": bool(operator_brief.get("is_closed")),
2020
+ }
2021
+ result["summary"] = summary
2022
+ return result
2023
+
2024
+
2025
+ def extract_governance_artifact_info(governance_type: str, dispatch_result: dict[str, Any]) -> dict[str, str]:
2026
+ key_map = {
2027
+ "decide": ("decision", "path"),
2028
+ "learn": ("learning", "path"),
2029
+ "arch": ("architecture", "path"),
2030
+ "req": ("requirement", "path"),
2031
+ "roadmap": ("roadmap", "directory"),
2032
+ "refactor": ("refactor", "path"),
2033
+ "guide": ("guide", "path"),
2034
+ "libdoc": ("libdoc", "path"),
2035
+ "trick": ("trick", "path"),
2036
+ "explore": ("explore", "path"),
2037
+ }
2038
+ result_key, path_key = key_map.get(governance_type, ("", "path"))
2039
+ artifact = dispatch_result.get(result_key, {}) if result_key and isinstance(dispatch_result.get(result_key), dict) else {}
2040
+ return {
2041
+ "result_key": result_key,
2042
+ "artifact_path": str(artifact.get(path_key, "")).strip(),
2043
+ "summary": str(artifact.get("summary", "")).strip(),
2044
+ "created": str(artifact.get("created", "")).strip(),
2045
+ }
2046
+
2047
+
2048
+ def build_governance_followup_bundle(
2049
+ governance_type: str,
2050
+ request: str,
2051
+ dispatch_result: dict[str, Any],
2052
+ ) -> tuple[dict[str, Any], list[dict[str, str]], bool]:
2053
+ artifact = extract_governance_artifact_info(governance_type, dispatch_result)
2054
+ payload = {
2055
+ "governance_type": governance_type,
2056
+ "source_request": request.strip(),
2057
+ "artifact_path": artifact["artifact_path"],
2058
+ "artifact_summary": artifact["summary"] or request.strip(),
2059
+ "created": artifact["created"] or "unknown",
2060
+ }
2061
+
2062
+ prompts: list[dict[str, str]] = []
2063
+ needs_user_reply = False
2064
+
2065
+ if governance_type == "guide":
2066
+ needs_user_reply = True
2067
+ payload["suggested_request"] = "继续完善刚刚生成的 guide,补全目的、步骤和使用边界。"
2068
+ prompts = [
2069
+ {"label": "目的", "question": "这份 guide 最终要帮谁完成什么任务?"},
2070
+ {"label": "步骤", "question": "最少需要哪些可执行步骤,用户才能照着完成?"},
2071
+ {"label": "边界", "question": "有哪些前置条件、非目标或常见误区需要提前说明?"},
2072
+ ]
2073
+ elif governance_type == "libdoc":
2074
+ needs_user_reply = True
2075
+ payload["suggested_request"] = "继续完善刚刚生成的 libdoc,补全公开契约、示例和边界说明。"
2076
+ prompts = [
2077
+ {"label": "契约", "question": "这个公开表面有哪些稳定输入、输出或字段约束?"},
2078
+ {"label": "示例", "question": "最小示例应该如何调用或使用它?"},
2079
+ {"label": "边界", "question": "有哪些限制、异常场景或版本边界需要写清楚?"},
2080
+ ]
2081
+ elif governance_type == "trick":
2082
+ needs_user_reply = True
2083
+ payload["suggested_request"] = "继续完善刚刚记录的 trick,补全默认做法、适用范围和反例。"
2084
+ prompts = [
2085
+ {"label": "做法", "question": "这条技巧最推荐的默认做法是什么?"},
2086
+ {"label": "范围", "question": "它适用于哪些场景,不适用于哪些场景?"},
2087
+ {"label": "反例", "question": "有没有常见误用或应该避免的写法?"},
2088
+ ]
2089
+ elif governance_type == "explore":
2090
+ needs_user_reply = True
2091
+ payload["suggested_request"] = "继续细化刚刚登记的 explore 请求,明确问题、目标模块和预期输出。"
2092
+ prompts = [
2093
+ {"label": "问题", "question": "这次 explore 最终想回答的核心问题是什么?"},
2094
+ {"label": "模块", "question": "优先应该读哪些目录、模块或文件?"},
2095
+ {"label": "输出", "question": "希望最后产出结论、模块总览,还是 spike 结论?"},
2096
+ ]
2097
+
2098
+ return payload, prompts, needs_user_reply
2099
+
2100
+
2101
+ def build_operator_brief(result: dict[str, Any]) -> dict[str, Any]:
2102
+ entry_mode = str(result.get("entry_mode", "")).strip()
2103
+ flow = str(result.get("flow", "")).strip()
2104
+ slug = str(result.get("slug", "")).strip()
2105
+ recommended_command = str(result.get("recommended_command", "")).strip()
2106
+ next_text = str(result.get("next", "")).strip()
2107
+ human_summary = str(result.get("human_summary", "")).strip()
2108
+ assistant_reply = str(result.get("assistant_reply", "")).strip()
2109
+ dispatched = bool(result.get("dispatched"))
2110
+ auto_dispatch = bool(result.get("auto_dispatch"))
2111
+ auto_dispatch_reason = str(result.get("auto_dispatch_reason", "")).strip()
2112
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
2113
+ replan_payload = (
2114
+ result.get("replan_payload", {})
2115
+ if isinstance(result.get("replan_payload"), dict)
2116
+ else {}
2117
+ )
2118
+ if dispatched and isinstance(dispatch_result.get("replan_payload"), dict) and dispatch_result.get("replan_payload"):
2119
+ merged_replan_payload = dict(replan_payload)
2120
+ merged_replan_payload.update(dispatch_result.get("replan_payload", {}))
2121
+ replan_payload = merged_replan_payload
2122
+
2123
+ def build_command_args(command: str, route_status: dict[str, Any] | None = None) -> list[str]:
2124
+ if not command:
2125
+ return []
2126
+ args: list[str] = []
2127
+ if matches_command(command, "cgc-plan"):
2128
+ if flow:
2129
+ args.extend(["--flow", flow])
2130
+ if slug:
2131
+ args.extend(["--slug", slug])
2132
+ summary = str(replan_payload.get("summary", "")).strip()
2133
+ if summary:
2134
+ args.extend(["--summary", summary])
2135
+ kind = str(replan_payload.get("kind", "")).strip()
2136
+ if kind:
2137
+ args.extend(["--kind", kind])
2138
+ for item in replan_payload.get("target_paths", []):
2139
+ if str(item).strip():
2140
+ args.extend(["--target-path", str(item).strip()])
2141
+ if flow == "feature":
2142
+ for key, flag in (
2143
+ ("goal", "--goal"),
2144
+ ("user_story", "--user-story"),
2145
+ ):
2146
+ value = str(replan_payload.get(key, "")).strip()
2147
+ if value:
2148
+ args.extend([flag, value])
2149
+ else:
2150
+ for key, flag in (
2151
+ ("symptom", "--symptom"),
2152
+ ("expected", "--expected"),
2153
+ ("actual", "--actual"),
2154
+ ("user_story", "--user-story"),
2155
+ ):
2156
+ value = str(replan_payload.get(key, "")).strip()
2157
+ if value:
2158
+ args.extend([flag, value])
2159
+ for item in replan_payload.get("in_scope", []):
2160
+ if str(item).strip():
2161
+ args.extend(["--in-scope", str(item).strip()])
2162
+ for item in replan_payload.get("acceptance", []):
2163
+ if str(item).strip():
2164
+ args.extend(["--acceptance", str(item).strip()])
2165
+ return args
2166
+ if slug and matches_command(command, "cgc-build", "cgc-fix", "cgc-test"):
2167
+ args.extend(["--slug", slug])
2168
+ if matches_command(command, "cgc-build", "cgc-fix", "cgc-test"):
2169
+ current_step_number = int((route_status or {}).get("current_step_number", 0) or 0)
2170
+ if current_step_number:
2171
+ args.extend(["--step-number", str(current_step_number)])
2172
+ reusable_session_id = str((route_status or {}).get("reusable_session_id", "")).strip()
2173
+ if reusable_session_id:
2174
+ args.extend(["--session-id", reusable_session_id])
2175
+ if matches_command(command, "cgc-review"):
2176
+ audit_path = str((route_status or {}).get("audit_path", "")).strip()
2177
+ review_decision = str((route_status or {}).get("review_decision", "")).strip()
2178
+ if audit_path:
2179
+ args.extend(["--audit-file", audit_path])
2180
+ if review_decision in {"accepted", "changes-requested"}:
2181
+ args.extend(["--decision", review_decision])
2182
+ return args
2183
+
2184
+ def build_machine_next_action(
2185
+ *,
2186
+ action_type: str,
2187
+ command: str,
2188
+ reason: str,
2189
+ route_status: dict[str, Any] | None = None,
2190
+ missing_fields: list[str] | None = None,
2191
+ reply_payload: dict[str, Any] | None = None,
2192
+ reply_prompts: list[dict[str, str]] | None = None,
2193
+ force_can_auto_dispatch: bool | None = None,
2194
+ ) -> dict[str, Any]:
2195
+ route_status = route_status or {}
2196
+ missing_fields = missing_fields or []
2197
+ reply_payload = reply_payload or {}
2198
+ reply_prompts = reply_prompts or []
2199
+ current_step_number = int(route_status.get("current_step_number", 0) or 0)
2200
+ audit_path = str(route_status.get("audit_path", "")).strip()
2201
+ review_decision = str(route_status.get("review_decision", "")).strip()
2202
+ review_fallback_stage = str(route_status.get("review_fallback_stage", "")).strip()
2203
+ review_policy_reason = str(route_status.get("review_policy_reason", "")).strip()
2204
+ review_action_kind = str(route_status.get("review_action_kind", "")).strip()
2205
+ dispatch_state = str(dispatch_result.get("state", "")).strip() if dispatched else ""
2206
+ dispatch_failure_type = str(dispatch_result.get("failure_type", "")).strip() if dispatched else ""
2207
+ dispatch_error = str(dispatch_result.get("error", "")).strip() if dispatched else ""
2208
+ dispatch_next = str(dispatch_result.get("next", "")).strip() if dispatched else ""
2209
+ dispatch_summary = str(dispatch_result.get("summary", "")).strip() if dispatched else ""
2210
+ dispatch_audit_path = str(dispatch_result.get("audit_path", "")).strip() if dispatched else ""
2211
+ dispatch_review_fallback_stage = str(dispatch_result.get("fallback_stage", "")).strip() if dispatched else ""
2212
+ dispatch_review_policy_reason = str(dispatch_result.get("policy_reason", "")).strip() if dispatched else ""
2213
+ dispatch_review_action_kind = str(dispatch_result.get("recommended_action_kind", "")).strip() if dispatched else ""
2214
+ dispatch_user_hint = build_dispatch_recovery_user_hint(dispatch_result, command) if dispatched else ""
2215
+ dispatch_recovery = infer_dispatch_recovery(command, dispatch_failure_type, dispatch_state, dispatch_next) if dispatched else {
2216
+ "recovery_command": "",
2217
+ "recovery_kind": "",
2218
+ "retry_hint": "",
2219
+ }
2220
+ followup_payload: dict[str, Any] = {}
2221
+ if reply_payload:
2222
+ followup_payload = {
2223
+ "payload": reply_payload,
2224
+ "prompts": reply_prompts,
2225
+ }
2226
+ if matches_command(command, "cgc-plan") and replan_payload:
2227
+ existing_payload = (
2228
+ followup_payload.get("payload", {})
2229
+ if isinstance(followup_payload.get("payload"), dict)
2230
+ else {}
2231
+ )
2232
+ merged_payload = dict(replan_payload)
2233
+ merged_payload.update(existing_payload)
2234
+ followup_payload = {
2235
+ "payload": merged_payload,
2236
+ "prompts": reply_prompts,
2237
+ }
2238
+ can_auto_dispatch = bool(auto_dispatch and command)
2239
+ if force_can_auto_dispatch is not None:
2240
+ can_auto_dispatch = force_can_auto_dispatch
2241
+ dispatch_blocker = ""
2242
+ if action_type == "wait-review-decision" and review_decision not in {"accepted", "changes-requested"}:
2243
+ dispatch_blocker = "review-decision-missing"
2244
+ elif dispatched and not dispatch_result.get("success"):
2245
+ dispatch_blocker = dispatch_failure_type or dispatch_error or dispatch_state or auto_dispatch_reason
2246
+ elif not can_auto_dispatch and auto_dispatch_reason:
2247
+ dispatch_blocker = auto_dispatch_reason
2248
+ command_args = build_command_args(command, route_status)
2249
+ user_action = {
2250
+ "summary": reason,
2251
+ "next_step": reason,
2252
+ "recovery_hint": dispatch_user_hint or str(dispatch_recovery.get("retry_hint", "")).strip(),
2253
+ "followup_payload": followup_payload,
2254
+ }
2255
+ diagnostics = {
2256
+ "dispatch_attempted": dispatched,
2257
+ "dispatch_success": bool(dispatch_result.get("success")) if dispatched else None,
2258
+ "dispatch_state": dispatch_state,
2259
+ "dispatch_failure_type": dispatch_failure_type,
2260
+ "dispatch_blocker": dispatch_blocker,
2261
+ "dispatch_error": dispatch_error,
2262
+ "dispatch_next": dispatch_next,
2263
+ "dispatch_summary": dispatch_summary,
2264
+ "recovery_command": str(dispatch_recovery.get("recovery_command", "")).strip(),
2265
+ "recovery_kind": str(dispatch_recovery.get("recovery_kind", "")).strip(),
2266
+ "retry_hint": dispatch_user_hint or str(dispatch_recovery.get("retry_hint", "")).strip(),
2267
+ "audit_path": dispatch_audit_path or audit_path,
2268
+ "review_fallback_stage": dispatch_review_fallback_stage or review_fallback_stage,
2269
+ "review_policy_reason": dispatch_review_policy_reason or review_policy_reason,
2270
+ "review_action_kind": dispatch_review_action_kind or review_action_kind,
2271
+ }
2272
+ execution = {
2273
+ "command": command,
2274
+ "command_args": command_args,
2275
+ "can_auto_dispatch": can_auto_dispatch,
2276
+ "auto_dispatch_reason": auto_dispatch_reason,
2277
+ "requires_user_reply": action_type == "wait-user-reply",
2278
+ "requires_decision": action_type in {"review", "wait-review-decision"} and review_decision not in {"accepted", "changes-requested"},
2279
+ "requires_audit_file": action_type == "review" and not audit_path,
2280
+ "requires_slug": not bool(slug) and action_type in {"dispatch", "review"},
2281
+ "requires_step_number": action_type == "dispatch" and matches_command(command, "cgc-build", "cgc-fix") and current_step_number <= 0,
2282
+ "missing_fields": missing_fields,
2283
+ "workflow_state": str(route_status.get("workflow_state", "")).strip(),
2284
+ "current_step_number": current_step_number,
2285
+ "current_task_id": str(route_status.get("current_task_id", "")).strip(),
2286
+ "current_kind": str(route_status.get("current_kind", "")).strip(),
2287
+ "review_decision": review_decision,
2288
+ "review_fallback_stage": dispatch_review_fallback_stage or review_fallback_stage,
2289
+ "review_policy_reason": dispatch_review_policy_reason or review_policy_reason,
2290
+ "review_action_kind": dispatch_review_action_kind or review_action_kind,
2291
+ }
2292
+ preferred_fields = {
2293
+ "user_message": "user_action.summary",
2294
+ "next_step": "user_action.next_step",
2295
+ "recovery_hint": "user_action.recovery_hint",
2296
+ "execution_command": "execution.command",
2297
+ "execution_args": "execution.command_args",
2298
+ "diagnostic_blocker": "diagnostics.dispatch_blocker",
2299
+ "diagnostic_error": "diagnostics.dispatch_error",
2300
+ "diagnostic_next": "diagnostics.dispatch_next",
2301
+ }
2302
+ compatibility_fields = {
2303
+ "reason": "请优先改用 user_action.summary。",
2304
+ "command": "请优先改用 execution.command。",
2305
+ "command_args": "请优先改用 execution.command_args。",
2306
+ "retry_hint": "请优先改用 user_action.recovery_hint 或 diagnostics.retry_hint。",
2307
+ "dispatch_blocker": "请优先改用 diagnostics.dispatch_blocker。",
2308
+ "dispatch_error": "请优先改用 diagnostics.dispatch_error。",
2309
+ "dispatch_next": "请优先改用 diagnostics.dispatch_next。",
2310
+ }
2311
+ return {
2312
+ "type": action_type,
2313
+ "command": command,
2314
+ "reason": reason,
2315
+ "can_auto_dispatch": can_auto_dispatch,
2316
+ "auto_dispatch_reason": auto_dispatch_reason,
2317
+ "command_args": command_args,
2318
+ "requires_user_reply": action_type == "wait-user-reply",
2319
+ "requires_decision": action_type in {"review", "wait-review-decision"} and review_decision not in {"accepted", "changes-requested"},
2320
+ "requires_audit_file": action_type == "review" and not audit_path,
2321
+ "requires_slug": not bool(slug) and action_type in {"dispatch", "review"},
2322
+ "requires_step_number": action_type == "dispatch" and matches_command(command, "cgc-build", "cgc-fix") and current_step_number <= 0,
2323
+ "missing_fields": missing_fields,
2324
+ "workflow_state": str(route_status.get("workflow_state", "")).strip(),
2325
+ "current_step_number": current_step_number,
2326
+ "current_task_id": str(route_status.get("current_task_id", "")).strip(),
2327
+ "current_kind": str(route_status.get("current_kind", "")).strip(),
2328
+ "audit_path": dispatch_audit_path or audit_path,
2329
+ "review_decision": review_decision,
2330
+ "review_fallback_stage": dispatch_review_fallback_stage or review_fallback_stage,
2331
+ "review_policy_reason": dispatch_review_policy_reason or review_policy_reason,
2332
+ "review_action_kind": dispatch_review_action_kind or review_action_kind,
2333
+ "dispatch_attempted": dispatched,
2334
+ "dispatch_success": bool(dispatch_result.get("success")) if dispatched else None,
2335
+ "dispatch_state": dispatch_state,
2336
+ "dispatch_failure_type": dispatch_failure_type,
2337
+ "dispatch_error": dispatch_error,
2338
+ "dispatch_next": dispatch_next,
2339
+ "dispatch_summary": dispatch_summary,
2340
+ "recovery_command": str(dispatch_recovery.get("recovery_command", "")).strip(),
2341
+ "recovery_kind": str(dispatch_recovery.get("recovery_kind", "")).strip(),
2342
+ "retry_hint": dispatch_user_hint or str(dispatch_recovery.get("retry_hint", "")).strip(),
2343
+ "followup_payload": followup_payload,
2344
+ "dispatch_blocker": dispatch_blocker,
2345
+ "user_action": user_action,
2346
+ "diagnostics": diagnostics,
2347
+ "execution": execution,
2348
+ "preferred_fields": preferred_fields,
2349
+ "compatibility_fields": compatibility_fields,
2350
+ }
2351
+
2352
+ brief: dict[str, Any] = {
2353
+ "entry_mode": entry_mode,
2354
+ "flow": flow,
2355
+ "slug": slug,
2356
+ "recommended_command": recommended_command,
2357
+ "next": next_text,
2358
+ "human_summary": human_summary,
2359
+ "assistant_reply": assistant_reply,
2360
+ "user_message": assistant_reply,
2361
+ "dispatched": dispatched,
2362
+ "needs_user_reply": False,
2363
+ "needs_execution": False,
2364
+ "needs_review": False,
2365
+ "is_closed": False,
2366
+ "reply_kind": "status",
2367
+ "machine_next_action": build_machine_next_action(
2368
+ action_type="idle",
2369
+ command=recommended_command,
2370
+ reason=next_text,
2371
+ ),
2372
+ "reply_payload": {},
2373
+ "reply_prompts": [],
2374
+ }
2375
+
2376
+ if entry_mode == "governance":
2377
+ governance_skill = str(result.get("recommended_skill", "")).strip()
2378
+ governance_type = str(result.get("governance_type", "")).strip()
2379
+ governance_dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
2380
+ governance_reply_payload, governance_reply_prompts, governance_needs_user_reply = build_governance_followup_bundle(
2381
+ governance_type,
2382
+ str(result.get("request", "")),
2383
+ governance_dispatch_result,
2384
+ )
2385
+ brief["reply_kind"] = "governance"
2386
+ brief["governance_type"] = governance_type
2387
+ brief["recommended_skill"] = governance_skill
2388
+ brief["needs_user_reply"] = governance_needs_user_reply
2389
+ brief["reply_payload"] = governance_reply_payload
2390
+ brief["reply_prompts"] = governance_reply_prompts
2391
+ brief["machine_next_action"] = {
2392
+ "type": "wait-governance-followup" if governance_needs_user_reply else "governance",
2393
+ "command": "",
2394
+ "reason": next_text,
2395
+ "recommended_skill": governance_skill,
2396
+ "governance_type": governance_type,
2397
+ "can_auto_dispatch": False,
2398
+ "requires_user_reply": governance_needs_user_reply,
2399
+ "workflow_state": "governance-routing",
2400
+ "user_action": {
2401
+ "summary": human_summary,
2402
+ "next_step": next_text,
2403
+ "recovery_hint": "",
2404
+ "followup_payload": {
2405
+ "payload": governance_reply_payload,
2406
+ "prompts": governance_reply_prompts,
2407
+ },
2408
+ },
2409
+ "diagnostics": {
2410
+ "dispatch_attempted": False,
2411
+ "dispatch_success": None,
2412
+ "dispatch_state": "",
2413
+ "dispatch_failure_type": "",
2414
+ "dispatch_blocker": "",
2415
+ "dispatch_error": "",
2416
+ "dispatch_next": "",
2417
+ "dispatch_summary": "",
2418
+ "recovery_command": "",
2419
+ "recovery_kind": "",
2420
+ "retry_hint": "",
2421
+ "audit_path": "",
2422
+ },
2423
+ "execution": {
2424
+ "command": "",
2425
+ "command_args": [],
2426
+ "can_auto_dispatch": False,
2427
+ "auto_dispatch_reason": "governance-skill-routing",
2428
+ "requires_user_reply": governance_needs_user_reply,
2429
+ "requires_decision": False,
2430
+ "requires_audit_file": False,
2431
+ "requires_slug": False,
2432
+ "requires_step_number": False,
2433
+ "missing_fields": [],
2434
+ "workflow_state": "governance-routing",
2435
+ "current_step_number": 0,
2436
+ "current_task_id": "",
2437
+ "current_kind": "",
2438
+ "review_decision": "",
2439
+ },
2440
+ "preferred_fields": {
2441
+ "user_message": "user_action.summary",
2442
+ "next_step": "user_action.next_step",
2443
+ "governance_skill": "recommended_skill",
2444
+ "followup_payload": "user_action.followup_payload.payload",
2445
+ },
2446
+ "compatibility_fields": {
2447
+ "reason": "请优先改用 user_action.summary。",
2448
+ },
2449
+ }
2450
+ return brief
2451
+
2452
+ if entry_mode == "new":
2453
+ planning_missing_fields = result.get("planning_missing_fields", [])
2454
+ clarification_prompts = result.get("clarification_prompts", [])
2455
+ brief["planning_status"] = str(result.get("planning_status", "")).strip()
2456
+ brief["planning_missing_fields"] = planning_missing_fields
2457
+ brief["resolved_kind"] = str(result.get("resolved_kind", "")).strip()
2458
+ brief["resolved_target_paths"] = result.get("resolved_target_paths", [])
2459
+ brief["needs_user_reply"] = bool(planning_missing_fields)
2460
+ brief["needs_execution"] = matches_command(recommended_command, "cgc-build", "cgc-fix")
2461
+ brief["reply_kind"] = "clarification" if planning_missing_fields else ("execution" if brief["needs_execution"] else "status")
2462
+ brief["reply_payload"] = result.get("suggested_reply_payload", {})
2463
+ brief["reply_prompts"] = clarification_prompts
2464
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
2465
+ brief["workflow_state"] = str(route_status.get("workflow_state", "")).strip()
2466
+ brief["current_step_number"] = int(route_status.get("current_step_number", 0) or 0)
2467
+ brief["current_task_id"] = str(route_status.get("current_task_id", "")).strip()
2468
+ brief["current_kind"] = str(route_status.get("current_kind", "")).strip()
2469
+ brief["current_target_paths"] = route_status.get("current_target_paths", [])
2470
+ brief["review_decision"] = str(route_status.get("review_decision", "")).strip()
2471
+ brief["audit_path"] = str(route_status.get("audit_path", "")).strip()
2472
+ brief["machine_next_action"] = build_machine_next_action(
2473
+ action_type="wait-user-reply" if planning_missing_fields else ("dispatch" if brief["needs_execution"] else "idle"),
2474
+ command=recommended_command,
2475
+ reason=next_text,
2476
+ route_status=route_status,
2477
+ missing_fields=[str(item) for item in planning_missing_fields],
2478
+ reply_payload=brief["reply_payload"] if isinstance(brief["reply_payload"], dict) else {},
2479
+ reply_prompts=clarification_prompts if isinstance(clarification_prompts, list) else [],
2480
+ )
2481
+ return brief
2482
+
2483
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
2484
+ brief["workflow_state"] = str(route_status.get("workflow_state", "")).strip()
2485
+ brief["needs_execution"] = bool(route_status.get("needs_execution"))
2486
+ brief["needs_review"] = bool(route_status.get("needs_review_decision"))
2487
+ brief["is_closed"] = bool(route_status.get("is_closed"))
2488
+ brief["current_step_number"] = int(route_status.get("current_step_number", 0) or 0)
2489
+ brief["current_task_id"] = str(route_status.get("current_task_id", "")).strip()
2490
+ brief["current_kind"] = str(route_status.get("current_kind", "")).strip()
2491
+ brief["current_target_paths"] = route_status.get("current_target_paths", [])
2492
+ brief["review_decision"] = str(route_status.get("review_decision", "")).strip()
2493
+ brief["audit_path"] = str(route_status.get("audit_path", "")).strip()
2494
+ brief["review_fallback_stage"] = str(route_status.get("review_fallback_stage", "")).strip()
2495
+ brief["review_policy_reason"] = str(route_status.get("review_policy_reason", "")).strip()
2496
+ brief["review_action_kind"] = str(route_status.get("review_action_kind", "")).strip()
2497
+ if dispatched:
2498
+ brief["dispatch_success"] = bool(dispatch_result.get("success"))
2499
+ brief["dispatch_error"] = str(dispatch_result.get("error", "")).strip()
2500
+ if dispatched and not dispatch_result.get("success"):
2501
+ failed_command = str(dispatch_result.get("recommended_command", "")).strip() or recommended_command
2502
+ failed_reason = build_dispatch_recovery_user_hint(dispatch_result, failed_command) or next_text
2503
+ brief["reply_kind"] = "blocked"
2504
+ brief["machine_next_action"] = build_machine_next_action(
2505
+ action_type="dispatch-failed",
2506
+ command=failed_command,
2507
+ reason=failed_reason,
2508
+ route_status=route_status,
2509
+ force_can_auto_dispatch=False,
2510
+ )
2511
+ elif brief["needs_review"]:
2512
+ brief["reply_kind"] = "review"
2513
+ brief["machine_next_action"] = build_machine_next_action(
2514
+ action_type="wait-review-decision" if not brief["review_decision"] else "review",
2515
+ command=recommended_command,
2516
+ reason=next_text,
2517
+ route_status=route_status,
2518
+ )
2519
+ elif brief["needs_execution"]:
2520
+ brief["reply_kind"] = "execution"
2521
+ brief["machine_next_action"] = build_machine_next_action(
2522
+ action_type="dispatch",
2523
+ command=recommended_command,
2524
+ reason=next_text,
2525
+ route_status=route_status,
2526
+ )
2527
+ elif brief["is_closed"]:
2528
+ brief["reply_kind"] = "closed"
2529
+ brief["machine_next_action"] = build_machine_next_action(
2530
+ action_type="closed",
2531
+ command="",
2532
+ reason=next_text,
2533
+ route_status=route_status,
2534
+ )
2535
+ else:
2536
+ brief["machine_next_action"] = build_machine_next_action(
2537
+ action_type="idle",
2538
+ command=recommended_command,
2539
+ reason=next_text,
2540
+ route_status=route_status,
2541
+ )
2542
+ return brief
2543
+
2544
+
2545
+ def discover_existing_flow(slug: str) -> tuple[str, str] | None:
2546
+ feature = discover_flow_directory("feature", slug, "auto")
2547
+ issue = discover_flow_directory("issue", slug, "auto")
2548
+ if feature and issue:
2549
+ raise ValueError(f"Slug '{slug}' exists as both feature and issue. Specify --flow explicitly.")
2550
+ if feature:
2551
+ return "feature", feature[0]
2552
+ if issue:
2553
+ return "issue", issue[0]
2554
+ return None
2555
+
2556
+
2557
+ def workflow_activity_timestamp(directory: Path) -> float:
2558
+ timestamps = [directory.stat().st_mtime]
2559
+ for path in directory.rglob("*"):
2560
+ try:
2561
+ timestamps.append(path.stat().st_mtime)
2562
+ except OSError:
2563
+ continue
2564
+ return max(timestamps) if timestamps else 0.0
2565
+
2566
+
2567
+ def build_latest_workflow_state_priority(request: str, explain_only: bool = False) -> dict[str, int]:
2568
+ if explain_only or request_implies_explain(request):
2569
+ return {
2570
+ "awaiting-review": 4,
2571
+ "awaiting-build": 3,
2572
+ "awaiting-fix": 3,
2573
+ "needs-planning": 2,
2574
+ "step-selected": 1,
2575
+ "closed": 0,
2576
+ }
2577
+
2578
+ if request_implies_review(request):
2579
+ return {
2580
+ "awaiting-review": 5,
2581
+ "awaiting-build": 3,
2582
+ "awaiting-fix": 3,
2583
+ "needs-planning": 2,
2584
+ "step-selected": 1,
2585
+ "closed": 0,
2586
+ }
2587
+
2588
+ if request_implies_execute(request) or request_implies_continue(request):
2589
+ return {
2590
+ "awaiting-build": 5,
2591
+ "awaiting-fix": 5,
2592
+ "awaiting-review": 4,
2593
+ "needs-planning": 3,
2594
+ "step-selected": 2,
2595
+ "closed": 0,
2596
+ }
2597
+
2598
+ return {
2599
+ "awaiting-review": 4,
2600
+ "awaiting-build": 3,
2601
+ "awaiting-fix": 3,
2602
+ "needs-planning": 2,
2603
+ "step-selected": 1,
2604
+ "closed": 0,
2605
+ }
2606
+
2607
+
2608
+ def build_latest_workflow_required_states(request: str, explain_only: bool = False) -> set[str]:
2609
+ if explain_only or request_implies_explain(request):
2610
+ return set()
2611
+ if request_implies_review(request):
2612
+ return {"awaiting-review"}
2613
+ return set()
2614
+
2615
+
2616
+ def score_latest_workflow_candidate(
2617
+ flow: str,
2618
+ slug: str,
2619
+ state_priority: dict[str, int] | None = None,
2620
+ ) -> tuple[int, int, str]:
2621
+ try:
2622
+ route = run_json_script("route_codecgc_workflow.py", "--flow", flow, "--slug", slug)
2623
+ except Exception:
2624
+ return (0, 0, "")
2625
+
2626
+ summary = route.get("summary", {}) if isinstance(route.get("summary"), dict) else {}
2627
+ workflow_state = str(summary.get("workflow_state", "")).strip()
2628
+ is_closed = bool(summary.get("is_closed"))
2629
+ recommended_command = str(route.get("recommended_command", "")).strip()
2630
+ priorities = state_priority or build_latest_workflow_state_priority("")
2631
+
2632
+ if not is_closed and workflow_state:
2633
+ return (int(priorities.get(workflow_state, 1)), 1, workflow_state)
2634
+ if not is_closed and recommended_command:
2635
+ return (1, 1, workflow_state)
2636
+ return (0, 0, workflow_state)
2637
+
2638
+
2639
+ def discover_latest_flow(
2640
+ include_fixtures: bool,
2641
+ *,
2642
+ request: str = "",
2643
+ explain_only: bool = False,
2644
+ ) -> tuple[str, str] | None:
2645
+ candidates: list[tuple[int, int, float, str, str]] = []
2646
+ state_priority = build_latest_workflow_state_priority(request, explain_only=explain_only)
2647
+ required_states = build_latest_workflow_required_states(request, explain_only=explain_only)
2648
+ artifact_classes = ["product", "fixture"] if include_fixtures else ["product"]
2649
+ for artifact_class in artifact_classes:
2650
+ for flow in ("feature", "issue"):
2651
+ root = flow_root(flow, artifact_class)
2652
+ if not root.exists():
2653
+ continue
2654
+ for child in root.iterdir():
2655
+ if not child.is_dir():
2656
+ continue
2657
+ lifecycle_priority, active_priority, workflow_state = score_latest_workflow_candidate(
2658
+ flow,
2659
+ child.name,
2660
+ state_priority=state_priority,
2661
+ )
2662
+ if required_states and workflow_state not in required_states:
2663
+ continue
2664
+ candidates.append(
2665
+ (
2666
+ active_priority,
2667
+ lifecycle_priority,
2668
+ workflow_activity_timestamp(child),
2669
+ flow,
2670
+ child.name,
2671
+ )
2672
+ )
2673
+ if not candidates:
2674
+ return None
2675
+ candidates.sort(key=lambda item: (item[0], item[1], item[2]), reverse=True)
2676
+ _, _, _, flow, slug = candidates[0]
2677
+ return flow, slug
2678
+
2679
+
2680
+ def resolve_existing_flow(args: argparse.Namespace) -> str:
2681
+ if args.flow:
2682
+ return args.flow
2683
+ use_latest = args.latest or (args.request.strip() and not args.slug.strip())
2684
+ if use_latest and not args.slug:
2685
+ discovered = discover_latest_flow(args.include_fixtures, request=args.request)
2686
+ if not discovered:
2687
+ raise ValueError("No existing workflow was found for the requested continue/explain operation.")
2688
+ return discovered[0]
2689
+ if not args.slug:
2690
+ raise ValueError("Existing workflow operations require --slug.")
2691
+ discovered = discover_existing_flow(args.slug)
2692
+ if not discovered:
2693
+ raise ValueError(f"No existing feature or issue workflow was found for slug '{args.slug}'.")
2694
+ return discovered[0]
2695
+
2696
+
2697
+ def infer_mode(args: argparse.Namespace) -> str:
2698
+ if args.mode != "auto":
2699
+ return args.mode
2700
+ request = args.request.strip()
2701
+ shortcut_mode = classify_existing_workflow_shortcut(request)
2702
+ if shortcut_mode:
2703
+ return shortcut_mode
2704
+ if request and request_implies_explain(request):
2705
+ return "explain" if not args.summary.strip() else "new"
2706
+ if request and request_implies_continue(request):
2707
+ return "continue"
2708
+ if args.latest and not args.slug:
2709
+ return "continue"
2710
+ if args.slug:
2711
+ discovered = discover_existing_flow(args.slug)
2712
+ if discovered:
2713
+ return "continue"
2714
+ if args.summary.strip():
2715
+ return "new"
2716
+ if args.summary.strip() or request or any(item.strip() for item in args.target_path):
2717
+ return "new"
2718
+ if args.slug:
2719
+ return "explain"
2720
+ raise ValueError("Auto mode could not infer intent. Provide --request, --summary, --slug, or --latest.")
2721
+
2722
+
2723
+ def build_plan_args(args: argparse.Namespace, flow: str) -> list[str]:
2724
+ slug = resolve_new_slug(args, flow)
2725
+ summary = resolve_new_summary(args)
2726
+ target_paths = resolve_target_paths(args)
2727
+ kind = resolve_kind(args, target_paths)
2728
+ goal = infer_feature_goal(args) if flow == "feature" else args.goal
2729
+ user_story = infer_feature_user_story(args) if flow == "feature" else args.user_story
2730
+ in_scope = infer_in_scope(args, flow, target_paths)
2731
+ acceptance = infer_acceptance(args, flow)
2732
+ symptom = infer_issue_symptom(args) if flow == "issue" else args.symptom
2733
+ expected = infer_issue_expected(args) if flow == "issue" else args.expected
2734
+ actual = infer_issue_actual(args) if flow == "issue" else args.actual
2735
+
2736
+ command = [
2737
+ "--flow",
2738
+ flow,
2739
+ "--slug",
2740
+ slug,
2741
+ "--summary",
2742
+ summary,
2743
+ "--kind",
2744
+ kind,
2745
+ "--artifact-class",
2746
+ args.artifact_class,
2747
+ ]
2748
+ if args.date:
2749
+ command.extend(["--date", args.date])
2750
+ for item in target_paths:
2751
+ command.extend(["--target-path", item])
2752
+ if goal:
2753
+ command.extend(["--goal", goal])
2754
+ if user_story:
2755
+ command.extend(["--user-story", user_story])
2756
+ for item in args.context:
2757
+ command.extend(["--context", item])
2758
+ for item in in_scope:
2759
+ command.extend(["--in-scope", item])
2760
+ for item in args.out_of_scope:
2761
+ command.extend(["--out-of-scope", item])
2762
+ for item in acceptance:
2763
+ command.extend(["--acceptance", item])
2764
+ for item in args.risk:
2765
+ command.extend(["--risk", item])
2766
+ for item in args.dependency:
2767
+ command.extend(["--dependency", item])
2768
+ for item in args.assumption:
2769
+ command.extend(["--assumption", item])
2770
+ for item in args.open_question:
2771
+ command.extend(["--open-question", item])
2772
+ for item in args.validation:
2773
+ command.extend(["--validation", item])
2774
+ for item in args.rollback:
2775
+ command.extend(["--rollback", item])
2776
+ if symptom:
2777
+ command.extend(["--symptom", symptom])
2778
+ if args.reproduction:
2779
+ command.extend(["--reproduction", args.reproduction])
2780
+ if expected:
2781
+ command.extend(["--expected", expected])
2782
+ if actual:
2783
+ command.extend(["--actual", actual])
2784
+ if args.root_cause:
2785
+ command.extend(["--root-cause", args.root_cause])
2786
+ if args.preferred_fix:
2787
+ command.extend(["--preferred-fix", args.preferred_fix])
2788
+ if args.rejected_fix:
2789
+ command.extend(["--rejected-fix", args.rejected_fix])
2790
+ if args.force:
2791
+ command.append("--force")
2792
+ return command
2793
+
2794
+
2795
+ def dispatch_from_route(flow: str, slug: str, route: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:
2796
+ command = to_internal_command(str(route.get("recommended_command", "")).strip())
2797
+ route_status = build_route_status_summary(route) if isinstance(route, dict) else {}
2798
+ reusable_session_id = str(route_status.get("reusable_session_id", "")).strip()
2799
+
2800
+ if command == "cgc-build":
2801
+ command_args = ["--slug", slug, "--timeout-seconds", str(args.timeout_seconds)]
2802
+ if args.step_number is not None:
2803
+ command_args.extend(["--step-number", str(args.step_number)])
2804
+ if args.checklist_file:
2805
+ command_args.extend(["--checklist-file", args.checklist_file])
2806
+ if args.audit_root:
2807
+ command_args.extend(["--audit-root", args.audit_root])
2808
+ if args.dry_run:
2809
+ command_args.append("--dry-run")
2810
+ if args.return_all_messages:
2811
+ command_args.append("--return-all-messages")
2812
+ if reusable_session_id:
2813
+ command_args.extend(["--session-id", reusable_session_id])
2814
+ return run_json_script("run_codecgc_build.py", *command_args)
2815
+
2816
+ if command == "cgc-fix":
2817
+ command_args = ["--slug", slug, "--timeout-seconds", str(args.timeout_seconds)]
2818
+ if args.step_number is not None:
2819
+ command_args.extend(["--step-number", str(args.step_number)])
2820
+ if args.checklist_file:
2821
+ command_args.extend(["--checklist-file", args.checklist_file])
2822
+ if args.audit_root:
2823
+ command_args.extend(["--audit-root", args.audit_root])
2824
+ if args.dry_run:
2825
+ command_args.append("--dry-run")
2826
+ if args.return_all_messages:
2827
+ command_args.append("--return-all-messages")
2828
+ if reusable_session_id:
2829
+ command_args.extend(["--session-id", reusable_session_id])
2830
+ return run_json_script("run_codecgc_fix.py", *command_args)
2831
+
2832
+ if command == "cgc-test":
2833
+ command_args = ["--flow", flow, "--slug", slug, "--timeout-seconds", str(args.timeout_seconds)]
2834
+ if args.step_number is not None:
2835
+ command_args.extend(["--step-number", str(args.step_number)])
2836
+ if args.checklist_file:
2837
+ command_args.extend(["--checklist-file", args.checklist_file])
2838
+ if args.audit_root:
2839
+ command_args.extend(["--audit-root", args.audit_root])
2840
+ if args.dry_run:
2841
+ command_args.append("--dry-run")
2842
+ if args.return_all_messages:
2843
+ command_args.append("--return-all-messages")
2844
+ if reusable_session_id:
2845
+ command_args.extend(["--session-id", reusable_session_id])
2846
+ return run_json_script("run_codecgc_test.py", *command_args)
2847
+
2848
+ if command == "cgc-review":
2849
+ audit_file = args.audit_file or str(route.get("audit_path", "")).strip()
2850
+ if not audit_file:
2851
+ return {
2852
+ "success": False,
2853
+ "error": "Review 调度需要先提供 audit 文件。",
2854
+ "recommended_command": to_public_command("cgc-review"),
2855
+ "next": route.get("next", "先提供 audit 文件,再执行 review 调度。"),
2856
+ }
2857
+ if not args.decision:
2858
+ return {
2859
+ "success": False,
2860
+ "error": "Review 调度需要先提供 --decision accepted|changes-requested。",
2861
+ "recommended_command": to_public_command("cgc-review"),
2862
+ "next": route.get("next", "先提供 review 决策,再执行 review 调度。"),
2863
+ }
2864
+ command_args = ["--audit-file", audit_file, "--decision", args.decision]
2865
+ for item in args.risk:
2866
+ command_args.extend(["--risk", item])
2867
+ if args.next_step:
2868
+ command_args.extend(["--next-step", args.next_step])
2869
+ if args.force:
2870
+ command_args.append("--force")
2871
+ return run_json_script("review_codecgc_workflow.py", *command_args)
2872
+
2873
+ return {
2874
+ "success": False,
2875
+ "error": f"当前状态 '{command or 'closed'}' 不支持自动调度。",
2876
+ "recommended_command": to_public_command(command),
2877
+ "next": route.get("next", "请手动执行推荐命令。"),
2878
+ }
2879
+
2880
+
2881
+ def refresh_route_after_dispatch(flow: str, slug: str, result: dict[str, Any]) -> None:
2882
+ dispatch_result = result.get("dispatch_result", {}) if isinstance(result.get("dispatch_result"), dict) else {}
2883
+ if not dispatch_result.get("success"):
2884
+ return
2885
+ if result.get("entry_mode") == "new":
2886
+ return
2887
+
2888
+ refreshed_route = run_json_script("route_codecgc_workflow.py", "--flow", flow, "--slug", slug)
2889
+ result["route"] = refreshed_route
2890
+ result["route_status"] = build_route_status_summary(refreshed_route)
2891
+ result["recommended_command"] = to_public_command(str(refreshed_route.get("recommended_command", "")).strip())
2892
+ result["next"] = str(refreshed_route.get("next", "")).strip()
2893
+
2894
+ if str(dispatch_result.get("review_state", "")).strip():
2895
+ policy_status = build_review_policy_status(dispatch_result)
2896
+ route_status = result.get("route_status", {}) if isinstance(result.get("route_status"), dict) else {}
2897
+ route_status["review_fallback_stage"] = policy_status["fallback_stage"]
2898
+ route_status["review_policy_reason"] = policy_status["policy_reason"]
2899
+ route_status["review_action_kind"] = policy_status["recommended_action_kind"]
2900
+ if policy_status["human_status_summary"]:
2901
+ route_status["human_status_summary"] = policy_status["human_status_summary"]
2902
+ if policy_status["operator_action_summary"]:
2903
+ route_status["operator_action_summary"] = policy_status["operator_action_summary"]
2904
+ result["route_status"] = route_status
2905
+
2906
+
2907
+ def run_new_mode(args: argparse.Namespace) -> dict[str, Any]:
2908
+ flow = infer_flow_for_new_work(args)
2909
+ resolved_target_paths = resolve_target_paths(args)
2910
+ resolved_kind = resolve_kind(args, resolved_target_paths)
2911
+ planning_snapshot = build_planning_snapshot(flow, args, resolved_target_paths, resolved_kind)
2912
+ plan = run_json_script("plan_codecgc_workflow.py", *build_plan_args(args, flow))
2913
+ planning_missing_fields = list(plan.get("planning_missing_fields", []))
2914
+ clarification_prompts = build_clarification_prompts(flow, planning_missing_fields)
2915
+ recommended_command = to_public_command(str(plan.get("recommended_command", "")).strip())
2916
+ effective_auto_dispatch, auto_dispatch_reason = resolve_effective_auto_dispatch(args, recommended_command)
2917
+ result = {
2918
+ "success": bool(plan.get("success")),
2919
+ "entry_mode": "new",
2920
+ "flow": flow,
2921
+ "slug": plan.get("slug", args.slug),
2922
+ "request": args.request,
2923
+ "resolved_kind": resolved_kind,
2924
+ "resolved_target_paths": resolved_target_paths,
2925
+ "planning_snapshot": planning_snapshot,
2926
+ "captured_fields": build_captured_fields(planning_snapshot),
2927
+ "auto_dispatch": effective_auto_dispatch,
2928
+ "auto_dispatch_reason": auto_dispatch_reason,
2929
+ "plan": plan,
2930
+ "planning_status": plan.get("planning_status", ""),
2931
+ "planning_reasons": plan.get("planning_reasons", []),
2932
+ "planning_missing_fields": planning_missing_fields,
2933
+ "clarification_mode": build_clarification_mode(flow, planning_missing_fields, resolved_kind),
2934
+ "clarification_prompts": clarification_prompts,
2935
+ "route": plan.get("route", {}),
2936
+ "recommended_command": recommended_command,
2937
+ "next": plan.get("next", ""),
2938
+ "dispatched": False,
2939
+ "dispatch_result": None,
2940
+ }
2941
+ result["suggested_reply_template"] = build_suggested_reply_template(flow, result["clarification_prompts"])
2942
+ result["suggested_reply_payload"] = build_suggested_reply_payload(flow, planning_snapshot, planning_missing_fields)
2943
+ if isinstance(plan.get("route"), dict):
2944
+ result["route_status"] = build_route_status_summary(plan.get("route", {}))
2945
+ if not effective_auto_dispatch or not plan.get("success"):
2946
+ result["next"] = build_user_facing_next(result)
2947
+ result["human_summary"] = build_new_mode_human_summary(result)
2948
+ result["assistant_reply"] = build_assistant_reply(result)
2949
+ result["operator_brief"] = build_operator_brief(result)
2950
+ return attach_entry_summary(result)
2951
+
2952
+ if recommended_command not in {"cgc-build", "cgc-fix"}:
2953
+ result["next"] = build_user_facing_next(result)
2954
+ result["human_summary"] = build_new_mode_human_summary(result)
2955
+ result["assistant_reply"] = build_assistant_reply(result)
2956
+ result["operator_brief"] = build_operator_brief(result)
2957
+ return attach_entry_summary(result)
2958
+
2959
+ dispatch_result = dispatch_from_route(flow, str(plan.get("slug", args.slug)), plan.get("route", {}), args)
2960
+ result["dispatched"] = True
2961
+ result["dispatch_result"] = dispatch_result
2962
+ if dispatch_result.get("success"):
2963
+ result["recommended_command"] = to_public_command(dispatch_result.get("recommended_command", ""))
2964
+ result["next"] = dispatch_result.get("next", "")
2965
+ else:
2966
+ apply_dispatch_failure_context(result, recommended_command)
2967
+ result["human_summary"] = build_new_mode_human_summary(result)
2968
+ result["assistant_reply"] = build_assistant_reply(result)
2969
+ result["operator_brief"] = build_operator_brief(result)
2970
+ return attach_entry_summary(result)
2971
+
2972
+
2973
+ def run_existing_mode(args: argparse.Namespace, explain_only: bool) -> dict[str, Any]:
2974
+ if not args.slug.strip() and not args.latest and not args.request.strip():
2975
+ raise ValueError("Continue or explain mode requires --slug, --latest, or a continue/explain request.")
2976
+ slug = args.slug
2977
+ use_latest = args.latest or (args.request.strip() and not args.slug.strip())
2978
+ latest_discovered: tuple[str, str] | None = None
2979
+ if not slug and use_latest:
2980
+ latest_discovered = discover_latest_flow(
2981
+ args.include_fixtures,
2982
+ request=args.request,
2983
+ explain_only=explain_only,
2984
+ )
2985
+ if not latest_discovered:
2986
+ if request_implies_review(args.request):
2987
+ return build_missing_existing_workflow_result(
2988
+ request=args.request,
2989
+ explain_only=explain_only,
2990
+ include_fixtures=args.include_fixtures,
2991
+ human_summary="当前没有待审核的 CodeCGC 工作流。",
2992
+ next_text="先用“现在下一步该做什么”查看最近工作流,或先完成执行步骤后再回来审核。",
2993
+ action_reason="当前仓库里没有处于 awaiting-review 的最近工作流,无法直接处理“通过/不通过/审核”这类请求。",
2994
+ workflow_state="needs-review-target",
2995
+ reply_kind="review-target-missing",
2996
+ action_type="wait-review-target",
2997
+ dispatch_blocker="missing-review-workflow",
2998
+ recovery_hint="先定位一个待审核 workflow,或先完成 build/fix 再回来审核。",
2999
+ recommended_command="cgc",
3000
+ command_args=["--request", "现在下一步该做什么"],
3001
+ )
3002
+ return build_missing_existing_workflow_result(
3003
+ request=args.request,
3004
+ explain_only=explain_only,
3005
+ include_fixtures=args.include_fixtures,
3006
+ recommended_command="cgc",
3007
+ command_args=["--request", "<描述你的新需求>"],
3008
+ )
3009
+ slug = latest_discovered[1]
3010
+ flow = args.flow or (latest_discovered[0] if latest_discovered else resolve_existing_flow(args))
3011
+ route = run_json_script("route_codecgc_workflow.py", "--flow", flow, "--slug", slug)
3012
+ recommended_command = to_public_command(str(route.get("recommended_command", "")).strip())
3013
+ replan_payload = build_existing_workflow_replan_payload(flow, slug)
3014
+ effective_auto_dispatch, auto_dispatch_reason = resolve_effective_auto_dispatch(
3015
+ args,
3016
+ recommended_command,
3017
+ explain_only=explain_only,
3018
+ )
3019
+ result = {
3020
+ "success": bool(route.get("success", True)),
3021
+ "entry_mode": "explain" if explain_only else "continue",
3022
+ "flow": flow,
3023
+ "slug": slug,
3024
+ "auto_dispatch": effective_auto_dispatch,
3025
+ "auto_dispatch_reason": auto_dispatch_reason,
3026
+ "latest": use_latest,
3027
+ "request": args.request,
3028
+ "route": route,
3029
+ "route_status": build_route_status_summary(route),
3030
+ "replan_payload": replan_payload,
3031
+ "recommended_command": recommended_command,
3032
+ "next": route.get("next", ""),
3033
+ "dispatched": False,
3034
+ "dispatch_result": None,
3035
+ }
3036
+ if explain_only or not effective_auto_dispatch:
3037
+ result["next"] = build_user_facing_next(result)
3038
+ result["human_summary"] = build_existing_mode_human_summary(result)
3039
+ result["assistant_reply"] = build_assistant_reply(result)
3040
+ result["operator_brief"] = build_operator_brief(result)
3041
+ return attach_entry_summary(result)
3042
+
3043
+ if recommended_command not in {"cgc-build", "cgc-fix", "cgc-review"}:
3044
+ result["next"] = build_user_facing_next(result)
3045
+ result["human_summary"] = build_existing_mode_human_summary(result)
3046
+ result["assistant_reply"] = build_assistant_reply(result)
3047
+ result["operator_brief"] = build_operator_brief(result)
3048
+ return attach_entry_summary(result)
3049
+
3050
+ dispatch_result = dispatch_from_route(flow, slug, route, args)
3051
+ result["dispatched"] = True
3052
+ result["dispatch_result"] = dispatch_result
3053
+ if dispatch_result.get("success"):
3054
+ result["recommended_command"] = to_public_command(dispatch_result.get("recommended_command", ""))
3055
+ result["next"] = dispatch_result.get("next", "")
3056
+ refresh_route_after_dispatch(flow, slug, result)
3057
+ result["next"] = build_user_facing_next(result)
3058
+ else:
3059
+ apply_dispatch_failure_context(result, recommended_command)
3060
+ result["human_summary"] = build_existing_mode_human_summary(result)
3061
+ result["assistant_reply"] = build_assistant_reply(result)
3062
+ result["operator_brief"] = build_operator_brief(result)
3063
+ return attach_entry_summary(result)
3064
+
3065
+
3066
+ def run_governance_mode(args: argparse.Namespace, governance: dict[str, str]) -> dict[str, Any]:
3067
+ request = args.request.strip()
3068
+ slug = args.slug.strip()
3069
+ flow = args.flow.strip()
3070
+ human_summary = str(governance.get("human_summary", "")).strip()
3071
+ next_text = str(governance.get("next", "")).strip()
3072
+ skill = str(governance.get("skill", "")).strip()
3073
+ governance_type = str(governance.get("governance_type", "")).strip()
3074
+ dispatch_result: dict[str, Any] | None = None
3075
+ auto_dispatch = False
3076
+ auto_dispatch_reason = "governance-skill-routing"
3077
+
3078
+ if governance_type == "decide":
3079
+ auto_dispatch = True
3080
+ auto_dispatch_reason = "governance-auto-write"
3081
+ dispatch_result = run_json_script(
3082
+ "write_codecgc_decision.py",
3083
+ "--summary",
3084
+ request,
3085
+ "--constraint",
3086
+ "将其视为约束未来 CodeCGC 行为的长期有效规则。",
3087
+ "--source",
3088
+ "cgc-entry 治理分诊",
3089
+ )
3090
+ if dispatch_result.get("success"):
3091
+ decision = dispatch_result.get("decision", {}) if isinstance(dispatch_result.get("decision"), dict) else {}
3092
+ decision_path = str(decision.get("path", "")).strip()
3093
+ human_summary = "已将这条长期决定写入 CodeCGC 的长期决策资产。"
3094
+ next_text = f"下一步检查 {decision_path or 'codecgc/compound/codecgc-decisions.md'},确认表述是否还需要补充。"
3095
+
3096
+ elif governance_type == "learn":
3097
+ auto_dispatch = True
3098
+ auto_dispatch_reason = "governance-auto-write"
3099
+ dispatch_result = run_json_script(
3100
+ "write_codecgc_learning.py",
3101
+ "--summary",
3102
+ request,
3103
+ "--kind",
3104
+ "practice",
3105
+ "--instruction",
3106
+ "遇到同类问题时,优先把可复用经验沉淀到长期资产,而不是只停留在会话里。",
3107
+ "--source",
3108
+ "cgc-entry 治理分诊",
3109
+ )
3110
+ if dispatch_result.get("success"):
3111
+ learning = dispatch_result.get("learning", {}) if isinstance(dispatch_result.get("learning"), dict) else {}
3112
+ learning_path = str(learning.get("path", "")).strip()
3113
+ human_summary = "已将这条可复用经验写入 CodeCGC 的经验资产。"
3114
+ next_text = f"下一步检查 {learning_path or 'codecgc/compound/codecgc-learning-log.md'},确认是否需要补充更具体的后续指引。"
3115
+
3116
+ elif governance_type == "arch":
3117
+ auto_dispatch = True
3118
+ auto_dispatch_reason = "governance-auto-write"
3119
+ dispatch_result = run_json_script(
3120
+ "write_codecgc_architecture.py",
3121
+ "--summary",
3122
+ request,
3123
+ "--note",
3124
+ "将其视为当前仓库架构现状的长期更新记录。",
3125
+ "--source",
3126
+ "cgc-entry 治理分诊",
3127
+ )
3128
+ if dispatch_result.get("success"):
3129
+ architecture = (
3130
+ dispatch_result.get("architecture", {})
3131
+ if isinstance(dispatch_result.get("architecture"), dict)
3132
+ else {}
3133
+ )
3134
+ architecture_path = str(architecture.get("path", "")).strip()
3135
+ human_summary = "已将这条当前态架构更新写入 CodeCGC 的架构资产。"
3136
+ next_text = (
3137
+ f"下一步检查 {architecture_path or 'codecgc/architecture/codecgc-system-map.md'},"
3138
+ "确认是否需要补充更细的模块边界或集成说明。"
3139
+ )
3140
+
3141
+ elif governance_type == "req":
3142
+ auto_dispatch = True
3143
+ auto_dispatch_reason = "governance-auto-write"
3144
+ dispatch_result = run_json_script(
3145
+ "write_codecgc_requirement.py",
3146
+ "--summary",
3147
+ request,
3148
+ "--note",
3149
+ "将其视为产品面的长期稳定需求更新。",
3150
+ "--source",
3151
+ "cgc-entry 治理分诊",
3152
+ )
3153
+ if dispatch_result.get("success"):
3154
+ requirement = (
3155
+ dispatch_result.get("requirement", {})
3156
+ if isinstance(dispatch_result.get("requirement"), dict)
3157
+ else {}
3158
+ )
3159
+ requirement_path = str(requirement.get("path", "")).strip()
3160
+ human_summary = "已将这条稳定需求更新写入 CodeCGC 的需求资产。"
3161
+ next_text = (
3162
+ f"下一步检查 {requirement_path or 'codecgc/requirements/codecgc-core-requirements.md'},"
3163
+ "确认是否需要补充更明确的能力边界或非目标说明。"
3164
+ )
3165
+
3166
+ elif governance_type == "roadmap":
3167
+ auto_dispatch = True
3168
+ auto_dispatch_reason = "governance-auto-write"
3169
+ dispatch_result = run_json_script(
3170
+ "write_codecgc_roadmap.py",
3171
+ "--summary",
3172
+ request,
3173
+ "--goal",
3174
+ request,
3175
+ "--source",
3176
+ "cgc-entry 治理分诊",
3177
+ )
3178
+ if dispatch_result.get("success"):
3179
+ roadmap = dispatch_result.get("roadmap", {}) if isinstance(dispatch_result.get("roadmap"), dict) else {}
3180
+ roadmap_directory = str(roadmap.get("directory", "")).strip()
3181
+ human_summary = "已将这条大规模规划请求初始化为 CodeCGC 的 roadmap 资产。"
3182
+ next_text = (
3183
+ f"下一步检查 {roadmap_directory or 'codecgc/roadmap/'},"
3184
+ "确认 phases、delivery plan 和后续 child workflow 拆分是否足够清晰。"
3185
+ )
3186
+
3187
+ elif governance_type == "refactor":
3188
+ auto_dispatch = True
3189
+ auto_dispatch_reason = "governance-auto-write"
3190
+ dispatch_result = run_json_script(
3191
+ "write_codecgc_refactor.py",
3192
+ "--summary",
3193
+ request,
3194
+ "--note",
3195
+ "将其视为保持行为不变、但仍需走受控执行流程的结构优化候选项。",
3196
+ "--source",
3197
+ "cgc-entry 治理分诊",
3198
+ )
3199
+ if dispatch_result.get("success"):
3200
+ refactor = dispatch_result.get("refactor", {}) if isinstance(dispatch_result.get("refactor"), dict) else {}
3201
+ refactor_path = str(refactor.get("path", "")).strip()
3202
+ human_summary = "已将这条受控重构请求写入 CodeCGC 的重构队列资产。"
3203
+ next_text = (
3204
+ f"下一步检查 {refactor_path or 'codecgc/compound/codecgc-productization-gap.md'},"
3205
+ "确认这条重构是否需要转成更具体的可执行 workflow。"
3206
+ )
3207
+
3208
+ elif governance_type == "guide":
3209
+ auto_dispatch = True
3210
+ auto_dispatch_reason = "governance-auto-write"
3211
+ dispatch_result = run_json_script(
3212
+ "write_codecgc_guide.py",
3213
+ "--summary",
3214
+ request,
3215
+ "--audience",
3216
+ "developer",
3217
+ "--note",
3218
+ "将其视为需要长期维护的任务导向指南入口。",
3219
+ "--source",
3220
+ "cgc-entry 治理分诊",
3221
+ )
3222
+ if dispatch_result.get("success"):
3223
+ guide = dispatch_result.get("guide", {}) if isinstance(dispatch_result.get("guide"), dict) else {}
3224
+ guide_path = str(guide.get("path", "")).strip()
3225
+ human_summary = "已将这条指南请求写入 CodeCGC 的 docs guide 资产。"
3226
+ next_text = (
3227
+ f"下一步检查 {guide_path or 'codecgc/docs/'},"
3228
+ "补全目的、步骤和使用边界。"
3229
+ )
3230
+
3231
+ elif governance_type == "libdoc":
3232
+ auto_dispatch = True
3233
+ auto_dispatch_reason = "governance-auto-write"
3234
+ dispatch_result = run_json_script(
3235
+ "write_codecgc_libdoc.py",
3236
+ "--summary",
3237
+ request,
3238
+ "--surface",
3239
+ "public-api",
3240
+ "--source",
3241
+ "cgc-entry 治理分诊",
3242
+ )
3243
+ if dispatch_result.get("success"):
3244
+ libdoc = dispatch_result.get("libdoc", {}) if isinstance(dispatch_result.get("libdoc"), dict) else {}
3245
+ libdoc_path = str(libdoc.get("path", "")).strip()
3246
+ human_summary = "已将这条参考文档请求写入 CodeCGC 的 libdoc 资产。"
3247
+ next_text = (
3248
+ f"下一步检查 {libdoc_path or 'codecgc/reference/'},"
3249
+ "补全公开契约、示例与边界说明。"
3250
+ )
3251
+
3252
+ elif governance_type == "trick":
3253
+ auto_dispatch = True
3254
+ auto_dispatch_reason = "governance-auto-write"
3255
+ dispatch_result = run_json_script(
3256
+ "write_codecgc_trick.py",
3257
+ "--summary",
3258
+ request,
3259
+ "--kind",
3260
+ "technique",
3261
+ "--instruction",
3262
+ "将其视为可复用默认做法,后续可继续补充示例与适用边界。",
3263
+ "--source",
3264
+ "cgc-entry 治理分诊",
3265
+ )
3266
+ if dispatch_result.get("success"):
3267
+ trick = dispatch_result.get("trick", {}) if isinstance(dispatch_result.get("trick"), dict) else {}
3268
+ trick_path = str(trick.get("path", "")).strip()
3269
+ human_summary = "已将这条技巧请求写入 CodeCGC 的 trick 资产。"
3270
+ next_text = (
3271
+ f"下一步检查 {trick_path or 'codecgc/compound/codecgc-tricks.md'},"
3272
+ "确认是否需要补更多默认做法与适用范围。"
3273
+ )
3274
+
3275
+ elif governance_type == "explore":
3276
+ auto_dispatch = True
3277
+ auto_dispatch_reason = "governance-auto-write"
3278
+ dispatch_result = run_json_script(
3279
+ "write_codecgc_explore.py",
3280
+ "--summary",
3281
+ request,
3282
+ "--kind",
3283
+ "question",
3284
+ "--source",
3285
+ "cgc-entry 治理分诊",
3286
+ )
3287
+ if dispatch_result.get("success"):
3288
+ explore = dispatch_result.get("explore", {}) if isinstance(dispatch_result.get("explore"), dict) else {}
3289
+ explore_path = str(explore.get("path", "")).strip()
3290
+ human_summary = "已将这条探索请求写入 CodeCGC 的 exploration 资产。"
3291
+ next_text = (
3292
+ f"下一步检查 {explore_path or 'codecgc/compound/codecgc-explorations.md'},"
3293
+ "再决定是否要继续做真实代码探索与证据回填。"
3294
+ )
3295
+
3296
+ result = {
3297
+ "success": True,
3298
+ "entry_mode": "governance",
3299
+ "flow": flow,
3300
+ "slug": slug,
3301
+ "request": request,
3302
+ "governance_type": governance_type,
3303
+ "recommended_skill": skill,
3304
+ "recommended_command": "",
3305
+ "next": next_text,
3306
+ "human_summary": human_summary,
3307
+ "assistant_reply": "",
3308
+ "dispatched": bool(dispatch_result),
3309
+ "dispatch_result": dispatch_result,
3310
+ "auto_dispatch": auto_dispatch,
3311
+ "auto_dispatch_reason": auto_dispatch_reason,
3312
+ "route_status": {
3313
+ "workflow_state": "governance-routing",
3314
+ "recommended_command": "",
3315
+ "current_step_number": 0,
3316
+ "current_task_id": "",
3317
+ "current_kind": "",
3318
+ "current_target_paths": [],
3319
+ "review_decision": "",
3320
+ "review_step_number": 0,
3321
+ "audit_path": "",
3322
+ "human_status_summary": human_summary,
3323
+ "operator_action_summary": next_text,
3324
+ "is_closed": False,
3325
+ "needs_review_decision": False,
3326
+ "needs_execution": False,
3327
+ },
3328
+ }
3329
+ result["assistant_reply"] = build_assistant_reply(result)
3330
+ result["operator_brief"] = build_operator_brief(result)
3331
+ return attach_entry_summary(result)
3332
+
3333
+
3334
+ def main() -> int:
3335
+ configure_utf8_stdio()
3336
+ parser = build_parser()
3337
+ args = parser.parse_args()
3338
+ args = apply_entry_payload(args)
3339
+
3340
+ try:
3341
+ followup_payload = load_payload_input(args)
3342
+ nested_payload = followup_payload.get("payload", {}) if isinstance(followup_payload.get("payload"), dict) else {}
3343
+ governance_followup = resolve_governance_followup(nested_payload, args.request)
3344
+ if governance_followup:
3345
+ dispatch_result = run_json_script(
3346
+ str(governance_followup.get("script", "")).strip(),
3347
+ *[str(item) for item in governance_followup.get("args", []) if str(item).strip()],
3348
+ )
3349
+ artifact_path = str(governance_followup.get("artifact_path", "")).strip() or str(nested_payload.get("artifact_path", "")).strip()
3350
+ governance_type = str(governance_followup.get("governance_type", "")).strip()
3351
+ result = {
3352
+ "success": bool(dispatch_result.get("success")),
3353
+ "entry_mode": "governance",
3354
+ "flow": "",
3355
+ "slug": "",
3356
+ "request": args.request.strip(),
3357
+ "governance_type": governance_type,
3358
+ "recommended_skill": f"cgc-{governance_type}",
3359
+ "recommended_command": "",
3360
+ "next": f"已把补充内容回写到 {artifact_path or '对应长期资产'},如还有缺口可继续补充。",
3361
+ "human_summary": "已将这轮补充内容回写到同一个治理资产中。",
3362
+ "assistant_reply": "",
3363
+ "dispatched": True,
3364
+ "dispatch_result": dispatch_result,
3365
+ "auto_dispatch": True,
3366
+ "auto_dispatch_reason": "governance-followup-write",
3367
+ "route_status": {
3368
+ "workflow_state": "governance-routing",
3369
+ "recommended_command": "",
3370
+ "current_step_number": 0,
3371
+ "current_task_id": "",
3372
+ "current_kind": "",
3373
+ "current_target_paths": [],
3374
+ "review_decision": "",
3375
+ "review_step_number": 0,
3376
+ "audit_path": "",
3377
+ "human_status_summary": "已将这轮补充内容回写到同一个治理资产中。",
3378
+ "operator_action_summary": f"继续检查 {artifact_path or '对应长期资产'},必要时补充下一轮内容。",
3379
+ "is_closed": False,
3380
+ "needs_review_decision": False,
3381
+ "needs_execution": False,
3382
+ },
3383
+ }
3384
+ result["assistant_reply"] = build_assistant_reply(result)
3385
+ result["operator_brief"] = build_operator_brief(result)
3386
+ result = attach_entry_summary(result)
3387
+ result["inferred_mode"] = "governance-followup"
3388
+ print_json(result)
3389
+ return 0 if result.get("success") else 1
3390
+
3391
+ governance = classify_governance_request(args.request)
3392
+ if governance:
3393
+ result = run_governance_mode(args, governance)
3394
+ result["inferred_mode"] = "governance"
3395
+ print_json(result)
3396
+ return 0
3397
+ inferred_mode = infer_mode(args)
3398
+ if inferred_mode == "new":
3399
+ result = run_new_mode(args)
3400
+ elif inferred_mode == "continue":
3401
+ result = run_existing_mode(args, explain_only=False)
3402
+ else:
3403
+ result = run_existing_mode(args, explain_only=True)
3404
+ result["inferred_mode"] = inferred_mode
3405
+ except Exception as error:
3406
+ print_json({"success": False, "error": str(error)}, file=sys.stderr)
3407
+ return 1
3408
+
3409
+ print_json(result)
3410
+ operator_brief = result.get("operator_brief", {}) if isinstance(result.get("operator_brief"), dict) else {}
3411
+ is_closed = bool(operator_brief.get("is_closed"))
3412
+ workflow_state = str(operator_brief.get("workflow_state", "")).strip()
3413
+ if result.get("success") or workflow_state in {"needs-planning", "awaiting-build", "awaiting-fix", "awaiting-review"} or not is_closed:
3414
+ return 0
3415
+ return 1
3416
+
3417
+
3418
+ if __name__ == "__main__":
3419
+ raise SystemExit(main())