← reports

Latest evidence: 2026-09-30: tested current builtin CLI paths ALREADY FIXED. Historical reproduction preserved.

#2299 · Builtin memory/automations/custom-instructions CLIs silently ignore unknown flags (core bb and tasks reject them)

Bug Priority: High Effort: Medium cli · automations · custom-instructions · provider-retry · memory open on GitHub 2026-08-24 · base 494f66526

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

Four builtin plugin CLIs (bb memory, bb automation, bb instructions, bb provider-retry) accept any --whatever flag, ignore it, run the command, and exit 0. Core bb commands (commander), bb tasks, bb workflows, bb docs and bb secret reject unknown flags. The worst consequence is a silent partial write: bb memory update <id> --summary "..." --details-file x.md applies the summary, drops --details-file (the flag does not exist), bumps the version and prints Updated mem_… to v2. — the record now has a new summary over stale details and nothing told the caller. The same happens with bb instructions set, which goes further and persists the unknown flag as instruction text. The cause is purely plugin-side: bb forwards the whole argv to the plugin untouched ("parsing argv is plugin-owned"), and each of these four plugins hand-rolls a parser that collects every --x into a map and only ever reads the names it knows — nothing checks what was left unread. bb tasks uses the same parser lineage but adds assertAllowed() at every verb, which is exactly what is missing. Reproduced end to end on a live instance and with failing unit tests; a 50-line prototype fix for the memory plugin makes both the tests and the live CLI reject the flags.

2. Claims vs findings

