Skip to content

Import eleven open upstream PRs, fix what they broke, and bring the README up to date - #6

Merged
Edo771977 merged 64 commits into
mainfrom
claude/focused-carson-khonz0
Sep 17, 2026
Merged

Edo771977 merged 64 commits into
mainfrom
claude/focused-carson-khonz0

Conversation

@Edo771977

@Edo771977 Edo771977 commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Imports eleven pull requests that are still open in openai/codex-plugin-cc, one merge commit each so any of them can be reverted on its own, fixes three defects this surfaced, and updates the README to describe what the fork actually does.

Upstream main has not moved since this fork's base (db52e28, 7 Jul), so there is nothing merged to catch up on — only open PRs.

Imported

Commit Upstream PR What it brings Conflicts
8c30e32 #565 the stop-review gate honors stop_hook_active instead of re-blocking every continuation turn until the retry cap none
1f6d4ad #748 CLAUDE_ENV_FILE keeps one export per key instead of growing on every session none
66c9b24 #746 --model/--effort on the review commands, and a warning instead of silence for an unrecognised option none
196276b #742 --sandbox <mode> on task and /codex:rescue 1
4a2ef41 #727 --resume-thread <id> to continue one specific Codex thread 4
9e6b38b #729 /codex:transfer works with a relocated CLAUDE_CONFIG_DIR none
4cda989 #724 --read-root <directory> for OS-enforced scoped reads 14
c4a4c4f #747 explicit 256 MiB maxBuffer, so a large git diff stops being truncated at Node's 1 MiB default 2
fce431e #763 a turn that fails without throwing persists its error text, so /codex:result says why 2
ed667e1 #731 the review-gate flag moves out of the transient state dir into a durable file under CODEX_HOME 2
62c5832 #737 hooks resolve Node through scripts/run-node.sh (nvm, fnm, asdf, mise, Volta, Homebrew) 1

Every conflict came from the imported PRs colliding with each other, not with the fork. Each merge commit records its own resolution.

Not imported: #761 (max/ultra reasoning efforts) — the same proposal from another author, #648, was closed upstream, so those values look unsupported rather than merely undocumented.

Resolutions worth reviewing

