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
6b8e3d9Thanks @ofri-peretz! - Help and refusals in a native burgee program now put the whole line to run on the first screen.--helplists the commands that run by their full path. When every command that runs fits in eight rows, the root help listsconfig get <key> Print one configuration valuewhere it listedconfig Read configuration. It uses the same rule as the unknown-command listing. Hidden commands stay hidden, and a larger program keeps its group rows.renderHelptakes the list as a new optionalcommandsoption, and without it renders as before.- A refused word that is really an argument gets a fix.
demo config user.name --format jsonnow printsfix: 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.namefitsget <key>and notset <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 jsonnow printsfix: demo config get user.name --json, notfix: --json. A near miss keeps its value (--nmae=adabecomes--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
./helpis 105 bytes heavier. Both are inside their budgets.
0.22.1
Patch Changes
- #870
54d3fd6Thanks @ofri-peretz! ---versionreports the CLI's own version when it runs through the bin link npm installs. It looked up the owningpackage.jsonfrom the link's directory, sonode_modules/.bin/burgee --versionprinted the installing project's version andnpx burgee --versionprinted "no version declared". The same applied to every CLI built withdefineProgramthat takes its version frompackage.json.
0.22.0
Minor Changes
-
#866
4f01ff0Thanks @ofri-peretz! -burgee migratefixes 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_mockModuleandrequire.resolveis now rewritten like an import, type arguments included. Avi.mock('chalk')left behind mocked a module nothing loaded any more. - The install command is pinned.
nextprints each family package at a caret range of the version that carries the graded drop-in,roundel@^1.0.0rather thanroundel, read from the packages' manifests at build time. A project that setsminimumReleaseAge(pnpm-workspace.yaml) orminimum-release-age(.npmrc) is told so underreleaseAge. - You choose what moves. New
--only <lib,…>and--skip <lib,…>flags take incumbent package names. By default only the incumbentspackage.jsondeclares (in any of its four dependency fields) are rewritten; one imported without being declared is listed underundeclaredand 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
heldand rewritten nowhere, so a program never runs on two copies of it. An incumbent another declared dependency still depends on is listed undertransitive, 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
summaryin--json, sayscomplete: …,partial: N files rewritten, M refusedornothing 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');--jsonis unchanged. - Mocks move with the imports they mock. The first argument of
0.21.1
Patch Changes
-
#857
d80b2d0Thanks @ofri-peretz! - The--jsonenvelope'soknow agrees with the exit code. A command whose result names a non-zeroexitCodeprinted"ok":trueand then exited non-zero, soburgee check <file> --jsonsaidokabout a plugin it refused andburgee migrate --jsonsaidokabout a run with refusals. Both now print"ok":false, with the report still indata:data.refused(code, message, fix) forcheck,data.refused[]formigrate. A result with noexitCode, orexitCode: 0, prints"ok":trueas before. -
#847
6d3eda2Thanks @ofri-peretz! -burgee migratereads dotenv 18.0.6 as the graded release ofseniority/dotenv(181 / 181, control 181 / 181). -
#848
6ae533eThanks @ofri-peretz! - The plugin host refuses a hookfilterthat is not{ command: RegExp }when the plugin is registered. Until now a stringcommandwas accepted, reported byburgee checkas "commands matching deploy", and threw aTypeErroron the first run of any command. A bare RegExp used as the whole filter has nocommand, so its hook fired for every command. A contributed command's refusal now carries the edit to make as itsfix, 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 nocontractis told to addcontract: 1. The README now shows a whole plugin as a default export, thecheck --jsondocument withdata.name,data.commandsanddata.hooks[].stage, the shape of a refusal, and how to build and runcheckfrom a clone. -
#853
86d746fThanks @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 stringcommandand 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
85dfea9Thanks @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 thanconfig. 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.namegetsfix: demo config get user.name. Before, the fix pointed atgreet, 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 jsonor--output=json, is moved into the fix as--json, before any--. An unknown--format jsonor--output jsonon a command now suggests--json.--jsonis also a candidate for near misses such as--jsno. - Help lists each command with its arguments, the way commander does:
get <key>, notget. - 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
./helpis 54 bytes heavier, inside its budget. - An unknown command lists the commands that run, with what each takes. If they fit in the eight rows, a group's refusal lists
-
#848
6ae533eThanks @ofri-peretz! - The familyschema.jsondescribes what five values look like.contractis1, and burgee refuses a plugin that declares none. A caique widget'sstatic(spec)gets{ kind, message, ...sample.done }. A seniorityranksits between the built-in layers at flag 0, environment 10, config file 20,package.json30 and default 40. A burgee hook'sfilteris{ command: RegExp }, now withcommandrequired, so the schema and the host refuse the same filters. -
#857
d80b2d0Thanks @ofri-peretz! - The familyschema.jsoncloses a bellpull resolver'swhenwithadditionalProperties: 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
7694faaThanks @ofri-peretz! -burgee migratereads ink 8.0.0 as the graded release ofcontrolroom/ink(1304 / 1304, control 1303 / 1304) and knows ink 8's export names. A project on ink 6 is now listed underoffMajorand left alone, because ink 6 is graded (540 / 584) but not claimed.
0.20.0
Minor Changes
-
#828
8118d5cThanks @ofri-peretz! -seniority/dotenvnow 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 fromDOTENV_PATH,DOTENV_ENCODING,DOTENV_QUIET,DOTENV_DEBUG,DOTENV_OVERRIDEandDOTENV_FAST, or their olderDOTENV_CONFIG_*names. It reports◇ injected env (n) from <paths>onconsole.errorunless quiet, and its debug lines behind┆onconsole.log.parse(src, { fast: true })is dotenv's character scanner, and the regular-expression parser now expands\nin a value that opens with a double quote even when the quote never closes, as dotenv does.configDotenvis exported. Two entries are new:seniority/dotenv/config(import 'dotenv/config', quiet unless asked) andseniority/dotenv/cli(dotenv run, with signal forwarding)..env.vault,DOTENV_KEYanddecryptare not built, because dotenv 18 removed them (D-20261001-seniority-dotenv-vault).burgee migratenow rewritesdotenvtoseniority/dotenv, anddotenv/configanddotenv/config.jstoseniority/dotenv/config, on dotenv 18. A project on dotenv 17 is left alone and listed underoffMajor: 17's own suite grades the drop-in 107 / 141, because the façade speaks 18.
Patch Changes
- Updated dependencies [
1945d65,1945d65,1945d65,8118d5c]:- bellpull@1.0.0
- closeout@1.0.0
- roundel@1.0.0
- seniority@0.8.0
0.19.0
Minor Changes
- #827
cd34546Thanks @ofri-peretz! -burgee migratereads the new graded releases: boxen 9.0.0 (flagstaff/boxen, level), dotenv 18.0.5 and@inquirer/core12.0.4. A project on boxen 8 or dotenv 17 is now listed underoffMajorand left alone, because those majors are no longer the ones graded.
0.18.0
Minor Changes
- #816
5230016Thanks @ofri-peretz! -burgee migratemoves ink tocontrolroom/inknow that the drop-in is level with ink's own suite (584 / 584), and itsnextcommand installsreact-reconcilerbesidecontrolroom: ink brought the reconciler in itself, and the drop-in takes it as an optional peer.
Patch Changes
- #816
5230016Thanks @ofri-peretz! - controlroom hosts plugins (R10):keymapsandpanesregister throughcontrolroom/plugin'sregister()against the family schema, a screen takes either by name, andcontrolroom check <plugin-file>reports what a plugin contributes. The family schema every host ships gains thekeymapsandpanesdefinitions. - 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
385f2d9Thanks @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 thecontrolroomroot and read 0 untilcontrolroom/inkexists.@inkjs/uiis graded the waycontrolroomwill ask users to run it: unmodified, with'ink'resolved to the target.burgee migratenow reportsinkas a graded drop-in path that is not level yet (controlroom, 0 / 593), and never rewrites it. -
#809
4f45dbfThanks @ofri-peretz! -controlroom/ink: a drop-in forink6.8 —render,renderToString,Box,Text,Static,Transform,Newline,Spacer, every ink hook andmeasureElement— 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.reactandreact-reconcilerare optional peers: installed alone, the subpath refuses on first import withE_PEER_MISSINGandnpm install react react-reconcileras itsfix, 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 gradecontrolroom/ink.burgee migratereportsink→controlroom/inkas a graded drop-in path that is not level yet, and does not rewrite it. -
#803
91ba281Thanks @ofri-peretz! -burgee migratenow 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 newguidedkey 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 ascreen.key(or.render(on anything else is left out. The exit code is unchanged: nothing was refused.
0.17.1
Patch Changes
-
#779
ec04103Thanks @ofri-peretz! -burgee/yargsfollows yargs 18.2.0 and passes all 816 of its tests (real yargs: 814). WithSHELLnaming fish,--get-yargs-completionsanswersvalue<TAB>descriptionand offers choices verbatim, andcompletionprints the fish script (> ~/.config/fish/completions/<app>.fish). The zsh script'szsh_eval_contexttest 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), andburgee migratenames 18.2.0 as the yargs releaseburgee/yargswas graded at. -
Updated dependencies [
d810ac0]:- seniority@0.7.1
0.17.0
Minor Changes
- #782
4eb5f44Thanks @ofri-peretz! - A failure now says what to do next. When aUSAGEorRUNTIMEfailure carries nofix, stderr adds the failing command'susage:line and its options, with required, default, choices, relations and env noted, and the--jsonenvelope carries the same thing aserror.usage: { command, options?, commands?, more? }. The list is at most 8 rows, andmorenames the--helpthat has the rest. An unknown command lists the commands that exist and points at--schema. When exactly one command is near, the error'sfixis 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 oneFor agents:line naming--schema,--jsonand--explain. Exit codes are unchanged. The core entry is unchanged too, at 0 bytes: the new text lives in the lazyfailure.js,surfaces.js,usage.jsandhelp.jschunks.burgee/commander,burgee/yargsandburgee/meoware untouched (D-20260930-failures-teach-recovery).
Patch Changes
- Updated dependencies [
df87199]:- linegauge@1.0.3
0.16.0
Minor Changes
-
#783
08976aeThanks @ofri-peretz! -ctx.interactiveis roundel'sinteractive(), the family's one rule for whether a person may be asked: a terminal on stdin, noCI, and no detected agent, withFORCE_TTY=1over 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
CIit isfalse, 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 istrue, because an answer can still be typed.
burgee loads
roundel/terminalin a chunk of its own (ctx.js, 330 B), only on the path that runs a handler. Help,--version,--schema,--mcpand failures never load it. The root entry shrinks (35,629 → 35,437 B on disk), and so does the bundled initial load ofimport { run } from 'burgee'(24,297 → 24,278 B).ctx.agent,detectAgent,AGENT_PROBESand help's colour, which reads stdout, are unchanged.runBurgeeforwardsttyonto stdin too, sotty: trueis 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, soCURSOR_TRACE_ID, which Cursor sets in every integrated terminal, never will. roundel'sAGENTSdocuments that rule, and a test pins that a person in Cursor is asked. - Under a non-empty
Patch Changes
0.15.0
Minor Changes
-
#778
24300f4Thanks @ofri-peretz! -seniority's cosmiconfig drop-in now reads YAML the way cosmiconfig does..yaml,.ymland extensionless rc files go throughseniority/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 aLoaderError(no YAML parser for <file>) for the rest. It now returns whatjs-yaml5 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 gradesseniorityat 240 / 243, up from 186 / 243, level with the control's 240 / 243.burgee migratenow rewritescosmiconfigtoseniority, because the row is level. A file that imports a namesenioritydoes not export, such as the typeLoaderSync, is refused asunknown-exportand stays on cosmiconfig. On Linux, the global config directory is still resolved from the home directory rather than fromXDG_CONFIG_HOME(D-20260930-seniority-yaml).
Patch Changes
-
#768
d901bcbThanks @ofri-peretz! -burgee migratenow refuses a file that imports@clack/promptsand also imports,import()s orrequire()s@clack/core. Until now the file's prompts moved tocaique/clackwhile itsupdateSettingsfrom@clack/corekept configuring clack, which caique's prompts never read, and nothing said so. The refusal's reason issibling-state, and it carries afix: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/coredoes not refuse, and a file that imports@clack/corewithout@clack/promptsis left alone (D-20260930-migrate-refuses-clack-core). -
#774
b44f426Thanks @ofri-peretz! -paratext/term-imgnow takes a file path, asterm-imgdoes.terminalImage('unicorn.jpg')reads the file withnode:fsand draws it, and a fileURLworks too. The read happens after the terminal check, so a path handed to a terminal that cannot draw it reaches yourfallback(orUnsupportedTerminalError) without the file being opened. A missing file on a supported terminal throwsnode:fs'sENOENT, asterm-imgdoes. TheOptionstype is exported under term-img's name and is generic over whatfallbackreturns, so afallbackthat returns nothing type-checks.paratext/term-imgnow 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 importsnode:fs. The rootimage()still takes bytes, and a lock fails ifnode:fsreaches the root or any other entry.Because the row is level,
burgee migratenow rewritesterm-imgtoparatext/term-img. -
#757
b69d513Thanks @ofri-peretz! - A flag's provenance now names the flag as it was typed:--dry-run,-nor--no-colorinmeta.provenanceand 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
1817b62Thanks @ofri-peretz! -caique/clacknow grades 16 / 16 against@clack/prompts1.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'sno prompt renders a guide when withGuide is globally false, is now excluded by its exact title. It importsupdateSettingsfrom@clack/coreand asserts that the prompts read that package's module state, so it grades@clack/coreand 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 migratenow rewrites@clack/promptstocaique/clack. It refuses a file that importsbox,progressortaskLog, whichcaique/clackdoes not build, and leaves that file on clack. It does not rewrite@clack/core. So a migrated program that importsupdateSettingsfrom@clack/coreis still changing clack's settings, and caique's prompts never read them. ImportupdateSettingsfromcaique/clackinstead. -
#755
ee2b2ceThanks @ofri-peretz! -burgee migrateno longer calls an incumbent removable, or suggestsnpm uninstallfor 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--jsonenvelope. -
Updated dependencies [
e928581,6c2e9c5]:- bellpull@0.5.0
- linegauge@1.0.1
0.14.2
Patch Changes
-
#760
a115799Thanks @ofri-peretz! - chalk's graded release is 6.0.1: compat-oracle's vendored suite is re-vendored atv6.0.1(59 tests, one added), andburgee migratenames 6.0.1 as the chalk releaseroundel/chalkwas graded at.B2 cold start also spawns
picocolors,roundel/tokensandroundel/chalk, and gates roundel's R8 time bar — each colour entry within picocolors + 10 ms — ascold-start-delta-ms, the median of per-round differences. -
Updated dependencies [
a115799]:- roundel@0.6.0
0.14.1
Patch Changes
-
#723
3e837cbThanks @ofri-peretz! -burgee devserves a commander program as commander again. ACommandhas aburgee()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 argvfrom: 'node', dropping the first two words. Four code paths no input could reach are removed from the engine, the MCP server andburgee's own commands, which takes 27 bytes off the core bundle; behaviour is otherwise unchanged. -
#736
1f9ad70Thanks @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
063025eThanks @ofri-peretz! - Three code paths no input could reach are removed fromburgee/meowandburgee migrate; behaviour is unchanged.burgee/meowno longer reads a deprecatedaliasas a spelling of its flag, because meow refuses a flag that declares one before anything is parsed.burgee migrateno 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
8b6b1e0Thanks @ofri-peretz! ---schemaover its budget lists each command by its declaredsummaryagain.a ?? b === undefinedbinds asa ?? (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--outputFormatand 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
e4c1f69Thanks @ofri-peretz! -caique/clacknow carries@clack/prompts' prompts, not onlylimitOptions:text,password,confirm,multiline,date,path,select,selectKey,multiselect,groupMultiselect,autocompleteandautocompleteMultiselect, withintro,outro,cancel,note,log,stream,spinner,tasks,group, theS_*glyphs,settings/updateSettingsandisCancel, 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 importsupdateSettingsfrom@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,progressandtaskLogare not built.burgee migratereports the new grade and, since it is not level with the control, still does not rewrite@clack/prompts. -
#677
8bc14e6Thanks @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 withsplit('\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'slineCountagainst the terminal's width, whichWriternow carries as an optionalcolumns(read through to the output stream bycreateIo). A writer without one is treated as never wrapping, as before.burgee/testing:stripAnsiis linegauge'sstrip. 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.burgeehelp: descriptions and epilogues are folded bylinegauge/wraprather 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.burgeeconfig 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
12fea8eThanks @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: withunknown-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 anda.bfrom a default or a second config threw "Cannot read properties of null". Anullparent 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 tofail, where yargs sends a command handler's rejection, so a.fail()handler receives it.burgee/meow:importMeta: nullthrows meow's own "TheimportMetaoption is required" TypeError instead of a null dereference, andinput: nullor 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 oftoString().
-
#673
5b3dafaThanks @ofri-peretz! -burgee/meownow 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). WithallowUnknownFlags: false, unknown flags are reported from the tokens as typed, so a declarednoAutoHelpno longer makes--no-auto-help"unknown", and a subcommand's own flags are left to the subcommand.--helpand--versionanswer 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: nullandbooleanDefault: nullare refused as meow refuses them.-Fkeeps its case,--flag ''satisfies a required flag,input.isRequiredreceives the input alone, andcli.pkgis normalized lazily in the object you passed.burgee migratenow rewritesmeowtoburgee/meow.Its types come along too:
burgee/meowexports meow'sFlag,AnyFlags,TypedFlags,FlagType,InputOption,InputOptionTypeandIsRequiredPredicate, with the genericOptions<Flags>andResult<Flags>, socli.flagskeeps its types after the rewrite. -
#674
e9f45d8Thanks @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
088ceccThanks @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, noCI, and no agent variable (CLAUDECODE,AI_AGENT,CURSOR_AGENT,CODEX_THREAD_ID,GEMINI_CLI, exported asAGENTS), withFORCE_TTY=1as the override — andunicode(rt), is-unicode-supported 2.1.0's table over{ env, platform }.caique/decidenow depends onroundeland asksinteractive(). Behaviour change: under an agent that has a terminal —CLAUDECODE=1and 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=1now prompts even without a terminal on stdin, as it does for burgee.caique/inquirer's tick andflagstaff/ora's log symbols and spinner fallback use roundel'sunicode(). 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✔, asfiguresdraws them.burgeehelp colour is roundel'scolorLevel(rt) > 0. Behaviour changes:NO_COLORnow beatsFORCE_COLOR;--no-colorand--color=…on the command line are honoured;CLI_ACCESSIBLEturns help colour off; and a terminal that sets noTERM(Windows' conhost) gets plain help unlessFORCE_COLOR,--colororCOLORTERMasks for colour.burgee/contrastrounds with roundel'sround2; no output changes.paratext: the supports-color fork behindparatext/terminal-linkis unchanged, and now held to roundel's policy by a parity test everywhere their two incumbents agree.
-
#683
5052d67Thanks @ofri-peretz! -burgee/meowfinds the caller'spackage.jsonwithseniority/find-upinstead of a directory walk of its own. The answer is unchanged — the nearestpackage.jsonabove the module that parses — and the walk is now bounded and ends on a symlink cycle. -
#655
4319563Thanks @ofri-peretz! - linegauge:linegauge/sliceis 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.slicerounds 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), andtruncate('あいう', 4)returnedあい…(five columns, over its budget). Both now stay inside the columns asked for.slicereads the escapes slice-ansi 9 reads:OSC 8links ended byESC \orU+009C, the C1OSCintroducer,DCS,SOS,PMandAPCstrings, a loneST, and truncated or malformedCSI. A malformedCSIends 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,
slicegives 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.widthis 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
widthmeasured as one column where string-width measures two.
burgee:
burgee migrateserves 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
b74a24dThanks @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.
extendsin 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 handObject.prototypeto theextendsloader, which deletesextendsfrom the object it is given.- zsh completions escape
\as well as:. zsh's_describestrips one level of backslashes, so a command, option or choice containing one completed without it.
-
#681
deffcc6Thanks @ofri-peretz! -burgee/yargs: resolving a config'sextendsno longer deletesextendsfrom 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
478cb96Thanks @ofri-peretz! -schemaanswers as an alias of--schema(D-151, GAPS B22).mytool schema,mytool schema deployandmytool schema deploy --field options.regionprint exactly what the flag forms print, because<tool> schemais 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 declaredschemacommand runs as written, and a root command that takes arguments receivesschemaas one. The root--helpnow lists--schema the program as dataamong its global options. Loaded through the existing lazysurfaces.js; the core entry grows 14 bundled bytes.
0.13.2
Patch Changes
-
#627
4a629a9Thanks @ofri-peretz! - Lint with every published Interlace ESLint plugin, and fix what the upgrade surfaced.- caique: the inquirer theme merge skips
__proto__,constructorandprototypekeys, 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-requirerule that now reports the config loader's dynamicrequire.
No public API or output changes.
- caique: the inquirer theme merge skips
-
Updated dependencies [
4a629a9]:- closeout@0.5.4
- roundel@0.5.4
- seniority@0.6.3
0.13.1
Patch Changes
- #604
0e7b1e8Thanks @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
a0c691aThanks @ofri-peretz! ---format=agentprints a command's result the way an agent wants to read it: one compact logfmt line per record —key=valuepairs, nested keys dotted, lists of scalars comma-joined, a value quoted only when it holds a space,=,"or,— with no envelope, nometa, 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--jsonon the same result: 31% over a representative set, from 20% on a twelve-row list to 96% on a bare count.--jsonwins when both are typed, so the envelope and D-140's failure contract are unchanged; a failure without--jsonis still prose on stderr; the exit code is the one the result names. A command that declares its ownformatoption keeps the flag. The formatter loads only when the flag is typed. -
#582
120ba7bThanks @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,--jsonkeeps its envelope, and a commander program that calledexitOverride()keeps its exits.--schemafrom a façade program now names the reserved surfaces the program shadows, asshadows(J4), andburgee/program-schema.jsondescribes it — and the optionnegatableflag the commander façade already printed, which the file had left out.
Patch Changes
-
#589
d7d9e75Thanks @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
--jsonfailure 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.jsonfield loads only for a program that turned on config discovery.
Output and exit codes are unchanged.
- What a failed run prints (the exit-code classification, the
-
#594
60c4603Thanks @ofri-peretz! -burgee migratedecides 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 whatmigraterewrites 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
c938fbfThanks @ofri-peretz! ---mcpholds 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
3a7131fThanks @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--jsonfor machine-readable output. A program that defines its ownconfig explainkeeps it. -
#472
c992bf2Thanks @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 anyhintandfixthe same wayUsageErrordoes, witherror.codein the JSON envelope. Subclasses inherit the code. Defining a second class with the same code throws when the module loads. -
#478
4e5054cThanks @ofri-peretz! - Options can compute their shell completions at TAB time. Give an optioncomplete: (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 fromchoices, 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
3e56073Thanks @ofri-peretz! ---json=<a,b>selects the fields of a command's result, and--json=lists the fields a command declares (fields: [...]ondefineCommand) without running it. An unknown field is refused with the valid set in the hint. Only the=form takes fields, socmd --json namekeepsnamea positional. Declared fields appear in--schema. -
#484
635eb8cThanks @ofri-peretz! - A handler can throw an error, or a plain{ code, message, fix }object, whosecodenames 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 asENOENT, still exits 1. -
#474
1955419Thanks @ofri-peretz! - burgee plugins can hook two more stages.parseruns before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin mapsdtodeploy.shutdownruns once as the program exits, whether the command succeeded or failed. The familyschema.jsonshipped in every package now describes both stages. -
#483
72a8103Thanks @ofri-peretz! -burgee/program-schema.jsonis published: a JSON Schema for the document--schemaprints, so anyone reading a program's--schemacan validate it.--schemaalso now includesexitCodes, the contract's seven exit codes by name, so a caller can branch on a code without reading prose. -
#475
8a7f387Thanks @ofri-peretz! - A positional argument declared withtype: 'file'treats-as standard input: the handler receives the stream asctx.stdin, while the positional still reads-. Giving-to two file arguments is a usage error, because standard input can only be read once. -
#517
0d65c75Thanks @ofri-peretz! -burgee migratenow 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-exitand eleven more. Drop-ins not yet level (dotenv, cosmiconfig, clack, meow, …) are reported underpartialwith their grade and left alone. The report names the family packages to add and printsnext, the install-and-uninstall command for the package manager your lockfile names.
Patch Changes
-
#490
8a02d68Thanks @ofri-peretz! -bellpull/node-whichis a drop-in replacement for node-which 7:which(cmd, opts)returns a promise andwhich.syncruns synchronously, with node-which'sall,nothrow,path,pathExtanddelimiteroptions and itsENOENTerror. It passes node-which's own test suite, 5 of 5.bellpull/whichis unchanged: it stays bellpull's own resolution API and never reads the process.require('bellpull/node-which')returns the function with.syncon it, asrequire('which')does, andburgee migratenow rewriteswhichto it. -
#526
91f46eeThanks @ofri-peretz! ---mcpkeeps stdout for JSON-RPC frames only. A tool call whose handler printed —console.login aburgee/commanderaction, the common case, or a directprocess.stdout.write— wrote onto the stream the frames go out on, so the client readhellobetween two replies as a malformed message and the tool result itself saiddata: null. During a call, stdout and the stdout-printingconsolemethods 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 anisErrorresult instead of ending the server. -
#522
f4be6a8Thanks @ofri-peretz! -require('bellpull/cross-spawn'),require('flagstaff/cli-table3'),require('burgee/yargs'),require('seniority/dotenv')andrequire('seniority/rc')now return what the incumbent'srequire()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
31efa5bThanks @ofri-peretz! - burgee's own commands move toprogram.js;cli.jsstays the bin and still re-exports them. Importing the definitions no longer runs the CLI. -
#519
77ff1cbThanks @ofri-peretz! - The drop-ins now export their incumbents' type names, so a TypeScript program migrates by its import alone:roundel/chalkgains chalk'sColor,ForegroundColor,BackgroundColor,ModifiersandOptions;flagstaff/oragainsSpinner,PrefixTextGeneratorandSuffixTextGenerator;flagstaff/boxengainsOptions,CustomBorderStyleandBoxes;flagstaff/log-update,linegauge,linegauge/wrapandcloseout/exit-hookgainOptions;burgee/yargs/parsergainsArguments,OptionsandConfiguration. Types only — no runtime bytes. -
#526
91f46eeThanks @ofri-peretz! - A handler that fails under--jsonis now one{ "ok": false, "error": … }envelope on stdout and the exit code its error names, on every front end. Aburgee/commanderaction that threw under a plainparseAsync(process.argv)escaped as a stack trace; the native engine wrote the failure envelope to stderr with stdout empty;burgee/yargslet a synchronous throw or a thrown non-Error escapeparseAsync(), 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 anAuthErrorwhere E6 promises 5. Without--jsona 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) anAuthErrornow exits 5 there too. -
#549
d5a1b02Thanks @ofri-peretz! -seniority/lilconfiggrades 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
ef512eaThanks @ofri-peretz! -burgee migratenow rewritesrequire()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 ownrequire()already returns a namespace, as their replacements' does, so the rewrite is exact; before, it was refused asrequire-of-default.require('burgee/yargs/parser')now returns the parser function, asrequire('yargs-parser')does. -
#543
dc1b1a7Thanks @ofri-peretz! - paratext implementsansi-escapes' CSI half —cursorTo,cursorMove,eraseLines,clearTerminal,enterAlternativeScreen,synchronizedOutputand the rest, byte-exact with ansi-escapes 7.3.0 — soimport ansiEscapes from 'paratext'is a full drop-in, graded 4 / 4 by ansi-escapes' own suite (it was 1 / 4 with CSI declaredundefined).burgee migratenow rewritesansi-escapestoparatext. -
#546
0592441Thanks @ofri-peretz! -paratext/terminal-linknow links exactly wheresupports-hyperlinks4.5.0 does. It honoursFORCE_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, soburgee migraterewritesterminal-linkimports toparatext/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
1aae1e2Thanks @ofri-peretz! - The installedburgeecommand runs.dist/cli.jsshipped without#!/usr/bin/env node, sonpx burgee …and the linked bin were handed to/bin/shon macOS and Linux and failed withimport: command not found.check:artifactsnow refuses any published bin that is missing from the pack list or does not start with the shebang. -
#508
1aae1e2Thanks @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 underburgee/commander, where commander negates only what the program declared — so--no-skip-blank,--color(for a lone--no-color) and--no-versionwere each a TAB away and eachunknown option. Completions now readOptionSpec.negatable, which the commander façade sets from the program's own declarations. -
#508
1aae1e2Thanks @ofri-peretz! - The README's MCP answer says what--mcpdoes: every runnable command declares itseffects,withheldis what keeps one out of the tool list, and aburgee/commanderorburgee/yargscommand that declared nothing is listed aseffects: 'undeclared'rather than left out. -
#508
1aae1e2Thanks @ofri-peretz! ---mcptool calls reach the program with multi-word options.tools/callrebuilt argv as--<property>, soskipBlankwent out as--skipBlankand both the engine andburgee/commanderrefused it; it now sends the flag the schema advertises (--skip-blank), and afalsefor a boolean that defaults on goes as--no-<name>. Underburgee/commander, a lone--no-coloris advertised asnoColor(flag--no-color) instead of a--colorcommander never accepts.run(defineCommand(…))now keeps the command'seffects,examples,argumentsandrelations, so a single-command program's tool carries the hints it declared and is named after the program rather than"". An unknown-optionfixis spelled as the flag is typed (--dry-run, never--dryRun).OptionSpecgainsnegatable?: boolean—falserefuses--no-<name>. -
#508
1aae1e2Thanks @ofri-peretz! - The README's "Start here" runs. The quickstart omittedeffects, whichdefineCommandrequires of every runnable command, so the first thing a new user pasted threwcommand "greet" is runnable and declares no effects; its--jsonline also left out themetathe 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
b8e97dcThanks @ofri-peretz! - Runs on Node 20 and 22, not just 24+:engines.nodeis now^20.19.0 || >=22.13.0. Those are the first releases whererequire(esm)loads without a warning, so the CommonJSrequire()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 callPromise.withResolvers, which Node 20 doesn't have.
Patch Changes
-
#505
9800b43Thanks @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. Thezero-dependencykeyword is nowno-external-dependencies. -
#506
891e132Thanks @ofri-peretz! -burgee/yargsexports yargs' types —Argv,Arguments,ArgumentsCamelCase,CommandModule,CommandBuilder,Options,PositionalOptions,InferredOptionTypes,MiddlewareFunctionand the rest of@types/yargs' ESM surface — and its default export is typed as a factory returningArgv, so a typed chain infersargvand an instance passes wherever a program saysArgv.burgee/commanderaddsOptionValues,OptionValueSource,HelpConfigurationandParseOptionsResult,opts<T>()/optsWithGlobals<T>()are generic as in commander, and itsOutputConfigurationtakes any subset.burgee migratenow 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 underkept, and any other is refused asunknown-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
23e8b35Thanks @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
0.9.2
Patch Changes
- #465
acf98f3Thanks @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
b4584e7Thanks @ofri-peretz! - Every package's npmhomepagenow 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:burgeegainscli-framework,argument-parser,subcommands,json-schema,mcp-server,model-context-protocol,ai-agent,llm,shell-completion,typescript,zero-dependency,commander-alternativeandyargs-alternative; the other eight gainagent,ai-agent,non-tty,jsonandzero-dependencywhere the package does that —zero-dependencyonly 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
bdaf364Thanks @ofri-peretz! - Every package now listsplugin,pluginsandextensiblein its npm keywords, because every package takes plugins through one shared contract.A plugin is a plain object, validated against the
schema.jsonthat ships in every package, and checked with the package's owncheckcommand. 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'scheckaccepts in CI. -
#435
7888524Thanks @ofri-peretz! -schema.jsonnow 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'sglyphs,spinners,bordersandcomponents, paratext'scapabilities, and linegauge'swidths. 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 onpaths - caique
widgets - closeout
handlers, including the phases a plugin may use - seniority
sources, including the rank bounds - burgee
commands,hooksandenforce
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.
- bellpull
-
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
db3c59eThanks @ofri-peretz! -ExitCode.AUTH— a refused credential gets its own code (E6).RUNTIMEis 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 } }AUTHsays get a credential and run it again, which is a different action fromUSAGE's fix the script andCONFIG'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
ghuses 4. Four isCANCELLEDhere 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.awsv2 uses 252/253/254 and agrees with nobody either — what matters is that the code is stable and documented.fixis the line a caller runs wherehintis the prose a person reads, and both reach the--jsonenvelope, so an agent never has to parse the message.374 bytes on the core entry.
-
#412
4b64f6aThanks @ofri-peretz! -burgee/meow— meow's surface, over burgee.import meow from 'burgee/meow'takes the place ofimport meow from 'meow': the options object, the flags contract (type,default,shortFlag,aliases,choices,isRequired,isMultiple),commands, the help and version blocks withautoHelpandautoVersion,showHelp/showVersion, and theinput/flags/unnormalizedFlags/pkgresult. 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 forburgee/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 onyargs-parserrather than carrying it, and a caller installing meow installs both. -
#428
f0370b4Thanks @ofri-peretz! - A deprecation now has to name its replacement.defineCommand({ name: 'push', deprecated: 'publish', /* … */ }); options: { legacy: { type: 'boolean', deprecated: '--force' } }deprecated: trueused to produce(deprecated)in help andwarning: 'push' is deprecatedon stderr, which tells the reader to stop without saying where to go.defineCommand, andManifest.use()for plugin commands, now refusetrueand''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,deprecatedin--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
fb92e13Thanks @ofri-peretz! - Help is coloured on a terminal, andNO_COLORandFORCE_COLORmean 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 leftFORCE_COLORwith nothing to override. Now one decision covers every help path:FORCE_COLORdecides whenever it is set:0orfalseturns colour off, anything else (the empty string included) turns it on, even through a pipe and overNO_COLOR. This matches Node's owngetColorDepth.- Otherwise help is coloured only on an interactive terminal with no non-empty
NO_COLORand aTERMother thandumb. 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=1gives back the plain render exactly. -
#415
47b541aThanks @ofri-peretz! - The root barrel stops holding five modules open, and the initial load a consumer pays halves.import { run } from 'burgee'was loadinghelp.ts,mcp.ts,schema.ts,plugin.ts,manifest.tsandseniority/precedencewhether or not a program read any of them, becauseindex.tsre-exported their value half as a convenience.execute.tsalready loaded each one behind anawait import(); the barrel was the only thing keeping them on the startup path.Measured over the transitive closure of
importstatements — the bytes a bundler actually puts on a consumer's startup path:before after burgeeinitial load, bundled57,880 B 28,637 B burgeestatic graph, on disk60,823 B 42,223 B cold start, burgee ÷ cac2.567× 1.737× modules Node loads for the import 22 17 Breaking, and narrowly. Every moved value has a subpath of its own:
was is now renderHelpburgee/helpserveMcp,toolsOf,annotationsOf,MCP_PROTOCOL_VERSIONburgee/mcpschemaOf,commandSchemaOf,inputSchemaOf,summaryOf,Manifestburgee/schemadefinePlugin,CONTRACT,PluginErrorburgee/pluginresolve,explain,envName,screaming,ConfigErrorburgee/configEvery
typestayed where it was. A type re-export is erased and costs a consumer nothing, so the whole type surface —Manifestincluded, which is what keepsdefineProgram's return type nameable — still imports fromburgee. A typed program that never called one of the moved functions needs no change at all.--explainand--schemaalso load on their own branch now rather than at import:schema.tsis 2,640 bundled bytes andseniority'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
db3c59eThanks @ofri-peretz! - Every plugin host has acheckcommand.npx linegauge check ./my-widths.mjs npx burgee check ./my-plugin.mjs --jsonPRINCIPLES 7 asks three things of an extension surface: the plugin is data validated against one published schema, there is a
checkcommand 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 inflagstaffalone. 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
encodeand itsfallback, 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, soburgee check --jsonis 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
okas the last line; - a refusal with a code from the family's vocabulary and a
fix, exit 1; E_NO_CONTRIBUTIONfor 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.
- a readable report, contribution by contribution, with
Patch Changes
-
#421
db3c59eThanks @ofri-peretz! -runBurgeeforwardscwd,stdinand 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 toexecute. The other three were computed and dropped, which.sdlc/intents/burgee/spec.mdrecords as T1 and calls "the row most likely to make a test pass for the wrong reason". It is, and precisely:tty: truechanged nothing.executereads TTY-ness offopts.stdout.isTTYand hands it todetectAgent; the harness passed a bare{ write }. A test asking for a terminal got the non-interactive floor and asserted on it happily.cwdchanged nothing. Config discovery starts atio.cwd, which fell through tohost.cwd()— the real process directory. A test pointing at a fixture tree was reading the repository it was running in.stdinchanged nothing, so nothing that reads it could be driven through the harness at all.
testing-harness-forward.test.tsis the check and both cases were proved to fail on the six-field version:interactiveread[false, false]fortty: true/false, and a<name>.config.jsonunder the givencwdnever reached the handler.The cost is 92 bytes on
burgee/testing, which is test-time only. -
#415
47b541aThanks @ofri-peretz! - The MCP server and the help renderer load on the branch that uses them.--mcpserves a protocol until stdin closes and--helplays out prose with measured columns; a program that does neither should carry neither. Both are now reached throughawait import(), so a bundler with code splitting leaves them off the startup path.Measured as an entry chunk:
burgee58,056 → 19,540 bytes, andburgee/commander69,431 → 51,306. No API changed —renderHelpandserveMcpare still exported from the root and still do the same thing.commander's--schemadeliberately 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
db3c59eThanks @ofri-peretz! -onErrorfires 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:
preRunopens, and exactly one ofpostRunoronErrorcloses. A plugin that starts a span, opens a file, takes a lock or writes an audit line inpreRunhas nowhere to finish it otherwise — and "otherwise" is every command that throws.The engine held that. Both façades ran
preRun → handler → postRunas a.thenchain, so a handler that threw skippedpostRunand never reachedonError: a plugin got an opening hook and no closing one at all. AndonErrorwas 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.tsholds 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 onburgee/commander. -
#418
c0fa8a3Thanks @ofri-peretz! -explainmoves fromseniority/precedencetoseniority/explain.A re-export is not free across a package boundary.
explainwas exported fromprecedence.ts, so every program that resolved a configuration loadedexplain.jswhether 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. Onlyseniority/precedencestops re-exporting it.burgee/configre-exports it the same way it always did, andburgee's engine loads it behind anawait import('seniority/explain')on the--explainbranch, 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
c0fa8a3Thanks @ofri-peretz! -burgee/yargsloads the MCP server on the--mcpbranch, not at import.yargs/factory.tsimportedserveMcpat the top of the file while the note above#surfacessaid "Completions and--mcpload 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 everyburgee/yargsprogram.burgee/yargs, bundled 107,665 B -> 105,240 B burgee/yargs ÷ yargs 0.969 -> 0.947The same shape as the root-barrel split, missed here because a comment said it had already been done. The weight lock's
./yargsbudget 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
0a37307Thanks @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.jsonand the specifiers source actually imports — rewritescommander,yargs,yargs/yargsandyargs/helpersacross 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 editspackage.json(D-054), and never touches an API call site (D-053). The compat figures are read fromcompat-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 againstcommander, once againstburgee/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 anexitCode, andemithonours it. Before this there was exactly one success path and it left withOK, 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.migrateitself is 10,878 bytes loaded through a dynamic import and is denied to the root entry by name, soimport 'burgee'never reaches it. -
#384
7f32875Thanks @ofri-peretz! - A program written in commander's or yargs' own syntax now gets burgee's agent surfaces.--mcplists every command instead of none: a façade command arrives with noeffectsbecause neither incumbent has such a concept, and the filter used to read undeclared as not a tool. It is now listed witheffects: "undeclared"— absent from the list is strictly worse for a caller than present with an honest annotation.withheldstill means absent.tools/callused to return nothing at all — the reply writer was read out of_outputConfigurationat reply time, and the first tool call replaces that so the run can be captured. A client waited forever. It now answers the--jsonenvelope, call after call.--jsonworks at the root of a command group and no longer swallows the operand after it, and a failure under--jsonis the envelope on stdout withfix:rather than prose on stderr.commander stays 1360 / 1360 and yargs 804 / 804 against their own suites.
Patch Changes
-
#385
c219e65Thanks @ofri-peretz! - A kebab-case option key is now refused where it is declared, instead of silently never reaching the handler.toParseConfigkebabs each declared name to build the flag andcanonicalcamels every parsed key back, so the flag layer handed toresolveLayersis keyed camelCase while the specs beside it are keyed as declared. Declare'dry-run'and the two never meet:--dry-runparses, resolves to nothing, and the handler is givenundefined. No error anywhere.Three of burgee's own commands were live instances.
burgee brand --allow-low-contrastnever suppressed the WCAG failure it names,--bordure-widthwas always1.5whatever was passed, andburgee dev --no-watchstill watched. All three are fixed by spelling the keycamelCase; the flags are unchanged, and--bordure-width 7now reaches the SVG asstroke-width="14"against the default's3,--allow-low-contrastemits, and--no-watchlogs no reload when the entry is edited.Breaking for a declaration, not for a command line.
checkDefinitionrefuses a key that does not survivecamel(kebab(key))—'dry-run', and alsoURL, whose canonical form isurl. 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 bykebab()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.
./pluginhad 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 incheckCommandinto the passcheckDefinitionwas 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 —flagis computed once andkebabis the identity on every reserved name, and a numeric bound is a fact about a spec rather than about a name.definition.jsis 51 bytes smaller than before the check existed.checkDefinitionnow carries the reserved names;checkCommandis 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
a1f1d40Thanks @ofri-peretz! - Stage 2's artifact is nowspec.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 atspec.mdrather thandesign.md.No behaviour changes. The published tarballs do move, by two bytes per surviving reference —
design.mdis nine characters andspec.mdis 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
3ea38c3Thanks @ofri-peretz! - E5 and O5, which the design has markedRsince it was written and which nothing implemented.exit-code.tshas declaredSIGINT: 130with the comment "SIGINT after the terminal was restored (E5)" from the first day of the contract, andexit-code-lock.test.tsgrades 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/srcreturned 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, againsthost.exit(code), which isprocess.exitand 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.labelis 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 ownexit— 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 wasprocess.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.tsis 1,001 of those and the engine's routing is the other 389.npm i burgeegains closeout's 81,360 bytes unpacked, and closeout depends on nothing. -
#361
a0cb8caThanks @ofri-peretz! - An error carriesfix— the exact flag to run next — besidehint.E3 asks for "
code,message,hint, and where possiblefix: the exact command or flag to run next". The envelope was{code, message, hint}, andhintis prose.The distinction matters most for the caller this package exists for. An agent can execute a
fix. Ahintit 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 carriedfix; 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"}}fixis omitted, never guessed, when there is no near match: an executed guess burns the turn the field exists to save. -
#361
a0cb8caThanks @ofri-peretz! ---help --jsonprints 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--jsonsurface exists to avoid, on the flag people type first. burgee's design recorded it as F2,Not built: "no JSON help surface;--help --jsonprints the same prose as--help."The document is
commandSchemaOfscoped to the node you asked about — the same shape--schemapublishes, so a reader learns it once — plusschemaVersion, 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
--helpis unchanged. -
#337
e974114Thanks @ofri-peretz! - Options can declaredependsOnandexclusive, and the declaration is enforced rather than documented.They are an alias, not a second engine:
exclusivedesugars toconflictsanddependsOntoimplies, into theRelationunion that already existed. Command-levelrelationsare 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/impliesboth 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, texterror: --out requires --forcewithhint: pass --force, and--jsongives 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:
--schemacarries both spellings, help renders(requires --force)and(conflicts with --table)— it rendered no constraint of any kind before this — and the Fig spec emitsdependsOn/exclusiveOn, Fig's own two keys. -
#334
0f00f72Thanks @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)wasreturn plugin;, andManifest.use()pushed the plugin and calledthis.add()directly — wheredefineCommandenforces the reserved names of V5 and runscheckDefinition. So a plugin's command was admitted unread, andtoParseConfigseedsjson: { type: 'boolean' }and then writes every declared option over the top of it. A plugin option namedjsontherefore did not clash with the envelope flag; it replaced it, on a framework whose entire agent-facing contract is that--jsonis machine-readable output. Four more were accepted the same way:enforce: 'mid'(NaNin the hook comparator), a plugin with noname(commands carriedplugin: undefined, so M3 attribution was silently lost), a hook with nohandler(aTypeErrorone run later, classifiedRUNTIME), and a contributed path that was already declared — whichfind()andresolve()answer differently.What
contractmeans, and how an old plugin fails.packages/burgee/src/plugin.tsis now a plugin host in the sense the rest of the family means: it ownsPlugin,validate(),PluginErrorand aPluginErrorCodeofE_PLUGIN_SCHEMA | E_PLUGIN_CONTRACT, both already in the vocabulary home's union.contractis the revision of the family plugin object a plugin was written against, and this burgee knows1. A plugin that declares none is refused withE_PLUGIN_CONTRACTnaming the version:plugin "acme" declares no contract; burgee 0.6.1 and earlier validated none of it — rebuild it against this burgee (
definePluginstampscontract: 1), or add that key by handThat refusal is the point rather than a side effect. An object with no
contractwas authored against a host that checked nothing, so the honest reading of its silence is unknown, not fine — and it may be carrying exactly thejsonoption above. A silent behaviour change on a published extension point is worse than a loud breaking one.definePluginnow 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.tsto a newdefinition.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. Importingvalidate.jswhole forcheckDefinitionput 6,409 bytes of run-time coercion into both front-ends and tookburgee/commanderover the 128,000-byte budget that exists to prove it is no heavier than commander's ownlib/; the split keeps it at 125,667. And burgee shipssrc/schema.json, byte-identical to the family's, exported asburgee/schema.json.Measured:
burgee/commander1,360 / 1,360 andburgee/yargs804 / 804 against the incumbents' own suites, unchanged. Core costs 4,191 bytes on disk (52,959 → 57,150), priced per entry inweight.test.ts. -
#361
a0cb8caThanks @ofri-peretz! - burgee's extension surface:./pluginis published, andeffectsis 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, soscripts/plugin-contract-lock.test.tshad to reach it by relative path and recorded the gap as a declared one. It is the shapeplugin-schema-lockcaught in flagstaff — a host whose own refusal names something the author cannot reach — and burgee's version was the quieter kind, because thefixnamed no specifier at all: it said rebuild it against this burgee,definePluginstamps the contract, and left the author to work out wheredefinePluginlives. The convention the family teaches isburgee/plugin, and that threwERR_PACKAGE_PATH_NOT_EXPORTED. Both halves are fixed: the subpath resolves, and the message says its name.burgee/pluginexportsCONTRACT,definePlugin,validate,PluginError,PluginandPluginErrorCode; the root barrel keeps the four it always carried, because one module behind two doors is whatburgee/yargsandburgee/yargs/helpersalready are, andManifest.use(plugin: Plugin)is a root export.validate()and thePlugininterface are the half only the subpath carries — the host's vocabulary rather than a program author's.effectsis 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: addeffects: 'withheld'to every runnable command that declared none, then replace it withread_only,idempotentornon_idempotenton each command an agent should be able to call.It is breaking on purpose, because the old behaviour was silent.
toolsOfserves only a command that declared itseffects, 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 asundefined, so the second shipped as the first — you released, and the tool you built for an agent simply was not intools/list, with a shorter list than you expected as the only evidence.So
effectshas 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 isread_only, the one value it could be confused with. Not a second boolean field either: beside a now-requiredeffectsthat 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.
--schemapublisheseffects: "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/listomits it. The Fig spec and the shell completions carry it exactly as before — nothing incompletions.tsreadseffectsand 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/commanderorburgee/yargsreaches 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. Andeffectsstays optional on the TypeScript type, because a field whose presence depends on a sibling's would mean splittingCommandinto 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 devis the first command in this repository to declare'withheld', and not as a formality:devis 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 branddeclaresnon_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 (+961over both changes, of which+938is the refusal),burgee/testing62,992 → 63,953,burgee/cli78,071 → 79,088,burgee/commander126,989 → 127,876 inside its unchanged 128,000, andburgee/yargs230,869 → 231,756 inside its unchanged 256,000. The newburgee/pluginentry is 7,095 and costs a program nothing:manifest.jsimportsvalidateas a value becauseuse()is synchronous, so every entry that reaches the manifest already carried those bytes. -
#360
61c51f9Thanks @ofri-peretz! -burgee/commanderspawnsexecutableSubcommandthroughbellpull, and burgee no longer importsnode:child_processanywhere.The gap was declared, dated, and carried its own release condition.
inline-implementation-lockread: "commander'sexecutableSubcommandspawns 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, becausespawndoes not searchPATHEXTand, since the fix for CVE-2024-27980, Node refuses a.cmdwithoutshell: 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.bellpullresolves the executable, builds thecmd.exe /d /s /cline itself and escapes every argument, so nothing reaches a shell as text.ChildProcesscomes frombellpull/cross-spawntoo, which re-exports it precisely so a consumer does not have to namenode:child_processfor a type.commander stays 1360 / 1360, ▲ 0, measured after the wiring — which is the whole question, since
spawnis mocked in roughly 23 of those cases and the earlier attempt at this took the row to ungradeable.bellpull/cross-spawnreadsspawnoff 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
3ea38c3Thanks @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.0ships four files —LICENSE,README.md,package.jsonandindex.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.tsrecorded 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 ownindex.d.ts, recorded with the version and the file's SHA-256, the waycompat-oraclevendors an incumbent's suite. No dependency is added: the table is forty strings, and the package is not installed, not indevDependenciesand not in the lockfile. It reproduces Fig's own asymmetry —SubcommandandOptionextendBaseSuggestion,Argextends nothing and so has nopriorityand nodisplayName— 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;
nameis present and isSingleOrArray<string>where Fig requires one;subcommands,options,argsandsuggestionshave the shapes Fig declares; the whole tree is walked. Not covered, and said rather than implied: Fig's semantics past its key names — apriorityoutside 0–100, a malformedgeneratorsentry, aloadSpecnaming 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
renderFigSpecemittingsubCommands— 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.tsraises signals on aProcessLikethat 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 modeISIGturns0x03into aSIGINT, and in raw mode the identical keystroke arrives as a byte and raises nothing. A test that writes\x03to a pipe grades the raw path whatever it believes it is grading — which is howcaiqueshipped 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.json a real pty, presses Ctrl+C, and asserts the tty echoed^C, that the handler the program registered ran, and that the process died ofSIGINTrather than callingexit(130)— which is the POSIX-correct outcome and not whatshutdown.test.ts's fake records: 130 is a shell's arithmetic for128 + 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-librarypty.fork(), so nothing enters the lockfile — the same borrowing as callinggitincompat-oracle/src/vendor.ts. Two other dependency-free routes were considered:script(1)was measured and rejected, because BSDscriptcallstcgetattron its own stdin and dies withOperation not supported on socketunder any test runner; andzsh/zpty, whichscripts/complete-zsh.zshalready uses for the zsh completion case, is right where the subject is a shell widget but is gated onhas('zsh')and an apt-install, wherepython3is preinstalled on every hosted runner. Windows is skipped with its reason, not quietly dropped: Python'sptyis POSIX-only and a Windows pseudo-console means ConPTY through a native addon, so the third OS PLAN 2.5.4 asks for costsnode-pty— a native build on every runner, andcompat.ymlinstalls 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.tsexit — a bareprocess.exit(130)on SIGINT — both cases fail, oncleanup ran: expected '' to be 'cleaned up'andkilled by SIGINT (2): expected +0 to be 2.commander 1360 / 1360, yargs 804 / 804 and cross-spawn 68 / 68 before and after, unchanged.
-
#332
3ea38c3Thanks @ofri-peretz! - Help measured its columns withString.length, so a CJK or emoji command name mis-drew its own help screen..lengthis 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.tsused 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 importedwidthfromlinegaugefor exactly this job since it was ported, and its own comment records the reason: cliui's port carried its ownstringWidth, the ITU T.416 sub-parameter formESC[38:2::255:0:0mthat chalk emits for truecolor left:2::255:0:0mbehind, and a 13-column string measured 25. burgee already depended onlinegauge. This file simply was not asking.- Every measurement of rendered text in
help.tsis nowlinegauge'swidth, and the term column iswidest, which is the function that exists so a caller does not spread a large array intoMath.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 ownwrapdefaults tohard: falsefor 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.
linegaugejoinscloseoutandseniority/precedenceas 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. - Every measurement of rendered text in
-
#343
b245fb0Thanks @ofri-peretz! -burgeedeclaressideEffects, 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.jsends inrun(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, wherefalsewould have been a claim the package does not meet.roundelandflagstaffalready carried the field;burgeedid 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/commanderorburgee/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
488cbe5Thanks @ofri-peretz! ---schemaon a yargs-shaped CLI now emits the same document as the other two front ends.yargs/factory.tshand-rolledJSON.stringify(schemaOf(manifest), null, 2)whereexecute.tsandcommander/command.tsboth callmachineJson(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--schemawrote 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
--schemais now compact by default, and indented only when--format=json-prettyis 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/yargsbundled (114,738 → 114,809):machineJsonand its flag constant could previously be tree-shaken out of that entry, and now cannot.burgeecore andburgee/commanderare unchanged to the byte. That entry is already overlighter-than-yargs(1.032 → 1.033), and this makes it marginally worse on purpose — three front ends that disagree about what--schemameans 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.tsdrives 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
ead5f01Thanks @ofri-peretz! -cliui'stoString()is now linear in the cell it renders.rowToStringended each line withstr.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 thatmeasurePaddinggot in #278. Output is unchanged: only U+0020 is removed, so a trailing tab still survives underwrap: falseas it did before. -
#332
3ea38c3Thanks @ofri-peretz! - A ratchet on whether the family actually composes: every package exceptburgeemust be used by another package in it.layer-boundaries-lockis 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, withcaique,paratext,closeout,bellpullandflagstaffused 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.
edgesmay only go up andawaitingmay only shrink, each entry carrying the reason it is still there. A package must leaveawaitingthe 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
3ea38c3Thanks @ofri-peretz! - Six designs now say what their package offers, how a consumer extends it, and what it deliberately does not do — derived frompackage.json'sexportsmap andsrc/plugin.ts, not from the README.burgee,roundel,flagstaff,caique,closeoutandbellpulleach 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 inroundel's R11 and in its shipped README and exists in neither theexportsmap norsrc/.bellpull's R7 promises a root default export matchingexeca's and a./run-pathsubpath; neither exists, so thenpm-run-pathoverride recipe cannot be written, and R3'swhichis spelledwhichSyncin the code while R5'stoJSONistoJson.closeout's R6 promises a root default matchingsignal-exit's, and the root has no default export.flagstaff's R6 namesflagstaff/tableas thecli-table3façade —./tableis the built-in grid component and./cli-table3is the façade, so a reader following R6 imports the wrong module — and its R10 "depends onroundelonly" is contradicted by the package's own shape test, which asserts three dependencies.Two structural findings cross package lines. burgee's
definePluginis not the shape the layers register against.manifest.tsdeclares{ name, commands?, hooks?, enforce? }— nocontract, no layer key — validates nothing (its body isreturn plugin;), refuses nothing, and has nosrc/plugin.ts, so it is outside the vocabulary lock that polices every other host. Plugin-contributed commands bypassdefineCommand, so the reserved-name guard andcheckDefinitionnever run on them, and a plugin option namedjsonsilently overwrites the envelope flag. And the sharedschema.jsondescribes none of the three newest keys:widgets,handlersandresolversvalidate only because the root setsadditionalProperties: true, socaique,closeoutandbellpulleach 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, andnpx tsx scripts/plan-progress.tsprints the same 22/36 before and after, byte for byte. -
#332
3ea38c3Thanks @ofri-peretz! - The cliui backtracking guard is checked by shape rather than by clock, after three timing instruments failed on it, each differently.< 400 msat a small size — a CI box returned 440. A 10% margin measures the runner.- 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.
- 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 oftoString()'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. Reintroducingstr.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
4a7b4caThanks @ofri-peretz! -scripts/lanes.ts --checknow grants every lane its own changeset, which.sdlc/LANES.mdhas granted since the first run of these lanes.The document said it; the script did not implement it. So
--checkcalled 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.jsonis still a stray,*does not cross a slash, and another lane's source file is still another lane's.lane-boundaries-lock.test.tsholds all three, and goes red when the exemption is reverted. -
#332
3ea38c3Thanks @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
burgeemeasures a string's width itself, or reaches forchalkinstead ofroundel, 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.tsalready 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/commanderspawns child processes and forwards five signals because commander'sexecutableSubcommanddoes, 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
88f7ba6Thanks @ofri-peretz! - Read the process through one live seam, and fix the cliui growth gate's instrument.src/runtime.tsis now the only file in the package that namesprocess(PLAN 4.3, Y9); the allow-list inprocess-reference-lock.test.tsis down from nine burgee entries to one. Every member of the newhostexport is a getter, because the commander and yargs front-ends reproduce their incumbents' process contracts and those suites swapprocess.argv,exitandenvper 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()inyargs/cliui.test.tswas 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 computed0.05 / 0.05and 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
f295630Thanks @ofri-peretz! -schema.jsonconstrains token names, because it was promising something no host honours.tokenswas described as any name to a#rrggbbcolour.roundel'svalidate()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 ownE_PLUGIN_SCHEMAerror tells them, comparing their object againstroundel/schema.json, got a green from the schema and"accent" is not a tokenfromregister(). Measured 2026-09-16 with{ accent: '[#336699](https://github.com/ofri-peretz/burgee/issues/336699)' }.The schema now carries
propertyNames.enum, andscripts/plugin-contract-lock.test.tspins 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.tsasserts it), which is why nine packages are listed. Only the keyroundelowns is constrained: describingwidgets,handlers,sources,resolversorcommandsin 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 inplugin-schema-lock'sUNDESCRIBEDlist with that reason.linegaugeis 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 supersededget-east-asian-widthbar beside it with the count of entries that cleared it. -
#326
88f7ba6Thanks @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.tsreads the world throughimport process from 'node:process', androundel/src/runtime.tsthrough a guarded(globalThis as { process?: … }).processbound 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.envforever, because the member read is now on a local whose name a textual pattern cannot tell from any other. The same hole theglobalThis.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 inchalk.tssaying 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
48aec0aThanks @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 besidesnameandcontractare 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.tsholds 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. Tamperingcaique's real file with agadgetskey 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
50cc1a3Thanks @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 againstundefinednever fired: the axis ran,claudefailed to authenticate on all 25 task-runs, and the skip reported thatclaude"answered but nothing it produced passed a task's own check" — pointing a reader at a prompt-quality problem that did not exist. -
#278
862e837Thanks @ofri-peretz! - 🐛 Fix — help screens wrap against the real width:width,stripandwrapcome fromlinegaugeburgee/yargs' cliui port carried its ownstringWidth,stripAnsiand a wrap-ansi implementation. The strip was wrong: the ITU T.416 sub-parameter formESC[38:2::255:0:0m— what chalk emits for truecolor — left:2::255:0:0min 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
0750ebcThanks @ofri-peretz! - ♻️ Refactor — precedence and config discovery come fromseniorityrather than a second copyburgee carried its own
precedence.tsandconfig.ts;config.tswas byte-identical to seniority's andprecedence.tsdiffered by nineteen lines. Two copies of a precedence order is two answers to "where did this value come from", and--explainis 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,ConfigErrorand their types are still exported fromburgee, now re-exported fromseniority@^0.1.0, which is a new runtime dependency.
0.5.0
Minor Changes
-
#194
8693415Thanks @ofri-peretz! - burgee consumesroundelinstead of carrying a copy of it.contrast.tsexisted 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
14b4cb2Thanks @ofri-peretz! ---schemapublishesrelations(S2/S6).validate.tshas enforcedexactlyOneOf,conflicts,impliesand the rest since the surface shipped, and the schema never said so — an agent could only discover a constraint by violating it. A predicateimpliespublishes as"(predicate)"rather than thenullJSON.stringifywould leave.
Patch Changes
0.4.0
Minor Changes
-
#156
799a461Thanks @ofri-peretz! ---schemais 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-prettyrestores indentation for a person reading it. Both writers change: the engine's--schemaand the commander front-end's. -
#160
b6fc349Thanks @ofri-peretz! - Every boolean option accepts--no-<name>. burgee's precedence isflag > env > config file > package.json field > default, and the config and package.json layers set options by name — so any boolean can arrivetruewithout the user typing anything, while a boolean flag carries no value and--x=falseis refused. Before this the top layer of that chain could only ever saytrue, and a boolean turned on in a config file could not be turned off from the command line at all.--xand--no-xtogether: the later one wins. String options are unaffected, and--no-configstill means "load none". -
#134
7a38fe7Thanks @ofri-peretz! -burgee/brandgains three options, so a family of marks can come out of one declaration.shapereplaces the swallowtail with your own silhouette (SVG path data, same0 0 100 100box, filledevenoddso 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.sheenlays 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 underprefers-reduced-motion.bevelis 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
8f1043aThanks @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 printednode: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
8f1043aThanks @ofri-peretz! - The repo now dogfoods a twelfth Interlace ESLint plugin,browser-security, scoped to the docs app — 41 more rules aterrorover the surface the engine never touches and a browser application does. -
#151
8f1043aThanks @ofri-peretz! - Marks for the foundation tier —linegauge,seniority,bellpullandcloseout— from the same declaration as the other four. No API change:burgee/brandalready hadshape, and this is four more of them. -
#162
a447fc4Thanks @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 throughflags(), so it had been completing-l dryRunwhere the flag is--dry-run— a flag the parser refuses — for every camelCase option. The reserved surfaces are excluded:--no-jsonand--no-helpare not accepted by the parser and are not offered. -
#158
97ca535Thanks @ofri-peretz! ---versionand-Vanswer on a program that is a pure command group.dispatchhad always handled them, but only once a command resolved, so a program whose root runs nothing fell through tounknown 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
3e4071cThanks @ofri-peretz! -burgee/yargsfinds its locale table from the package root rather than from the depth of one file. It resolved../localesrelative to itself, which was correct only while that file sat directly indist/; the moment it moved into a directory the path becamedist/locales, y18n returned the key for every string, and 14 of yargs' own 804 tests failed. Walking up topackage.jsonresolves the same fromsrc/, fromdist/, and from an installednode_modules/burgee/dist/.
0.3.0
Minor Changes
-
#24
cf951deThanks @ofri-peretz! -Runtimegains aclock(now,schedule) with a deterministic fake inburgee/testing, andrenderHelpaccepts{ color, theme }— a structural token map the output stack can fill without burgee importing it. -
#33
8ad4d4aThanks @ofri-peretz! - Large CLIs (M1–M6):load: () => import('./x.js')on a command loads its handler on dispatch only — help,--schema(which marks itlazy), completions and the MCP tool list are complete without it;sharedOptions(name, specs)declares a set once and each copy is taggedsharedFromin the schema; a command withdeprecated: 'new'warns once on stderr and runs;groupandpluginride on the schema;resolveCommandandrunCommandare 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
0770990Thanks @ofri-peretz! -burgee dev <entry>: your agent is connected to your CLI while you write it. The entry (exportingprogramas a burgee manifest, a commanderCommandor 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_changedgoes 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 serverserveMcp()now wraps. -
#19
9670224Thanks @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.manifestprojected from what the program registered (builders run on a scratch instance, as yargs' completion does),use(plugin)withpreRun/postRunaround every handler,.effects()inside a builder,--jsonas the{ ok, data, meta }envelope when the program did not declare it anywhere in its tree,--schema,--mcpandcompletion <shell>from the manifest, and.burgee({ stdout, stderr, exit })injecting the streams and reporting E1 exit codes. -
#16
c7d1aa0Thanks @ofri-peretz! -burgee/yargs/parser: the ported yargs-parser as its own entry — whatimport parser from 'yargs-parser'gave, for a program that imported the parser directly. With it, and with the compatibility harness able torequire()its vendored root as a package, both façades now pass 100% of their hosts' own suites:burgee/commander1,361 / 1,361 andburgee/yargs804 / 804.
0.2.0
Minor Changes
- #14
95a954fThanks @ofri-peretz! -burgee/yargsandburgee/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 thatParseris the same object as theyargs-parsernpm package, which a dependency-free port cannot be.import yargs from 'burgee/yargs'andimport { hideBin, applyExtends, Parser } from 'burgee/yargs/helpers'are the drop-in;examples/conformanceproves the demo byte-identical on both.
Patch Changes
- #13
21d0f4fThanks @ofri-peretz! -burgee/commander:parse()is synchronous again andparseAsync()starts synchronously, exactly as commander's do. Since the--schema/--mcpsurface landed, both went through anasyncsurface check, so a synchronous action ran a microtask afterparse()returned and apreActionhook afterparseAsync()handed back its promise — commander's own suite asserts on both after every parse. 638 of its 1,331 tests had been failing onmainwhile the Compatibility job reported success, because a| teepipe hid the grader's exit code; every workflow step that pipes intoteenow runs withpipefail.