burgee

Changelog

Every release of burgee, newest first, from its CHANGELOG.md — what changed and the pull request it came from.

0.22.2

Patch Changes

  • #891 6b8e3d9 Thanks @ofri-peretz! - Help and refusals in a native burgee program now put the whole line to run on the first screen.

    • --help lists the commands that run by their full path. When every command that runs fits in eight rows, the root help lists config get <key> Print one configuration value where it listed config Read configuration. It uses the same rule as the unknown-command listing. Hidden commands stay hidden, and a larger program keeps its group rows. renderHelp takes the list as a new optional commands option, and without it renders as before.
    • A refused word that is really an argument gets a fix. demo config user.name --format json now prints fix: demo config get user.name --json. This happens when one command below the group takes arguments, or when the words fit the arguments of exactly one of them: config user.name fits get <key> and not set <key> <value>. When two fit, nothing is guessed. A flag is never read as an argument.
    • An unknown-option fix is the whole corrected command line. demo config get user.name --format json now prints fix: demo config get user.name --json, not fix: --json. A near miss keeps its value (--nmae=ada becomes --name=ada), and words a shell needs quoted are quoted.

    The façades are unchanged: commander 1360/1360, yargs 816/816. The core entry is 19 bytes heavier, and ./help is 105 bytes heavier. Both are inside their budgets.

0.22.1

Patch Changes

  • #870 54d3fd6 Thanks @ofri-peretz! - --version reports the CLI's own version when it runs through the bin link npm installs. It looked up the owning package.json from the link's directory, so node_modules/.bin/burgee --version printed the installing project's version and npx burgee --version printed "no version declared". The same applied to every CLI built with defineProgram that takes its version from package.json.

0.22.0

Minor Changes

  • #866 4f01ff0 Thanks @ofri-peretz! - burgee migrate fixes five findings from a trial on two real CLIs (D-20261008-migrate-u12-findings):

    • Mocks move with the imports they mock. The first argument of vi.mock, vi.doMock, vi.unmock, vi.doUnmock, vi.importActual, vi.importMock, jest.mock, jest.doMock, jest.unmock, jest.requireActual, jest.requireMock, jest.unstable_mockModule and require.resolve is now rewritten like an import, type arguments included. A vi.mock('chalk') left behind mocked a module nothing loaded any more.
    • The install command is pinned. next prints each family package at a caret range of the version that carries the graded drop-in, roundel@^1.0.0 rather than roundel, read from the packages' manifests at build time. A project that sets minimumReleaseAge (pnpm-workspace.yaml) or minimum-release-age (.npmrc) is told so under releaseAge.
    • You choose what moves. New --only <lib,…> and --skip <lib,…> flags take incumbent package names. By default only the incumbents package.json declares (in any of its four dependency fields) are rewritten; one imported without being declared is listed under undeclared and left alone. An unknown name is a usage error, exit 2.
    • An incumbent moves in every file or in none. When one of its imports is refused or kept, it is listed under held and rewritten nowhere, so a program never runs on two copies of it. An incumbent another declared dependency still depends on is listed under transitive, and imports of the error classes that dependency still throws stay on the incumbent.
    • The report reads as text. The human report is sentences and aligned lines instead of one-line JSON, and its first line, also summary in --json, says complete: …, partial: N files rewritten, M refused or nothing to rewrite. The exit codes are unchanged.

    The engine's text surface for a result that is not a string moved to its own lazily loaded chunk, which also prints a result's own text when it carries one under Symbol.for('burgee.text'); --json is unchanged.

0.21.1

Patch Changes

  • #857 d80b2d0 Thanks @ofri-peretz! - The --json envelope's ok now agrees with the exit code. A command whose result names a non-zero exitCode printed "ok":true and then exited non-zero, so burgee check <file> --json said ok about a plugin it refused and burgee migrate --json said ok about a run with refusals. Both now print "ok":false, with the report still in data: data.refused (code, message, fix) for check, data.refused[] for migrate. A result with no exitCode, or exitCode: 0, prints "ok":true as before.

  • #847 6d3eda2 Thanks @ofri-peretz! - burgee migrate reads dotenv 18.0.6 as the graded release of seniority/dotenv (181 / 181, control 181 / 181).

  • #848 6ae533e Thanks @ofri-peretz! - The plugin host refuses a hook filter that is not { command: RegExp } when the plugin is registered. Until now a string command was accepted, reported by burgee check as "commands matching deploy", and threw a TypeError on the first run of any command. A bare RegExp used as the whole filter has no command, so its hook fired for every command. A contributed command's refusal now carries the edit to make as its fix, taken from the refusal itself (declare read_only, idempotent, non_idempotent — or withheld, …), where it used to give one sentence for every command. A plugin with no contract is told to add contract: 1. The README now shows a whole plugin as a default export, the check --json document with data.name, data.commands and data.hooks[].stage, the shape of a refusal, and how to build and run check from a clone.

  • #853 86d746f Thanks @ofri-peretz! - The plugin host's hook refusals are now a single refusal, <stage> is not { handler, filter?: { command: RegExp } }, and its fix shows that shape. It covers a missing handler, a function or an array where the hook object should be, a string command and a bare RegExp used as the filter. A contributed command whose refusal carries no separate remedy now gets its own message as the fix. The core bundle goes from 24,431 back to 24,260 bytes, under the 24,282 ceiling that #848 crossed.

  • #865 85dfea9 Thanks @ofri-peretz! - Refusals in a native burgee program now name the command to run, so an agent no longer has to spend a turn working it out.

    • An unknown command lists the commands that run, with what each takes. If they fit in the eight rows, a group's refusal lists config get <key> rather than config. A larger tree still lists one level, now also with arguments.
    • A word that exactly names a deeper command is corrected to that command. get user.name gets fix: demo config get user.name. Before, the fix pointed at greet, two edits away. When two deeper commands share the word, nothing is guessed.
    • The fix asks for JSON in burgee's spelling. A JSON request typed before the command (--json config get k, --format json …), or spelled as --format json or --output=json, is moved into the fix as --json, before any --. An unknown --format json or --output json on a command now suggests --json. --json is also a candidate for near misses such as --jsno.
    • Help lists each command with its arguments, the way commander does: get <key>, not get.
    • A missing required argument no longer adds "run --help to see what it takes". The refusal already prints the command's usage line.

    The façades are unchanged. The core entry is 66 bytes lighter, and ./help is 54 bytes heavier, inside its budget.

  • #848 6ae533e Thanks @ofri-peretz! - The family schema.json describes what five values look like. contract is 1, and burgee refuses a plugin that declares none. A caique widget's static(spec) gets { kind, message, ...sample.done }. A seniority rank sits between the built-in layers at flag 0, environment 10, config file 20, package.json 30 and default 40. A burgee hook's filter is { command: RegExp }, now with command required, so the schema and the host refuse the same filters.

  • #857 d80b2d0 Thanks @ofri-peretz! - The family schema.json closes a bellpull resolver's when with additionalProperties: false, so the schema and bellpull's own validation refuse the same unknown keys.

  • Updated dependencies [d80b2d0, 6ae533e, d80b2d0, 6ae533e, 6aab9c5, 6d3eda2]:

    • bellpull@1.0.1
    • closeout@1.0.1
    • linegauge@1.0.5
    • roundel@1.0.1
    • seniority@1.0.0

0.21.0

Minor Changes

  • #839 7694faa Thanks @ofri-peretz! - burgee migrate reads ink 8.0.0 as the graded release of controlroom/ink (1304 / 1304, control 1303 / 1304) and knows ink 8's export names. A project on ink 6 is now listed under offMajor and left alone, because ink 6 is graded (540 / 584) but not claimed.

0.20.0

Minor Changes

  • #828 8118d5c Thanks @ofri-peretz! - seniority/dotenv now follows dotenv 18, graded by dotenv 18.0.5's own suite at 179 / 179, level with dotenv itself (it was 106 / 141 against 17.4.2). config() takes its defaults from DOTENV_PATH, DOTENV_ENCODING, DOTENV_QUIET, DOTENV_DEBUG, DOTENV_OVERRIDE and DOTENV_FAST, or their older DOTENV_CONFIG_* names. It reports ◇ injected env (n) from <paths> on console.error unless quiet, and its debug lines behind ┆ on console.log. parse(src, { fast: true }) is dotenv's character scanner, and the regular-expression parser now expands \n in a value that opens with a double quote even when the quote never closes, as dotenv does. configDotenv is exported. Two entries are new: seniority/dotenv/config (import 'dotenv/config', quiet unless asked) and seniority/dotenv/cli (dotenv run, with signal forwarding). .env.vault, DOTENV_KEY and decrypt are not built, because dotenv 18 removed them (D-20261001-seniority-dotenv-vault).

    burgee migrate now rewrites dotenv to seniority/dotenv, and dotenv/config and dotenv/config.js to seniority/dotenv/config, on dotenv 18. A project on dotenv 17 is left alone and listed under offMajor: 17's own suite grades the drop-in 107 / 141, because the façade speaks 18.

Patch Changes

0.19.0

Minor Changes

  • #827 cd34546 Thanks @ofri-peretz! - burgee migrate reads the new graded releases: boxen 9.0.0 (flagstaff/boxen, level), dotenv 18.0.5 and @inquirer/core 12.0.4. A project on boxen 8 or dotenv 17 is now listed under offMajor and left alone, because those majors are no longer the ones graded.

0.18.0

Minor Changes

  • #816 5230016 Thanks @ofri-peretz! - burgee migrate moves ink to controlroom/ink now that the drop-in is level with ink's own suite (584 / 584), and its next command installs react-reconciler beside controlroom: ink brought the reconciler in itself, and the drop-in takes it as an optional peer.

Patch Changes

  • #816 5230016 Thanks @ofri-peretz! - controlroom hosts plugins (R10): keymaps and panes register through controlroom/plugin's register() against the family schema, a screen takes either by name, and controlroom check <plugin-file> reports what a plugin contributes. The family schema every host ships gains the keymaps and panes definitions.
  • Updated dependencies [5230016, 5230016]:
    • closeout@0.7.0
    • bellpull@0.5.2
    • linegauge@1.0.4
    • roundel@0.6.3
    • seniority@0.7.2

0.17.2

Patch Changes

  • #804 385f2d9 Thanks @ofri-peretz! - compat-oracle grades two new incumbents: Ink's own suite at 6.8.0 (593 cases, plus 148 on the internals line) and @inkjs/ui's at 2.0.0 (103 cases), each with a control run against the real package. Both target the controlroom root and read 0 until controlroom/ink exists. @inkjs/ui is graded the way controlroom will ask users to run it: unmodified, with 'ink' resolved to the target.

    burgee migrate now reports ink as a graded drop-in path that is not level yet (controlroom, 0 / 593), and never rewrites it.

  • #809 4f45dbf Thanks @ofri-peretz! - controlroom/ink: a drop-in for ink 6.8 — render, renderToString, Box, Text, Static, Transform, Newline, Spacer, every ink hook and measureElement — rendered by the program's own React through React's own reconciler (React 18 and 19), and laid out by a TypeScript port of yoga's flexbox for the props ink exposes, with no yoga. react and react-reconciler are optional peers: installed alone, the subpath refuses on first import with E_PEER_MISSING and npm install react react-reconciler as its fix, and the package root never loads either. Graded by ink's own suite at 576 of 584 cases and by @inkjs/ui's at 103 / 103, with 'ink' resolved to the drop-in.

    compat-oracle resolves a target's optional peers from the suite's own tree (Host.peers) and can serve a gated file's internal import from the target's own module (Host.targetInternals); both ink rows now grade controlroom/ink.

    burgee migrate reports ink → controlroom/ink as a graded drop-in path that is not level yet, and does not rewrite it.

  • #803 91ba281 Thanks @ofri-peretz! - burgee migrate now reports blessed, neo-blessed and terminal-kit programs. They have no drop-in, so nothing is rewritten: each screen, box, list, key, render, alternate-screen, mouse and text-input site goes under a new guided key in the report, with its file, line and a link to the section of that incumbent's coming-from guide. A site is reported only when its receiver came from one of the three packages in the same file, so a screen.key( or .render( on anything else is left out. The exit code is unchanged: nothing was refused.

0.17.1

Patch Changes

  • #779 ec04103 Thanks @ofri-peretz! - burgee/yargs follows yargs 18.2.0 and passes all 816 of its tests (real yargs: 814). With SHELL naming fish, --get-yargs-completions answers value<TAB>description and offers choices verbatim, and completion prints the fish script (> ~/.config/fish/completions/<app>.fish). The zsh script's zsh_eval_context test no longer carries a stray quote, so an autoloaded function is called rather than re-registered — the fix 18.2.0 made. The façade's bundle is 3 bytes smaller than before, fish included.

    compat-oracle's yargs suite is re-vendored at v18.2.0 (816 tests, twelve added), and burgee migrate names 18.2.0 as the yargs release burgee/yargs was graded at.

  • Updated dependencies [d810ac0]:

    • seniority@0.7.1

0.17.0

Minor Changes

  • #782 4eb5f44 Thanks @ofri-peretz! - A failure now says what to do next. When a USAGE or RUNTIME failure carries no fix, stderr adds the failing command's usage: line and its options, with required, default, choices, relations and env noted, and the --json envelope carries the same thing as error.usage: { command, options?, commands?, more? }. The list is at most 8 rows, and more names the --help that has the rest. An unknown command lists the commands that exist and points at --schema. When exactly one command is near, the error's fix is the caller's own command line with the word corrected (fix: tool push --target prod). Every runnable command's help lists --explain <option>, and the root help ends with one For agents: line naming --schema, --json and --explain. Exit codes are unchanged. The core entry is unchanged too, at 0 bytes: the new text lives in the lazy failure.js, surfaces.js, usage.js and help.js chunks. burgee/commander, burgee/yargs and burgee/meow are untouched (D-20260930-failures-teach-recovery).

Patch Changes

  • Updated dependencies [df87199]:
    • linegauge@1.0.3

0.16.0

