ccgx-workflow 1.0.3 → 1.0.5

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +182 -2
  3. package/dist/index.mjs +3 -2
  4. package/dist/shared/{ccgx-workflow.BnfaZnVu.mjs → ccgx-workflow.CZSjTyQd.mjs} +79 -1
  5. package/package.json +1 -1
  6. package/templates/commands/agents/code-fixer.md +1 -1
  7. package/templates/commands/agents/codebase-mapper.md +1 -1
  8. package/templates/commands/agents/debug-session-manager.md +1 -1
  9. package/templates/commands/agents/debugger.md +1 -1
  10. package/templates/commands/agents/interface-auditor.md +34 -8
  11. package/templates/commands/agents/phase-runner.md +27 -27
  12. package/templates/commands/agents/plan-checker.md +4 -4
  13. package/templates/commands/analyze.md +10 -10
  14. package/templates/commands/autonomous.md +45 -46
  15. package/templates/commands/cancel.md +8 -8
  16. package/templates/commands/codex-exec.md +2 -2
  17. package/templates/commands/debate.md +5 -5
  18. package/templates/commands/debug.md +8 -8
  19. package/templates/commands/execute.md +6 -6
  20. package/templates/commands/init.md +1 -1
  21. package/templates/commands/optimize.md +10 -10
  22. package/templates/commands/plan.md +15 -15
  23. package/templates/commands/result.md +1 -1
  24. package/templates/commands/review.md +70 -31
  25. package/templates/commands/spec-impl.md +1 -1
  26. package/templates/commands/spec-plan.md +2 -2
  27. package/templates/commands/spec-research.md +1 -1
  28. package/templates/commands/spec-review.md +1 -1
  29. package/templates/commands/status.md +15 -15
  30. package/templates/commands/team-exec.md +1 -1
  31. package/templates/commands/team.md +6 -6
  32. package/templates/commands/test.md +9 -9
  33. package/templates/commands/verify-work.md +8 -8
  34. package/templates/commands/verify.md +3 -3
  35. package/templates/commands/workflow.md +2 -2
  36. package/templates/rules/ccg-skills.md +1 -1
  37. package/templates/scripts/ccgx-call-plugin.mjs +324 -0
  38. package/templates/scripts/repatch-gemini-plugin.mjs +10 -0
  39. package/templates/skills/tools/extract-learnings/SKILL.md +1 -3
  40. package/templates/skills/tools/forensics/SKILL.md +0 -2
  41. package/templates/skills/tools/health/SKILL.md +0 -2
  42. package/templates/skills/tools/map-codebase/SKILL.md +0 -2
  43. package/templates/skills/tools/verify-change/SKILL.md +2 -2
  44. package/templates/skills/tools/verify-module/SKILL.md +2 -2
  45. package/templates/skills/tools/verify-quality/SKILL.md +2 -2
  46. package/templates/skills/tools/verify-security/SKILL.md +2 -2
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: '后台任务观测:列表 / 单查 / 阻塞等待 / dashboard / tail 流式 / 卡点检测 / 单 phase cancel(v4.5 P7 升级)'
2
+ description: '后台任务观测:列表 / 单查 / 阻塞等待 / dashboard / tail 流式 / 卡点检测 / 单 phase cancel'
3
3
  argument-hint: "[<job-id>] [--wait --timeout-ms <ms>] [--tail <job-id>] [--cancel <phase-id>]"
4
4
  allowed-tools:
5
5
  - Read
@@ -7,9 +7,9 @@ allowed-tools:
7
7
  - Glob
8
8
  ---
9
9
 
10
- # Status — 后台任务观测(v4.5 dashboard + tail)
10
+ # Status — 后台任务观测(dashboard + tail)
11
11
 
12
- CCG v4.5 起 phase-runner 走 Bash subprocess(`claude -p --output-format stream-json ...`),stream 落盘到 `<workdir>/.context/jobs/<job-id>/progress.jsonl`。失去 sidechain inline UI 后,本命令是用户**唯一**的微观干预入口,必须复刻这四个长跑场景:
12
+ phase-runner 走 Bash subprocess(`claude -p --output-format stream-json ...`),stream 落盘到 `<workdir>/.context/jobs/<job-id>/progress.jsonl`。失去 sidechain inline UI 后,本命令是用户**唯一**的微观干预入口,必须复刻这四个长跑场景:
13
13
 
14
14
  | 场景 | 模式 |
15
15
  |------|------|
