Skip to content

feat(companion): handle --help per subcommand so it cannot start a run - #681

Open
cjsteigerwald wants to merge 9 commits into
openai:mainfrom
cjsteigerwald:feat/companion-subcommand-help
Open

feat(companion): handle --help per subcommand so it cannot start a run#681
cjsteigerwald wants to merge 9 commits into
openai:mainfrom
cjsteigerwald:feat/companion-subcommand-help

Conversation

@cjsteigerwald

@cjsteigerwald cjsteigerwald commented Aug 24, 2026

Copy link
Copy Markdown

Problem

--help is only recognised as the first argument (subcommand === "--help"). Anywhere else it lands in argv, and because parseArgs treats any unrecognised --token as a positional, it is carried into the subcommand as data rather than handled as a request for usage.

For adversarial-review the positionals become the review's focus text, so:

node scripts/codex-companion.mjs adversarial-review --help

runs a full review against the default base — minutes of wall clock and a real model turn — for someone who asked what the flags were. I burned ~10 minutes to this twice in one afternoon before realising the flag was being eaten.

The same shape applies to any subcommand whose parser accepts positionals; adversarial-review is simply the one where the cost is highest.

Fix

main() checks for --help, -h or help in argv before the dispatch switch, and prints usage for that subcommand alone.

Checking before the switch is the substance of the change, not an implementation detail: it means a help request can never reach a handler capable of dispatching. Putting the check inside each handler would leave the same bug one refactor away.

Bare --help, -h, and help as the subcommand still print the full usage block, unchanged.

The usage lines move into a Map keyed by subcommand so one line can be printed without duplicating the text. The full block prints in the same order as before.

$ node scripts/codex-companion.mjs adversarial-review --help
Usage:
  node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]

Tests

Two tests in tests/runtime.test.mjs:

  • adversarial-review --help prints usage and starts no Codex turn — asserted against the fake app server's recorded lastTurnStart, since "printed usage" alone would not prove the review was skipped
  • task -h prints only the task line, not the whole block

Both verified non-vacuous against db52e28, where they fail.

Full suite: 93 passing / 0 failing of 93.

Correction: an earlier revision of this description reported 3 pre-existing failures on main. That was wrong. They were caused by CODEX_COMPANION_SESSION_ID being set in my shell: filterJobsForCurrentClaudeSession then filters jobs to job.sessionId === sessionId, and the status/result test fixtures are handcrafted without a sessionId, so they were all filtered out. With that variable unset the suite is green. There are no pre-existing failures.

`--help` is only recognised as the first argument. For anything else it lands in
argv, and because every subcommand parser treats an unrecognised token as a
positional, it is carried into the command as data.

For `adversarial-review` that means `--help` is joined into the review's focus
text and a full review runs: minutes of wall clock and a real model turn, for
someone who asked what the flags were. The same shape applies to any subcommand
whose parser accepts positionals.

main() now checks for --help, -h or help in argv before the dispatch switch, and
prints usage for that subcommand alone. Checking before the switch is the point:
a help request can never reach a handler that would dispatch. Bare `--help`,
`-h` and `help` as the subcommand still print the full usage block, unchanged.

The usage lines move into a Map keyed by subcommand so a single line can be
printed without duplicating the text. The full block prints in the same order as
before.

Tests: `adversarial-review --help` prints usage and — the assertion that matters
— starts no Codex turn, checked against the fake app server's recorded
lastTurnStart; and `task -h` prints only the task line. Both verified
non-vacuous against db52e28, where they fail.
@cjsteigerwald
cjsteigerwald requested a review from a team August 24, 2026 14:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6622ade45f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

