ccgx-workflow 2.1.0 → 2.1.2

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.DVsx1kL4.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.dK6O-vwe.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.DVsx1kL4.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.DVsx1kL4.mjs';
1
+ import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.dK6O-vwe.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.dK6O-vwe.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.1.0";
16
+ const version = "2.1.2";
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.1.0",
3
+ "version": "2.1.2",
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
  // ---------------------------------------------------------------------------
@@ -103,7 +103,26 @@ function parseArgs(argv) {
103
103
  vendor: null,
104
104
  promptFile: null,
105
105
  json: true,
106
- 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.2: idle detection is OPT-IN (default 0 = disabled). The original
112
+ // 2.1.1 assumption that "healthy long-running audits emit continuous
113
+ // stdout/stderr chunks" is FALSE for both vendors:
114
+ // - codex-companion `task`: writes progress to its internal job
115
+ // state.json (read via `codex status`); helper sees ZERO stdout
116
+ // until the final JSON envelope.
117
+ // - gemini-batch / gemini-cli `--output-format json`: emits 4-5
118
+ // startup warning lines, then SILENT through model reasoning + all
119
+ // tool calls, then the final JSON.
120
+ // Deep reasoning passes can silently exceed any reasonable idle
121
+ // threshold while being perfectly healthy. Wall-time (default 2h)
122
+ // remains the only safety bound by default. Callers who know their
123
+ // task class is supposed to stream (e.g. future protocol additions)
124
+ // can pass --idle-timeout-ms <N> explicitly.
125
+ idleTimeoutMs: 0,
107
126
  maxBudgetUsd: 50,
108
127
  // 1.0.5 regression fix: pre-1.0.5 callers always passed --write directly
109
128
  // to codex-companion. Default true preserves BC; --no-write opt-out for
@@ -127,6 +146,7 @@ function parseArgs(argv) {
127
146
  case '--json': opts.json = true; break
128
147
  case '--no-json': opts.json = false; break
129
148
  case '--timeout-ms': opts.timeoutMs = Number.parseInt(next(), 10); break
149
+ case '--idle-timeout-ms': opts.idleTimeoutMs = Number.parseInt(next(), 10); break
130
150
  case '--max-budget-usd': opts.maxBudgetUsd = Number.parseFloat(next()); break
131
151
  case '--write': opts.write = true; break
132
152
  case '--no-write': opts.write = false; break
@@ -166,7 +186,8 @@ Required:
166
186
  Optional:
167
187
  --json Pass --json to companion (default: true)
168
188
  --no-json Disable --json (text output)
169
- --timeout-ms <N> Kill companion after N ms (default: 600000)
189
+ --timeout-ms <N> Kill companion if total wall-time exceeds N ms (default: 7200000, 2h; pass 0 to disable)
190
+ --idle-timeout-ms <N> Kill companion if no stdout/stderr output for N ms (default: 0 = disabled). NOTE: codex-companion and gemini-batch both emit NOTHING to stdout/stderr during execution (progress lives in internal state files); idle defaults to disabled to avoid false-positive kills of healthy long tasks. Set to a positive value only if your task class is known to stream.
170
191
  --max-budget-usd <N> Per-call cost cap (default: 50)
171
192
  --write Enable workspace-write sandbox (default: true; codex needs this to spawn read commands too under default approval policy)
172
193
  --no-write Force read-only sandbox (companion approval-policy will decline most commands)
@@ -285,6 +306,35 @@ async function main(argv) {
285
306
  process.exit(66) // EX_NOINPUT
286
307
  }
287
308
 
309
+ // 2.1.1: auto-delete the prompt file after a successful read iff the
310
+ // PHYSICAL path sits inside the caller's `<cwd>/.context/tmp/` AND the
311
+ // filename starts with `ccg-`. This is the workflow-managed throwaway-
312
+ // prompt directory (see templates/commands/review.md — Step 1 writes
313
+ // prompts here).
314
+ //
315
+ // CRITICAL — codex audit 2026-05-12 raised: lexical `resolve()` is
316
+ // insufficient. If `.context` or `.context/tmp` is a symlink/junction
317
+ // pointing outside the workspace, OR the caller passes a path whose
318
+ // lexical form is `<cwd>/.context/tmp/ccg-x.txt` but its physical
319
+ // target is `/etc/passwd` (or worse, `C:\Windows\System32\...`), the
320
+ // lexical-only check would still authorize the unlink. realpathSync
321
+ // canonicalizes ALL symlinks/junctions/`..` segments before the
322
+ // whitelist compare — both for the candidate file AND the safeRoot.
323
+ // Two ANDed gates: physical prefix + filename prefix.
324
+ try {
325
+ const resolvedFile = realpathSync(opts.promptFile)
326
+ const safeRoot = realpathSync(resolve(process.cwd(), '.context', 'tmp')) + sep
327
+ if (resolvedFile.startsWith(safeRoot) && basename(resolvedFile).startsWith('ccg-')) {
328
+ unlinkSync(resolvedFile)
329
+ }
330
+ }
331
+ catch {
332
+ // Best-effort. ENOENT (.context/tmp absent, file already gone, etc.)
333
+ // is the dominant non-error case here; we deliberately don't surface
334
+ // it. Real failures (permission etc.) will leave a file behind that
335
+ // .gitignore catches and the user's next run overwrites.
336
+ }
337
+
288
338
  // Discover companion via SSoT (no glob, no head -1)
289
339
  const disc = discoverCompanion(opts.vendor)
290
340
  if (disc.error) {
@@ -327,24 +377,53 @@ async function main(argv) {
327
377
 
328
378
  let stdoutBuf = ''
329
379
  let stderrBuf = ''
330
- child.stdout.on('data', (chunk) => { stdoutBuf += chunk.toString('utf-8') })
331
- child.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8') })
380
+ let lastActivityAt = Date.now()
381
+ child.stdout.on('data', (chunk) => {
382
+ stdoutBuf += chunk.toString('utf-8')
383
+ lastActivityAt = Date.now()
384
+ })
385
+ child.stderr.on('data', (chunk) => {
386
+ stderrBuf += chunk.toString('utf-8')
387
+ lastActivityAt = Date.now()
388
+ })
332
389
 
333
- // Timeout: kill child if exceeds opts.timeoutMs
390
+ // 2.1.1: two-layer timeout. wall-time is the safety ceiling; idle is the
391
+ // primary "stuck" detector — a healthy long task keeps producing tool-call
392
+ // / progress chunks, a truly hung companion produces nothing. SIGTERM is
393
+ // best-effort on Windows where the kernel doesn't propagate to the
394
+ // grandchild tree; companion processes on this code path are expected to
395
+ // do their own subtree cleanup (taskkill /T) when they receive SIGTERM.
334
396
  let timedOut = false
335
- const timer = setTimeout(() => {
397
+ let timeoutReason = null
398
+ const killChild = (reason) => {
336
399
  timedOut = true
400
+ timeoutReason = reason
337
401
  try { child.kill('SIGTERM') } catch { /* ignore */ }
338
402
  setTimeout(() => {
339
403
  try { child.kill('SIGKILL') } catch { /* ignore */ }
340
404
  }, 5000).unref?.()
341
- }, opts.timeoutMs)
405
+ }
406
+ const wallTimer = opts.timeoutMs > 0
407
+ ? setTimeout(() => killChild(`wall-time ${opts.timeoutMs}ms`), opts.timeoutMs)
408
+ : null
409
+ // Check idle every 30s. A 30s sampling granularity adds at most 30s slop
410
+ // to the configured idle threshold, which is negligible at 600s default.
411
+ const idleChecker = opts.idleTimeoutMs > 0
412
+ ? setInterval(() => {
413
+ const silent = Date.now() - lastActivityAt
414
+ if (silent >= opts.idleTimeoutMs) {
415
+ killChild(`idle ${silent}ms exceeds ${opts.idleTimeoutMs}ms`)
416
+ }
417
+ }, 30000)
418
+ : null
419
+ if (idleChecker?.unref) idleChecker.unref()
342
420
 
343
421
  const exit = await new Promise((resolve) => {
344
422
  child.once('exit', (code, signal) => resolve({ code, signal }))
345
423
  child.once('error', (err) => resolve({ code: 70, signal: null, errorMsg: err.message }))
346
424
  })
347
- clearTimeout(timer)
425
+ if (wallTimer) clearTimeout(wallTimer)
426
+ if (idleChecker) clearInterval(idleChecker)
348
427
 
349
428
  const durationMs = Date.now() - startedAt
350
429
  const status = (!timedOut && exit.code === 0) ? 'ok' : 'error'
@@ -357,7 +436,7 @@ async function main(argv) {
357
436
  stdout: stdoutBuf,
358
437
  stderr: stderrBuf,
359
438
  }
360
- if (timedOut) result.error = `timeout after ${opts.timeoutMs}ms`
439
+ if (timedOut) result.error = `companion killed: ${timeoutReason}`
361
440
  else if (exit.errorMsg) result.error = `spawn error: ${exit.errorMsg}`
362
441
  else if (exit.code !== 0) result.error = `companion exited with code ${exit.code}`
363
442