@@ -33,8 +33,8 @@ CCG v4.5 起 phase-runner 走 Bash subprocess(`claude -p --output-format strea
33
33
  /ccg:status # 模式 A:列表 / dashboard
34
34
  /ccg:status <job-id> # 模式 B:单查详情
35
35
  /ccg:status <job-id> --wait --timeout-ms <ms> # 模式 C:阻塞等待
36
- /ccg:status --tail <job-id> # 模式 D:流式 tail(v4.5 新增)
37
- /ccg:status --cancel <phase-id> # 模式 E:单 phase 协作 cancel(v4.5 新增)
36
+ /ccg:status --tail <job-id> # 模式 D:流式 tail
37
+ /ccg:status --cancel <phase-id> # 模式 E:单 phase 协作 cancel
38
38
  ```
39
39
 
40
40
  ## 模式 A:Dashboard(无参数)
@@ -77,7 +77,7 @@ Phase 7 (Status v2) [============> ] 60% (12m 04s) 🛠️ edit_file
77
77
  4. 超时 → "⏱ Timeout after <X>s — job still in <status>,retry with longer --timeout-ms 或 /ccg:cancel <id>"
78
78
  5. 超时退出码 0(不视为失败)
79
79
 
80
- ## 模式 D:Tail 流式(v4.5 新增)
80
+ ## 模式 D:Tail 流式
81
81
 
82
82
  `/ccg:status --tail <job-id>` 持续读 `progress.jsonl`,单行覆写:
83
83
 
@@ -130,7 +130,7 @@ done
130
130
 
131
131
  每次 tail 前调 `detectStuck` 注入 banner(loop / slow-tool / stalled 三类警告)。
132
132
 
133
- ## 模式 E:单 phase 协作 cancel(v4.5 新增)
133
+ ## 模式 E:单 phase 协作 cancel
134
134
 
135
135
  `/ccg:status --cancel <phase-id>` 流程:
136
136
 
@@ -138,7 +138,7 @@ done
138
138
  2. 写 `.context/jobs/<job-id>/cancel.flag` —— 内容 `phase=<phase-id>\nrequested-at=<iso>`
139
139
  3. 翻 `state.cancel_requested=true`
140
140
  4. **5 秒 grace 等待** —— 给 phase-runner 子进程读 cancel.flag 优雅退出
141
- 5. 5s 后子进程仍 running → 调用 `killProcessTree({ pid: state.cli_pid, pgid: state.process_group_id, graceMs: 5000 })`(来自 `src/utils/process-tree.ts`,v4.5 P1b 已落地,P1f wired)
141
+ 5. 5s 后子进程仍 running → 调用 `killProcessTree({ pid: state.cli_pid, pgid: state.process_group_id, graceMs: 5000 })`(来自 `src/utils/process-tree.ts`)
142
142
  6. 输出最终结果:`canceled gracefully` / `force-killed pid=N` / `not found`
143
143
 
144
144
  **实施样板**(主线 LLM 用 Bash + node -e 调用 helper):
@@ -187,20 +187,20 @@ const { renderJsonl, progressBar, formatElapsed } = require('~/.claude/.ccg/dist
187
187
  // 卡点检测
188
188
  const { detectStuck, hasStuckWarning } = require('~/.claude/.ccg/dist/index.mjs')
189
189
 
190
- // 已有 v4.0 helper
190
+ // job helper
191
191
  const { listJobs, getJob, requestCancel } = require('~/.claude/.ccg/dist/index.mjs')
192
192
  ```
193
193
 
194
194
  源码真相:
195
195
  - `src/utils/jobs.ts` — `listJobs / getJob / requestCancel`
196
- - `src/utils/stream-renderer.ts`(v4.5 P7) — `renderJsonl / renderEvent / progressBar / formatElapsed`
197
- - `src/utils/stuck-detector.ts`(v4.5 P7) — `detectStuck / hasStuckWarning`
198
- - `src/utils/process-tree.ts`(v4.5 P1b 落地 + P1f wired) — `killProcessTree / sampleProcessRssMb / writeDegradedFlag / readDegradedFlag / reconcileStaleJobs`
196
+ - `src/utils/stream-renderer.ts` — `renderJsonl / renderEvent / progressBar / formatElapsed`
197
+ - `src/utils/stuck-detector.ts` — `detectStuck / hasStuckWarning`
198
+ - `src/utils/process-tree.ts` — `killProcessTree / sampleProcessRssMb / writeDegradedFlag / readDegradedFlag / reconcileStaleJobs`
199
199
 
200
- v4.x 暂未把 `dist/` 暴露给命令模板,主线 LLM 走 Bash + Read 等价行为:
200
+ `dist/` 未暴露给命令模板时,主线 LLM 走 Bash + Read 等价行为:
201
201
 
202
202
  - dashboard:调 `node -e` 读各 state.json + 简单进度推断 + `=`/`>`/空格手动拼字符串
203
203
  - tail:调 `node -e` 解析 ndjson + 走 `renderEvent` 等价 switch(TS helper 是真相源)
204
- - cancel:直接 `echo > cancel.flag` + sleep 5 + 调 process-tree(Phase 2 ready 后)
204
+ - cancel:直接 `echo > cancel.flag` + sleep 5 + 调 process-tree
205
205
 
206
- 升级路径见 `.ccg-migration/v4.4-to-v4.5.md` § "/ccg:status v2 升级"。
206
+ 历史升级记录见 `.ccg-migration/INTERNAL-DEV-LOG.md`。
@@ -167,7 +167,7 @@ subagent_freshness: required
167
167
 
168
168
  用 `AskUserQuestion` 让用户选择。**重试**最多再走一轮 wave 调度,第二次仍失败则强制选 2/3。
169
169
 
170
- ### Step 5.5: Frontmatter-only Summary 读取(v4.0 Phase 2 状态机)
170
+ ### Step 5.5: Frontmatter-only Summary 读取(状态机契约)
171
171
 
172
172
  **核心契约**:Lead 不接 Builder 的全部 stdout。每个 Builder 完成任务后,**必须由 Lead 读取该任务对应的 `.context/<phase>/SUMMARY.md` 的 YAML frontmatter**——不读 body,不读 builder transcript。
173
173
 
@@ -3,16 +3,16 @@ description: 'Agent Teams 8 阶段企业级工作流 - 7 角色全流程统一
3
3
  ---
4
4
  <!-- CCG:TEAM:UNIFIED:START -->
5
5
 
6
- ## 子命令路由(v4.1-p18)
6
+ ## 子命令路由
7
7
 
8
8
  `/ccg:team` 同时承载子命令调度。根据 `$ARGUMENTS` 第一个 token 路由到具体阶段:
9
9
 
10
10
  | 子命令 | 含义 | 替代旧命令 |
11
11
  |--------|------|----------|
12
12
  | `/ccg:team` (无参 / 任意非保留字) | 8 阶段全流程 | (主流程) |
13
- | `/ccg:team research <需求>` | 仅跑需求研究阶段 | `/ccg:team-research`(v4.1 删除) |
14
- | `/ccg:team plan <约束文件>` | 仅跑规划阶段 | `/ccg:team-plan`(v4.1 删除) |
15
- | `/ccg:team review [git-range]` | 仅跑双模型审查阶段 | `/ccg:team-review`(v4.1 删除) |
13
+ | `/ccg:team research <需求>` | 仅跑需求研究阶段 | `/ccg:team-research`(已删除) |
14
+ | `/ccg:team plan <约束文件>` | 仅跑规划阶段 | `/ccg:team-plan`(已删除) |
15
+ | `/ccg:team review [git-range]` | 仅跑双模型审查阶段 | `/ccg:team-review`(已删除) |
16
16
  | `/ccg:team exec <plan-file>` | 仅跑并行实施阶段 | 等价 `/ccg:team-exec` |
17
17
 
18
18
  > **路由规则**:将 `$ARGUMENTS` 拆分为 `[subcmd, ...rest]`。若 `subcmd ∈ {research, plan, review, exec}`,跳到对应 Phase(Research → Phase 1 / plan → Phase 3 / review → Phase 6 / exec → Phase 4);否则走完整 Phase 0-8。
@@ -178,7 +178,7 @@ Phase 8: INTEGRATION → Lead 全量验证 + 报告 + 清理
178
178
  })
179
179
  ```
180
180
 
181
- **事件驱动等待 (v4.5.2)**:spawn 两个 Bash bg 后说明 task-id 然后 **turn end**。引擎在每个 task 完成时自动发 `<task-notification>`,主线在通知触发的新 turn 处理结果。**不调 TaskOutput**。两个 task 都收到通知后才进下一步。
181
+ **事件驱动等待**:spawn 两个 Bash bg 后说明 task-id 然后 **turn end**。引擎在每个 task 完成时自动发 `<task-notification>`,主线在通知触发的新 turn 处理结果。**不调 TaskOutput**。两个 task 都收到通知后才进下一步。
182
182
 
183
183
  ⛔ **禁止**:调 `TaskOutput({block: true, timeout: 600000})` (旧 freeze poll 模式) / Kill task。
184
184
  ⚠️ **失败处理**:notification status=failed / exit ≠ 0 / parse 失败 → v1.7.87 标准 2-retry / 5s / 3-attempts;3 次全失败才降级单模型。
@@ -330,7 +330,7 @@ Phase 8: INTEGRATION → Lead 全量验证 + 报告 + 清理
330
330
  })