Minor Changes

  • #783 08976ae Thanks @ofri-peretz! - ctx.interactive is roundel's interactive(), the family's one rule for whether a person may be asked: a terminal on stdin, no CI, and no detected agent, with FORCE_TTY=1 over all three. caique's prompts already ask it, so a handler and a prompt now agree about the same shell.

    It used to read stdout and ignore CI. Three things change for a handler that reads it:

    • Under a non-empty CI it is false, even on a pseudo-terminal.
    • With stdin piped it is false, even when stdout is a terminal.
    • With stdout piped and stdin a terminal (tool deploy | tee log) it is true, because an answer can still be typed.

    burgee loads roundel/terminal in a chunk of its own (ctx.js, 330 B), only on the path that runs a handler. Help, --version, --schema, --mcp and failures never load it. The root entry shrinks (35,629 → 35,437 B on disk), and so does the bundled initial load of import { run } from 'burgee' (24,297 → 24,278 B). ctx.agent, detectAgent, AGENT_PROBES and help's colour, which reads stdout, are unchanged. runBurgee forwards tty onto stdin too, so tty: true is still a terminal a person can answer on.

    The agent variables stay at five (AI_AGENT, CLAUDECODE, CURSOR_AGENT, CODEX_THREAD_ID, GEMINI_CLI). One joins only when it uniquely identifies an agent, so CURSOR_TRACE_ID, which Cursor sets in every integrated terminal, never will. roundel's AGENTS documents that rule, and a test pins that a person in Cursor is asked.

Patch Changes

0.15.0

Minor Changes

  • #778 24300f4 Thanks @ofri-peretz! - seniority's cosmiconfig drop-in now reads YAML the way cosmiconfig does. .yaml, .yml and extensionless rc files go through seniority/yaml, the package's own parser, which is loaded the first time a YAML file is read and never before. The loader used to read only the JSON subset of YAML and throw a LoaderError (no YAML parser for <file>) for the rest. It now returns what js-yaml 5 returns. A malformed file throws cosmiconfig's own message, YAML Error in <file>: followed by the reason and (line:column). cosmiconfig 10.0.1's own suite grades seniority at 240 / 243, up from 186 / 243, level with the control's 240 / 243.

    burgee migrate now rewrites cosmiconfig to seniority, because the row is level. A file that imports a name seniority does not export, such as the type LoaderSync, is refused as unknown-export and stays on cosmiconfig. On Linux, the global config directory is still resolved from the home directory rather than from XDG_CONFIG_HOME (D-20260930-seniority-yaml).

Patch Changes

  • #768 d901bcb Thanks @ofri-peretz! - burgee migrate now refuses a file that imports @clack/prompts and also imports, import()s or require()s @clack/core. Until now the file's prompts moved to caique/clack while its updateSettings from @clack/core kept configuring clack, which caique's prompts never read, and nothing said so. The refusal's reason is sibling-state, and it carries a fix: import { updateSettings } from 'caique/clack' instead of '@clack/core', then re-run burgee migrate. The file is left whole and the run exits 1. A type-only import of @clack/core does not refuse, and a file that imports @clack/core without @clack/prompts is left alone (D-20260930-migrate-refuses-clack-core).

  • #774 b44f426 Thanks @ofri-peretz! - paratext/term-img now takes a file path, as term-img does. terminalImage('unicorn.jpg') reads the file with node:fs and draws it, and a file URL works too. The read happens after the terminal check, so a path handed to a terminal that cannot draw it reaches your fallback (or UnsupportedTerminalError) without the file being opened. A missing file on a supported terminal throws node:fs's ENOENT, as term-img does. The Options type is exported under term-img's name and is generic over what fallback returns, so a fallback that returns nothing type-checks.

    paratext/term-img now grades 18 / 18 against term-img 7.1.0's own suite, level with term-img itself. It was 12 / 18: the six cases that pass a path were refused under D-030. This supersedes D-030 for this subpath only (D-20260930-paratext-term-img-path). It is the only paratext entry that imports node:fs. The root image() still takes bytes, and a lock fails if node:fs reaches the root or any other entry.

    Because the row is level, burgee migrate now rewrites term-img to paratext/term-img.

  • #757 b69d513 Thanks @ofri-peretz! - A flag's provenance now names the flag as it was typed: --dry-run, -n or --no-color in meta.provenance and in --explain, instead of the camelCase key (--dryRun, which burgee itself refuses). A flag that was not typed is listed in kebab-case.

  • Updated dependencies [6195b99, 24300f4, 8ba7c33]:

    • roundel@0.6.1
    • seniority@0.7.0

0.14.3

Patch Changes

  • #764 1817b62 Thanks @ofri-peretz! - caique/clack now grades 16 / 16 against @clack/prompts 1.8.1's own suite, level with clack itself at 16 / 16. It was 16 / 17. The pass count did not change. The denominator did: one case, guide.test.ts's no prompt renders a guide when withGuide is globally false, is now excluded by its exact title. It imports updateSettings from @clack/core and asserts that the prompts read that package's module state, so it grades @clack/core and not @clack/prompts (D-20260930-caique-clack-core-exclusion). The exclusion and its reason are on the compatibility page.

    Because the row is level, burgee migrate now rewrites @clack/prompts to caique/clack. It refuses a file that imports box, progress or taskLog, which caique/clack does not build, and leaves that file on clack. It does not rewrite @clack/core. So a migrated program that imports updateSettings from @clack/core is still changing clack's settings, and caique's prompts never read them. Import updateSettings from caique/clack instead.

  • #755 ee2b2ce Thanks @ofri-peretz! - burgee migrate no longer calls an incumbent removable, or suggests npm uninstall for it, when the only file that imports it was refused and left as it was. The dirty-tree refusal now prints its fix (commit or stash your changes, or pass --force) on stderr and in the --json envelope.

  • Updated dependencies [e928581, 6c2e9c5]:

    • bellpull@0.5.0
    • linegauge@1.0.1

0.14.2

Patch Changes

  • #760 a115799 Thanks @ofri-peretz! - chalk's graded release is 6.0.1: compat-oracle's vendored suite is re-vendored at v6.0.1 (59 tests, one added), and burgee migrate names 6.0.1 as the chalk release roundel/chalk was graded at.

    B2 cold start also spawns picocolors, roundel/tokens and roundel/chalk, and gates roundel's R8 time bar — each colour entry within picocolors + 10 ms — as cold-start-delta-ms, the median of per-round differences.

  • Updated dependencies [a115799]:

    • roundel@0.6.0

0.14.1

Patch Changes

  • #723 3e837cb Thanks @ofri-peretz! - burgee dev serves a commander program as commander again. A Command has a burgee() method too, so it was recognised as a yargs instance: a tool call ran without the injected streams, writing onto the MCP channel, and read its argv from: 'node', dropping the first two words. Four code paths no input could reach are removed from the engine, the MCP server and burgee's own commands, which takes 27 bytes off the core bundle; behaviour is otherwise unchanged.

  • #736 1f9ad70 Thanks @ofri-peretz! - Five fallbacks no input could reach are removed from help, the unknown-command error and the contrast check; behaviour is unchanged. Help no longer defaults a subcommand's last path word, an env name under a filter that keeps only options with one, or a wrapped description's first row, all of which are always there. The unknown-command error names the word it already read, and a field colour between two stops no longer tests for a zero-width span the loop has already stepped past.

  • #724 063025e Thanks @ofri-peretz! - Three code paths no input could reach are removed from burgee/meow and burgee migrate; behaviour is unchanged. burgee/meow no longer reads a deprecated alias as a spelling of its flag, because meow refuses a flag that declares one before anything is parsed. burgee migrate no longer falls back to an empty table for a drop-in with no export list or no supported majors, because two locks hold a table for every one.

  • #725 8b6b1e0 Thanks @ofri-peretz! - --schema over its budget lists each command by its declared summary again. a ?? b === undefined binds as a ?? (b === undefined), so a declared summary read as true and was dropped, leaving only commands that had a description and no summary with one. The PowerShell completion script offers a camelCase option's choices after the flag as typed, --output-format, where it was keyed by --outputFormat and never matched. Code paths no input could reach are removed from completions, suggestions, the failure path, relations, config layers, the brand renderer and the test harness; behaviour is otherwise unchanged.

  • #638 e4c1f69 Thanks @ofri-peretz! - caique/clack now carries @clack/prompts' prompts, not only limitOptions: text, password, confirm, multiline, date, path, select, selectKey, multiselect, groupMultiselect, autocomplete and autocompleteMultiselect, with intro, outro, cancel, note, log, stream, spinner, tasks, group, the S_* glyphs, settings/updateSettings and isCancel, under clack's names and options. They run on caique's own keypress loop and reach nothing outside this repository. Graded by clack's own suite at 16 / 17 (was 14 / 17; the control is 17 / 17): the one case left imports updateSettings from @clack/core, which caique does not depend on (D-152). The spinner animates only on a terminal outside CI and prints each message once anywhere else. box, progress and taskLog are not built. burgee migrate reports the new grade and, since it is not level with the control, still does not rewrite @clack/prompts.

  • #677 8bc14e6 Thanks @ofri-peretz! - Measuring text against a terminal is linegauge's job, and four places did it by hand.

    • caique/raw: the repaint counted a frame's rows with split('\n') and ignored wrapping, so a choice whose hint was wider than the terminal left a stale copy of the question on screen after every keypress. It counts rows with linegauge's lineCount against the terminal's width, which Writer now carries as an optional columns (read through to the output stream by createIo). A writer without one is treated as never wrapping, as before.
    • burgee/testing: stripAnsi is linegauge's strip. The regex it used left private modes (ESC[?25l), the colon form of a truecolor SGR (ESC[38:2::255:0:0m) and OSC 8 hyperlinks in the text.
    • burgee help: descriptions and epilogues are folded by linegauge/wrap rather than a loop of help's own. A styled description now opens and closes its styles on each row instead of running its colour into the next row's indent, and a run of spaces at a break no longer leaves trailing whitespace. Lines the author indented are still kept verbatim, and a word wider than the row still overflows rather than breaking.
    • burgee config explain: the option column is padded in terminal columns, so a CJK option name no longer pushes its value two columns right of the others.
  • #665 12fea8e Thanks @ofri-peretz! - Three defects carried over from the incumbents, and three places where a port answered differently from the incumbent on a bad value.

    • burgee/yargs/parser: with unknown-options-as-args, every --prefixed argument ran through five flag regexes, and two of them backtracked — one quadratically, one cubically (a 4,000-character argument took ten seconds). They are linear scans now, and give the same answer as the regexes for every input, checked against them over every short string of the characters involved. yargs' own suite still passes 804 of 804.
    • burgee/yargs/parser: { "a": null } in one config and a.b from a default or a second config threw "Cannot read properties of null". A null parent is now an absent one, as the parser's own key lookup already treated it.
    • burgee/yargs: showHelp() with an async default-command builder that rejected left an unhandled rejection that ended the process. The rejection now goes to fail, where yargs sends a command handler's rejection, so a .fail() handler receives it.
    • burgee/meow: importMeta: null throws meow's own "The importMeta option is required" TypeError instead of a null dereference, and input: null or an array is refused as meow refuses it.
    • flagstaff/cli-table3: a style name that cannot be read off a colour function (caller, arguments) draws the cell plain, as cli-table3 does, instead of throwing out of toString().
  • #673 5b3dafa Thanks @ofri-peretz! - burgee/meow now behaves as meow 14 does in the places it did not, and meow's own suite grades it 146 / 148, level with real meow (was 132 / 148). With allowUnknownFlags: false, unknown flags are reported from the tokens as typed, so a declared noAutoHelp no longer makes --no-auto-help "unknown", and a subcommand's own flags are left to the subcommand. --help and --version answer only when they are the whole command line, including when the program declares them (-h, -v). The help block keeps meow's trailing blank line. Choices of the wrong type, flags: null and booleanDefault: null are refused as meow refuses them. -F keeps its case, --flag '' satisfies a required flag, input.isRequired receives the input alone, and cli.pkg is normalized lazily in the object you passed. burgee migrate now rewrites meow to burgee/meow.

    Its types come along too: burgee/meow exports meow's Flag, AnyFlags, TypedFlags, FlagType, InputOption, InputOptionType and IsRequiredPredicate, with the generic Options<Flags> and Result<Flags>, so cli.flags keeps its types after the rewrite.

  • #674 e9f45d8 Thanks @ofri-peretz! - README: family header, badges, install, migrating, the family table.

    Every package README now opens the same way — lockup, tagline, one badge row in one order (npm version, downloads, Quality Gate, the package's own coverage, OpenSSF Scorecard, unpacked size, dependencies, types, Node, licence, npm provenance), a row of compatibility badges read from the graded baseline — and carries the same sections in the same order: Install for npm, pnpm, yarn and bun, Quick start, Migrating as a before/after diff, Compatibility, Benchmarks, For agents, API, and a generated table of the nine packages. Links are absolute, so they work on npm as well as GitHub.

  • #678 088cecc Thanks @ofri-peretz! - Whether anybody is there, whether to colour, and whether a tick can be drawn are roundel's questions, and three packages answered them by hand.

    • roundel/terminal (new subpath, 878 B, reaching nothing): interactive(rt) — a terminal on stdin, no CI, and no agent variable (CLAUDECODE, AI_AGENT, CURSOR_AGENT, CODEX_THREAD_ID, GEMINI_CLI, exported as AGENTS), with FORCE_TTY=1 as the override — and unicode(rt), is-unicode-supported 2.1.0's table over { env, platform }.
    • caique/decide now depends on roundel and asks interactive(). Behaviour change: under an agent that has a terminal — CLAUDECODE=1 and a TTY on stdin — a missing required value is refused with a usage error naming the flag (--x is required when nobody is there to answer) instead of prompting and hanging the agent. FORCE_TTY=1 now prompts even without a terminal on stdin, as it does for burgee.
    • caique/inquirer's tick and flagstaff/ora's log symbols and spinner fallback use roundel's unicode(). caique's copy was a four-condition subset: the Linux console (TERM=linux) now gets √ rather than ✔, and ConEmu/Cmder, Terminus, Alacritty, rxvt-unicode and JetBrains' terminal on Windows now get ✔, as figures draws them.
    • burgee help colour is roundel's colorLevel(rt) > 0. Behaviour changes: NO_COLOR now beats FORCE_COLOR; --no-color and --color=… on the command line are honoured; CLI_ACCESSIBLE turns help colour off; and a terminal that sets no TERM (Windows' conhost) gets plain help unless FORCE_COLOR, --color or COLORTERM asks for colour.
    • burgee/contrast rounds with roundel's round2; no output changes.
    • paratext: the supports-color fork behind paratext/terminal-link is unchanged, and now held to roundel's policy by a parity test everywhere their two incumbents agree.
  • #683 5052d67 Thanks @ofri-peretz! - burgee/meow finds the caller's package.json with seniority/find-up instead of a directory walk of its own. The answer is unchanged — the nearest package.json above the module that parses — and the walk is now bounded and ends on a symlink cycle.

  • #655 4319563 Thanks @ofri-peretz! - linegauge: linegauge/slice is graded against slice-ansi 9.0.1's own suite (104 cases, up from 15 at 7.1.2) and passes all 104; East Asian Width is Unicode 17.

    • slice rounds inward at a wide character, as slice-ansi does: a cluster the range only half covers is left out instead of returned whole. Before, slice('あいう', 0, 3) returned あい (four columns), and truncate('あいう', 4) returned あい… (five columns, over its budget). Both now stay inside the columns asked for.
    • slice reads the escapes slice-ansi 9 reads: OSC 8 links ended by ESC \ or U+009C, the C1 OSC introducer, DCS, SOS, PM and APC strings, a lone ST, and truncated or malformed CSI. A malformed CSI ends at the first byte that cannot belong to it, so the text after it is kept.
    • An escape inside a grapheme cluster (e, a style, then a combining mark) no longer splits the cluster.
    • Hyperlinks don't nest: a second open replaces the first, and the first is closed with its own introducer and terminator. A link around no visible text is removed. A close just past the end of the range is kept as written, and an opener with no text after it is dropped.
    • Where a cut lands, slice gives every cluster at least one position (so CRLF and zero-width characters can start or end a range) and a lone regional indicator two, as slice-ansi does. width is unchanged and still answers as string-width does.
    • The Wide/Fullwidth table is generated from get-east-asian-width 1.7.0 (Unicode 17), like the Ambiguous table next to it. The hand-written table was missing 1,147 code points, which width measured as one column where string-width measures two.

    burgee: burgee migrate serves slice-ansi 9 (its compatibility row moved from 7.1.2 to 9.0.1). slice-ansi 7 is still graded, at 14 of 15, but no longer claimed, so a project on slice-ansi 7 is left on it.

  • #651 b74a24d Thanks @ofri-peretz! - burgee/yargs: four defects carried over from yargs 18.1.0, fixed without changing an answer yargs' own suite checks (804 of 804).

    • Parsing a command string is linear. Upstream's parse-command regexes backtracked quadratically on a long run of whitespace or dots (20,000 took 0.7 s); the rewrite gives the same result for every input, checked against the original regexes over every short string of the characters involved.
    • extends in a config tells a path from a module name in linear time, for the same reason.
    • pkgConf(key) reads only a key the package.json has. pkgConf('__proto__') used to hand Object.prototype to the extends loader, which deletes extends from the object it is given.
    • zsh completions escape \ as well as :. zsh's _describe strips one level of backslashes, so a command, option or choice containing one completed without it.
  • #681 deffcc6 Thanks @ofri-peretz! - burgee/yargs: resolving a config's extends no longer deletes extends from the object you passed in. It reads a copy instead, so a config object is never written to while it is being resolved (CodeQL #27). The resolved result is unchanged, and yargs' own suite still passes 804 of 804.

  • Updated dependencies [3250edf, 6d8aadf, ff4159d, 5e635b0, ff4159d, 6ef8227, 7c77cd2, 6ef8227, 6ef8227, e9f45d8, 620fc74, 088cecc, bc68493, ee6780b, 4319563]:

    • bellpull@0.4.3
    • closeout@0.6.0
    • linegauge@1.0.0
    • roundel@0.5.5
    • seniority@0.6.4

