Changelog
Every release of seniority, newest first, from its CHANGELOG.md — what changed and the pull request it came from.
0.6.4
Patch Changes
-
#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.
-
#731
bc68493Thanks @ofri-peretz! -seniority/cosmiconfignow matches cosmiconfig 10.0.1 in three places where it did not.stopDir: ''searches the start directory alone, as upstream's truthiness check does. Before, an emptystopDirswitched the search toglobaland walked up to the working directory and then into the global config directory.packagePropwalks a path the way upstream does. A path through a string reads the string's own properties, so'name.length'is a number. A path through anull, such as"foo": nullunderpackageProp: 'foo.bar', throws theTypeErrorupstream throws, annotated with the file. Before, both answered "not found", and the search moved on to the next file.- A start directory that cannot be
stated for any reason but absence rejects the search with thestaterror, as upstream'sisDirectorydoes. Before, every such failure read as "no config here". Which paths fail that way is the platform's call: on Linux and macOS,search('<file>/sub')rejects withENOTDIRand a directory the process may not enter withEACCES; Windows reports<file>\subas not found, so there it is still "no config here", as it is upstream.
loadJsonno longer wraps a non-Errorin anError, becauseJSON.parseof a string throws nothing else. -
#732
ee6780bThanks @ofri-peretz! - Code no input could reach is removed. There is no behaviour change.seniority checkloses a "(replaces …)" helper it never called, and a map that returned each source name unchanged. It loads one plugin into an emptied registry, so there is nothing for a source to replace. The header now says that.seniority/lilconfigno longer writesdirname(p) || sep, because Node'sdirnamenever returns''.seniority/dotenv's parser no longer checks that a match has a key, because the key group in its pattern is not optional.seniority/rc's comment stripper reads characters withcharAt, so there is no fallback for an index that is always in range.- The JSON loader behind
seniority/configquotes the parser's own message without first checking that it threw anError, becauseJSON.parseof a string throws nothing else.
0.6.3
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
0.6.2
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.
0.6.1
Patch Changes
- #588
073037aThanks @ofri-peretz! - READMEs and package descriptions now match what each drop-in path is graded at. bellpull namesbellpull/node-whichas the drop-in for npmwhich(5 / 5) and no longer lists execa as a drop-in. paratext's ansi-escapes row is 4 / 4, with the CSI half implemented. seniority's rc row is 1 / 1, dotenv'sconfig()defaults toprocess.env, and lilconfig is 77 / 77. linegauge documentsambiguousIsNarrowandstripas shipped. No code changes.
0.6.0
Minor Changes
- #486
f08ff58Thanks @ofri-peretz! - Theseniority/dotenvandseniority/rcdrop-ins now read the process by default, like the packages they replace.config()with no arguments populatesprocess.envfrom./.env;rc(name)reads the process environment.config()also accepts aURLor~/path, callsfs/osin a way test stubs can intercept, and returnsparsedalongside anyerror, matching dotenv 17. Graded by each incumbent's own test suite: dotenv 80 → 106 of 141, rc 0 → 1 of 1. seniority's resolver still never reads the process itself.
Patch Changes
-
#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. -
#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.
0.5.1
Patch Changes
- #508
1aae1e2Thanks @ofri-peretz! -<package> --helpand--versionanswer instead of crashing. The bin took its first argument as the plugin file to import, soroundel --helpfailed withCannot find module '…/--help'and exit 1.-h/--helpnow print usage and exit 0,-V/--versionprint the version and exit 0, and any other flag where the plugin file belongs is a usage error, exit 2.
0.5.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 Benchmarks section's weight ceiling is re-measured against a fresh install of each incumbent's latest release (cosmiconfig 10.0.1, slice-ansi 9.0.1, which 7.0.0, dotenv 18.0.3, …) instead of the copies hoisted in this workspace, and names incumbents that were measured but left out of the ceiling as exactly that.
0.4.3
Patch Changes
- #494
f7f6d4bThanks @ofri-peretz! - Each package'shomepageand README docs link now point at its own documentation site,https://<package>.interlace.tools, instead of a page on burgee's site. The oldburgee.interlace.tools/docs/packages/<package>URLs answer with a 301 to the new host, so nothing already linked breaks. closeout's README override example also resolves to the current release again (npm:closeout@^0.4; the 0.4.0 release left it at^0.3).
0.4.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.
0.4.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
-
#445
dac303eThanks @ofri-peretz! - Every package now declaressideEffectstruthfully, so bundlers can drop what you don't import.Six packages declared nothing, so no bundler could drop any of their modules. A named import from the root now bundles to the same bytes as the same import from its subpath:
import before after import { explain } from 'seniority'2,939 B 1,067 B import { decide } from 'caique'1,235 B 734 B import { strip } from 'linegauge'1,102 B 940 B import { once } from 'closeout'353 B 235 B flagstaff and roundel used to declare
false, but each ships acheckcommand whose file runs when loaded. Each now lists that file, which is the true statement. paratext also lists the two modules that register its built-in capabilities when they load.
0.4.0
Minor Changes
-
#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
-
#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'.
Patch Changes
-
#430
4d1b2b3Thanks @ofri-peretz! -checknow reports every refusal with its code and its fix, wherever it was raised.Some plugin files register themselves on import: they call
register()at the top of the module and export the result. Until now, when such a file was refused, the error was thrown insidecheck'simport(), before the onlytrythat turns aPluginErrorintoE_PLUGIN_SCHEMA: …plus afix:line. The author got the bare message on stderr, with no code and no fix. Now the whole ofcheckruns inside that one handler, so every refusal comes out the same way on every host.
0.3.1
Patch Changes
-
#386
5a85175Thanks @ofri-peretz! - The Unicode segmenter is built on first use rather than at import, and both packages now strip comments from what they publish.new Intl.Segmenter()loads ICU's grapheme-break data. Two were constructed at module scope, and almost nothing paid for them: every caller takes the ASCII fast path first, so a run of printable ASCII — a help screen, a flag name, a path — never reachessegment().segmenteris now a function; the two call sites becomesegmenter().linegaugeandsenioritywere also the two published packages whose build never ranstrip-commentsat all. Unpacked: linegauge 83,538 → 56,148 and seniority 193,682 → 139,793.Together these take
import 'burgee'from 56.87 ms to 44.39 ms, medians of seven.
0.3.0
Minor Changes
-
#379
955b979Thanks @ofri-peretz! -seniority/lilconfigandseniority/rc— two new drop-in subpaths, andseniority/dotenvgrows the default export its incumbent has.seniority/lilconfigis lilconfig 3.1.3's surface:lilconfig,lilconfigSync,defaultLoaders,defaultLoadersSync, and no fifth runtime export, because lilconfig's own suite compares the module's keys against cosmiconfig's. Graded at 67 / 77 against that suite, up from 0 — the same number its control scores, so no case in it now separates the two. The zero was not a missing feature: the row had been pointed at the package root, which presents cosmiconfig's surface and answerslilconfigSync is not a functionseventy-seven times.seniority/rcis rc 1.2.8's merge with none of its four dependencies: the file stack in rc's own order,__nesting for environment keys, JSON-with-comments,deep-extend's merge, andconfigs/configreporting which files were read. INI is refused by name with the argument that would parse it, the way YAML already is. Its environment arrives as an argument rather than off the process, which is the one divergence and the reason its compat row stays at 0 / 1.seniority/dotenvnow has a default export carryingconfig,parseandpopulate, so a CJS callerrequire()ing it gets the same mutable objectrequire('dotenv')gives — which is what dotenv's own suite stubs.populatealso matches 17.4.2 more closely: it validatesparsed(notprocessEnv), returns the keys it actually set, and logs underdebug. The row moves 74 / 141 → 80 / 141.
0.2.0
Minor Changes
-
#316
c8acb28Thanks @ofri-peretz! - seniority to 1.0's requirement set: cosmiconfig's surface, measured at 186 / 241.The root export now carries
cosmiconfig's own API —cosmiconfig,cosmiconfigSync,Explorer,ExplorerSync,defaultLoaders,defaultLoadersSync, the search-place and loader tables,decodeFileContentandgetPropertyByPath— with all three search strategies, both caches,$importwithmergeImportArrays, and the meta-config merge. Graded by cosmiconfig 10.0.1's own suite, run unmodified, it goes from 3 / 241 (1.2%) to 186 / 241 (77.2%) against a control of 240 / 241.Every one of the 55 cases it does not pass is one of two named divergences: 54 are the absent YAML parser — this package bundles no format parser, so
loadYamlreads the JSON subset of YAML and refuses the rest with an error naming theloadersoption that supplies one — and one is a test-harness file path. None is a difference in how a config is found, merged or reported.Two new compatibility subpaths:
seniority/dotenv— dotenv 17'sparseandpopulate, grammar included.configtakes the environment it populates asprocessEnvrather than reaching forprocess.env, so nothing in this package touches the process.seniority/find-up—findUp,findUpSync,findUpMultiple,findUpMultipleSyncover a bounded, symlink-cycle-safe upward walk, in place of four packages.
Also new on the root export, all additive:
explanation()—--explainas a record, withexplain()'s text now literally a rendering of it, alongsideexplanationJson()andexplanationEvent().search()/searchAll()— the walk, bounded bystopAt, a depth limit and the filesystem root.loadPath(),loaderFor(),LoaderError— four builtin loaders, injected loaders for every other format, and a usage-class error naming the extension and the option.validate()/check()— a violation reported with its provenance:`out` must be a string; `./mytool.config.js:3` set it to `4`.provenance.line, recorded per key for JSON config layers, anddiscover'sloaders,extensions,upwardandstopAtoptions.
No behaviour of the existing API changes, and the package still declares zero dependencies.
-
#294
3f92a60Thanks @ofri-peretz! - Hostsources, the plugin key, atseniority/plugin— a plugin adds a resolution source (a vault, a CI variable set, a remote config) and--explainnames it as the provenance of the value it won.Sourceis now an open union, so this is additive rather than a type break, and the one code change it costs isdescribe()'sdefaultbranch: an unknown source renders itself from thesourceandlocationit declared, which is how a plugin explains itself without seniority knowing its name.ORDERis exported and is the single declaration the union and the newRANKare both generated from — the design's array and the shipped union had disagreed (project/home/pkgagainstconfig/package), and a plugin must not register against two spellings. The shipped five win, becauseprovenance.sourceis a value users already read and becauseprojectversushomewas one kind with two locations, whichlocationalready names precisely.A plugin's
rankslots its source between two built-ins and is refused outside(flag, default): it can never beat the flag the user typed, nor sink below the declared default. The order stays the fixed thing it claims to be.
Patch Changes
-
#316
c8acb28Thanks @ofri-peretz! -mergeAllrefusesprototypeas well as__proto__andconstructor, and the guard is three comparisons rather than aSetlookup.The guard existed and had no test — it was written, and believed. CodeQL's
js/prototype-polluting-functioncould not see it through theSetbinding and blocked a merge on it, which is a fair complaint about a security guard: one a reader has to follow a binding to find is one a reviewer will miss too.What it actually prevents, measured rather than assumed. A first attempt at the test asserted
({}).polluted === undefinedafter merging a__proto__key and passed with the guard deleted —target['__proto__'] = vgoes through the setter and swaps that object's prototype; it does not writeObject.prototype. The damage is narrower and quieter: the config object handed back to the caller silently inherits whatever the file said, soconfig.isAdmincan answer for a key no file set at the top level.cosmiconfig-util.test.tsnow asserts that, and four of its six cases go red when the guard is removed. -
#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.
0.1.0
Minor Changes
-
#219
ac7b9e5Thanks @ofri-peretz! - seniority resolves flags, env, config and defaults — with provenanceThe package existed as a reserved name exporting a string constant. It now does the job its description has always claimed.
flag > env > config file > package.json field > defaultresolve(specs, layers)returns the values, the provenance of each, and every candidate that lost — soexplain(name, resolution)can print the winning source and the ones it beat, generated by the same code that picked the value. A--explainbuilt any other way can drift from the truth; this one cannot.resolveis pure: layers in, values out, no filesystem and noprocess.env.discoveris the half that touches the disk and is a separate import for that reason — searchingNAME_CONFIG,./name.config.{json,mjs,js,cjs}and$XDG_CONFIG_HOME/name/config.json, followingextendswith deep merge and rejecting cycles with the chain that formed them.The code is burgee's, moved down a layer where it belongs:
precedence.tsandconfig.tswith their 28 tests, which passed unchanged. ItsOptionSpechere declares only the three fields resolution reads, so any program's richer option type satisfies it structurally — no adapter, no import, and no dependency pointing back up the stack.Zero dependencies; Node builtins only.