// Checked before the switch, so help can never reach a handler that would dispatch.
if (USAGE_LINES.has(subcommand) && isHelpRequest(argv)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize raw arguments before detecting help

The plugin entrypoints pass $ARGUMENTS as one quoted argument (for example, plugins/codex/commands/adversarial-review.md:50), so /codex:adversarial-review --base main --help reaches this check with argv equal to ["--base main --help"]. isHelpRequest therefore returns false; the handler subsequently splits the string and starts a full focused review with --help as focus text, preserving the costly behavior this change is intended to prevent whenever help is combined with another option. Normalize argv before performing this check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 5770f78 — thank you, this was the difference between the fix working and only appearing to work.

I reproduced it exactly as you described. Running the real invocation shape started a genuine review:

$ node scripts/codex-companion.mjs adversarial-review "--base main --help"
[codex] Starting Codex task thread.
[codex] Turn started (01a03503-8c7c-7d10-891f-b37dce89f48f).

I had to cancel that job. My tests passed discrete argv entries, which is not how the plugin commands invoke this — they pass "$ARGUMENTS" as one quoted argument — so the tests never exercised the path that actually matters.

The check now runs normalizeArgv(argv) first, the same normalization parseCommandInput applies, so detection sees the tokens the handler would.

Normalizing brings a consequence worth calling out, because fixing this naively introduces a new bug: the bare word help becomes a token in ordinary focus text, so adversarial-review "review the help system" would print usage instead of running the review. isHelpRequest therefore matches only --help and -h. A bare help subcommand is still handled separately before dispatch, so codex-companion.mjs help is unchanged.

Two tests:

  • help combined with another flag in a single quoted string prints usage and starts no turn — verified non-vacuous against 6622ade, where it fails
  • focus text containing "help" still reaches turn/start with the focus intact. To be straight about it, this one passes with and without the fix: it is a guard against the regression normalizing would otherwise introduce, not evidence of the original bug.

Full suite: 95 passing / 0 failing of 95.

@cjsteigerwald

Copy link
Copy Markdown
Author

Correction to my earlier test-suite claims on this PR.

I reported 3 pre-existing failures on main (status shows phases..., status preserves adversarial review kind labels, result returns the stored output...). That was wrong, and the cause is worth knowing because it is a property of the suite rather than of my machine.

CODEX_COMPANION_SESSION_ID was set in my shell. When it is set, filterJobsForCurrentClaudeSession narrows jobs to job.sessionId === sessionId:

function filterJobsForCurrentClaudeSession(jobs) {
  const sessionId = getCurrentClaudeSessionId();
  if (!sessionId) return jobs;
  return jobs.filter((job) => job.sessionId === sessionId);
}

The status and result tests write their fixtures by hand and none carries a sessionId, so with that variable set every fixture job is filtered out and the commands correctly report No jobs recorded yet. The production behaviour is right; the fixtures simply predate the filter.

Unsetting the variable turns all three green, and every branch is fully green:

#677  94 passing / 0 failing of 94
#680  100 passing / 0 failing of 100
#681  93 passing / 0 failing of 93

So: there are no pre-existing failures on main — please disregard that part of my earlier comments.

One thing this does suggest: since this plugin is for Claude Code, contributors are likely to run npm test from inside a Claude Code session, where CODEX_COMPANION_SESSION_ID is set and these three tests fail for reasons unrelated to their change. Isolating that variable in the test harness (or giving the fixtures a matching sessionId) would remove a confusing false negative. Happy to send that as a separate PR if it would be welcome.

Addresses the P1 review finding on openai#681.

The plugin commands invoke the companion with "$ARGUMENTS" as a single quoted
argument (plugins/codex/commands/adversarial-review.md:50), so
`/codex:adversarial-review --base main --help` reaches main() as
argv === ["--base main --help"]. Comparing raw tokens misses the flag, the
request falls through to the handler, and parseCommandInput then splits the
string itself and starts a full review with --help as focus text -- preserving
the exact behaviour this change exists to prevent whenever help is combined
with any other argument.

The check now runs normalizeArgv(argv) first, which is the same normalization
parseCommandInput applies, so detection sees the same tokens the handler would.

Normalizing has a consequence that must be handled at the same time: the bare
word "help" now appears as a token in ordinary focus text. Matching it would
turn `adversarial-review "review the help system"` into a usage dump instead of
the review the user asked for. isHelpRequest therefore matches only --help and
-h. A bare `help` subcommand is still handled separately before dispatch, so
`codex-companion.mjs help` is unaffected.

Tests:
- help combined with another flag in one quoted string prints usage and starts
  no Codex turn. Verified non-vacuous against 6622ade, where it fails.
- focus text containing the word "help" still runs the review and reaches
  turn/start with the focus intact. This one passes both with and without the
  fix: it is a guard against the regression normalizing would otherwise
  introduce, not evidence of the original bug.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

5770f78 is the current head and has not been reviewed — the previous pass covered 6622ade.

It normalizes argv before help detection (per your P1) and narrows isHelpRequest to --help/-h only, since normalizing makes the bare word help a token in ordinary focus text.

Worth scrutinising: whether normalizeArgv + splitRawArgumentString can produce a --help/-h token from input where the user did not intend a flag — quoted or escaped focus text being the obvious candidate.

Self-review follow-up to 5770f78, which introduced this while fixing the P1.

Normalizing argv before help detection is necessary, but it also splits focus
text into tokens. A token scan therefore matched user prose: `adversarial-review
"why does --help start a review"` produced a --help token and printed usage, so
that review could never be run at all. That is a hard block on legitimate input,
not merely a surprise, and it is a defect the previous commit introduced.

Help detection now parses rather than scans, and treats the request as help only
when the flag is present AND nothing else was asked for -- no focus text left
over:

  return options.help === true && positionals.length === 0;

That keeps every case right:

  --help                            -> usage
  --base main --help                -> usage (main is consumed as a value)
  --model spark -h                  -> usage
  "why does --help start a review"  -> review, focus intact
  "review the help system"          -> review, focus intact

Test: focus text containing --help reaches turn/start with the focus preserved.
Verified non-vacuous against 5770f78, where it fails.

Full suite: 96 passing / 0 failing of 96.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5770f78873

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// raw token comparison misses the flag -- the handler would then split it itself and
// start a full review with --help as focus text, which is exactly what this prevents.
// Checked before the switch, so help can never reach a handler that would dispatch.
if (USAGE_LINES.has(subcommand) && isHelpRequest(normalizeArgv(argv))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve help flags inside a quoted focus argument

When the companion is invoked directly with a single quoted focus argument, such as adversarial-review "review --help handling", the shell removes the quotes and normalizeArgv splits that one positional into review, --help, and handling; this check then prints usage instead of running the requested focused review. Escaped/quoted raw tokens and tokens after the parser's -- delimiter have the same problem because help detection discards that positional context, so detect help without treating protected focus text as a flag.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in 50648ea, before this comment arrived — I found the same thing reviewing my own change and posted it above.

adversarial-review "review --help handling" now runs the focused review with the focus intact, because help requires the flag and no focus text left over rather than a token match.

Your point about the -- delimiter is handled too, and by the same mechanism rather than a special case: after addad6f, detection parses with the subcommand's real schema, so anything following -- is a positional and a literal --help in focus text stays focus text.

"review --help handling"  -> review, focus intact
"-- --help"               -> review, focus intact

There is a regression test for the quoted-focus case. To be straight about it, that test passes both with and without the fix, so it is a guard against reintroducing the token-scan approach rather than evidence of the original bug.

@cjsteigerwald

Copy link
Copy Markdown
Author

Follow-up: I checked the concern I raised in my own review request, and it was real — so 50648ea fixes it before it reaches you.

Normalizing argv is necessary for the P1 fix, but it also splits focus text into tokens. My 5770f78 token scan therefore matched user prose:

$ adversarial-review "why does --help start a review"
Usage: ...

That review could never be run at all — a hard block on legitimate input, and a defect 5770f78 introduced while fixing the P1.

Help detection now parses instead of scanning, and treats it as help only when the flag is present and nothing else was asked for:

return options.help === true && positionals.length === 0;

Behaviour across the cases that matter:

input result
--help usage
--base main --help usage (main consumed as a value)
--model spark -h usage
"why does --help start a review" review, focus intact
"review the help system" review, focus intact

Test asserts the focus text reaches turn/start intact; verified non-vacuous against 5770f78, where it fails.

Current head is 50648ea. Full suite 96 passing / 0 failing of 96.

@codex review — please review 50648ea rather than 5770f78.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50648ea762

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

function isHelpRequest(argv) {
const { options, positionals } = parseArgs(normalizeArgv(argv), {
valueOptions: HELP_DETECTION_VALUE_OPTIONS,
booleanOptions: ["help"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse recognized boolean options before deciding help

When help follows a valid boolean option, such as the plugin-shaped adversarial-review "--wait --help", this parser recognizes only help, so --wait becomes a positional and isHelpRequest returns false. The actual review parser then consumes --wait while leaving --help as focus text, starting a full Codex review instead of printing usage. Fresh evidence in the current head is that the help parser omits the boolean options accepted by each handler; reuse the subcommand's option schema or include those options here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in addad6f — this was a real bug in the head you reviewed.

adversarial-review "--wait --help" put --wait into positionals (my help parser did not know it), the "no focus text left over" rule saw a positional and returned false, and the real parser then consumed --wait and reviewed --help as focus text.

I took your first suggestion rather than the second, because the hand-maintained list was the defect — its contents were only the symptom, and any option added to a handler later would have reopened the gap silently. There is now one schema per subcommand:

const COMMAND_OPTION_SCHEMAS = new Map([
  ["setup", { valueOptions: ["cwd"], booleanOptions: ["json", "enable-review-gate", "disable-review-gate"] }],
  ["review", REVIEW_OPTION_SCHEMA],
  ["adversarial-review", REVIEW_OPTION_SCHEMA],
  ...
]);

read by both the handler and help detection. The handlers' inline literals are now lookups into it, which is the only way the guarantee holds over time.

Behaviour:

input result
--wait --help usage
--background --json --help usage
--base main --help usage
status --all --help usage
"why does --help start a review" review, focus intact
"review --help handling" review, focus intact
"-- --help" review, focus intact

Test asserts --wait --help prints usage and starts no turn; non-vacuous against 50648ea, where it fails. Full suite 98 passing / 0 failing of 98.

Addresses the P1 review finding on 50648ea.

Help detection parsed with its own hand-maintained option list, so it did not
know options the handlers accept. `adversarial-review "--wait --help"` therefore
put --wait into positionals, the "no focus text left over" rule saw a positional
and returned false, and the real parser then consumed --wait and started a full
review with --help as focus text -- the behaviour this PR exists to prevent.

The list was the defect, not its contents: any option added to a handler later
would reintroduce the same gap silently. There is now one schema per subcommand
in COMMAND_OPTION_SCHEMAS, read by both the handler and help detection, so the
two cannot disagree. The handlers' inline literals are replaced by lookups into
it, which is the only way the guarantee holds over time.

Parsing with the real schema also fixes the `--` case for free: anything after
the delimiter is a positional, so a literal `--help` in focus text stays focus
text rather than being read as a flag.

Behaviour:

  --help                            -> usage
  --wait --help                     -> usage
  --background --json --help        -> usage
  --base main --help                -> usage
  status --all --help               -> usage
  "why does --help start a review"  -> review, focus intact
  "review --help handling"          -> review, focus intact
  "-- --help"                       -> review, focus intact

Tests: help following another recognized flag prints usage and starts no turn
(non-vacuous against 50648ea, where it fails); and a quoted focus argument keeps
its help-looking words, which passes both ways and is a guard rather than
evidence, since 50648ea already handled that case.

Full suite: 98 passing / 0 failing of 98.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

Current head is addad6f; the previous passes covered 6622ade, 5770f78 and 50648ea.

This round replaces the hand-maintained help-detection option list with one COMMAND_OPTION_SCHEMAS map per subcommand, read by both the handlers and help detection so the two cannot drift.

Worth scrutinising:

  • whether replacing each handler's inline parse config with a lookup changed any subcommand's accepted options — the schemas were transcribed from the literals, and a transcription slip would silently narrow or widen a parser
  • whether result, cancel or transfer accept options I did not carry across
  • whether task-worker and task-resume-candidate, which are deliberately not in the map and not in USAGE_LINES, are correctly excluded from help handling

addad6f claimed the handlers' inline parse literals were replaced by lookups
into COMMAND_OPTION_SCHEMAS. That was true for setup, review, adversarial-review,
task, transfer and status, but not for result and cancel, which kept literals
duplicating their map entries. Those two could therefore still drift from help
detection -- the exact failure that commit set out to make impossible.

Both now read the shared schema, so every subcommand in USAGE_LINES has one
definition. handleTaskWorker and handleTaskResumeCandidate keep their own
literals deliberately: neither is in USAGE_LINES, so neither is help-handled,
and task-worker's --job-id has no business in a user-facing schema.

No behaviour change -- the literals and the map entries were identical, which is
why the suite did not catch the gap. Verified by hand as well:

  result --json         -> parses (reports no finished jobs)
  cancel --json         -> parses (reports nothing to cancel)
  result --help         -> usage
  cancel "--json --help" -> usage

Full suite: 98 passing / 0 failing of 98.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: addad6f071

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +126 to +130
const { options, positionals } = parseArgs(normalizeArgv(argv), {
valueOptions: schema.valueOptions ?? [],
booleanOptions: [...(schema.booleanOptions ?? []), "help"],
aliasMap: { ...(schema.aliasMap ?? {}), h: "help" }
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse the shared -C alias during help detection

When help is combined with the accepted -C <dir> option, this alias map does not recognize -C, so it and its value become positionals and isHelpRequest returns false. parseCommandInput subsequently recognizes -C as cwd but leaves --help as positional input, causing commands such as task or adversarial-review to dispatch work instead of printing usage. Include the shared C: "cwd" alias here or perform help detection through parseCommandInput.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in a057dac — and worth flagging that this still reproduced on fda50dc, which the later automated pass reported clean. I verified before fixing:

adversarial-review "-C /tmp --help"  -> started a review
task "-C /tmp --help"                -> started a task
status "-C /tmp --help"              -> looked for a job named "--help"
adversarial-review "--cwd /tmp --help" -> Usage:   (control)

Root cause exactly as you describe: isHelpRequest called parseArgs directly, so it never saw the C: "cwd" alias that parseCommandInput injects.

I took your second suggestion rather than the first. Adding C: "cwd" to the alias map here would have worked today, but it is the same shape as the bug you caught in the previous round — a second copy of the parser's knowledge that drifts the moment a shared alias is added. Detection now goes through parseCommandInput, the entry point the handlers use, so it inherits the alias and the argv normalization instead of restating them:

const { options, positionals } = parseCommandInput(argv, {
  ...schema,
  booleanOptions: [...(schema.booleanOptions ?? []), "help"],
  aliasMap: { ...(schema.aliasMap ?? {}), h: "help" }
});

That was the last remaining copy.

input result
-C /tmp --help usage
--cwd /tmp --help usage
--wait --help usage
"why does --help start a review" review
"review --help handling" review
"-- --help" review

Test asserts -C <dir> --help prints usage and starts no turn; non-vacuous against fda50dc. Suite 99 passing / 0 failing of 99.

Comment on lines +111 to +112
["result", { valueOptions: ["cwd"], booleanOptions: ["json"] }],
["cancel", { valueOptions: ["cwd"], booleanOptions: ["json"] }]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route result and cancel through their shared schemas

These new result and cancel entries are read only by help detection: handleResult and handleCancel still parse separate inline option literals. Consequently, the promised single-source invariant does not hold for these two public commands, and adding an option to either handler can again make help detection classify that valid option as positional and execute the command rather than show help. Make both handlers consume these map entries like the other public handlers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in fda50dc, which landed before this comment — I found the same gap self-checking addad6f and said so in the thread.

Your diagnosis matches mine exactly: the result and cancel map entries were read only by help detection while the handlers kept inline literals, so the single-source invariant that commit claimed did not actually hold for those two public commands.

Both handlers now consume the map entries like the others. handleTaskWorker and handleTaskResumeCandidate keep their own literals deliberately — neither is in USAGE_LINES, so neither is help-handled, and task-worker's --job-id is not a user-facing option.

@cjsteigerwald

Copy link
Copy Markdown
Author

Two follow-ups from checking my own work, plus one observation about the suite.

1. addad6f overstated what it did. Its message says the handlers' inline parse literals were replaced by lookups into COMMAND_OPTION_SCHEMAS. That was true for setup, review, adversarial-review, task, transfer and status — but not for result and cancel, which kept literals duplicating their map entries. Those two could still drift from help detection, which is precisely what that commit set out to make impossible. Fixed in fda50dc; every subcommand in USAGE_LINES now has exactly one definition.

handleTaskWorker and handleTaskResumeCandidate keep their own literals deliberately — neither is in USAGE_LINES, so neither is help-handled, and task-worker's --job-id has no business in a user-facing schema.

No behaviour change: the literals and map entries were identical, which is why the suite did not catch the gap. Verified by hand too:

result --json           -> parses (no finished jobs)
cancel --json           -> parses (nothing to cancel)
result --help           -> usage
cancel "--json --help"  -> usage

2. On the transcription risk I raised — I compared every schema against the originals at 6622ade and they match field for field. Worth an independent check rather than my own word.

3. A flaky test, reported as an observation. During this work cancel sends turn interrupt to the shared app-server before killing a brokered task failed once under the full suite, then passed 3/3 in isolation and the full suite passed 98/98 twice more. I could not reproduce it again, so I am not claiming a diagnosis — but it failed once with no relevant change in the diff, which suggests timing sensitivity under load rather than anything in this PR.

Current head is fda50dc, suite 98 passing / 0 failing of 98.

@codex review — please review fda50dc.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: fda50dce89

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…tood

Addresses the second P1 on openai#681. It still reproduced on fda50dc, which the
automated pass had reported clean.

Help detection called parseArgs directly, so it never saw the `C: "cwd"` alias
that parseCommandInput injects for every subcommand. `-C <dir> --help` therefore
left -C and its value as positionals, the "no focus text left over" rule
returned false, and the handler dispatched with --help still in the input:

  adversarial-review "-C /tmp --help"  -> started a review
  task "-C /tmp --help"                -> started a task
  status "-C /tmp --help"              -> looked for a job named "--help"

Detection now goes through parseCommandInput, the same entry point the handlers
use, so it inherits the shared alias and argv normalization instead of
restating them. That is the same reason the option schemas were unified: every
copy of the parser's knowledge is a copy that can drift, and this was the last
one left.

  -C /tmp --help    -> usage      "why does --help start a review" -> review
  --cwd /tmp --help -> usage      "review --help handling"         -> review
  --wait --help     -> usage      "-- --help"                      -> review

Test: help combined with -C prints usage and starts no turn. Verified
non-vacuous against fda50dc, where it fails.

Full suite: 99 passing / 0 failing of 99.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

Current head is a057dac. Flagging one thing for this round: the P1 about the -C alias still reproduced on fda50dc, which the pass at 19:12 reported as "Didn't find any major issues". I verified it by hand before fixing — adversarial-review "-C /tmp --help" started a real review on that commit. So the clean verdict was a false negative, and I would rather say so than let it stand as evidence.

a057dac routes help detection through parseCommandInput instead of parseArgs, so it inherits the shared -C alias and argv normalization rather than restating them.

Worth scrutinising:

  • any remaining path that reads option knowledge without going through parseCommandInput
  • whether routing detection through parseCommandInput changes behaviour for a subcommand whose schema sets its own aliasMap (review/adversarial-review/task all set m: "model")
  • whether -- handling still holds now that normalization happens inside parseCommandInput rather than before it

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a057dac675

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

booleanOptions: [...(schema.booleanOptions ?? []), "help"],
aliasMap: { ...(schema.aliasMap ?? {}), h: "help" }
});
return options.help === true && positionals.length === 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor help before canceling a named job

When a job ID accompanies the help flag, as in the plugin-shaped cancel "task-live --help", parsing produces options.help === true and positionals === ["task-live"], so this condition rejects the help request. handleCancel then uses the first positional as the job reference and actually interrupts and marks that job cancelled. Help detection should allow the structured job-ID positional for cancel (and similarly status/result) rather than applying the free-form focus-text rule to every command.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 4747ec2. This was the worst of the findings so far — the others wasted a model turn, this one destroyed state. Reproduced on a057dac:

cancel "job-x --help"  -> No job found for "job-x". Run /codex:status to list known jobs.
status "job-x --help"  -> No job found for "job-x". ...
result "job-x --help"  -> No job found for "job-x". ...
cancel "--help"        -> Usage:    (control)

With a real job present, cancel "task-live --help" cancelled task-live. Asking what a command does should never destroy the thing being asked about.

Your framing was the fix: the "no positionals" rule only makes sense where the positional is arbitrary user prose. Schemas now mark that explicitly, and only free-form subcommands require an empty positional list:

return schema.freeFormPositionals !== true || positionals.length === 0;

review, adversarial-review and task are free-form; setup, transfer, status, result and cancel are not.

input result
cancel "job-x --help" usage
status "job-x --help" usage
result "job-x --help" usage
"why does --help start a review" review
"review --help handling" review
task "explain --help output" task
--wait --help, -C /tmp --help usage

The test writes a running job into state, runs cancel "task-live --help", then asserts usage was printed and the job is still running — printing usage alone would not prove the job survived. Non-vacuous against a057dac, where it is cancelled.

Suite 100 passing / 0 failing of 100.

Addresses the P1 on a057dac. This one was destructive rather than wasteful.

The "help only when no positionals remain" rule was applied to every subcommand.
It exists because review focus text and task prompts are arbitrary user prose,
where a --help token may be something the user meant literally. status, result
and cancel take a structured job id instead, and there the leftover positional
is not ambiguous at all -- so the rule rejected the help request and the handler
proceeded:

  cancel "task-live --help"  -> cancelled task-live
  status "task-live --help"  -> looked up task-live
  result "task-live --help"  -> looked up task-live

Asking what a command does should never destroy the thing being asked about.

Schemas now mark which subcommands take free-form positionals, and only those
require the positional list to be empty:

  return schema.freeFormPositionals !== true || positionals.length === 0;

review, adversarial-review and task are free-form; setup, transfer, status,
result and cancel are not.

  cancel "job-x --help"             -> usage      "why does --help start a review" -> review
  status "job-x --help"             -> usage      "review --help handling"         -> review
  result "job-x --help"             -> usage      "explain --help output" (task)   -> task
  --wait --help / -C /tmp --help    -> usage

Test: a running job in state, `cancel "task-live --help"`, then assert usage was
printed AND the job is still running -- the assertion that matters, since
printing usage alone would not prove the job survived. Verified non-vacuous
against a057dac, where the job is cancelled.

Full suite: 100 passing / 0 failing of 100.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

Current head is 4747ec2.

This round distinguishes subcommands whose positionals are free-form prose (review, adversarial-review, task) from those taking a structured job id (status, result, cancel). Only the former require an empty positional list for a help request to be honoured.

Worth scrutinising:

  • whether any subcommand is classified wrongly — in particular review, which shares a handler with adversarial-review but rejects focus text via validateNativeReviewRequest
  • whether setup and transfer accept positionals I have overlooked, since both are now treated as non-free-form and would let help win over them
  • whether help winning over a structured positional can mask a genuine job reference a user wanted acted on

For context on this thread: seven findings so far on this PR, five of them defects introduced by the preceding fix. I would rather over-request review than assume this round converged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4747ec2956

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// here is automatically known to both, so the two can never disagree again.
const COMMAND_OPTION_SCHEMAS = new Map([
["setup", { valueOptions: ["cwd"], booleanOptions: ["json", "enable-review-gate", "disable-review-gate"] }],
["review", REVIEW_OPTION_SCHEMA],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat review as having no free-form positionals

When --help is accompanied by any positional, such as review "--scope working-tree focus --help", this shared classification suppresses help and validateNativeReviewRequest instead rejects the positional as unsupported focus text. Unlike adversarial-review, review has no valid free-form positional use case, so it should not inherit freeFormPositionals: true; help can safely win over every positional for this subcommand.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in b15c07a. This is the question I flagged for this round, and you answered it correctly — I had it wrong.

Reproduced on 4747ec2:

review "--scope working-tree focus --help"
  -> `/codex:review` ... does not support custom focus text

A help request answered with an unrelated complaint. Your reasoning holds: validateNativeReviewRequest rejects all focus text, so review has no free-form positional use and help can safely win over any positional. It only inherited the classification because it shares a handler with adversarial-review.

The parse options stay shared as REVIEW_PARSE_OPTIONS, since those genuinely are identical. Only the positional semantics differ now:

["review", REVIEW_PARSE_OPTIONS],
["adversarial-review", { ...REVIEW_PARSE_OPTIONS, freeFormPositionals: true }],
input result
review "--scope working-tree focus --help" usage
review "some focus text" unchanged focus-text error
adversarial-review "review --help handling" review, focus intact
adversarial-review "--wait --help" usage
cancel "job-x --help" usage

Test asserts usage is printed, the focus-text error is not emitted, and no turn starts. Non-vacuous against 4747ec2. Suite 101 passing / 0 failing of 101.

Addresses the P2 on 4747ec2.

review and adversarial-review share a handler, so they shared one schema and
review inherited freeFormPositionals: true. But the two differ precisely on
positionals: adversarial-review takes focus text, where a --help token may be
prose the user meant literally, while validateNativeReviewRequest rejects ALL
focus text, so review has no free-form use at all.

The consequence was a help request answered with an unrelated complaint:

  review "--scope working-tree focus --help"
    -> `/codex:review` ... does not support custom focus text

The parse options stay shared as REVIEW_PARSE_OPTIONS, since they genuinely are
identical. Only the positional classification differs: adversarial-review adds
freeFormPositionals, review does not.

  review "--scope working-tree focus --help"  -> usage
  review "some focus text"                    -> unchanged focus-text error
  adversarial-review "review --help handling" -> review, focus intact
  cancel "job-x --help"                       -> usage

Test asserts usage is printed, the focus-text error is NOT emitted, and no turn
starts. Verified non-vacuous against 4747ec2, where it fails.

Full suite: 101 passing / 0 failing of 101.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

Current head is b15c07a.

review no longer inherits freeFormPositionals from the schema it shares with adversarial-review; only adversarial-review and task are classified free-form now.

Worth scrutinising:

  • task is the only remaining free-form subcommand besides adversarial-review. Its positional is the prompt, and --prompt-file is an alternative source — is there an input where task should honour help but the positional rule suppresses it?
  • setup and transfer are classified non-free-form and so let help win over any positional. Neither documents a positional, but I have not proven neither accepts one.
  • whether splitting REVIEW_PARSE_OPTIONS from the classification left handleReviewCommand parsing exactly as before for both subcommands.

Eight findings so far on this PR. I am not treating this round as converged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b15c07ab79

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Only free-form subcommands need to defend against a flag-looking token that the user
// meant as prose. Where the positional is a structured job id, a leftover positional is
// not a reason to dispatch -- `cancel "job-1 --help"` must print usage, not cancel job-1.
return schema.freeFormPositionals !== true || positionals.length === 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Let help override positionals ignored by --prompt-file

When task receives both --prompt-file and a positional, the positional does not protect literal prompt text because readTaskPrompt unconditionally uses the file, yet this rule suppresses help whenever that ignored positional exists. For example, plugin-shaped input task "--prompt-file prompt.txt ignored --help" starts a real Codex turn using prompt.txt instead of printing usage; make the free-form exception conditional on the positional actually being the selected prompt source.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in bc4cb6f. Notably this is the first of the three questions I raised for the previous round — I checked task "--prompt-file p.txt --help" (no positional, prints usage) and concluded it was fine, missing the case where a positional accompanies the file. You found the case I did not construct.

Reproduced on b15c07a:

task "--prompt-file prompt.txt ignored --help"  -> started a real Codex turn using prompt.txt
task "--prompt-file prompt.txt --help"          -> Usage:   (the case I had tested)

readTaskPrompt returns the file unconditionally when --prompt-file is set and never reads the positional, so there was no literal prompt text to protect.

The classification is now a predicate over the parsed options rather than a flag, since whether the positional is the input depends on the other options:

positionalsAreFreeForm: (options) => !options["prompt-file"]

adversarial-review declares () => true — its focus text is always used. Everything else omits it, so help wins over a structured job id or a discarded positional.

input result
task "--prompt-file f.txt ignored --help" usage
task "--prompt-file f.txt --help" usage
task "do the thing --help" task, prompt intact
adversarial-review "why does --help start a review" review, focus intact
cancel "job-x --help" usage
review "--scope working-tree focus --help" usage

Two tests: help wins when --prompt-file displaces the positional (non-vacuous against b15c07a, where a turn starts), and a literal prompt mentioning --help still runs with the prompt intact. The second passes both ways — it guards the protection this change narrows rather than evidencing the bug.

Suite 103 passing / 0 failing of 103.

…e input

Addresses the P1 on b15c07a.

task was marked free-form unconditionally, but readTaskPrompt returns the file
whenever --prompt-file is given and never looks at the positional. A positional
alongside --prompt-file is therefore discarded, so there is no literal prompt
text to protect -- yet it still suppressed help:

  task "--prompt-file prompt.txt ignored --help"
    -> started a real Codex turn using prompt.txt

The classification is now a predicate over the parsed options rather than a
flag, because whether the positional is the input depends on the other options:

  positionalsAreFreeForm: (options) => !options["prompt-file"]

adversarial-review declares () => true; its focus text is always used. Every
other subcommand omits it, so help wins over a structured job id or an ignored
positional.

  task "--prompt-file f.txt ignored --help" -> usage
  task "--prompt-file f.txt --help"         -> usage
  task "do the thing --help"                -> task, prompt intact
  adversarial-review "why does --help ..."  -> review, focus intact
  cancel "job-x --help"                     -> usage
  review "--scope ... focus --help"         -> usage

Tests: help wins when --prompt-file displaces the positional (non-vacuous
against b15c07a, where a turn starts); and a literal prompt mentioning --help
still runs with the prompt intact. The second passes both ways -- it guards the
protection this change narrows rather than evidencing the bug.

Full suite: 103 passing / 0 failing of 103.
@cjsteigerwald

Copy link
Copy Markdown
Author

@codex review

Current head is bc4cb6f.

positionalsAreFreeForm is now a predicate over the parsed options, so a positional blocks help only when the handler will actually use it as input.

Worth scrutinising, in the same shape as the last two findings — a positional that looks like input but is not:

  • task --resume-last / --resume / --fresh: does the positional prompt still reach readTaskPrompt in every combination, or is it displaced the way --prompt-file displaces it?
  • piped stdin: readTaskPrompt falls back to readStdinIfPiped() only when there are no positionals, so stdin should not create a case — but I have not proven it.
  • adversarial-review declares () => true unconditionally. Is there any flag combination where its focus text is ignored the way --prompt-file ignores task's positional?

Nine findings on this PR now, six of them defects introduced by the preceding fix. Still not treating it as converged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc4cb6fdf7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +151 to +155
const { options, positionals } = parseCommandInput(argv, {
...schema,
booleanOptions: [...(schema.booleanOptions ?? []), "help"],
aliasMap: { ...(schema.aliasMap ?? {}), h: "help" }
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect help before consuming option values

When a help flag immediately follows a value-taking option, parseArgs consumes it as that option's value, so options.help remains unset and the handler dispatches. For example, the plugin-shaped adversarial-review "--model --help" starts a review attempt with model --help instead of printing usage; -h and the other value options have the same problem. Treat a standalone help token as help before allowing it to satisfy a missing option value, while preserving explicit inline values such as --model=--help if those must remain supported.

Useful? React with 👍 / 👎.

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