ccgx-workflow 2.0.0 → 2.1.1

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.Bd3sZyxW.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.BIcRT31-.mjs';
5
5
  import { execSync } from 'node:child_process';
6
6
  import 'inquirer';
7
7
  import 'ora';
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.BIcRT31-.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.BIcRT31-.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 = "2.0.0";
16
+ const version = "2.1.1";
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": "2.0.0",
3
+ "version": "2.1.1",
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",
@@ -66,20 +66,20 @@ argument-hint: "[代码或描述] [--adversarial] [--fix [--all] [--auto]] [--ro
66
66
  Step 1. 用 Write 工具把【小任务描述】写到 tmpfile
67
67
  ⚠️ 内容必须是任务说明(≤ 2KB),不要塞 git diff / 文件内容
68
68
  Write({
69
- file_path: "/tmp/ccg-review-codex-$JOB.txt",
69
+ file_path: ".context/tmp/ccg-review-codex-$JOB.txt",
70
70
  content: <下方"任务描述模板">
71
71
  })
72
72
  Write({ 同上,gemini 版本 })
73
73
 
74
74
  Step 2. 用 Bash 调 helper(占位符已渲染):
75
75
  Bash({
76
- command: `{{CODEX_BASH_TASK}} --prompt-file /tmp/ccg-review-codex-$JOB.txt`,
76
+ command: `{{CODEX_BASH_TASK}} --prompt-file .context/tmp/ccg-review-codex-$JOB.txt`,
77
77
  description: "Review: backend (codex direct)",
78
78
  run_in_background: true,
79
79
  timeout: 3600000
80
80
  })
