@williamthorsen/nmr 0.39.0 → 0.40.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 (88) hide show
  1. package/.meta/changelog.json +128 -4
  2. package/.readyup/kits/default.js +47 -32
  3. package/.readyup/manifest.json +12 -7
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +41 -73
  6. package/agents/README.md +8 -8
  7. package/agents/guidance/rulebooks/nmr.md +8 -8
  8. package/bin/ensure-prepublish-hooks.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-taze.js +1 -1
  15. package/bin/nmr.js +1 -1
  16. package/dist/esm/UserError.d.ts +1 -2
  17. package/dist/esm/check-cache.d.ts +59 -61
  18. package/dist/esm/commands/build-output.d.ts +35 -36
  19. package/dist/esm/commands/build.d.ts +13 -14
  20. package/dist/esm/commands/build.js +3 -3
  21. package/dist/esm/commands/clean.d.ts +6 -6
  22. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -5
  23. package/dist/esm/commands/fmt.d.ts +8 -8
  24. package/dist/esm/commands/fmt.js +1 -1
  25. package/dist/esm/commands/report-catalog.d.ts +5 -7
  26. package/dist/esm/commands/report-catalog.js +1 -1
  27. package/dist/esm/commands/report-overrides.d.ts +0 -2
  28. package/dist/esm/commands/report-overrides.js +1 -1
  29. package/dist/esm/commands/taze.d.ts +9 -8
  30. package/dist/esm/config.d.ts +5 -5
  31. package/dist/esm/config.js +3 -3
  32. package/dist/esm/context.d.ts +1 -2
  33. package/dist/esm/default-scripts.d.ts +5 -6
  34. package/dist/esm/defineConfig.d.ts +1 -1
  35. package/dist/esm/git-ignored-paths.d.ts +9 -0
  36. package/dist/esm/git-ignored-paths.js +10 -0
  37. package/dist/esm/glyphs.d.ts +3 -3
  38. package/dist/esm/help.d.ts +2 -2
  39. package/dist/esm/helpers/clampToBytes.d.ts +4 -5
  40. package/dist/esm/helpers/config-path.d.ts +0 -2
  41. package/dist/esm/helpers/createBoundedBuffer.d.ts +2 -2
  42. package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
  43. package/dist/esm/helpers/duration.d.ts +8 -9
  44. package/dist/esm/helpers/filter-selection.d.ts +19 -19
  45. package/dist/esm/helpers/findClosestName.d.ts +1 -1
  46. package/dist/esm/helpers/hook-name.d.ts +2 -5
  47. package/dist/esm/helpers/package-json.d.ts +9 -8
  48. package/dist/esm/helpers/readStringValues.d.ts +2 -3
  49. package/dist/esm/helpers/reportClosing.d.ts +8 -9
  50. package/dist/esm/helpers/transcript.d.ts +13 -13
  51. package/dist/esm/helpers/type-guards.d.ts +1 -0
  52. package/dist/esm/output-style.d.ts +12 -13
  53. package/dist/esm/prettier.d.ts +2 -2
  54. package/dist/esm/recording.d.ts +20 -15
  55. package/dist/esm/recording.js +7 -7
  56. package/dist/esm/replay-assembly.d.ts +10 -10
  57. package/dist/esm/report-format.d.ts +14 -19
  58. package/dist/esm/reportCliFailure.d.ts +2 -3
  59. package/dist/esm/resolve-scripts.d.ts +4 -5
  60. package/dist/esm/resolveBinStyles.d.ts +4 -5
  61. package/dist/esm/resolver.d.ts +14 -18
  62. package/dist/esm/runCli.d.ts +6 -6
  63. package/dist/esm/runCli.js +21 -21
  64. package/dist/esm/runner.d.ts +19 -19
  65. package/dist/esm/steps.d.ts +43 -42
  66. package/dist/esm/taze.d.ts +1 -1
  67. package/dist/esm/tests.d.ts +18 -13
  68. package/dist/esm/tests.js +2 -2
  69. package/dist/esm/tiers.d.ts +27 -23
  70. package/dist/esm/tiers.js +6 -2
  71. package/dist/esm/types.d.ts +5 -5
  72. package/dist/esm/verbosity.d.ts +22 -20
  73. package/dist/esm/verdict.d.ts +33 -33
  74. package/dist/esm/verdict.js +1 -1
  75. package/dist/esm/vitest-source-resolution.d.ts +11 -11
  76. package/dist/esm/vitest-source-resolution.js +2 -2
  77. package/dist/esm/vitest.d.ts +25 -24
  78. package/dist/esm/vitest.js +16 -8
  79. package/dist/esm/workspace.d.ts +4 -4
  80. package/docs/check-cache.md +57 -57
  81. package/docs/prettier.md +10 -10
  82. package/docs/reporting.md +46 -46
  83. package/docs/scripts.md +29 -29
  84. package/docs/upgrades.md +18 -18
  85. package/docs/utilities.md +13 -13
  86. package/docs/vitest.md +64 -62
  87. package/docs/workspace.md +19 -19
  88. package/package.json +5 -4