331
331
  ```
332
332
 
333
- **事件驱动等待 (v4.5.2)**:spawn 两个 Bash bg 后说明 task-id 然后 **turn end**。引擎在每个 task 完成时自动发 `<task-notification>`,主线在通知触发的新 turn 处理。**不调 TaskOutput**。两个 task 都收到通知才进 step 3。
333
+ **事件驱动等待**:spawn 两个 Bash bg 后说明 task-id 然后 **turn end**。引擎在每个 task 完成时自动发 `<task-notification>`,主线在通知触发的新 turn 处理。**不调 TaskOutput**。两个 task 都收到通知才进 step 3。
334
334
 
335
335
  ⛔ **禁止**:调 `TaskOutput({block: true, timeout: 600000})` (旧 freeze poll 模式) / Kill task。
336
336
  ⚠️ **失败处理**:notification status=failed / exit ≠ 0 / parse 失败 → v1.7.87 标准 2-retry / 5s / 3-attempts;3 次全失败才降级单模型。
@@ -13,7 +13,7 @@ argument-hint: "<测试目标> [--role=architect|critic|implementer|tester|write
13
13
  /test <测试目标> [--role=<name>]
14
14
  ```
15
15
 
16
- ## Role-based routing(v4.1 specialist matrix)
16
+ ## Role-based routing(specialist matrix)
17
17
 
18
18
  可选 `--role=<name>` 叠加 role 维度路由(默认 `tester`,可显式覆盖):
19
19
 
@@ -23,7 +23,7 @@ argument-hint: "<测试目标> [--role=architect|critic|implementer|tester|write
23
23
  | **frontend** | gemini/architect.md | gemini/reviewer.md (adversarial) | gemini/architect.md | gemini/tester.md | gemini/analyzer.md |
