@williamthorsen/nmr 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.readyup/kits/default.js +27 -16
  2. package/.readyup/manifest.json +2 -2
  3. package/README.md +299 -62
  4. package/agents/guidance/rulebooks/nmr.md +4 -2
  5. package/bin/nmr-report-catalog.js +11 -0
  6. package/dist/esm/UserError.d.ts +3 -0
  7. package/dist/esm/UserError.js +6 -0
  8. package/dist/esm/check-cache.d.ts +36 -1
  9. package/dist/esm/check-cache.js +91 -23
  10. package/dist/esm/cli-build.js +2 -1
  11. package/dist/esm/cli-clean.js +2 -1
  12. package/dist/esm/cli-ensure-prepublish-hooks.js +2 -1
  13. package/dist/esm/cli-report-catalog.d.ts +1 -0
  14. package/dist/esm/cli-report-catalog.js +11 -0
  15. package/dist/esm/cli-report-overrides.js +2 -1
  16. package/dist/esm/cli-taze.js +2 -1
  17. package/dist/esm/cli.js +2 -1
  18. package/dist/esm/commands/report-catalog.d.ts +1 -0
  19. package/dist/esm/commands/report-catalog.js +34 -0
  20. package/dist/esm/commands/report-overrides.js +22 -7
  21. package/dist/esm/commands/taze.js +2 -1
  22. package/dist/esm/config.d.ts +1 -0
  23. package/dist/esm/config.js +66 -18
  24. package/dist/esm/default-scripts.d.ts +5 -1
  25. package/dist/esm/default-scripts.js +13 -10
  26. package/dist/esm/defineConfig.d.ts +1 -1
  27. package/dist/esm/help.js +3 -1
  28. package/dist/esm/helpers/clampToBytes.d.ts +2 -0
  29. package/dist/esm/helpers/clampToBytes.js +20 -0
  30. package/dist/esm/helpers/createBoundedBuffer.js +1 -4
  31. package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
  32. package/dist/esm/helpers/deriveExcerpt.js +30 -0
  33. package/dist/esm/helpers/duration.js +16 -9
  34. package/dist/esm/helpers/package-json.d.ts +11 -4
  35. package/dist/esm/helpers/package-json.js +51 -22
  36. package/dist/esm/helpers/readStringValues.d.ts +1 -0
  37. package/dist/esm/helpers/readStringValues.js +8 -0
  38. package/dist/esm/helpers/transcript.d.ts +5 -0
  39. package/dist/esm/helpers/transcript.js +59 -0
  40. package/dist/esm/recording.d.ts +65 -0
  41. package/dist/esm/recording.js +96 -0
  42. package/dist/esm/replay-assembly.d.ts +9 -0
  43. package/dist/esm/replay-assembly.js +26 -0
  44. package/dist/esm/report-format.d.ts +18 -0
  45. package/dist/esm/report-format.js +25 -0
  46. package/dist/esm/reportCliFailure.d.ts +2 -0
  47. package/dist/esm/reportCliFailure.js +11 -0
  48. package/dist/esm/resolve-scripts.d.ts +1 -1
  49. package/dist/esm/resolver.d.ts +9 -1
  50. package/dist/esm/resolver.js +26 -17
  51. package/dist/esm/runCli.js +339 -74
  52. package/dist/esm/runner.d.ts +9 -3
  53. package/dist/esm/runner.js +25 -4
  54. package/dist/esm/scripts.d.ts +1 -1
  55. package/dist/esm/steps.d.ts +9 -2
  56. package/dist/esm/steps.js +117 -4
  57. package/dist/esm/types.d.ts +11 -2
  58. package/dist/esm/verbosity.d.ts +15 -4
  59. package/dist/esm/verbosity.js +32 -12
  60. package/dist/esm/verdict.d.ts +29 -0
  61. package/dist/esm/verdict.js +150 -0
  62. package/dist/esm/workspace.d.ts +2 -0
  63. package/dist/esm/workspace.js +20 -3
  64. package/package.json +12 -5
package/README.md CHANGED
@@ -3,25 +3,69 @@
3
3
  Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
4
4
 
5
5
  <!-- section:release-notes -->