package/README.md CHANGED
@@ -2,53 +2,21 @@
2
2
 
3
3
  # @williamthorsen/nmr
4
4
 
5
- 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.
5
+ Context-aware script runner for pnpm monorepos. Includes an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v0.39.0 (2026-09-22)
8
+ ## Release notes — v0.40.0 (2026-10-01)
9
9
 
10
10
  ### 🎉 Features
11
11
 
12
- - Report why a workspace selects no package, and no-op nmr's own -R steps (#841)
12
+ - Makes `checkTestFileConventions` and the readyup kit's tier and placement checks skip untracked paths that git ignores, so that local build output such as a stale `.netlify/` directory no longer fails the check. (#898)
13
+ - Excludes untracked paths that git ignores from collection in `defineVitestConfig` and `defineRootVitestConfig`, so that Vitest does not run a test file that the conventions check skips. (#898)
14
+ - Rewords the `nmr --log` refusal for a run that the check-result cache does not cover to `the check-result cache does not apply here (NMR_DEBUG=1 reports why)`, and renames the matching `docs/check-cache.md` heading to "When the cache does not apply", which changes its anchor. (#923)
13
15
 
14
- - Drops a `-R` step that nmr composed into a script where the workspace holds no package, so `typecheck` and the five `test` scripts run there instead of failing the run.
15
- - Adds a no-op verdict for a command that the drop leaves with no step at all.
16
- - Replaces the single refusal for a workspace that selects no package with one message per cause.
16
+ ### 🐛 Bug fixes
17
17
 
18
- Migration: In a repo that declares a workspace package pattern and deliberately holds no package, delete the `.config/nmr.config.ts` entries added to work around the `-R` refusal, the `build: ':'` override included.
19
-
20
- - 🚨 **Breaking:** Prefix the booleans and reject unrecognized Vitest option keys (#849)
21
-
22
- - Renames two boolean options of `defineVitestConfig` and `defineRootVitestConfig`, the factories that build a repo's Vitest config: `isolateGit` becomes `shouldIsolateGit`, and `resolveFromSource` becomes `shouldResolveFromSource`.
23
- - Makes both factories reject an option key outside the recognized set, naming the offending keys and the recognized ones, rather than ignoring a misspelled or retired key and letting its option fall back to the default with nothing in the run reporting it.
24
- - Adds a retired-key check that runs ahead of the unrecognized-key check, reporting the new name for a config that still sets `isolateGit` or `resolveFromSource`.
25
-
26
- Migration: In each options object passed to `defineVitestConfig` or `defineRootVitestConfig`, rename `isolateGit` to `shouldIsolateGit` and `resolveFromSource` to `shouldResolveFromSource`, and correct any other key that the factory now rejects. `defineRootVitestConfig` recognizes `monorepoRoot` on the last layer alone, so a config that sets it on an earlier layer now throws where the old code ignored it.
27
-
28
- - 🚨 **Breaking:** End nmr's source value names with the kind of the value (#850)
29
-
30
- - Renames two fields of `TestFileConventionsReport`, `misplaced` and `untiered`, to `misplacedFiles` and `untieredFiles`, and renames `TestFileConventionsOptions`'s `exclude` to `excludedBasenames`, in the `@williamthorsen/nmr/tests` entry point.
31
- - Adds a guard to `checkTestFileConventions` and `reportTestFileConventions` that throws a `TypeError` naming `excludedBasenames` when a call still sets `exclude`.
32
-
33
- Migration: Read `report.misplacedFiles` and `report.untieredFiles` in place of `report.misplaced` and `report.untiered`, and pass `excludedBasenames` in place of `exclude`. A call that still sets `exclude` throws rather than running with no exclusions, so the failure surfaces where Vitest loads the call site.
34
-
35
- - 🚨 **Breaking:** Hoist the pnpm-workspace resolver into nmr-core as a total API (#864)
36
-
37
- - Adds a `./workspace` subpath export to `@williamthorsen/nmr-core`, whose `resolveWorkspace` returns a pnpm workspace's package directories, or the cause of an empty result, as a value rather than as a thrown error.
38
- - Reports a `pnpm-workspace.yaml` that the YAML parser rejects under a cause of its own, `unreadable-manifest`, so `nmr -R build` names the syntax error and its remedy in place of failing with the parser's own error.
39
- - Removes `diagnoseEmptyWorkspace` and `EmptyWorkspaceDiagnosis` from `@williamthorsen/nmr/workspace`, since `resolveWorkspace` reports the same cause in one call.
40
-
41
- Migration: Replace each `diagnoseEmptyWorkspace` call with `resolveWorkspace`, which reports the cause on the empty result it already returns, and rename the `no-manifest` cause to `no-package`. `EmptyWorkspaceCause` also gains `unreadable-manifest`, so an exhaustive switch over it stops compiling until it handles the new member; every other export of `@williamthorsen/nmr/workspace` keeps its signature and its `UserError` throws.
42
-
43
- - 🚨 **Breaking:** Adopt the shared workspace resolver in release-kit, honoring exclusions (#865)
44
-
45
- - 🚨 **Breaking:** Narrows `no-pattern` in `@williamthorsen/nmr-core`'s `EmptyWorkspaceCause`, moving an absent or empty `packages` list onto `no-packages-list` and a value that is not a list of strings onto `unreadable-packages`.
46
- - Stops `release-kit` from tagging, publishing, and label-syncing a package that a `!` entry in `pnpm-workspace.yaml` excludes, which its previous `glob`-based resolution could not remove from the match.
47
- - Replaces the release of the root package alone with a failure naming the condition that emptied the workspace, when `pnpm-workspace.yaml` does not hold valid YAML or declares a workspace that does not resolve to a package.
48
- - Replaces the `no-pattern` remedy that `nmr` printed for a `pnpm-workspace.yaml` declaring no `packages` list, and for one declaring a `packages` value that is not a list of strings.
49
- - Raises `engines.node` to `>=24.16.0` in `@williamthorsen/nmr-core` and `@williamthorsen/release-kit`, because Node 24.0 through 24.15 ignores the `followSymlinks` option that the workspace matcher passes to `globSync` and drops a symlinked package directory from the resolution without reporting it.
50
-
51
- Migration: Add a `no-packages-list` arm and an `unreadable-packages` arm to any `switch` that covers `EmptyWorkspaceCause` exhaustively. `no-pattern` now names only a list that the resolver read in full and whose entries all failed to become a positive pattern, so move the handling of an absent `packages` key, an empty list, and a value that is not a list of strings out of the `no-pattern` arm into the two new ones. Route `no-packages-list` to single-package behavior rather than to a failure path: pnpm resolves such a manifest to the root package alone.
18
+ - Rewords the messages printed by `nmr`, its bin wrappers, and its readyup kit, which used figures such as "sits under" and "holds", em-dashes, and "no" as a determiner, so that a diagnosis such as "-F/--filter did not match any workspace" states its condition directly. (#932)
19
+ - Fixes the issue that the `nmr` rulebook recommended `-q` without stating that nmr reads its own flags only before the command name, which led agents to run `nmr ci -q` and fail when `nmr-fmt` rejected the flag. (#948)
52
20
  <!-- /section:release-notes -->
53
21
 
54
22
  ## Installation
@@ -61,16 +29,16 @@ pnpm add -D @williamthorsen/nmr
61
29
 
62
30
  ### Making `nmr` resolvable
63
31
 
64
- nmr installs as a workspace bin, so the bare `nmr` command works only when your shell can find `<root>/node_modules/.bin/nmr`. Choose one:
32
+ nmr installs as a workspace bin, so the bare `nmr` command works only when the shell can find `<root>/node_modules/.bin/nmr`. Choose one:
65
33
 
66
34
  - **[direnv](https://direnv.net/)** (recommended for contributors). Add `PATH_add node_modules/.bin` to the repo's `.envrc`, and bare `nmr` works from any subdirectory.
67
- - **`pnpm exec nmr <command>`**. Works with no setup: pnpm resolves the bin from the workspace root.
35
+ - **`pnpm exec nmr <command>`**. Works without any setup: pnpm resolves the bin from the workspace root.
68
36
 
69
37
  > **Note:** Avoid `npx nmr`. Inside a git worktree, `npx` can resolve a different nmr binary from outside the working tree, so the command succeeds while running the wrong code.
70
38
 
71
39
  ## Quick start
72
40
 
73
- nmr works out of the box with no configuration. It ships with built-in scripts for common monorepo tasks.
41
+ nmr works without any configuration. It includes built-in scripts for common monorepo tasks.
74
42
 
75
43
  From a package directory:
76
44
 
@@ -86,7 +54,7 @@ From the monorepo root:
86
54
  nmr test # Run root tests + recursive workspace tests
87
55
  nmr build # Build all packages
88
56
  nmr ci # Build, then run the strict checks
89
- nmr prepush # Run everything the remote runs, before you push
57
+ nmr prepush # Run everything the remote runs, before a push
90
58
  ```
91
59
 
92
60
  Target packages from anywhere:
@@ -96,19 +64,19 @@ nmr --filter core test # Test only the package whose manifest name is `core`
96
64
  nmr --recursive lint # Lint all workspace packages
97
65
  ```
98
66
 
99
- `--recursive`, and a `--filter` that selects more than one package, give each package no terminal input. An interactive command such as `test:watch` needs a filter that selects a single package.
67
+ `--recursive`, and a `--filter` that selects more than one package, do not give any package terminal input. An interactive command such as `test:watch` needs a filter that selects a single package.
100
68
 
101
- 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.
69
+ 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.
102
70
 
103
- `nmr --help` lists every flag and command, with the shell command each command resolves to. Per-package `package.json` files need no script entries.
71
+ `nmr --help` lists every flag and command, with the shell command to which each command resolves. Per-package `package.json` files do not need any script entries.
104
72
 
105
- nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
73
+ nmr detects the directory from which it is invoked and selects the right scripts automatically. See [context-aware resolution](#context-aware-resolution) below.
106
74
 
107
75
  ## Context-aware resolution
108
76
 
109
- nmr's key feature is that the same command runs different scripts depending on where you invoke it. It walks up from your current directory to find `pnpm-workspace.yaml`, then checks whether your CWD is inside a workspace package directory.
77
+ nmr's key feature is that the same command runs different scripts depending on where it is invoked. It walks up from the current directory to find `pnpm-workspace.yaml`, then checks whether the current directory is inside a workspace package directory.
110
78
 
111
- | Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
79
+ | Where `nmr` is invoked | Registry used | Working directory | `nmr test` runs |
112
80
  | --------------------------------- | ----------------- | ----------------- | -------------------------------------------------------------------- |
113
81
  | Monorepo root | Root scripts | Monorepo root | `root:test`, then `-R test` |
114
82
  | Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project unit --project tool` (that package only) |
@@ -126,21 +94,21 @@ nmr --workspace-root check
126
94
  Two consequences:
127
95
 
128
96
  - `nmr --workspace-root clean` sweeps every workspace package, as `nmr clean` does from the root.
129
- - Passthrough paths resolve against the working directory: from `packages/nmr/src/`, `nmr --workspace-root fmt pnpm-workspace.yaml` formats the file at the monorepo root.
97
+ - Passthrough paths resolve against the working directory: From `packages/nmr/src/`, `nmr --workspace-root fmt pnpm-workspace.yaml` formats the file at the monorepo root.
130
98
 
131
99
  ## Three-tier override system
132
100
 
133
101
  Scripts resolve through three tiers. Higher tiers override lower ones:
134
102
 
135
- 1. **Built-in defaults** — scripts shipped with this package
136
- 2. **Repo-wide config** — additions and overrides in `.config/nmr.config.ts`
137
- 3. **Per-package overrides** — scripts in a package's `package.json`
103
+ 1. **Built-in defaults**: Scripts included in this package
104
+ 2. **Repo-wide config**: Additions and overrides in `.config/nmr.config.ts`
105
+ 3. **Per-package overrides**: Scripts in a package's `package.json`
138
106
 
139
107
  [Scripts and configuration](docs/scripts.md#three-tier-override-system) shows a worked example, how to skip a script for one package, and what nmr does with a `package.json` entry that re-invokes its own command.
140
108
 
141
109
  ## Pre and post hooks
142
110
 
143
- Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
111
+ Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: Nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
144
112
 
145
113
  Extend `nmr build` with a pre-build step for every workspace package:
146
114
 
@@ -159,7 +127,7 @@ export default defineConfig({
159
127
 
160
128
  ## Configuration
161
129
 
162
- Create `.config/nmr.config.ts` in the monorepo root to add or override scripts. A package may carry one too, for [build settings of its own](docs/scripts.md#package-level-configuration):
130
+ Create `.config/nmr.config.ts` in the monorepo root to add or override scripts. A package may have one too, for [build settings of its own](docs/scripts.md#package-level-configuration):
163
131
 
164
132
  ```ts
165
133
  import { defineConfig } from '@williamthorsen/nmr/config';
@@ -178,7 +146,7 @@ export default defineConfig({
178
146
 
179
147
  ## What nmr reports
180
148
 
181
- Every command nmr runs reports one line naming the scope it ran at, the command, the outcome, and the timing:
149
+ Every command that nmr runs reports one line naming the scope at which it ran, the command, the outcome, and the timing:
182
150
 
183
151
  ```console
184
152
  ✅ nmr-core: test: passed in 12.4s
@@ -189,9 +157,9 @@ Every command nmr runs reports one line naming the scope it ran at, the command,
189
157
 
190
158
  **A pipe and a CI log get words rather than emoji.** The same four lines read `PASS`, `FAIL`, `SKIP`, and `NOOP` there, which `grep FAIL` finds. `--output-style` and `NMR_OUTPUT_STYLE` each force either style.
191
159
 
192
- **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 that ran nothing reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed: an override resolving to `""` or `":"`, and a fan-out step that a package-free workspace left nothing to do. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
160
+ **Verdicts print in every verbosity.** `nmr -q <command>` withholds the output of the commands that nmr runs, never nmr's own words: A passing quiet run reports its verdicts and nothing else. A command that ran nothing reports the skip rather than exiting 0 in silence, which separates it from a command that passed: an override resolving to `""` or `":"`, and a fan-out step that a package-free workspace left nothing to do. An `NMR_RUN_IF_PRESENT` miss reports nothing, because it does not have a command to report on.
193
161
 
194
- [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, the output style, and where the verbosity of a run comes from.
162
+ [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, the output style, and what sets the verbosity of a run.
195
163
 
196
164
  ## Check-result cache
197
165
 
@@ -220,7 +188,7 @@ import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
220
188
  export default defineVitestConfig();
221
189
  ```
222
190
 
223
- `@williamthorsen/nmr/tests` exports a check that fails a test run on a test file outside `__tests__` or one that names no tier. [Shared Vitest config](docs/vitest.md) covers the test tiers, that check, what the factory supplies, and customizing it by scope.
191
+ `@williamthorsen/nmr/tests` exports a check that fails a test run on a test file outside `__tests__` or one that does not name a tier. [Shared Vitest config](docs/vitest.md) covers the test tiers, that check, what the factory supplies, and customizing it by scope.
224
192
 
225
193
  ### Shared Prettier config
226
194
 
@@ -241,7 +209,7 @@ export default definePrettierConfig();
241
209
 
242
210
  ### Workspace introspection
243
211
 
244
- Repo-wide tests and scripts often need to know where the monorepo root is, or which directories its workspace packages live in. The `@williamthorsen/nmr/workspace` subpath publishes the two pnpm-workspace lookups nmr uses internally:
212
+ Repo-wide tests and scripts often need to know where the monorepo root is, or which directories its workspace packages live in. The `@williamthorsen/nmr/workspace` subpath publishes the two pnpm-workspace lookups that nmr uses internally:
245
213
 
246
214
  ```ts
247
215
  // __tests__/packages.test.ts
@@ -254,11 +222,11 @@ for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
254
222
  }
255
223
  ```
256
224
 
257
- [Workspace introspection](docs/workspace.md) covers what each lookup returns and where it diverges from pnpm.
225
+ [Workspace introspection](docs/workspace.md) covers what each lookup returns and the cases in which it diverges from pnpm.
258
226
 
259
227
  ## Agent guidance
260
228
 
261
- nmr ships the rules an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
229
+ nmr includes the rules that an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
262
230
 
263
231
  ```yaml
264
232
  packages:
@@ -266,7 +234,7 @@ packages:
266
234
  - '@williamthorsen/nmr'
267
235
  ```
268
236
 
269
- Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it with no second step.
237
+ Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file that each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it without a second step.
270
238
 
271
239
  Wiring the sync to `postinstall` keeps it current without a hand-run command:
272
240
 
@@ -282,7 +250,7 @@ Wiring the sync to `postinstall` keeps it current without a hand-run command:
282
250
 
283
251
  ## Conformance checks
284
252
 
285
- nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the test-file conventions ([tier names](docs/vitest.md#test-tiers) and `__tests__` placement), that the repo's own suite gates them, that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output each wrapper loads, that no root install script runs `lefthook install` without `lefthook check-install`, and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade. A repo that declares the [conventions check](docs/vitest.md#gating-the-test-file-conventions) gates both conventions in its own test run, and the kit then leaves the test files to that check.
253
+ nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the test-file conventions ([tier names](docs/vitest.md#test-tiers) and `__tests__` placement), that the repo's own suite gates them, that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output that each wrapper loads, that the root install scripts do not run `lefthook install` without `lefthook check-install`, and that the `package.json` files in the tree do not declare a `pnpm` field, from which pnpm 11 does not read any key. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit is included in the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr takes effect in the repo when it upgrades nmr. A repo that declares the [conventions check](docs/vitest.md#gating-the-test-file-conventions) gates both conventions in its own test run, and the kit then leaves the test files to that check.
286
254
 
287
255
  Add `readyup` as a devDependency, then name nmr in its config:
288
256
 
@@ -296,20 +264,20 @@ export default defineRdyConfig({
296
264
  ```
297
265
 
298
266
  ```bash
299
- rdy run --packages # every kit each listed package publishes
267
+ rdy run --packages # every kit that each listed package publishes
300
268
  rdy run --from npm:@williamthorsen/nmr # nmr's kit alone, without the config entry
301
269
  rdy list --from npm:@williamthorsen/nmr # what nmr publishes
302
270
  ```
303
271
 
304
- `--packages` is the form that survives nmr publishing further kits. Both need `readyup` 0.23 or later, and `@williamthorsen/nmr` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
272
+ `--packages` is the form that survives nmr publishing further kits. Both need `readyup` 0.23 or later, and `@williamthorsen/nmr` as a _direct_ devDependency: A strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
305
273
 
306
274
  ## Documentation
307
275
 
308
- - [Scripts and configuration](docs/scripts.md): override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
309
- - [What nmr reports](docs/reporting.md): verdicts, `--json`, output style, and verbosity
310
- - [Check-result cache](docs/check-cache.md): what is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
276
+ - [Scripts and configuration](docs/scripts.md): Override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
277
+ - [What nmr reports](docs/reporting.md): Verdicts, `--json`, output style, and verbosity
278
+ - [Check-result cache](docs/check-cache.md): What is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
311
279
  - [Dependency upgrades](docs/upgrades.md): `nmr upgrade`, the upgrade policy, `report-catalog`, `report-overrides`, and `nmr-taze`
312
280
  - [Standalone utilities](docs/utilities.md): `nmr-clean`, `nmr-compile`, `nmr-fmt`, and `ensure-prepublish-hooks`
313
- - [Shared Vitest config](docs/vitest.md): test tiers, test selections, the conventions check, and customization
314
- - [Shared Prettier config](docs/prettier.md): what it formats and how to customize it
315
- - [Workspace introspection](docs/workspace.md): the two workspace lookups
281
+ - [Shared Vitest config](docs/vitest.md): Test tiers, test selections, the conventions check, and customization
282
+ - [Shared Prettier config](docs/prettier.md): What it formats and how to customize it
283
+ - [Workspace introspection](docs/workspace.md): The two workspace lookups
package/agents/README.md CHANGED
@@ -4,21 +4,21 @@ This directory is a CodeAssembly content root, declared by the `codeassembly.con
4
4
 
5
5
  ## The ambient cheatsheet
6
6
 
7
- `guidance/rulebooks/nmr.md` carries `delivery: ambient`, so its body is injected at launch in every consuming repo. Every line is paid for on every task, whether or not it is relevant. It is a cheatsheet, not a manual.
7
+ `guidance/rulebooks/nmr.md` declares `delivery: ambient`, so its body is injected at launch in every consuming repo. Every line consumes launch tokens on every task, whether or not it is relevant. It is a cheatsheet, not a manual.
8
8
 
9
- A line earns a place in it only if both hold:
9
+ A line belongs in it only if both hold:
10
10
 
11
- 1. **It is absent from nmr's own output.** Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
12
- 2. **The obvious action goes wrong without it.** Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
11
+ 1. **It is absent from nmr's own output.** Bare `nmr` already prints the flags, every command, and the shell command to which each resolves; failing checks already name their own fix. Repeating any of that spends launch tokens to say something the agent will be told anyway, at the moment it matters.
12
+ 2. **The obvious action goes wrong without it.** Guidance that prevents a silent mistake belongs in it; guidance that prevents a mistake that the next error message would diagnose does not.
13
13
 
14
- Everything else belongs in the topic document under `packages/nmr/docs/` that covers it, reachable on demand at `node_modules/@williamthorsen/nmr/docs/`, with a pointer from the cheatsheet naming the topic and its document so an agent knows where to look. When adding a feature, document it there and extend that pointer rather than the cheatsheet.
14
+ Everything else belongs in the topic document under `packages/nmr/docs/` that covers it, reachable on demand at `node_modules/@williamthorsen/nmr/docs/`, with a pointer from the cheatsheet naming the topic and its document so that an agent knows where to look. When adding a feature, document it there and extend that pointer rather than the cheatsheet.
15
15
 
16
16
  ## Constraints on rulebook bodies
17
17
 
18
- Rulebook bodies carry no links to other rulebooks. CodeAssembly's renderer rewrites link targets under `skills/` and `scripts/` and rejects everything else, so a cross-reference names the deployed `consult-<slug>` in prose and declares the edge under `dependencies:`.
18
+ Rulebook bodies do not contain any links to other rulebooks. CodeAssembly's renderer rewrites link targets under `skills/` and `scripts/` and rejects everything else, so a cross-reference names the deployed `consult-<slug>` in prose and declares the edge under `dependencies:`.
19
19
 
20
- Anchor-only links are checked against the body they appear in, so a fragment naming a heading that lives in another document fails the run. Check `](#` before moving a section between bodies.
20
+ Because anchor-only links are checked against the body in which they appear, a fragment naming a heading in another document fails the run. Check `](#` before moving a section between bodies.
21
21
 
22
22
  ## Publishing
23
23
 
24
- The directory is published because `files` in `packages/nmr/package.json` lists it. Dropping that entry fails nothing locally, because this repo resolves the guidance through a `workspace:*` self-link that `files` does not govern. `packaging.tool.test.ts` is what catches it.
24
+ The directory is published because `files` in `packages/nmr/package.json` lists it. Dropping that entry fails nothing locally, because this repo resolves the guidance through a `workspace:*` self-link that `files` does not govern. `packaging.tool.test.ts` catches it.
@@ -6,12 +6,12 @@ delivery: ambient
6
6
 
7
7
  # nmr: agent guidance
8
8
 
9
- - Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command it resolves to; a wrong guess reports only `Unknown command`, with no list.
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
- - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
9
+ - Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command to which it resolves; a wrong guess reports only `Unknown command`, without a list.
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
+ - 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 `SKIP <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 -- `PASS <scope>: <command>: passed in 12.4s`, `FAIL … failed in 1.2s (exit 1)`, `SKIP … passed 4m ago on this tree`, or `NOOP … skipped, the override is empty` -- and a composite reports alongside every command it expands into. Those are the words a pipe and a CI log get; a terminal gets ✅, ❌, ⏩, and ⚪ in their place, and `--output-style rich` or `NMR_OUTPUT_STYLE=rich` forces the emoji on any stream. 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
- - Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing the test reaches -- `tool` (a program the environment supplies), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --packages` reports both and warns that the check is missing.
17
- - The rest is in `node_modules/@williamthorsen/nmr/docs/`: `scripts.md` for pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`) and script overrides and their `""`/`":"` skip values; `upgrades.md` for `nmr upgrade` ceilings (report-only until `nmr upgrade --write`); `utilities.md` for the `nmr-compile` build (there is no repo-local build script); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; and `check-cache.md` for 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.
13
+ - A check that already passed on the current working tree skips, printing `SKIP <scope>: <command>: passed …` and, when the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it. Read the replayed excerpt rather than running the check again: Never pass `--no-cache` to get a "real" run, which takes 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 when there are too many to fit. A skip without an 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. Output that you need in full is a recording to read rather than a command to run again. It runs nothing, and it is held to the tree rather than to the terminal: A recording made by a piped run prints for a reader at one. A composite prints its constituents' excerpts, not their transcripts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. When there is nothing to show, `--log` says so on one line, names which case it is, and exits non-zero.
15
+ - Every command that nmr runs reports one line -- `PASS <scope>: <command>: passed in 12.4s`, `FAIL … failed in 1.2s (exit 1)`, `SKIP … passed 4m ago on this tree`, or `NOOP … skipped, the override is empty` -- and a composite reports alongside every command into which it expands. Those are the words that a pipe and a CI log get; a terminal gets ✅, ❌, ⏩, and ⚪ in their place, and `--output-style rich` or `NMR_OUTPUT_STYLE=rich` forces the emoji on any stream. The lines print in every verbosity, so `nmr -q <command>` is the low-noise way to run rather than a silent one: It withholds the output of the commands that nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. nmr reads its own flags only before the command name and passes everything after the name to the commands that run: `nmr ci -q` hands `-q` to `nmr-fmt`, which rejects it. 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
+ - Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing that the test reaches -- `tool` (a program supplied by the environment), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --packages` reports both and warns that the check is missing.
17
+ - The rest is in `node_modules/@williamthorsen/nmr/docs/`: `scripts.md` for pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`) and script overrides and their `""`/`":"` skip values; `upgrades.md` for `nmr upgrade` ceilings (report-only until `nmr upgrade --write`); `utilities.md` for the `nmr-compile` build (the repo does not define a build script of its own); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; and `check-cache.md` for the check-result cache's key, the retention key to which a replayed excerpt is additionally held, the retention ceiling within which `--log` prints, its `checkCache` config, and every condition that disables it.
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-ensure-prepublish-hooks.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('ensure-prepublish-hooks: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('ensure-prepublish-hooks: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`ensure-prepublish-hooks: failed to load: ${error.message}\n`);
9
9
  }
package/bin/nmr-clean.js CHANGED
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-clean.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-clean: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr-clean: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-clean: failed to load: ${error.message}\n`);
9
9
  }
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-build.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-compile: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr-compile: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-compile: failed to load: ${error.message}\n`);
9
9
  }
package/bin/nmr-fmt.js CHANGED
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-fmt.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-fmt: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr-fmt: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-fmt: failed to load: ${error.message}\n`);
9
9
  }
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-report-catalog.js');
4
4
  } catch (error) {
5
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');
6
+ process.stderr.write('nmr-report-catalog: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-report-catalog: failed to load: ${error.message}\n`);
9
9
  }
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-report-overrides.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-report-overrides: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr-report-overrides: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-report-overrides: failed to load: ${error.message}\n`);
9
9
  }
package/bin/nmr-taze.js CHANGED
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli-taze.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-taze: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr-taze: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr-taze: failed to load: ${error.message}\n`);
9
9
  }
package/bin/nmr.js CHANGED
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/cli.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr: build output not found — run `pnpm run bootstrap` first\n');
6
+ process.stderr.write('nmr: build output not found; run `pnpm run bootstrap` first\n');
7
7
  } else {
8
8
  process.stderr.write(`nmr: failed to load: ${error.message}\n`);
9
9
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * A failure in what the user declared -- a config file, a `package.json` -- rather than a fault in nmr.
3
3
  *
4
- * The CLI boundary prints one of these as its message alone. A stack trace records where nmr noticed the
5
- * problem, which says nothing about the file the reader has to edit.
4
+ * The CLI boundary prints one of these as its message alone.
6
5
  */
7
6
  export declare class UserError extends Error {
8
7
  constructor(message: string);