24
24
  | **fullstack** | codex+gemini/architect.md | both reviewer.md (adversarial) | runner 决(per-file) | runner 决(per-file) | claude |
25
25
 
26
- **未传 --role 时按 v4.0 智能路由(后端 → {{BACKEND_PRIMARY}}/tester.md,前端 → {{FRONTEND_PRIMARY}}/tester.md,全栈并行),完全兼容**。`--role=critic` 触发"测试用例反例挖掘"——专门构造打破现有测试假设的边界条件。详见 `src/utils/specialist-router.ts`。
26
+ **未传 --role 时按智能路由(后端 → {{BACKEND_PRIMARY}}/tester.md,前端 → {{FRONTEND_PRIMARY}}/tester.md,全栈并行),完全兼容**。`--role=critic` 触发"测试用例反例挖掘"——专门构造打破现有测试假设的边界条件。详见 `src/utils/specialist-router.ts`。
27
27
 
28
28
  ## 上下文
29
29
 
@@ -40,12 +40,12 @@ argument-hint: "<测试目标> [--role=architect|critic|implementer|tester|write
40
40
 
41
41
  ---
42
42
 
43
- ## 调用通道路由(v4.1 Phase 20,CCG codeagent 退役)
43
+ ## 调用通道路由(CCG codeagent 退役)
44
44
 
45
- CCG v4.1 把双模型并行通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn:
45
+ CCG 把双模型并行通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn:
46
46
 
47
47
  1. **优先 plugin spawn**(默认):装了 `codex@openai-codex` + `gemini@google-gemini` plugin → 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线接 ≤200 token 摘要。
48
- 2. **降级 codeagent-wrapper**(v4.0 BC fallback):plugin 未装 → fallback 到 Bash 调用,行为与 v4.0 完全一致。
48
+ 2. **降级 codeagent-wrapper**(BC fallback):plugin 未装 → fallback 到 Bash 调用,行为与 plugin 路径等价。
49
49
 
50
50
  **判定**:preflight `Bash` 跑 `ls ~/.claude/plugins/` 看有无 `codex@*` / `gemini@*` 子目录。helper 见 `src/utils/plugin-detection.ts`。
51
51
 
@@ -60,7 +60,7 @@ CCG v4.1 把双模型并行通道从 `Bash(codeagent-wrapper)` **默认切换**
60
60
  - 如果用户通过 `/add-dir` 添加了多个工作区,先用 Glob/Grep 确定任务相关的工作区
61
61
  - 如果无法确定,用 `AskUserQuestion` 询问用户选择目标工作区
62
62
 
63
- **调用语法**(v4.1 Phase 20 双通道):
63
+ **调用语法**(双通道):
64
64
 
65
65
  **通道 A — plugin spawn(默认)**:
66
66
 
@@ -109,7 +109,7 @@ EOF",
109
109
  })