81
81
  Bash({
82
- command: `{{GEMINI_BASH_TASK}} --prompt-file /tmp/ccg-review-gemini-$JOB.txt`,
82
+ command: `{{GEMINI_BASH_TASK}} --prompt-file .context/tmp/ccg-review-gemini-$JOB.txt`,
83
83
  description: "Review: frontend (gemini direct)",
84
84
  run_in_background: true,
85
85
  timeout: 3600000
@@ -148,14 +148,14 @@ CLI 空答 / auth 过期时 wrapper 受 instruction-tuning 驱动**自答 fabric
148
148
 
149
149
  ```
150
150
  Bash({
151
- command: "cat /tmp/ccg-review-codex-$JOB.txt | ~/.claude/bin/codeagent-wrapper {{LITE_MODE_FLAG}}--progress --backend codex - \"{{WORKDIR}}\"",
151
+ command: "cat .context/tmp/ccg-review-codex-$JOB.txt | ~/.claude/bin/codeagent-wrapper {{LITE_MODE_FLAG}}--progress --backend codex - \"{{WORKDIR}}\"",
152
152
  run_in_background: true,
153
153
  timeout: 3600000,
154
154
  description: "Review: backend (wrapper fallback)"
155
155
  })
156
156
  ```
157
157
 
158
- `/tmp/ccg-review-codex-$JOB.txt` 同 通道 A 的"任务描述模板"——不变。
158
+ `.context/tmp/ccg-review-codex-$JOB.txt` 同 通道 A 的"任务描述模板"——不变。
159
159
 
160
160
  > ⚠️ 通道 B `codeagent-wrapper` 已标 **deprecated**,仅 plugin 未装时使用。
161
161
 
@@ -227,11 +227,11 @@ Bash({
227
227
 
228
228
  ```
229
229
  Step 1. 用 Write 把任务描述写入 tmpfile(同样不塞 diff,让 codex 自己读):
230
- Write({ file_path: "/tmp/ccg-review-adv-$JOB.txt", content: <下方任务描述> })
230
+ Write({ file_path: ".context/tmp/ccg-review-adv-$JOB.txt", content: <下方任务描述> })
231
231
 
232
232
  Step 2. 调 helper:
233
233
  Bash({
234
- command: `{{CODEX_BASH_TASK}} --prompt-file /tmp/ccg-review-adv-$JOB.txt`,
234
+ command: `{{CODEX_BASH_TASK}} --prompt-file .context/tmp/ccg-review-adv-$JOB.txt`,
235
235
  description: "Adversarial review (codex direct)",
236
236
  run_in_background: true,
237
237
  timeout: 3600000
@@ -44,7 +44,7 @@
44
44
  // --prompt-file <path> Required. Path to file containing prompt body.
45
45
  // --json Pass --json to companion (default: true).
46
46
  // --no-json Disable --json (text output).
47
- // --timeout-ms <N> Kill companion after N ms (default: 600000).
47
+ // --timeout-ms <N> Kill companion if total wall-time exceeds N ms (default: 7200000, 2h; pass 0 to disable).
48
48
  // --max-budget-usd <N> Forwarded to companion (default: 50).
49
49
  //
50
50
  // Cross-cutting:
@@ -55,9 +55,9 @@
55
55
  // =============================================================================
56
56
 
57
57
  import { spawn } from 'node:child_process'
58
- import { existsSync, readFileSync } from 'node:fs'
58
+ import { existsSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
59
59
  import { homedir, platform } from 'node:os'
60
- import { join } from 'node:path'
60
+ import { basename, join, resolve, sep } from 'node:path'
61
61
  import { fileURLToPath } from 'node:url'
62
62
 
63
63
  // ---------------------------------------------------------------------------
@@ -73,6 +73,27 @@ const VENDOR_KEYS = {
73
73
  gemini: ['gemini@gemini-ccgx', 'gemini@google-gemini'],
74
74
  }
75
75
 
76
+ // ---------------------------------------------------------------------------
77
+ // Vendor → preferred entry script (relative to <installPath>/scripts/).
78
+ //
79
+ // gemini: prefer `gemini-batch.mjs` (CCG-only, bypasses ACP broker entirely
80
+ // — direct gemini-cli batch mode via stdin + --output-format json). Falls
81
+ // back to the legacy `gemini-companion.mjs task` (ACP path) only if the
82
+ // batch entry isn't shipped.
83
+ //
84
+ // Why bypass ACP: gemini-cli 0.40+'s session/new RPC runs MCP setup + auth
85
+ // refresh + chat startup synchronously inside the agent. On Windows the
86
+ // gemini-plugin-cc ACP broker + named-pipe transport reliably hangs trivial
87
+ // trips for 5+ minutes even when the direct CLI returns in 10-30s. Batch
88
+ // mode side-steps the entire transport stack (no broker, no detached
89
+ // process, no orphan MCP children).
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const VENDOR_ENTRY_SCRIPTS = {
93
+ codex: ['codex-companion.mjs'],
94
+ gemini: ['gemini-batch.mjs', 'gemini-companion.mjs'],
95
+ }
96
+
76
97
  // ---------------------------------------------------------------------------
77
98
  // Argument parsing — minimal, KISS, no external CLI lib.
78
99
  // ---------------------------------------------------------------------------
@@ -82,8 +103,25 @@ function parseArgs(argv) {
82
103
  vendor: null,
83
104
  promptFile: null,
84
105
  json: true,
85
- timeoutMs: 600000,
106
+ // 2.1.1: wall-time bumped 600s → 7200s (2h). Real codex audits /
107
+ // multi-file reviews / planning tasks routinely run 5-30 min; the old
108
+ // 600s default was SIGTERM-ing healthy tasks mid-thought. 2h is a
109
+ // generous safety ceiling — pass 0 to disable entirely.
110
+ timeoutMs: 7200000,
111
+ // 2.1.1: NEW idle timeout. Wall-time alone is the wrong signal — a
112
+ // healthy long-running audit produces continuous stdout/stderr (tool
113
+ // calls, progress lines). A truly hung companion produces nothing.
114
+ // We track lastActivityAt on every stdout/stderr chunk and SIGTERM if
115
+ // silent for idleTimeoutMs (default 600s = 10min). Pass 0 to disable.
116
+ idleTimeoutMs: 600000,
86
117
  maxBudgetUsd: 50,
118
+ // 1.0.5 regression fix: pre-1.0.5 callers always passed --write directly
119
+ // to codex-companion. Default true preserves BC; --no-write opt-out for
120
+ // read-only review flows.
121
+ write: true,
122
+ model: null,
123
+ effort: null,
124
+ cwd: null,
87
125
  }
88
126
 
89
127
  const positional = []
@@ -99,7 +137,13 @@ function parseArgs(argv) {
99
137
  case '--json': opts.json = true; break
100
138
  case '--no-json': opts.json = false; break
101
139
  case '--timeout-ms': opts.timeoutMs = Number.parseInt(next(), 10); break
140
+ case '--idle-timeout-ms': opts.idleTimeoutMs = Number.parseInt(next(), 10); break
102
141
  case '--max-budget-usd': opts.maxBudgetUsd = Number.parseFloat(next()); break
142
+ case '--write': opts.write = true; break
143
+ case '--no-write': opts.write = false; break
144
+ case '--model': opts.model = next(); break
145
+ case '--effort': opts.effort = next(); break
146
+ case '--cwd': opts.cwd = next(); break
103
147
  case '--help':
104
148
  case '-h':
105
149
  printHelp()
@@ -133,8 +177,14 @@ Required:
133
177
  Optional:
134
178
  --json Pass --json to companion (default: true)
135
179
  --no-json Disable --json (text output)
136
- --timeout-ms <N> Kill companion after N ms (default: 600000)
180
+ --timeout-ms <N> Kill companion if total wall-time exceeds N ms (default: 7200000, 2h; pass 0 to disable)
181
+ --idle-timeout-ms <N> Kill companion if no stdout/stderr output for N ms (default: 600000, 10min; pass 0 to disable). Idle detection is the right signal for "hung" — healthy long-running audits keep producing tool-call progress.
137
182
  --max-budget-usd <N> Per-call cost cap (default: 50)
183
+ --write Enable workspace-write sandbox (default: true; codex needs this to spawn read commands too under default approval policy)
184
+ --no-write Force read-only sandbox (companion approval-policy will decline most commands)
185
+ --model <name> Override model (codex only; gemini ignores)
186
+ --effort <level> Reasoning effort: none|minimal|low|medium|high|xhigh (codex only)
187
+ --cwd <path> Override working directory for the companion call. codex sandbox is cwd-bound; set this when auditing a repo other than the caller's cwd.
138
188
 
139
189
  Outputs JSON to stdout: {status, vendor, version, durationMs, exitCode, stdout, stderr, error?}
140
190
  `)
@@ -180,9 +230,19 @@ function discoverCompanion(vendor, homeDir = homedir()) {
180
230
  return { error: `plugin ${matchedKey} has no installPath in installed_plugins.json` }
181
231
  }
182
232
 
183
- const companionPath = join(installPath, 'scripts', `${vendor}-companion.mjs`)
184
- if (!existsSync(companionPath)) {
185
- return { error: `companion script missing: ${companionPath}` }
233
+ // Walk the preferred entry-script list. First existing file wins.
234
+ // BC: vendors with only the legacy companion still resolve correctly.
235
+ const entryCandidates = VENDOR_ENTRY_SCRIPTS[vendor] ?? [`${vendor}-companion.mjs`]
236
+ let companionPath = null
237
+ for (const name of entryCandidates) {
238
+ const candidate = join(installPath, 'scripts', name)
239
+ if (existsSync(candidate)) {
240
+ companionPath = candidate
241
+ break
242
+ }
243
+ }
244
+ if (!companionPath) {
245
+ return { error: `no entry script found for ${vendor} (tried: ${entryCandidates.join(', ')}) in ${join(installPath, 'scripts')}` }
186
246
  }
187
247
 
188
248
  return {
@@ -237,6 +297,35 @@ async function main(argv) {
237
297
  process.exit(66) // EX_NOINPUT
238
298
  }
239
299
 
300
+ // 2.1.1: auto-delete the prompt file after a successful read iff the
301
+ // PHYSICAL path sits inside the caller's `<cwd>/.context/tmp/` AND the
302
+ // filename starts with `ccg-`. This is the workflow-managed throwaway-
303
+ // prompt directory (see templates/commands/review.md — Step 1 writes
304
+ // prompts here).
305
+ //
306
+ // CRITICAL — codex audit 2026-05-12 raised: lexical `resolve()` is
307
+ // insufficient. If `.context` or `.context/tmp` is a symlink/junction
308
+ // pointing outside the workspace, OR the caller passes a path whose
309
+ // lexical form is `<cwd>/.context/tmp/ccg-x.txt` but its physical
310
+ // target is `/etc/passwd` (or worse, `C:\Windows\System32\...`), the
311
+ // lexical-only check would still authorize the unlink. realpathSync
312
+ // canonicalizes ALL symlinks/junctions/`..` segments before the
313
+ // whitelist compare — both for the candidate file AND the safeRoot.
314
+ // Two ANDed gates: physical prefix + filename prefix.
315
+ try {
316
+ const resolvedFile = realpathSync(opts.promptFile)
317
+ const safeRoot = realpathSync(resolve(process.cwd(), '.context', 'tmp')) + sep
318
+ if (resolvedFile.startsWith(safeRoot) && basename(resolvedFile).startsWith('ccg-')) {
319
+ unlinkSync(resolvedFile)
320
+ }
321
+ }
322
+ catch {
323
+ // Best-effort. ENOENT (.context/tmp absent, file already gone, etc.)
324
+ // is the dominant non-error case here; we deliberately don't surface
325
+ // it. Real failures (permission etc.) will leave a file behind that
326
+ // .gitignore catches and the user's next run overwrites.
327
+ }
328
+
240
329
  // Discover companion via SSoT (no glob, no head -1)
241
330
  const disc = discoverCompanion(opts.vendor)
242
331
  if (disc.error) {
@@ -252,6 +341,15 @@ async function main(argv) {
252
341
  promptBody,
253
342
  ]
254
343
  if (opts.json) companionArgs.push('--json')
344
+ // codex-companion flag pass-through (1.0.5 regression fix). gemini-companion
345
+ // ignores unknown flags safely; both vendors accept this surface.
346
+ if (opts.write) companionArgs.push('--write')
347
+ if (opts.model) companionArgs.push('--model', opts.model)
348
+ if (opts.effort) companionArgs.push('--effort', opts.effort)
349
+ // --cwd pass-through: codex sandbox is cwd-bound. For cross-repo audits
350
+ // the caller must set this to the repo being audited (codex-companion
351
+ // does not expose --add-dir; multi-workspace requires deeper changes).
352
+ if (opts.cwd) companionArgs.push('--cwd', opts.cwd)
255
353
 
256
354
  const startedAt = Date.now()
257
355
 
@@ -270,24 +368,53 @@ async function main(argv) {
270
368
 
271
369
  let stdoutBuf = ''
272
370
  let stderrBuf = ''
273
- child.stdout.on('data', (chunk) => { stdoutBuf += chunk.toString('utf-8') })
274
- child.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8') })
371
+ let lastActivityAt = Date.now()
372
+ child.stdout.on('data', (chunk) => {
373
+ stdoutBuf += chunk.toString('utf-8')
374
+ lastActivityAt = Date.now()
375
+ })
376
+ child.stderr.on('data', (chunk) => {
377
+ stderrBuf += chunk.toString('utf-8')
378
+ lastActivityAt = Date.now()
379
+ })
275
380
 
276
- // Timeout: kill child if exceeds opts.timeoutMs
381
+ // 2.1.1: two-layer timeout. wall-time is the safety ceiling; idle is the
382
+ // primary "stuck" detector — a healthy long task keeps producing tool-call
383
+ // / progress chunks, a truly hung companion produces nothing. SIGTERM is
384
+ // best-effort on Windows where the kernel doesn't propagate to the
385
+ // grandchild tree; companion processes on this code path are expected to
386
+ // do their own subtree cleanup (taskkill /T) when they receive SIGTERM.
277
387
  let timedOut = false
278
- const timer = setTimeout(() => {
388
+ let timeoutReason = null
389
+ const killChild = (reason) => {
279
390
  timedOut = true
391
+ timeoutReason = reason
280
392
  try { child.kill('SIGTERM') } catch { /* ignore */ }
281
393
  setTimeout(() => {
282
394
  try { child.kill('SIGKILL') } catch { /* ignore */ }
283
395
  }, 5000).unref?.()
284
- }, opts.timeoutMs)
396
+ }
397
+ const wallTimer = opts.timeoutMs > 0
398
+ ? setTimeout(() => killChild(`wall-time ${opts.timeoutMs}ms`), opts.timeoutMs)
399
+ : null
400
+ // Check idle every 30s. A 30s sampling granularity adds at most 30s slop
401
+ // to the configured idle threshold, which is negligible at 600s default.
402
+ const idleChecker = opts.idleTimeoutMs > 0
403
+ ? setInterval(() => {
404
+ const silent = Date.now() - lastActivityAt
405
+ if (silent >= opts.idleTimeoutMs) {
406
+ killChild(`idle ${silent}ms exceeds ${opts.idleTimeoutMs}ms`)
407
+ }
408
+ }, 30000)
409
+ : null
410
+ if (idleChecker?.unref) idleChecker.unref()
285
411
 
286
412
  const exit = await new Promise((resolve) => {
287
413
  child.once('exit', (code, signal) => resolve({ code, signal }))
288
414
  child.once('error', (err) => resolve({ code: 70, signal: null, errorMsg: err.message }))
289
415
  })
290
- clearTimeout(timer)
416
+ if (wallTimer) clearTimeout(wallTimer)
417
+ if (idleChecker) clearInterval(idleChecker)
291
418
 
292
419
  const durationMs = Date.now() - startedAt
293
420
  const status = (!timedOut && exit.code === 0) ? 'ok' : 'error'
@@ -300,7 +427,7 @@ async function main(argv) {
300
427
  stdout: stdoutBuf,
301
428
  stderr: stderrBuf,
302
429
  }
303
- if (timedOut) result.error = `timeout after ${opts.timeoutMs}ms`
430
+ if (timedOut) result.error = `companion killed: ${timeoutReason}`
304
431
  else if (exit.errorMsg) result.error = `spawn error: ${exit.errorMsg}`
305
432
  else if (exit.code !== 0) result.error = `companion exited with code ${exit.code}`
306
433