0.14.0

Minor Changes

  • #631 478cb96 Thanks @ofri-peretz! - schema answers as an alias of --schema (D-151, GAPS B22). mytool schema, mytool schema deploy and mytool schema deploy --field options.region print exactly what the flag forms print, because <tool> schema is where clispec.dev and the agents that follow it look for a program's schema. A program's own meaning of the word wins: a declared schema command runs as written, and a root command that takes arguments receives schema as one. The root --help now lists --schema the program as data among its global options. Loaded through the existing lazy surfaces.js; the core entry grows 14 bundled bytes.

0.13.2

Patch Changes

  • #627 4a629a9 Thanks @ofri-peretz! - Lint with every published Interlace ESLint plugin, and fix what the upgrade surfaced.

    • caique: the inquirer theme merge skips __proto__, constructor and prototype keys, so a theme object cannot swap the merged object's prototype.
    • burgee: last-element reads use .at(-1).
    • burgee, closeout, flagstaff, roundel: helpers that capture nothing from their enclosing function move to module scope.
    • seniority: suppression comments name the no-dynamic-require rule that now reports the config loader's dynamic require.

    No public API or output changes.

  • Updated dependencies [4a629a9]:

    • closeout@0.5.4
    • roundel@0.5.4
    • seniority@0.6.3

0.13.1

Patch Changes

  • #604 0e7b1e8 Thanks @ofri-peretz! - Each README links to its migration guides under the docs link: "Migrating from: chalk", "ora · log-update · boxen · cli-table3", and so on. That puts a path from the npm page to the guide for the library you are replacing. No code changes.
  • Updated dependencies [0e7b1e8]:
    • roundel@0.5.3
    • linegauge@0.5.4
    • seniority@0.6.2
    • bellpull@0.4.2
    • closeout@0.5.3

0.13.0

Minor Changes

  • #586 a0c691a Thanks @ofri-peretz! - --format=agent prints a command's result the way an agent wants to read it: one compact logfmt line per record — key=value pairs, nested keys dotted, lists of scalars comma-joined, a value quoted only when it holds a space, =, " or , — with no envelope, no meta, no summary and whitespace collapsed. A list result is one line per element; a scalar is itself. It is not JSON on purpose (oxlint's and vitest's agent reporters converged on the same shape), and it is smaller than --json on the same result: 31% over a representative set, from 20% on a twelve-row list to 96% on a bare count. --json wins when both are typed, so the envelope and D-140's failure contract are unchanged; a failure without --json is still prose on stderr; the exit code is the one the result names. A command that declares its own format option keeps the flag. The formatter loads only when the flag is typed.

  • #582 120ba7b Thanks @ofri-peretz! - .burgee({ floor: true }) on the commander and yargs façades turns on the behavioural floor in one call (J3, D-121): a usage error exits 2 rather than the host's 1, and a handler that throws or rejects prints one line and exits with its E1 code rather than a stack trace (and, on yargs, the help screen). Off by default, so the hosts' own suites still pass, and it changes nothing else — a returned value is not printed, --json keeps its envelope, and a commander program that called exitOverride() keeps its exits. --schema from a façade program now names the reserved surfaces the program shadows, as shadows (J4), and burgee/program-schema.json describes it — and the option negatable flag the commander façade already printed, which the file had left out.

Patch Changes

  • #589 d7d9e75 Thanks @ofri-peretz! - import 'burgee' loads less at startup. Four things now load only when they are needed:

    • What a failed run prints (the exit-code classification, the --json failure envelope and the stderr message) loads only when a run fails.
    • The checks between options (exactlyOneOf, conflicts, implies, dependsOn, exclusive) load only for a command that declares one.
    • Everything answered without running a command (help, --version, help [command], completion, config explain, --schema, --mcp) loads only when it is asked for.
    • Reading a config file and the package.json field loads only for a program that turned on config discovery.

    Output and exit codes are unchanged.

  • #594 60c4603 Thanks @ofri-peretz! - burgee migrate decides whether a project's commander, yargs or other incumbent is on a major it may rewrite from a declared range per drop-in (SUPPORTED_MAJORS), rather than from the one graded version alone. Every range is the current major today, so what migrate rewrites is unchanged: commander 14 and yargs 17 are now graded by their own suites (1329 / 1331 and 191 / 794) and neither grades level, so neither is claimed. The compatibility page publishes both rows.

  • Updated dependencies [073037a]:

    • bellpull@0.4.1
    • linegauge@0.5.3
    • seniority@0.6.1

0.12.1

Patch Changes

  • #566 c938fbf Thanks @ofri-peretz! - --mcp holds stdout for the whole session, not only during a tool call. A timer or stream a handler left behind that printed after its reply went out still landed on the JSON-RPC transport, and a strict client stopped parsing there. Between calls, anything written to stdout now goes to stderr; replies are the only thing on the transport.

0.12.0

Minor Changes

  • #481 3a7131f Thanks @ofri-peretz! - config explain [command…] is added automatically to programs that read config. It prints the precedence order (flag > env > config > package.json > default), then each option's resolved value and the source that won, including file and line. The order comes from the resolver itself, so the output always matches what a real run does. Pass --json for machine-readable output. A program that defines its own config explain keeps it.

  • #472 c992bf2 Thanks @ofri-peretz! - defineError({ name, code }) declares an error class with its own exit code (7–125). Throw it from a handler and the program leaves with that code, printing the message and any hint and fix the same way UsageError does, with error.code in the JSON envelope. Subclasses inherit the code. Defining a second class with the same code throws when the module loads.

  • #478 4e5054c Thanks @ofri-peretz! - Options can compute their shell completions at TAB time. Give an option complete: (partial) => string[], and the generated bash, zsh, fish and PowerShell scripts call the program back for that option only. Options without a completer still complete statically from choices, and the program never runs for them. The callback prints nothing and exits 0 if a completer fails, so TAB never shows an error.

  • #470 3e56073 Thanks @ofri-peretz! - --json=<a,b> selects the fields of a command's result, and --json= lists the fields a command declares (fields: [...] on defineCommand) without running it. An unknown field is refused with the valid set in the hint. Only the = form takes fields, so cmd --json name keeps name a positional. Declared fields appear in --schema.

  • #484 635eb8c Thanks @ofri-peretz! - A handler can throw an error, or a plain { code, message, fix } object, whose code names an exit code: 'USAGE' exits 2, 'CONFIG' 3, 'CANCELLED' 4, 'AUTH' 5. The message and fix print as usual. This lets caique's prompt refusals set the right exit status: a question that can't be asked without a terminal exits 2 and names the flag to pass instead, and a cancelled question exits 4, never 1. burgee still does not import caique. Any other string code, such as ENOENT, still exits 1.

  • #474 1955419 Thanks @ofri-peretz! - burgee plugins can hook two more stages. parse runs before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin maps d to deploy. shutdown runs once as the program exits, whether the command succeeded or failed. The family schema.json shipped in every package now describes both stages.

  • #483 72a8103 Thanks @ofri-peretz! - burgee/program-schema.json is published: a JSON Schema for the document --schema prints, so anyone reading a program's --schema can validate it. --schema also now includes exitCodes, the contract's seven exit codes by name, so a caller can branch on a code without reading prose.

  • #475 8a7f387 Thanks @ofri-peretz! - A positional argument declared with type: 'file' treats - as standard input: the handler receives the stream as ctx.stdin, while the positional still reads -. Giving - to two file arguments is a usage error, because standard input can only be read once.

  • #517 0d65c75 Thanks @ofri-peretz! - burgee migrate now migrates the whole family, not just commander and yargs. Every drop-in the compatibility oracle grades level with its incumbent is rewritten in the same run: chalk → roundel/chalk, ora → flagstaff/ora, string-width → linegauge, cross-spawn → bellpull/cross-spawn, signal-exit → closeout/signal-exit and eleven more. Drop-ins not yet level (dotenv, cosmiconfig, clack, meow, …) are reported under partial with their grade and left alone. The report names the family packages to add and prints next, the install-and-uninstall command for the package manager your lockfile names.