Claim from the issueStatusEvidence
bb memory update <id> --summary … --details-file <path> applies the summary, silently discards --details-file, prints Updated … to vN, exits 0; record left with corrected summary over stale detailsVerifiedLive probe §4 step 3: rc=0, Updated mem_-_ure79ckuw to v2., then get shows "summary": "corrected summary", "details": "d". Unit test memory/unknown-flags.repro.test.ts fails the same way.
The memory parser never rejects any unknown option (add … --zzz-nonsense zzzval → rc 0, record saved)VerifiedLive: add/search/catalog/get/forget all rc=0 with the probe flag; add returned the saved record. Code: parseArgv L327–349 collects every --x; no consumer validates.
core bb (commander) rejects: error: unknown option '--x'Verifiedbb status --zzz-nonsense zzzval → error: unknown option '--zzz-nonsense', rc=1.
builtin tasks rejectsVerifiedbb tasks list/show … --zzz-nonsense → unknown option --zzz-nonsense, rc=1. Ordering control confirmed: tasks show DOESNOTEXIST-1 --zzz-nonsense zzzval fails on the flag, the same command without the flag fails with task not found.
builtin workflows rejectsVerifiedbb workflows list --zzz-nonsense zzzval → Unknown option --zzz-nonsense, rc=1 (plugins/workflows/src/cli.ts parseArguments has an allow-list).
builtin github errors "for the wrong reason" (Unexpected argument "zzzval")Verified (from code)Not installed on my instance; validateGithubCliArgs (plugins/github/server.ts#L395-L400) treats --zzz-nonsense as the sub-argument and zzzval as an excess positional, so the error names zzzval. Still a rejection, so no silent write.
builtin keep-awake errors, prints usageVerifiedbb keep-awake status --zzz-nonsense zzzval → usage line, rc=1. It strips --json only and the remaining tokens fail the positional check. Incidental, not a real parser.
builtin automations (list/show/update/pause) silentVerifiedWith a real project: list/create/show/update/pause/delete … --zzz-nonsense zzzval all rc=0 and performed their action (an automation was created, renamed, paused, deleted with the bogus flag present). Note the command is bb automation (singular). Without --project every verb fails with Missing required option --project, which masks the bug if the probe omits it.
builtin custom-instructions (get, set) silent; set swallows the flag into its variadic positional as dataVerifiedbb instructions set hello world --zzz-nonsense zzzval → rc=0, then get --json → {"instructions":"hello world --zzz-nonsense zzzval"}. The run handler only filters --json and joins the rest with spaces (server.ts#L93-L106).
builtin provider-retry status silentVerifiedbb provider-retry status --zzz-nonsense zzzval → No provider retries are pending., rc=0. requestedThreadId only looks for the first non--- token (cli.ts#L5-L12); note that status --zzz-nonsense zzzval also silently treats zzzval as the thread id.
Mechanism: memory parseArgv collects every --x; option()/requireOption() only pull known names; tasks has the same lineage plus assertAllowed at ~29 call sitesVerifiedSource matches the bundle reading exactly. assertAllowed appears 32 times in plugins/tasks/cli/index.ts. Both parsers share CliError/option/requireOption names; tasks' version is stricter in other ways too (a valued option without a value is an error, duplicates are an error).
bb memory has no *-file flags; tasks has --description-file/--body-fileVerifiedgrep -rn "details-file\|description-file\|body-file": three hits in plugins/tasks/cli/index.ts, none under plugins/memory. Memory's USAGE (L821–829) lists only --details TEXT.
"4 of 9 builtin plugins are not strict"Partially verifiedThe four named are confirmed. The repo has more plugins with CLIs than the issue probed: docs and secrets are strict (allow-lists), connect was not probed. So the count is "at least 4".
Measured on bb 0.39.0UnverifiedI reproduced on 494f66526 (main, 2026-08-24). No commit between the report and base touches these parsers; behaviour is identical.

3. Environment

4. Minimal reproduction

All commands below were run with BB_SERVER_URL pointing at my dev instance (bb = node packages/scripts/dist/commands/run-cli.js). Full scripts and raw output: 2299/repro/ (probe.sh → probe-output.txt, probe-automations.sh → probe-automations-output.txt).

  1. Controls — the strict surfaces reject the probe flag:
    $ bb status --zzz-nonsense zzzval
    [stderr] error: unknown option '--zzz-nonsense'
    rc=1
    
    $ bb tasks list --zzz-nonsense zzzval
    [stderr] unknown option --zzz-nonsense
    rc=1
    
    $ bb workflows list --zzz-nonsense zzzval
    [stderr] Unknown option --zzz-nonsense
    rc=1
  2. bb memory add with an unknown flag — expected a non-zero exit and no write; actual: record saved, rc=0:
    $ bb memory add --scope global --name n2299 --summary s --details d --reason r --zzz-nonsense zzzval --json
    {
      "ok": true,
      "memory": {
        "id": "mem_-_ure79ckuw",
        "scope": "global",
        "name": "n2299",
        "summary": "s",
        "details": "d",
        ...
        "version": 1
      }
    }
    rc=0
  3. The silent partial write from the issue — --details-file is not a memory flag. Expected: unknown option --details-file, rc≠0, record untouched. Actual: summary applied, details untouched, version bumped, rc=0:
    $ printf 'details that should have been written\n' > /tmp/bb-2299-details.md
    $ bb memory update mem_-_ure79ckuw --expected-version 1 --reason "fix summary and details" \
        --summary "corrected summary" --details-file /tmp/bb-2299-details.md
    Updated mem_-_ure79ckuw to v2.
    rc=0
    
    $ bb memory get mem_-_ure79ckuw --json
    {
      "ok": true,
      "memory": {
        "id": "mem_-_ure79ckuw",
        "summary": "corrected summary",
        "details": "d",                      <-- stale; the file was never read
        "writeReason": "fix summary and details",
        "version": 2
      }
    }
    rc=0
  4. Every other memory verb accepts the flag too (search, catalog, get, forget all rc=0; forget … --zzz-nonsense zzzval really deleted the record — see probe-output.txt).
  5. bb instructions — the unknown flag becomes content:
    $ bb instructions set hello world --zzz-nonsense zzzval
    Custom instructions updated
    rc=0
    
    $ bb instructions get --json
    {"instructions":"hello world --zzz-nonsense zzzval"}
    rc=0
  6. bb automation (needs a project id; the verbs fail on --project first if you omit it, which hides the bug):
    $ bb automation create --project proj_gcsxf77yek --name qa2299 --in 1d --script "echo hi" --zzz-nonsense zzzval --json
    { "id": "auto_ebdgzb3jhu4", "name": "qa2299", ... }
    rc=0
    $ bb automation update auto_ebdgzb3jhu4 --project proj_gcsxf77yek --name qa2299-renamed --zzz-nonsense zzzval
    Automation auto_ebdgzb3jhu4 updated
    rc=0
    $ bb automation pause auto_ebdgzb3jhu4 --project proj_gcsxf77yek --zzz-nonsense zzzval
    Automation auto_ebdgzb3jhu4 paused
    rc=0
    $ bb automation delete auto_ebdgzb3jhu4 --project proj_gcsxf77yek --yes --zzz-nonsense zzzval
    Automation auto_ebdgzb3jhu4 deleted
    rc=0
  7. bb provider-retry:
    $ bb provider-retry status --zzz-nonsense zzzval
    No provider retries are pending.
    rc=0

Unit-level repro (fails on 494f66526)

plugins/memory/unknown-flags.repro.test.ts — run from plugins/memory with pnpm exec vitest run unknown-flags.repro.test.ts. Both tests fail on base: the first because exitCode is 0 and the record is saved; the second because update exits 0 with "summary": "new summary", "details": "old details", "version": 2. Log: memory-repro-test.log. Saved copy: memory-unknown-flags.repro.test.ts.

// Repro for get-bb/bb#2299: the memory plugin's CLI parser never rejects an
// unknown option. `bb memory update <id> --summary ... --details-file <path>`
// applies the summary, silently drops --details-file, and exits 0.
import { describe, expect, it } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import memoryPlugin from "./server";

const ctx = { projectId: "project-a", threadId: "thread-a" };

async function load() {
  const host = createFakePluginHost({ pluginId: "memory" });
  await memoryPlugin(host.bb);
  return host;
}

describe("#2299 memory CLI ignores unknown flags", () => {
  it("add: rejects --zzz-nonsense instead of saving the record", async () => {
    const host = await load();
    const result = await host.harness.runCli(
      [
        "add", "--scope", "global", "--name", "n", "--summary", "s",
        "--details", "d", "--reason", "r", "--zzz-nonsense", "zzzval", "--json",
      ],
      ctx,
    );
    // FAILS on 494f66526: exitCode is 0 and the record is saved.
    expect(result.exitCode, result.stdout).not.toBe(0);
    expect(result.stderr).toMatch(/unknown option --zzz-nonsense/);
  });

  it("update: --details-file is dropped, summary applied, version bumped (silent partial write)", async () => {
    const host = await load();
    const added = await host.harness.runCli(
      [
        "add", "--scope", "global", "--name", "n", "--summary", "old summary",
        "--details", "old details", "--reason", "r", "--json",
      ],
      ctx,
    );
    expect(added.exitCode, added.stderr).toBe(0);
    const { memory } = JSON.parse(added.stdout) as {
      memory: { id: string; version: number };
    };

    const updated = await host.harness.runCli(
      [
        "update", memory.id, "--expected-version", String(memory.version),
        "--reason", "fix", "--summary", "new summary",
        "--details-file", "/tmp/does-not-matter.md", "--json",
      ],
      ctx,
    );
    const got = await host.harness.runCli(["get", memory.id, "--json"], ctx);
    const record = JSON.parse(got.stdout).memory as {
      version: number; summary: string; details: string;
    };

    // FAILS on 494f66526: exit 0, v2, summary "new summary", details "old details".
    expect(updated.exitCode, `update exited 0 with stdout ${updated.stdout}; record now ${JSON.stringify(record)}`).not.toBe(0);
    expect(record.version).toBe(1);
    expect(record.summary).toBe("old summary");
  });
});

A second file, plugins/custom-instructions/unknown-flags.repro.test.ts (copy, log), fails with stored instructions: "hello world --zzz-nonsense zzzval".

5. Root cause

Where argv is parsed. Plugin commands are not commander commands. apps/cli/src/index.ts only lets commander own core names; an unknown top-level word is looked up in the server's contributions and the raw tail of argv is POSTed to /api/v1/plugins/:id/cli unchanged (index.ts#L152-L154, plugin-cli-proxy.ts#L410-L431). The SDK contract says so explicitly: "Parsing argv is plugin-owned" (backend-contract.ts#L336-L342). So commander's strictness never applies to bb memory …, and each plugin's hand-rolled parser decides.

memory (server.ts#L327-L359): parseArgv puts every --name value into options and every bare --name into flags; option()/requireOption() only read. The dispatcher (#L996) calls parseArgv(rest) and goes straight to the verb. In update (#L1084-L1133) the "at least one field" check passes because --summary is present, details: option(args, "details") is undefined so the store keeps the old details, and store.update bumps the version. Every guard behaves correctly; the flag simply never reaches anything that could object.

function parseArgv(argv: string[]): ParsedArgv {
  ...
    const name = token.slice(2);
    const next = argv[index + 1];
    if (next === undefined || next.startsWith("--")) {
      flags.add(name);            // any --x is accepted
      continue;
    }
    const values = options.get(name) ?? [];
    values.push(next);
    options.set(name, values);    // any --x value is accepted
  ...
}
function option(args: ParsedArgv, name: string): string | undefined {
  const values = args.options.get(name);
  return values?.[values.length - 1];   // nothing ever asks what was NOT read
}

tasks has the same parser shape but adds an allow-list check and calls it at the top of every verb (args.ts#L61-L73; 32 call sites in cli/index.ts). That single function is the whole difference the issue observed.

export function assertAllowed(args, allowedOptions, allowedFlags = []) {
  const optionNames = new Set(allowedOptions);
  const flagNames = new Set(["help", "json", ...allowedFlags]);
  for (const name of args.options.keys())
    if (!optionNames.has(name)) throw new CliError(`unknown option --${name}`);
  for (const name of args.flags)
    if (!flagNames.has(name)) throw new CliError(`unknown option --${name}`);
}

automations (cli.ts#L49-L90): same pattern — parseArgs fills a flags map from every --x[=v]; flag()/requireFlag()/boolFlag() read; no verb checks for leftovers. custom-instructions (server.ts#L92-L106) has no parser at all: it removes --json and joins whatever remains into the instruction text, so an unknown flag is persisted as data. provider-retry (cli.ts#L5-L12) picks the first non--- token as the thread id and checks args.includes("--json"); anything else is ignored (and --zzz-nonsense zzzval makes zzzval the thread id).

Why it matters beyond the probe. A memory record is the agent's durable knowledge; a summary that claims something its details do not contain is exactly the failure the issue describes, and it is invisible to --expected-version, to the exit code, and to the version bump. For bb instructions set the ignored flag is injected into every future agent prompt. Each plugin built its own parser because the SDK gives plugin authors nothing shared here; the docs and secrets plugins each wrote their own allow-list too. The deeper issue is that there is no common, strict argv helper in @get-bb/plugin-sdk, so strictness depends on whoever wrote each plugin.

6. Proposed fix (first principles)

Immediate (per plugin, no wire change, no protocol bump): reject unread options at the top of each verb.

Structural: ship one strict argv helper in @get-bb/plugin-sdk (shape of tasks/cli/args.ts: parseArgs + assertAllowed + option/requireOption) and move the builtin plugins onto it. Per AGENTS.md any new public SDK export needs an experimental_ prefix and an entry in docs/api_to_audit.md. The secondary ask (--details-file/--summary-file for memory) is a separate feature; with strict parsing in place, passing it before it exists becomes a loud error instead of a quiet one.

Risks: any script currently passing a misspelled or stale flag to these commands will start failing — which is the point. No server/daemon wire shape changes, so HOST_DAEMON_PROTOCOL_VERSION is not involved.

7. PR review

No open PR is linked to this issue (gh pr list --search 2299 returns nothing).

8. Related issues

9. Appendix

Artifacts

Commands run (abridged)

gh api repos/get-bb/bb/issues/2299 --jq .body
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
git fetch origin main; git log 494f66526..origin/main --oneline        # empty
scripts/bb-dev-app current                                           # :11750 / :19750 / :27750
pnpm bb:dev plugin install builtin:memory --yes; ... builtin:tasks --yes; plugin enable workflows
curl -X POST $BB_SERVER_URL/api/v1/projects ... /tmp/bb-2299-scratch # proj_gcsxf77yek
2299/repro/probe.sh; PROJECT=proj_gcsxf77yek 2299/repro/probe-automations.sh
cd plugins/memory && pnpm exec vitest run unknown-flags.repro.test.ts   # 2 failed (bug)
cd plugins/custom-instructions && pnpm exec vitest run unknown-flags.repro.test.ts  # 2 failed (bug)
git apply 2299/repro/memory-fix.patch; pnpm exec vitest run unknown-flags.repro.test.ts server.test.ts  # 10 passed
2299/repro/probe-with-fix.sh                                          # live CLI now rejects
git checkout -- plugins/memory/server.ts
pnpm dev:stop; rm -rf ~/.bb-dev/<instance> /tmp/bb-2299-scratch

Full live probe output

########## CONTROLS (expected: reject) ##########
$ bb status --zzz-nonsense zzzval
[stderr] error: unknown option '--zzz-nonsense'
rc=1

$ bb tasks list --zzz-nonsense zzzval
[stderr] unknown option --zzz-nonsense
rc=1

$ bb workflows list --zzz-nonsense zzzval
[stderr] Unknown option --zzz-nonsense
rc=1

$ bb keep-awake status --zzz-nonsense zzzval
[stderr] Usage: bb keep-awake <status|enable|disable|hosts> [arguments] [--json]
rc=1

########## memory ##########
$ bb memory add --scope global --name n2299 --summary s --details d --reason r --zzz-nonsense zzzval --json
{ "ok": true, "memory": { "id": "mem_-_ure79ckuw", ..., "version": 1 } }
rc=0

$ bb memory search n2299 --zzz-nonsense zzzval
mem_-_ure79ckuw v1 [global/fact] n2299
rc=0

$ bb memory catalog --zzz-nonsense zzzval
mem_-_ure79ckuw v1 [global/fact] n2299
rc=0

$ bb memory get n2299 --zzz-nonsense zzzval
mem_-_ure79ckuw v1 [global/fact] n2299
rc=0

$ bb memory update mem_-_ure79ckuw --expected-version 1 --reason "fix summary and details" --summary "corrected summary" --details-file /tmp/bb-2299-details.md
Updated mem_-_ure79ckuw to v2.
rc=0

$ bb memory get mem_-_ure79ckuw --json
{ "ok": true, "memory": { "summary": "corrected summary", "details": "d", "version": 2, ... } }
rc=0

$ bb memory forget mem_-_ure79ckuw --expected-version 2 --reason cleanup --zzz-nonsense zzzval
Forgot mem_-_ure79ckuw at v3.
rc=0

########## automations (command name: automation) ##########
$ bb automation list --zzz-nonsense zzzval
[stderr] Missing required option --project <value>.
rc=1
(with --project proj_gcsxf77yek: list/create/show/update/pause/delete all rc=0 — see probe-automations-output.txt)

########## custom-instructions (command name: instructions) ##########
$ bb instructions get --zzz-nonsense zzzval
rc=0

$ bb instructions set hello world --zzz-nonsense zzzval
Custom instructions updated
rc=0

$ bb instructions get --json
{"instructions":"hello world --zzz-nonsense zzzval"}
rc=0

########## provider-retry ##########
$ bb provider-retry status --zzz-nonsense zzzval
No provider retries are pending.
rc=0

2026-09-30 current-main verification

ALREADY FIXED — high confidence for the tested builtin CLI argument boundary at d57836d6c75ba1623d62b93bd9f061675a9045af. This is new current-main evidence strengthening an existing report, not a new report or a retraction of the historical reproduction. The historical all-verbs/live-runtime matrix has not been repeated.

Expected and actual

Expected: unknown options produce a nonzero exit before reading or mutating plugin state; recognized commands still succeed. Actual: all 15 unsupported-flag cases rejected with an unknown-option diagnostic. Memory and custom-instructions returned 1; automation returned 1; provider-retry returned 2. Valid controls returned 0.

Faithfulness and limits

The test invokes actual builtin CLI registrations through the repository's plugin host harness and shared parser. Memory uses real temporary SQLite storage and plugin migrations; custom-instructions uses isolated in-process settings, not production persistence. Automation calls terminate in capture-only adapters; provider-retry uses a synthetic queue reader. Every unstubbed SDK operation fails instead of contacting a server. This tests argument handling and write prevention, not server transport, installed CLI packaging, daemon persistence, real scheduling, providers or browser UI. Other memory verbs, automation create/show/delete, other plugins, core CLI/tasks comparison and help behavior remain outside this new verification. No visual behavior is claimed, so no new screenshot is required.

Two clean executions

The SAME agent personally executed the identical test in two clean detached checkouts at the full recorded SHA, each with a separate frozen installation and fresh synthetic storage. This is same-agent clean repetition, not independent verification. Both final tests executed with Turbo force (zero cached test tasks), passed, and emitted identical captured evidence. Both memory plugin builds passed; upstream build caches may be reused. Environment: Linux, Node 24.19.0, repository-pinned pnpm 9.15.0, Vitest 4.1.1. No extra dependency was added. Only reproducible dependencies from a completed investigation were reclaimed; prior tests, logs and reports were retained.

Exact repeatable steps

git clone https://github.com/get-bb/bb.git first
cd first
git checkout --detach d57836d6c75ba1623d62b93bd9f061675a9045af
corepack pnpm install --frozen-lockfile
# Save the inline test as plugins/memory/issue2299.test.ts
corepack pnpm exec turbo run test --filter=bb-plugin-memory --force -- issue2299.test.ts --silent=false
corepack pnpm exec turbo run build --filter=bb-plugin-memory
# Repeat in a second clean checkout at the identical SHA, with fresh fixture state.

Execution used the existing pinned package-manager launcher and package store; private local cache paths are omitted. The test is independently derived from trusted repository implementation and existing test harness conventions. No commands, patches or scripts from the issue or historical report were executed.

import { expect, it } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import memoryPlugin from "./server";
import instructionsPlugin from "../custom-instructions/server";
import { registerAutomationCli } from "../automations/src/cli";
import type { AutomationService } from "../automations/src/service";
import { registerProviderRetryCli } from "../provider-retry/src/cli";

it("rejects unsupported flags before synthetic writes across four builtin CLIs", async () => {
  const evidence: Record<string, unknown> = {};
  const memory = createFakePluginHost({ pluginId: "memory" });
  const instructions = createFakePluginHost({ pluginId: "custom-instructions" });
  const automation = createFakePluginHost({ pluginId: "automations" });
  let queueReads = 0;
  const retry = createFakePluginHost({ pluginId: "provider-retry", sdk: {
    threads: { queue: { list: async () => { queueReads++; return []; } } },
  } });
  const unsupported = [["--unsupported-probe"], ["--unsupported-probe", "synthetic-value"]];
  async function rejects(host: typeof memory, argv: string[]) {
    const result = await host.harness.runCli(argv, { projectId: "synthetic-project" });
    expect(result.exitCode).not.toBe(0);
    expect(`${result.stdout ?? ""}${result.stderr ?? ""}`).toContain("unknown option");
    return { exitCode: result.exitCode, unknownOption: true };
  }
  try {
    await memoryPlugin(memory.bb);
    await instructionsPlugin(instructions.bb);
    registerProviderRetryCli(retry.bb);
    const add = ["add", "--scope", "global", "--name", "fixture", "--summary", "original summary", "--details", "original details", "--reason", "synthetic control", "--json"];
    const rejectedAdds = [];
    for (const extra of unsupported) rejectedAdds.push(await rejects(memory, [...add, ...extra]));
    const catalogBefore = await memory.harness.runCli(["catalog", "--scope", "all", "--json"]);
    const added = await memory.harness.runCli(add);
    expect(added.exitCode, added.stderr).toBe(0);
    const record = JSON.parse(added.stdout).memory;
    const update = ["update", record.id, "--expected-version", "1", "--summary", "new summary", "--reason", "synthetic control", "--json"];
    const rejectedUpdates = [];
    for (const extra of [...unsupported, ["--details-file", "synthetic-unused.txt"]]) rejectedUpdates.push(await rejects(memory, [...update, ...extra]));
    const get = async () => JSON.parse((await memory.harness.runCli(["get", record.id, "--json"])).stdout).memory;
    const unchanged = await get();
    expect(unchanged).toMatchObject({ version: 1, summary: "original summary", details: "original details" });
    const validUpdate = await memory.harness.runCli([...update, "--details", "new details"]);
    expect(validUpdate.exitCode, validUpdate.stderr).toBe(0);
    expect(await get()).toMatchObject({ version: 2, summary: "new summary", details: "new details" });
    evidence.memory = { rejectedAdds, catalogBefore: JSON.parse(catalogBefore.stdout), rejectedUpdates, unchanged: { version: unchanged.version, summary: unchanged.summary, details: unchanged.details }, validUpdateExit: validUpdate.exitCode, validFinal: { version: 2, summary: "new summary", details: "new details" } };
    expect((await instructions.harness.runCli(["set", "original text"])).exitCode).toBe(0);
    const rejectedSets = [];
    for (const extra of unsupported) rejectedSets.push(await rejects(instructions, ["set", "new text", ...extra]));
    const after = await instructions.harness.runCli(["get"]);
    expect(after.stdout).toBe("original text");
    expect((await instructions.harness.runCli(["set", "--", "--literal-text"])).exitCode).toBe(0);
    expect((await instructions.harness.runCli(["get"])).stdout).toBe("--literal-text");
    evidence.instructions = { rejectedSets, unchanged: after.stdout, explicitDoubleDashLiteral: "--literal-text" };
    const calls: unknown[] = [];
    const adapter = {
      list: (input: unknown) => { calls.push({ method: "list", input }); return []; },
      update: async (input: unknown) => { calls.push({ method: "update", input }); return { id: "synthetic-automation" }; },
      pause: (input: unknown) => { calls.push({ method: "pause", input }); return { id: "synthetic-automation" }; },
    };
    registerAutomationCli({ bb: automation.bb, service: adapter as unknown as AutomationService });
    const rejectedAutomation = [];
    for (const argv of [["list", "--project", "synthetic-project", "--json"], ["update", "synthetic-automation", "--project", "synthetic-project", "--name", "new name", "--json"], ["pause", "synthetic-automation", "--project", "synthetic-project", "--json"]]) {
      for (const extra of unsupported) rejectedAutomation.push(await rejects(automation, [...argv, ...extra]));
      expect(calls).toHaveLength(0);
    }
    const controls = [];
    for (const argv of [["list", "--project", "synthetic-project", "--json"], ["update", "synthetic-automation", "--project", "synthetic-project", "--name", "new name", "--json"], ["pause", "synthetic-automation", "--project", "synthetic-project", "--json"]]) {
      const result = await automation.harness.runCli(argv);
      expect(result.exitCode, result.stderr).toBe(0); controls.push(result.exitCode);
    }
    expect(calls).toHaveLength(3);
    evidence.automation = { rejectedAutomation, rejectedServiceCalls: 0, controlExits: controls, capturedControlCalls: calls };
    const rejectedRetry = [];
    for (const extra of unsupported) rejectedRetry.push(await rejects(retry, ["status", "--json", ...extra]));
    expect(queueReads).toBe(0);
    const retryControl = await retry.harness.runCli(["status", "--json"]);
    expect(retryControl.exitCode).toBe(0); expect(queueReads).toBe(1);
    evidence.providerRetry = { rejectedRetry, invalidQueueReads: 0, controlExit: retryControl.exitCode, controlQueueReads: queueReads, result: JSON.parse(retryControl.stdout) };
    console.log("ISSUE2299_EVIDENCE=" + JSON.stringify(evidence));
  } finally {
    for (const host of [memory, instructions, automation, retry]) await host.harness.dispose();
  }
});

Actual evidence from both runs

{
  "outcomes": {
    "first": {
      "memory": {
        "rejectedAdds": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "catalogBefore": {
          "ok": true,
          "scope": "all",
          "memories": [],
          "total": 0
        },
        "rejectedUpdates": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "unchanged": {
          "version": 1,
          "summary": "original summary",
          "details": "original details"
        },
        "validUpdateExit": 0,
        "validFinal": {
          "version": 2,
          "summary": "new summary",
          "details": "new details"
        }
      },
      "instructions": {
        "rejectedSets": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "unchanged": "original text",
        "explicitDoubleDashLiteral": "--literal-text"
      },
      "automation": {
        "rejectedAutomation": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "rejectedServiceCalls": 0,
        "controlExits": [
          0,
          0,
          0
        ],
        "capturedControlCalls": [
          {
            "method": "list",
            "input": {
              "projectId": "synthetic-project"
            }
          },
          {
            "method": "update",
            "input": {
              "projectId": "synthetic-project",
              "automationId": "synthetic-automation",
              "name": "new name"
            }
          },
          {
            "method": "pause",
            "input": {
              "projectId": "synthetic-project",
              "automationId": "synthetic-automation"
            }
          }
        ]
      },
      "providerRetry": {
        "rejectedRetry": [
          {
            "exitCode": 2,
            "unknownOption": true
          },
          {
            "exitCode": 2,
            "unknownOption": true
          }
        ],
        "invalidQueueReads": 0,
        "controlExit": 0,
        "controlQueueReads": 1,
        "result": {
          "retries": []
        }
      }
    },
    "second": {
      "memory": {
        "rejectedAdds": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "catalogBefore": {
          "ok": true,
          "scope": "all",
          "memories": [],
          "total": 0
        },
        "rejectedUpdates": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "unchanged": {
          "version": 1,
          "summary": "original summary",
          "details": "original details"
        },
        "validUpdateExit": 0,
        "validFinal": {
          "version": 2,
          "summary": "new summary",
          "details": "new details"
        }
      },
      "instructions": {
        "rejectedSets": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "unchanged": "original text",
        "explicitDoubleDashLiteral": "--literal-text"
      },
      "automation": {
        "rejectedAutomation": [
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          },
          {
            "exitCode": 1,
            "unknownOption": true
          }
        ],
        "rejectedServiceCalls": 0,
        "controlExits": [
          0,
          0,
          0
        ],
        "capturedControlCalls": [
          {
            "method": "list",
            "input": {
              "projectId": "synthetic-project"
            }
          },
          {
            "method": "update",
            "input": {
              "projectId": "synthetic-project",
              "automationId": "synthetic-automation",
              "name": "new name"
            }
          },
          {
            "method": "pause",
            "input": {
              "projectId": "synthetic-project",
              "automationId": "synthetic-automation"
            }
          }
        ]
      },
      "providerRetry": {
        "rejectedRetry": [
          {
            "exitCode": 2,
            "unknownOption": true
          },
          {
            "exitCode": 2,
            "unknownOption": true
          }
        ],
        "invalidQueueReads": 0,
        "controlExit": 0,
        "controlQueueReads": 1,
        "result": {
          "retries": []
        }
      }
    }
  },
  "runs": {
    "first": {
      "test": "1 passed, 15 rejected unsupported-flag cases plus valid controls; fresh execution, zero cached tasks",
      "test_tasks": "Tasks:    6 successful, 6 total",
      "build_tasks": "Tasks:    4 successful, 4 total"
    },
    "second": {
      "test": "1 passed, 15 rejected unsupported-flag cases plus valid controls; fresh execution, zero cached tasks",
      "test_tasks": "Tasks:    6 successful, 6 total",
      "build_tasks": "Tasks:    4 successful, 4 total"
    }
  }
}

Verified current mechanism and next step

The shared CLI parser rejects unknown options. Current memory update, instruction set, automation update and retry status declare allowed options through that parser. The CLI unification landed in 3a4288bd0f34f888a5eb43f1099f7b60fe86eea4; the executed current-main tests, not that commit alone, support the scoped fixed verdict. No production fix is proposed for the tested paths. Retain regression coverage and extend the remaining verb/installed-runtime matrix if full end-to-end closure evidence is needed.

Historical findings, inline tests, output and uncertainty are preserved in the original report. Historical raw artifact paths in metadata are not newly recreated or represented as available evidence. The issue's command/code examples were untrusted claims only; no external issue URL, real runtime, account, credential or user data was used. No issue closure or extra comment accompanies this update. Historical confirmed-repro labeling remains because no dedicated already-fixed label convention is established.