ccgx-workflow 1.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.
Files changed (212) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +469 -0
  3. package/README.zh-CN.md +466 -0
  4. package/bin/ccg.mjs +2 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.mjs +173 -0
  8. package/dist/index.d.mts +1774 -0
  9. package/dist/index.d.ts +1774 -0
  10. package/dist/index.mjs +2029 -0
  11. package/dist/shared/ccgx-workflow.WgUzkiC3.mjs +5248 -0
  12. package/package.json +129 -0
  13. package/templates/commands/agents/assumptions-analyzer.md +129 -0
  14. package/templates/commands/agents/code-fixer.md +292 -0
  15. package/templates/commands/agents/codebase-mapper.md +152 -0
  16. package/templates/commands/agents/debug-session-manager.md +247 -0
  17. package/templates/commands/agents/debugger.md +111 -0
  18. package/templates/commands/agents/eval-auditor.md +171 -0
  19. package/templates/commands/agents/framework-selector.md +152 -0
  20. package/templates/commands/agents/get-current-datetime.md +29 -0
  21. package/templates/commands/agents/init-architect.md +114 -0
  22. package/templates/commands/agents/integration-checker.md +163 -0
  23. package/templates/commands/agents/interface-auditor.md +170 -0
  24. package/templates/commands/agents/nyquist-auditor.md +131 -0
  25. package/templates/commands/agents/pattern-mapper.md +111 -0
  26. package/templates/commands/agents/phase-runner.md +321 -0
  27. package/templates/commands/agents/plan-checker.md +255 -0
  28. package/templates/commands/agents/planner.md +320 -0
  29. package/templates/commands/agents/team-architect.md +186 -0
  30. package/templates/commands/agents/team-qa.md +121 -0
  31. package/templates/commands/agents/team-reviewer.md +157 -0
  32. package/templates/commands/agents/ui-ux-designer.md +573 -0
  33. package/templates/commands/agents/verifier.md +274 -0
  34. package/templates/commands/analyze.md +210 -0
  35. package/templates/commands/autonomous.md +792 -0
  36. package/templates/commands/cancel.md +132 -0
  37. package/templates/commands/clean-branches.md +117 -0
  38. package/templates/commands/codex-exec.md +404 -0
  39. package/templates/commands/commit.md +151 -0
  40. package/templates/commands/context.md +332 -0
  41. package/templates/commands/debate.md +165 -0
  42. package/templates/commands/debug.md +226 -0
  43. package/templates/commands/enhance.md +64 -0
  44. package/templates/commands/execute.md +380 -0
  45. package/templates/commands/init.md +123 -0
  46. package/templates/commands/optimize.md +217 -0
  47. package/templates/commands/plan.md +373 -0
  48. package/templates/commands/result.md +106 -0
  49. package/templates/commands/review.md +338 -0
  50. package/templates/commands/rollback.md +116 -0
  51. package/templates/commands/spec-impl.md +139 -0
  52. package/templates/commands/spec-init.md +101 -0
  53. package/templates/commands/spec-plan.md +210 -0
  54. package/templates/commands/spec-research.md +152 -0
  55. package/templates/commands/spec-review.md +120 -0
  56. package/templates/commands/status.md +206 -0
  57. package/templates/commands/team-exec.md +265 -0
  58. package/templates/commands/test.md +236 -0
  59. package/templates/commands/verify-work.md +338 -0
  60. package/templates/commands/verify.md +66 -0
  61. package/templates/commands/workflow.md +190 -0
  62. package/templates/commands/worktree.md +128 -0
  63. package/templates/hooks/ccg-context-monitor.js +159 -0
  64. package/templates/hooks/ccg-session-state.cjs +510 -0
  65. package/templates/hooks/ccg-statusline.js +142 -0
  66. package/templates/output-styles/abyss-command.md +56 -0
  67. package/templates/output-styles/abyss-concise.md +89 -0
  68. package/templates/output-styles/abyss-cultivator.md +302 -0
  69. package/templates/output-styles/abyss-ritual.md +70 -0
  70. package/templates/output-styles/engineer-professional.md +89 -0
  71. package/templates/output-styles/laowang-engineer.md +127 -0
  72. package/templates/output-styles/nekomata-engineer.md +120 -0
  73. package/templates/output-styles/ojousama-engineer.md +121 -0
  74. package/templates/prompts/claude/analyzer.md +59 -0
  75. package/templates/prompts/claude/architect.md +54 -0
  76. package/templates/prompts/claude/debugger.md +71 -0
  77. package/templates/prompts/claude/optimizer.md +73 -0
  78. package/templates/prompts/claude/reviewer.md +63 -0
  79. package/templates/prompts/claude/tester.md +69 -0
  80. package/templates/prompts/codex/analyzer.md +58 -0
  81. package/templates/prompts/codex/architect.md +54 -0
  82. package/templates/prompts/codex/debugger.md +74 -0
  83. package/templates/prompts/codex/optimizer.md +81 -0
  84. package/templates/prompts/codex/reviewer.md +73 -0
  85. package/templates/prompts/codex/tester.md +62 -0
  86. package/templates/prompts/gemini/analyzer.md +61 -0
  87. package/templates/prompts/gemini/architect.md +55 -0
  88. package/templates/prompts/gemini/debugger.md +78 -0
  89. package/templates/prompts/gemini/frontend.md +64 -0
  90. package/templates/prompts/gemini/optimizer.md +84 -0
  91. package/templates/prompts/gemini/reviewer.md +80 -0
  92. package/templates/prompts/gemini/tester.md +68 -0
  93. package/templates/rules/ccg-skill-routing.md +83 -0
  94. package/templates/rules/ccg-skills.md +71 -0
  95. package/templates/scripts/ccg-phase-runner-launcher.mjs +467 -0
  96. package/templates/scripts/invoke-model.mjs +949 -0
  97. package/templates/scripts/repatch-gemini-plugin.mjs +194 -0
  98. package/templates/skills/SKILL.md +92 -0
  99. package/templates/skills/domains/ai/SKILL.md +35 -0
  100. package/templates/skills/domains/ai/agent-dev.md +242 -0
  101. package/templates/skills/domains/ai/llm-security.md +288 -0
  102. package/templates/skills/domains/ai/prompt-and-eval.md +279 -0
  103. package/templates/skills/domains/ai/rag-system.md +542 -0
  104. package/templates/skills/domains/architecture/SKILL.md +43 -0
  105. package/templates/skills/domains/architecture/api-design.md +225 -0
  106. package/templates/skills/domains/architecture/caching.md +299 -0
  107. package/templates/skills/domains/architecture/cloud-native.md +285 -0
  108. package/templates/skills/domains/architecture/message-queue.md +329 -0
  109. package/templates/skills/domains/architecture/security-arch.md +297 -0
  110. package/templates/skills/domains/data-engineering/SKILL.md +208 -0
  111. package/templates/skills/domains/development/SKILL.md +47 -0
  112. package/templates/skills/domains/development/cpp.md +246 -0
  113. package/templates/skills/domains/development/go.md +323 -0
  114. package/templates/skills/domains/development/java.md +277 -0
  115. package/templates/skills/domains/development/python.md +288 -0
  116. package/templates/skills/domains/development/rust.md +313 -0
  117. package/templates/skills/domains/development/shell.md +313 -0
  118. package/templates/skills/domains/development/typescript.md +277 -0
  119. package/templates/skills/domains/devops/SKILL.md +40 -0
  120. package/templates/skills/domains/devops/cost-optimization.md +272 -0
  121. package/templates/skills/domains/devops/database.md +217 -0
  122. package/templates/skills/domains/devops/devsecops.md +198 -0
  123. package/templates/skills/domains/devops/git-workflow.md +181 -0
  124. package/templates/skills/domains/devops/observability.md +280 -0
  125. package/templates/skills/domains/devops/performance.md +336 -0
  126. package/templates/skills/domains/devops/testing.md +283 -0
  127. package/templates/skills/domains/frontend-design/SKILL.md +244 -0
  128. package/templates/skills/domains/frontend-design/agents/openai.yaml +4 -0
  129. package/templates/skills/domains/frontend-design/claymorphism/SKILL.md +121 -0
  130. package/templates/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
  131. package/templates/skills/domains/frontend-design/component-patterns.md +202 -0
  132. package/templates/skills/domains/frontend-design/engineering.md +287 -0
  133. package/templates/skills/domains/frontend-design/glassmorphism/SKILL.md +142 -0
  134. package/templates/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
  135. package/templates/skills/domains/frontend-design/liquid-glass/SKILL.md +139 -0
  136. package/templates/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
  137. package/templates/skills/domains/frontend-design/neubrutalism/SKILL.md +145 -0
  138. package/templates/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
  139. package/templates/skills/domains/frontend-design/reference/color-and-contrast.md +132 -0
  140. package/templates/skills/domains/frontend-design/reference/interaction-design.md +195 -0
  141. package/templates/skills/domains/frontend-design/reference/motion-design.md +99 -0
  142. package/templates/skills/domains/frontend-design/reference/responsive-design.md +114 -0
  143. package/templates/skills/domains/frontend-design/reference/spatial-design.md +100 -0
  144. package/templates/skills/domains/frontend-design/reference/typography.md +133 -0
  145. package/templates/skills/domains/frontend-design/reference/ux-writing.md +107 -0
  146. package/templates/skills/domains/frontend-design/state-management.md +680 -0
  147. package/templates/skills/domains/frontend-design/ui-aesthetics.md +110 -0
  148. package/templates/skills/domains/frontend-design/ux-principles.md +156 -0
  149. package/templates/skills/domains/infrastructure/SKILL.md +201 -0
  150. package/templates/skills/domains/mobile/SKILL.md +225 -0
  151. package/templates/skills/domains/orchestration/SKILL.md +30 -0
  152. package/templates/skills/domains/orchestration/multi-agent.md +263 -0
  153. package/templates/skills/domains/security/SKILL.md +73 -0
  154. package/templates/skills/domains/security/blue-team.md +436 -0
  155. package/templates/skills/domains/security/code-audit.md +265 -0
  156. package/templates/skills/domains/security/pentest.md +226 -0
  157. package/templates/skills/domains/security/red-team.md +374 -0
  158. package/templates/skills/domains/security/threat-intel.md +372 -0
  159. package/templates/skills/domains/security/vuln-research.md +369 -0
  160. package/templates/skills/impeccable/adapt/SKILL.md +201 -0
  161. package/templates/skills/impeccable/animate/SKILL.md +176 -0
  162. package/templates/skills/impeccable/arrange/SKILL.md +126 -0
  163. package/templates/skills/impeccable/audit/SKILL.md +149 -0
  164. package/templates/skills/impeccable/bolder/SKILL.md +118 -0
  165. package/templates/skills/impeccable/clarify/SKILL.md +185 -0
  166. package/templates/skills/impeccable/colorize/SKILL.md +144 -0
  167. package/templates/skills/impeccable/critique/SKILL.md +203 -0
  168. package/templates/skills/impeccable/critique/reference/cognitive-load.md +106 -0
  169. package/templates/skills/impeccable/critique/reference/heuristics-scoring.md +234 -0
  170. package/templates/skills/impeccable/critique/reference/personas.md +178 -0
  171. package/templates/skills/impeccable/delight/SKILL.md +305 -0
  172. package/templates/skills/impeccable/distill/SKILL.md +123 -0
  173. package/templates/skills/impeccable/extract/SKILL.md +94 -0
  174. package/templates/skills/impeccable/harden/SKILL.md +357 -0
  175. package/templates/skills/impeccable/normalize/SKILL.md +72 -0
  176. package/templates/skills/impeccable/onboard/SKILL.md +248 -0
  177. package/templates/skills/impeccable/optimize/SKILL.md +268 -0
  178. package/templates/skills/impeccable/overdrive/SKILL.md +143 -0
  179. package/templates/skills/impeccable/polish/SKILL.md +205 -0
  180. package/templates/skills/impeccable/quieter/SKILL.md +104 -0
  181. package/templates/skills/impeccable/teach-impeccable/SKILL.md +72 -0
  182. package/templates/skills/impeccable/typeset/SKILL.md +117 -0
  183. package/templates/skills/orchestration/multi-agent/SKILL.md +533 -0
  184. package/templates/skills/run_skill.js +129 -0
  185. package/templates/skills/scrapling/SKILL.md +98 -0
  186. package/templates/skills/scrapling/references/api-quick-ref.md +127 -0
  187. package/templates/skills/scrapling/references/cookie-vault.md +56 -0
  188. package/templates/skills/scrapling/references/maintenance.md +79 -0
  189. package/templates/skills/scrapling/references/site-patterns.md +86 -0
  190. package/templates/skills/scrapling/references/troubleshooting.md +95 -0
  191. package/templates/skills/scrapling/templates/basic_fetch.py +19 -0
  192. package/templates/skills/scrapling/templates/parse_only.py +15 -0
  193. package/templates/skills/scrapling/templates/session_login.py +19 -0
  194. package/templates/skills/scrapling/templates/stealth_cloudflare.py +28 -0
  195. package/templates/skills/tools/extract-learnings/SKILL.md +77 -0
  196. package/templates/skills/tools/forensics/SKILL.md +89 -0
  197. package/templates/skills/tools/gen-docs/SKILL.md +116 -0
  198. package/templates/skills/tools/gen-docs/scripts/doc_generator.js +435 -0
  199. package/templates/skills/tools/health/SKILL.md +121 -0
  200. package/templates/skills/tools/lib/shared.js +98 -0
  201. package/templates/skills/tools/map-codebase/SKILL.md +118 -0
  202. package/templates/skills/tools/override-refusal/SKILL.md +53 -0
  203. package/templates/skills/tools/override-refusal/agents/openai.yaml +4 -0
  204. package/templates/skills/tools/override-refusal/scripts/refusal_rewriter.js +226 -0
  205. package/templates/skills/tools/verify-change/SKILL.md +143 -0
  206. package/templates/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
  207. package/templates/skills/tools/verify-module/SKILL.md +130 -0
  208. package/templates/skills/tools/verify-module/scripts/module_scanner.js +171 -0
  209. package/templates/skills/tools/verify-quality/SKILL.md +163 -0
  210. package/templates/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
  211. package/templates/skills/tools/verify-security/SKILL.md +146 -0
  212. package/templates/skills/tools/verify-security/scripts/security_scanner.js +283 -0