#724 vs #742: who owns the sandbox mode. #724 derived it from --write; #742 makes --sandbox the input and derives write from it. The resolution keeps #742's direction and layers read roots on top. Three things follow from the combination and exist in neither PR alone:

  • --read-root together with --sandbox danger-full-access is now rejected — that mode turns the sandbox off, so no read scope would be enforced.
  • In runAppServerTurn(), #724's try/catch around thread start/resume wraps #742's assertResumedSandbox() rather than replacing it. The plain auto-merge would have dropped the resume check.
  • tests/fake-codex-fixture.mjs recorded each thread start twice after the auto-merge (#724's raw params, then #742's structured record silently overwriting them). Both handlers now store {...message.params, threadId}, which satisfies both PRs' assertions.

Not taken from #724: its version bump to 1.0.11 and the five CHANGELOG sections that go with it. Those releases do not exist in this fork, so the plugin stays at 1.0.6 (npm run check-version passes) and the entries are folded into an Unreleased section.

In hooks.json the SessionEnd timeout stays at this fork's 30s rather than reverting to upstream's 5s, since the bounded teardown imported earlier needs it.

Defects fixed along the way

dd70d87 — a busy broker refusing shutdown is not an identity rejection. shutdownBrokerSessionLocked() treated every error reply to broker/shutdown as an identity mismatch, the BUSY refusal included, so when a client connected between the session-end guard check and the shutdown request — the normal race — SessionEnd exited 1 blaming the instance token for a broker that was simply still in use. It now returns { refused: true, exited: false } and leaves the process and the record alone. This also retires the two tests that had been failing since the earlier imports: one predated #623's busy check and now parks its lingering socket as a peer shutdown requester (what the broker itself calls "not work"); the other expected null from sendBrokerShutdown() and now asserts the outcome shape that matters.

62c5832 — #737 shipped with its precedence inverted. find_managed_node() listed /home/linuxbrew, /opt/homebrew, /usr/local/bin and /opt/local/bin before the version-manager roots, so on any machine carrying /usr/local/bin/node the system install shadowed every nvm/fnm/asdf/mise toolchain — the precise failure the PR set out to fix. Four of its own tests catch it. The managed roots now come first; HOMEBREW_PREFIX keeps its place ahead of them because it is configured rather than guessed.

ed667e1 — #731 wrote its new config file unprotected. writeDurableConfig() used fs.writeFileSync and a plain mkdirSync, while every other artifact state.mjs creates goes through ensurePrivateDir()/writeJsonFileAtomic(). It now does too: 0600 like the rest, and a torn write can no longer make readDurableConfig() fall back and silently disable the review gate.

4f5c23e — the typecheck was already red on main. CI's npm run build step rejected six TS2339 errors that reproduce identically on the base commit (7c86c7f); they arrived with the earlier lifecycle imports, merged while Actions was not running on this fork. Three annotations with no runtime effect: proc declared on the client base class, a socket error typed as NodeJS.ErrnoException, and a @param for terminateProcessTreeAndExit's options.

README

The landing page still described upstream's command surface, so several flags this fork supports were undocumented:

Both review commands' argument-hint now name --model/--effort, which the script already accepted. tests/commands.test.mjs asserted the adversarial hint as one adjacent string ([--scope ...] [focus ...]); it now asserts the scope, effort and focus fragments separately, so inserting a flag between them no longer fails a documentation test.

Testing

npm test: 254/254, green on repeated full runs, and CI is green on the head. npx tsc -p tsconfig.app-server.json is clean and npm run check-version passes.

Authorship

The commits carried in by the merges keep their original authors and hashes, so they still match the upstream PRs and can be compared against them later. GitHub marks them Unverified, as it does for any third-party work merged into a fork.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg

mittalpk and others added 30 commits July 27, 2026 16:08
When the review gate is enabled, main() ran the stop-time Codex review
and emitted {"decision":"block"} on any non-ok outcome (no output,
timeout, failure, invalid JSON) without checking whether this
invocation was already a forced retry. Claude Code re-invokes Stop
hooks with stop_hook_active: true after a block decision, so a review
that times out or fails would re-run and re-block on every retry,
until the harness's forced-retry cap force-ends the turn -- worst in
exactly the cases where the gate is least useful.

Fix: skip the review and return cleanly when stop_hook_active is set,
matching the fix the issue reporter suggested. The issue also flagged
codex-companion.mjs's `config.stopReviewGate` branches as worth
auditing for the same problem; traced them and confirmed they're only
config get/set/report code, not the review-blocking logic itself
(that lives solely in stop-review-gate-hook.mjs), so no second file
needs the fix.

Added a regression test that snapshots the fake Codex binary's
appServerStarts counter before and after a stop_hook_active retry,
proving the review subprocess is not spawned a second time and no
"decision":"block" is emitted. Confirmed the test fails against the
pre-fix code (git stash isolation) with the exact stale block
decision the issue describes. Full suite: 92/92 passed, no
regressions.
logNote() only writes to stderr, which isn't surfaced for a
successful hook run -- a skipped review looked identical to a
passed one. Emit a systemMessage instead (no "decision" key, so
it still doesn't block) so the skip is visible right when it
matters most, since the whole reason there's a retry turn is that
the previous review blocked on something.

Per review feedback from @Wintersta7e on the PR.
`task` always mapped `--write` to `workspace-write` and everything else to
`read-only`, so a rescue run could never ask for `danger-full-access` even
when the work needs it (test tooling that writes outside the repository, or
network access). Add an explicit `--sandbox <read-only|workspace-write|
danger-full-access>` flag:

- the flag wins over `--write`; `--write --sandbox read-only` is rejected
- the resolved mode is stored in the job request, so the detached worker
  and `--resume-last` reuse exactly what the caller asked for
- jobs started with a writable sandbox keep the review hints that `--write`
  jobs already get
- older stored requests without a sandbox field keep the old mapping

The fake app-server fixture now records the `thread/start` and
`thread/resume` params so tests can assert what reached Codex. The rescue
command, agent and runtime skill forward `--sandbox` as a runtime flag and
never add `danger-full-access` on their own.

Closes openai#145

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a stored request without a sandbox field

Measured on Codex CLI 0.153.2: the app-server keeps the sandbox a thread
was started with when it is resumed, whichever mode the new request names
on thread/resume. Say so in the README instead of the opposite, and keep
the resume test to what the plugin forwards.

Add a task-worker test that replays a job record written before this
change, with no sandbox field in the request, and checks it still starts
the thread with the --write mapping.

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

A rescue request such as `--sandbox read-only fix the failing test` reads
as write intent, and the agent that forwards it may still add --write; a
hard error there returns nothing to the user. The explicit flag now wins:
`--write --sandbox read-only` starts a read-only thread. Say so in the
README and the runtime skill, and scope `--sandbox` to /codex:rescue in
the README, since the review commands stay read-only.

The rescue command carries the same "never add danger-full-access
yourself" rule as the agent and the skill. The fake app-server records
only the thread params the tests assert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Measured on Codex CLI 0.153.2: the mode named on thread/resume is applied
when the app-server loads the thread again from disk, and ignored while
the thread is still live in the app-server. The plugin keeps one shared
app-server per session, so inside a session a resumed rescue keeps the
sandbox it started with. The README says exactly that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hat asks for another sandbox

Two findings from the Codex review of this branch.

The argument parser recognises flags anywhere in the request, so a task
whose text merely mentioned `--sandbox danger-full-access` ran without a
sandbox and with the words stripped from the prompt. `task` now takes
`--sandbox` only from the leading option block; once the task text has
begun the same words stay part of the prompt. Other options keep their
existing behaviour.

A thread the shared app-server still holds keeps its sandbox on resume,
so forwarding a different mode on `thread/resume` produced a turn with
broader or narrower access than requested, silently. The job record now
carries the sandbox a thread was started with, and `--resume-last`
refuses a request whose sandbox differs from it, naming `--fresh` and the
matching `--sandbox` as the ways out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ext --sandbox in the prompt, validate the whole inline value

Three findings from the second Codex review of this branch.

The shared app-server answers thread/resume with the sandbox the thread
actually has (measured on Codex CLI 0.153.2: a live danger-full-access
thread answers dangerFullAccess to a read-only request). runAppServerTurn
now compares that answer with the requested mode before turn/start and
refuses a mismatch, so a resume is checked twice: against the job record
before connecting, and against the server's own answer when the record
is missing or predates the sandbox field. The fake app-server keeps a
sandbox per thread and echoes it the same way.

The rescue command, agent and skill told the wrapper to treat any
--sandbox as a runtime flag, which would have hoisted one out of the task
text and defeated the leading-only parser. They now say: forward the
request in the user's order; a leading --sandbox is a flag, one inside
the task text stays there as prompt text.

extractLeadingSandbox split the inline value at the second "=", so
--sandbox=danger-full-access=false passed as danger-full-access. It now
splits at the first "=" and validates the whole remainder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… as the first token

The runtime skill said "starts with --sandbox", which would have made the
forwarder miss `--model spark --sandbox danger-full-access run tests` or a
request with --resume prepended, leaving the tokens in the prompt while
it added its default --write. It now matches the command, the agent and
the parser: any --sandbox before the task text is the control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`--sandbox=` and a trailing `--sandbox` produced an empty value that
normalized to null and silently fell back to the --write mapping. The
flag now needs one of the documented modes whenever it is present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fscfede-beep and others added 18 commits September 5, 2026 16:14
`/codex:adversarial-review --model gpt-6-astra --effort xhigh` silently does
something other than what it says.

`handleReviewCommand` parses valueOptions ["base","scope","model","cwd"], with
no "effort" - only `handleTask` parses it. `parseArgs` then demotes any
unrecognised long option to a positional, and `handleReviewCommand` builds the
review's focus text with `positionals.join(" ")`. So the two tokens `--effort`
and `xhigh` are concatenated into the prompt handed to the reviewer, the run
uses whatever `model_reasoning_effort` the config holds, and nothing warns.
The run looks like it did what was asked.

That is two separate problems, fixed separately.

1. The review commands now accept `--effort`, normalised through the existing
   `normalizeReasoningEffort` and threaded into the `runAppServerTurn` call that
   the adversarial path already makes. `runAppServerTurn` already accepts an
   `effort` option, so this is plumbing rather than new capability. The usage
   line is updated to match, including the `--model` flag it already supported
   but did not advertise.

2. `parseArgs` now returns `unknownOptions` alongside `options` and
   `positionals`, and `parseCommandInput` warns on stderr for each one.
   Behaviour is deliberately unchanged - the token still reaches positionals,
   because commands such as adversarial-review take free-form focus text and a
   hard error would break a prose word that happens to start with two dashes.
   The point is only that the demotion stops being silent. This generalises past
   `--effort`: any future flag typo on any command currently ends up pasted into
   a prompt with no indication.

Tests: five in tests/args.test.mjs, covering the unknown-option report, the
clean case, the `--` passthrough boundary, `--effort` staying out of the focus
text under the real review config, and quoted focus phrases. Mutation-tested by
reverting the `unknownOptions.push` and confirming the first fails, then
restoring it.

Full suite on Windows goes from 79 passing / 12 failing to 84 passing / 12
failing. Those 12 fail identically on clean main - they are platform tests
(Unix sockets, temp-backed state dirs) and are untouched by this change.
…mmary (openai#757)

Two gaps combined to make server-side turn failures illegible in status:

1. errorMessage was only written in the catch path, so a turn that failed
   with exitStatus != 0 but completed normally stored no reason anywhere
   reachable from 'status'.

2. The summary took the first line of the pretty-printed error body,
   which is a bare opening brace.

Now:
- tracked-jobs.mjs reads execution.errorMessage on the success branch and
  writes it to both the job file and the index when failed.
- codex-companion.mjs task/review paths surface result.error.message as
  errorMessage and prefer it over rawOutput for the summary.
- Summary and Codex error: progress line are passed through shorten() so
  multi-line JSON bodies cannot reduce to punctuation.
- Regressions covered by two new state.test.mjs tests.
Upstream PR openai#565 (mittalpk), merged without conflicts.

Claude Code re-invokes the Stop hook with stop_hook_active: true after a
"block" decision. Without this guard the gate re-runs the review and
re-blocks on every non-ok outcome until the harness's forced-retry cap
ends the turn anyway. The skip is surfaced through systemMessage rather
than logNote(), which is invisible on a successful hook run.

Verified: node --check on the hook, tests/runtime.test.mjs 92/92.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#748 (MeGaNeKoS), merged without conflicts.

SessionStart appended to CLAUDE_ENV_FILE unconditionally, so the file grew
one duplicate export per session and the shell resolved whichever line came
last. appendEnvVar() is replaced by setEnv(), which rewrites the file with
at most one export per key and swaps it in via a temp file + rename.

Verified: node --check on the hook, tests/runtime.test.mjs and
tests/state.test.mjs 100/100.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#746 (taur-us), merged without conflicts.

- review/adversarial-review now accept --effort and forward it to the turn,
  which previously only task could do.
- parseArgs() collects unrecognised long options and parseCommandInput()
  warns about them on stderr. They are still treated as positionals (some
  commands take free-form text), but a mistyped flag no longer disappears
  silently into a prompt.

Verified: node --check on codex-companion.mjs and lib/args.mjs; the new
tests/args.test.mjs plus tests/commands.test.mjs and tests/runtime.test.mjs
105/105.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#742 (alirezarzg), one conflict resolved.

task and /codex:rescue take --sandbox read-only|workspace-write|
danger-full-access; --write stays as the shorthand for workspace-write. A
resume whose recorded starting sandbox cannot be compared is refused rather
than trusted, and the sandbox is inferred only from records that carry one.

Conflict: the usage block in codex-companion.mjs, against openai#746 merged just
before. Both sides are additive, so the resolved line keeps openai#746's --model/
--effort on adversarial-review and adds openai#742's --sandbox to task.

Verified: node --check on codex-companion.mjs; tests/args.test.mjs,
tests/commands.test.mjs and tests/runtime.test.mjs 113/113.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#727 (fscfede-beep), four conflicts resolved.

task and /codex:rescue can resume one specific Codex thread by id instead of
only the most recent one; --resume-last, --resume-thread and --fresh are
mutually exclusive, and an empty id is rejected.

Conflicts: all four are the same additive collision with openai#742 (--sandbox),
merged just before -- the rescue argument-hint, the rescue agent's routing
bullets, the usage block, and buildTaskRequest()'s parameter list plus the
task valueOptions. Each is resolved as the union of both sides.

Checked afterwards that the two features compose: --resume-thread flows into
runAppServerTurn as resumeThreadId, so openai#742's assertResumedSandbox() guard
covers an explicit thread id exactly as it covers --resume-last.

Verified: node --check on codex-companion.mjs; tests/args.test.mjs,
tests/commands.test.mjs and tests/runtime.test.mjs 118/118.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#729 (fscfede-beep), merged without conflicts.

transfer only accepted sessions under ~/.claude/projects, so a relocated
CLAUDE_CONFIG_DIR made it unusable. The source is now resolved against the
configured Claude root, staged under a collision-free lease so concurrent
transfers cannot clobber each other, and staging ancestors that escape the
expected root are rejected.

Verified: node --check on claude-session-transfer.mjs and
codex-companion.mjs; the new tests/claude-session-transfer.test.mjs plus
tests/runtime.test.mjs and tests/commands.test.mjs 117/117.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#724 (kilhyeonjun), fourteen conflicts resolved.

task and /codex:rescue accept repeatable --read-root <directory>, which opts
the run into an OS-enforced Codex permission profile: reads are denied
outside the approved roots and Codex's minimal runtime paths, and a runtime
without permission profiles fails closed instead of running unscoped.

Conflicts: all against openai#742 (--sandbox) and openai#727 (--resume-thread), merged
just before. The substantive one is ownership of the sandbox mode. openai#724
derived it from --write (`request.write ? "workspace-write" : "read-only"`),
while openai#742 makes --sandbox the input and derives write from it. The resolved
code keeps openai#742's direction and layers read roots on top:

- buildThreadAccessParams() receives the already-resolved sandbox, so a
  scoped profile and an explicit --sandbox no longer disagree.
- the workspace-coverage check now triggers on any write-capable sandbox,
  not only on a literal --write.
- --read-root with --sandbox danger-full-access is rejected: that mode turns
  the sandbox off, so no read scope would be enforced. This case did not
  exist in either PR alone.
- in runAppServerTurn(), openai#724's try/catch around thread start/resume wraps
  openai#742's assertResumedSandbox() rather than replacing it; scopedAccessError()
  passes a sandbox-mismatch error through untouched.
- tests/fake-codex-fixture.mjs recorded thread starts twice after the
  auto-merge (openai#724's raw params, then openai#742's structured record, silently
  overwriting it). Both handlers now store `{...message.params, threadId}`,
  which satisfies both PRs' assertions.

Not taken: the version bump to 1.0.11 and its five CHANGELOG sections. Those
releases do not exist in this fork, so the plugin stays at 1.0.6 and the
entries are folded into an Unreleased section (npm run check-version passes).

Verified: node --check on every touched script; full npm test 230/232, with
the only two failures the ones already failing on the parent commit
(tests/broker-lifecycle.test.mjs, pre-existing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
The landing page still described upstream's command surface, so several
flags this fork supports were undocumented.

- A "Commands At A Glance" table lists every slash command with its real
  flags, plus the accepted --effort values and the new warning for an
  unrecognised option.
- The install snippet points at this fork's marketplace, and says how to
  get upstream instead (the marketplace name is the same, so only one of
  the two can be added at a time).
- /codex:review and /codex:adversarial-review document --model/--effort
  (openai#746); /codex:rescue documents --resume-thread (openai#727) and the resume
  flags' exclusivity; /codex:transfer documents CLAUDE_CONFIG_DIR (openai#729);
  the review gate section explains the stop_hook_active skip (openai#565).
- A "Differences From Upstream" section lists every imported PR with a link,
  grouped into broker lifecycle and command surface, and states that the
  plugin version stays at the upstream number.

Both review commands' argument-hint now name --model/--effort, which the
script already accepted. tests/commands.test.mjs asserted the adversarial
hint as one adjacent string ("[--scope ...] [focus ...]"); it now asserts
the scope, effort and focus fragments separately, so inserting a flag
between them no longer fails a documentation test.

Verified: full npm test 230/232, the two failures being the pre-existing
tests/broker-lifecycle.test.mjs ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
shutdownBrokerSessionLocked() treated every error reply to broker/shutdown
as an identity mismatch, including the broker's BUSY refusal. The refusal is
the normal answer when a client connects between the session-end guard check
and the shutdown request, so SessionEnd exited 1 with "broker rejected
shutdown identity" for a broker that had in fact recognized it and was simply
still in use. It now returns { refused: true, exited: false } and leaves the
process and the persisted record alone, which is what the hook's own
pre-check already does with a refusal; a later session end, or the broker's
idle shutdown, retires it. Every outcome carries `refused` so callers can
discriminate without inspecting the error message.

The two tests that had been failing since the earlier imports are both
consequences of this area, and neither was a stale-assertion-only fix:

- "shutdown closes idle half-open broker clients" (from openai#541)
  predates the busy check that openai#623 later added, which
  refuses shutdown while ANY other client is connected — deliberately,
  because a worker between requests has no in-flight request to detect. The
  test's own subject is that shutdown destroys a lingering half-open socket
  instead of waiting on it, so it now parks that socket as a peer shutdown
  requester, which the broker itself defines as "not work". Same coverage,
  current contract. It also awaits "end" rather than "close": an
  allowHalfOpen client never closes itself, so the old expectation could not
  have been met either way.
- "shutdown request always uses a finite deadline" expected null from
  sendBrokerShutdown(), which has returned an outcome object since the
  lifecycle merges. It now asserts the shape that matters: a broker that
  accepts the connection and then says nothing is not delivered, not
  refused, and deliberately NOT unreachable, so teardown cannot reap a
  broker that may still be serving someone.

Added "a busy broker refuses shutdown instead of failing it", which covers
the fix directly: an ordinary connected client keeps the broker alive and the
record intact without raising, and the same call retires it once that client
goes away.

Verified: full npm test twice, 233/233 both times (was 230/232).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg

Copy link
Copy Markdown
Owner Author

Pushed dd70d87: the two pre-existing failures this PR's description listed as not addressed are now fixed, so the suite is 233/233 (green on two consecutive full runs).

One of them turned out to be a real defect rather than a stale test. shutdownBrokerSessionLocked() treated every error reply to broker/shutdown as an identity mismatch, the BUSY refusal included — so when a client connected between the session-end guard check and the shutdown request (the normal race), SessionEnd exited 1 with "broker rejected shutdown identity" about a broker that had recognized it perfectly well and was simply still in use. It now returns { refused: true, exited: false } and leaves the process and the persisted record alone, which is exactly what the hook's own pre-check already did with a refusal.

On the tests:

  • shutdown closes idle half-open broker clients (from #541) predates the busy check #623 added later, which refuses shutdown while any other client is connected — deliberately, since a worker between requests has no in-flight request to detect. Rather than weaken that rule, the test now parks its lingering socket as a peer shutdown requester, which the broker itself defines as "not work": same subject (shutdown destroys a half-open socket instead of waiting on it), current contract. It also awaits end instead of close — an allowHalfOpen client never closes itself, so the old expectation could not have been met either way.
  • shutdown request always uses a finite deadline expected null from sendBrokerShutdown(), which has returned an outcome object since the lifecycle merges. It now asserts what matters: a broker that accepts the connection and then says nothing is not delivered, not refused, and deliberately not unreachable, so teardown cannot reap a broker that may still be serving someone.

Added a busy broker refuses shutdown instead of failing it to cover the fix directly.


Generated by Claude Code

Upstream PR openai#747 (sylvesterkaczmarek), two conflicts resolved.

runCommand() passed options.maxBuffer straight through, so an unset value
left Node's 1 MiB spawnSync default in place and a large `git diff` came back
truncated with ENOBUFS. It now defaults to an explicit 256 MiB, and an
explicit override still wins.

Conflicts: both are adjacency, not disagreement. In process.mjs the fork had
added `timeout`/`killSignal` on the lines around `maxBuffer`; the resolved
call keeps all three. In tests/process.test.mjs the PR's import line is
dropped (the fork's import block already names runCommand and
terminateProcessTree) and its two tests are appended.

Verified: node --check on both files; tests/process.test.mjs 13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#763 (Soumya95), two conflicts resolved.

A turn that failed without throwing (a rejected model, an unsupported
parameter) stored only a truncated summary, so /codex:result showed that the
job failed but not why. The failure text is now persisted as errorMessage on
both the job file and the state index, and summaries are shortened to 96
characters instead of carrying a whole error into an index entry.

Conflicts: both in tests/state.test.mjs and both unions. The import block
keeps the fork's list and adds runTrackedJob. The test block keeps the fork's
tests and appends the PR's two; the conflict cut through the fork's last test
(its closing `});` was the line both sides shared after the marker), so that
brace is restored explicitly in the resolution.

Verified: node --check on the three touched scripts; tests/state.test.mjs,
tests/tracked-jobs.test.mjs and tests/runtime.test.mjs 139/139.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#731 (fscfede-beep), two conflicts resolved.

The review-gate flag lived in the workspace state file under
CLAUDE_PLUGIN_DATA, so a different plugin data root (or a cleared state dir)
silently reverted the gate to off while /codex:setup still reported it as
enabled. It now lives in a durable per-workspace file under CODEX_HOME, with
the state copy kept in sync as a cache.

Conflicts: both in tests/state.test.mjs, both unions, and the second one cut
through the fork's last test again — its closing `});` is restored in the
resolution.

One change beyond the PR: writeDurableConfig() created the file with
fs.writeFileSync and a plain mkdirSync, while every other artifact this
module writes goes through ensurePrivateDir()/writeJsonFileAtomic(). It now
does too, so the new config file is 0600 like the rest and a torn write
cannot silently disable the gate on the next read.

Verified: node --check on state.mjs; tests/state.test.mjs,
tests/runtime.test.mjs and tests/commands.test.mjs 134/134.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Upstream PR openai#737 (fscfede-beep), one conflict resolved.

Hooks ran `node` directly, so a Node installed through nvm, fnm, asdf, mise,
Volta or Homebrew was invisible under the minimal PATH hooks get, and the
plugin looked broken for anyone who does not have a system-wide node. The
hooks and the node-invoking commands now go through scripts/run-node.sh,
which finds a supported (>=18.18) toolchain, prefers one that ships codex
next to it, and restores the npm global prefix.

Conflict: plugins/codex/hooks/hooks.json, SessionEnd. The launcher command
is taken from the PR; the timeout stays at this fork's 30s rather than
reverting to upstream's 5s, since the bounded teardown imported earlier needs
it.

One fix beyond the PR: find_managed_node() listed /home/linuxbrew,
/opt/homebrew, /usr/local/bin and /opt/local/bin BEFORE the version-manager
roots, so on any machine carrying /usr/local/bin/node (Debian and Ubuntu
packages, CI images, this repo's own container) the system install shadowed
every managed toolchain — the precise failure the PR set out to fix. Four of
its own tests caught it here and passed only where no system node exists.
The managed roots now come first; HOMEBREW_PREFIX keeps its place ahead of
them because it is configured rather than guessed.

Verified: bash -n on run-node.sh; the new tests/node-launcher.test.mjs 14/14;
full npm test 254/254; npm run check-version passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Adds openai#731, openai#737, openai#747 and openai#763 to "Differences From Upstream", and notes in
Requirements that Node no longer has to be on the system PATH now that the
hooks go through scripts/run-node.sh (including CODEX_COMPANION_NODE for
pinning one).

Verified: full npm test 254/254.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg

Copy link
Copy Markdown
Owner Author

Pushed the second batch (c4a4c4f…4783ed1): four more upstream PRs, same one-merge-commit-each shape. Suite is 254/254, green on two consecutive full runs.

Commit Upstream PR What it brings Conflicts
c4a4c4f #747 explicit 256 MiB maxBuffer, so a large git diff stops being truncated at Node's 1 MiB default 2
fce431e #763 a turn that fails without throwing persists its error text, so /codex:result says why 2
ed667e1 #731 the review-gate flag moves out of the transient state dir into a durable file under CODEX_HOME 2
62c5832 #737 hooks resolve Node through scripts/run-node.sh (nvm, fnm, asdf, mise, Volta, Homebrew) 1

Two places needed more than a merge:

openai#737 shipped a defect its own tests caught here. find_managed_node() listed /home/linuxbrew, /opt/homebrew, /usr/local/bin and /opt/local/bin before the version-manager roots, so on any machine that has /usr/local/bin/node — Debian and Ubuntu packages, CI images, this repo's own container — the system install shadowed every nvm/fnm/asdf/mise toolchain. That is precisely the failure the PR set out to fix, and four of its tests (discovers Node from a custom NVM_DIR, custom FNM_DIR, custom ASDF_DATA_DIR and MISE_DATA_DIR, skips unsupported Node versions) fail unless you happen to have no system node. The managed roots now come first; HOMEBREW_PREFIX keeps its place ahead of them because it is configured rather than guessed. All 14 launcher tests pass.

openai#731 wrote its new config file unprotected. writeDurableConfig() used fs.writeFileSync and a plain mkdirSync, while every other artifact state.mjs creates goes through ensurePrivateDir()/writeJsonFileAtomic(). It now does too — 0600 like the rest, and a torn write can no longer silently disable the review gate on the next read.

Also worth recording: in hooks.json the SessionEnd timeout stays at this fork's 30s rather than reverting to upstream's 5s, since the bounded teardown imported earlier needs it.

Not imported: #761 (max/ultra reasoning efforts). The same proposal from another author, #648, was closed upstream, so those values look unsupported rather than merely undocumented.


Generated by Claude Code

CI's `npm run build` step rejects six type errors. None of them come from
this branch: they reproduce identically on main (7c86c7f), which is where
they arrived with the earlier lifecycle imports, merged while Actions was not
running on this fork. The PR cannot go green without them, and each is an
annotation with no runtime effect:

- app-server.mjs:441 reached for `client.proc` on the client union, which only
  the spawned transport has. The base class now declares `proc` as a nullable
  ChildProcess, so the shared cleanup path is honest about both transports
  instead of reading a property half of them never carry. The spawned client
  still assigns the real child in its own constructor, after super().
- broker-lifecycle.mjs:74 read `error.code` off a socket error typed as plain
  Error; the callback parameter is annotated NodeJS.ErrnoException.
- process.mjs terminateProcessTreeAndExit() took `beforeKill` with no default,
  so it was absent from the options type TypeScript inferred from `= {}`. A
  @PARAM now states the shape.

Verified: npx tsc -p tsconfig.app-server.json clean (it reported all six
before, on this branch and on main alike); full npm test 254/254.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg

Copy link
Copy Markdown
Owner Author

CI ran for the first time on this fork and failed on dd70d87 — not on the tests (233/233 at that commit), but on the npm run build typecheck, with six TS2339 errors.

They are not this branch's. All six reproduce identically on main (7c86c7f): I ran npx tsc -p tsconfig.app-server.json against a worktree of the base commit and got the same list, same files, same positions. They arrived with the earlier lifecycle imports, which were merged while Actions was not running on this fork, so nothing ever flagged them.

Since no fix for them exists anywhere and the PR cannot reach green while the base is red, 4f5c23e fixes them here. All three are annotations with no runtime effect:

  • app-server.mjs:441 read client.proc off the client union, which only the spawned transport has. The base class now declares proc as a nullable ChildProcess, so the shared cleanup path is honest about both transports; the spawned client still assigns the real child in its own constructor, after super().
  • broker-lifecycle.mjs:74 read error.code off a socket error typed as plain Error — the callback parameter is annotated NodeJS.ErrnoException.
  • process.mjs terminateProcessTreeAndExit() took beforeKill with no default, so it was absent from the options type inferred from = {}. A @param states the shape.

npx tsc -p tsconfig.app-server.json is now clean, and the suite is 254/254.

If you would rather keep this branch strictly to the imports, the commit reverts cleanly on its own — but then main stays red on the build step until the same fix lands there.


Generated by Claude Code

@Edo771977 Edo771977 changed the title Import seven open upstream PRs, and bring the README up to date Import eleven open upstream PRs, fix what they broke, and bring the README up to date Sep 17, 2026
@Edo771977
Edo771977 merged commit 8484b71 into main Sep 17, 2026
1 check passed
@Edo771977
Edo771977 deleted the claude/focused-carson-khonz0 branch September 17, 2026 12:27
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.

9 participants