#3833 · Unsupported plan requests dispatch normally

Bug · Priority: Medium · Effort: Low · providers · cli · provider-pi · provider-acp
2026-09-17 · Base 3e9bef842539d5a1dd83d648fe9d5a7a20de4397
GitHub issue

Verdict: REPRODUCED · Root-cause confidence: high · Reproduction label: confirmed-repro

TL;DR

The CLI constructs a structured plan command without consulting provider capabilities. When Pi receives that input through the server command builder, the builder succeeds and produces an ordinary start command with no promptMode. A focused test requiring rejection failed identically in two clean checkouts of trusted main. This verifies the missing capability rejection; no live model turn, approval UI, or reported agent reasoning was reproduced.

Claims vs findings

ClaimFindingEvidence
The CLI accepts plan requests without checking capabilities.Verified in sourceBoth spawn and tell call buildPromptInputs, whose plan branch unconditionally constructs the builtin command. The existing CLI suites pass.
Pi declares no plan action and receives no plan mode.Verified dynamicallyThe failing assertion receives a thread.start command, including the original structured plan input and options without promptMode.
The server gate is correct.QualifiedIt correctly omits unsupported plan mode, but does not reject the requested action. That omission is part of the silent failure.
Claude Code supports the action.Verified at command constructionThe existing runtime test asserts promptMode=plan and Claude Code provider options. All 30 tests in that file pass.
ACP has the same declaration.Verified statically onlyIts declaration contains composerActions: []; no ACP runtime was launched.
Live Pi reasoning, missing approval card, plugin toggle controls, and reported release behavior.Unverified hereNo live providers or user runtime data accessed.

Environment

Trusted get-bb/bb origin/main at the full commit above, fetched from the target repository. macOS / Darwin arm64; Node v22.22.3; pnpm 9.15.0 via Corepack; Vitest 4.1.1. Frozen dependency installation and full Turbo build passed (58 tasks). Tests use the repository harness with migrated in-memory SQLite and temporary fixture directories. No application server, provider session, exposed port, or real user data directory was used.

Minimal reproduction

  1. Clone the trusted repository and check out the recorded commit.
  2. Run the frozen install and build.
  3. Save the test below as apps/server/test/threads/issue-3833.test.ts.
  4. Run the focused test. Exit status 1 is the expected observation on this affected commit.
git clone https://github.com/get-bb/bb.git bb-repro
cd bb-repro
git checkout 3e9bef842539d5a1dd83d648fe9d5a7a20de4397
corepack pnpm install --frozen-lockfile --prefer-offline
corepack pnpm exec turbo run build
corepack pnpm exec turbo run test --filter=@bb/server -- test/threads/issue-3833.test.ts

Expected: reject an unsupported structured plan request before constructing an ordinary provider turn. Actual, in both runs:

AssertionError: promise resolved "{ type: 'thread.start', …(15) }" instead of rejecting

The resolved command has type thread.start, providerId pi, input text /plan Review the change with a builtin command mention, and no options.promptMode. The test uses the exact domain constructor called by the CLI, rather than hand-writing a command or relying on model behavior.

Complete reproduction test

import { describe, expect, it } from "vitest";
import { createBuiltinPlanCommandTextInput, encodeClientTurnRequestIdNumber } from "@bb/domain";
import { buildThreadStartCommand } from "../../src/services/threads/thread-commands.js";
import { seedEnvironment, seedHostSession, seedProjectWithSource, seedThread } from "../helpers/seed.js";
import { withTestHarness } from "../helpers/test-app.js";