110
110
  ```
111
111
 
112
- > ⚠️ 通道 B `codeagent-wrapper` 在 v4.1 已标 **deprecated**,将在 v5.0 移除。
112
+ > ⚠️ 通道 B `codeagent-wrapper` 已标 **deprecated**。
113
113
 
114
114
  **角色提示词**:
115
115
 
@@ -126,7 +126,7 @@ EOF",
126
126
  | 前端 | {{FRONTEND_PRIMARY}} |
127
127
  | 全栈 | 并行执行两者 |
128
128
 
129
- **并行调用 + 事件驱动等待(v4.5.2 起)**:
129
+ **并行调用 + 事件驱动等待**:
130
130
 
131
131
  1. 同 message 内 spawn 多个 `Bash(run_in_background: true)` 并行任务
132
132
  2. spawn 完后主线说明已启动 task-id,**直接 turn end**,**不调 TaskOutput**
@@ -135,7 +135,7 @@ EOF",
135
135
  5. **必须等所有相关 task 都收到通知**才进入下一阶段(按 task-id 计数已收齐)
136
136
 
137
137
  ⛔ **禁止**:
138
- - 调 `TaskOutput({block: true, timeout: 600000})` —— v4.5.1 之前的旧 freeze poll 模式,已废弃
138
+ - 调 `TaskOutput({block: true, timeout: 600000})` —— freeze poll 模式,已废弃
139
139
  - 收到部分通知就跳过等其他模型
140
140
  - 主动 Kill task
141
141
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ccg:verify-work
3
- description: 会话式 UAT 工作流 - UAT.md 状态文件 + cold-start smoke 自动注入 + 自动 diagnose-plan-fix 收敛环(v4.0 P9)
3
+ description: 会话式 UAT 工作流 - UAT.md 状态文件 + cold-start smoke 自动注入 + 自动 diagnose-plan-fix 收敛环
4
4
  argument-hint: "[task-id]"
5
5
  allowed-tools:
6
6
  - Read
@@ -13,23 +13,23 @@ allowed-tools:
13
13
  - Agent
14
14
  ---
15
15
 
16
- # Verify Work — 会话式 UAT 工作流(v4.0 Phase 9)
16
+ # Verify Work — 会话式 UAT 工作流
17
17
 
18
- v3.0 verify-work 是纯**编排器**——按变更性质开 verify-{module,security,quality,change} 子门,跑完聚合报告。但它**没法做真正的 UAT**:
18
+ 早期的 verify-work 是纯**编排器**——按变更性质开 verify-{module,security,quality,change} 子门,跑完聚合报告。但它**没法做真正的 UAT**:
19
19
 
20
20
  1. 用户得自己拿着报告人肉对照"这事儿到底验没验过";
21
21
  2. `/clear` 后所有上下文丢失,UAT 进度归零;
22
22
  3. 只看代码不跑冷启动——race condition / silent seed failure / 缺环境变量在生产才暴露;
23
23
  4. 用户报 issue 后没有自动收敛环——靠用户手动来回贴报告。
24
24
 
25
- v4.0 verify-work 改造成**有状态的会话工作流**:
25
+ 当前 verify-work 是**有状态的会话工作流**:
26
26
 
27
27
  - **UAT.md frontmatter 状态文件**:跨 `/clear` 持久化,下次进入命令自动 resume;
28
28
  - **逐项核对**(show expected → ask if matches):每条期望行为都明示问,不让模糊滑过;
29
29
  - **Cold-start smoke 自动注入**:扫 git diff 命中关键路径即注入"杀进程 → 清临时态 → 冷启动 → 主查询返回数据"测试;
30
30
  - **自动 diagnose → planner --gaps → plan-checker 收敛环**(max 3 轮):用户报 issue 立即触发,无需手动调度。
31
31
 
32
- **与 v3.0 的关系**:v3.0 的多门聚合不删,迁移为 Step 2(verify-* 子门作为静态扫描)。v4.0 的会话循环裹在外层(Step 0/1/3/4/5)。
32
+ **多门聚合保留**:早期的多门聚合不删,迁移为 Step 2(verify-* 子门作为静态扫描)。会话循环裹在外层(Step 0/1/3/4/5)。
33
33
 
34
34
  ---
35
35
 
@@ -132,9 +132,9 @@ fi
132
132
 
133
133
  ### Step 2 — 静态门 + cold-start smoke 注入
134
134
 
135
- #### 2a. 静态扫描(v3.0 多门保留)
135
+ #### 2a. 静态扫描(多门保留)
136
136
 
137
- 按 git diff 性质开门(v3.0 决策矩阵):
137
+ 按 git diff 性质开门(决策矩阵):
138
138
 
139
139
  | 变更性质 | 触发判据 | 门组 |
140
140
  |---------|---------|------|
@@ -317,7 +317,7 @@ else:
317
317
  | verify-change | Step 2a 静态门,常规改动必跑 |
318
318
  | verifier agent | Step 2a 末尾兜底,做需求矩阵反向溯源(v4 Phase 8 加 Level 4 数据流) |
319
319
 
320
- ## 与 v4.0 helper 的契约
320
+ ## 与 helper 的契约
321
321
 
322
322
  | Helper | 用途 |
323
323
  |--------|------|
@@ -9,9 +9,9 @@ allowed-tools:
9
9
  - Agent
10
10
  ---
11
11
 
12
- # /ccg:verify - 统一校验关卡(v4.0+)
12
+ # /ccg:verify - 统一校验关卡
13
13
 
14
- 替代 v3.x 时代的 4 个独立命令 `/ccg:verify-{change,quality,security,module}`,统一入口 + 子门路由,降低命令面板认知负担。
14
+ 替代早期的 4 个独立命令 `/ccg:verify-{change,quality,security,module}`,统一入口 + 子门路由,降低命令面板认知负担。
15
15
 
16
16
  ## 使用方法
17
17
 
@@ -48,7 +48,7 @@ $ARGUMENTS
48
48
 
49
49
  ## 兼容性
50
50
 
51
- 旧的 `/ccg:verify-change` / `/ccg:verify-quality` / `/ccg:verify-security` / `/ccg:verify-module` 仍可工作(由 Skill Registry 自动生成),但 SKILL.md 已标记 `deprecated_in: v4.0`、`replaced_by: /ccg:verify --gate=<name>`。建议新工作流使用本统一命令。
51
+ 旧的 `/ccg:verify-change` / `/ccg:verify-quality` / `/ccg:verify-security` / `/ccg:verify-module` 仍可工作(由 Skill Registry 自动生成),但 SKILL.md 已标记 `deprecated_in: 1.0.0`、`replaced_by: /ccg:verify --gate=<name>`。建议新工作流使用本统一命令。
52
52
 
53
53
  ## 执行流程
54
54
 
@@ -83,7 +83,7 @@ EOF",
83
83
 
84
84
  **会话复用**:每次调用返回 `SESSION_ID: xxx`,后续阶段用 `resume xxx` 复用上下文(注意:是 `resume`,不是 `--resume`)。
85
85
 
86
- **并行调用 + 事件驱动等待(v4.5.2 起)**:
86
+ **并行调用 + 事件驱动等待**:
87
87
 
88
88
  1. 同 message 内 spawn 多个 `Bash(run_in_background: true)` 并行任务
89
89
  2. spawn 完后主线说明已启动 task-id,**直接 turn end**,**不调 TaskOutput**
@@ -92,7 +92,7 @@ EOF",
92
92
  5. **必须等所有相关 task 都收到通知**才进入下一阶段(按 task-id 计数已收齐)
93
93
 
94
94
  ⛔ **禁止**:
95
- - 调 `TaskOutput({block: true, timeout: 600000})` —— v4.5.1 之前的旧 freeze poll 模式,已废弃
95
+ - 调 `TaskOutput({block: true, timeout: 600000})` —— freeze poll 模式,已废弃
96
96
  - 收到部分通知就跳过等其他模型
97
97
  - 主动 Kill task
98
98
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  When working in a project, automatically invoke the corresponding quality gate skills based on the scenario below. These skills are installed at `~/.claude/skills/ccg/` and can be called directly.
4
4
 
5
- **v4.0+ NOTE**: The unified entry point `/ccg:verify --gate=<name>` is preferred over the legacy `verify-*` skill names. Both still work in v4.x for BC. Examples below show the new form first, with the legacy alias in parentheses.
5
+ **NOTE**: The unified entry point `/ccg:verify --gate=<name>` is preferred over the legacy `verify-*` skill names. Both still work for BC. Examples below show the new form first, with the legacy alias in parentheses.
6
6
 
7
7
  ## Trigger Rules
8
8
 
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // ccgx-call-plugin.mjs 1.0.5
4
+ // -----------------------------------------------------------------------------
5
+ // Pure-Node helper for invoking codex/gemini plugin companions WITHOUT any
6
+ // shell-escape risk. Replaces the 1.0.4 heredoc-via-Bash pattern.
7
+ //
8
+ // Why this exists (1.0.5 design after 1.0.4 dogfood):
9
+ // LLM-constructed Bash commands proved unreliable. Two failure modes hit
10
+ // in 1.0.4:
11
+ // 1. LLM cargo-culted anti-example code from review.md docs
12
+ // 2. LLM in actual review session still wrote `ls $(...) | head -1`
13
+ // glob-hack patterns despite placeholder system
14
+ //
15
+ // Root cause: any design that asks the LLM to construct or substitute parts
16
+ // of a shell command has X% failure rate. X varies but is never zero.
17
+ //
18
+ // Fix: collapse the LLM surface to "choose vendor + pass prompt-file path".
19
+ // All path resolution, flag construction, shell-quote-avoidance are done
20
+ // internally by Node spawn with array args (no shell).
21
+ //
22
+ // Usage (LLM workflow):
23
+ // 1. Write prompt body to a temp file (via Write tool):
24
+ // /tmp/ccg-codex-1234.txt
25
+ // 2. Run helper via Bash:
26
+ // node ~/.claude/.ccg/scripts/ccgx-call-plugin.mjs codex \
27
+ // --prompt-file /tmp/ccg-codex-1234.txt
28
+ // 3. Parse the JSON output emitted to stdout
29
+ //
30
+ // Output schema (always JSON, even on error):
31
+ // {
32
+ // "status": "ok" | "error",
33
+ // "vendor": "codex" | "gemini",
34
+ // "version": "<plugin version>",
35
+ // "durationMs": <number>,
36
+ // "exitCode": <number | null>,
37
+ // "stdout": "<companion stdout>",
38
+ // "stderr": "<companion stderr>",
39
+ // "error": "<error message if status=error, else absent>"
40
+ // }
41
+ //
42
+ // CLI args:
43
+ // <vendor> Required. 'codex' or 'gemini'.
44
+ // --prompt-file <path> Required. Path to file containing prompt body.
45
+ // --json Pass --json to companion (default: true).
46
+ // --no-json Disable --json (text output).
47
+ // --timeout-ms <N> Kill companion after N ms (default: 600000).
48
+ // --max-budget-usd <N> Forwarded to companion (default: 50).
49
+ //
50
+ // Cross-cutting:
51
+ // - Pure stdlib (fs, child_process, path, os). No deps.
52
+ // - Always emits valid JSON to stdout (so LLM can always JSON.parse).
53
+ // - Plugin path resolution via SSoT (~/.claude/plugins/installed_plugins.json).
54
+ // - spawn uses array args + windowsHide:true → zero shell escape surface.
55
+ // =============================================================================
56
+
57
+ import { spawn } from 'node:child_process'
58
+ import { existsSync, readFileSync } from 'node:fs'
59
+ import { homedir, platform } from 'node:os'
60
+ import { join } from 'node:path'
61
+ import { fileURLToPath } from 'node:url'
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Vendor → marketplace key (matches plugin-bash-codegen.ts SSoT lookup)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const VENDOR_KEYS = {
68
+ codex: 'codex@openai-codex',
69
+ gemini: 'gemini@google-gemini',
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Argument parsing — minimal, KISS, no external CLI lib.
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function parseArgs(argv) {
77
+ const opts = {
78
+ vendor: null,
79
+ promptFile: null,
80
+ json: true,
81
+ timeoutMs: 600000,
82
+ maxBudgetUsd: 50,
83
+ }
84
+
85
+ const positional = []
86
+ for (let i = 2; i < argv.length; i++) {
87
+ const arg = argv[i]
88
+ const next = () => {
89
+ const v = argv[++i]
90
+ if (v === undefined) throw new Error(`flag ${arg} requires a value`)
91
+ return v
92
+ }
93
+ switch (arg) {
94
+ case '--prompt-file': opts.promptFile = next(); break
95
+ case '--json': opts.json = true; break
96
+ case '--no-json': opts.json = false; break
97
+ case '--timeout-ms': opts.timeoutMs = Number.parseInt(next(), 10); break
98
+ case '--max-budget-usd': opts.maxBudgetUsd = Number.parseFloat(next()); break
99
+ case '--help':
100
+ case '-h':
101
+ printHelp()
102
+ process.exit(0)
103
+ default:
104
+ if (arg.startsWith('--')) throw new Error(`unknown flag: ${arg}`)
105
+ positional.push(arg)
106
+ }
107
+ }
108
+
109
+ if (positional.length === 0) throw new Error('vendor (codex|gemini) is required')
110
+ if (positional.length > 1) throw new Error(`too many positional args: ${positional.join(' ')}`)
111
+ opts.vendor = positional[0]
112
+
113
+ if (!VENDOR_KEYS[opts.vendor]) {
114
+ throw new Error(`unknown vendor: ${opts.vendor} (must be 'codex' or 'gemini')`)
115
+ }
116
+ if (!opts.promptFile) {
117
+ throw new Error('--prompt-file is required (path to file containing prompt body)')
118
+ }
119
+ return opts
120
+ }
121
+
122
+ function printHelp() {
123
+ process.stderr.write(`Usage: ccgx-call-plugin.mjs <vendor> --prompt-file <path> [flags]
124
+
125
+ Required:
126
+ <vendor> 'codex' or 'gemini'
127
+ --prompt-file <path> File containing prompt body (any content, no escape needed)
128
+
129
+ Optional:
130
+ --json Pass --json to companion (default: true)
131
+ --no-json Disable --json (text output)
132
+ --timeout-ms <N> Kill companion after N ms (default: 600000)
133
+ --max-budget-usd <N> Per-call cost cap (default: 50)
134
+
135
+ Outputs JSON to stdout: {status, vendor, version, durationMs, exitCode, stdout, stderr, error?}
136
+ `)
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Plugin discovery (mirror of plugin-bash-codegen.ts:discoverCompanion)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function discoverCompanion(vendor, homeDir = homedir()) {
144
+ const ssotPath = join(homeDir, '.claude', 'plugins', 'installed_plugins.json')
145
+ if (!existsSync(ssotPath)) {
146
+ return { error: `installed_plugins.json not found at ${ssotPath}` }
147
+ }
148
+
149
+ let raw
150
+ try {
151
+ raw = JSON.parse(readFileSync(ssotPath, 'utf-8'))
152
+ }
153
+ catch (e) {
154
+ return { error: `installed_plugins.json parse failed: ${e.message}` }
155
+ }
156
+
157
+ const key = VENDOR_KEYS[vendor]
158
+ const instances = raw?.plugins?.[key]
159
+ if (!Array.isArray(instances) || instances.length === 0) {
160
+ return { error: `plugin ${key} not installed (no entry in installed_plugins.json)` }
161
+ }
162
+
163
+ const inst = instances[0]
164
+ const installPath = inst?.installPath
165
+ if (typeof installPath !== 'string' || !installPath) {
166
+ return { error: `plugin ${key} has no installPath in installed_plugins.json` }
167
+ }
168
+
169
+ const companionPath = join(installPath, 'scripts', `${vendor}-companion.mjs`)
170
+ if (!existsSync(companionPath)) {
171
+ return { error: `companion script missing: ${companionPath}` }
172
+ }
173
+
174
+ return {
175
+ companionPath,
176
+ version: typeof inst?.version === 'string' ? inst.version : 'unknown',
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Output emission — always JSON, never throws to stdout
182
+ // ---------------------------------------------------------------------------
183
+
184
+ function emitJson(result) {
185
+ process.stdout.write(`${JSON.stringify(result)}\n`)
186
+ }
187
+
188
+ function emitError(vendor, version, error, extra = {}) {
189
+ emitJson({
190
+ status: 'error',
191
+ vendor,
192
+ version,
193
+ durationMs: 0,
194
+ exitCode: null,
195
+ stdout: '',
196
+ stderr: '',
197
+ error,
198
+ ...extra,
199
+ })
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Main: spawn companion with array args (no shell)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ async function main(argv) {
207
+ let opts
208
+ try {
209
+ opts = parseArgs(argv)
210
+ }
211
+ catch (err) {
212
+ emitError(null, null, `arg parse: ${err.message}`)
213
+ process.exit(64) // EX_USAGE
214
+ }
215
+
216
+ // Read prompt body from file (no shell, no escape concerns)
217
+ let promptBody
218
+ try {
219
+ promptBody = readFileSync(opts.promptFile, 'utf-8')
220
+ }
221
+ catch (err) {
222
+ emitError(opts.vendor, null, `prompt file read failed: ${err.message}`)
223
+ process.exit(66) // EX_NOINPUT
224
+ }
225
+
226
+ // Discover companion via SSoT (no glob, no head -1)
227
+ const disc = discoverCompanion(opts.vendor)
228
+ if (disc.error) {
229
+ emitError(opts.vendor, null, disc.error)
230
+ process.exit(69) // EX_UNAVAILABLE
231
+ }
232
+
233
+ // Build companion argv as ARRAY — no shell layer ever
234
+ const companionArgs = [
235
+ disc.companionPath,
236
+ 'task',
237
+ '-p',
238
+ promptBody,
239
+ ]
240
+ if (opts.json) companionArgs.push('--json')
241
+
242
+ const startedAt = Date.now()
243
+
244
+ // spawn 'node' with array args. Node will look up its own executable;
245
+ // companionPath is passed as a literal arg, no shell interpretation.
246
+ const child = spawn(process.execPath, companionArgs, {
247
+ stdio: ['ignore', 'pipe', 'pipe'],
248
+ windowsHide: true,
249
+ env: {
250
+ ...process.env,
251
+ // Forward budget cap if companion respects it (codex does)
252
+ CODEX_MAX_BUDGET_USD: String(opts.maxBudgetUsd),
253
+ GEMINI_MAX_BUDGET_USD: String(opts.maxBudgetUsd),
254
+ },
255
+ })
256
+
257
+ let stdoutBuf = ''
258
+ let stderrBuf = ''
259
+ child.stdout.on('data', (chunk) => { stdoutBuf += chunk.toString('utf-8') })
260
+ child.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8') })
261
+
262
+ // Timeout: kill child if exceeds opts.timeoutMs
263
+ let timedOut = false
264
+ const timer = setTimeout(() => {
265
+ timedOut = true
266
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
267
+ setTimeout(() => {
268
+ try { child.kill('SIGKILL') } catch { /* ignore */ }
269
+ }, 5000).unref?.()
270
+ }, opts.timeoutMs)
271
+
272
+ const exit = await new Promise((resolve) => {
273
+ child.once('exit', (code, signal) => resolve({ code, signal }))
274
+ child.once('error', (err) => resolve({ code: 70, signal: null, errorMsg: err.message }))
275
+ })
276
+ clearTimeout(timer)
277
+
278
+ const durationMs = Date.now() - startedAt
279
+ const status = (!timedOut && exit.code === 0) ? 'ok' : 'error'
280
+ const result = {
281
+ status,
282
+ vendor: opts.vendor,
283
+ version: disc.version,
284
+ durationMs,
285
+ exitCode: exit.code,
286
+ stdout: stdoutBuf,
287
+ stderr: stderrBuf,
288
+ }
289
+ if (timedOut) result.error = `timeout after ${opts.timeoutMs}ms`
290
+ else if (exit.errorMsg) result.error = `spawn error: ${exit.errorMsg}`
291
+ else if (exit.code !== 0) result.error = `companion exited with code ${exit.code}`
292
+
293
+ emitJson(result)
294
+ process.exit(status === 'ok' ? 0 : 1)
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Entry point — only run main() when invoked as a script (not on import).
299
+ // ---------------------------------------------------------------------------
300
+
301
+ function isMainModule() {
302
+ if (!process.argv[1]) return false
303
+ try {
304
+ const here = fileURLToPath(import.meta.url)
305
+ return here === process.argv[1]
306
+ }
307
+ catch {
308
+ return false
309
+ }
310
+ }
311
+
312
+ if (isMainModule()) {
313
+ main(process.argv).catch((err) => {
314
+ emitError(null, null, `fatal: ${err.stack || err.message}`)
315
+ process.exit(70)
316
+ })
317
+ }
318
+
319
+ // Test surface for unit tests
320
+ export const ccgxCallPluginExports = {
321
+ parseArgs,
322
+ discoverCompanion,
323
+ VENDOR_KEYS,
324
+ }
@@ -132,6 +132,16 @@ const PATCHES = [
132
132
  match: /(spawnSync\(command,\s*\[name\],\s*\{\s*encoding:\s*"utf8",\s*stdio:\s*"pipe")(\s*\})/,
133
133
  replace: '$1, windowsHide: true$2',
134
134
  },
135
+ {
136
+ id: "P-9",
137
+ file: "lib/acp-client.mjs",
138
+ description: "JSON-RPC error swallowing (reject with bare {code,message} → caller sees '[object Object]')",
139
+ // Guard: any patched marker present
140
+ guard: /CCG P-9 patch/,
141
+ // Match: the bare reject(message.error) inside the response branch
142
+ match: /(if \(message\.error\) \{\s*\r?\n\s*)pending\.reject\(message\.error\);(\s*\r?\n\s*\} else \{)/,
143
+ replace: `$1// CCG P-9 patch: wrap JSON-RPC error object in Error instance.\n // Without this, callers doing \`e instanceof Error ? e.message : String(e)\`\n // get "[object Object]" — losing real error info (auth-expired, broker-dead,\n // parse-error, etc). See .ccg-migration/PLUGIN-PATCHES.md P-9.\n const _err = message.error;\n const _wrapped = Object.assign(\n new Error(typeof _err === "object" && _err !== null && _err.message\n ? String(_err.message)\n : String(_err)),\n {\n jsonrpcCode: typeof _err === "object" && _err !== null ? _err.code : undefined,\n jsonrpcData: typeof _err === "object" && _err !== null ? _err.data : undefined,\n },\n );\n pending.reject(_wrapped);$2`,
144
+ },
135
145
  ];
136
146
 
137
147
  let applied = 0;