Skip to content

release: v1.4.0 - #8

Merged
CBEPX merged 43 commits into
mainfrom
release/v1.4.0
Sep 28, 2026
Merged

CBEPX merged 43 commits into
mainfrom
release/v1.4.0

Conversation

@CBEPX

@CBEPX CBEPX commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Summary

Release v1.4.0: engineering maturity plus Windows without $SHELL. The Windows CI matrix is now required.

Fixed

Added

  • SECURITY.md (supported versions, GitHub Security Advisories reporting) (Create SECURITY.md for security policy openai/codex-plugin-cc#326).
  • Node 24 development tooling: ESLint, a typecheck:tests script over tests/** and scripts/** (checkJs is off for now — turning it on surfaced 54 pre-existing findings, deferred), c8 coverage (npm run test:coverage, thresholds in .c8rc.json), Stryker mutation testing over the critical args.mjs/model-catalog.mjs pair (npm run test:mutation:critical), a check:changelog gate that keeps CHANGELOG.md and plugins/codex/CHANGELOG.md byte-identical, Dependabot, and npm run setup:git-hooks (pre-commit lint + typecheck).
  • CI: one workflow run per SHA (push limited to main, release branches checked via manual workflow_dispatch, with a concurrency group cancelling superseded runs) and a new quality job on Node 24 alongside the existing OS × Node matrix.

Changed

  • The per-user fallback state root used when CLAUDE_PLUGIN_DATA is unset is now %LOCALAPPDATA%\codex-companion on Windows. A pre-1.4.0 root under %TEMP%\codex-companion-user keeps being used, with a one-line stderr notice, until it is removed by hand — nothing is migrated automatically. This transitional notice only applies to an unversioned/dev checkout: a marketplace install's state-root hash is derived from CLAUDE_PLUGIN_ROOT, which already includes the plugin version in the marketplace cache, so a normal upgrade never sees the legacy root.
  • BROKER_BUSY_RETRY_MS widened from 1000 to 3000 ms; the "teardown only after the broker confirmed idle" invariant is unchanged.
  • Deferred v1.3.0 review minors folded in: CODEX_REVIEW_GATE_MAX_ROUNDS rejects a non-integer, negative or otherwise malformed value (falls back to 3 with a warning) instead of misreading it; setup --review-gate-model "" / --review-gate-effort "" now fails with --<flag> needs a value; use inherit to clear it. instead of writing anything; a job id used to build the workerCommandLine match is now regex-escaped; the kill-failed broker-teardown reason is documented in the README's reason table. Also added (test coverage only, no behavior change): a catalogue-only model alias resolving correctly on a priority tie, and the fallback-root refusal covering a non-standard directory mode.

Known limitations

  • Kills issued from stored process records (/codex:cancel, SessionEnd cleanup, stale-broker replacement, broker teardown) are still refused on Windows (identity-unavailable); process identity verification is now targeted for v1.4.1, not v1.4.0 as previously stated. /codex:cancel still sends the turn interrupt on a best-effort basis; SessionEnd only refuses. A worker or broker left behind exits when its turn ends and, for the broker, once every client has disconnected and its idle timeout elapses — an unbounded turn is not reaped on Windows until v1.4.1.

Ported with reference to upstream PRs by mohammad-malik, mittalpk, stantheman0128, tmchow, D2758695161, ikbear, e345ee, tanakauo.

Gates

  • CI run 36374712852 on 088ada2: ubuntu/macos/windows x node 18/22/24 + quality all green; Windows jobs are required (continue-on-error removed).
  • Codex adversarial review: six passes (--effort max), passes 1-5 found real Windows executable-resolution paths (bare where.exe/taskkill/cmd via cwd, unresolved-name fallback, shim interpreter via cwd, where.exe code-page decoding, relative PATH entries) - all fixed; pass 6 = SHIP.
  • Local: npm run check 357 tests / 356 pass / 1 win32-only skip, 0 leaked test processes, claude plugin validate . --strict, npm audit --omit=dev 0 vulnerabilities, npm pack --dry-run 101 files.
  • SDD ledger and per-task reports: .superpowers/sdd/2026-09-28-codex-plugin-cc-v1.4.0/ in the release worktree (archived under docs/superpowers/reports/v1.4.0/ after merge).

🤖 Generated with Claude Code

CBEPX and others added 30 commits September 28, 2026 01:02
…ndows fixes)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…iscovery

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n contract, node 24 tooling, CI dedupe)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…de/signal expectations)

- .gitattributes forces LF; scripts/run-tests.mjs lists tests/*.test.mjs
  explicitly (Node 18/22/24, cmd.exe) and forwards extra CLI args.
- prebuild uses node's mkdirSync instead of POSIX mkdir -p.
- run() no longer defaults to a shell; tests spawn process.execPath.
- test-env uses fileURLToPath; commands.test normalises CRLF; transfer
  tests fake USERPROFILE via homeEnv(); broker-endpoint expectation uses
  the host path.join; the torn-state writer runs from a temp module.
- POSIX-only scenarios (mode bits, SIGTERM-immune/graceful-SIGTERM,
  pgrep, kill(-pid), npm-less PATH) are skipped or guarded on win32;
  awaited cancel accepts the documented cancellationPending on win32.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Group-kill cleanups (kill(-pid)) fall back to kill(pid) so a missing process
group (win32) never leaks the child. The awaited-cancel test keeps the text
cancel output on POSIX and uses --json only for the win32 assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On win32 a record without a worker identity is never signalled until v1.4.1:
cancel answers cancellationPending/identity-unavailable with exit 1,
SessionEnd keeps the record and says why, and ensureBrokerSession kills
nothing. The affected tests assert that documented outcome on win32; the
POSIX expectations are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…en reader

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…xpectation on win32

- state: the torn-state test is skipped on win32 (rename over an open reader
  is EPERM there); the product retry lands in Task 7, which un-skips it.
- runtime: the running job's own file now says `running`, as a live worker's
  does; the status-less fixture read as terminal on disk and the reaper
  reconciled that into the record SessionEnd kept on win32. The win32 branch
  asserts the kept record exactly; the awaited-cancel win32 branch asserts the
  whole refusal payload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndabot, SECURITY.md, changelog gate

- eslint flat config, tsconfig.tests.json (extends tsconfig.app-server.json;
  checkJs off for now: 54 errors in tests/)
- check:changelog accepts `## 1.3.0 — date` headings and requires CHANGELOG.md
  and plugins/codex/CHANGELOG.md to be identical
- c8 coverage via .c8rc.json (thresholds = measured - 2), Stryker critical shard
  over args.mjs and model-catalog.mjs with a weekly workflow
- model-catalog unit tests for source fallback, normalisation and caching
- pre-commit hook, dependabot, SECURITY.md, README Development section
- quality CI jobs run changelog, lint and tests typecheck; PR CI adds coverage

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…f the Stryker sandbox

- the erroring-codex case now carries a real catalogue so the error path is what
  is asserted; timeoutMs no longer pinned
- stryker ignorePatterns for .worktrees/docs/.superpowers/reports
- README: setup:git-hooks writes the shared .git/config and needs the codex CLI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A backslash escapes only a following quote, backslash or whitespace.
Before any other character it stays literal, so Windows paths survive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A trailing backslash is now appended inside the loop (nothing follows it to
escape), so the post-loop branch could never run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….exe launch for .cmd shims, direct taskkill/powershell

On win32 nothing runs through a shell any more. Git Bash as $SHELL (the
usual case on GitHub runners) mangled `taskkill /PID` and PowerShell
arguments.

- resolveExecutable: raw spawnSync("where.exe") (shell:false, 5s timeout),
  first hit whose extension is in PATHEXT (.com/.exe/.bat/.cmd); the
  extensionless bash shim is skipped; nothing found -> null, and the bare
  name is spawned so ENOENT surfaces as before.
- buildLaunch: .exe/.com run directly; .cmd/.bat run as
  `cmd.exe /d /s /c "<line>"` with windowsVerbatimArguments. quoteForCmd
  applies CRT quoting, then caret-escapes the cmd metacharacters twice (once
  for cmd /c, once for the shim's %* re-parse), as cross-spawn does for
  cmd-shims, so `a&b` and `%PATH%` stay literal.
- runCommand: shell:false on win32; paths and explicit extensions skip
  where.exe. Posix is unchanged.
- terminateProcessTree spawns taskkill.exe with shell:false; its flags,
  missing-process detection and ENOENT fallback are unchanged.
- app-server spawns the resolved codex launch with shell:false; the
  terminateChild comment now names the cmd.exe -> node -> codex tree.
- spawnBrokerProcess sets windowsHide.
- Fake codex fixture on win32: codex.cjs + codex.cmd shim pointing at it.
- README: codex/npm reachable only inside Git Bash are no longer found.

Co-authored-by: mohammad-malik <mohammad-malik@users.noreply.github.com>
Co-authored-by: mittalpk <mittalpk@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- where.exe searches $PATH: only, never the current directory, and shares the
  caller's timeout budget instead of adding up to 5 s to it
- cmd.exe runs with /v:off so a literal ! survives; a line break in an argument
  is refused instead of silently truncating the command line
- %VAR:a=b% substitution stays a documented ceiling: every caller passes literals
- round-trip test adds ^, ! and a path with parentheses

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eview prompt via stdin

New lib/hook-input.mjs readHookInput(): streams stdin with a deadline (2 s
default, CODEX_HOOK_STDIN_TIMEOUT_MS for tests), a 1 MiB byte limit and a
StringDecoder across chunk boundaries. EOF parses the whole input; at the
deadline a buffer that is already a complete JSON object is accepted (only
the EOF was late), anything else is `timeout`; over the limit is `overflow`
without parsing; bad JSON is `invalid-json`. Listeners and the timer are
removed and stdin destroyed, so the hook exits right after deciding.

Stop hook: any read error blocks as before (fail-closed), except a timeout
with nothing received while the workspace gate (CLAUDE_PROJECT_DIR / cwd) is
off, which allows (openai#530/openai#544). With the gate on, a timeout blocks naming
"hook input did not arrive". The review prompt now reaches the companion via
`task --prompt-stdin`, not argv, lifting the Windows/Linux argv limits.

SessionStart/SessionEnd hook: reads with a 1 s deadline before the 12 s
SessionEnd budget starts; on any read error it prints one stderr line and
returns without cleanup. The hooks.json timeout test now asserts 15 s >
budget + read.

readStdinIfPiped (companion): readSync loop that keeps accumulated bytes
across EAGAIN on a non-blocking stdin (up to 50 x 20 ms consecutive) and
throws on exhaustion instead of returning a partial prompt
(openai#120/openai#247/openai#123/openai#150/openai#165).

Co-authored-by: stantheman0128 <stantheman0128@users.noreply.github.com>
Co-authored-by: tmchow <tmchow@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…put; unreadable state keeps the gate closed

1. A stdin stream error is a distinct read-error outcome, never the deadline.
2. The deadline path flushes the UTF-8 decoder before the completeness check.
3. gateEnabledForProject reads state.json itself; an unreadable or corrupt
   file keeps the gate on instead of reading as defaults (gate off).
4. A no-op error listener stays after the read so a failing destroy() cannot
   crash the hook.
5. SessionStart reads stdin with a 5 s deadline; SessionEnd keeps 1 s.
6. Open-stdin tests carry a 30 s timeout and SIGKILL the child after 20 s.
7. Overflowing hook input with the gate off allows the stop; gate on blocks.
8. EAGAIN test gap 2500 ms; README names the always-blocked case and the
   escape hatch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Mirrors the local gate (sleep 10; pgrep -f codex-plugin-test-) on every
matrix job; Windows lists matching Win32_Process rows. Reported only until
the Windows kill path lands in v1.4.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nded EPERM retries on Windows

- state.mjs: resolveFallbackStateRoot takes a `platform` option; on win32 with
  LOCALAPPDATA set the root is %LOCALAPPDATA%\codex-companion\<hash> (the
  per-user profile ACL stands in for the POSIX owner/mode check, which is
  unchanged). A pre-1.4.0 <tmpdir>\codex-companion-user\<hash> root keeps being
  used, without migration, while the new one does not exist; one stderr notice
  per process.
- state.mjs: new retryOnWindows(op, codes) helper: win32 only, listed codes
  only, 20 attempts 15 ms apart, then rethrows the last error unchanged.
  writeFileAtomic retries the rename on EPERM/EBUSY/EACCES (rename over an open
  reader); readLockEntryOwner retries the ticket read on EPERM/EBUSY (read racing
  a peer's rename/unlink); loadState retries its read on EBUSY. Lock and kill
  semantics are unchanged.
- tests: un-skip "concurrent writers never leave a torn state.json" on Windows;
  helper tests with a fake op (recovers after 19, rethrows the 20th, no retry on
  posix or ENOENT); fallback root with/without LOCALAPPDATA and the transitional
  legacy-root case.
- README: Windows section names %LOCALAPPDATA%\codex-companion and the
  transitional policy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A reader racing a writer's rename over state.json sees EPERM as well as
EBUSY, as the lock-ticket reader did on CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- model catalogue fixture: catalogue-only `nova` alias (gpt-7-nova / gpt-6-nova,
  priority 0) proves resolution comes from the catalogue, not FALLBACK_ALIASES,
  and that a priority tie picks the newest family
- state: refusal test chmods 0o755 explicitly (umask-independent); new
  foreign-uid refusal test
- stop gate: CODEX_REVIEW_GATE_MAX_ROUNDS accepts only a non-negative integer
  (0 = unlimited); anything else falls back to 3 with one stderr warning;
  tests for =0, =5 and =0.5
- setup: an empty --review-gate-model / --review-gate-effort is rejected
  ("use inherit to clear") before anything is written
- broker teardown: document the reason enum including kill-failed (JSDoc +
  README table)
- workerCommandLine escapes the job id (strictly narrower matching)
- runtime G1 test kills the SIGTERM-immune worker group in t.after; fresh-broker
  test title/comment say "killed as a process group"
- README transfer: one clause for ~/.claude/projects / $CLAUDE_CONFIG_DIR/projects;
  registerThread comment on the single-tenant broker assumption

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…retries and identity-match

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The duplicate added in dfd941d failed under pipefail when pgrep found
nothing; the existing step now covers Windows with a Win32_Process listing
and runs after a red suite too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… wording

- fixture lists the older family first and the test also checks the reversed
  catalogue, so a stable sort by priority alone fails it
- identity-mismatch and command-line-match rows describe what the code does
- max-rounds warning says "plain digit string"

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The same single test measured 6.6 s on one hosted VM and 33 s on another
with identical code, so the turn-timeout assertion is now relative to a
20 s fake turn, the cancel test waits up to 30 s for the running job, and
the matrix jobs get 40 minutes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI runners set LOCALAPPDATA, so since 35f6bed the fallback root lives there,
not under the temp dir.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README: drop the demo video element; rewrite the Windows section to
reflect v1.4.0 as shipped (spawn no longer goes through $SHELL, bounded
hook stdin, %LOCALAPPDATA% fallback root and its transitional notice,
bounded EPERM/EBUSY/EACCES retries) and correct the stale "identity
lands in v1.4.0" claim to v1.4.1.

CHANGELOG: add the 1.4.0 section (Added/Changed/Fixed/Known
limitations) and copy it to plugins/codex/CHANGELOG.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…w second spawn

- the refusal case no longer races a real writer against the retry budget:
  the child's readSync throws EAGAIN forever and the call count is asserted
- the shared app-server test gives the broker a 15 s idle timeout so a slow
  runner cannot let it exit between the two CLI runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
libuv searches the child cwd before PATH, so a bare where.exe/cmd.exe/
taskkill.exe would run a same-named file planted in the reviewed repo.
Also: gate state read tolerates a transient EPERM; hook timing pins 10 s;
release-verify gets the cross-platform leak step and a 20-min quality job;
changelog names the cmd.exe quoting ceilings and lock-ticket reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dir keeps the gate closed

- runCommand and the app-server launch report ENOENT without spawning when
  where.exe finds nothing, so libuv never searches the workspace for a
  same-named .exe
- gateEnabledForProject reads state.json directly: only ENOENT means "never
  configured"; EACCES on the file or a parent (where existsSync says false)
  keeps the gate closed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CBEPX and others added 13 commits September 28, 2026 05:48
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…m the cwd

npm .cmd shims run a bare `node`; cmd.exe looks it up in the current
directory (the reviewed repo) before PATH. Both cmd.exe launch paths now set
NoDefaultCurrentDirectoryInExePath=1, and the Windows round-trip test plants
a node.cmd sentinel in the cwd.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Draft one English comment per upstream issue closed by v1.4.0 (Windows
spawn/taskkill cluster, windowsHide, stop-hook stdin deadline, EAGAIN
retries), and update the triage doc's statuses/queue buckets to match:
fixed-in v1.4.0 for those 9 issues plus their reference PRs and openai#326/openai#735,
planned v1.4.1 for openai#336/openai#423/openai#577 (Windows process-identity work deferred
from v1.4.0), and a note on openai#743 that the Windows kill-from-record refusal
now targets v1.4.1. Docs only, nothing posted upstream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test harness, spawn path and state retries now pass on windows-latest,
so the continue-on-error escape hatch from the CI spike is removed in both
workflows. Only the Windows leak report stays advisory until v1.4.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
openai#626/openai#633 were fixed in v1.3.0 (comment drafts added); openai#70/openai#416/openai#487/openai#718
move to v1.4.1 (kill from stored records), openai#57/openai#349 to v1.5.0 (sandbox
surface), the rest to verify after a reporter retest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ere.exe

where.exe output is in the console code page, so a non-ASCII install path
came back mangled under UTF-8 decoding. The lookup now walks PATH and
PATHEXT with fs: no child process, no decoding, relative entries and the
cwd are never searched, extensionless shims are never hits.

Docs: the Windows kill limitation no longer promises an interrupt on
SessionEnd or a bounded leak; cancel interrupts best-effort, a leaked broker
exits on idle once every client has disconnected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NoDefaultCurrentDirectoryInExePath only removes the implicit cwd; an explicit
"." or relative "tools" entry in PATH would still let the shim's bare node
resolve inside the reviewed repo. cmd.exe now gets the same absolute-only
PATH the resolver walks, under the caller's own PATH key. The Windows
round-trip test plants node.cmd in the cwd and under a relative entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he turn

A plain turn always gets turn/completed. Inferring completion 250 ms after
the final answer let a delayed terminal notification with status "failed"
be recorded as "completed" on slow hosts (openai#757 test flaked twice on CI).
The fake codex can now delay that notification to reproduce it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rn as failed

captureTurn now observes the client's exit and finalizes the turn as failed
with the captured output, standing down while the turn-timeout path owns the
outcome. The broker shuts down when its shared app-server exits so every
client sees its socket close. The fake codex can exit right after the final
answer; direct and brokered regressions added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…worker's broker alive

The test has failed twice on ubuntu/node 18 in CI with "still serving another
session"; the socket accounting in the broker log is what explains that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CBEPX
CBEPX merged commit 6f0467f into main Sep 28, 2026
10 checks passed
@CBEPX
CBEPX deleted the release/v1.4.0 branch September 28, 2026 07:18
CBEPX added a commit that referenced this pull request Sep 28, 2026
- CI leak check and cimTree helper follow the PowerShell launch rules: absolute
  in-box path (helper via runPowerShell), ASCII protocol on stdout, table on stderr
- parseProtocolLines checks exact lines without trim; TAB/NBSP/empty-line junk tests
- renderCancelPending/emitCancelPending given in full with survivors.length > 0;
  emit test with injected stdout/stderr/log writers
- hook keeps main().catch inside the direct-execution guard
- Task 6 README: file-based resolver, no where.exe, CR/LF and %VAR:a=b% ceilings
- ROOT is local to runtime.test.mjs; fileTimeAt everywhere

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant