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.
- package/README.md +1 -1
- package/dist/cli.mjs +182 -2
- package/dist/index.mjs +3 -2
- package/dist/shared/{ccgx-workflow.BnfaZnVu.mjs → ccgx-workflow.CZSjTyQd.mjs} +79 -1
- package/package.json +1 -1
- package/templates/commands/agents/code-fixer.md +1 -1
- package/templates/commands/agents/codebase-mapper.md +1 -1
- package/templates/commands/agents/debug-session-manager.md +1 -1
- package/templates/commands/agents/debugger.md +1 -1
- package/templates/commands/agents/interface-auditor.md +34 -8
- package/templates/commands/agents/phase-runner.md +27 -27
- package/templates/commands/agents/plan-checker.md +4 -4
- package/templates/commands/analyze.md +10 -10
- package/templates/commands/autonomous.md +45 -46
- package/templates/commands/cancel.md +8 -8
- package/templates/commands/codex-exec.md +2 -2
- package/templates/commands/debate.md +5 -5
- package/templates/commands/debug.md +8 -8
- package/templates/commands/execute.md +6 -6
- package/templates/commands/init.md +1 -1
- package/templates/commands/optimize.md +10 -10
- package/templates/commands/plan.md +15 -15
- package/templates/commands/result.md +1 -1
- package/templates/commands/review.md +70 -31
- package/templates/commands/spec-impl.md +1 -1
- package/templates/commands/spec-plan.md +2 -2
- package/templates/commands/spec-research.md +1 -1
- package/templates/commands/spec-review.md +1 -1
- package/templates/commands/status.md +15 -15
- package/templates/commands/team-exec.md +1 -1
- package/templates/commands/team.md +6 -6
- package/templates/commands/test.md +9 -9
- package/templates/commands/verify-work.md +8 -8
- package/templates/commands/verify.md +3 -3
- package/templates/commands/workflow.md +2 -2
- package/templates/rules/ccg-skills.md +1 -1
- package/templates/scripts/ccgx-call-plugin.mjs +324 -0
- package/templates/scripts/repatch-gemini-plugin.mjs +10 -0
- package/templates/skills/tools/extract-learnings/SKILL.md +1 -3
- package/templates/skills/tools/forensics/SKILL.md +0 -2
- package/templates/skills/tools/health/SKILL.md +0 -2
- package/templates/skills/tools/map-codebase/SKILL.md +0 -2
- package/templates/skills/tools/verify-change/SKILL.md +2 -2
- package/templates/skills/tools/verify-module/SKILL.md +2 -2
- package/templates/skills/tools/verify-quality/SKILL.md +2 -2
- package/templates/skills/tools/verify-security/SKILL.md +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: '后台任务观测:列表 / 单查 / 阻塞等待 / dashboard / tail 流式 / 卡点检测 / 单 phase cancel
|
|
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 — 后台任务观测(
|
|
10
|
+
# Status — 后台任务观测(dashboard + tail)
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
37
|
-
/ccg:status --cancel <phase-id> # 模式 E:单 phase 协作 cancel
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
197
|
-
- `src/utils/stuck-detector.ts
|
|
198
|
-
- `src/utils/process-tree.ts
|
|
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
|
-
|
|
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
|
|
204
|
+
- cancel:直接 `echo > cancel.flag` + sleep 5 + 调 process-tree
|
|
205
205
|
|
|
206
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
14
|
-
| `/ccg:team plan <约束文件>` | 仅跑规划阶段 | `/ccg:team-plan
|
|
15
|
-
| `/ccg:team review [git-range]` | 仅跑双模型审查阶段 | `/ccg:team-review
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
## 调用通道路由(
|
|
43
|
+
## 调用通道路由(CCG codeagent 退役)
|
|
44
44
|
|
|
45
|
-
CCG
|
|
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**(
|
|
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
|
-
|
|
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`
|
|
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
|
-
**并行调用 +
|
|
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})` ——
|
|
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
|
|
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
|
|
16
|
+
# Verify Work — 会话式 UAT 工作流
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
135
|
+
#### 2a. 静态扫描(多门保留)
|
|
136
136
|
|
|
137
|
-
按 git diff
|
|
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
|
-
## 与
|
|
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 -
|
|
12
|
+
# /ccg:verify - 统一校验关卡
|
|
13
13
|
|
|
14
|
-
|
|
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:
|
|
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
|
-
**并行调用 +
|
|
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})` ——
|
|
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
|
-
**
|
|
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;
|