6
- ## Release notes — v0.30.0 (2026-08-10)
6
+ ## Release notes — v0.31.0 (2026-08-13)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - 🚨 **Breaking:** Resolve a command to a step list and reject a script element carrying shell syntax (#648)
10
+ - Report a verdict for every command nmr runs (#659)
11
11
 
12
- A script defined in `.config/nmr.config.ts` as a list of nmr commands now holds one command name per entry, optionally preceded by nmr's own flags. An entry that carries a quoted argument or any other shell syntax previously ran as written and is now rejected when the config loads. Before upgrading, a repo relying on such an entry should define that command as a script of its own and reference it by name from the list.
12
+ `nmr` now reports one line for every command it runs, naming the scope, command, outcome, and timing. The lines print in every verbosity, which makes `-q` the low-noise way to run rather than a silent one: a passing quiet run reports its verdicts and nothing else, so a pass, a cached skip, and a no-op override stay distinguishable.
13
13
 
14
- - 🚨 **Breaking:** Sequence a command's steps in nmr rather than in a shell (#649)
14
+ Verdicts nest. A composite reports alongside every command it expands into, while a `:pre`/`:post` hook leaf and a `--filter`/`--recursive` delegate report nothing, since each is already covered by the levels around it.
15
15
 
16
- When a run in quiet mode fails, it now shows only the output of the command that failed instead of everything the run had produced up to that point. Quiet mode can now be set for a whole shell session with the `NMR_COMMAND_VERBOSITY` environment variable, which accepts `full` or `quiet`; `-q` still takes precedence for a single run, and an unrecognized value is reported as an error. Interrupting a run now stops the commands that would have followed it.
16
+ - Resolve a run's verbosity from repo config and agent-harness detection (#660)
17
17
 
18
- Breaking: a `devBin` entry that maps `nmr` itself no longer takes effect inside a command that runs other nmr commands. Mapping any other tool (the documented use) is unaffected.
18
+ A verbosity preference for `nmr` can now be set in the repo's root config, and `nmr` now automatically switches to quiet mode when it detects a known agent harness. This makes four ways to set verbosity: the `-q` option, the `NMR_COMMAND_VERBOSITY` env var, config, and harness detection, consulted in that order and defaulting to `full` when none applies.
19
19
 
20
- - 🚨 **Breaking:** Express the seven root defaults as steps (#650)
20
+ - 🚨 **Breaking:** Refine nmr upgrade to support catalogs (#661)
21
21
 
22
- Running `nmr build`, `nmr test`, `nmr test:all`, `nmr test:coverage`, `nmr test:tool`, `nmr test:unit`, or `nmr typecheck` from a monorepo root now passes over a package that does not define the command, instead of failing there. The first run of `test`, `test:coverage`, `test:tool`, `test:unit`, or `typecheck` after the upgrade no longer skips as already passed; skipping resumes on the run after.
22
+ Adds `nmr-report-catalog`, which names the dependencies a package takes from a pnpm catalog, their `catalog:` specifiers, and the monorepo root a covering pass runs from. The workspace `upgrade` default chains it ahead of the upgrade tool, and `nmr report-catalog` runs it alone. Because taze reads `pnpm-workspace.yaml` only when that file sits at its working directory, changes to the catalog are not attempted during a package-scoped upgrade.
23
23
 
24
- `nmr` now also warns when a command it is about to run invokes `nmr` again through a shell, where a failure reports the whole inner run's output instead of the failing command's. The warning catches only a direct `nmr` call, so a command that reaches `nmr` through another program goes unreported.
24
+ Separately, `nmr-report-overrides` now reports the `overrides` block in `pnpm-workspace.yaml` and fails on a `pnpm.overrides` block in the root `package.json`. Both `upgrade` and `root:upgrade` run the reporter ahead of taze. This avoids a scenario where taze would meaninglessly write to the latter block.
25
+
26
+ Migration: Move the block to `overrides` in `pnpm-workspace.yaml`, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`. `engines` declares `pnpm: ">=11"`.
27
+
28
+ - Replay an excerpt of the run a skip recalls (#667)
29
+
30
+ `nmr` can now show the result of the previous check when a check is skipped. When `nmr` runs a command, it stores the result, keyed by presentation environment. The result is deemed to be the last blank-line-delimited block written by the command. If the command is run again in the same presentation environment, the stored result is shown, labeled "replayed:" for clarity.
31
+
32
+ - Assemble a skipped composite's replay from its constituents' excerpts (#668)
33
+
34
+ A skipped composite now replays what its constituents recorded, with each line attributed to the scope and command that produced it:
35
+
36
+ ```console
37
+ ⏭️ nmr: check: passed 35.7s ago on this tree, saved ~10s — replayed: nmr: fmt:check: All matched files use Prettier code style!; nmr: test: Test Files 52 passed (52) Tests 987 passed (987)
38
+ ```
39
+
40
+ Nesting collapses. A constituent that fans out across packages contributes a line from every package that recorded one; a constituent that recorded nothing is omitted.
41
+
42
+ A new reserved variable, `NMR_RUN_ID`, carries a run's identity to every process below it. Every recorded excerpt names the run that last certified it. A composite summary includes saved results with the same status as a fresh result.
43
+
44
+ - Print a retained run with nmr --log (#669)
45
+
46
+ Adds `nmr --log <command>`, which prints what the last recorded pass wrote instead of running the command again. The whole transcript, up to a limit of 256 KiB, is now saved for any passing command that is not writing straight to a terminal; `--log` prints it under a header giving the instant, the duration, and the command chain that succeeded. The log of a composite is the sum of its constituents' excerpts. If a change disqualifies the recording, `--log` instead reports the change.
47
+
48
+ - Emit one JSON object per command for machine consumers (#672)
49
+
50
+ Adds `--json` to `nmr`, allowing the caller to request output in JSON format instead of prose. An env var carries the choice of `json` or `text` down a spawned chain. When the output is JSON, the consumer receives one JSON object per command; each object carries the command, scope, and outcome, and a recalled pass carries the recorded excerpts.
51
+
52
+ The mode implies quiet. Stdout carries the objects alone (with no prose verdict line and no override notice); a failing command still displays its output on stderr.
53
+
54
+ - 🚨 **Breaking:** Bind trailing arguments to every step that accepts them (#675)
55
+
56
+ Routes an argument passed to an `nmr` command to every step of that command that accepts it, in place of the last step alone. Thus, `nmr test <file>` therefore runs only that file's tests; previously, the root suite ran in full alongside the narrowed package runs. Steps by default accept passed arguments, but can be configured to ignore them. If no step of a command accepts the argument, `nmr` names the command and runs nothing.
57
+
58
+ A command carrying an argument also reads the check-result cache nowhere in its chain, so it no longer skips part of its work from a pass recorded for an unnarrowed run.
59
+
60
+ Migration: Existing configs need no change, since a bare string element keeps its present meaning. Two behaviors differ: A composite passes its arguments to every accepting step rather than to its final step alone; and `nmr typecheck <file>` at the repo root is refused, where it previously checked that file under default compiler options at the root and hunted for it in every package.
61
+
62
+ ### 🐛 Bug fixes
63
+
64
+ - Derive the shelled-nmr remedy from the declaration site and reject a malformed package.json (#658)
65
+
66
+ Fixes a shelled-nmr warning that named `.config/nmr.config.ts` as the remedy for every step it reported, including the `package.json` entries that tier cannot resolve. The line now names the relevant declaration and remedy. Shelled nmr can now be detected not only in a step's first token but anywhere in the step.
67
+
68
+ Separately, a configuration mistake now reports as a plain message rather than a Node stack trace.
25
69
  <!-- /section:release-notes -->
26
70
 
27
71
  ## Installation
@@ -111,7 +155,7 @@ If no per-package override exists, the highest-tier value that is set wins. Set
111
155
 
112
156
  ### Script values
113
157
 
114
- Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-invocations:
158
+ A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` carry an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
115
159
 
116
160
  ```ts
117
161
  // "fix": ["lint", "fmt"]
@@ -120,11 +164,28 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-
120
164
 
121
165
  Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace — `-R test` runs the element in every package, the way `nmr -R test` does. An element carrying a quoted argument or shell syntax is rejected when the config loads: name that command as a script of its own and reference the name.
122
166
 
123
- Passthrough arguments attach to the final step alone: `nmr fix --dry-run` runs `nmr lint`, then `nmr fmt --dry-run`.
167
+ #### Where trailing arguments go
168
+
169
+ Trailing arguments reach every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
170
+
171
+ ```ts
172
+ // "ci": [{ run: "build", declinesArgs: true }, "check:strict"]
173
+ // nmr ci src/foo.ts -> nmr build && nmr check:strict src/foo.ts
174
+ ```
175
+
176
+ `{ run }` is the bare string element with room for the declaration; `declinesArgs` defaults to `false`, and an element that accepts is written as the string. Decline where the step is a prerequisite the narrowed steps run against, or where the tool it reaches would be misled — `tsgo --noEmit foo.ts` abandons the tsconfig, which is why every default `typecheck` step declines. A leaf tool that merely ignores what it is handed is not a reason to decline: nmr forwards, and the tool decides.
177
+
178
+ Where no step of a command accepts them, nmr rejects the invocation and runs nothing, rather than running the whole command unnarrowed. That is what `nmr typecheck src/foo.ts` gets at the root, whose two steps both decline.
179
+
180
+ Two kinds of step are not declarable. A string script is one command handed to a leaf tool, so it always receives the arguments — which is why `nmr typecheck src/foo.ts` inside a package still reaches `tsgo`, where the root rejects it. A `:pre` or `:post` hook never receives them, the arguments having been placed before the hooks wrap the chain.
181
+
182
+ An invocation carrying trailing arguments also serves no part of its work from the [check-result cache](#check-result-cache), the steps that decline them included.
124
183
 
125
- A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step whose first token is `nmr`.
184
+ A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step reaching nmr through a shell.
126
185
 
127
- That line reports a boundary rather than enforcing a rule, and it is partial: a step reaching nmr through another program, such as `pnpm --recursive exec nmr build`, goes unreported. Only the built-in defaults and `.config/nmr.config.ts` carry arrays, so a `package.json` script embedding nmr has nowhere to move but up a tier.
186
+ The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first.
187
+
188
+ nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, or `|`, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. The line reports a boundary rather than enforcing a rule, and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
128
189
 
129
190
  ## Configuration
130
191
 
@@ -149,11 +210,11 @@ export default defineConfig({
149
210
  | ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
150
211
  | `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
151
212
  | `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](#check-result-cache) may skip |
152
- | `workspaceScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the workspace registry (tier 2) |
153
- | `rootScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the root registry (tier 2) |
213
+ | `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2) |
214
+ | `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2) |
154
215
  | `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
155
216
 
156
- All fields are optional. `workspaceScripts` and `rootScripts` values follow the same `string | string[]` convention described in [script values](#script-values).
217
+ All fields are optional. A `ScriptValue` is a command string or an array of steps, each a command string or `{ run, declinesArgs? }`; both are described in [script values](#script-values), and both types are exported from `@williamthorsen/nmr/config`.
157
218
 
158
219
  Each field belongs to exactly one tier, and a config loads only the fields its own tier honors: `build` in a package config, the other four in the monorepo-root config. Declaring a field at the wrong tier fails with a message naming it and where it goes, as does a key nmr does not recognize at all — a typo cannot degrade into a setting that silently applies nowhere.
159
220
 
@@ -230,6 +291,64 @@ Attach a step to one package's hook:
230
291
 
231
292
  The second example calls the bin directly: `report-overrides` is a root-registry command, so `nmr report-overrides` from inside a package fails with `Unknown command`, while the bin runs from either scope.
232
293
 
294
+ ## What nmr reports
295
+
296
+ Every command nmr runs reports one line naming the scope it ran at, the command, the outcome, and the timing:
297
+
298
+ ```console
299
+ ✅ nmr-core: test: passed in 12.4s
300
+ ❌ nmr: build: failed in 1.2s (exit 1)
301
+ ⏭️ nmr-core: test: passed 4m ago on this tree, saved ~12s
302
+ ⛔ nmr-core: lint: skipped, the override is empty
303
+ ```
304
+
305
+ A recalled pass also carries an excerpt of the run it recalls, where that run left one; see [Replaying a skipped run's output](#replaying-a-skipped-runs-output).
306
+
307
+ The scope is the directory the command's registry belongs to: a package's own directory, or the monorepo root. Durations and ages truncate and carry at most two units, so a check that passed 90 seconds ago reports `1m 30s ago` and never `2m ago`.
308
+
309
+ Verdicts nest. A composite reports, and so does every command it expands into; a `:pre` or `:post` hook reports nothing of its own, since the command it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either, every scope it fans out to reporting instead. A cold repo-wide `nmr ci` in this package's own monorepo spends 26 lines on a run that prints roughly 250.
310
+
311
+ A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` POSIX permits, so packages running concurrently under `pnpm --recursive` cannot interleave within a line. A line longer than that is cut and marked with `…`; nothing nmr reports today comes near it.
312
+
313
+ **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
314
+
315
+ ### Reporting for a machine
316
+
317
+ `--json` renders each verdict as a JSON object instead of a line of prose — one object per line, newline-delimited, with no enclosing array and no separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line it already emitted parseable, which an enclosing array would not.
318
+
319
+ ```console
320
+ $ nmr --json check
321
+ {"command":"typecheck","scope":"nmr-core","outcome":"passed","durationMs":255}
322
+ {"command":"fmt:check","scope":"nmr-core","outcome":"recalled","ageMs":240000,"savedMs":3888,"replay":[{"command":"fmt:check","excerpt":"Checking formatting... All matched files use Prettier code style!","scope":"nmr-core"}]}
323
+ {"command":"check","scope":"nmr-core","outcome":"passed","durationMs":31204}
324
+ ```
325
+
326
+ Every object carries `command`, `scope`, and `outcome`, and then what that outcome carries and no other does:
327
+
328
+ | `outcome` | Also carries |
329
+ | ---------- | ---------------------------------------------------------------------------------------------------------- |
330
+ | `passed` | `durationMs` |
331
+ | `failed` | `durationMs`, `exitCode` |
332
+ | `recalled` | `ageMs`, `savedMs`, and `replay` where the skip has excerpts to [replay](#replaying-a-skipped-runs-output) |
333
+ | `no-op` | `reason`, either `empty-override` or `noop-override` |
334
+
335
+ Both renderings are produced from one record, so neither reports what the other does not. Where the prose line makes a presentation decision the record does not — a saving too small to be worth a clause, say — the object carries the fact and leaves the decision to whoever reads it.
336
+
337
+ **An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) a prose line is held to, so packages running concurrently under `pnpm --recursive` cannot interleave within one. Cuts land inside the record's text rather than across its structure, so the line still parses, and a record that overruns is fitted by rungs — each shedding what a reader can better spare than the one below it:
338
+
339
+ 1. **The excerpts are shortened toward one another**, so an assembly's constituents come down together rather than the first ones being emptied to leave the last whole. No excerpt is cut below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
340
+ 2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry carrying no `excerpt` key is one whose text did not fit.
341
+ 3. **The trailing constituents go**, as the prose line drops its own tail, and the `replay` array itself once none fit.
342
+ 4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself: a line that overruns can be split across a pipe and corrupt the records of every scope sharing it, where a marked cut costs only its own.
343
+
344
+ A package-scoped `check` carries four constituents and fits uncut. A four-package root `check` assembles twelve — `typecheck` and `test` each fan out to every package, and a nested composite's list is spliced in flat — which is more than the ceiling can name: it reaches rung 3, keeping eight and dropping the trailing four. Rung 4 needs a scope and command running to hundreds of bytes between them.
345
+
346
+ **The mode implies quiet.** The commands' own output is withheld, so stdout carries the objects alone — no prose verdict, and no override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. A failure still surrenders the failing command's output, on stderr, as under any quiet run. Both modes leave a command on the same channels, so a machine-readable run and a quiet one share a [retention key](#replaying-a-skipped-runs-output): each replays what the other recorded.
347
+
348
+ The format is nmr's own reporting and not a mode the commands it runs are told about, so it is out of the [pass key](#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor costs a replay.
349
+
350
+ `--log`, `--help`, and `--version` are unaffected: none of them reports a verdict, and what `--log` prints is a tool's own transcript, which has no rendering as a record.
351
+
233
352
  ## Check-result cache
234
353
 
235
354
  A check that passed on a working tree passes again on the same working tree. nmr records that, and the next run of the same command on an unchanged tree exits 0 without doing the work:
@@ -239,10 +358,10 @@ $ nmr ci
239
358
  # ... four minutes of build, typecheck, lint, and coverage ...
240
359
 
241
360
  $ nmr ci
242
- ⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m).
361
+ ⏭️ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
243
362
  ```
244
363
 
245
- Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip line is suppressed by `-q`.
364
+ Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip is [reported](#what-nmr-reports) in every verbosity.
246
365
 
247
366
  The cache is a working-tree cache, not a build cache: it answers "has this exact tree already passed this check?" and nothing else. It is also local to one checkout (the install fingerprint in its key is machine-specific), so it saves repeated work in a working copy rather than sharing results across machines or with CI.
248
367
 
@@ -302,10 +421,79 @@ The gate never wrongly skips. Where it cannot be sure, it does nothing and the c
302
421
  - When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
303
422
  - When there is no pnpm install to fingerprint.
304
423
  - Under a `devBin` substitution, which runs a binary built from somewhere the hash does not describe.
305
- - For any invocation carrying arguments after the command name.
424
+ - For any invocation carrying arguments after the command name, and for every step below it: the arguments narrow what runs, and a step served from a recorded pass is a step that did not.
306
425
 
307
426
  `NMR_DEBUG=1` reports why a run did not skip and why the gate stood aside.
308
427
 
428
+ ### Replaying a skipped run's output
429
+
430
+ A skip reports an excerpt of the run it recalls, marked as a recording rather than as this run's output:
431
+
432
+ ```console
433
+ $ nmr test
434
+ # ... the suite runs ...
435
+
436
+ $ nmr test
437
+ ⏭️ nmr: test: passed 2m ago on this tree, saved ~12s — replayed: Test Files 6 passed (6) Tests 41 passed (41)
438
+ ```
439
+
440
+ The excerpt is the last blank-line-delimited block of what the command wrote, which is the closing statement a tool separates from its progress output. It is flattened onto one line, with escape sequences stripped, table rules dropped, and a redrawn progress line reduced to what a reader was left looking at. Nothing is parsed and no figure is computed: nmr replays bytes it recorded. A block wider than a few kilobytes is cut and marked with `…`, so a command whose closing statement is one long line cannot carry its whole output into the cache.
441
+
442
+ The excerpt and the whole transcript it is drawn from are written by the same operation that records the pass, so a pass declined because the tree moved or build output changed leaves neither behind, and `nmr clean` clears both along with the passes they belong to.
443
+
444
+ **Capture happens at the command, and only where nmr can see its output.** A command whose stdout or stderr is a terminal writes where nmr never reads, so an interactive run retains nothing and its progress display is untouched; a piped or redirected run, and any run under `-q`, retains both streams. A composite hands its descriptors to the child `nmr` processes below it and captures nothing of its own, and a `:pre` or `:post` hook contributes nothing to the command it wraps: an excerpt is the command's own output or none.
445
+
446
+ **A composite replays its constituents' excerpts.** Capturing nothing of its own, a composite assembles what its constituents recorded, each line attributed to the scope and command that produced it:
447
+
448
+ ```console
449
+ $ nmr check
450
+ ⏭️ nmr-core: check: passed 3m ago on this tree, saved ~48s — replayed: nmr-core: fmt:check: All matched files use Prettier code style!; nmr-core: test: Test Files 6 passed (6) Tests 41 passed (41)
451
+ ```
452
+
453
+ The assembly is flat: a constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test:coverage` rather than one opaque `check:strict` line. A delegate expands into the scopes it fans out to, and a constituent that recorded no excerpt is absent rather than inferred: `typecheck` and `lint:check` print nothing on success and contribute nothing above, and a command outside the [cacheable set](#what-is-cached) records nothing at all, so `ci` replays what `check:strict` earned and nothing for `build`.
454
+
455
+ It is assembled when the composite records its pass rather than when it skips, so every excerpt in it was certified during that one run, on that one tree. `NMR_RUN_ID` is what makes that provable; see [reserved environment variables](#reserved-environment-variables).
456
+
457
+ **A skip certifies what it replays.** Recalling a pass proves the excerpt describes this tree, and a matching retention key proves it describes this presentation environment, so a skip restamps the recalled entry with the run replaying it. A constituent already warm when a composite ran therefore still contributes its line: `nmr check` after `nmr test` replays the test summary. The restamp touches nothing else -- the instant and the duration belong to the run that earned the pass.
458
+
459
+ **A replay is held to more than the pass is.** Retention carries its own key: the [pass key](#what-the-key-is-made-of) folded with the channel each output stream ran on and with `CI`, `COLUMNS`, `FORCE_COLOR`, `NO_COLOR`, and `TERM`. A recording made under a different one is recalled as a pass all the same; its excerpt is simply not replayed, and the verdict prints alone, as it does for a pass that retained nothing.
460
+
461
+ The whole line, excerpt included, is held to the same [512-byte ceiling](#what-nmr-reports) every verdict is, so one write still carries it whole under concurrent fan-out. An assembly wide enough to overrun it is cut and marked with `…` rather than spread over several lines.
462
+
463
+ ### Reading a retained run
464
+
465
+ The excerpt is a line; the run behind it is kept whole. `nmr --log <command>` prints it, led by a header dating what follows:
466
+
467
+ ```console
468
+ $ nmr --log test
469
+ 📼 nmr: test — recorded 2026-08-12T15:04:05.412Z (12m ago), ran in 12.4s
470
+ $ pnpm exec vitest --project unit --project tool
471
+
472
+ ✓ src/__tests__/resolver.unit.test.ts (18 tests) 12ms
473
+
474
+ Test Files 6 passed (6)
475
+ Tests 41 passed (41)
476
+ ```
477
+
478
+ The header is what presents the body as a recording rather than as this invocation's output. The command string is the whole chain, hooks included, so it names what earned the pass and not merely what was typed. A flag rather than a subcommand, since `log` would collide with the script registry.
479
+
480
+ **Reading a recording runs nothing.** No hook runs, no verdict prints, and nothing is recorded or restamped. `--no-cache` governs running rather than reading and is ignored.
481
+
482
+ **A recording is held to the tree, not to the terminal.** `--log` prints exactly what a skip would have recalled: the [pass key](#what-the-key-is-made-of) has to match. The [retention key](#replaying-a-skipped-runs-output) is not consulted, so a recording made under a different terminal width, or through a pipe, still prints -- a reader at a terminal being shown what a piped run wrote is the case the flag exists for.
483
+
484
+ **A composite prints its assembly.** Retaining nothing of its own, a composite prints the excerpts its constituents recorded, one attributed line each. The transcript of any one of them is that constituent's own `--log` to print.
485
+
486
+ **Nothing to show is said rather than left blank.** A refusal names which it is -- the command is outside the [cacheable set](#what-is-cached), the gate is [standing aside](#when-the-gate-stands-aside), nothing has recorded a pass, the last pass was recorded under something this run does not share, or the pass retained no output, having printed none or written to a terminal -- and exits non-zero, so a caller can tell an empty `stdout` from a recording. A mismatch names the ingredient that moved, the tree or the chain or a version, so a reader whose `git status` is clean is not sent looking there.
487
+
488
+ ```console
489
+ $ nmr --log test
490
+ 📭 nmr: test: no recording; the last pass was 3m ago, on a tree this is not
491
+ ```
492
+
493
+ **`--log` reaches the scopes `-F` and `-R` select.** The flag rides the delegate, so each scope prints its own recording. There a scope with nothing to show reports its gap and exits 0: partial coverage is a survey's normal shape, and failing on the first gap would hide every scope that had something.
494
+
495
+ **Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes a verdict line is held to. A transcript overrunning it keeps 128 KiB from each end, because a run's opening and its closing statement each carry what the other does not, and the drop is marked with a line naming the bytes it stands for rather than being cut silently.
496
+
309
497
  ### Bypassing and clearing
310
498
 
311
499
  Reach for these in order; the first is almost always the right one.
@@ -323,10 +511,29 @@ Reach for these in order; the first is almost always the right one.
323
511
 
324
512
  `NMR_TREE_SNAPSHOT` is nmr's own: it carries one observation of the tree from a top-level invocation down to the processes it spawns, so a chain hashes the tree once rather than at every link. nmr trusts an inherited value only while `HEAD` still stands where it did when the observation was taken, which bounds a process that outlives the run that spawned it. That bound does not extend to a tree edited without committing, so **a process that survives its run and later invokes nmr should clear `NMR_TREE_SNAPSHOT`** — a test suite that shells out to `nmr` is the case worth checking.
325
513
 
326
- `NMR_COMMAND_VERBOSITY` is nmr's own as well, and carries how loudly a run reports the output of the commands it runs. Its values are `full` and `quiet`; any other non-empty value is reported and exits 1, rather than falling back to a mode nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command it runs rather than of everything below it, so a failure still surrenders the failing command's output. nmr's own messages — the skip line, the override notice, and the `""`/`":"` skip notices — keep the suppression `-q` has always given them.
514
+ `NMR_RUN_ID` is nmr's own too: it carries one run's identity from a top-level invocation down to the processes it spawns, so the excerpts recorded at every scope during one run are recognizable as that run's, and a composite can assemble from them. Where `NMR_TREE_SNAPSHOT` is bounded by `HEAD`, this is bounded by the tree hash each entry records, so a process that outlives its run and carries the identity into a later one contributes nothing to that run's assemblies.
515
+
516
+ `NMR_COMMAND_VERBOSITY` is nmr's own as well, and carries how loudly a run reports the output of the commands it runs. Its values are `full` and `quiet`; any other non-empty value is reported and exits 1, rather than falling back to a mode nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command it runs rather than of everything below it, so a failure still surrenders the failing command's output. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message a quiet run withholds.
327
517
 
328
518
  `-q` sets it for the run and outranks an inherited value. Both values are spelled out so either direction is expressible: export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
329
519
 
520
+ `NMR_REPORT_FORMAT` is nmr's own as well, and carries how nmr renders its own verdicts. Its values are `text` and `json`; any other non-empty value is reported and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so either direction is expressible: export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. Two sources, then, where a verbosity has four: no repo-level default and no harness detection, since detection would hand every agent JSON in place of the prose the [shipped guidance](#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects carry.
521
+
522
+ #### Where a verbosity comes from
523
+
524
+ Four sources, in precedence order. Each is read once, at the top of a chain: the resolved value travels down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
525
+
526
+ | Source | Set it when |
527
+ | ----------------------------------------------------- | ---------------------------------------------------------- |
528
+ | `-q` | One invocation should be quiet. |
529
+ | `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
530
+ | `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
531
+ | An agent harness nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
532
+
533
+ Falling off the bottom leaves the run `full`, which is what an unrecognized harness gets: detection adds quiet where it fires and changes nothing where it does not.
534
+
535
+ Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The shipped set is `CLAUDECODE`, `ROVODEV_CLI`, and `ROVO_CLI`. **Treat that set as perishable.** The names belong to ecosystems nmr does not control and that rename, and a rename fails silently here, since the symptom is a return to loud output rather than an error. `output.extraAgentEnvVars` is how a repo adds a name — a harness nmr has never heard of, or one whose marker was renamed after the installed version shipped — without waiting for a release. A repo that wants no detection at all configures `output.commandVerbosity: 'full'`, which outranks it.
536
+
330
537
  ### Configuring
331
538
 
332
539
  ```ts
@@ -342,18 +549,35 @@ export default defineConfig({
342
549
 
343
550
  `extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest; `excludeCommands` is applied afterwards, so a name in both is excluded. `enabled: false` turns the gate off entirely. The key is `checkCache`, in the monorepo-root config.
344
551
 
552
+ ```ts
553
+ export default defineConfig({
554
+ output: {
555
+ // The verbosity a run takes when neither `-q` nor the environment named one.
556
+ commandVerbosity: 'quiet',
557
+ // Markers of a harness the shipped set does not name, added to it rather than replacing it.
558
+ extraAgentEnvVars: ['MY_HARNESS'],
559
+ },
560
+ });
561
+ ```
562
+
563
+ `output` is a monorepo-root key, and both of its fields feed the [verbosity ladder](#where-a-verbosity-comes-from).
564
+
345
565
  ## Dependency upgrades
346
566
 
347
567
  `nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
348
568
 
349
- | Invocation | Covers |
350
- | ---------------------------- | ------------------------------------------- |
351
- | `nmr upgrade` (from root) | the root `package.json` and every workspace |
352
- | `nmr upgrade` (in a package) | that package |
353
- | `nmr -F <package> upgrade` | that package, from anywhere |
354
- | `nmr root:upgrade` | the root `package.json` alone |
569
+ | Invocation | Covers | Catalogs |
570
+ | ---------------------------- | ------------------------------------------- | -------- |
571
+ | `nmr upgrade` (from root) | the root `package.json` and every workspace | yes |
572
+ | `nmr upgrade` (in a package) | that package | no |
573
+ | `nmr -F <package> upgrade` | that package, from anywhere | no |
574
+ | `nmr root:upgrade` | the root `package.json` | yes |
575
+
576
+ A [pnpm catalog](https://pnpm.io/catalogs) is declared in `pnpm-workspace.yaml`, and the upgrade tool reads that file only when it sits at the working directory, irrespective of `--recursive`, which is why `root:upgrade` covers every catalog too. A package-scoped invocation therefore cannot see a dependency the catalog declares, and a package whose dependencies are all catalogued would otherwise report itself up to date. It runs [`report-catalog`](#report-catalog) first instead, which names each catalogued dependency and the root a covering pass runs from.
577
+
578
+ Both root-scoped invocations precede their report with any active override, which pnpm declares in `pnpm-workspace.yaml`. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
355
579
 
356
- `nmr upgrade` from the root precedes its report with any active `pnpm.overrides` declared in the root `package.json`; `nmr root:upgrade` does not. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
580
+ A `pnpm.overrides` block left in the root `package.json` fails the command instead. pnpm reads no setting from that field as of pnpm 11, so the block pins nothing, while the upgrade tool keeps its own list of dependency fields and goes on rewriting the versions in it under `--write` — a block that looks maintained and governs nothing. Failing ahead of the tool is what keeps that write from happening, on `root:upgrade` as much as on `upgrade`. nmr supports pnpm 11 and later.
357
581
 
358
582
  The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
359
583
 
@@ -390,29 +614,30 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
390
614
 
391
615
  ### Workspace scripts
392
616
 
393
- | Command | Runs |
394
- | --------------- | ----------------------------------------------------------- |
395
- | `build` | `compile` |
396
- | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
397
- | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
398
- | `clean` | `nmr-clean` |
399
- | `compile` | `nmr-compile` |
400
- | `fix` | `lint`, `fmt` |
401
- | `fix:check` | `fmt:check`, `lint:check` |
402
- | `fmt` | `nmr-fmt --write` |
403
- | `fmt:check` | `nmr-fmt --check` |
404
- | `lint` | `eslint --fix .` |
405
- | `lint:check` | `eslint .` |
406
- | `lint:strict` | `strict-lint` |
407
- | `test` | `pnpm exec vitest --project unit --project tool` |
408
- | `test:all` | `pnpm exec vitest` |
409
- | `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
410
- | `test:tool` | `pnpm exec vitest --project tool` |
411
- | `test:unit` | `pnpm exec vitest --project unit` |
412
- | `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
413
- | `typecheck` | `tsgo --noEmit` |
414
- | `upgrade` | `nmr-taze --include-locked` |
415
- | `view-coverage` | `open coverage/index.html` |
617
+ | Command | Runs |
618
+ | ---------------- | ----------------------------------------------------------- |
619
+ | `build` | `compile` |
620
+ | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
621
+ | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
622
+ | `clean` | `nmr-clean` |
623
+ | `compile` | `nmr-compile` |
624
+ | `fix` | `lint`, `fmt` |
625
+ | `fix:check` | `fmt:check`, `lint:check` |
626
+ | `fmt` | `nmr-fmt --write` |
627
+ | `fmt:check` | `nmr-fmt --check` |
628
+ | `lint` | `eslint --fix .` |
629
+ | `lint:check` | `eslint .` |
630
+ | `lint:strict` | `strict-lint` |
631
+ | `report-catalog` | `nmr-report-catalog` |
632
+ | `test` | `pnpm exec vitest --project unit --project tool` |
633
+ | `test:all` | `pnpm exec vitest` |
634
+ | `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
635
+ | `test:tool` | `pnpm exec vitest --project tool` |
636
+ | `test:unit` | `pnpm exec vitest --project unit` |
637
+ | `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
638
+ | `typecheck` | `tsgo --noEmit` |
639
+ | `upgrade` | `nmr-report-catalog && nmr-taze --include-locked` |
640
+ | `view-coverage` | `open coverage/index.html` |
416
641
 
417
642
  `fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
418
643
 
@@ -546,7 +771,7 @@ These scripts operate on root-level code only (not workspace packages):
546
771
  | `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
547
772
  | `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
548
773
  | `root:typecheck` | `tsgo --noEmit` |
549
- | `root:upgrade` | `nmr-taze --include-locked` |
774
+ | `root:upgrade` | `nmr-report-overrides && nmr-taze --include-locked` |
550
775
 
551
776
  #### Utilities
552
777
 
@@ -564,15 +789,17 @@ nmr [flags] <command> [args...]
564
789
 
565
790
  Position determines ownership: flags before the command name are nmr's own, and everything after the command name is forwarded untouched to the resolved command.
566
791
 
567
- | Flag | Description | Default |
568
- | ------------------------ | ------------------------------------------------------- | ------- |
569
- | `-F, --filter <pattern>` | Run command in matching packages | — |
570
- | `-R, --recursive` | Run command in all packages | — |
571
- | `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
572
- | `-q, --quiet` | Suppress info messages; show full output on failure | — |
573
- | `--no-cache` | Run even if this tree already passed; record the result | — |
574
- | `-?, --help` | Show available commands | — |
575
- | `-V, --version` | Show version number | — |
792
+ | Flag | Description | Default |
793
+ | ------------------------ | -------------------------------------------------------------- | ------- |
794
+ | `-F, --filter <pattern>` | Run command in matching packages | — |
795
+ | `-R, --recursive` | Run command in all packages | — |
796
+ | `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
797
+ | `-q, --quiet` | Suppress command output, keeping nmr's verdicts | — |
798
+ | `--json` | Report one JSON object per command, withholding command output | — |
799
+ | `--log` | Print the recorded run instead of running it | — |
800
+ | `--no-cache` | Run even if this tree already passed; record the result | — |
801
+ | `-?, --help` | Show available commands | — |
802
+ | `-V, --version` | Show version number | — |
576
803
 
577
804
  ### Examples
578
805
 
@@ -598,9 +825,19 @@ nmr --workspace-root check # Run root check from a package dir
598
825
 
599
826
  These commands are available as `nmr` subcommands and as standalone `nmr-`-prefixed binaries (for use in lifecycle hooks).
600
827
 
828
+ ### `report-catalog`
829
+
830
+ Report the dependencies the current package takes from a [pnpm catalog](https://pnpm.io/catalogs), which a package-scoped `upgrade` cannot reach (see [dependency upgrades](#dependency-upgrades) for why). Each line names the dependency, its `catalog:` specifier, and the monorepo root a covering pass runs from. The workspace `upgrade` script runs it automatically, so no per-repo wiring is needed. The subcommand belongs to the workspace registry, so it runs from a package directory; the `nmr-report-catalog` bin reports nothing when run from the monorepo root, because a root-scoped pass reads the catalog itself.
831
+
832
+ ```bash
833
+ nmr report-catalog
834
+ ```
835
+
601
836
  ### `report-overrides`
602
837
 
603
- Report any active `pnpm.overrides` in the root `package.json`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so no per-repo wiring is needed. Invoking it directly is useful for a one-off report.
838
+ Report any active pnpm override, declared in the root `pnpm-workspace.yaml`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so no per-repo wiring is needed. Invoking it directly is useful for a one-off report.
839
+
840
+ A `pnpm.overrides` block in the root `package.json` fails the command, naming every entry it holds and where to move it — see [dependency upgrades](#dependency-upgrades) for why the block is inert. `pnpx codemod run pnpm-v10-to-v11` performs the move.
604
841
 
605
842
  ```bash
606
843
  nmr report-overrides
@@ -679,7 +916,7 @@ nmr-fmt --write packages/nmr
679
916
 
680
917
  ### `nmr-taze`
681
918
 
682
- Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what `root:upgrade` resolves to, and what the root `upgrade` script ends with — see [dependency upgrades](#dependency-upgrades) for the workflow.
919
+ Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow.
683
920
 
684
921
  Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
685
922
 
@@ -10,5 +10,7 @@ delivery: ambient
10
10
  - Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
11
11
  - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
12
12
  - `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs the audit first, then `ci`, so a vulnerability stops the run before the long gate. Neither installs or binds a git hook.
13
- - A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. Any edit, addition, or deletion re-runs it, so a skip is a real pass and needs no confirming: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`.
14
- - The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), and the check-result cache's key, its `checkCache` config, and every condition that disables it.
13
+ - A check that already passed on the current working tree skips, printing `⏭️ <scope>: <command>: passed …` and, where the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
14
+ - `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it -- so output you need in full is a recording to read rather than a run to pay for again. It runs nothing, and it is held to the tree rather than to the terminal: a recording a piped run made prints for a reader at one. A composite prints its constituents' excerpts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. Nothing to show is said on one line and exits non-zero, naming which it is.
15
+ - Every command nmr runs reports one line -- `✅ <scope>: <command>: passed in 12.4s`, `❌ … failed in 1.2s (exit 1)`, `⏭️ … passed 4m ago on this tree`, or `⛔ … skipped, the override is empty` -- and a composite reports alongside every command it expands into. The lines print in every verbosity, so `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
16
+ - The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), and the check-result cache's key, the retention key a replayed excerpt is additionally held to, the retention ceiling `--log` prints within, its `checkCache` config, and every condition that disables it.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ try {
3
+ await import('../dist/esm/cli-report-catalog.js');
4
+ } catch (error) {
5
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
+ process.stderr.write('nmr-report-catalog: build output not found — run `pnpm run bootstrap` first\n');
7
+ } else {
8
+ process.stderr.write(`nmr-report-catalog: failed to load: ${error.message}\n`);
9
+ }
10
+ process.exit(1);
11
+ }
@@ -0,0 +1,3 @@
1
+ export declare class UserError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class UserError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UserError';
5
+ }
6
+ }