Patch Changes

  • #490 8a02d68 Thanks @ofri-peretz! - bellpull/node-which is a drop-in replacement for node-which 7: which(cmd, opts) returns a promise and which.sync runs synchronously, with node-which's all, nothrow, path, pathExt and delimiter options and its ENOENT error. It passes node-which's own test suite, 5 of 5. bellpull/which is unchanged: it stays bellpull's own resolution API and never reads the process.

    require('bellpull/node-which') returns the function with .sync on it, as require('which') does, and burgee migrate now rewrites which to it.

  • #526 91f46ee Thanks @ofri-peretz! - --mcp keeps stdout for JSON-RPC frames only. A tool call whose handler printed — console.log in a burgee/commander action, the common case, or a direct process.stdout.write — wrote onto the stream the frames go out on, so the client read hello between two replies as a malformed message and the tool result itself said data: null. During a call, stdout and the stdout-printing console methods are now captured and returned as a second text item after the envelope, so printed output becomes the tool result; stderr is left on stderr. Both are restored when the call settles, including when it throws, and a runner that rejects is answered as an isError result instead of ending the server.

  • #522 f4be6a8 Thanks @ofri-peretz! - require('bellpull/cross-spawn'), require('flagstaff/cli-table3'), require('burgee/yargs'), require('seniority/dotenv') and require('seniority/rc') now return what the incumbent's require() does — the function, the class, the factory, the object — instead of an ES module namespace. Each exports its default as 'module.exports', which is what Node hands a CommonJS caller, and which yargs' own entry already does. const spawn = require('…'); spawn(…) threw before.

  • #495 31efa5b Thanks @ofri-peretz! - burgee's own commands move to program.js; cli.js stays the bin and still re-exports them. Importing the definitions no longer runs the CLI.

  • #519 77ff1cb Thanks @ofri-peretz! - The drop-ins now export their incumbents' type names, so a TypeScript program migrates by its import alone: roundel/chalk gains chalk's Color, ForegroundColor, BackgroundColor, Modifiers and Options; flagstaff/ora gains Spinner, PrefixTextGenerator and SuffixTextGenerator; flagstaff/boxen gains Options, CustomBorderStyle and Boxes; flagstaff/log-update, linegauge, linegauge/wrap and closeout/exit-hook gain Options; burgee/yargs/parser gains Arguments, Options and Configuration. Types only — no runtime bytes.

  • #526 91f46ee Thanks @ofri-peretz! - A handler that fails under --json is now one { "ok": false, "error": … } envelope on stdout and the exit code its error names, on every front end. A burgee/commander action that threw under a plain parseAsync(process.argv) escaped as a stack trace; the native engine wrote the failure envelope to stderr with stdout empty; burgee/yargs let a synchronous throw or a thrown non-Error escape parseAsync(), filed a thrown string as a usage error, and wrote an async rejection's envelope twice under --mcp; and both façades exited 1 for an AuthError where E6 promises 5. Without --json a façade program run the incumbent's way behaves exactly as before — a throw is still commander's or yargs' to surface; through an injected seam (--mcp, burgee/testing) an AuthError now exits 5 there too.

  • #549 d5a1b02 Thanks @ofri-peretz! - seniority/lilconfig grades 77 / 77 against lilconfig 3.1.3's own suite, up from 67. The difference was all in the harness: two jest semantics it didn't reproduce made ten cases fail against lilconfig itself. burgee migrate's report carries the new grade.

  • #547 ef512ea Thanks @ofri-peretz! - burgee migrate now rewrites require() of the incumbents that ship ESM only: ansi-escapes, chalk 6, ora 9, log-update, boxen 8, string-width, strip-ansi, wrap-ansi, slice-ansi, restore-cursor, exit-hook and terminal-link. Their own require() already returns a namespace, as their replacements' does, so the rewrite is exact; before, it was refused as require-of-default. require('burgee/yargs/parser') now returns the parser function, as require('yargs-parser') does.

  • #543 dc1b1a7 Thanks @ofri-peretz! - paratext implements ansi-escapes' CSI half — cursorTo, cursorMove, eraseLines, clearTerminal, enterAlternativeScreen, synchronizedOutput and the rest, byte-exact with ansi-escapes 7.3.0 — so import ansiEscapes from 'paratext' is a full drop-in, graded 4 / 4 by ansi-escapes' own suite (it was 1 / 4 with CSI declared undefined). burgee migrate now rewrites ansi-escapes to paratext.

  • #546 0592441 Thanks @ofri-peretz! - paratext/terminal-link now links exactly where supports-hyperlinks 4.5.0 does. It honours FORCE_HYPERLINK, --no-hyperlink / --hyperlink=always, CI, win32 outside Windows Terminal, NETLIFY, and the incumbent's version floors for iTerm2, WezTerm, VS Code and VTE (0.50.0 segfaults on OSC 8). It also recognises kitty, alacritty, ghostty, zed, Orca and Cursor. Its previous guess disagreed with the incumbent in 30 of 55 environments. The compatibility row is now 8 / 8, so burgee migrate rewrites terminal-link imports to paratext/terminal-link.

  • Updated dependencies [866b972, 8a02d68, 1955419, f4be6a8, 1b002e0, 77ff1cb, f08ff58]:

    • closeout@0.5.2
    • bellpull@0.4.0
    • linegauge@0.5.2
    • roundel@0.5.2
    • seniority@0.6.0

0.11.1

Patch Changes

  • #508 1aae1e2 Thanks @ofri-peretz! - The installed burgee command runs. dist/cli.js shipped without #!/usr/bin/env node, so npx burgee … and the linked bin were handed to /bin/sh on macOS and Linux and failed with import: command not found. check:artifacts now refuses any published bin that is missing from the pack list or does not start with the shebang.

  • #508 1aae1e2 Thanks @ofri-peretz! - Shell completions offer only flags the parser accepts. Every declared boolean was completed with a --no-<name> twin, which is right for burgee's own parser and wrong under burgee/commander, where commander negates only what the program declared — so --no-skip-blank, --color (for a lone --no-color) and --no-version were each a TAB away and each unknown option. Completions now read OptionSpec.negatable, which the commander façade sets from the program's own declarations.

  • #508 1aae1e2 Thanks @ofri-peretz! - The README's MCP answer says what --mcp does: every runnable command declares its effects, withheld is what keeps one out of the tool list, and a burgee/commander or burgee/yargs command that declared nothing is listed as effects: 'undeclared' rather than left out.

  • #508 1aae1e2 Thanks @ofri-peretz! - --mcp tool calls reach the program with multi-word options. tools/call rebuilt argv as --<property>, so skipBlank went out as --skipBlank and both the engine and burgee/commander refused it; it now sends the flag the schema advertises (--skip-blank), and a false for a boolean that defaults on goes as --no-<name>. Under burgee/commander, a lone --no-color is advertised as noColor (flag --no-color) instead of a --color commander never accepts. run(defineCommand(…)) now keeps the command's effects, examples, arguments and relations, so a single-command program's tool carries the hints it declared and is named after the program rather than "". An unknown-option fix is spelled as the flag is typed (--dry-run, never --dryRun). OptionSpec gains negatable?: boolean — false refuses --no-<name>.

  • #508 1aae1e2 Thanks @ofri-peretz! - The README's "Start here" runs. The quickstart omitted effects, which defineCommand requires of every runnable command, so the first thing a new user pasted threw command "greet" is runnable and declares no effects; its --json line also left out the meta the envelope carries. The shape test now runs the snippet and its transcript straight from both READMEs instead of from a private copy.

  • Updated dependencies [1aae1e2]:

    • bellpull@0.3.1
    • closeout@0.5.1
    • linegauge@0.5.1
    • roundel@0.5.1
    • seniority@0.5.1

0.11.0

Minor Changes

  • #507 b8e97dc Thanks @ofri-peretz! - Runs on Node 20 and 22, not just 24+: engines.node is now ^20.19.0 || >=22.13.0. Those are the first releases where require(esm) loads without a warning, so the CommonJS require() path keeps working. Every package's test suite runs on exactly 20.19.0 and 22.13.0, on Linux, macOS and Windows. caique's prompts no longer call Promise.withResolvers, which Node 20 doesn't have.

Patch Changes

  • #505 9800b43 Thanks @ofri-peretz! - Docs: the README says "no dependency outside the burgee family" instead of implying none at all — burgee declares five, every one of them a sibling. The zero-dependency keyword is now no-external-dependencies.

  • #506 891e132 Thanks @ofri-peretz! - burgee/yargs exports yargs' types — Argv, Arguments, ArgumentsCamelCase, CommandModule, CommandBuilder, Options, PositionalOptions, InferredOptionTypes, MiddlewareFunction and the rest of @types/yargs' ESM surface — and its default export is typed as a factory returning Argv, so a typed chain infers argv and an instance passes wherever a program says Argv. burgee/commander adds OptionValues, OptionValueSource, HelpConfiguration and ParseOptionsResult, opts<T>() / optsWithGlobals<T>() are generic as in commander, and its OutputConfiguration takes any subset. burgee migrate now checks every name an import asks for against what the façade exports: a type-only import of a name it lacks stays on the incumbent and is reported under kept, and any other is refused as unknown-export.

  • Updated dependencies [9800b43, b8e97dc]:

    • bellpull@0.3.0
    • closeout@0.5.0
    • linegauge@0.5.0
    • seniority@0.5.0
    • roundel@0.5.0

0.10.0

Minor Changes

  • #476 23e8b35 Thanks @ofri-peretz! - --schema <command> --field <path> returns a single field of a command's schema, such as --field options.region, so an agent can read the part it needs without loading the whole document. An unknown path step is refused with the valid fields at that level listed.

Patch Changes

  • Updated dependencies [69563d1, 2dc573f]:
    • closeout@0.4.0
    • linegauge@0.4.3

0.9.2

Patch Changes

  • #465 acf98f3 Thanks @ofri-peretz! - Each README now opens with the incumbent it replaces and the agent surface it serves (--json, an agent event, or a static projection), so npm shows both above the fold. README text only; no code changed.
  • Updated dependencies [acf98f3]:
    • roundel@0.4.2
    • linegauge@0.4.2
    • seniority@0.4.2
    • bellpull@0.2.2
    • closeout@0.3.2

0.9.1

Patch Changes

  • #454 b4584e7 Thanks @ofri-peretz! - Every package's npm homepage now points at its page on the docs site, https://burgee.interlace.tools/docs/packages/<name>, and each README links it under the header. The keywords add what people and models search for: burgee gains cli-framework, argument-parser, subcommands, json-schema, mcp-server, model-context-protocol, ai-agent, llm, shell-completion, typescript, zero-dependency, commander-alternative and yargs-alternative; the other eight gain agent, ai-agent, non-tty, json and zero-dependency where the package does that — zero-dependency only on the six that install nothing at all.

    burgee's README gains a short FAQ (commander alternative, agent use, MCP, dependencies) and states the compatibility counts the oracle holds — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' — where it had said 1,215 and 1,185. caique's README no longer calls a released package pre-release.

  • #442 bdaf364 Thanks @ofri-peretz! - Every package now lists plugin, plugins and extensible in its npm keywords, because every package takes plugins through one shared contract.

    A plugin is a plain object, validated against the schema.json that ships in every package, and checked with the package's own check command. Each package reads its own key and ignores the rest, so one object can extend any subset of the family. The plugins page has a nine-layer example that every package's check accepts in CI.

  • #435 7888524 Thanks @ofri-peretz! - schema.json now describes every plugin host in the family.

    The one schema each package ships as its plugin contract used to cover only four hosts: roundel's tokens, flagstaff's glyphs, spinners, borders and components, paratext's capabilities, and linegauge's widths. Five hosts validated their keys in their own code, but the file an author (or a model) writes against said nothing about them. It now describes all of them:

    • bellpull resolvers, including the absolute-path rule on paths
    • caique widgets
    • closeout handlers, including the phases a plugin may use
    • seniority sources, including the rank bounds
    • burgee commands, hooks and enforce

    Where the schema can express a rule, it gives the same verdict as the host's own validator, and a test holds the two together. Function-valued fields (static, run, read, handler) are described and required, but not typed, because JSON Schema can't say "function".

    flagstaff now validates a plugin against only its own keys, not the whole family schema. It no longer refuses a plugin over another host's key, which lets one plugin object contribute to several hosts. Its entry points are also 4.7–5.9 KB lighter for it.

  • Updated dependencies [b4584e7, bdaf364, 7888524, dac303e]:

    • bellpull@0.2.1
    • closeout@0.3.1
    • linegauge@0.4.1
    • roundel@0.4.1
    • seniority@0.4.1

0.9.0

Minor Changes

  • #421 db3c59e Thanks @ofri-peretz! - ExitCode.AUTH — a refused credential gets its own code (E6).

    RUNTIME is the code for everything: it failed, read the message. A caller — a script, a retry loop, an agent — cannot branch on it, so a 401 and a null-pointer look identical from outside and a retry on one is a retry on both, forever.

    import { AuthError } from "burgee";
    
    throw new AuthError(
      "the registry refused the token",
      "the token has expired",
      "mytool login",
    );
    // exit 5, and on --json: { ok: false, error: { code: 5, message, hint, fix } }

    AUTH says get a credential and run it again, which is a different action from USAGE's fix the script and CONFIG's fix the runner. The requirement calls it "the most actionable single code in the survey"; it was the one the taxonomy was missing.

    5, where gh uses 4. Four is CANCELLED here and has been since the contract was written, and moving a published code to match a neighbour's is a breaking change for everyone already branching on it. aws v2 uses 252/253/254 and agrees with nobody either — what matters is that the code is stable and documented.

    fix is the line a caller runs where hint is the prose a person reads, and both reach the --json envelope, so an agent never has to parse the message.

    374 bytes on the core entry.

  • #412 4b64f6a Thanks @ofri-peretz! - burgee/meow — meow's surface, over burgee.

    import meow from 'burgee/meow' takes the place of import meow from 'meow': the options object, the flags contract (type, default, shortFlag, aliases, choices, isRequired, isMultiple), commands, the help and version blocks with autoHelp and autoVersion, showHelp/showVersion, and the input/flags/unnormalizedFlags/pkg result. Graded by meow's own suite at 132 / 148 (89.2%) against a control of 146 / 148.

    meow is one function over yargs-parser, and burgee already ships its own for burgee/yargs, so this takes no new dependency into the tree. The entry costs 59,820 bundled bytes; upstream meow looks lighter only because it depends on yargs-parser rather than carrying it, and a caller installing meow installs both.

  • #428 f0370b4 Thanks @ofri-peretz! - A deprecation now has to name its replacement.

    defineCommand({ name: 'push', deprecated: 'publish', /* … */ });
    options: { legacy: { type: 'boolean', deprecated: '--force' } }

    deprecated: true used to produce (deprecated) in help and warning: 'push' is deprecated on stderr, which tells the reader to stop without saying where to go. defineCommand, and Manifest.use() for plugin commands, now refuse true and '' on a command or any option, and the error says how to fix it. A named replacement already appeared in all three places: (deprecated: use publish) in help, deprecated in --schema, and , use 'publish' in the warning.

    Breaking for anyone who wrote deprecated: true: replace it with the name of what to use instead. Commander and yargs programs running on burgee's front-ends are unaffected. Those incumbents accept a bare deprecation, and the front-ends keep accepting it.

  • #424 fb92e13 Thanks @ofri-peretz! - Help is coloured on a terminal, and NO_COLOR and FORCE_COLOR mean what they say.

    The engine rendered help plain everywhere. That met "no ANSI in a pipe or under NO_COLOR" only by never colouring at all, and it left FORCE_COLOR with nothing to override. Now one decision covers every help path:

    • FORCE_COLOR decides whenever it is set: 0 or false turns colour off, anything else (the empty string included) turns it on, even through a pipe and over NO_COLOR. This matches Node's own getColorDepth.
    • Otherwise help is coloured only on an interactive terminal with no non-empty NO_COLOR and a TERM other than dumb. A detected agent is not an interactive terminal, so Claude Code, Cursor and the rest still read plain help even when they have a TTY.

    Colour adds ANSI and nothing else: stripped, coloured help is byte-identical to the plain render, and NO_COLOR=1 gives back the plain render exactly.

  • #415 47b541a Thanks @ofri-peretz! - The root barrel stops holding five modules open, and the initial load a consumer pays halves.

    import { run } from 'burgee' was loading help.ts, mcp.ts, schema.ts, plugin.ts, manifest.ts and seniority/precedence whether or not a program read any of them, because index.ts re-exported their value half as a convenience. execute.ts already loaded each one behind an await import(); the barrel was the only thing keeping them on the startup path.

    Measured over the transitive closure of import statements — the bytes a bundler actually puts on a consumer's startup path:

    beforeafter
    burgee initial load, bundled57,880 B28,637 B
    burgee static graph, on disk60,823 B42,223 B
    cold start, burgee ÷ cac2.567×1.737×
    modules Node loads for the import2217

    Breaking, and narrowly. Every moved value has a subpath of its own:

    wasis now
    renderHelpburgee/help
    serveMcp, toolsOf, annotationsOf, MCP_PROTOCOL_VERSIONburgee/mcp
    schemaOf, commandSchemaOf, inputSchemaOf, summaryOf, Manifestburgee/schema
    definePlugin, CONTRACT, PluginErrorburgee/plugin
    resolve, explain, envName, screaming, ConfigErrorburgee/config

    Every type stayed where it was. A type re-export is erased and costs a consumer nothing, so the whole type surface — Manifest included, which is what keeps defineProgram's return type nameable — still imports from burgee. A typed program that never called one of the moved functions needs no change at all.

    --explain and --schema also load on their own branch now rather than at import: schema.ts is 2,640 bundled bytes and seniority's explain half is 1,018, and neither runs unless a reader asks for a document.

    This is D-093 reversed. That decision declined the split on a cold-start argument it did not have a number for; the number is 830 ms of ratio and 29,243 bytes.

  • #421 db3c59e Thanks @ofri-peretz! - Every plugin host has a check command.

    npx linegauge check ./my-widths.mjs
    npx burgee check ./my-plugin.mjs --json

    PRINCIPLES 7 asks three things of an extension surface: the plugin is data validated against one published schema, there is a check command that shows it every way it can be seen, and the bar is measured. The first was built in all nine hosts; the second existed in flagstaff alone. So an author writing a plugin for any other host found out what it did by shipping it into a program — and a surface nobody can check is a surface nobody outside this repository can write against.

    Each command validates, registers, and shows what the host does with the plugin, in the host's own terms: linegauge measures each code point before and after the override, paratext shows a capability's encode and its fallback, roundel each token and what it replaced, caique each widget's static projection rendered with its own sample. burgee's returns a document rather than printing one, so burgee check --json is the form an agent that just wrote a plugin reads.

    They share one contract with the author, held identically across all nine:

    • a readable report, contribution by contribution, with ok as the last line;
    • a refusal with a code from the family's vocabulary and a fix, exit 1;
    • E_NO_CONTRIBUTION for a plugin that contributes nothing to this host — the schema allows unknown keys so one object registers everywhere, which makes a misspelled key silent, and this is how that typo tells on itself;
    • exit 2 with no file.

    Each host also gains an eval case measuring the one-turn claim, proved to discriminate before it was committed: green against a correct plugin, red against the same plugin with one field broken.

