Improve CLI recovery, unify plugin commands, and retain terminal output - #3913
Merged
SawyerHood merged 7 commits intoSep 18, 2026
Merged
SawyerHood merged 7 commits into
SawyerHood merged 7 commits into
Conversation
Agents run a command with `bb terminal create --command`, the command exits, and `bb terminal output` then answers "Terminal output is unavailable because the session is not running". Scrollback lived only in the host daemon's live session, which was deleted on exit, and the server refused every output read for a terminal that was not running. There was no way to read the output of a finished command. The daemon now keeps an exited terminal's scrollback for 30 minutes, bounded to 32 terminals and 16 MiB, oldest evicted first, and answers an output replay from it. Input, resize and interactive attach still fail for an exited terminal. The server reads that replay for an exited terminal through the host's connected daemon session, retries once when a terminal exits during a read, and the output response now carries status, exitCode and closeReason. When the scrollback is gone (evicted, daemon restarted, or an older daemon that never kept it) the 409 says so and says why. HOST_DAEMON_PROTOCOL_VERSION moves to 214 because terminal.attach now answers for an exited terminal. An older daemon still answers terminal_not_found, which the server maps to the same explained 409. Co-Authored-By: Claude Code <noreply@anthropic.com>
Every plugin CLI parsed argv by hand, and each failed differently: some
rejected --help or answered it with exit 1, some silently ignored unknown
flags (which lost data on real writes), some reported one missing flag per
run, and none shared an error format.
experimental_defineCli and experimental_cliCommand turn a declared spec
into the registration bb.cli.register already takes. Every command built
this way answers --help at any level with exit 0, names the nearest
command or option for a typo (including under nested paths), reports all
missing required options in one error, validates typed values (boolean,
string, integer, enum, duration), accepts hidden alias spellings, takes a
value that starts with a dash unless it names a declared option, and with
--json prints {"ok": false, "error": {code, message, hint}} on stdout
while stderr keeps the readable text. experimental_CliError gives a
command the same reporting for its own failures.
Registrations built from a spec set experimental_rendersHelp, which the
server passes through its CLI contributions so the bb CLI can forward
--help to the plugin for full option help. A hand-written run leaves it
unset and keeps the host-rendered usage line, because the host cannot know
that such a parser will not act on the remaining arguments.
Plugin SDK 0.4.102.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Sixteen plugin CLIs hand-rolled their argv parsing. The failures agents hit most: `bb memory add` reported one missing flag per run and silently dropped the flags agents guess (--tags, --type, --title, --body), `bb connect --help` answered "Unknown flag --help", `bb browser-automation run --timeout` was rejected because the flag is --timeout-ms, `bb workflows validate|status|list` rejected --json, `bb automation list` failed on a missing --project without naming the thread's project, and `bb tasks list --project proj_...` said "project not found" for a bb project id. Each CLI now declares its commands with experimental_defineCli, so --help works at every level with exit 0, unknown options and stray positionals are rejected with the nearest spelling, all missing required options are reported at once, limits appear in --help, and --json failures use the shared error envelope. Successful output is unchanged; help and parse error wording changed. Notable behavior: - memory add accepts --tags a,b, --type, --title, --body, --text and --content as aliases, and says a bare positional belongs in --details. - browser-automation run accepts --timeout with a unit (90s, 2m, 1500ms); a bare number resolves by range (1-120 seconds, 1000-120000 ms). Its help states the screenshot limits, and run --json failures are JSON. - connect list, tasks get and similar guesses suggest or alias the real command; tasks status says it is plugin health, not a task filter. - A command that requires --project or a thread stays strict but prints the exact flag to add when the invoking thread supplies the value. - --json is accepted on every command that prints data, including workflows, github, account-pool and push-notifications. - A bare `bb <plugin>` with no command prints help with a non-zero exit, matching core command groups (tasks, github and docs exited 0 before). - custom-instructions set with no text is now an error instead of clearing the instructions; clear exists for that. Co-Authored-By: Claude Code <noreply@anthropic.com>
Mining agent transcripts showed how agents misuse the CLI: they guess
verbs that do not exist (thread get, thread message, host list), reuse
flags from sibling commands (terminal output --thread), omit context the
CLI already knows (thread spawn --project), pass durations the parser
silently truncated (`thread wait --timeout 4h` waited 4 seconds), cannot
tell a bare array from a wrapped one under --json, and pass Markdown in
double quotes where the shell executes `backticks` before bb sees them.
Nine percent of all bb invocations were --help lookups.
Errors now carry the fix:
- A parse error prints the nearest command or option (by synonym, then
spelling), the usage line and the valid options. Help for a subcommand
that does not exist fails instead of printing the parent's help.
- A missing --project, --thread, --environment or --host prints the exact
flag to add with the current ID filled in; they stay required.
- With --json a failure prints {"ok": false, "error": {code, message,
hint}} on stdout, unless the command already printed its own payload,
and stderr keeps the readable message.
Guesses that are unambiguous now work: thread get|view|status, thread
message|send, thread messages|timeline, thread create|new, terminal read,
plugin uninstall, environment get, and at the top level host|hosts|
machines, env and the plural group names unless a plugin owns the name.
Terminal verbs that take a terminal ID accept and ignore the scope flags
of list and create; plugin remove accepts --yes.
Also:
- One duration parser for --timeout and --poll-interval on thread wait
and terminal wait: seconds or milliseconds as before, or 90s, 5m, 4h.
- --message-file and --prompt-file (with - for stdin) on thread tell,
spawn, fork, edit-message and queue create|update.
- terminal output reports a finished terminal's exit code; terminal wait
matches text split across polls and, when the terminal exits first,
stops at once with its exit code and last output.
- `bb guide json` documents the --json contract and each command's shape,
and the most-parsed commands end their help with that shape. `bb guide
commands [group]` lists every command with its options on one page, and
guide chapters accept singular and synonym names.
- The CLI forwards --help to a plugin that renders its own help.
- Failed invocations are recorded in <data dir>/logs/cli-errors.jsonl
(command path, error code, unknown command or flag; never argument
values). `bb diagnostics cli-errors` tallies them and BB_CLI_ERROR_LOG=0
turns recording off.
- Node's NO_COLOR warning no longer precedes output when a harness also
sets FORCE_COLOR.
Co-Authored-By: Claude Code <noreply@anthropic.com>
--prompt stopped being a commander required option when --prompt-file was added, so its help line no longer implied it. State the requirement in the option description. Co-Authored-By: Claude Code <noreply@anthropic.com>
The declarative CLI spec shipped behind experimental_ names in this unreleased SDK version. Sixteen built-in plugin CLIs now run on it and it was exercised against a live server, so drop the prefix before it is published: - experimental_defineCli, experimental_cliCommand -> defineCli, cliCommand - experimental_CliError -> PluginCliError - ExperimentalCli* types -> PluginCli* (PluginCliSpec, PluginCliCommand, PluginCliOption and its five members, PluginCliPositional, PluginCliConstraint, PluginCliRunInput, PluginCliOptionValues, PluginCliPositionalValues, PluginCliDurationUnit, PluginCliErrorCode), matching PluginCliRegistration and its siblings - PluginCliRegistration.experimental_rendersHelp -> rendersHelp Audit outcomes, recorded here because the api_to_audit entry is removed: the two-function shape stays, since a single call loses option type inference; rendersHelp stays an explicit registration field so a hand-written run is never sent --help it might ignore; the JSON error envelope matches the core CLI, and a bare or incomplete invocation now reports missing_command as the core CLI does instead of unknown_command; the known gaps found while migrating (typed positionals, a two-token option, a requires-any constraint, a context-aware hint for a required option) are all additive and can land without breaking the spec. Co-Authored-By: Claude Code <noreply@anthropic.com>
An independent review of this branch confirmed ten defects. CLI: - The error log recorded argument values. An unknown option written as --name=value was logged whole, so a mistyped secret flag leaked its value. Only name-shaped tokens are recorded now: a flag with its inline value stripped, or a lowercase command word; anything else is omitted. - `terminal wait` trimmed its retained window before searching, so a marker followed by more than 256 KiB in one read was never matched. It searches the overlap plus the whole new batch, then trims. - `bb hosts --help` and the other soft aliases failed when the server was unreachable, because the plugin lookup exited first. An alias now falls through to the core command, and an unreachable server under --json prints the error envelope (server_unreachable). - The --<name>-stdin rewrite scanned past `--`, so it could consume stdin for a flag meant for a passed-through command. It stops at `--`. Plugin CLI spec: - A leading-dash number is a positional, so `bb pool account priority <id> -1` works again. - `--help` on a command prefix such as `bb tasks project --help` prints that group's commands with exit 0 instead of failing. - defineCli rejects a spec where two options claim one spelling, where split is declared without repeatable (it silently dropped every value after the first), or where a short form is not one character. `short` accepts "f" or "-f". - `--json=true` selects the JSON error envelope like `--json`. Plugins and tests: - docs pull declared --dir as an alias of both --into and --folder; --dir now means --into only. - The browser-automation CLI test types its mock instead of casting through unknown. Co-Authored-By: Claude Code <noreply@anthropic.com>
SawyerHood
deleted the
bb/can-you-look-through-my-entire-history-of-transc-thr_s2gt62gmpr
branch
September 18, 2026 18:30
ymichael
added a commit
that referenced
this pull request
Sep 19, 2026
## Human comments
## What was wrong
Plugin app bundles reached `@hugeicons/core-free-icons` through its ESM
barrel, which re-exports 5123 separate modules, and lucide-react through
its 1756-module one. esbuild has to resolve, open and parse every one
before tree-shaking can drop them.
For the tasks plugin that was **6879 of 7263 modules and 7.98 MB of the
10.6 MB esbuild parsed, contributing 11 KB to a 1115 KB bundle**.
Tree-shaking worked fine; the cost was reaching the point where it could
run. It is file-count-bound rather than byte-bound, which is why it
showed up as system time.
`lucide-react` was never imported directly by `apps/app` or any plugin.
It entered through twelve shadcn components in `packages/shared-ui`,
each for one to three glyphs, of which only three were reachable
(`command`, `carousel`, `radio-group`).
## What changed
**Icons are imported by subpath**, not from the barrel — `import
BrainIcon from "@hugeicons/core-free-icons/BrainIcon"`. 340 imports
across 24 files in `packages/shared-ui`, `packages/plugin-api-map`,
`apps/app`, `apps/web` and four bundled plugins. The alias-only names
(`FolderIcon` → `Folder01Icon`, `GitPullRequestArrow` →
`GitPullRequestIcon`) were mapped from the barrel's own export table
rather than assumed.
**`@hugeicons/core-free-icons` bumped `^4.1.3` → `^4.3.4`.** Required:
4.1.3 ships only `dist/types/index.d.ts`, so every subpath import would
be `any` under `noImplicitAny`. 4.3.4 ships 6072 per-icon declarations.
**Every shared-ui shadcn component moves off lucide-react** onto
shared-ui's own `Icon`, which the build shims to the `sharedUiIcon` host
runtime slot, so it costs a plugin bundle nothing. `calendar.tsx` loses
20 lines where three near-identical chevron branches collapse into one.
`ArrowLeft`, `Minus` and `GripVertical` had no mapping; the first two
were added, the third reuses the existing `DragDropVertical`.
`lucide-react` leaves `packages/shared-ui`, `packages/plugin-registry`
and `pnpm-lock.yaml` entirely.
**`packages/plugin-registry/r/*.json`** regenerated — `lucide-react`
leaves every item's `dependencies`; `@bb/icon` joins their
`registryDependencies`. An author running `shadcn add @bb/command` now
gets the glyph from the host runtime instead of bundling an icon
library.
**The Plugin Guide's sidebar thread-list example**
(`docs/plugin-sidebar-thread-list.md`) was the last `lucide-react`
import in tracked source. It now uses the subpath-imported Hugeicons
idiom the bundled plugins use, with the glyphs `ThreadStatusGlyph`
actually renders for those two indicators (`Loading03Icon`,
`HelpCircleIcon`).
**`apps/app` `theme.css` / `disclosure.tsx`** — `.lucide { stroke-width:
var(--icon-stroke-width) }` renamed `.bb-icon-stroke`, matching the
`bb-` prefix the other app-owned classes in that file use. Its one
caller also carried a dead `strokeWidth="0.75"` (a CSS rule beats a
presentation attribute regardless of `@layer`, so the class always won).
**Five Plugin Guide wireframes corrected.** Splitting the grouped
imports made visible that `packages/plugin-api-map/src/wireframes.tsx`
drew several affordances with a different glyph than the surface it
illustrates. Each now uses the icon the app actually renders:
| wireframe affordance | drew | app renders |
| --- | --- | --- |
| composer mic | `Mic01Icon` | `Mic02Icon` (`Icon name="Mic"`) |
| settings | `Settings02Icon` | `Settings01Icon` (`Settings`) |
| edit | `PencilEdit01Icon` | `Edit02Icon` (`Edit`) |
| side chat | `MessageAdd01Icon` | `MessageAdd02Icon` (`SideChat`) |
| terminal | `TerminalIcon` | `ComputerTerminal01Icon` (`Terminal`) |
All five predate this PR. A sweep of the file's 26 icons now finds one
left outside the app's palette — `SparklesIcon`, for which the app has
no equivalent, so it stays.
**`packages/plugin-build` is untouched.** BB's resolver behaviour is
unchanged — `git diff main -- packages/plugin-build` is empty.
## Numbers
Modules esbuild parses per plugin app bundle, from its metafile.
Deterministic, not timing:
| plugin | files before | after | parsed before | after |
| --- | ---: | ---: | ---: | ---: |
| tasks | 7263 | **400** | 10604 KB | 2636 KB |
| docs | 5366 | **257** | 8260 KB | 1826 KB |
| secrets | 5223 | **103** | 7004 KB | 563 KB |
| theme-preview | 5187 | **66** | 6709 KB | 286 KB |
| plugin-api-docs | 5143 | **72** | 6636 KB | 239 KB |
| keep-awake | 1788 | **32** | 1634 KB | 97 KB |
| **all 29 app bundles** | **31037** | **1997** | **45.3 MB** | **10.9
MB** |
**−94% modules, −76% bytes parsed.** Combined output across all 29
bundles moves 5124565 → 5121854 bytes (**−0.05%**) — the same artifacts,
without reading 29k files to produce them.
`prepare:bundled` wall time, before/after interleaved per plugin, min of
5 runs. This machine is shared, so min is the least contaminated
statistic; the last two rows are controls that touch neither barrel:
| plugin | before | after |
| --- | ---: | ---: |
| tasks | 0.70s | **0.31s** |
| plugin-api-docs | 0.47s | **0.22s** |
| theme-preview | 0.48s | **0.25s** |
| secrets | 0.46s | **0.23s** |
| docs | 0.51s | **0.27s** |
| keep-awake | 0.56s | **0.47s** |
| monaco-editor | 0.30s | 0.30s |
| account-pool | 0.22s | 0.22s |
## How you verified
- `pnpm exec turbo run typecheck lint test --filter='...[origin/main]'`
— 163 of 169 tasks pass; see the exception below.
- **After rebasing onto `origin/main`** (13 commits, including plugin
manifest edits from #3913 that touch the same `package.json` files):
rebase applied cleanly, `pnpm install --frozen-lockfile` reports the
lockfile already consistent, and `turbo run typecheck lint
--filter='...[origin/main]'` is green across all 33 in-scope packages
(38/38 tasks).
- **Wireframe icons**: resolved every icon in `wireframes.tsx` to its
actual module (aliases like `FolderIcon` → `Folder01Icon.js` make names
misleading) and compared against the app's palette. 25 of 26 now match;
`SparklesIcon` is the deliberate exception.
- **Icon artwork across the bump**: compared the path data of all 181
icons bb uses between 4.1.3 and 4.3.4. **174 are byte-identical; 7 were
redrawn upstream** (`Copy01Icon`, `Mic01Icon`, `Mic02Icon`,
`SquareUnlock02Icon`, `UserAdd01Icon`, `UserIcon`, `UserSwitchIcon`). A
first pass flagged all 181 because 4.3.4 drops the `/*#__PURE__*/`
annotation; the comparison ignores it.
- **All 181 names resolve** as subpaths in 4.3.4, with declarations,
including the two alias-only ones.
- **Output equivalence for the lucide swap**: `app.css` byte-identical;
`app.js` differs by 29 bytes of minifier naming; both bundles contain
the same 43 SVG path strings.
- **Visual**: rendered the three reachable components in Ladle and
diffed the screenshots pixel-for-pixel — **399 of 1,118,720 pixels
changed (0.036%)**, confined to the search icon, the carousel arrows and
the radio dot. `theme.css` sets `--icon-stroke-width: 1.75` and the old
`.lucide` rule applied it, so lucide glyphs never rendered at their
native stroke-2; the real change is 1.75 → hugeicons' baked 1.5.
- **Stroke rename is behaviour-preserving**: computed style measured in
a browser — old markup 1.75px, new markup 1.75px, neither-class control
0.75px. Re-confirmed in the real
`thread--timeline--rows--reasoning--completed` story.
- **Plugin stylesheet unaffected**: the rule lives in an `@layer base`
block and `generate-plugin-theme.mjs` only extracts `@theme` blocks;
regenerating confirms no change.
Not verified: `@bb/desktop`'s `preload-build.test.ts` times out here —
it spawns a real Electron window and reports no output (391 of 393
desktop tests pass). It has no dependency on anything in this diff and
only re-runs because the lockfile edit invalidates turbo's global hash,
but I could not get it green in this environment.
## Behaviour changes worth a second opinion
1. **7 icons redrawn upstream** by the 4.1.3 → 4.3.4 bump, listed above.
They appear wherever bb renders those names.
2. **Glyph swap** in shared-ui `CommandInput` (machine / project /
parent-thread / environment pickers, tasks plugin), `Carousel`
(`PluginMarketplaceListing`) and `RadioGroup` (keep-awake), from the
lucide removal.
3. **Third-party plugins importing `lucide-react` directly** lose the
host's 1.75 stroke normalisation and fall back to lucide's own stroke,
since `.lucide` no longer exists.
## Why not the `production` export condition
An earlier revision of this PR set `conditions: ["production"]` on the
plugin app build, which resolves hugeicons to its prebuilt single-file
bundle. Two findings killed it.
It is a no-op on any current hugeicons. 4.2.1 repointed that condition
at the same tree-shakeable entry as `default`, so it only does anything
on ≤4.2.0 — the version we were pinned to. Of 185 marketplace plugin
apps, 103 depend on hugeicons and 97 of those declare `^4.1.3`, which
installs 4.3.4 today: no benefit for any of them.
More to the point, upstream removed it deliberately, in
[hugeicons/hugeicons#26](hugeicons/hugeicons#26)
— *"production export condition breaks tree-shaking on rspack/Vite"*.
Their guidance is to keep the minified single-file build "for CDN/script
use, just not as the bundler entry". Subpath imports are what the
package is designed for, and they measure better anyway: 10.9 MB parsed
versus 36.7 MB for the condition.
Checked while evaluating that revision: of the **119 distinct runtime
dependencies** across all 185 marketplace plugin apps, only two declare
a `production` condition — hugeicons and `@excalidraw/excalidraw`, whose
root entry lists `default` and `production` as the same file.
> AGENT GENERATED
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human comments
What was wrong
CLI usage errors often left agents without enough information to correct their next invocation. Built-in plugins each maintained their own argument parsing and help, producing inconsistent validation and output. Terminal scrollback also became unavailable as soon as a process exited.
What changed
This PR makes BB’s CLI easier for agents to use and recover from mistakes:
terminal readandterminal wait.bb diagnostics cli-errorsto summarize local command failures.0.4.102and the host daemon protocol to214.How you verified
@bb/cli,@get-bb/plugin-sdk, and changed plugin packages.git diff --check origin/main...HEAD.