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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user