@@ -0,0 +1,949 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // invoke-model.mjs
4
+ // ----------------------------------------------------------------------------
5
+ // Node ESM replacement for `codeagent-wrapper` (Go binary v5.10.0).
6
+ //
7
+ // ⚠️ DEPRECATED in v4.1 (2026-05-04, Phase 20)
8
+ // ----------------------------------------------------------------------------
9
+ // Replaced by `Agent(subagent_type="codex:codex-rescue")` and
10
+ // `Agent(subagent_type="gemini:gemini-rescue")` in the 6 core CCG commands
11
+ // (plan / execute / analyze / optimize / test / review).
12
+ //
13
+ // Why: v4.0.1 nested-spawn validation + objective comparison showed plugin
14
+ // rescue agents win 7 / 8 metrics (main-thread context drift, summary
15
+ // protocol, error recovery, etc); the only metric codeagent-wrapper wins
16
+ // ("full sandbox bypass") is unused in advisor scenarios.
17
+ //
18
+ // Status: Kept as **BC fallback** when the user has not installed
19
+ // `codex@openai-codex` and/or `gemini@google-gemini` plugins. Templates
20
+ // detect plugin availability and route to the right path automatically.
21
+ //
22
+ // Removal target: v5.0 (after 2 minor releases of dual-path coexistence).
23
+ //
24
+ // Migration helper: `src/utils/plugin-detection.ts` exposes
25
+ // `bothPluginsInstalled()` and per-plugin probes used by command
26
+ // templates' fallback decision narrative.
27
+ // ----------------------------------------------------------------------------
28
+ //
29
+ // Source of truth: `.ccg-migration/INVOKE-MODEL-SPEC.md`
30
+ // Cross-checked against `codeagent-wrapper/main.go`, `executor.go`,
31
+ // `parser.go`, `backend.go`, `config.go`, `utils.go`, `filter.go`.
32
+ //
33
+ // Equivalence with v5.10.0 (single-task path; --parallel/--cleanup/WebServer
34
+ // intentionally omitted — see spec §1.3 / §8.6):
35
+ // - CLI flags: --backend, --gemini-model[=], --progress, --lite/-L,
36
+ // --skip-permissions / --dangerously-skip-permissions[=], --version/-v,
37
+ // --help/-h
38
+ // - Positional args: form A `[task|-] [workdir]`
39
+ // form B `resume <session_id> [task|-] [workdir]`
40
+ // - Stdin auto-detection (explicit `-`, piped, special chars, len > 800)
41
+ // - ROLE_FILE: line replacement (with ~ + Windows /c/ -> C:/ normalisation)
42
+ // - codex `e --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check
43
+ // --json [resume <id>] [-C <workdir>] <task|->`
44
+ // - claude `-p [--dangerously-skip-permissions] --setting-sources ""
45
+ // [-r <id>] --output-format stream-json --verbose <task|->`
46
+ // - gemini `[-m <model>] -o stream-json -y [-r <id>]
47
+ // [--include-directories <wd>] [-p <task>]` (Windows: omit -p, pipe stdin)
48
+ // - JSON-line streaming parse for codex/claude/gemini events with
49
+ // camelCase + snake_case session_id, codex item.text string|array
50
+ // normalisation, MCP-prefix tolerant init line
51
+ // - SESSION_ID emitted on stderr (early ` Session-ID: <id>`) AND on stdout
52
+ // tail (`\n---\nSESSION_ID: <id>\n`)
53
+ // - post-message delay (5s default, 1s lite, env override 0..60s) before
54
+ // force-killing a backend that delivered agent_message but not
55
+ // turn.completed
56
+ // - stderr noise filter (10 substrings)
57
+ // - `~/.claude/settings.json` env injection
58
+ // - Cross-platform process termination (Windows taskkill /T /F /PID, Unix
59
+ // SIGTERM + 5s SIGKILL fallback)
60
+ // - Exit codes: 0 ok, 1 generic, 124 timeout, 127 not-found, 130 SIGINT,
61
+ // passthrough otherwise
62
+ // - --version prints `codeagent-wrapper version 5.10.0` (matches
63
+ // installer.ts EXPECTED_BINARY_VERSION check)
64
+ //
65
+ // Out of scope (intentionally NOT ported, see spec §1.3 / §8.6):
66
+ // - --parallel / --full-output / ---TASK--- ---CONTENT--- protocol
67
+ // - --cleanup / log file generation / async logger / log rotation
68
+ // - WebServer / SSE streaming
69
+ // - Structured report extraction (coverage / files / tests metrics)
70
+ // - ASCII mode, wrapper symlink alias
71
+ //
72
+ // Dependencies: Node.js built-in modules only.
73
+ // =============================================================================
74
+
75
+ import { spawn } from 'node:child_process';
76
+ import { readFileSync, statSync } from 'node:fs';
77
+ import { homedir } from 'node:os';
78
+ import path from 'node:path';
79
+ import process from 'node:process';
80
+ import { Buffer } from 'node:buffer';
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Constants (mirror codeagent-wrapper/main.go:16, executor.go:28,
84
+ // parser.go:57, filter.go:9).
85
+ // ---------------------------------------------------------------------------
86
+ const VERSION = '5.10.0';
87
+ const WRAPPER_NAME = 'codeagent-wrapper';
88
+ const DEFAULT_WORKDIR = '.';
89
+ const DEFAULT_TIMEOUT_SEC = 7200; // 2h, matches Go defaultTimeout
90
+ const DEFAULT_BACKEND = 'codex';
91
+ const STDIN_SPECIAL_CHARS = '\n\\"\'`$'; // utils.go:22
92
+ const STDIN_LENGTH_THRESHOLD = 800; // utils.go:54
93
+ const POST_MESSAGE_DELAY_DEFAULT_MS = 5_000; // executor.go:36
94
+ const POST_MESSAGE_DELAY_LITE_MS = 1_000; // executor.go:31
95
+ const POST_MESSAGE_DELAY_MAX_SEC = 60; // executor.go:45
96
+ const FORCE_KILL_DELAY_MS = 5_000; // main.go:67 forceKillDelay=5s
97
+ const FALLBACK_EXIT_GRACE_MS = 2_000; // executor.go:1200 (+2s)
98
+ const STDOUT_DRAIN_TIMEOUT_MS = 100; // main.go:31
99
+ const JSON_LINE_MAX_BYTES = 10 * 1024 * 1024; // parser.go:59
100
+ const PROGRESS_SNIPPET_MAX_RUNES = 120; // parser.go:272
101
+ const MAX_CLAUDE_SETTINGS_BYTES = 1 << 20; // backend.go:39
102
+ const STDERR_TAIL_BYTES = 4 * 1024; // main.go:23
103
+
104
+ // filter.go:9-23
105
+ const STDERR_NOISE_PATTERNS = [
106
+ '[STARTUP]',
107
+ 'Session cleanup disabled',
108
+ 'Warning:',
109
+ '(node:',
110
+ '(Use `node --trace-warnings',
111
+ 'Loaded cached credentials',
112
+ 'Loading extension:',
113
+ 'YOLO mode is enabled',
114
+ '[WARN] Skipping unreadable directory',
115
+ 'supports tool updates. Listening for changes',
116
+ ];
117
+
118
+ const IS_WINDOWS = process.platform === 'win32';
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Tiny utilities
122
+ // ---------------------------------------------------------------------------
123
+ function envFlagEnabled(key) {
124
+ const raw = process.env[key];
125
+ if (raw === undefined) return false;
126
+ const v = String(raw).trim().toLowerCase();
127
+ return !(v === '' || v === '0' || v === 'false' || v === 'no' || v === 'off');
128
+ }
129
+
130
+ function parseBoolFlag(val, fallback) {
131
+ const v = String(val ?? '').trim().toLowerCase();
132
+ if (['1', 'true', 'yes', 'on'].includes(v)) return true;
133
+ if (['0', 'false', 'no', 'off'].includes(v)) return false;
134
+ return fallback;
135
+ }
136
+
137
+ function safeProgressSnippet(s, maxLen = PROGRESS_SNIPPET_MAX_RUNES) {
138
+ let str = (s ?? '').replace(/\n/g, ' ');
139
+ str = str.split(/\s+/).filter(Boolean).join(' ');
140
+ const runes = [...str];
141
+ if (maxLen <= 0 || runes.length <= maxLen) return str;
142
+ if (maxLen <= 3) return runes.slice(0, maxLen).join('');
143
+ return runes.slice(0, maxLen - 3).join('') + '...';
144
+ }
145
+
146
+ function quoteForProgress(s) { return JSON.stringify(s ?? ''); }
147
+
148
+ function normalizeWindowsPath(p) {
149
+ // utils.go:125 — only invoked when running on Windows.
150
+ let out = p.replace(/\\/g, '/');
151
+ const m = /^\/([a-zA-Z])\//.exec(out);
152
+ if (m) out = m[1].toUpperCase() + ':' + out.slice(2);
153
+ return out;
154
+ }
155
+
156
+ // Mirror Go exec.LookPath: Node child_process.spawn does not consult PATHEXT
157
+ // on Windows, so `spawn('codex')` ENOENTs even when codex.cmd is in PATH.
158
+ // Replace bare command names with their resolved absolute path before spawn.
159
+ function lookPath(cmd, opts = {}) {
160
+ const env = opts.env || process.env;
161
+ const platform = opts.platform || process.platform;
162
+ const stat = opts.statFn || statSync;
163
+ if (platform !== 'win32') return cmd;
164
+ if (path.isAbsolute(cmd) || cmd.includes('/') || cmd.includes('\\')) return cmd;
165
+ const pathExt = (env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
166
+ .split(';').map((s) => s.trim()).filter(Boolean);
167
+ // Mirror Go exec.LookPath: on Windows, an extensionless name like `codex` is
168
+ // never executable on its own — must match PATHEXT. Only when the name
169
+ // already contains a dot do we additionally try the raw form.
170
+ const hasDot = cmd.includes('.');
171
+ const candidates = hasDot
172
+ ? [cmd, ...pathExt.map((e) => cmd + e)]
173
+ : pathExt.map((e) => cmd + e);
174
+ const sep = platform === 'win32' ? ';' : ':';
175
+ const dirs = (env.PATH || env.Path || '').split(sep).filter(Boolean);
176
+ // Windows searches current directory first (CreateProcess), then PATH.
177
+ for (const dir of ['', ...dirs]) {
178
+ for (const c of candidates) {
179
+ const full = dir ? path.join(dir, c) : c;
180
+ try {
181
+ const info = stat(full);
182
+ if (info && info.isFile && info.isFile()) return full;
183
+ } catch { /* not found, continue */ }
184
+ }
185
+ }
186
+ return cmd; // let spawn surface ENOENT
187
+ }
188
+
189
+ function expandHome(p) {
190
+ if (typeof p !== 'string') return p;
191
+ if (p === '~') return homedir();
192
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
193
+ return path.join(homedir(), p.slice(2));
194
+ }
195
+ return p;
196
+ }
197
+
198
+ function logWarn(msg) { process.stderr.write(`[WARN] ${msg}\n`); }
199
+ function logError(msg) { process.stderr.write(`[ERROR] ${msg}\n`); }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // ROLE_FILE injection (utils.go:75)
203
+ // ---------------------------------------------------------------------------
204
+ function injectRoleFile(taskText) {
205
+ return taskText.replace(/^ROLE_FILE:\s*(.+)$/gm, (match, rawPath) => {
206
+ let filePath = rawPath.trim();
207
+ filePath = expandHome(filePath);
208
+ if (IS_WINDOWS) filePath = normalizeWindowsPath(filePath);
209
+ try {
210
+ return readFileSync(filePath, 'utf8');
211
+ } catch (err) {
212
+ logWarn(`Failed to read ROLE_FILE '${filePath}': ${err.message}`);
213
+ return match; // preserve original line on read failure (utils.go:108)
214
+ }
215
+ });
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // `~/.claude/settings.json` env loader (backend.go:43)
220
+ // ---------------------------------------------------------------------------
221
+ function loadMinimalEnvSettings() {
222
+ let home;
223
+ try { home = homedir(); } catch { return {}; }
224
+ if (!home) return {};
225
+ const settingsPath = path.join(home, '.claude', 'settings.json');
226
+ let info;
227
+ try { info = statSync(settingsPath); } catch { return {}; }
228
+ if (!info || info.size > MAX_CLAUDE_SETTINGS_BYTES) return {};
229
+ let data;
230
+ try { data = readFileSync(settingsPath, 'utf8'); } catch { return {}; }
231
+ let parsed;
232
+ try { parsed = JSON.parse(data); } catch { return {}; }
233
+ const env = {};
234
+ if (parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object') {
235
+ for (const [k, v] of Object.entries(parsed.env)) {
236
+ if (typeof v === 'string') env[k] = v;
237
+ }
238
+ }
239
+ return env;
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Argument parsing (config.go:197)
244
+ // ---------------------------------------------------------------------------
245
+ function parseCliArgs(argv) {
246
+ let backend = DEFAULT_BACKEND;
247
+ let geminiModel = (process.env.GEMINI_MODEL || '').trim();
248
+ let progress = false;
249
+ let lite = envFlagEnabled('CODEAGENT_LITE_MODE');
250
+ let skipPermissions = envFlagEnabled('CODEAGENT_SKIP_PERMISSIONS');
251
+ const filtered = [];
252
+
253
+ for (let i = 0; i < argv.length; i++) {
254
+ const a = argv[i];
255
+ if (a === '--lite' || a === '-L') { lite = true; continue; }
256
+ if (a === '--progress') { progress = true; continue; }
257
+ if (a === '--backend') {
258
+ if (i + 1 >= argv.length) throw new Error('--backend flag requires a value');
259
+ backend = argv[++i];
260
+ continue;
261
+ }
262
+ if (a.startsWith('--backend=')) {
263
+ const v = a.slice('--backend='.length);
264
+ if (!v) throw new Error('--backend flag requires a value');
265
+ backend = v;
266
+ continue;
267
+ }
268
+ if (a === '--gemini-model') {
269
+ if (i + 1 >= argv.length) throw new Error('--gemini-model flag requires a non-empty model name');
270
+ const v = (argv[++i] || '').trim();
271
+ if (!v) throw new Error('--gemini-model flag requires a non-empty model name');
272
+ geminiModel = v;
273
+ continue;
274
+ }
275
+ if (a.startsWith('--gemini-model=')) {
276
+ const v = a.slice('--gemini-model='.length).trim();
277
+ if (!v) throw new Error('--gemini-model flag requires a non-empty model name');
278
+ geminiModel = v;
279
+ continue;
280
+ }
281
+ if (a === '--skip-permissions' || a === '--dangerously-skip-permissions') { skipPermissions = true; continue; }
282
+ if (a.startsWith('--skip-permissions=')) {
283
+ skipPermissions = parseBoolFlag(a.slice('--skip-permissions='.length), skipPermissions);
284
+ continue;
285
+ }
286
+ if (a.startsWith('--dangerously-skip-permissions=')) {
287
+ skipPermissions = parseBoolFlag(a.slice('--dangerously-skip-permissions='.length), skipPermissions);
288
+ continue;
289
+ }
290
+ filtered.push(a);
291
+ }
292
+
293
+ if (filtered.length === 0) throw new Error('task required');
294
+
295
+ const cfg = {
296
+ mode: 'new',
297
+ task: '',
298
+ sessionId: '',
299
+ workDir: DEFAULT_WORKDIR,
300
+ explicitStdin: false,
301
+ backend: (backend || DEFAULT_BACKEND).toLowerCase().trim(),
302
+ skipPermissions,
303
+ geminiModel,
304
+ progress,
305
+ lite,
306
+ };
307
+
308
+ if (filtered[0] === 'resume') {
309
+ if (filtered.length < 3) throw new Error('resume mode requires: resume <session_id> <task>');
310
+ cfg.mode = 'resume';
311
+ cfg.sessionId = (filtered[1] || '').trim();
312
+ if (!cfg.sessionId) throw new Error('resume mode requires non-empty session_id');
313
+ cfg.task = filtered[2];
314
+ cfg.explicitStdin = filtered[2] === '-';
315
+ if (filtered.length > 3) cfg.workDir = filtered[3];
316
+ } else {
317
+ cfg.task = filtered[0];
318
+ cfg.explicitStdin = filtered[0] === '-';
319
+ if (filtered.length > 1) cfg.workDir = filtered[1];
320
+ }
321
+
322
+ if (!['codex', 'gemini', 'claude'].includes(cfg.backend)) {
323
+ throw new Error(`unsupported backend "${cfg.backend}"`);
324
+ }
325
+ return cfg;
326
+ }
327
+
328
+ function shouldUseStdin(taskText, piped) {
329
+ if (piped) return true;
330
+ if (taskText.length > STDIN_LENGTH_THRESHOLD) return true;
331
+ for (const c of STDIN_SPECIAL_CHARS) if (taskText.includes(c)) return true;
332
+ return false;
333
+ }
334
+
335
+ function resolveTimeoutSec() {
336
+ const raw = (process.env.CODEX_TIMEOUT || '').trim();
337
+ if (!raw) return DEFAULT_TIMEOUT_SEC;
338
+ const n = Number.parseInt(raw, 10);
339
+ if (!Number.isFinite(n) || n <= 0) {
340
+ logWarn(`Invalid CODEX_TIMEOUT '${raw}', falling back to ${DEFAULT_TIMEOUT_SEC}s`);
341
+ return DEFAULT_TIMEOUT_SEC;
342
+ }
343
+ return n > 10000 ? Math.floor(n / 1000) : n;
344
+ }
345
+
346
+ function resolvePostMessageDelayMs(lite) {
347
+ if (lite) return POST_MESSAGE_DELAY_LITE_MS;
348
+ const raw = (process.env.CODEAGENT_POST_MESSAGE_DELAY || '').trim();
349
+ if (!raw) return POST_MESSAGE_DELAY_DEFAULT_MS;
350
+ const v = Number.parseInt(raw, 10);
351
+ if (!Number.isFinite(v) || v < 0) {
352
+ logWarn(`Invalid CODEAGENT_POST_MESSAGE_DELAY=${JSON.stringify(raw)}, falling back to 5s`);
353
+ return POST_MESSAGE_DELAY_DEFAULT_MS;
354
+ }
355
+ if (v > POST_MESSAGE_DELAY_MAX_SEC) {
356
+ logWarn(`CODEAGENT_POST_MESSAGE_DELAY=${v} exceeds 60s, capping at 60s`);
357
+ return POST_MESSAGE_DELAY_MAX_SEC * 1000;
358
+ }
359
+ return v * 1000;
360
+ }
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Backend argv builders (backend.go + executor.go:757 buildCodexArgs)
364
+ // ---------------------------------------------------------------------------
365
+ function buildCodexArgs(cfg, targetArg) {
366
+ const args = ['e'];
367
+ if (!envFlagEnabled('CODEX_REQUIRE_APPROVAL')) args.push('--dangerously-bypass-approvals-and-sandbox');
368
+ if (!envFlagEnabled('CODEX_DISABLE_SKIP_GIT_CHECK')) args.push('--skip-git-repo-check');
369
+ if (cfg.mode === 'resume') {
370
+ args.push('--json', 'resume', cfg.sessionId, targetArg);
371
+ return args;
372
+ }
373
+ args.push('-C', cfg.workDir, '--json', targetArg);
374
+ return args;
375
+ }
376
+
377
+ function buildClaudeArgs(cfg, targetArg) {
378
+ const args = ['-p'];
379
+ if (cfg.skipPermissions) args.push('--dangerously-skip-permissions');
380
+ args.push('--setting-sources', '');
381
+ if (cfg.mode === 'resume' && cfg.sessionId) args.push('-r', cfg.sessionId);
382
+ args.push('--output-format', 'stream-json', '--verbose', targetArg);
383
+ return args;
384
+ }
385
+
386
+ function buildGeminiArgs(cfg, targetArg) {
387
+ const args = [];
388
+ const model = (cfg.geminiModel || '').trim();
389
+ if (model) args.push('-m', model);
390
+ args.push('-o', 'stream-json', '-y');
391
+ if (cfg.mode === 'resume' && cfg.sessionId) args.push('-r', cfg.sessionId);
392
+ if (cfg.mode !== 'resume' && cfg.workDir) args.push('--include-directories', cfg.workDir);
393
+ if (targetArg !== '') args.push('-p', targetArg);
394
+ return args;
395
+ }
396
+
397
+ function backendCommandAndArgs(cfg, targetArg) {
398
+ switch (cfg.backend) {
399
+ case 'codex': return { command: 'codex', args: buildCodexArgs(cfg, targetArg) };
400
+ case 'claude': return { command: 'claude', args: buildClaudeArgs(cfg, targetArg) };
401
+ case 'gemini': return { command: 'gemini', args: buildGeminiArgs(cfg, targetArg) };
402
+ default: throw new Error(`unsupported backend "${cfg.backend}"`);
403
+ }
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Stderr noise filter (filter.go) — line-buffered.
408
+ // ---------------------------------------------------------------------------
409
+ function makeStderrFilter(target) {
410
+ let pending = '';
411
+ const tail = []; let tailLen = 0;
412
+ const appendTail = (s) => {
413
+ tail.push(s); tailLen += Buffer.byteLength(s, 'utf8');
414
+ while (tailLen > STDERR_TAIL_BYTES && tail.length > 1) {
415
+ tailLen -= Buffer.byteLength(tail.shift(), 'utf8');
416
+ }
417
+ };
418
+ const shouldFilter = (line) => STDERR_NOISE_PATTERNS.some((p) => line.includes(p));
419
+ return {
420
+ write(chunk) {
421
+ pending += chunk;
422
+ let idx;
423
+ while ((idx = pending.indexOf('\n')) !== -1) {
424
+ const line = pending.slice(0, idx + 1);
425
+ pending = pending.slice(idx + 1);
426
+ appendTail(line);
427
+ if (!shouldFilter(line)) target.write(line);
428
+ }
429
+ },
430
+ flush() {
431
+ if (!pending) return;
432
+ appendTail(pending);
433
+ if (!shouldFilter(pending)) target.write(pending);
434
+ pending = '';
435
+ },
436
+ tail() { return tail.join('').slice(-STDERR_TAIL_BYTES); },
437
+ };
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // JSON-line stream parser (parser.go).
442
+ //
443
+ // Yields a parsed { message, sessionId } and emits side-effect callbacks
444
+ // (onMessage / onComplete / onProgress / onSession) similar to
445
+ // parseJSONStreamInternalWithContent.
446
+ // ---------------------------------------------------------------------------
447
+ function makeJsonStreamParser({ onProgress, onSession, onMessage, onComplete }) {
448
+ let pending = Buffer.alloc(0);
449
+ let totalEvents = 0;
450
+ let codexMessage = '';
451
+ let claudeMessage = '';
452
+ const geminiBuffer = [];
453
+ let sessionId = '';
454
+
455
+ const emitProgress = (event, fields) => {
456
+ if (!onProgress) return;
457
+ const parts = [event];
458
+ if (fields) {
459
+ for (const key of ['id', 'text', 'cmd', 'exit', 'total_events']) {
460
+ const v = fields[key];
461
+ if (v !== undefined && String(v).trim() !== '') parts.push(`${key}=${v}`);
462
+ }
463
+ }
464
+ onProgress(`[PROGRESS] ${parts.join(' ')}`);
465
+ };
466
+
467
+ const emitSession = (id) => {
468
+ if (!id) return;
469
+ if (!sessionId) sessionId = id;
470
+ if (onSession) onSession(id);
471
+ };
472
+
473
+ // codex agent_message text may be string or []string (parser.go:522)
474
+ const normalizeText = (t) => {
475
+ if (typeof t === 'string') return t;
476
+ if (Array.isArray(t)) return t.filter((x) => typeof x === 'string').join('');
477
+ return '';
478
+ };
479
+
480
+ const handleLine = (rawLine) => {
481
+ let line = rawLine.trim();
482
+ if (!line) return;
483
+ if (Buffer.byteLength(line, 'utf8') > JSON_LINE_MAX_BYTES) {
484
+ logWarn(`Skipped overlong JSON line (> ${JSON_LINE_MAX_BYTES} bytes)`);
485
+ return;
486
+ }
487
+ totalEvents++;
488
+
489
+ let evt;
490
+ try { evt = JSON.parse(line); }
491
+ catch (_) {
492
+ // Gemini init line may be prefixed with MCP banner text (parser.go:178)
493
+ const idx = line.indexOf('{');
494
+ if (idx > 0) {
495
+ try { evt = JSON.parse(line.slice(idx)); }
496
+ catch (_e2) { return; }
497
+ } else { return; }
498
+ }
499
+ if (!evt || typeof evt !== 'object') return;
500
+
501
+ // Session id from snake_case OR camelCase (parser.go:97)
502
+ const evtSession = evt.session_id || evt.sessionId || '';
503
+ if (evtSession && !sessionId) emitSession(evtSession);
504
+
505
+ const itemType = evt.item && typeof evt.item === 'object' ? evt.item.type : '';
506
+ const isCodex = !!evt.thread_id || evt.type === 'turn.completed' || evt.type === 'turn.started' || (evt.item && itemType);
507
+ const isClaude = (evt.subtype !== undefined && evt.subtype !== '') || (evt.result !== undefined && evt.result !== '')
508
+ || (evt.type === 'result' && evtSession && evt.status === undefined);
509
+ const isGemini = (evt.role !== undefined && evt.role !== '')
510
+ || evt.delta !== undefined
511
+ || (evt.status !== undefined && evt.status !== '')
512
+ || (evt.type === 'init' && evtSession);
513
+
514
+ if (isCodex) {
515
+ switch (evt.type) {
516
+ case 'thread.started':
517
+ if (evt.thread_id) emitSession(evt.thread_id);
518
+ emitProgress('session_started', { id: sessionId });
519
+ break;
520
+ case 'turn.started':
521
+ emitProgress('turn_started');
522
+ break;
523
+ case 'thread.completed':
524
+ case 'turn.completed': {
525
+ if (evt.thread_id && !sessionId) emitSession(evt.thread_id);
526
+ const ev = evt.type === 'thread.completed' ? 'session_completed' : 'turn_completed';
527
+ emitProgress(ev, { total_events: totalEvents });
528
+ if (onComplete) onComplete();
529
+ break;
530
+ }
531
+ case 'item.completed': {
532
+ if (itemType === 'agent_message' || itemType === 'reasoning') {
533
+ const text = normalizeText(evt.item && evt.item.text);
534
+ if (text) {
535
+ if (itemType === 'agent_message') {
536
+ codexMessage = text;
537
+ if (onMessage) onMessage();
538
+ emitProgress('message', { text: quoteForProgress(safeProgressSnippet(text)) });
539
+ } else {
540
+ emitProgress('reasoning', { text: quoteForProgress(safeProgressSnippet(text)) });
541
+ }
542
+ }
543
+ } else if (itemType === 'command_execution') {
544
+ const cmdItem = evt.item || {};
545
+ const fields = { cmd: quoteForProgress(safeProgressSnippet(cmdItem.command || '')) };
546
+ if (cmdItem.exit_code !== undefined && cmdItem.exit_code !== null) fields.exit = cmdItem.exit_code;
547
+ emitProgress('cmd_done', fields);
548
+ } else if (itemType === 'mcp_tool_call') {
549
+ emitProgress('mcp_call');
550
+ }
551
+ break;
552
+ }
553
+ }
554
+ return;
555
+ }
556
+
557
+ if (isClaude) {
558
+ if (typeof evt.result === 'string' && evt.result !== '') {
559
+ claudeMessage = evt.result;
560
+ if (onMessage) onMessage();
561
+ }
562
+ if (evt.type === 'result' && onComplete) onComplete();
563
+ return;
564
+ }
565
+
566
+ if (isGemini) {
567
+ if (typeof evt.content === 'string' && evt.content !== '') geminiBuffer.push(evt.content);
568
+ if (evt.status) {
569
+ if (onMessage) onMessage();
570
+ if (evt.type === 'result' && ['success', 'error', 'complete', 'failed'].includes(evt.status) && onComplete) onComplete();
571
+ }
572
+ return;
573
+ }
574
+ // unknown event — ignore
575
+ };
576
+
577
+ return {
578
+ feed(buf) {
579
+ pending = pending.length === 0 ? buf : Buffer.concat([pending, buf]);
580
+ let nlIdx;
581
+ while ((nlIdx = pending.indexOf(0x0a)) !== -1) {
582
+ const lineBuf = pending.subarray(0, nlIdx);
583
+ pending = pending.subarray(nlIdx + 1);
584
+ handleLine(lineBuf.toString('utf8'));
585
+ }
586
+ },
587
+ end() {
588
+ if (pending.length > 0) {
589
+ handleLine(pending.toString('utf8'));
590
+ pending = Buffer.alloc(0);
591
+ }
592
+ },
593
+ result() {
594
+ let message;
595
+ if (geminiBuffer.length > 0) message = geminiBuffer.join('');
596
+ else if (claudeMessage) message = claudeMessage;
597
+ else message = codexMessage;
598
+ return { message, sessionId };
599
+ },
600
+ };
601
+ }
602
+
603
+ // ---------------------------------------------------------------------------
604
+ // Process termination (executor.go:1421 killProcessTree, terminateCommand).
605
+ // ---------------------------------------------------------------------------
606
+ function killWindowsTree(pid) {
607
+ try {
608
+ const r = spawn('taskkill', ['/T', '/F', '/PID', String(pid)], {
609
+ stdio: 'ignore',
610
+ windowsHide: true,
611
+ });
612
+ r.on('error', () => {});
613
+ } catch { /* ignore */ }
614
+ }
615
+
616
+ function terminateChild(child, { force } = { force: false }) {
617
+ if (!child || child.killed || child.exitCode !== null) return;
618
+ if (IS_WINDOWS) {
619
+ killWindowsTree(child.pid);
620
+ return;
621
+ }
622
+ try { child.kill(force ? 'SIGKILL' : 'SIGTERM'); } catch { /* ignore */ }
623
+ }
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // Read full stdin into a UTF-8 string.
627
+ // ---------------------------------------------------------------------------
628
+ function readAllStdin() {
629
+ return new Promise((resolve, reject) => {
630
+ const chunks = [];
631
+ process.stdin.on('data', (d) => chunks.push(d));
632
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
633
+ process.stdin.on('error', reject);
634
+ });
635
+ }
636
+
637
+ function isStdinPiped() {
638
+ try { return !process.stdin.isTTY; } catch { return false; }
639
+ }
640
+
641
+ // ---------------------------------------------------------------------------
642
+ // Help / version
643
+ // ---------------------------------------------------------------------------
644
+ function printHelp() {
645
+ process.stdout.write(`${WRAPPER_NAME} - Node shim for AI CLI backends (replaces Go binary v${VERSION})
646
+
647
+ Usage:
648
+ ${WRAPPER_NAME} [--backend codex|gemini|claude] [--gemini-model NAME] [--progress] [--lite] "task" [workdir]
649
+ ${WRAPPER_NAME} [flags] - [workdir] Read task from stdin
650
+ ${WRAPPER_NAME} [flags] resume <session_id> "task" [workdir]
651
+ ${WRAPPER_NAME} [flags] resume <session_id> - [workdir]
652
+ ${WRAPPER_NAME} --version
653
+ ${WRAPPER_NAME} --help
654
+
655
+ Exit codes: 0 ok | 1 error | 124 timeout | 127 not-found | 130 SIGINT | else passthrough
656
+ `);
657
+ }
658
+
659
+ // ---------------------------------------------------------------------------
660
+ // Main
661
+ // ---------------------------------------------------------------------------
662
+ async function main() {
663
+ const argv = process.argv.slice(2);
664
+
665
+ if (argv.length === 0) { printHelp(); return 1; }
666
+ const first = argv[0];
667
+ if (first === '--version' || first === '-v') {
668
+ process.stdout.write(`${WRAPPER_NAME} version ${VERSION}\n`);
669
+ return 0;
670
+ }
671
+ if (first === '--help' || first === '-h') { printHelp(); return 0; }
672
+
673
+ let cfg;
674
+ try { cfg = parseCliArgs(argv); }
675
+ catch (e) { logError(e.message); return 1; }
676
+
677
+ if (cfg.geminiModel && cfg.backend !== 'gemini') {
678
+ logWarn('--gemini-model parameter is only effective with --backend gemini');
679
+ }
680
+
681
+ const timeoutSec = resolveTimeoutSec();
682
+
683
+ // Resolve task text -------------------------------------------------------
684
+ const piped = isStdinPiped();
685
+ let taskText;
686
+ if (cfg.explicitStdin) {
687
+ const data = await readAllStdin();
688
+ if (!data) { logError('Explicit stdin mode requires task input from stdin'); return 1; }
689
+ taskText = data;
690
+ } else if (piped) {
691
+ const data = await readAllStdin();
692
+ taskText = data || cfg.task;
693
+ } else {
694
+ taskText = cfg.task;
695
+ }
696
+ taskText = injectRoleFile(taskText);
697
+
698
+ const useStdin = cfg.explicitStdin || shouldUseStdin(taskText, piped);
699
+
700
+ // targetArg switch (executor.go:864)
701
+ const geminiDirect = useStdin && cfg.backend === 'gemini' && !IS_WINDOWS;
702
+ const geminiStdinPipe = useStdin && cfg.backend === 'gemini' && IS_WINDOWS;
703
+ let targetArg = taskText;
704
+ if (useStdin && !geminiDirect && !geminiStdinPipe) targetArg = '-';
705
+ if (geminiStdinPipe) targetArg = '';
706
+
707
+ const { command, args } = backendCommandAndArgs(cfg, targetArg);
708
+
709
+ // Startup banner (main.go:432)
710
+ process.stderr.write(
711
+ `[${WRAPPER_NAME}]\n` +
712
+ ` Backend: ${cfg.backend}\n` +
713
+ ` Command: ${command} ${args.join(' ')}\n` +
714
+ ` PID: ${process.pid}\n` +
715
+ ` Log: <stderr>\n`,
716
+ );
717
+
718
+ // Spawn ------------------------------------------------------------------
719
+ const env = { ...process.env, ...loadMinimalEnvSettings() };
720
+ const spawnOpts = {
721
+ stdio: ['pipe', 'pipe', 'pipe'],
722
+ windowsHide: true,
723
+ env,
724
+ };
725
+ // Codex passes workdir via -C flag — don't set Dir (executor.go:1001).
726
+ if (cfg.mode !== 'resume' && cfg.workDir && cfg.backend !== 'codex') {
727
+ spawnOpts.cwd = cfg.workDir;
728
+ }
729
+
730
+ let resolvedCommand = lookPath(command);
731
+ let resolvedArgs = args;
732
+ // Windows: spawning .cmd/.bat directly throws EINVAL (Node CVE-2024-27980
733
+ // mitigation). Wrap with cmd.exe /c to keep arg array semantics without
734
+ // tripping DEP0190 (`shell:true + args[]` deprecation in Node 24+).
735
+ if (IS_WINDOWS && /\.(cmd|bat)$/i.test(resolvedCommand)) {
736
+ spawnOpts.windowsVerbatimArguments = true;
737
+ resolvedArgs = ['/d', '/s', '/c', `"${resolvedCommand}"`, ...args];
738
+ resolvedCommand = process.env.ComSpec || 'cmd.exe';
739
+ }
740
+
741
+ let child;
742
+ try { child = spawn(resolvedCommand, resolvedArgs, spawnOpts); }
743
+ catch (e) {
744
+ logError(`failed to start ${command}: ${e.message}`);
745
+ return 1;
746
+ }
747
+
748
+ let spawnErrored = false;
749
+ child.on('error', (err) => {
750
+ spawnErrored = true;
751
+ if (err && err.code === 'ENOENT') {
752
+ logError(`${command} command not found in PATH`);
753
+ mainExitCode = 127;
754
+ } else {
755
+ logError(`failed to start ${command}: ${err && err.message ? err.message : String(err)}`);
756
+ mainExitCode = 1;
757
+ }
758
+ });
759
+
760
+ // Stdin -------------------------------------------------------------------
761
+ // For non-gemini-direct stdin path, write taskText then close.
762
+ if (useStdin && !geminiDirect && child.stdin) {
763
+ child.stdin.on('error', () => { /* swallow EPIPE */ });
764
+ child.stdin.end(taskText, 'utf8');
765
+ } else if (child.stdin && !useStdin) {
766
+ // Even when not piping a task, ensure the child's stdin is closed so it
767
+ // does not block waiting for input.
768
+ child.stdin.end();
769
+ }
770
+
771
+ // Stderr filter -----------------------------------------------------------
772
+ const stderrFilter = makeStderrFilter(process.stderr);
773
+ child.stderr.setEncoding('utf8');
774
+ child.stderr.on('data', (chunk) => stderrFilter.write(chunk));
775
+
776
+ // Stdout JSON parsing -----------------------------------------------------
777
+ let messageSeen = false;
778
+ let completeSeen = false;
779
+ let postMessageTimer = null;
780
+ let fallbackExitTimer = null;
781
+ let forceKillTimer = null;
782
+ let forcedAfterComplete = false;
783
+ let sessionEmitted = false;
784
+ let sessionId = '';
785
+
786
+ const onSession = (id) => {
787
+ if (sessionEmitted || !id) return;
788
+ sessionEmitted = true;
789
+ sessionId = id;
790
+ process.stderr.write(` Session-ID: ${id}\n`);
791
+ };
792
+
793
+ const startPostMessageTimer = () => {
794
+ if (postMessageTimer) return;
795
+ postMessageTimer = setTimeout(() => {
796
+ postMessageTimer = null;
797
+ forcedAfterComplete = true;
798
+ // Close stdout BEFORE killing on Windows so cmd.Wait()-equivalent
799
+ // (the 'exit' / 'close' events) is unblocked (executor.go:1190).
800
+ try { child.stdout.destroy(); } catch { /* ignore */ }
801
+ terminateChild(child);
802
+ // Schedule force-kill (5s on Unix; immediate is no-op on Windows since
803
+ // taskkill /F already happened above).
804
+ if (!IS_WINDOWS && !forceKillTimer) {
805
+ forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
806
+ }
807
+ // Fallback exit timer (executor.go:1199): if 'exit' never fires, bail.
808
+ if (!fallbackExitTimer) {
809
+ fallbackExitTimer = setTimeout(() => {
810
+ fallbackExitTimer = null;
811
+ finalize({ forced: true });
812
+ }, FORCE_KILL_DELAY_MS + FALLBACK_EXIT_GRACE_MS);
813
+ }
814
+ }, resolvePostMessageDelayMs(cfg.lite));
815
+ };
816
+
817
+ const parser = makeJsonStreamParser({
818
+ onProgress: cfg.progress ? (line) => process.stderr.write(line + '\n') : undefined,
819
+ onSession,
820
+ onMessage: () => { messageSeen = true; },
821
+ onComplete: () => {
822
+ completeSeen = true;
823
+ // post-message delay window opens when we observe completion
824
+ // (executor.go:1210 — but post-delay timer started after the FIRST
825
+ // completion event regardless of message arrival).
826
+ startPostMessageTimer();
827
+ },
828
+ });
829
+ child.stdout.on('data', (chunk) => parser.feed(chunk));
830
+ child.stdout.on('end', () => parser.end());
831
+
832
+ // Signal handling ---------------------------------------------------------
833
+ let externalSignal = null;
834
+ const installSignalHandlers = () => {
835
+ const onSig = (sig) => {
836
+ externalSignal = sig;
837
+ terminateChild(child);
838
+ if (!IS_WINDOWS && !forceKillTimer) {
839
+ forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
840
+ }
841
+ };
842
+ process.on('SIGINT', () => onSig('SIGINT'));
843
+ process.on('SIGTERM', () => onSig('SIGTERM'));
844
+ };
845
+ installSignalHandlers();
846
+
847
+ // Timeout -----------------------------------------------------------------
848
+ let timedOut = false;
849
+ const timeoutHandle = setTimeout(() => {
850
+ timedOut = true;
851
+ terminateChild(child);
852
+ if (!IS_WINDOWS && !forceKillTimer) {
853
+ forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
854
+ }
855
+ }, timeoutSec * 1000);
856
+
857
+ // Wait for exit + finalize ------------------------------------------------
858
+ let mainExitCode = 0;
859
+ let finalized = false;
860
+ let resolveDone;
861
+ const done = new Promise((r) => { resolveDone = r; });
862
+
863
+ const finalize = ({ forced = false } = {}) => {
864
+ if (finalized) return;
865
+ finalized = true;
866
+ clearTimeout(timeoutHandle);
867
+ if (postMessageTimer) clearTimeout(postMessageTimer);
868
+ if (fallbackExitTimer) clearTimeout(fallbackExitTimer);
869
+ if (forceKillTimer) clearTimeout(forceKillTimer);
870
+
871
+ // Drain any tail bytes from stdout/stderr.
872
+ parser.end();
873
+ stderrFilter.flush();
874
+ resolveDone(forced);
875
+ };
876
+
877
+ child.on('exit', () => {
878
+ // Allow stdout 'end' event to arrive before parsing the result so we don't
879
+ // miss a trailing turn.completed event.
880
+ setTimeout(finalize, STDOUT_DRAIN_TIMEOUT_MS);
881
+ });
882
+ child.on('close', () => setTimeout(finalize, STDOUT_DRAIN_TIMEOUT_MS));
883
+
884
+ await done;
885
+
886
+ // Determine exit code -----------------------------------------------------
887
+ const { message, sessionId: parsedSession } = parser.result();
888
+ const finalSession = sessionId || parsedSession;
889
+
890
+ if (spawnErrored) {
891
+ return mainExitCode || 1;
892
+ }
893
+ if (externalSignal) return 130;
894
+ if (timedOut) return 124;
895
+
896
+ const childExit = child.exitCode;
897
+ const childSig = child.signalCode;
898
+
899
+ // forcedAfterComplete + non-empty message -> success (executor.go:1286)
900
+ if (forcedAfterComplete && message) {
901
+ process.stdout.write(message);
902
+ if (finalSession) process.stdout.write(`\n---\nSESSION_ID: ${finalSession}\n`);
903
+ return 0;
904
+ }
905
+
906
+ if (childExit !== null && childExit !== 0) {
907
+ // Recent stderr tail for diagnostics
908
+ const tail = stderrFilter.tail();
909
+ if (tail) process.stderr.write(`\n=== Recent Errors ===\n${tail}`);
910
+ return childExit;
911
+ }
912
+ if (childSig === 'SIGINT' || childSig === 'SIGTERM') return 130;
913
+
914
+ if (!message) {
915
+ logError(`${cfg.backend} completed without agent_message output`);
916
+ return 1;
917
+ }
918
+
919
+ process.stdout.write(message);
920
+ if (finalSession) process.stdout.write(`\n---\nSESSION_ID: ${finalSession}\n`);
921
+ return 0;
922
+ }
923
+
924
+ // ---------------------------------------------------------------------------
925
+ // Entry point — only run when invoked as a script, so tests can import helpers.
926
+ // ---------------------------------------------------------------------------
927
+ const isMainModule = (() => {
928
+ try {
929
+ const entry = process.argv[1] && path.resolve(process.argv[1]);
930
+ const self = new URL(import.meta.url).pathname;
931
+ const selfNorm = process.platform === 'win32'
932
+ ? path.resolve(self.replace(/^\//, ''))
933
+ : path.resolve(self);
934
+ return entry && entry === selfNorm;
935
+ } catch { return true; }
936
+ })();
937
+
938
+ if (isMainModule) {
939
+ main().then((code) => {
940
+ // Flush stdout (Windows Git Bash bug, main.go:496).
941
+ if (process.stdout.write('')) process.exit(code);
942
+ else process.stdout.once('drain', () => process.exit(code));
943
+ }).catch((err) => {
944
+ logError(err && err.stack ? err.stack : String(err));
945
+ process.exit(1);
946
+ });
947
+ }
948
+
949
+ export { lookPath };