describe("unsupported plan dispatch", () => {
  it("rejects a structured plan request before dispatching a normal Pi turn", async () => {
    await withTestHarness(async (harness) => {
      const { host } = seedHostSession(harness.deps, { id: "host-plan-repro" });
      const { project } = seedProjectWithSource(harness.deps, { hostId: host.id });
      const environment = seedEnvironment(harness.deps, { hostId: host.id, projectId: project.id });
      const thread = seedThread(harness.deps, { projectId: project.id, environmentId: environment.id, providerId: "pi" });
      const input = [createBuiltinPlanCommandTextInput("Review the change")];
      await expect(buildThreadStartCommand(harness.deps, {
        environment,
        execution: { model: "pi-model", permissionMode: "accept-edits", reasoningLevel: "medium", serviceTier: "default", source: "client/turn/requested" },
        fork: null,
        permissionEscalation: "ask",
        input,
        projectId: project.id,
        providerId: "pi",
        requestId: encodeClientTurnRequestIdNumber({ value: 1 }),
        syncGeneratedTitle: false,
        thread,
      })).rejects.toThrow(/plan/i);
    });
  });
});

Root cause

apps/cli/src/commands/thread/helpers.ts:31-L40 takes a plan boolean but no provider information. Both CLI entry points use it. packages/domain/src/shared-types.ts:314-L341 turns that flag into text plus a structured builtin command mention.

plugins/provider-pi/src/declaration.ts:36 declares no composer actions; plugins/provider-acp/src/declaration.ts:101 does likewise. apps/server/src/services/providers/provider-plan-command.ts:4-L13 returns null for these providers. Then apps/server/src/services/threads/thread-commands.ts:179-L190 immediately returns undefined instead of rejecting the requested action:

const planCommand = resolveProviderPlanCommand(registry, args.providerId);
if (planCommand === null) return undefined;

The command builder preserves the input and omits promptMode from execution options. The existing runtime test even asserts that Pi has no promptMode after receiving a plan mention, explaining why existing checks pass.

Proposed fix and automation scope

Validate structured builtin plan requests against the final resolved provider before accepting them. Keep literal user text distinct from a structured command and preserve supported providers. Check creation after provider fallback resolution and sends before either dispatch or queue acceptance. The server owns this policy, so direct SDK callers should receive the same diagnostic.

No fix branch or PR was created. The assessed implementation requires six files: the shared provider-plan-command helper, thread-create, thread-send-request, regression coverage, the CLI guide chapter, and its skill reference. That exceeds this automation's five-file ceiling. A dispatch-only exception is insufficient: apps/server/src/services/threads/thread-send-request.ts:33-L52 can return successful queue acceptance before dispatch, and creation resolves provider fallback separately. Low estimated implementation effort does not imply eligibility under the stricter automatic-fix constraints.

Verification

The same agent repeated the test in a second fresh clone at the identical commit with a separate frozen installation and temporary harness state. Production files were unchanged; only the reproduction test was added. The identical rejection assertion failed because the promise resolved to thread.start. No report correction was needed. This is a repeated clean run, not an independent review.

First checkout: 1 test failed; promise resolved instead of rejecting.
Second checkout: 1 test failed; same assertion and result.
Existing server runtime configuration suite: 30 tests passed.
Existing CLI spawn/tell suites: 57 tests passed.
Full build: 58 tasks successful.

Related issues and pull requests

No open linked pull request was found in issue timeline metadata or the repository's open-PR search for this issue number. Related-issue claims in the submission were not used as evidence.

Appendix and limits

Raw build and test logs are retained locally, excluded from the public repository. The complete test and exact failure above are sufficient to repeat the result. Additional checks were run with:

corepack pnpm exec turbo run test --filter=@bb/server -- test/threads/thread-runtime-config.test.ts
corepack pnpm exec turbo run test --filter=@bb/cli -- src/__tests__/command-output/thread-spawn.test.ts src/__tests__/command-output/thread-tell.test.ts

The installed pnpm launcher initially referred to a missing installation; a temporary Corepack launcher fixed the local tooling without changing repository dependencies. All issue content was treated as untrusted claims; no issue-supplied command, external link, patch, or script was executed. Live model output and UI behavior remain outside this test's scope.

AGENT GENERATED