@hunyed15/codecgc 0.1.7 → 0.1.9
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.
- package/.claude/hooks/route-edit.ps1 +58 -57
- package/INSTALLATION.md +122 -484
- package/README.md +124 -149
- package/bin/cgc-external-status.js +4 -0
- package/bin/cgc-start.js +4 -0
- package/bin/codecgc.js +141 -20
- package/codecgc/compound/codecgc-capability-matrix.md +37 -0
- package/codecgc/reference/README.md +69 -0
- package/codecgc/reference/execution-model.md +3 -1
- package/codecgc/reference/maintainer-guide.md +93 -0
- package/codecgc/reference/mcp-tool-surface.md +112 -0
- package/codecgc/reference/onboarding.md +69 -0
- package/codecgc/reference/operation-guide.md +29 -23
- package/codecgc/reference/path-contract.md +53 -0
- package/codecgc/reference/policy-routing.md +58 -0
- package/codecgc/reference/project-structure.md +87 -0
- package/codecgc/reference/quickstart.md +110 -0
- package/codecgc/reference/real-workflow-loop.md +123 -0
- package/codecgc/reference/recovery-loop.md +109 -0
- package/codecgc/reference/release-maintenance-playbook.md +4 -1
- package/codecgc/reference/troubleshooting.md +114 -0
- package/codecgc/roadmap/codecgc-release-maintenance/delivery-plan.md +49 -0
- package/codecgc/roadmap/codecgc-release-maintenance/overview.md +41 -0
- package/codecgc/roadmap/codecgc-release-maintenance/phases.md +84 -0
- package/codecgc/templates/claude/settings.local.json +27 -0
- package/codecgc/templates/codex/codecgcrc.json +22 -0
- package/codecgc/templates/gemini/codecgc-policy.toml +47 -0
- package/codecgcmcp/README.md +57 -11
- package/codecgcmcp/src/codecgcmcp/server.py +164 -26
- package/codexmcp/src/codexmcp/server.py +45 -0
- package/geminimcp/src/geminimcp/server.py +106 -24
- package/model-routing.yaml +31 -6
- package/package.json +12 -4
- package/scripts/audit_codecgc_external_capabilities.py +83 -4
- package/scripts/audit_codecgc_historical_audits.py +42 -2
- package/scripts/audit_codecgc_package_runtime.py +76 -4
- package/scripts/audit_codecgc_release_readiness.py +55 -3
- package/scripts/audit_codecgc_workflow_history.py +8 -5
- package/scripts/build_codecgc_task.py +69 -45
- package/scripts/codecgc_artifact_roots.py +8 -40
- package/scripts/codecgc_console_io.py +3 -45
- package/scripts/codecgc_executor_registry.py +4 -54
- package/scripts/codecgc_path_contract.py +7 -0
- package/scripts/codecgc_policy.py +447 -0
- package/scripts/codecgc_routing_paths.py +3 -16
- package/scripts/codecgc_routing_template.py +11 -135
- package/scripts/codecgc_runtime/__init__.py +1 -0
- package/scripts/codecgc_runtime/artifacts.py +42 -0
- package/scripts/codecgc_runtime/console.py +45 -0
- package/scripts/codecgc_runtime/executor_registry.py +55 -0
- package/scripts/codecgc_runtime/mcp_config.py +72 -0
- package/scripts/codecgc_runtime/path_contract.py +123 -0
- package/scripts/codecgc_runtime/paths.py +22 -0
- package/scripts/codecgc_runtime/routing_paths.py +16 -0
- package/scripts/codecgc_runtime/routing_template.py +171 -0
- package/scripts/codecgc_runtime/workflow_runtime.py +72 -0
- package/scripts/codecgc_runtime_paths.py +3 -22
- package/scripts/codecgc_step_control.py +3 -2
- package/scripts/codecgc_workflow_runtime.py +3 -71
- package/scripts/entry_codecgc_workflow.py +4 -0
- package/scripts/install_codecgc.py +560 -32
- package/scripts/normalize_codecgc_audits.py +5 -3
- package/scripts/postinstall_codecgc.js +6 -56
- package/scripts/review_codecgc_workflow.py +6 -3
- package/scripts/route_codecgc_workflow.py +67 -36
- package/scripts/run_codecgc_build.py +28 -0
- package/scripts/run_codecgc_fix.py +28 -0
- package/scripts/run_codecgc_task.py +5 -2
- package/scripts/run_codecgc_test.py +28 -0
- package/scripts/sync_codecgc_mcp_config.py +4 -54
- package/scripts/write_codecgc_review.py +7 -3
|
@@ -18,6 +18,10 @@ from mcp.server.fastmcp import FastMCP
|
|
|
18
18
|
from pydantic import BeforeValidator, Field
|
|
19
19
|
import shutil
|
|
20
20
|
|
|
21
|
+
DEFAULT_GEMINI_APPROVAL_MODE = "auto_edit"
|
|
22
|
+
DEFAULT_GEMINI_TIMEOUT_SECONDS = 600
|
|
23
|
+
PROJECT_GEMINI_POLICY_RELATIVE_PATH = Path(".gemini") / "policies" / "codecgc-policy.toml"
|
|
24
|
+
|
|
21
25
|
mcp = FastMCP("Gemini MCP Server-from guda.studio")
|
|
22
26
|
|
|
23
27
|
# Mirror of model-routing.yaml backend_paths — keep these hints in sync with
|
|
@@ -49,6 +53,14 @@ def _normalize_path_text(path_value: Path | str) -> str:
|
|
|
49
53
|
return str(path_value).replace("\\", "/").strip()
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
def _resolve_project_gemini_policy(cd: Path) -> Path | None:
|
|
57
|
+
"""Return the CodeCGC project-local Gemini policy if the workspace installed it."""
|
|
58
|
+
policy_path = cd / PROJECT_GEMINI_POLICY_RELATIVE_PATH
|
|
59
|
+
if policy_path.is_file():
|
|
60
|
+
return policy_path
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
52
64
|
def _is_probably_backend_path(path_value: Path | str) -> bool:
|
|
53
65
|
"""Best-effort check to keep frontend-only Gemini tasks away from backend files."""
|
|
54
66
|
normalized = _normalize_path_text(path_value).lower().lstrip("./")
|
|
@@ -126,7 +138,29 @@ def _validate_frontend_target_paths(target_paths: List[Path]) -> tuple[bool, Lis
|
|
|
126
138
|
return True, policy_checks, ""
|
|
127
139
|
|
|
128
140
|
|
|
129
|
-
def
|
|
141
|
+
def _terminate_process_tree(process: subprocess.Popen[str]) -> None:
|
|
142
|
+
"""Terminate a process and its children best-effort."""
|
|
143
|
+
if process.poll() is not None:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if os.name == "nt":
|
|
147
|
+
subprocess.run(
|
|
148
|
+
["taskkill", "/PID", str(process.pid), "/T", "/F"],
|
|
149
|
+
stdin=subprocess.DEVNULL,
|
|
150
|
+
stdout=subprocess.DEVNULL,
|
|
151
|
+
stderr=subprocess.DEVNULL,
|
|
152
|
+
check=False,
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
process.kill()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_shell_command(
|
|
160
|
+
cmd: list[str],
|
|
161
|
+
cwd: str | None = None,
|
|
162
|
+
timeout_seconds: int = DEFAULT_GEMINI_TIMEOUT_SECONDS,
|
|
163
|
+
) -> Generator[str, None, None]:
|
|
130
164
|
"""Execute a command and stream its output line-by-line.
|
|
131
165
|
|
|
132
166
|
Args:
|
|
@@ -158,6 +192,8 @@ def run_shell_command(cmd: list[str], cwd: str | None = None) -> Generator[str,
|
|
|
158
192
|
|
|
159
193
|
output_queue: queue.Queue[str | None] = queue.Queue()
|
|
160
194
|
GRACEFUL_SHUTDOWN_DELAY = 0.3
|
|
195
|
+
started_at = time.monotonic()
|
|
196
|
+
timed_out = False
|
|
161
197
|
|
|
162
198
|
def is_turn_completed(line: str) -> bool:
|
|
163
199
|
"""Check if the line indicates turn completion via JSON parsing."""
|
|
@@ -185,6 +221,11 @@ def run_shell_command(cmd: list[str], cwd: str | None = None) -> Generator[str,
|
|
|
185
221
|
|
|
186
222
|
# Yield lines while process is running
|
|
187
223
|
while True:
|
|
224
|
+
if timeout_seconds > 0 and time.monotonic() - started_at > timeout_seconds:
|
|
225
|
+
timed_out = True
|
|
226
|
+
_terminate_process_tree(process)
|
|
227
|
+
break
|
|
228
|
+
|
|
188
229
|
try:
|
|
189
230
|
line = output_queue.get(timeout=0.5)
|
|
190
231
|
if line is None:
|
|
@@ -197,7 +238,7 @@ def run_shell_command(cmd: list[str], cwd: str | None = None) -> Generator[str,
|
|
|
197
238
|
try:
|
|
198
239
|
process.wait(timeout=5)
|
|
199
240
|
except subprocess.TimeoutExpired:
|
|
200
|
-
process
|
|
241
|
+
_terminate_process_tree(process)
|
|
201
242
|
process.wait()
|
|
202
243
|
thread.join(timeout=5)
|
|
203
244
|
|
|
@@ -209,6 +250,13 @@ def run_shell_command(cmd: list[str], cwd: str | None = None) -> Generator[str,
|
|
|
209
250
|
except queue.Empty:
|
|
210
251
|
break
|
|
211
252
|
|
|
253
|
+
if timed_out:
|
|
254
|
+
raise TimeoutError(
|
|
255
|
+
f"Gemini CLI timed out after {timeout_seconds} seconds. "
|
|
256
|
+
"This usually means the CLI was waiting for interactive approval, "
|
|
257
|
+
"network/authentication, or a long-running tool call."
|
|
258
|
+
)
|
|
259
|
+
|
|
212
260
|
|
|
213
261
|
def _execute_gemini_session(
|
|
214
262
|
*,
|
|
@@ -218,6 +266,7 @@ def _execute_gemini_session(
|
|
|
218
266
|
session_id: str,
|
|
219
267
|
return_all_messages: bool,
|
|
220
268
|
model: str,
|
|
269
|
+
timeout_seconds: int = DEFAULT_GEMINI_TIMEOUT_SECONDS,
|
|
221
270
|
) -> Dict[str, Any]:
|
|
222
271
|
"""Execute Gemini CLI and return the parsed MCP response payload."""
|
|
223
272
|
if not cd.exists():
|
|
@@ -229,7 +278,21 @@ def _execute_gemini_session(
|
|
|
229
278
|
if os.name == "nt":
|
|
230
279
|
prompt = windows_escape(prompt)
|
|
231
280
|
|
|
232
|
-
|
|
281
|
+
effective_timeout_seconds = int(timeout_seconds or 0) or DEFAULT_GEMINI_TIMEOUT_SECONDS
|
|
282
|
+
cmd = [
|
|
283
|
+
"gemini",
|
|
284
|
+
"--skip-trust",
|
|
285
|
+
"--approval-mode",
|
|
286
|
+
DEFAULT_GEMINI_APPROVAL_MODE,
|
|
287
|
+
"--prompt",
|
|
288
|
+
prompt,
|
|
289
|
+
"-o",
|
|
290
|
+
"stream-json",
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
project_policy = _resolve_project_gemini_policy(cd)
|
|
294
|
+
if project_policy is not None:
|
|
295
|
+
cmd.extend(["--policy", project_policy.absolute().as_posix()])
|
|
233
296
|
|
|
234
297
|
if sandbox:
|
|
235
298
|
cmd.extend(["--sandbox"])
|
|
@@ -246,27 +309,36 @@ def _execute_gemini_session(
|
|
|
246
309
|
err_message = ""
|
|
247
310
|
thread_id: Optional[str] = None
|
|
248
311
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
312
|
+
try:
|
|
313
|
+
for line in run_shell_command(
|
|
314
|
+
cmd,
|
|
315
|
+
cwd=cd.absolute().as_posix(),
|
|
316
|
+
timeout_seconds=effective_timeout_seconds,
|
|
317
|
+
):
|
|
318
|
+
try:
|
|
319
|
+
line_dict = json.loads(line.strip())
|
|
320
|
+
all_messages.append(line_dict)
|
|
321
|
+
item_type = line_dict.get("type", "")
|
|
322
|
+
item_role = line_dict.get("role", "")
|
|
323
|
+
if item_type == "message" and item_role == "assistant":
|
|
324
|
+
if (
|
|
325
|
+
"The --prompt (-p) flag has been deprecated and will be removed in a future version. Please use a positional argument for your prompt. See gemini --help for more information.\n"
|
|
326
|
+
in line_dict.get("content", "")
|
|
327
|
+
):
|
|
328
|
+
continue
|
|
329
|
+
agent_messages = agent_messages + line_dict.get("content", "")
|
|
330
|
+
if line_dict.get("session_id") is not None:
|
|
331
|
+
thread_id = line_dict.get("session_id")
|
|
332
|
+
except json.JSONDecodeError:
|
|
333
|
+
err_message += "\n\n[json decode error] " + line
|
|
334
|
+
continue
|
|
335
|
+
except Exception as error:
|
|
336
|
+
err_message += "\n\n[unexpected error] " + f"Unexpected error: {error}. Line: {line!r}"
|
|
337
|
+
success = False
|
|
338
|
+
break
|
|
339
|
+
except TimeoutError as error:
|
|
340
|
+
success = False
|
|
341
|
+
err_message += "\n\n[timeout] " + str(error)
|
|
270
342
|
|
|
271
343
|
if thread_id is None:
|
|
272
344
|
success = False
|
|
@@ -359,6 +431,10 @@ async def gemini(
|
|
|
359
431
|
str,
|
|
360
432
|
"The model to use for the gemini session. This parameter is strictly prohibited unless explicitly specified by the user.",
|
|
361
433
|
] = "",
|
|
434
|
+
timeout_seconds: Annotated[
|
|
435
|
+
int,
|
|
436
|
+
Field(description="Maximum Gemini CLI process runtime in seconds. Defaults to 600."),
|
|
437
|
+
] = DEFAULT_GEMINI_TIMEOUT_SECONDS,
|
|
362
438
|
) -> Dict[str, Any]:
|
|
363
439
|
"""Execute a gemini CLI session and return the results."""
|
|
364
440
|
return await asyncio.to_thread(
|
|
@@ -370,6 +446,7 @@ async def gemini(
|
|
|
370
446
|
session_id=SESSION_ID,
|
|
371
447
|
return_all_messages=return_all_messages,
|
|
372
448
|
model=model,
|
|
449
|
+
timeout_seconds=timeout_seconds,
|
|
373
450
|
)
|
|
374
451
|
)
|
|
375
452
|
|
|
@@ -421,6 +498,10 @@ async def implement_frontend_task(
|
|
|
421
498
|
str,
|
|
422
499
|
"Optional model override. Only use when explicitly requested by the user.",
|
|
423
500
|
] = "",
|
|
501
|
+
timeout_seconds: Annotated[
|
|
502
|
+
int,
|
|
503
|
+
Field(description="Maximum Gemini CLI process runtime in seconds. Defaults to 600."),
|
|
504
|
+
] = DEFAULT_GEMINI_TIMEOUT_SECONDS,
|
|
424
505
|
) -> Dict[str, Any]:
|
|
425
506
|
"""Execute a frontend-only Gemini task with CodeCGC policy checks."""
|
|
426
507
|
valid, policy_checks, validation_error = _validate_frontend_target_paths(target_paths)
|
|
@@ -447,6 +528,7 @@ async def implement_frontend_task(
|
|
|
447
528
|
session_id=SESSION_ID,
|
|
448
529
|
return_all_messages=return_all_messages,
|
|
449
530
|
model=model,
|
|
531
|
+
timeout_seconds=timeout_seconds,
|
|
450
532
|
)
|
|
451
533
|
)
|
|
452
534
|
|
package/model-routing.yaml
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
version:
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
orchestration_paths:
|
|
4
|
+
- "codecgc/**"
|
|
5
|
+
- ".claude/commands/**"
|
|
6
|
+
- ".claude/settings.json"
|
|
7
|
+
- ".mcp.json"
|
|
8
|
+
- "model-routing.yaml"
|
|
9
|
+
|
|
10
|
+
docs_paths:
|
|
11
|
+
- "README.md"
|
|
12
|
+
- "INSTALLATION.md"
|
|
13
|
+
- "docs/**"
|
|
14
|
+
- "CHANGELOG.md"
|
|
2
15
|
|
|
3
16
|
frontend_paths:
|
|
4
17
|
- "apps/web/**"
|
|
@@ -9,8 +22,6 @@ frontend_paths:
|
|
|
9
22
|
- "web/**"
|
|
10
23
|
- "frontend/**"
|
|
11
24
|
|
|
12
|
-
custom_frontend_paths:
|
|
13
|
-
|
|
14
25
|
backend_paths:
|
|
15
26
|
- "apps/api/**"
|
|
16
27
|
- "server/**"
|
|
@@ -19,7 +30,19 @@ backend_paths:
|
|
|
19
30
|
- "src/repositories/**"
|
|
20
31
|
- "backend/**"
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
test_paths:
|
|
34
|
+
frontend:
|
|
35
|
+
- "apps/web/*.test.*"
|
|
36
|
+
- "apps/web/*.spec.*"
|
|
37
|
+
- "apps/web/**/*.test.*"
|
|
38
|
+
- "apps/web/**/*.spec.*"
|
|
39
|
+
- "tests/frontend/**"
|
|
40
|
+
backend:
|
|
41
|
+
- "apps/api/*.test.*"
|
|
42
|
+
- "apps/api/*.spec.*"
|
|
43
|
+
- "apps/api/**/*.test.*"
|
|
44
|
+
- "apps/api/**/*.spec.*"
|
|
45
|
+
- "tests/backend/**"
|
|
23
46
|
|
|
24
47
|
shared_paths:
|
|
25
48
|
- "packages/shared/**"
|
|
@@ -28,7 +51,9 @@ shared_paths:
|
|
|
28
51
|
- "src/types/**"
|
|
29
52
|
|
|
30
53
|
rules:
|
|
31
|
-
|
|
54
|
+
claude_allowed_owners:
|
|
55
|
+
- "orchestration"
|
|
56
|
+
- "docs"
|
|
32
57
|
backend_executor: "codexmcp"
|
|
58
|
+
frontend_executor: "geminimcp"
|
|
33
59
|
shared_policy: "split-first"
|
|
34
|
-
claude_role: "plan-review-accept-only"
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunyed15/codecgc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Claude-hosted multi-model workflow product shell for CodeCGC.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"cgc": "bin/cgc.js",
|
|
9
|
+
"cgc-start": "bin/cgc-start.js",
|
|
9
10
|
"cgc-install": "bin/cgc-install.js",
|
|
10
11
|
"cgc-status": "bin/cgc-status.js",
|
|
11
12
|
"cgc-doctor": "bin/cgc-doctor.js",
|
|
12
13
|
"cgc-package-audit": "bin/cgc-package-audit.js",
|
|
13
14
|
"cgc-external-audit": "bin/cgc-external-audit.js",
|
|
15
|
+
"cgc-external-status": "bin/cgc-external-status.js",
|
|
14
16
|
"cgc-release-readiness": "bin/cgc-release-readiness.js",
|
|
15
17
|
"cgc-lifecycle": "bin/cgc-lifecycle.js",
|
|
16
18
|
"cgc-history": "bin/cgc-history.js",
|
|
@@ -25,10 +27,12 @@
|
|
|
25
27
|
"scripts": {
|
|
26
28
|
"postinstall": "node scripts/postinstall_codecgc.js",
|
|
27
29
|
"cgc:help": "node bin/cgc.js --help",
|
|
30
|
+
"cgc:start": "node bin/cgc-start.js --format summary",
|
|
28
31
|
"cgc:status": "node bin/cgc-status.js --format summary",
|
|
29
32
|
"cgc:doctor": "node bin/cgc-doctor.js --format summary",
|
|
30
33
|
"cgc:package-audit": "node bin/cgc-package-audit.js --format summary",
|
|
31
34
|
"cgc:external-audit": "node bin/cgc-external-audit.js --format summary",
|
|
35
|
+
"cgc:external-status": "node bin/cgc-external-status.js --format summary",
|
|
32
36
|
"cgc:release-readiness": "node bin/cgc-release-readiness.js --format summary",
|
|
33
37
|
"cgc:lifecycle": "node bin/cgc-lifecycle.js --format summary",
|
|
34
38
|
"cgc:history": "node bin/cgc-history.js --status open --last 10 --format summary",
|
|
@@ -40,6 +44,7 @@
|
|
|
40
44
|
"files": [
|
|
41
45
|
"bin/",
|
|
42
46
|
"scripts/*.py",
|
|
47
|
+
"scripts/codecgc_runtime/*.py",
|
|
43
48
|
"scripts/postinstall_codecgc.js",
|
|
44
49
|
"scripts/README-codecgc-cli.md",
|
|
45
50
|
".claude/hooks/route-edit.ps1",
|
|
@@ -56,17 +61,20 @@
|
|
|
56
61
|
"codecgc/cgc-review/",
|
|
57
62
|
"codecgc/cgc-roadmap/",
|
|
58
63
|
"codecgc/cgc-test/",
|
|
64
|
+
"codecgc/compound/",
|
|
59
65
|
"codecgc/reference/",
|
|
66
|
+
"codecgc/roadmap/",
|
|
67
|
+
"codecgc/templates/",
|
|
60
68
|
"codecgcmcp/pyproject.toml",
|
|
61
69
|
"codecgcmcp/README.md",
|
|
62
|
-
"codecgcmcp/src/codecgcmcp
|
|
70
|
+
"codecgcmcp/src/codecgcmcp/*.py",
|
|
63
71
|
"codexmcp/pyproject.toml",
|
|
64
72
|
"codexmcp/README.md",
|
|
65
73
|
"codexmcp/LICENSE",
|
|
66
|
-
"codexmcp/src/codexmcp
|
|
74
|
+
"codexmcp/src/codexmcp/*.py",
|
|
67
75
|
"geminimcp/pyproject.toml",
|
|
68
76
|
"geminimcp/README.md",
|
|
69
|
-
"geminimcp/src/geminimcp
|
|
77
|
+
"geminimcp/src/geminimcp/*.py",
|
|
70
78
|
"requirements.txt",
|
|
71
79
|
"INSTALLATION.md",
|
|
72
80
|
"model-routing.yaml",
|
|
@@ -9,6 +9,16 @@ from codecgc_runtime_paths import resolve_workspace_root
|
|
|
9
9
|
|
|
10
10
|
WORKSPACE = Path(__file__).resolve().parents[1]
|
|
11
11
|
REGISTRY_PATH = WORKSPACE / "codecgc" / "reference" / "external-capability-registry.json"
|
|
12
|
+
STATUS_PANEL_CAPABILITY_ORDER = [
|
|
13
|
+
"memos",
|
|
14
|
+
"github-mcp",
|
|
15
|
+
"linear-mcp",
|
|
16
|
+
"sentry-mcp",
|
|
17
|
+
]
|
|
18
|
+
STATUS_PANEL_SUPPORT_CAPABILITY_ORDER = [
|
|
19
|
+
"augment-search",
|
|
20
|
+
"jira-mcp",
|
|
21
|
+
]
|
|
12
22
|
|
|
13
23
|
|
|
14
24
|
def load_registry() -> dict[str, Any]:
|
|
@@ -107,7 +117,7 @@ def normalize_capability_entry(entry: dict[str, Any], workspace_servers: dict[st
|
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
|
|
110
|
-
def audit_external_capabilities(workspace_override: str = "") -> dict[str, Any]:
|
|
120
|
+
def audit_external_capabilities(workspace_override: str = "", view: str = "audit") -> dict[str, Any]:
|
|
111
121
|
registry = load_registry()
|
|
112
122
|
workspace_servers, workspace_root = load_workspace_mcp_servers(workspace_override)
|
|
113
123
|
entries = registry.get("capabilities", [])
|
|
@@ -142,6 +152,8 @@ def audit_external_capabilities(workspace_override: str = "") -> dict[str, Any]:
|
|
|
142
152
|
human_summary = "外部能力登记表存在缺失字段或非法项。"
|
|
143
153
|
elif drift_items:
|
|
144
154
|
human_summary = "外部能力登记表已就绪,但发现本地额外接入漂移。"
|
|
155
|
+
elif view == "status":
|
|
156
|
+
human_summary = "外部能力状态面板已就绪。"
|
|
145
157
|
|
|
146
158
|
recommended_next_action = ""
|
|
147
159
|
if blocking_items:
|
|
@@ -158,12 +170,14 @@ def audit_external_capabilities(workspace_override: str = "") -> dict[str, Any]:
|
|
|
158
170
|
return {
|
|
159
171
|
"success": ready,
|
|
160
172
|
"mode": "external-capability-audit",
|
|
173
|
+
"presentation_view": view,
|
|
161
174
|
"workspace": str(workspace_root),
|
|
162
175
|
"registry_path": str(REGISTRY_PATH),
|
|
163
176
|
"summary": {
|
|
164
177
|
"ready": ready,
|
|
165
178
|
"scope": "外部能力白名单、接入状态声明与本地 MCP 观测状态",
|
|
166
179
|
"human_summary": human_summary,
|
|
180
|
+
"view": view,
|
|
167
181
|
"capability_count": len(normalized),
|
|
168
182
|
"integrated_count": counts["integrated"],
|
|
169
183
|
"planned_count": counts["planned"],
|
|
@@ -178,7 +192,31 @@ def audit_external_capabilities(workspace_override: str = "") -> dict[str, Any]:
|
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
|
|
181
|
-
def
|
|
195
|
+
def _capability_by_id(result: dict[str, Any], capability_id: str) -> dict[str, Any] | None:
|
|
196
|
+
for item in result.get("capabilities", []):
|
|
197
|
+
if not isinstance(item, dict):
|
|
198
|
+
continue
|
|
199
|
+
if str(item.get("id", "")).strip() == capability_id:
|
|
200
|
+
return item
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _format_capability_panel_line(item: dict[str, Any]) -> str:
|
|
205
|
+
status = str(item.get("status", "")).strip()
|
|
206
|
+
status_label = {
|
|
207
|
+
"integrated": "已纳管",
|
|
208
|
+
"planned": "规划中",
|
|
209
|
+
"optional": "可选",
|
|
210
|
+
}.get(status, status or "未知")
|
|
211
|
+
local_label = "已观测" if item.get("local_ready") else "未观测"
|
|
212
|
+
observed = ", ".join(str(value).strip() for value in item.get("observed_servers", []) if str(value).strip()) or "无"
|
|
213
|
+
return (
|
|
214
|
+
f"- {item.get('name', '')} [{item.get('id', '')}]: "
|
|
215
|
+
f"{status_label} | 本地={local_label} | 服务器={observed}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_audit_summary(result: dict[str, Any]) -> str:
|
|
182
220
|
summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
|
|
183
221
|
lines = [
|
|
184
222
|
f"- 工作区: {result.get('workspace', '')}",
|
|
@@ -249,8 +287,46 @@ def build_summary(result: dict[str, Any]) -> str:
|
|
|
249
287
|
return render_summary_block("CodeCGC 外部能力审计", lines, next_actions)
|
|
250
288
|
|
|
251
289
|
|
|
290
|
+
def build_status_panel(result: dict[str, Any]) -> str:
|
|
291
|
+
summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
|
|
292
|
+
lines = [
|
|
293
|
+
f"- 工作区: {result.get('workspace', '')}",
|
|
294
|
+
f"- 登记表: {result.get('registry_path', '')}",
|
|
295
|
+
f"- 范围: {summary.get('scope', '')}",
|
|
296
|
+
f"- 摘要: {summary.get('human_summary', '')}",
|
|
297
|
+
f"- 就绪: {'是' if summary.get('ready') else '否'}",
|
|
298
|
+
f"- 正式接入: {summary.get('integrated_count', 0)}",
|
|
299
|
+
f"- 规划中: {summary.get('planned_count', 0)}",
|
|
300
|
+
f"- 可选项: {summary.get('optional_count', 0)}",
|
|
301
|
+
f"- 阻塞项: {summary.get('blocking_count', 0)}",
|
|
302
|
+
f"- 漂移项: {summary.get('drift_count', 0)}",
|
|
303
|
+
"- 正式能力面板:",
|
|
304
|
+
]
|
|
305
|
+
for capability_id in STATUS_PANEL_CAPABILITY_ORDER:
|
|
306
|
+
item = _capability_by_id(result, capability_id)
|
|
307
|
+
if isinstance(item, dict):
|
|
308
|
+
lines.append(_format_capability_panel_line(item))
|
|
309
|
+
lines.append("- 其他受管能力:")
|
|
310
|
+
for capability_id in STATUS_PANEL_SUPPORT_CAPABILITY_ORDER:
|
|
311
|
+
item = _capability_by_id(result, capability_id)
|
|
312
|
+
if isinstance(item, dict):
|
|
313
|
+
lines.append(_format_capability_panel_line(item))
|
|
314
|
+
next_actions = []
|
|
315
|
+
next_action = str(summary.get("recommended_next_action", "")).strip()
|
|
316
|
+
if next_action:
|
|
317
|
+
next_actions.append(next_action)
|
|
318
|
+
next_actions.append("需要更细的登记一致性检查时,再跑 cgc-external-audit")
|
|
319
|
+
return render_summary_block("CodeCGC 外部能力状态面板", lines, next_actions)
|
|
320
|
+
|
|
321
|
+
|
|
252
322
|
def main() -> int:
|
|
253
323
|
parser = argparse.ArgumentParser(description="Audit CodeCGC external capability registry and local MCP observations.")
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--view",
|
|
326
|
+
choices=["audit", "status"],
|
|
327
|
+
default="audit",
|
|
328
|
+
help="Rendered summary view. Audit is detailed; status is the concise panel view.",
|
|
329
|
+
)
|
|
254
330
|
parser.add_argument(
|
|
255
331
|
"--format",
|
|
256
332
|
choices=["json", "summary"],
|
|
@@ -264,9 +340,12 @@ def main() -> int:
|
|
|
264
340
|
)
|
|
265
341
|
args = parser.parse_args()
|
|
266
342
|
|
|
267
|
-
result = audit_external_capabilities(args.workspace)
|
|
343
|
+
result = audit_external_capabilities(args.workspace, view=args.view)
|
|
268
344
|
if args.format == "summary":
|
|
269
|
-
|
|
345
|
+
if args.view == "status":
|
|
346
|
+
print(build_status_panel(result))
|
|
347
|
+
else:
|
|
348
|
+
print(build_audit_summary(result))
|
|
270
349
|
else:
|
|
271
350
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
272
351
|
return 0 if result.get("success") else 1
|
|
@@ -6,6 +6,8 @@ from typing import Any
|
|
|
6
6
|
from codecgc_artifact_roots import discover_flow_directory
|
|
7
7
|
from codecgc_artifact_roots import execution_root
|
|
8
8
|
from codecgc_artifact_roots import normalize_artifact_class
|
|
9
|
+
from codecgc_path_contract import is_project_relative_path
|
|
10
|
+
from codecgc_path_contract import resolve_project_path
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
WORKSPACE = Path(__file__).resolve().parents[1]
|
|
@@ -33,6 +35,19 @@ def contains_old_repo_name(value: Any) -> bool:
|
|
|
33
35
|
return isinstance(value, str) and OLD_REPO_NAME in value
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
def contains_persisted_absolute_project_path(value: Any) -> bool:
|
|
39
|
+
if not isinstance(value, str) or not value.strip():
|
|
40
|
+
return False
|
|
41
|
+
if is_project_relative_path(value):
|
|
42
|
+
return False
|
|
43
|
+
resolved = resolve_project_path(value)
|
|
44
|
+
try:
|
|
45
|
+
resolved.relative_to(WORKSPACE.resolve())
|
|
46
|
+
return True
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
36
51
|
def expected_artifact_filename(artifact_type: str, artifact_slug: str) -> str:
|
|
37
52
|
base_slug = artifact_slug[11:] if len(artifact_slug) > 11 and artifact_slug[4] == "-" else artifact_slug
|
|
38
53
|
if artifact_type == "feature":
|
|
@@ -97,7 +112,7 @@ def validate_source_contract(source: dict[str, Any]) -> list[dict[str, str]]:
|
|
|
97
112
|
)
|
|
98
113
|
|
|
99
114
|
if artifact_file:
|
|
100
|
-
artifact_file_path =
|
|
115
|
+
artifact_file_path = resolve_project_path(artifact_file)
|
|
101
116
|
if not artifact_file_path.exists():
|
|
102
117
|
issues.append(
|
|
103
118
|
{
|
|
@@ -105,7 +120,7 @@ def validate_source_contract(source: dict[str, Any]) -> list[dict[str, str]]:
|
|
|
105
120
|
"detail": artifact_file,
|
|
106
121
|
}
|
|
107
122
|
)
|
|
108
|
-
elif directory not in artifact_file_path.parents:
|
|
123
|
+
elif directory.resolve() not in artifact_file_path.resolve().parents:
|
|
109
124
|
issues.append(
|
|
110
125
|
{
|
|
111
126
|
"problem": "source-artifact-file-directory-mismatch",
|
|
@@ -174,6 +189,31 @@ def inspect_audit(path: Path) -> list[dict[str, str]]:
|
|
|
174
189
|
}
|
|
175
190
|
)
|
|
176
191
|
|
|
192
|
+
if contains_persisted_absolute_project_path(data.get("routing_file")):
|
|
193
|
+
issues.append(
|
|
194
|
+
{
|
|
195
|
+
"path": str(path),
|
|
196
|
+
"problem": "absolute-project-routing-file",
|
|
197
|
+
"detail": str(data.get("routing_file", "")),
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
if contains_persisted_absolute_project_path(data.get("cd")):
|
|
201
|
+
issues.append(
|
|
202
|
+
{
|
|
203
|
+
"path": str(path),
|
|
204
|
+
"problem": "absolute-project-cd",
|
|
205
|
+
"detail": str(data.get("cd", "")),
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
if contains_persisted_absolute_project_path(source.get("artifact_file")):
|
|
209
|
+
issues.append(
|
|
210
|
+
{
|
|
211
|
+
"path": str(path),
|
|
212
|
+
"problem": "absolute-project-artifact-file",
|
|
213
|
+
"detail": str(source.get("artifact_file", "")),
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
177
217
|
for item in validate_source_contract(source):
|
|
178
218
|
issues.append(
|
|
179
219
|
{
|