Compare commits

..

9 Commits

Author SHA1 Message Date
Dominik Kundel d0423ccd3f fix: respect SHELL on Windows for Git Bash 2026-04-07 19:53:21 -07:00
VOIDXAI 8e403f9d4b tests: reduce background task timing flakiness (#37)
Co-authored-by: VOIDXAI <VOIDXAI@users.noreply.github.com>
2026-03-31 13:04:24 -07:00
Dominik Kundel 6360899e1c chore: bump plugin version to 1.0.2 (#74) 2026-03-31 12:59:31 -07:00
Dominik Kundel 65486f2878 ci: add pull request workflow for tests and build (#73) 2026-03-31 12:56:09 -07:00
Pranav 64dc174d83 Fix /codex:rescue AskUserQuestion contract (#43) 2026-03-31 12:44:30 -07:00
ZETA a1266348d6 fix: make test suite portable across platforms (#56)
* fix: make test suite portable across platforms

Replace hardcoded macOS path with fileURLToPath for cross-platform
compatibility. Add .cmd wrapper creation and platform-aware PATH
separator in test fixtures so the fake codex binary is discoverable
on Windows. Add shell and windowsHide options to the test helper
run() function to match production behavior.

Test results on Windows improve from 12/64 pass to 59/64 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip shell for absolute-path commands to avoid Windows space-in-path breakage

When `process.execPath` resolves to a path with spaces (e.g.,
`C:\Program Files\nodejs\node.exe`), `shell: true` causes cmd.exe
to split the path at the space. Guard with `path.isAbsolute()` so
only bare command names (which need `.cmd` shim resolution) use the
shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Dominik Kundel <dkundel@openai.com>
2026-03-31 12:40:08 -07:00
ZETA 1a79ae57ec fix: resolve Windows ENOENT when spawning codex app-server (#55)
* fix: add shell and windowsHide options for Windows spawn in app-server

On Windows, spawn("codex", ["app-server"]) fails with ENOENT because
Node.js cannot resolve .cmd shims without shell: true. This adds
platform-gated shell and windowsHide options to the app-server spawn
call, and uses terminateProcessTree for proper process tree cleanup
since shell: true wraps the child in cmd.exe.

Without terminateProcessTree, plain SIGTERM only kills cmd.exe and
leaves the actual codex node process orphaned — verified with 274+
zombie node.exe processes accumulating on Windows.

Fixes #32
Fixes #46

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard terminateProcessTree against PID reuse after process exit

ChildProcess.killed only reflects whether .kill() was called by this
process — it stays false when the child exits on its own. On Windows,
where PIDs are recycled quickly, the 50 ms timer could fire after
cmd.exe has exited and its PID has been reassigned, causing taskkill
to terminate an unrelated process.

Add an exitCode === null check so the tree-kill path is skipped once
the child has already exited.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:39:32 -07:00
VOIDXAI 19642ad95f tests: derive repo roots from test file locations (#34)
Co-authored-by: VOIDXAI <VOIDXAI@users.noreply.github.com>
2026-03-31 12:33:32 -07:00
Dominik Kundel 9cb4fe4099 Bump v1.0.1 (#22)
* chore: bump plugin version to 1.0.1

* chore: bump plugin metadata version to 1.0.1
2026-03-30 19:23:08 -07:00
12 changed files with 85 additions and 18 deletions
+2 -2
View File
@@ -5,13 +5,13 @@
},
"metadata": {
"description": "Codex plugins to use in Claude Code for delegation and code review.",
"version": "1.0.1"
"version": "1.0.2"
},
"plugins": [
{
"name": "codex",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"version": "1.0.1",
"version": "1.0.2",
"author": {
"name": "OpenAI"
},
+35
View File
@@ -0,0 +1,35 @@
name: Pull Request CI
on:
pull_request:
permissions:
contents: read
jobs:
ci:
name: CI
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Codex CLI
run: npm install -g @openai/codex
- name: Run test suite
run: npm test
- name: Run build
run: npm run build
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@openai/codex-plugin-cc",
"version": "1.0.1",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openai/codex-plugin-cc",
"version": "1.0.1",
"version": "1.0.2",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^25.5.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openai/codex-plugin-cc",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codex",
"version": "1.0.1",
"version": "1.0.2",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"author": {
"name": "OpenAI"
+1 -1
View File
@@ -2,7 +2,7 @@
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
argument-hint: "[--background|--wait] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [what Codex should investigate, solve, or continue]"
context: fork
allowed-tools: Bash(node:*)
allowed-tools: Bash(node:*), AskUserQuestion
---
Route this request to the `codex:codex-rescue` subagent.
+18 -3
View File
@@ -14,6 +14,7 @@ import { spawn } from "node:child_process";
import readline from "node:readline";
import { parseBrokerEndpoint } from "./broker-endpoint.mjs";
import { ensureBrokerSession } from "./broker-lifecycle.mjs";
import { terminateProcessTree } from "./process.mjs";
const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url);
const PLUGIN_MANIFEST = JSON.parse(fs.readFileSync(PLUGIN_MANIFEST_URL, "utf8"));
@@ -188,7 +189,9 @@ class SpawnedCodexAppServerClient extends AppServerClientBase {
this.proc = spawn("codex", ["app-server"], {
cwd: this.cwd,
env: this.options.env,
stdio: ["pipe", "pipe", "pipe"]
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
windowsHide: true
});
this.proc.stdout.setEncoding("utf8");
@@ -237,8 +240,20 @@ class SpawnedCodexAppServerClient extends AppServerClientBase {
if (this.proc && !this.proc.killed) {
this.proc.stdin.end();
setTimeout(() => {
if (this.proc && !this.proc.killed) {
this.proc.kill("SIGTERM");
if (this.proc && !this.proc.killed && this.proc.exitCode === null) {
// On Windows with shell: true, the direct child is cmd.exe.
// Use terminateProcessTree to kill the entire tree including
// the grandchild node process.
if (process.platform === "win32") {
try {
terminateProcessTree(this.proc.pid);
} catch {
// Best-effort cleanup inside an unref'd timer — swallow errors
// to avoid crashing the host process during shutdown.
}
} else {
this.proc.kill("SIGTERM");
}
}
}, 50).unref?.();
}
+2 -1
View File
@@ -8,7 +8,8 @@ export function runCommand(command, args = [], options = {}) {
encoding: "utf8",
input: options.input,
stdio: options.stdio ?? "pipe",
shell: process.platform === "win32"
shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
windowsHide: true
});
return {
+3 -1
View File
@@ -2,8 +2,9 @@ import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
const ROOT = "/Users/dkundel/code/codex-plugin";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex");
function read(relativePath) {
@@ -89,6 +90,7 @@ test("rescue command absorbs continue semantics", () => {
const runtimeSkill = read("skills/codex-cli-runtime/SKILL.md");
assert.match(rescue, /The final user-visible response must be Codex's output verbatim/i);
assert.match(rescue, /allowed-tools:\s*Bash\(node:\*\),\s*AskUserQuestion/);
assert.match(rescue, /--background\|--wait/);
assert.match(rescue, /--resume\|--fresh/);
assert.match(rescue, /--model <model\|spark>/);
+12 -2
View File
@@ -1,4 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { writeExecutable } from "./helpers.mjs";
@@ -465,7 +467,7 @@ rl.on("line", (line) => {
}
}
send({ method: "turn/completed", params: { threadId: thread.id, turn: buildTurn(turnId, "completed") } });
}, 400);
}, 5000);
interruptibleTurns.set(turnId, { threadId: thread.id, timer });
} else if (BEHAVIOR === "slow-task") {
emitTurnCompletedLater(thread.id, turnId, items, 400);
@@ -507,11 +509,19 @@ rl.on("line", (line) => {
});
`;
writeExecutable(scriptPath, source);
// On Windows, npm global binaries are invoked via .cmd wrappers.
// Create a codex.cmd so the fake binary is discoverable by spawn with shell: true.
if (process.platform === "win32") {
const cmdWrapper = `@echo off\r\nnode "%~dp0codex" %*\r\n`;
fs.writeFileSync(path.join(binDir, "codex.cmd"), cmdWrapper, { encoding: "utf8" });
}
}
export function buildEnv(binDir) {
const sep = process.platform === "win32" ? ";" : ":";
return {
...process.env,
PATH: `${binDir}:${process.env.PATH}`
PATH: `${binDir}${sep}${process.env.PATH}`
};
}
+4 -1
View File
@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
export function makeTempDir(prefix = "codex-plugin-test-") {
@@ -16,7 +17,9 @@ export function run(command, args, options = {}) {
cwd: options.cwd,
env: options.env,
encoding: "utf8",
input: options.input
input: options.input,
shell: process.platform === "win32" && !path.isAbsolute(command),
windowsHide: true
});
}
+4 -3
View File
@@ -3,13 +3,14 @@ import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs";
import { initGitRepo, makeTempDir, run } from "./helpers.mjs";
import { loadBrokerSession } from "../plugins/codex/scripts/lib/broker-lifecycle.mjs";
import { resolveStateDir } from "../plugins/codex/scripts/lib/state.mjs";
const ROOT = "/Users/dkundel/code/codex-plugin";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex");
const SCRIPT = path.join(PLUGIN_ROOT, "scripts", "codex-companion.mjs");
const STOP_HOOK = path.join(PLUGIN_ROOT, "scripts", "stop-review-gate-hook.mjs");
@@ -553,7 +554,7 @@ test("task --background enqueues a detached worker and exposes per-job status",
const waitedStatus = run(
"node",
[SCRIPT, "status", launchPayload.jobId, "--wait", "--timeout-ms", "5000", "--json"],
[SCRIPT, "status", launchPayload.jobId, "--wait", "--timeout-ms", "15000", "--json"],
{
cwd: repo,
env: buildEnv(binDir)
@@ -1275,7 +1276,7 @@ test("cancel sends turn interrupt to the shared app-server before killing a brok
return job;
}
return null;
});
}, { timeoutMs: 15000 });
const cancelResult = run("node", [SCRIPT, "cancel", jobId, "--json"], {
cwd: repo,