Patch Changes

  • #421 db3c59e Thanks @ofri-peretz! - runBurgee forwards cwd, stdin and TTY-ness to the engine. It did not.

    The harness built a whole fakeRuntime — argv, env, cwd, stdin, per-stream TTY-ness, exit, clock — and then passed six of those nine to execute. The other three were computed and dropped, which .sdlc/intents/burgee/spec.md records as T1 and calls "the row most likely to make a test pass for the wrong reason". It is, and precisely:

    • tty: true changed nothing. execute reads TTY-ness off opts.stdout.isTTY and hands it to detectAgent; the harness passed a bare { write }. A test asking for a terminal got the non-interactive floor and asserted on it happily.
    • cwd changed nothing. Config discovery starts at io.cwd, which fell through to host.cwd() — the real process directory. A test pointing at a fixture tree was reading the repository it was running in.
    • stdin changed nothing, so nothing that reads it could be driven through the harness at all.

    testing-harness-forward.test.ts is the check and both cases were proved to fail on the six-field version: interactive read [false, false] for tty: true/false, and a <name>.config.json under the given cwd never reached the handler.

    The cost is 92 bytes on burgee/testing, which is test-time only.

  • #415 47b541a Thanks @ofri-peretz! - The MCP server and the help renderer load on the branch that uses them.

    --mcp serves a protocol until stdin closes and --help lays out prose with measured columns; a program that does neither should carry neither. Both are now reached through await import(), so a bundler with code splitting leaves them off the startup path.

    Measured as an entry chunk: burgee 58,056 → 19,540 bytes, and burgee/commander 69,431 → 51,306. No API changed — renderHelp and serveMcp are still exported from the root and still do the same thing.

    commander's --schema deliberately stayed synchronous: parse() is synchronous by contract and a lazy import there returned a Promise nobody awaited, so the document never printed. The suite caught it.

  • #421 db3c59e Thanks @ofri-peretz! - onError fires on the commander and yargs front ends. It did not.

    The plugin contract is three hooks, and the contract between them is what makes them usable: preRun opens, and exactly one of postRun or onError closes. A plugin that starts a span, opens a file, takes a lock or writes an audit line in preRun has nowhere to finish it otherwise — and "otherwise" is every command that throws.

    The engine held that. Both façades ran preRun → handler → postRun as a .then chain, so a handler that threw skipped postRun and never reached onError: a plugin got an opening hook and no closing one at all. And onError was never fired by either façade under any circumstances, so a plugin declaring it was silently dead on a commander- or yargs-syntax program — the two drop-in front ends this package exists for, and a hook neither commander nor yargs can offer at all.

    plugin-lifecycle.test.ts holds the contract on both façades, in both directions, and all four new cases were proved to fail on the .then-only chain.

    68 bytes on burgee/yargs, 62 on burgee/commander.

  • #418 c0fa8a3 Thanks @ofri-peretz! - explain moves from seniority/precedence to seniority/explain.

    A re-export is not free across a package boundary. explain was exported from precedence.ts, so every program that resolved a configuration loaded explain.js whether or not anything ever explained one — 1,018 bundled bytes and one more module on the startup path for the branch taken when a user asks why did this option get that value.

    import { explain } from 'seniority' is unchanged: the root barrel still exports it, from its new home. Only seniority/precedence stops re-exporting it. burgee/config re-exports it the same way it always did, and burgee's engine loads it behind an await import('seniority/explain') on the --explain branch, which is now the only thing that pays for it.

    Measured on burgee's core entry: 28,637 → 27,552 bundled bytes, and 22 → 21 modules for import 'burgee'.

  • #418 c0fa8a3 Thanks @ofri-peretz! - burgee/yargs loads the MCP server on the --mcp branch, not at import.

    yargs/factory.ts imported serveMcp at the top of the file while the note above #surfaces said "Completions and --mcp load lazily, so those two return a promise". The note was right about the shape and wrong about the fact: the branch already returns a promise, so the server always could have been loaded on it, and until now its 2,520 bytes sat on the startup path of every burgee/yargs program.

    burgee/yargs, bundled     107,665 B  ->  105,240 B
    burgee/yargs ÷ yargs          0.969  ->  0.947

    The same shape as the root-barrel split, missed here because a comment said it had already been done. The weight lock's ./yargs budget comes down from 256,000 to 214,800 with it — a ceiling 41 KB above the measurement is not a ratchet.

  • Updated dependencies [47b541a, 4d1b2b3, db3c59e, db3c59e, c0fa8a3]:

    • bellpull@0.2.0
    • closeout@0.3.0
    • linegauge@0.4.0
    • roundel@0.4.0
    • seniority@0.4.0

0.8.0

Minor Changes

  • #382 0a37307 Thanks @ofri-peretz! - burgee migrate — the mechanical path from commander or yargs to burgee.

    Every migration that actually happened shipped the codemod before the wave, not after it: jest-codemods, pnpm import, biome migrate eslint. burgee already had the strongest possible version of the claim — change one import and commander's own 1,360 tests still pass — and no path from could to did.

    $ burgee migrate --dry-run
    files: 3
    imports: 3
    mapped: [{"from":"commander","to":"burgee/commander","imports":3,"files":3}]
    refused: []
    detected: {"declared":["commander"],"imported":["commander"]}
    dependencies: {"before":["commander"],"removable":["commander"],"after":0}
    graded: [{"host":"commander","reference":1360,"passed":1360,"rate":1}]

    It detects hosts from two independent sources that are allowed to disagree — package.json and the specifiers source actually imports — rewrites commander, yargs, yargs/yargs and yargs/helpers across all five specifier positions, and refuses by file and line on a deep import or a non-literal dynamic specifier, leaving that whole file untouched (D-051). It refuses a dirty git tree unless --force, writes nothing under --dry-run, never edits package.json (D-054), and never touches an API call site (D-053). The compat figures are read from compat-oracle's baseline through a lock, never typed into the report.

    Specifiers, not syntax trees (D-050): a scan over five known positions needs no parser and therefore no dependency, and it is the fast choice as well as the rule-2 one. Measured over a generated 1,000-file tree: the whole scan phase is 17 ms and the slowest single file 0.074 ms against a 1 ms budget; the rest of the command is filesystem.

    The gate is examples/demo-cli-commander, which holds the same program written twice — once against commander, once against burgee/commander — and predates this feature. Migrating the commander variant produces the hand-written drop-in's import byte for byte.

    The engine gains one thing on its behalf, in execute.ts: a command's result may name an exitCode, and emit honours it. Before this there was exactly one success path and it left with OK, so a command could emit a document or fail, never both — and an agent migrating a repository unattended needs the refusal list and the code. 162 bytes measured (60,661 → 60,823 on the root entry), opt-in by naming the field. migrate itself is 10,878 bytes loaded through a dynamic import and is denied to the root entry by name, so import 'burgee' never reaches it.

  • #384 7f32875 Thanks @ofri-peretz! - A program written in commander's or yargs' own syntax now gets burgee's agent surfaces.

    --mcp lists every command instead of none: a façade command arrives with no effects because neither incumbent has such a concept, and the filter used to read undeclared as not a tool. It is now listed with effects: "undeclared" — absent from the list is strictly worse for a caller than present with an honest annotation. withheld still means absent.

    tools/call used to return nothing at all — the reply writer was read out of _outputConfiguration at reply time, and the first tool call replaces that so the run can be captured. A client waited forever. It now answers the --json envelope, call after call.

    --json works at the root of a command group and no longer swallows the operand after it, and a failure under --json is the envelope on stdout with fix: rather than prose on stderr.

    commander stays 1360 / 1360 and yargs 804 / 804 against their own suites.

Patch Changes

  • #385 c219e65 Thanks @ofri-peretz! - A kebab-case option key is now refused where it is declared, instead of silently never reaching the handler.

    toParseConfig kebabs each declared name to build the flag and canonical camels every parsed key back, so the flag layer handed to resolveLayers is keyed camelCase while the specs beside it are keyed as declared. Declare 'dry-run' and the two never meet: --dry-run parses, resolves to nothing, and the handler is given undefined. No error anywhere.

    Three of burgee's own commands were live instances. burgee brand --allow-low-contrast never suppressed the WCAG failure it names, --bordure-width was always 1.5 whatever was passed, and burgee dev --no-watch still watched. All three are fixed by spelling the key camelCase; the flags are unchanged, and --bordure-width 7 now reaches the SVG as stroke-width="14" against the default's 3, --allow-low-contrast emits, and --no-watch logs no reload when the entry is edited.

    Breaking for a declaration, not for a command line. checkDefinition refuses a key that does not survive camel(kebab(key)) — 'dry-run', and also URL, whose canonical form is url. It throws through the clash message that was already there, because it is the same defect: two keys that meet on the command line, one of them written by kebab() rather than by the author. The engine was not taught a second spelling. Threading one through help, --schema, Fig, the env, config and package.json layers and the relation names, to reach a key that already has exactly one canonical form, is a larger surface than the bug.

    No weight ceiling moved, which is what D-073 asks for. ./plugin had 5 bytes of headroom and a standalone message cost 187, so two things moved to pay for it: V5's reserved names out of their own loop in checkCommand into the pass checkDefinition was already making, and the numeric-bound check out of that loop into the spec helper beside the relation names. Both read better where they are now — flag is computed once and kebab is the identity on every reserved name, and a numeric bound is a fact about a spec rather than about a name. definition.js is 51 bytes smaller than before the check existed. checkDefinition now carries the reserved names; checkCommand is still the one door both callers reach.

    Found while building burgee migrate: it showed up only through the built binary, because every in-process case had been written camelCase.

  • Updated dependencies [5a85175, 88a6996]:

    • linegauge@0.3.2
    • seniority@0.3.1

0.7.1

Patch Changes

  • #373 a1f1d40 Thanks @ofri-peretz! - Stage 2's artifact is now spec.md, the name Anthropic's AI-Native SDLC playbook gives it, so the source comments and README sections that cite a package's own design document point at spec.md rather than design.md.

    No behaviour changes. The published tarballs do move, by two bytes per surviving reference — design.md is nine characters and spec.md is seven — so the four packages carrying a weight band were re-measured against it: linegauge 83,538 to 83,536; paratext 66,343 to 66,341; closeout 84,455 to 84,453; bellpull 86,113 to 86,107.

  • Updated dependencies [955b979, a1f1d40]:

    • seniority@0.3.0
    • bellpull@0.1.1
    • closeout@0.2.1
    • linegauge@0.3.1

0.7.0

