fix(coding-agent): close remaining extension-flag edge cases (GPT-5.5 review)

Three issues from an adversarial review, all rooted in the startup argv parse
running before extensions load:

1. Flag-looking string values (`--name --print`): the extension-aware reparse
   consumed the following token as the value, disagreeing with the startup
   parse that treated `--print` as the built-in flag — so the reparse could
   silently flip command shape. Extension string flags now consume a following
   token only in `--flag=value` form or when it is not flag-looking; pass a
   flag-looking value as `--flag=value`. Keeps both parses consistent.

2. `@file` string values (`--target @notes.md`): file args were processed from
   the startup parse, which misreads the value as a file and reads it into the
   prompt. processFileArguments now runs on the extension-aware parse
   (initialArgs.fileArgs); pipedInput stays early for mode detection.

3. Built-in collisions: an extension flag named like a built-in (e.g. `model`)
   was consumed by the built-in branch and never delivered to the runner.
   registerFlag now rejects names in BUILTIN_FLAG_NAMES with a clear error
   (isolated per-extension by loadExtension's try/catch).

Adds tests for all three plus the documented startup-parse misclassification.
This commit is contained in:
Erik Svilich
2026-05-31 04:45:51 +02:00
committed by can1357
parent 295c52a307
commit d6f887d152
4 changed files with 132 additions and 13 deletions
+56 -1
View File
@@ -54,6 +54,54 @@ export interface Args {
unknownFlags: Map<string, boolean | string>;
}
/**
* Long names of every built-in CLI flag recognized by {@link parseArgs}.
* Extension flags that would shadow one of these are rejected at registration
* (see ExtensionAPI.registerFlag), because a built-in branch in parseArgs would
* consume the flag before the extension ever sees it. Keep in sync with the
* flag branches below.
*/
export const BUILTIN_FLAG_NAMES: ReadonlySet<string> = new Set([
"help",
"version",
"allow-home",
"mode",
"continue",
"resume",
"session",
"fork",
"provider",
"model",
"smol",
"slow",
"plan",
"api-key",
"system-prompt",
"append-system-prompt",
"provider-session-id",
"no-session",
"session-dir",
"models",
"no-tools",
"no-lsp",
"no-pty",
"tools",
"thinking",
"print",
"export",
"hook",
"extension",
"plugin-dir",
"no-extensions",
"no-skills",
"no-rules",
"no-title",
"auto-approve",
"yolo",
"approval-mode",
"skills",
"list-models",
]);
export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
// Work on a copy: the `--option=value` handling below splices the value
// into the array, and callers reuse the same argv (the post-extension
@@ -216,7 +264,14 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
if (extFlag.type === "boolean") {
result.unknownFlags.set(flagName, true);
} else if (extFlag.type === "string" && i + 1 < args.length) {
result.unknownFlags.set(flagName, args[++i]);
// Consume the value in `--flag=value` form, or when the next token
// is not flag-looking. A `-`-prefixed token in space form is left
// to be parsed as its own flag, so this extension-aware parse agrees
// with the extension-unaware startup parse on command shape; pass a
// flag-looking value as `--flag=value`.
if (equalsValueIndex !== -1 || !args[i + 1].startsWith("-")) {
result.unknownFlags.set(flagName, args[++i]);
}
}
}
// Unknown flags without extensionFlags are silently ignored (first pass)