ccgx-workflow 1.0.9 → 1.0.10

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.9nxM0FK2.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.9nxM0FK2.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.9nxM0FK2.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';
@@ -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 = "1.0.10";
17
17
 
18
18
  function cmd(id, order, category, name, nameEn, description, descriptionEn, cmdOverride) {
19
19
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgx-workflow",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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",
@@ -142,6 +142,18 @@ const PATCHES = [
142
142
  match: /(if \(message\.error\) \{\s*\r?\n\s*)pending\.reject\(message\.error\);(\s*\r?\n\s*\} else \{)/,
143
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
144
  },
145
+ {
146
+ id: "P-12",
147
+ file: "gemini-companion.mjs",
148
+ description: "CLAUDE_PLUGIN_DATA env cross-contamination — recompute from script path",
149
+ // Guard: any patched marker present
150
+ guard: /CCG P-12 patch/,
151
+ // Anchor on the last import line (createStreamHandler). Newer plugin
152
+ // versions may add imports after this; if so, the regex misses (reported
153
+ // by the script's [MISS] line) and the patch is re-evaluated upstream.
154
+ match: /(import\s+\{\s*createStreamHandler\s*\}\s+from\s+"\.\/lib\/stream-output\.mjs";?)/,
155
+ 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}`,
156
+ },
145
157
  ];
146
158
 
147
159
  let applied = 0;