Minor Changes

  • #332 3ea38c3 Thanks @ofri-peretz! - E5 and O5, which the design has marked R since it was written and which nothing implemented.

    exit-code.ts has declared SIGINT: 130 with the comment "SIGINT after the terminal was restored (E5)" from the first day of the contract, and exit-code-lock.test.ts grades that no other literal reaches an exit. Neither could see what was actually missing: no code path produced 130 and nothing restored anything. grep -rn SIGINT packages/burgee/src returned the declaration and nothing else. O5 — "stdout is flushed before any exit path", yargs #1519 and #2118, "No truncated JSON" — was the same shape one line down, against host.exit(code), which is process.exit and truncates a pipe by definition.

    Both are now closeout's, which is burgee's first dependency on it and the reason it exists: bound every exit path, run the handlers exactly once, hand the terminal back last. Writing the listener in burgee instead would have been the fourth copy of one in this repository.

    • ctx.onExit(handler, label?) — cleanup that runs on every path out of a run: a normal return, ctx.exit, Ctrl-C, SIGTERM, a terminal closing out from under you, an uncaught throw. It runs after stdout has drained and before the terminal is handed back, and exactly once however many of those arrive together. label is what a breached shutdown deadline calls it; an unlabelled arrow is reported as (anonymous), and the anonymous arrow is the shape that hangs.
    • A run that owns the process gets closeout's full wiring — exit, beforeExit, five signals, uncaughtException, unhandledRejection. A run that injects its own exit — the harness, the MCP loop, every façade test — gets the same registry detached, with no listeners on anybody's process.
    • Every exit now goes through one place. ctx.exit(code) used to call the injected exit and then throw; on the real path the first half was process.exit, so cleanup registered a line earlier could never run. It now throws only, and the failure path drains, runs the cleanup and leaves — which makes the file's own sentence, "exactly one code path from argv to exit", true of the exit as well as of the parse.

    Measured: commander 1360 / 1360 and yargs 804 / 804 before and after, unchanged — the two front-ends do not reach execute.ts. The core entry is 51,293 → 52,683 bytes against an unchanged 53,300 budget, so nothing was ratcheted for it; shutdown.ts is 1,001 of those and the engine's routing is the other 389. npm i burgee gains closeout's 81,360 bytes unpacked, and closeout depends on nothing.

  • #361 a0cb8ca Thanks @ofri-peretz! - An error carries fix — the exact flag to run next — beside hint.

    E3 asks for "code, message, hint, and where possible fix: the exact command or flag to run next". The envelope was {code, message, hint}, and hint is prose.

    The distinction matters most for the caller this package exists for. An agent can execute a fix. A hint it has to read, interpret and guess at — one more turn, and the turn where it invents a flag that does not exist. Every plugin error in the family already carried fix; the engine's own did not.

    $ tool deploy --forc --json
    {"ok":false,"error":{"code":2,"message":"unknown option --forc",
                         "hint":"did you mean --force?","fix":"--force"}}

    fix is omitted, never guessed, when there is no near match: an executed guess burns the turn the field exists to save.

  • #361 a0cb8ca Thanks @ofri-peretz! - --help --json prints help as data.

    It printed the same prose as --help. A caller who asked for a machine-readable answer got one they had to parse — the exact failure the whole --json surface exists to avoid, on the flag people type first. burgee's design recorded it as F2, Not built: "no JSON help surface; --help --json prints the same prose as --help."

    The document is commandSchemaOf scoped to the node you asked about — the same shape --schema publishes, so a reader learns it once — plus schemaVersion, and for a group the names of its children. A group's help is a menu; a reader who wants a child's detail asks for that child, which is the same walk they would do on the text.

    Plain --help is unchanged.

  • #337 e974114 Thanks @ofri-peretz! - Options can declare dependsOn and exclusive, and the declaration is enforced rather than documented.

    They are an alias, not a second engine: exclusive desugars to conflicts and dependsOn to implies, into the Relation union that already existed. Command-level relations are evaluated first and the derived entries after, so no existing command changes which error it reports first.

    The second spelling exists because it is the one the two incumbents use on the option rather than on the command — commander's .conflicts() / .implies() and yargs' conflicts / implies both hang off an option, and a drop-in that only accepts the command-level form is not drop-in.

    Enforced at parse time through the existing usage path: UsageError, exit 2, text error: --out requires --force with hint: pass --force, and --json gives exactly {ok:false,error:{code:2,message,hint}}. No new error shape.

    Refused at definition time when a name is not an option of that command, or is the option itself. Both are silent at run time, in opposite directions: the first can never fire, the second always does.

    Projected three ways, because a relationship a caller cannot see is a relationship they will violate: --schema carries both spellings, help renders (requires --force) and (conflicts with --table) — it rendered no constraint of any kind before this — and the Fig spec emits dependsOn / exclusiveOn, Fig's own two keys.

  • #334 0f00f72 Thanks @ofri-peretz! - burgee's plugin host now refuses a plugin it cannot host, and a plugin's commands go through the guards a first-party command goes through. This is a breaking change to a published extension point, and it is deliberately a loud one.

    The defect, measured rather than inferred. definePlugin(plugin) was return plugin;, and Manifest.use() pushed the plugin and called this.add() directly — where defineCommand enforces the reserved names of V5 and runs checkDefinition. So a plugin's command was admitted unread, and toParseConfig seeds json: { type: 'boolean' } and then writes every declared option over the top of it. A plugin option named json therefore did not clash with the envelope flag; it replaced it, on a framework whose entire agent-facing contract is that --json is machine-readable output. Four more were accepted the same way: enforce: 'mid' (NaN in the hook comparator), a plugin with no name (commands carried plugin: undefined, so M3 attribution was silently lost), a hook with no handler (a TypeError one run later, classified RUNTIME), and a contributed path that was already declared — which find() and resolve() answer differently.

    What contract means, and how an old plugin fails. packages/burgee/src/plugin.ts is now a plugin host in the sense the rest of the family means: it owns Plugin, validate(), PluginError and a PluginErrorCode of E_PLUGIN_SCHEMA | E_PLUGIN_CONTRACT, both already in the vocabulary home's union. contract is the revision of the family plugin object a plugin was written against, and this burgee knows 1. A plugin that declares none is refused with E_PLUGIN_CONTRACT naming the version:

    plugin "acme" declares no contract; burgee 0.6.1 and earlier validated none of it — rebuild it against this burgee (definePlugin stamps contract: 1), or add that key by hand

    That refusal is the point rather than a side effect. An object with no contract was authored against a host that checked nothing, so the honest reading of its silence is unknown, not fine — and it may be carrying exactly the json option above. A silent behaviour change on a published extension point is worse than a loud breaking one. definePlugin now stamps the contract it was compiled against, so a plugin rebuilt against this release needs no edit, and only objects built against the unvalidated host are refused.

    Two supporting changes. The definition-time checks moved from validate.ts to a new definition.ts, because the plugin host pulls them into every graph that reaches the manifest — including the commander and yargs front-ends, which reach nothing else of the engine. Importing validate.js whole for checkDefinition put 6,409 bytes of run-time coercion into both front-ends and took burgee/commander over the 128,000-byte budget that exists to prove it is no heavier than commander's own lib/; the split keeps it at 125,667. And burgee ships src/schema.json, byte-identical to the family's, exported as burgee/schema.json.

    Measured: burgee/commander 1,360 / 1,360 and burgee/yargs 804 / 804 against the incumbents' own suites, unchanged. Core costs 4,191 bytes on disk (52,959 → 57,150), priced per entry in weight.test.ts.

  • #361 a0cb8ca Thanks @ofri-peretz! - burgee's extension surface: ./plugin is published, and effects is no longer optional.

    burgee/plugin. Every other host in the family publishes its plugin module at <host>/plugin — bellpull, caique, closeout, flagstaff, paratext, roundel, seniority. burgee, the package that declares the plugin shape the other seven register against, did not, so scripts/plugin-contract-lock.test.ts had to reach it by relative path and recorded the gap as a declared one. It is the shape plugin-schema-lock caught in flagstaff — a host whose own refusal names something the author cannot reach — and burgee's version was the quieter kind, because the fix named no specifier at all: it said rebuild it against this burgee, definePlugin stamps the contract, and left the author to work out where definePlugin lives. The convention the family teaches is burgee/plugin, and that threw ERR_PACKAGE_PATH_NOT_EXPORTED. Both halves are fixed: the subpath resolves, and the message says its name. burgee/plugin exports CONTRACT, definePlugin, validate, PluginError, Plugin and PluginErrorCode; the root barrel keeps the four it always carried, because one module behind two doors is what burgee/yargs and burgee/yargs/helpers already are, and Manifest.use(plugin: Plugin) is a root export. validate() and the Plugin interface are the half only the subpath carries — the host's vocabulary rather than a program author's.

    effects is required on a command that runs — a breaking change. Any CLI with an un-annotated runnable command will now fail at definition time rather than starting. The one-line migration: add effects: 'withheld' to every runnable command that declared none, then replace it with read_only, idempotent or non_idempotent on each command an agent should be able to call.

    It is breaking on purpose, because the old behaviour was silent. toolsOf serves only a command that declared its effects, and that filter is right and unchanged: an agent gaining shell-equivalent power over a CLI nobody meant to publish is a security posture, not a convenience. What was wrong is that its input had one spelling for two different things. I decided agents should not have this and I forgot both arrived as undefined, so the second shipped as the first — you released, and the tool you built for an agent simply was not in tools/list, with a shorter list than you expected as the only evidence.

    So effects has no default, and declining is something an author writes down: effects: 'withheld', a fourth value of the same field. Not 'none', which reads as this command has no effects — that is read_only, the one value it could be confused with. Not a second boolean field either: beside a now-required effects that would mean declaring what a command does to the world silently opts it into the tool list, and the new field's default would be the silence this change removes. One field, four answers, no default, and therefore no state in which forgetting is possible.

    The three projections then disagree on purpose, each correctly. --schema publishes effects: "withheld", because an agent reading a program as data is better served by this exists and is not for you than by a gap it cannot tell from a command that does not exist. tools/list omits it. The Fig spec and the shell completions carry it exactly as before — nothing in completions.ts reads effects and nothing here makes it start, because withholding is about agents and a person typing at a terminal is not one.

    Two limits, stated rather than implied. The refusal is on burgee's own declaration API: a command built through burgee/commander or burgee/yargs reaches the manifest without passing that door, because neither incumbent has a notion of effects and their graded suites declare none — commander 1360 / 1360 and yargs 804 / 804, both unchanged by this release — so a façade user's command is withheld in fact and cannot be made to say so. And effects stays optional on the TypeScript type, because a field whose presence depends on a sibling's would mean splitting Command into a union at the cost of the option-spec inference every caller relies on; the check is at definition time, not at compile time.

    burgee dev is the first command in this repository to declare 'withheld', and not as a formality: dev is an MCP server, so a tool call that started it would be a second, never-finishing server nested inside the first, on the same pipe. burgee brand declares non_idempotent, because it overwrites six files in a directory the caller names.

    Weight, measured on a forced rebuild rather than a cached dist/: the core entry goes 58,771 → 59,732 bytes on disk (+961 over both changes, of which +938 is the refusal), burgee/testing 62,992 → 63,953, burgee/cli 78,071 → 79,088, burgee/commander 126,989 → 127,876 inside its unchanged 128,000, and burgee/yargs 230,869 → 231,756 inside its unchanged 256,000. The new burgee/plugin entry is 7,095 and costs a program nothing: manifest.js imports validate as a value because use() is synchronous, so every entry that reaches the manifest already carried those bytes.

  • #360 61c51f9 Thanks @ofri-peretz! - burgee/commander spawns executableSubcommand through bellpull, and burgee no longer imports node:child_process anywhere.

    The gap was declared, dated, and carried its own release condition. inline-implementation-lock read: "commander's executableSubcommand spawns a sub-binary and forwards five signals to it. Both are bellpull's job; bellpull was a seven-line placeholder when this was written, and the engine lane adopts it once bellpull grades against cross-spawn's suite." It grades 68 / 68.

    What it buys a consumer is the Windows branch. Upstream commander sends every Windows spawn through node, because spawn does not search PATHEXT and, since the fix for CVE-2024-27980, Node refuses a .cmd without shell: true. That is a workaround for a resolution problem, and it is wrong for a subcommand that is a .cmd, a .bat, or has a shebang that is not node — a real program with a real sub-binary. bellpull resolves the executable, builds the cmd.exe /d /s /c line itself and escapes every argument, so nothing reaches a shell as text.

    ChildProcess comes from bellpull/cross-spawn too, which re-exports it precisely so a consumer does not have to name node:child_process for a type.

    commander stays 1360 / 1360, ▲ 0, measured after the wiring — which is the whole question, since spawn is mocked in roughly 23 of those cases and the earlier attempt at this took the row to ungradeable. bellpull/cross-spawn reads spawn off its default import for that reason, and this consumer reads it off the namespace at the call site.

    Cost: +1,097 B on ./commander, ratcheted at the measurement.

