ccgx-workflow 1.0.9 → 2.0.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.
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import cac from 'cac';
3
3
  import ansis from 'ansis';
4
- import { d as diagnoseMcpConfig, i as isWindows, r as readClaudeCodeConfig, f as fixWindowsMcpConfig, w as writeClaudeCodeConfig, a as readCcgConfig, b as initI18n, c as i18n, s as showMainMenu, e as init, g as configMcp, v as version } from './shared/ccgx-workflow.BmN81Bej.mjs';
4
+ import { d as diagnoseMcpConfig, i as isWindows, r as readClaudeCodeConfig, f as fixWindowsMcpConfig, w as writeClaudeCodeConfig, a as readCcgConfig, b as initI18n, c as i18n, s as showMainMenu, e as init, g as configMcp, v as version } from './shared/ccgx-workflow.Bd3sZyxW.mjs';
5
5
  import { execSync } from 'node:child_process';
6
6
  import 'inquirer';
7
7
  import 'ora';
@@ -75,6 +75,17 @@ async function fixMcp() {
75
75
  }
76
76
  }
77
77
 
78
+ const STUCK_MIN_AGE_SECONDS = 300;
79
+ const STUCK_CPU_RATIO_THRESHOLD = 0.01;
80
+ function isBrokerProcess(p) {
81
+ return /acp-broker|app-server-broker|broker-lifecycle/.test(p.cmdLine);
82
+ }
83
+ function isStuck(p) {
84
+ if (!isBrokerProcess(p)) return false;
85
+ const wallSeconds = p.ageHours * 3600;
86
+ if (wallSeconds < STUCK_MIN_AGE_SECONDS) return false;
87
+ return p.cpuSeconds / wallSeconds < STUCK_CPU_RATIO_THRESHOLD;
88
+ }
78
89
  function categorize(cmdLine) {
79
90
  const cmd = cmdLine.toLowerCase();
80
91
  if (/quasar|vite|webpack|next\b|nuxt|astro|svelte-kit|pnpm.*run.*dev|npm.*run.*dev|yarn.*run.*dev|yarn dev|pnpm dev|npm run dev/.test(cmd)) {
@@ -92,7 +103,10 @@ function listNodeProcessesWindows() {
92
103
  $cd = $_.CreationDate
93
104
  $h = if ($cd) { ((Get-Date) - $cd).TotalHours } else { 0 }
94
105
  $cmd = if ($_.CommandLine) { $_.CommandLine } else { '' }
95
- "$($_.ProcessId)|$h|$cmd"
106
+ $kt = if ($_.KernelModeTime) { $_.KernelModeTime } else { 0 }
107
+ $ut = if ($_.UserModeTime) { $_.UserModeTime } else { 0 }
108
+ $cpuS = ($kt + $ut) / 10000000.0
109
+ "$($_.ProcessId)|$h|$cpuS|$cmd"
96
110
  }`;
97
111
  let out;
98
112
  try {
@@ -111,43 +125,56 @@ function listNodeProcessesWindows() {
111
125
  if (!trimmed) continue;
112
126
  const sep1 = trimmed.indexOf("|");
113
127
  const sep2 = trimmed.indexOf("|", sep1 + 1);
114
- if (sep1 < 0 || sep2 < 0) continue;
128
+ const sep3 = trimmed.indexOf("|", sep2 + 1);
129
+ if (sep1 < 0 || sep2 < 0 || sep3 < 0) continue;
115
130
  const pid = Number.parseInt(trimmed.slice(0, sep1), 10);
116
131
  const ageHours = Number.parseFloat(trimmed.slice(sep1 + 1, sep2));
117
- const cmdLine = trimmed.slice(sep2 + 1);
132
+ const cpuSeconds = Number.parseFloat(trimmed.slice(sep2 + 1, sep3));
133
+ const cmdLine = trimmed.slice(sep3 + 1);
118
134
  if (Number.isNaN(pid)) continue;
119
135
  procs.push({
120
136
  pid,
121
137
  ageHours: Number.isFinite(ageHours) ? ageHours : 0,
138
+ cpuSeconds: Number.isFinite(cpuSeconds) ? cpuSeconds : 0,
122
139
  cmdLine,
123
140
  category: categorize(cmdLine)
124
141
  });
125
142
  }
126
143
  return procs;
127
144
  }
145
+ function parseHmsToSeconds(spec) {
146
+ const parts = spec.split(/[-:]/).map((s) => Number.parseInt(s, 10));
147
+ if (parts.length === 4) return parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60 + parts[3];
148
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
149
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
150
+ return 0;
151
+ }
128
152
  function listNodeProcessesPosix() {
129
153
  let out;
130
154
  try {
131
- out = execSync("ps -eo pid,etime,command", { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
155
+ out = execSync("ps -eo pid,etime,time,command", { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
132
156
  } catch {
133
157
  return [];
134
158
  }
135
159
  const procs = [];
136
160
  for (const line of out.split("\n").slice(1)) {
137
- const m = line.match(/^\s*(\d+)\s+(\S+)\s+(.+)$/);
161
+ const m = line.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
138
162
  if (!m) continue;
139
- const cmdLine = m[3];
163
+ const cmdLine = m[4];
140
164
  if (!/(^|\/)node(\s|$)/.test(cmdLine)) continue;
141
165
  const pid = Number.parseInt(m[1], 10);
142
166
  const etime = m[2];
167
+ const cpuTime = m[3];
143
168
  const parts = etime.split(/[-:]/).map((s) => Number.parseInt(s, 10));
144
169
  let ageHours = 0;
145
170
  if (parts.length === 4) ageHours = parts[0] * 24 + parts[1] + parts[2] / 60;
146
171
  else if (parts.length === 3) ageHours = parts[0] + parts[1] / 60;
147
172
  else if (parts.length === 2) ageHours = parts[0] / 60;
173
+ const cpuSeconds = parseHmsToSeconds(cpuTime);
148
174
  procs.push({
149
175
  pid,
150
176
  ageHours,
177
+ cpuSeconds,
151
178
  cmdLine,
152
179
  category: categorize(cmdLine)
153
180
  });
@@ -159,13 +186,13 @@ function listNodeProcesses() {
159
186
  }
160
187
  function killProcessWindows(pid) {
161
188
  try {
162
- execSync(`powershell -NoProfile -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`, { stdio: "pipe" });
163
- return { ok: true, method: "Stop-Process" };
189
+ execSync(`taskkill /F /T /PID ${pid}`, { stdio: "pipe" });
190
+ return { ok: true, method: "taskkill /F /T" };
164
191
  } catch {
165
192
  }
166
193
  try {
167
- execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
168
- return { ok: true, method: "taskkill /F" };
194
+ execSync(`powershell -NoProfile -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`, { stdio: "pipe" });
195
+ return { ok: true, method: "Stop-Process" };
169
196
  } catch {
170
197
  }
171
198
  try {
@@ -176,6 +203,11 @@ function killProcessWindows(pid) {
176
203
  }
177
204
  }
178
205
  function killProcessPosix(pid) {
206
+ try {
207
+ execSync(`kill -TERM -- -${pid}`, { stdio: "pipe" });
208
+ return { ok: true, method: "SIGTERM (group)" };
209
+ } catch {
210
+ }
179
211
  try {
180
212
  execSync(`kill -TERM ${pid}`, { stdio: "pipe" });
181
213
  return { ok: true, method: "SIGTERM" };
@@ -191,8 +223,10 @@ function killProcessPosix(pid) {
191
223
  async function killOrphans(options = {}) {
192
224
  const dryRun = options.dryRun ?? true;
193
225
  const minAgeHours = options.minAgeHours ?? 1;
226
+ const stuckOnly = options.stuckOnly ?? false;
227
+ const targetDesc = stuckOnly ? "stuck broker daemons only (CPU/wall < 1%, age > 5min, ignores companions/MCP)" : `orphan node processes >${minAgeHours}h`;
194
228
  console.log(ansis.cyan.bold("\n ccgx kill-orphans"));
195
- console.log(ansis.gray(` ${dryRun ? "[DRY-RUN]" : "[KILL MODE]"} target: orphan node processes >${minAgeHours}h
229
+ console.log(ansis.gray(` ${dryRun ? "[DRY-RUN]" : "[KILL MODE]"} target: ${targetDesc}
196
230
  `));
197
231
  const all = listNodeProcesses();
198
232
  if (all.length === 0) {
@@ -209,24 +243,37 @@ async function killOrphans(options = {}) {
209
243
  const list = groups.get(cat) ?? [];
210
244
  if (list.length === 0) continue;
211
245
  const oldestH = Math.max(...list.map((p) => p.ageHours));
246
+ const stuckCount = list.filter(isStuck).length;
212
247
  const tag = cat === "dev-server" ? ansis.green("SAFE") : cat === "other" ? ansis.gray("UNKN") : ansis.yellow("ORPH");
213
- console.log(` ${tag} ${cat.padEnd(14)} ${String(list.length).padStart(3)} processes (oldest ${oldestH.toFixed(1)}h)`);
248
+ const stuckTag = stuckCount > 0 ? ansis.red(` ${stuckCount} stuck`) : "";
249
+ console.log(` ${tag} ${cat.padEnd(14)} ${String(list.length).padStart(3)} processes (oldest ${oldestH.toFixed(1)}h)${stuckTag}`);
214
250
  }
215
251
  console.log();
216
252
  const ORPHAN_CATS = ["mcp-server", "codex-cli", "gemini-cli", "phase-runner"];
217
- const targets = all.filter((p) => ORPHAN_CATS.includes(p.category) && p.ageHours >= minAgeHours);
253
+ const targets = all.filter((p) => {
254
+ if (!ORPHAN_CATS.includes(p.category)) return false;
255
+ if (stuckOnly) return isStuck(p);
256
+ return p.ageHours >= minAgeHours;
257
+ });
218
258
  if (targets.length === 0) {
219
- console.log(ansis.green(` \u2713 No orphans >${minAgeHours}h to clean.`));
259
+ const reason = stuckOnly ? "stuck" : `>${minAgeHours}h`;
260
+ console.log(ansis.green(` \u2713 No ${reason} orphans to clean.`));
220
261
  return;
221
262
  }
222
263
  console.log(ansis.bold(` Targets (${targets.length}):`));
223
264
  for (const p of targets) {
224
- const cmdShort = p.cmdLine.length > 70 ? `${p.cmdLine.slice(0, 70)}...` : p.cmdLine;
225
- console.log(` PID ${String(p.pid).padStart(6)} ${p.ageHours.toFixed(1).padStart(5)}h ${ansis.gray(p.category.padEnd(14))} ${cmdShort}`);
265
+ const cmdShort = p.cmdLine.length > 60 ? `${p.cmdLine.slice(0, 60)}...` : p.cmdLine;
266
+ const wallS = p.ageHours * 3600;
267
+ const ratio = wallS > 0 ? p.cpuSeconds / wallS * 100 : 0;
268
+ const stuckMark = isStuck(p) ? ansis.red(" STUCK") : "";
269
+ console.log(
270
+ ` PID ${String(p.pid).padStart(6)} ${p.ageHours.toFixed(1).padStart(5)}h CPU ${p.cpuSeconds.toFixed(1).padStart(6)}s (${ratio.toFixed(2).padStart(5)}%) ${ansis.gray(p.category.padEnd(14))} ${cmdShort}${stuckMark}`
271
+ );
226
272
  }
227
273
  console.log();
228
274
  if (dryRun) {
229
- console.log(ansis.gray(` Run with --kill to actually terminate (skips dev-server / other categories).`));
275
+ const flag = stuckOnly ? "--stuck --kill" : "--kill";
276
+ console.log(ansis.gray(` Run with ${flag} to actually terminate (skips dev-server / other categories).`));
230
277
  return;
231
278
  }
232
279
  const killer = isWindows() ? killProcessWindows : killProcessPosix;
@@ -327,10 +374,11 @@ async function setupCommands(cli) {
327
374
  cli.command("diagnose-mcp", i18n.t("cli:help.commandDescriptions.diagnoseMcp")).action(async () => {
328
375
  await diagnoseMcp();
329
376
  });
330
- cli.command("kill-orphans", "\u6E05\u7406 Claude Code session \u9000\u51FA\u540E\u6B8B\u7559\u7684\u5B64\u513F node \u8FDB\u7A0B\uFF08MCP server / codex / gemini\uFF09").option("--kill", "\u5B9E\u9645\u6267\u884C kill (\u9ED8\u8BA4 dry-run)").option("--min-age-hours <N>", "\u53EA\u6E05\u7406\u8D85\u8FC7 N \u5C0F\u65F6\u7684\u8FDB\u7A0B (\u9ED8\u8BA4 1)").action(async (options) => {
377
+ cli.command("kill-orphans", "\u6E05\u7406 Claude Code session \u9000\u51FA\u540E\u6B8B\u7559\u7684\u5B64\u513F node \u8FDB\u7A0B\uFF08MCP server / codex / gemini\uFF09").option("--kill", "\u5B9E\u9645\u6267\u884C kill (\u9ED8\u8BA4 dry-run)").option("--min-age-hours <N>", "\u53EA\u6E05\u7406\u8D85\u8FC7 N \u5C0F\u65F6\u7684\u8FDB\u7A0B (\u9ED8\u8BA4 1)").option("--stuck", "\u53EA\u6E05\u7406\u505C\u6EDE\u8FDB\u7A0B\uFF1ACPU \u5360\u6BD4 < 1% \u4E14 wall age > 5min\uFF08\u8986\u76D6 --min-age-hours\uFF09").action(async (options) => {
331
378
  await killOrphans({
332
379
  dryRun: !options.kill,
333
- minAgeHours: options.minAgeHours ? Number.parseFloat(options.minAgeHours) : 1
380
+ minAgeHours: options.minAgeHours ? Number.parseFloat(options.minAgeHours) : 1,
381
+ stuckOnly: options.stuck
334
382
  });
335
383
  });
336
384
  cli.command("fix-mcp", i18n.t("cli:help.commandDescriptions.fixMcp")).action(async () => {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.BmN81Bej.mjs';
2
- export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.BmN81Bej.mjs';
1
+ import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.Bd3sZyxW.mjs';
2
+ export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.Bd3sZyxW.mjs';
3
3
  import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync } from 'node:fs';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { homedir } from 'node:os';
@@ -775,7 +775,7 @@ function pluginRoot(home = homedir()) {
775
775
  const PLUGIN_MARKERS = ["SKILL.md", "plugin.json", "package.json", "manifest.json"];
776
776
  const PLUGIN_PREFIXES = {
777
777
  codex: ["codex@", "codex-rescue@", "openai-codex@"],
778
- gemini: ["gemini@", "gemini-rescue@", "google-gemini@"]
778
+ gemini: ["gemini@", "gemini-rescue@", "gemini-ccgx@", "google-gemini@"]
779
779
  };
780
780
  function detectPlugin(name, homeDir = homedir()) {
781
781
  const root = pluginRoot(homeDir);
@@ -13,7 +13,7 @@ import { join as join$2 } from 'node:path/posix';
13
13
  import { join as join$1 } from 'node:path';
14
14
  import i18next from 'i18next';
15
15
 
16
- const version = "1.0.9";
16
+ const version = "2.0.0";
17
17
 
18
18
  function cmd(id, order, category, name, nameEn, description, descriptionEn, cmdOverride) {
19
19
  return {
@@ -295,8 +295,18 @@ function getMcpCommand(command) {
295
295
  }
296
296
 
297
297
  const VENDOR_MARKETPLACE_KEYS = {
298
- codex: "codex@openai-codex",
299
- gemini: "gemini@google-gemini"
298
+ codex: ["codex@openai-codex"],
299
+ gemini: ["gemini@gemini-ccgx", "gemini@google-gemini"]
300
+ };
301
+ const VENDOR_PRIMARY_INSTALL = {
302
+ codex: {
303
+ marketplace: "openai/codex-plugin-cc",
304
+ key: "codex@openai-codex"
305
+ },
306
+ gemini: {
307
+ marketplace: "wzyxdwll/gemini-plugin-cc",
308
+ key: "gemini@gemini-ccgx"
309
+ }
300
310
  };
301
311
  function discoverCompanion(vendor, homeDir = homedir()) {
302
312
  const ssotPath = join$1(homeDir, ".claude", "plugins", "installed_plugins.json");
@@ -307,10 +317,16 @@ function discoverCompanion(vendor, homeDir = homedir()) {
307
317
  } catch {
308
318
  return null;
309
319
  }
310
- const key = VENDOR_MARKETPLACE_KEYS[vendor];
311
- const instances = raw?.plugins?.[key];
312
- if (!Array.isArray(instances) || instances.length === 0) return null;
313
- const inst = instances[0];
320
+ const keys = VENDOR_MARKETPLACE_KEYS[vendor];
321
+ let inst = null;
322
+ for (const key of keys) {
323
+ const instances = raw?.plugins?.[key];
324
+ if (Array.isArray(instances) && instances.length > 0) {
325
+ inst = instances[0];
326
+ break;
327
+ }
328
+ }
329
+ if (!inst) return null;
314
330
  const installPath = inst?.installPath;
315
331
  if (typeof installPath !== "string" || !installPath) return null;
316
332
  const companionPath = join$1(installPath, "scripts", `${vendor}-companion.mjs`);
@@ -335,10 +351,12 @@ function buildBashCommand(_loc, options = {}) {
335
351
  return `node ${quotedHelper} ${vendor}${jsonFlag}`;
336
352
  }
337
353
  function buildPluginMissingFallback(vendor) {
338
- const key = VENDOR_MARKETPLACE_KEYS[vendor];
354
+ const { marketplace, key } = VENDOR_PRIMARY_INSTALL[vendor];
339
355
  return [
340
- `# CCG: ${vendor} plugin (${key}) not installed at CCG install time.`,
341
- `# Install with: claude plugin install ${key}`,
356
+ `# CCG: ${vendor} plugin not installed at CCG install time.`,
357
+ `# Install with:`,
358
+ `# claude plugin marketplace add ${marketplace}`,
359
+ `# claude plugin install ${key}`,
342
360
  `# Then re-run: npx ccgx-workflow init --skip-prompt --skip-mcp`,
343
361
  `echo 'CCG: ${vendor} plugin not available' >&2 && exit 1`
344
362
  ].join("\n");
@@ -4064,7 +4082,9 @@ ${exportCommand}
4064
4082
  console.log();
4065
4083
  console.log(` ${ansis.green("\u2605")} ${ansis.bold("Plugin (recommended, one-click in Claude Code):")}`);
4066
4084
  console.log(` ${ansis.cyan("/plugins install codex@openai-codex")}`);
4067
- console.log(` ${ansis.cyan("/plugins install gemini@google-gemini")}`);
4085
+ console.log(` ${ansis.cyan("claude plugin marketplace add wzyxdwll/gemini-plugin-cc")} ${ansis.gray("# 2.0.0+ ccgx fork")}`);
4086
+ console.log(` ${ansis.cyan("/plugins install gemini@gemini-ccgx")} ${ansis.gray("# preferred (\u542B P-1..P-21 + W1/W2/I1 patch)")}`);
4087
+ console.log(` ${ansis.gray(" \u2014 or upstream: /plugins install gemini@google-gemini (requires repatch)")}`);
4068
4088
  console.log();
4069
4089
  console.log(` ${ansis.green("\u2606")} ${ansis.bold("CLI fallback (standalone binaries):")}`);
4070
4090
  console.log(` ${ansis.cyan("npm i -g @openai/codex")} ${ansis.gray("# then: codex login")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgx-workflow",
3
- "version": "1.0.9",
3
+ "version": "2.0.0",
4
4
  "description": "Multi-model orchestration for Claude Code. Codex + Gemini parallel collaboration with fresh-context subagent protocols, OS-level process isolation, and Plan-Critic-Verify quality tiers. Successor to ccg-workflow.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -39,7 +39,7 @@ argument-hint: "<分析问题或任务> [--role=architect|critic|implementer|tes
39
39
 
40
40
  CCG 把双模型并行通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn:
41
41
 
42
- 1. **优先 plugin spawn**(默认):装了 `codex@openai-codex` + `gemini@google-gemini` plugin 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线接 ≤200 token 摘要。
42
+ 1. **优先 plugin spawn**(默认):装了 `codex@openai-codex` + gemini plugin(推荐 `gemini@gemini-ccgx` fork,含 P-1..P-21 + W1/W2/I1 patch;或上游 `gemini@google-gemini` repatch 脚本)→ 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线接 ≤200 token 摘要。
43
43
  2. **降级 codeagent-wrapper**(BC fallback):plugin 未装 → fallback 到 Bash 调用,行为与 plugin 路径等价。
44
44
 
45
45
  **判定**:preflight `Bash` 跑 `ls ~/.claude/plugins/` 看有无 `codex@*` / `gemini@*` 子目录。helper 见 `src/utils/plugin-detection.ts`。
@@ -24,7 +24,7 @@ $ARGUMENTS
24
24
 
25
25
  CCG 把 6 核心命令的"双模型并行"通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn。判定流程:
26
26
 
27
- 1. **优先 plugin spawn 路径**(默认):用户已装 `codex@openai-codex` 和 `gemini@google-gemini` plugin 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行 spawn,主线只接 plugin 自家 ≤200 token 摘要。
27
+ 1. **优先 plugin spawn 路径**(默认):用户已装 `codex@openai-codex` 和 gemini plugin(推荐 `gemini@gemini-ccgx` fork,已含全部 patch;或上游 `gemini@google-gemini` repatch)→ 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行 spawn,主线只接 plugin 自家 ≤200 token 摘要。
28
28
  2. **降级 codeagent-wrapper 路径**(BC fallback):plugin 未装 → fallback 到 `Bash(~/.claude/bin/codeagent-wrapper ...)`,行为与 plugin 路径等价。
29
29
 
30
30
  **判断方法**:preflight 用 `Bash` 跑 `ls ~/.claude/plugins/ 2>/dev/null | grep -E '^(codex|gemini)@'`;两个 plugin 独立判定。
@@ -44,7 +44,7 @@ argument-hint: "<优化目标> [--role=architect|critic|implementer|tester|write
44
44
 
45
45
  CCG 把双模型并行通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn:
46
46
 
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 摘要。
47
+ 1. **优先 plugin spawn**(默认):装了 `codex@openai-codex` + gemini plugin(推荐 `gemini@gemini-ccgx` fork,含 P-1..P-21 + W1/W2/I1 patch;或上游 `gemini@google-gemini` repatch 脚本)→ 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线接 ≤200 token 摘要。
48
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`。
@@ -54,7 +54,7 @@ $ARGUMENTS
54
54
 
55
55
  CCG 把 6 核心命令的"双模型并行"通道从 `Bash(codeagent-wrapper)` **默认切换**为 plugin spawn。判定流程:
56
56
 
57
- 1. **优先 plugin spawn 路径**(默认):用户已装 `codex@openai-codex` 和 `gemini@google-gemini` plugin 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行 spawn,主线只接 plugin 自家 ≤200 token 摘要协议(`STATUS: ... / FINDINGS: ... / NOTES: ...`)。
57
+ 1. **优先 plugin spawn 路径**(默认):用户已装 `codex@openai-codex` 和 gemini plugin(推荐 `gemini@gemini-ccgx` fork,已含全部 patch;或上游 `gemini@google-gemini` repatch)→ 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行 spawn,主线只接 plugin 自家 ≤200 token 摘要协议(`STATUS: ... / FINDINGS: ... / NOTES: ...`)。
58
58
  2. **降级 codeagent-wrapper 路径**(BC fallback):plugin 未装 → fallback 到 `Bash(~/.claude/bin/codeagent-wrapper --backend ... resume ... <<'EOF' ... EOF)`,与 plugin 路径行为等价。
59
59
 
60
60
  **判断方法**:preflight 用 `Bash` 跑 `ls ~/.claude/plugins/ 2>/dev/null | grep -E '^codex@'` 与 `... | grep -E '^gemini@'` 各一次。匹配到对应行 → plugin 已装。两个 plugin 独立判定,可分别 mix-and-match(仅 codex plugin 装了 → backend 走 plugin、frontend 走 codeagent)。
@@ -21,7 +21,7 @@ argument-hint: "[代码或描述] [--adversarial] [--fix [--all] [--auto]] [--ro
21
21
 
22
22
  双模型并行审查,交叉验证综合反馈。无参数时自动审查当前 git 变更。
23
23
 
24
- **双模型并行通道**:默认走 plugin spawn —— 装了 `codex@openai-codex` + `gemini@google-gemini` plugin 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线只接 ≤200 token 摘要;plugin 未装 → fallback 到 codeagent-wrapper 路径(BC fallback)。preflight 用 `Bash` 跑 `ls ~/.claude/plugins/` 检测,helper 见 `src/utils/plugin-detection.ts`。
24
+ **双模型并行通道**:默认走 plugin spawn —— 装了 `codex@openai-codex` + gemini plugin(推荐 `gemini@gemini-ccgx` fork,含 P-1..P-21 + W1/W2/I1 patch;或上游 `gemini@google-gemini` repatch 脚本)→ 用 `Agent(subagent_type="codex:codex-rescue")` + `Agent(subagent_type="gemini:gemini-rescue")` 并行,主线只接 ≤200 token 摘要;plugin 未装 → fallback 到 codeagent-wrapper 路径(BC fallback)。preflight 用 `Bash` 跑 `ls ~/.claude/plugins/` 检测,helper 见 `src/utils/plugin-detection.ts`。
25
25
 
26
26
  `--adversarial` 模式下额外触发第三层"敌对视角"审查,由官方 codex plugin 的 `Agent(codex:codex-rescue)` 在 fresh context 中专门挑前两轮意见的漏洞,适合极重要 PR / 安全敏感变更。需用户已装 `codex@openai-codex` plugin,否则降级为双模型审查。
27
27
 
@@ -61,12 +61,16 @@ import { join } from 'node:path'
61
61
  import { fileURLToPath } from 'node:url'
62
62
 
63
63
  // ---------------------------------------------------------------------------
64
- // Vendor → marketplace key (matches plugin-bash-codegen.ts SSoT lookup)
64
+ // Vendor → marketplace keys (ordered preference list).
65
+ //
66
+ // CCG 2.0.0: gemini-ccgx (ccgx-maintained fork shipping P-1..P-21 + W1/W2/I1
67
+ // inline, no repatch needed) is preferred over google-gemini (upstream).
68
+ // Keep this list in sync with src/utils/plugin-bash-codegen.ts.
65
69
  // ---------------------------------------------------------------------------
66
70
 
67
71
  const VENDOR_KEYS = {
68
- codex: 'codex@openai-codex',
69
- gemini: 'gemini@google-gemini',
72
+ codex: ['codex@openai-codex'],
73
+ gemini: ['gemini@gemini-ccgx', 'gemini@google-gemini'],
70
74
  }
71
75
 
72
76
  // ---------------------------------------------------------------------------
@@ -154,16 +158,26 @@ function discoverCompanion(vendor, homeDir = homedir()) {
154
158
  return { error: `installed_plugins.json parse failed: ${e.message}` }
155
159
  }
156
160
 
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
+ // Try preferred keys in order — fork first, upstream fallback.
162
+ const keys = VENDOR_KEYS[vendor]
163
+ let instances = null
164
+ let matchedKey = null
165
+ for (const key of keys) {
166
+ const candidate = raw?.plugins?.[key]
167
+ if (Array.isArray(candidate) && candidate.length > 0) {
168
+ instances = candidate
169
+ matchedKey = key
170
+ break
171
+ }
172
+ }
173
+ if (!instances || instances.length === 0) {
174
+ return { error: `${vendor} plugin not installed (tried ${keys.join(', ')})` }
161
175
  }
162
176
 
163
177
  const inst = instances[0]
164
178
  const installPath = inst?.installPath
165
179
  if (typeof installPath !== 'string' || !installPath) {
166
- return { error: `plugin ${key} has no installPath in installed_plugins.json` }
180
+ return { error: `plugin ${matchedKey} has no installPath in installed_plugins.json` }
167
181
  }
168
182
 
169
183
  const companionPath = join(installPath, 'scripts', `${vendor}-companion.mjs`)
@@ -217,7 +217,7 @@ function exitMissingBackend(backend) {
217
217
  const pluginInstall = backend === 'codex'
218
218
  ? '/plugins install codex@openai-codex'
219
219
  : backend === 'gemini'
220
- ? '/plugins install gemini@google-gemini'
220
+ ? '/plugins install gemini@gemini-ccgx (ccgx 2.0.0 fork, recommended) — or /plugins install gemini@google-gemini (upstream)'
221
221
  : null;
222
222
  const npmInstall = backend === 'codex'
223
223
  ? 'npm i -g @openai/codex'
@@ -51,8 +51,23 @@ function findPluginVersion() {
51
51
  return versions[versions.length - 1];
52
52
  }
53
53
 
54
- const VERSION = findPluginVersion();
55
- const SCRIPTS = join(PLUGIN_BASE, VERSION, "scripts");
54
+ // Allow redirecting the patch target to a custom location, e.g. a local fork.
55
+ // When GEMINI_PLUGIN_SCRIPTS is set, it must point directly to the `scripts/`
56
+ // directory (containing acp-broker.mjs, gemini-companion.mjs, lib/...).
57
+ // Falls back to ~/.claude/plugins/cache/.../gemini/<latest>/scripts/.
58
+ let SCRIPTS;
59
+ let VERSION;
60
+ if (process.env.GEMINI_PLUGIN_SCRIPTS) {
61
+ SCRIPTS = process.env.GEMINI_PLUGIN_SCRIPTS;
62
+ if (!existsSync(SCRIPTS)) {
63
+ throw new Error(`GEMINI_PLUGIN_SCRIPTS path does not exist: ${SCRIPTS}`);
64
+ }
65
+ VERSION = `<custom: ${SCRIPTS}>`;
66
+ }
67
+ else {
68
+ VERSION = findPluginVersion();
69
+ SCRIPTS = join(PLUGIN_BASE, VERSION, "scripts");
70
+ }
56
71
 
57
72
  console.log(`Patching gemini plugin ${VERSION} at:\n ${SCRIPTS}\n`);
58
73
 
@@ -142,6 +157,18 @@ const PATCHES = [
142
157
  match: /(if \(message\.error\) \{\s*\r?\n\s*)pending\.reject\(message\.error\);(\s*\r?\n\s*\} else \{)/,
143
158
  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
159
  },
160
+ {
161
+ id: "P-12",
162
+ file: "gemini-companion.mjs",
163
+ description: "CLAUDE_PLUGIN_DATA env cross-contamination — recompute from script path",
164
+ // Guard: any patched marker present
165
+ guard: /CCG P-12 patch/,
166
+ // Anchor on the last import line (createStreamHandler). Newer plugin
167
+ // versions may add imports after this; if so, the regex misses (reported
168
+ // by the script's [MISS] line) and the patch is re-evaluated upstream.
169
+ match: /(import\s+\{\s*createStreamHandler\s*\}\s+from\s+"\.\/lib\/stream-output\.mjs";?)/,
170
+ replace: `$1\n\n// CCG P-12 patch: prevent CLAUDE_PLUGIN_DATA cross-contamination from previous\n// plugin invocations (e.g., codex). Recompute from this script's physical path\n// so the broker session file lands in our own data dir, enabling broker reuse.\n// See .ccg-migration/PLUGIN-PATCHES.md P-12.\n{\n const _scriptDir = path.dirname(fileURLToPath(import.meta.url));\n const _versionDir = path.dirname(_scriptDir);\n const _pluginDir = path.dirname(_versionDir);\n const _marketplaceDir = path.dirname(_pluginDir);\n const _cacheDir = path.dirname(_marketplaceDir);\n const _pluginsDir = path.dirname(_cacheDir);\n const _pluginName = path.basename(_pluginDir);\n const _marketplaceName = path.basename(_marketplaceDir);\n process.env.CLAUDE_PLUGIN_DATA = path.join(_pluginsDir, "data", _pluginName + "-" + _marketplaceName);\n}`,
171
+ },
145
172
  ];
146
173
 
147
174
  let applied = 0;