Patch Changes

  • #332 3ea38c3 Thanks @ofri-peretz! - Two checks that grade what was previously asserted by snapshot or by fake: the Fig spec is checked against Fig's own vocabulary, and Ctrl+C is pressed at a real terminal.

    fig-schema.test.ts — Fig publishes no schema, and that is the finding. PLAN 2.5.3 asks for the emitted spec to be validated "against Fig's own schema". There is none: @withfig/autocomplete-types@1.31.0 ships four files — LICENSE, README.md, package.json and index.d.ts — so the contract is a TypeScript namespace declaration, not anything a validator reads at runtime. It was last published 2024-05-08.

    So the check is structural, and the objection fig-spec.test.ts recorded against doing one — "writing the allowed-key table from memory would be worse than not checking" — is answered by giving the table a provenance rather than by giving up. The allowed keys were extracted mechanically from Fig's own index.d.ts, recorded with the version and the file's SHA-256, the way compat-oracle vendors an incumbent's suite. No dependency is added: the table is forty strings, and the package is not installed, not in devDependencies and not in the lockfile. It reproduces Fig's own asymmetry — Subcommand and Option extend BaseSuggestion, Arg extends nothing and so has no priority and no displayName — which is the part a table written from memory gets wrong.

    Covered: every emitted key is one Fig declares, on the node type it is emitted on; name is present and is SingleOrArray<string> where Fig requires one; subcommands, options, args and suggestions have the shapes Fig declares; the whole tree is walked. Not covered, and said rather than implied: Fig's semantics past its key names — a priority outside 0–100, a malformed generators entry, a loadSpec naming a spec that does not exist. Nothing here means "this works in Fig", only "this is not obviously not a Fig spec".

    Proven red on the unfixed state: with renderFigSpec emitting subCommands — the typo the snapshot blessed — the case fails with <root>: 'subCommands' is not a key Fig declares on a subcommand. Nine more cases prove it refuses a missing name, a name of the wrong type, a container that is not a list, an arg given a subcommand-only key, and a fault three levels down; one more proves it is not simply refusing everything.

    pty-signal.test.ts — the signal path, graded through a real tty. shutdown.test.ts raises signals on a ProcessLike that records, so the "signal" never leaves the test process. Between a keypress and a handler sits the tty line discipline, which neither that test nor a pipe has: in canonical mode ISIG turns 0x03 into a SIGINT, and in raw mode the identical keystroke arrives as a byte and raises nothing. A test that writes \x03 to a pipe grades the raw path whatever it believes it is grading — which is how caique shipped a prompt that restored the cursor on a cancel and never on a signal, with a green suite.

    This runs burgee's built dist/shutdown.js on a real pty, presses Ctrl+C, and asserts the tty echoed ^C, that the handler the program registered ran, and that the process died of SIGINT rather than calling exit(130) — which is the POSIX-correct outcome and not what shutdown.test.ts's fake records: 130 is a shell's arithmetic for 128 + 2, not an exit call. A program that exited 130 here would tell its parent it chose to stop.

    The pty comes from python3's standard-library pty.fork(), so nothing enters the lockfile — the same borrowing as calling git in compat-oracle/src/vendor.ts. Two other dependency-free routes were considered: script(1) was measured and rejected, because BSD script calls tcgetattr on its own stdin and dies with Operation not supported on socket under any test runner; and zsh/zpty, which scripts/complete-zsh.zsh already uses for the zsh completion case, is right where the subject is a shell widget but is gated on has('zsh') and an apt-install, where python3 is preinstalled on every hosted runner. Windows is skipped with its reason, not quietly dropped: Python's pty is POSIX-only and a Windows pseudo-console means ConPTY through a native addon, so the third OS PLAN 2.5.4 asks for costs node-pty — a native build on every runner, and compat.yml installs with --ignore-scripts. That is a decision for a person.

    Proven red on the unfixed state: with the fixture using the engine's pre-shutdown.ts exit — a bare process.exit(130) on SIGINT — both cases fail, on cleanup ran: expected '' to be 'cleaned up' and killed by SIGINT (2): expected +0 to be 2.

    commander 1360 / 1360, yargs 804 / 804 and cross-spawn 68 / 68 before and after, unchanged.

  • #332 3ea38c3 Thanks @ofri-peretz! - Help measured its columns with String.length, so a CJK or emoji command name mis-drew its own help screen.

    .length is the count of UTF-16 code units, which equals the number of columns a terminal draws only for the Latin-1 subset. 部署 is two code units and four columns; 🚀 is two and two. help.ts used it in five places — sizing the shared term column, deciding which terms overflow it, padding after a term, and both width tests inside the word wrapper — so a program whose commands are not spelled in ASCII got a description column that did not line up and description text wider than the terminal it asked for.

    yargs/cliui.ts, one directory over, has imported width from linegauge for exactly this job since it was ported, and its own comment records the reason: cliui's port carried its own stringWidth, the ITU T.416 sub-parameter form ESC[38:2::255:0:0m that chalk emits for truecolor left :2::255:0:0m behind, and a 13-column string measured 25. burgee already depended on linegauge. This file simply was not asking.

    • Every measurement of rendered text in help.ts is now linegauge's width, and the term column is widest, which is the function that exists so a caller does not spread a large array into Math.max. The wrapper carries a running column count rather than re-measuring the accumulated line per word, so a long paragraph stays linear.
    • For ASCII the two agree exactly, which is why no graded screen moves: commander 1360 / 1360 and yargs 804 / 804 before and after, unchanged.
    • What is still not fixed, in any character set: a single token longer than the row is not broken. wrap('see https://…/no/spaces now', 20) leaves the URL on one over-long row today, linegauge's own wrap defaults to hard: false for the same reason, and hard-breaking would re-draw the graded screens that contain URLs. A test pins that as a known limit rather than leaving it to be re-found.

    Help also has snapshots now (PLAN 2.5.1), which it had none of: five command shapes × the plan's three widths — 33 where the term column is clamped, 80 where the ordinary case wraps, 120 where alignment is what is on trial. A help screen is a drawing and a drawing is a contract, which is the call this repository already made for boxen; the renderer's by-construction fixes were each asserted once by a test that names the property it checks, and therefore could not see a change nobody was looking for.

    The core entry is 52,683 → 52,893 bytes against an unchanged 53,300 budget, so nothing was ratcheted. linegauge joins closeout and seniority/precedence as a bare import core admits, on the same argument as both: measuring a line is linegauge's own job the way precedence is the parser's, and the alternative here was the second copy of a width function staying wrong.

  • #343 b245fb0 Thanks @ofri-peretz! - burgee declares sideEffects, so a consumer's bundler may drop a module nothing imports.

    Every module in the package is a declaration or a pure const except one, and that one is named rather than the field being set to a flat false: dist/cli.js ends in run(program), because it is the package's own command line and executing on import is the whole point of it. sideEffects: ["./dist/cli.js"] is therefore the accurate statement, where false would have been a claim the package does not meet. roundel and flagstaff already carried the field; burgee did not, which is the only reason this is a change rather than a fact.

    Measured against the B4 fixtures, esbuild takes 9 bytes off the core entry point (56,868 → 56,859) and nothing off burgee/commander or burgee/yargs — esbuild's own tree-shaking had already reached everything the field would have licensed it to drop. The field is worth more to webpack and rollup, which consult it directly and are conservative without it. No entry point changes shape, and every compat row is where it was: commander 1360 / 1360, yargs 804 / 804.

  • #351 488cbe5 Thanks @ofri-peretz! - --schema on a yargs-shaped CLI now emits the same document as the other two front ends.

    yargs/factory.ts hand-rolled JSON.stringify(schemaOf(manifest), null, 2) where execute.ts and commander/command.ts both call machineJson(value, head). The façade therefore could not see --format=json-pretty — the escape hatch R1 added for the person debugging a schema — and emitted the indented document unconditionally. On the fixture this change is tested against, a plain --schema wrote 365 bytes through yargs against 240 through the engine and through commander: the same value, 52% more bytes, and no way to ask for either form.

    What changes for a caller. A yargs-shaped program's --schema is now compact by default, and indented only when --format=json-pretty is passed. Both documents parse to the same value, so a reader that parses is unaffected; a reader that diffed the raw bytes, or eyeballed the stream, will see the compact form where it used to see the pretty one.

    The cost is +71 bytes on burgee/yargs bundled (114,738 → 114,809): machineJson and its flag constant could previously be tree-shaken out of that entry, and now cannot. burgee core and burgee/commander are unchanged to the byte. That entry is already over lighter-than-yargs (1.032 → 1.033), and this makes it marginally worse on purpose — three front ends that disagree about what --schema means is not a weight saving, it is a defect the weight measurement was hiding.

    The property is now locked end to end rather than per writer: machine-json.test.ts drives one CLI definition through all three front ends and asserts the bytes are identical, in both the compact and the pretty form. The two suites that existed before were each true of a single writer in isolation, which is how three writers came to disagree.

  • #298 ead5f01 Thanks @ofri-peretz! - cliui's toString() is now linear in the cell it renders. rowToString ended each line with str.replace(/ +$/, ""), whose unanchored start makes the engine retry at every position in a run of trailing spaces; a row built from a 50,000-space cell cost 1,223 ms, and doubling the cell quadrupled it. The trim now scans, and the same call takes 69 ms — the second half of the fix that measurePadding got in #278. Output is unchanged: only U+0020 is removed, so a trailing tab still survives under wrap: false as it did before.

  • #332 3ea38c3 Thanks @ofri-peretz! - A ratchet on whether the family actually composes: every package except burgee must be used by another package in it.

    layer-boundaries-lock is the negative half of PRINCIPLES rule 14 — no package does a job a sibling exists to do. This is the positive half, and it is the one that was failing. Measured: five dependency edges in a nine-package family, with caique, paratext, closeout, bellpull and flagstaff used by nothing at all — and one job, putting the cursor back however the process dies, implemented three times, by three files each of which argues in its own comments that a second copy is the danger.

    A layer nothing else uses has never been proven to fit the stack. The split into nine packages is only real if the packages compose; otherwise it is a directory layout and the fit is an assumption.

    edges may only go up and awaiting may only shrink, each entry carrying the reason it is still there. A package must leave awaiting the moment it gains a consumer — otherwise the list becomes a place to park the problem, and the ratchet never notices the work was done.

  • #332 3ea38c3 Thanks @ofri-peretz! - Six designs now say what their package offers, how a consumer extends it, and what it deliberately does not do — derived from package.json's exports map and src/plugin.ts, not from the README.

    burgee, roundel, flagstaff, caique, closeout and bellpull each gain two sections: a table with one row per published subpath and the exported names behind it, an extension section stating what a plugin may contribute, what is validated, what is refused and what happens on a bad one — and a record of every claim the design was making that the code does not support. Each table carries the two commands that re-derive it, so the next reader checks rather than trusts.

    The findings are the point. roundel/import (fromBase16, fromITerm) is described in roundel's R11 and in its shipped README and exists in neither the exports map nor src/. bellpull's R7 promises a root default export matching execa's and a ./run-path subpath; neither exists, so the npm-run-path override recipe cannot be written, and R3's which is spelled whichSync in the code while R5's toJSON is toJson. closeout's R6 promises a root default matching signal-exit's, and the root has no default export. flagstaff's R6 names flagstaff/table as the cli-table3 façade — ./table is the built-in grid component and ./cli-table3 is the façade, so a reader following R6 imports the wrong module — and its R10 "depends on roundel only" is contradicted by the package's own shape test, which asserts three dependencies.

    Two structural findings cross package lines. burgee's definePlugin is not the shape the layers register against. manifest.ts declares { name, commands?, hooks?, enforce? } — no contract, no layer key — validates nothing (its body is return plugin;), refuses nothing, and has no src/plugin.ts, so it is outside the vocabulary lock that polices every other host. Plugin-contributed commands bypass defineCommand, so the reserved-name guard and checkDefinition never run on them, and a plugin option named json silently overwrites the envelope flag. And the shared schema.json describes none of the three newest keys: widgets, handlers and resolvers validate only because the root sets additionalProperties: true, so caique, closeout and bellpull each publish a schema that says nothing about the one key they host — and announces itself as flagstaff's file.

    Documentation only: no packages/** file is touched, and npx tsx scripts/plan-progress.ts prints the same 22/36 before and after, byte for byte.

  • #332 3ea38c3 Thanks @ofri-peretz! - The cliui backtracking guard is checked by shape rather than by clock, after three timing instruments failed on it, each differently.

    1. < 400 ms at a small size — a CI box returned 440. A 10% margin measures the runner.
    2. A growth ratio read 15.04 on macOS CI against 4.09 locally for identical code, batched 256 times, so not noise. Per call that runner was 3x slower at n and 11x slower at 4n: a 48,000-character cell is 96 KB of UTF-16 where a 12,000-character one is 24 KB, and the larger crosses a cache boundary the smaller does not. The ratio measured the memory hierarchy, and no ceiling repairs that.
    3. An absolute budget cannot work either, and the numbers say why: at n = 50,000 the quadratic implementation costs 1,072 ms here while the linear one costs ~1,780 ms on CI. Correct code on the slow machine is dearer than buggy code on the fast one, so no threshold separates them — and any threshold that passes CI cannot fail locally.

    The bug is one shape: a quantifier with no anchor before it, matched against the row text, so the engine retries at every position in a long run and each attempt walks to the end. cliui.ts's own comment records the cost — 1,049 ms of toString()'s 1,223 ms for a cell of 50,000 spaces, quadrupling when the cell doubled. The check now asserts that shape is absent: deterministic, microseconds, no flake. Reintroducing str.replace(/ +$/, "") turns it red.

    What it gives up is generality — it catches the shape rather than the behaviour, so a new quadratic written another way would pass. That is stated in the test. Its first run also matched the comment that documents the bug, which is why comments are stripped first: the third checker in this repository to be caught reading printed source rather than shape.

  • #324 4a7b4ca Thanks @ofri-peretz! - scripts/lanes.ts --check now grants every lane its own changeset, which .sdlc/LANES.md has granted since the first run of these lanes.

    The document said it; the script did not implement it. So --check called each lane's own changeset a stray, and every lane brief had to tell its agent to ignore the result of its own boundary check — which makes the check worth nothing. A rule stated in the document and absent from the enforcement is the exact drift this file exists to prevent, committed by the file that prevents it.

    The exemption is read from the paragraph that grants it rather than written down a second time, and it is narrow on both axes: .changeset/config.json is still a stray, * does not cross a slash, and another lane's source file is still another lane's. lane-boundaries-lock.test.ts holds all three, and goes red when the exemption is reverted.

  • #332 3ea38c3 Thanks @ofri-peretz! - A lock for PRINCIPLES rule 14: no package does a job another package in the family exists to do.

    The family splits nine ways precisely so a program can adopt one layer without the other eight. The moment burgee measures a string's width itself, or reaches for chalk instead of roundel, that split stops being real and the layers become a directory layout. Until now that was intent — every other invariant here has a lock and this one did not.

    The concern table is not restated. compat-oracle/src/demand.ts already declares which incumbents each layer replaces, and that list is the definition of each layer's job, so the rule is derived from it: a package may not depend on an incumbent another layer replaces, nor on the one it replaces itself. Needing that job is the same thing as needing the sibling.

    It does not forbid a drop-in façade reproducing its own incumbent — burgee/commander spawns child processes and forwards five signals because commander's executableSubcommand does, and commander's own 1360-case suite grades exactly that. Reproducing the incumbent is the compatibility claim.

    Family state today: no package depends on any incumbent, its own or a sibling's, and no package carries a runtime dependency outside the family.

  • #326 88f7ba6 Thanks @ofri-peretz! - Read the process through one live seam, and fix the cliui growth gate's instrument.

    src/runtime.ts is now the only file in the package that names process (PLAN 4.3, Y9); the allow-list in process-reference-lock.test.ts is down from nine burgee entries to one. Every member of the new host export is a getter, because the commander and yargs front-ends reproduce their incumbents' process contracts and those suites swap process.argv, exit and env per test — a captured object literal would hand a test the value from before its own swap. Graded before and after: commander 1360/1360, yargs 804/804, unchanged. Two reads that had been captured at import are now live, yargs-parser's default env among them.

    growth() in yargs/cliui.test.ts was measuring the clock on one of its two assertions: at n = 12,000 both the cost at n and the cost at 4n fell under the helper's 0.05 ms floor, so the padding gate computed 0.05 / 0.05 and reported 1.0000 in 17 of 20 runs. It now calibrates a batch until the window at n is a real measurement, and takes the minimum of each side across samples rather than the minimum of the per-sample ratios — the second is what let a GC-perturbed numerator produce the 10.145 that failed CI. The ceiling stays at 8.

  • #339 f295630 Thanks @ofri-peretz! - schema.json constrains token names, because it was promising something no host honours.

    tokens was described as any name to a #rrggbb colour. roundel's validate() accepts ten semantic names — error, warn, ok, hint, muted, command, flag, value, heading, ground — and throws on everything else. So a plugin author doing exactly what their own E_PLUGIN_SCHEMA error tells them, comparing their object against roundel/schema.json, got a green from the schema and "accent" is not a token from register(). Measured 2026-09-16 with { accent: '[#336699](https://github.com/ofri-peretz/burgee/issues/336699)' }.

    The schema now carries propertyNames.enum, and scripts/plugin-contract-lock.test.ts pins the enum and the runtime set to each other from both sides, so neither can grow a name the other does not know.

    Every host ships a byte-identical copy of this file (plugin-schema-lock.test.ts asserts it), which is why nine packages are listed. Only the key roundel owns is constrained: describing widgets, handlers, sources, resolvers or commands in a file all eight hosts share is what made flagstaff start validating caique's key last time (PluginError: plugin.widgets.later: expected object, got boolean), and those stay in plugin-schema-lock's UNDESCRIBED list with that reason.

    linegauge is in the list for a different change: ceilings.json's R9 block now records the bar as D1's tree-inclusive ceiling — 83,538 against 170,342, a ratio of 0.4904 — and keeps the superseded get-east-asian-width bar beside it with the count of entries that cleared it.

  • #326 88f7ba6 Thanks @ofri-peretz! - The process-reference lock now catches the binding, not only the member read.

    The seams built for PLAN 4.3 showed the hole up. flagstaff/src/runtime.ts reads the world through import process from 'node:process', and roundel/src/runtime.ts through a guarded (globalThis as { process?: … }).process bound to a local — and both files passed the existing pattern untouched. Their being on the allow-list was a statement of intent rather than something the lock enforced.

    Which means any file in any package could have done the same and stayed green: bind the global once, then read proc.env forever, because the member read is now on a local whose name a textual pattern cannot tell from any other. The same hole the globalThis. lookbehind closed in September, reopened through a different door.

    Proven against a real file in linegauge/src — a package with no allow-list entry — in both spellings, each green before the change and caught after. And the new pattern's own first catch was a comment in chalk.ts saying where a cast had moved to, which is the defect this file already carries a paragraph about: a checker that reads printed source and not shape. Comments are stripped before it sees them, and that case is now one of its row-by-row tests.

  • #321 48aec0a Thanks @ofri-peretz! - Every package README now carries a generated ## Where it sits: which key plugins register under, and what is above and below the package in the family (PLAN 5.2).

    Both facts are derived rather than written down a second time. The keys come off each package's own export interface Plugin — its members besides name and contract are the keys — and the edges come from the manifests' own dependency lists. The first version matched a fixed alternation of key names instead and reported flagstaff as hosting none, when it hosts four the alternation had never heard of: the whole argument against a second copy, made by the function that was the second copy.

    scripts/readme-lock.test.ts holds it. The assertion that matters is 5.2's own done-condition — a hand edit fails — and it is proven rather than asserted: the test edits a README and requires the check to notice. Tampering caique's real file with a gadgets key turns it red, which is the check that makes the other five mean something.

  • Updated dependencies [3ea38c3, 3ea38c3, 3ea38c3, c8acb28, fc640dd, 3f92a60, 3ea38c3, fc640dd, 3ea38c3, c8acb28, c8acb28, 88f7ba6, 3f92a60, f295630, c8acb28, 3f92a60]:

    • bellpull@0.1.0
    • closeout@0.2.0
    • linegauge@0.3.0
    • seniority@0.2.0
    • roundel@0.3.1

0.6.1

Patch Changes

  • #280 50cc1a3 Thanks @ofri-peretz! - The B1 agent-cost axis now recognises an empty credential as no credential. A workflow that maps an unset repository secret into the environment leaves the variable present and empty, not absent, so a guard testing against undefined never fired: the axis ran, claude failed to authenticate on all 25 task-runs, and the skip reported that claude "answered but nothing it produced passed a task's own check" — pointing a reader at a prompt-quality problem that did not exist.

  • #278 862e837 Thanks @ofri-peretz! - 🐛 Fix — help screens wrap against the real width: width, strip and wrap come from linegauge

    burgee/yargs' cliui port carried its own stringWidth, stripAnsi and a wrap-ansi implementation. The strip was wrong: the ITU T.416 sub-parameter form ESC[38:2::255:0:0m — what chalk emits for truecolor — left :2::255:0:0m in the string, so a 13-column string measured as 25 and every help screen wrapped against a width that was not the width. linegauge owns measuring and wrapping text and had already fixed it.

0.6.0

Minor Changes

  • #265 0750ebc Thanks @ofri-peretz! - ♻️ Refactor — precedence and config discovery come from seniority rather than a second copy

    burgee carried its own precedence.ts and config.ts; config.ts was byte-identical to seniority's and precedence.ts differed by nineteen lines. Two copies of a precedence order is two answers to "where did this value come from", and --explain is only worth anything if the thing that picked the value is the thing that reports it.

    The public surface is unchanged — resolve, explain, envName, screaming, ConfigError and their types are still exported from burgee, now re-exported from seniority@^0.1.0, which is a new runtime dependency.

0.5.0

Minor Changes

  • #194 8693415 Thanks @ofri-peretz! - burgee consumes roundel instead of carrying a copy of it.

    contrast.ts existed twice — the same WCAG luminance and ratio code in both packages, identical constants and identical maths, differing only in which package name the hex error message says. That is what a rule forbidding the dependency arrow produces: it does not remove the need, it converts it into a copy, which is the one outcome zero-external-deps exists to prevent.

    The family order now runs bottom-up — foundation, output stack, engine — so each layer consumes the layers below it. burgee is last, because a command declares itself and then asks the layers beneath it to render, colour and prompt.

    What a caller installs still comes from one repo: zero external dependencies is unchanged, and is the claim that was ever worth making.

  • #246 14b4cb2 Thanks @ofri-peretz! - --schema publishes relations (S2/S6). validate.ts has enforced exactlyOneOf, conflicts, implies and the rest since the surface shipped, and the schema never said so — an agent could only discover a constraint by violating it. A predicate implies publishes as "(predicate)" rather than the null JSON.stringify would leave.

Patch Changes

0.4.0

Minor Changes

  • #156 799a461 Thanks @ofri-peretz! - --schema is compact by default (agent-headroom R1). The document an agent reads to discover a CLI drops 42% — 39,512 → 22,964 bytes on the large reference demo — for a byte-identical parse. --format=json-pretty restores indentation for a person reading it. Both writers change: the engine's --schema and the commander front-end's.

  • #160 b6fc349 Thanks @ofri-peretz! - Every boolean option accepts --no-<name>. burgee's precedence is flag > env > config file > package.json field > default, and the config and package.json layers set options by name — so any boolean can arrive true without the user typing anything, while a boolean flag carries no value and --x=false is refused. Before this the top layer of that chain could only ever say true, and a boolean turned on in a config file could not be turned off from the command line at all. --x and --no-x together: the later one wins. String options are unaffected, and --no-config still means "load none".

  • #134 7a38fe7 Thanks @ofri-peretz! - burgee/brand gains three options, so a family of marks can come out of one declaration.

    shape replaces the swallowtail with your own silhouette (SVG path data, same 0 0 100 100 box, filled evenodd so a nested subpath cuts a hole) — for a sibling brand whose name is not a flag: a roundel is rings, a parrot is a parrot.

    sheen lays a soft highlight across the field, clipped to the silhouette and drawn under the charge, so the mark keeps the contrast it was measured at. alive() is the same mark with that highlight sweeping across — for a site header, never a favicon — parked still under prefers-reduced-motion.

    bevel is the third dimension a logo can afford: two stroked copies of the silhouette clipped to itself, light offset toward the light source and dark away from it, so the edge lifts and the face stays flat. Sub-pixel at 16px, where it disappears rather than muddies.

    All three are additive: a brand that declares none renders byte-for-byte what it rendered before.

  • #151 8f1043a Thanks @ofri-peretz! - A mistyped option now says which one was meant: error: unknown option --nmae / hint: did you mean --name?, taken from the options the command declared. Exit 2 already told an agent to rewrite the command; this is the half that says how. Previously the native path printed node:util.parseArgs's own message — three lines about -- and positional arguments, which never mentions the option the caller almost typed — while the commander façade had suggestions all along.

    The edit-distance code is loaded on the failure path only, so import { defineCommand } from 'burgee' does not carry it. The core entry point got smaller, because the single-dash hint moved out of it too.

Patch Changes

  • #151 8f1043a Thanks @ofri-peretz! - The repo now dogfoods a twelfth Interlace ESLint plugin, browser-security, scoped to the docs app — 41 more rules at error over the surface the engine never touches and a browser application does.

  • #151 8f1043a Thanks @ofri-peretz! - Marks for the foundation tier — linegauge, seniority, bellpull and closeout — from the same declaration as the other four. No API change: burgee/brand already had shape, and this is four more of them.

  • #162 a447fc4 Thanks @ofri-peretz! - Completions offer --no-<name> for every declared boolean, in all four shells, and fish emits the CLI form rather than the declaration name. fish alone built its own spelling instead of going through flags(), so it had been completing -l dryRun where the flag is --dry-run — a flag the parser refuses — for every camelCase option. The reserved surfaces are excluded: --no-json and --no-help are not accepted by the parser and are not offered.

  • #158 97ca535 Thanks @ofri-peretz! - --version and -V answer on a program that is a pure command group. dispatch had always handled them, but only once a command resolved, so a program whose root runs nothing fell through to unknown command "--version" and exit 2 — which under E1 means rewrite the command. Real commander and real yargs both print the version and exit 0 for the identical program.

  • #170 3e4071c Thanks @ofri-peretz! - burgee/yargs finds its locale table from the package root rather than from the depth of one file. It resolved ../locales relative to itself, which was correct only while that file sat directly in dist/; the moment it moved into a directory the path became dist/locales, y18n returned the key for every string, and 14 of yargs' own 804 tests failed. Walking up to package.json resolves the same from src/, from dist/, and from an installed node_modules/burgee/dist/.

0.3.0

Minor Changes

  • #24 cf951de Thanks @ofri-peretz! - Runtime gains a clock (now, schedule) with a deterministic fake in burgee/testing, and renderHelp accepts { color, theme } — a structural token map the output stack can fill without burgee importing it.

  • #33 8ad4d4a Thanks @ofri-peretz! - Large CLIs (M1–M6): load: () => import('./x.js') on a command loads its handler on dispatch only — help, --schema (which marks it lazy), completions and the MCP tool list are complete without it; sharedOptions(name, specs) declares a set once and each copy is tagged sharedFrom in the schema; a command with deprecated: 'new' warns once on stderr and runs; group and plugin ride on the schema; resolveCommand and runCommand are public. The commander façade gains .deprecate(use?) and projects .helpGroup(). A required positional that argv did not supply is now a usage error naming it — it had never been enforced.

  • #23 0770990 Thanks @ofri-peretz! - burgee dev <entry>: your agent is connected to your CLI while you write it. The entry (exporting program as a burgee manifest, a commander Command or a yargs instance) is served as MCP on stdio; on every save it is re-imported as a fresh module graph, the served manifest is swapped, notifications/tools/list_changed goes out, and the diff plus the rendered help are printed on stderr. Dev-time only and removable: nothing a shipped CLI imports can reach it. startMcp() is the swappable server serveMcp() now wraps.

  • #19 9670224 Thanks @ofri-peretz! - burgee's additions on yargs syntax, guarded so a program that asks for none of them runs exactly as on yargs (804 / 804 still): yargs.manifest projected from what the program registered (builders run on a scratch instance, as yargs' completion does), use(plugin) with preRun/postRun around every handler, .effects() inside a builder, --json as the { ok, data, meta } envelope when the program did not declare it anywhere in its tree, --schema, --mcp and completion <shell> from the manifest, and .burgee({ stdout, stderr, exit }) injecting the streams and reporting E1 exit codes.

  • #16 c7d1aa0 Thanks @ofri-peretz! - burgee/yargs/parser: the ported yargs-parser as its own entry — what import parser from 'yargs-parser' gave, for a program that imported the parser directly. With it, and with the compatibility harness able to require() its vendored root as a package, both façades now pass 100% of their hosts' own suites: burgee/commander 1,361 / 1,361 and burgee/yargs 804 / 804.

0.2.0

Minor Changes

  • #14 95a954f Thanks @ofri-peretz! - burgee/yargs and burgee/yargs/helpers: yargs 18 ported method for method — with its whole dependency tree (yargs-parser 22, cliui 9 with string-width and wrap-ansi, y18n 5 and the 29 locales, escalade, get-caller-file) reimplemented over no dependency — and graded by yargs' own suite: 782 / 804 on the first run, one short of the 783 the real package scores in the same environment. The one test left asserts that Parser is the same object as the yargs-parser npm package, which a dependency-free port cannot be. import yargs from 'burgee/yargs' and import { hideBin, applyExtends, Parser } from 'burgee/yargs/helpers' are the drop-in; examples/conformance proves the demo byte-identical on both.

Patch Changes

  • #13 21d0f4f Thanks @ofri-peretz! - burgee/commander: parse() is synchronous again and parseAsync() starts synchronously, exactly as commander's do. Since the --schema/--mcp surface landed, both went through an async surface check, so a synchronous action ran a microtask after parse() returned and a preAction hook after parseAsync() handed back its promise — commander's own suite asserts on both after every parse. 638 of its 1,331 tests had been failing on main while the Compatibility job reported success, because a | tee pipe hid the grader's exit code; every workflow step that pipes into tee now runs with pipefail.

On this page