@williamthorsen/nmr 0.10.0 → 0.12.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.
package/AGENTS.md ADDED
@@ -0,0 +1,41 @@
1
+ ---
2
+ source: '@williamthorsen/nmr@0.0.0-source'
3
+ ---
4
+
5
+ # nmr: agent guidance
6
+
7
+ This file is managed by `@williamthorsen/nmr`. Do not edit — re-run `pnpm exec nmr sync-agent-files` after an nmr upgrade to refresh it.
8
+
9
+ ## Discover scripts by running nmr
10
+
11
+ Run `nmr` with no command (from the monorepo root or any workspace package) to list every available script, including composite expansions and resolved shell commands. Check this before guessing a script name from another repo — the registry is authoritative.
12
+
13
+ ## Invocation rules
14
+
15
+ - Use `nmr <command>` for anything nmr provides. Do not use `pnpm run <command>`.
16
+ - Use `pnpm exec nmr`, not `npx nmr`. Inside git worktrees, `npx` can resolve a different nmr binary from outside the working tree.
17
+ - If `nmr` itself fails to run (fresh clone, missing build output), run `pnpm run bootstrap` from the repo root first.
18
+
19
+ ## Root vs. workspace context
20
+
21
+ nmr walks up to find `pnpm-workspace.yaml`, then decides which registry to use based on whether your cwd is inside a workspace package. The same command name (e.g. `build`, `test`, `check:strict`) often exists in both registries with different behavior — the root version typically delegates across all workspaces. Use `-w` to force the root registry from inside a package dir, and `-F <pkg>` to run a single package's script from anywhere.
22
+
23
+ ## Composite scripts
24
+
25
+ A script value shown in `nmr` output as `[a, b, c]` is a composite: it runs `nmr a && nmr b && nmr c`, stopping at the first failure. Composites can reference other composites.
26
+
27
+ ## `root:*` siblings
28
+
29
+ Some root scripts (e.g. `lint`, `typecheck`, `test`) expand to `nmr root:X && pnpm --recursive exec nmr X`. The `root:X` variant runs only against root-level files; the plain name runs everywhere. Use `root:X` directly when you want to isolate a failure to the root code.
30
+
31
+ ## Override behaviors
32
+
33
+ In `.config/nmr.config.ts` or a package's `package.json`, override values have special semantics:
34
+
35
+ - `""` (empty string) — skip the script with a "Skipping" message; exit 0.
36
+ - `":"` — no-op; exit 0. Prefer this over `""` if your repo enforces non-empty script values.
37
+ - Any other string — runs in place of the default.
38
+
39
+ ## Agent-file sync
40
+
41
+ The presence and version stamp of `.agents/nmr/AGENTS.md` is verified by `check:agent-files`, which is part of the default root `check:strict` composite. If it fails, run `nmr sync-agent-files`.
package/README.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
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
+ <!-- section:release-notes -->
6
+ ## Release notes — v0.12.0 (2026-04-23)
7
+
8
+ ### Features
9
+
10
+ - Add agent-facing AGENTS.md and sync-agent-files command (#263)
11
+
12
+ Ships nmr-owned agent guidance alongside the `@williamthorsen/nmr` package so consuming repos stop hand-maintaining their own copies of the runner's invocation rules. A new `nmr sync-agent-files` command pulls that guidance into `.agents/nmr/AGENTS.md` in the consuming repo, stamped with the installed nmr version; a companion `--check` variant verifies the stamp against the installed version on every root `check:strict` run. Drift between installed nmr and the committed guidance now fails the quality gate automatically with a single actionable fix message, without per-consumer wiring.
13
+
14
+ - Rename check:fixable script to fix:check (#266)
15
+
16
+ Renames the `check:fixable` convenience script to `fix:check` in both the workspace and root default script registries. The new name mirrors the existing `fmt` / `fmt:check` pattern, aligning the read-only variant with its mutating counterpart (`fix`) so the command's read-only semantics are recognizable from the name alone.
17
+
18
+ The script's expansion (`fmt:check`, `lint:check`) is unchanged.
19
+ <!-- /section:release-notes -->
20
+
5
21
  ## Installation
6
22
 
7
23
  ```bash
@@ -137,6 +153,7 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
137
153
  | `clean` | `pnpm exec rimraf dist/*` |
138
154
  | `compile` | `tsx ../../config/build.ts` |
139
155
  | `fix` | `lint`, `fmt` |
156
+ | `fix:check` | `fmt:check`, `lint:check` |
140
157
  | `fmt` | `prettier --list-different --write .` |
141
158
  | `fmt:check` | `prettier --check .` |
142
159
  | `generate-typings` | `tsc --project tsconfig.generate-typings.json` |
@@ -172,10 +189,17 @@ Packages with a `vitest.integration.config.ts` file get different test commands.
172
189
 
173
190
  #### Check and quality
174
191
 
175
- | Command | Runs |
176
- | -------------- | ----------------------------------------------------------------- |
177
- | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
178
- | `check:strict` | `typecheck`, `fmt:check`, `audit`, `lint:strict`, `test:coverage` |
192
+ | Command | Runs |
193
+ | ------------------- | ----------------------------------------------------------------------------- |
194
+ | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
195
+ | `check:agent-files` | `nmr-sync-agent-files --check` |
196
+ | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
197
+
198
+ #### Fix
199
+
200
+ | Command | Runs |
201
+ | ----------- | ------------------------- |
202
+ | `fix:check` | `fmt:check`, `lint:check` |
179
203
 
180
204
  #### Test
181
205
 
@@ -210,11 +234,11 @@ Packages with a `vitest.integration.config.ts` file get different test commands.
210
234
 
211
235
  #### Audit
212
236
 
213
- | Command | Runs |
214
- | ------------ | ------------------------------------------------------------------ |
215
- | `audit` | `audit:prod`, `audit:dev` |
216
- | `audit:dev` | `pnpm dlx audit-ci@^6 --config .config/audit-ci/config.dev.json5` |
217
- | `audit:prod` | `pnpm dlx audit-ci@^6 --config .config/audit-ci/config.prod.json5` |
237
+ | Command | Runs |
238
+ | ------------ | ----------------------------- |
239
+ | `audit` | `audit:prod`, `audit:dev` |
240
+ | `audit:dev` | `pnpm exec audit-deps --dev` |
241
+ | `audit:prod` | `pnpm exec audit-deps --prod` |
218
242
 
219
243
  #### Dependencies
220
244
 
@@ -243,6 +267,7 @@ These scripts operate on root-level code only (not workspace packages):
243
267
  | Command | Runs |
244
268
  | ------------------- | ----------------------- |
245
269
  | `report-overrides` | `nmr-report-overrides` |
270
+ | `sync-agent-files` | `nmr-sync-agent-files` |
246
271
  | `sync-pnpm-version` | `nmr-sync-pnpm-version` |
247
272
 
248
273
  ## CLI reference
@@ -294,6 +319,25 @@ Report any active `pnpm.overrides` in the root `package.json`. Useful as a `post
294
319
  nmr report-overrides
295
320
  ```
296
321
 
322
+ ### `sync-agent-files`
323
+
324
+ Sync the agent-facing guidance shipped with nmr into the consuming repo.
325
+
326
+ ```bash
327
+ nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
328
+ nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
329
+ ```
330
+
331
+ Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
332
+
333
+ The default root `check:strict` composite includes `check:agent-files`, which runs `--check` automatically — so any CI pipeline already running `check:strict` catches drift without per-consumer wiring.
334
+
335
+ To expose the synced guidance to Claude Code sessions, add this include to the consuming repo's `.agents/PROJECT.md`:
336
+
337
+ ```markdown
338
+ @.agents/nmr/AGENTS.md
339
+ ```
340
+
297
341
  ### `sync-pnpm-version`
298
342
 
299
343
  Synchronize the pnpm version from the root `package.json` `packageManager` field into the GitHub `code-quality.yaml` workflow file.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ try {
3
+ await import('../dist/esm/cli-sync-agent-files.js');
4
+ } catch (error) {
5
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
+ process.stderr.write('nmr-sync-agent-files: build output not found — run `pnpm run build` first\n');
7
+ } else {
8
+ process.stderr.write(`nmr-sync-agent-files: failed to load: ${error.message}\n`);
9
+ }
10
+ process.exit(1);
11
+ }
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- ef769bb7d525506880d297aa0dceea5565adc3c06e0073b219032e0c58a64af4
1
+ d28e365779e353583b02eb587a2dfe64797c3a00544b42b8b18922aff2959d34
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { check, sync } from "./commands/sync-agent-files.js";
2
+ import { findMonorepoRoot } from "./context.js";
3
+ function parseArgs(argv) {
4
+ const args = argv.slice(2);
5
+ if (args.length === 0) return { mode: "sync" };
6
+ if (args.length === 1 && args[0] === "--check") return { mode: "check" };
7
+ return { error: `Usage: nmr sync-agent-files [--check]` };
8
+ }
9
+ try {
10
+ const parsed = parseArgs(process.argv);
11
+ if ("error" in parsed) {
12
+ console.error(parsed.error);
13
+ process.exit(1);
14
+ }
15
+ const monorepoRoot = findMonorepoRoot();
16
+ if (parsed.mode === "sync") {
17
+ const { written, stamp } = sync(monorepoRoot);
18
+ console.info(`\u2713 Wrote ${written} (${stamp})`);
19
+ process.exit(0);
20
+ }
21
+ const result = check(monorepoRoot);
22
+ if (result.ok) {
23
+ console.info(`\u2713 .agents/nmr/AGENTS.md is in sync (${result.stamp})`);
24
+ process.exit(0);
25
+ }
26
+ console.error(result.reason);
27
+ process.exit(1);
28
+ } catch (error) {
29
+ console.error(error instanceof Error ? error.message : String(error));
30
+ process.exit(1);
31
+ }
package/dist/esm/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import process from "node:process";
2
3
  import { resolveContext } from "./context.js";
3
4
  import { generateHelp } from "./help.js";
@@ -105,14 +106,21 @@ async function main() {
105
106
  console.error(`Unknown command: ${command}`);
106
107
  process.exit(1);
107
108
  }
109
+ const packageName = path.basename(packageDir);
108
110
  if (resolved.command === "") {
109
111
  if (!parsed.quiet) {
110
- console.info("Override script is defined but empty. Skipping.");
112
+ console.info(`\u26D4 ${packageName}: Override script is defined but empty. Skipping.`);
113
+ }
114
+ process.exit(0);
115
+ }
116
+ if (resolved.command === ":") {
117
+ if (!parsed.quiet) {
118
+ console.info(`\u26D4 ${packageName}: Override script is a no-op. Skipping.`);
111
119
  }
112
120
  process.exit(0);
113
121
  }
114
122
  if (resolved.source === "package" && !parsed.quiet && registry[command] !== void 0) {
115
- console.info(`Using override script: ${resolved.command}`);
123
+ console.info(`\u{1F4E6} ${packageName}: Using override script: ${resolved.command}`);
116
124
  }
117
125
  const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
118
126
  const fullCommand = substitutedCommand + passthrough;
@@ -0,0 +1,14 @@
1
+ export declare function parseSourceStamp(content: string): string | null;
2
+ export interface SyncResult {
3
+ written: string;
4
+ stamp: string;
5
+ }
6
+ export declare function sync(destinationDir: string): SyncResult;
7
+ export type CheckResult = {
8
+ ok: true;
9
+ stamp: string;
10
+ } | {
11
+ ok: false;
12
+ reason: string;
13
+ };
14
+ export declare function check(destinationDir: string): CheckResult;
@@ -0,0 +1,77 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { VERSION } from "../version.js";
5
+ const PACKAGE_NAME = "@williamthorsen/nmr";
6
+ const DESTINATION_RELATIVE_PATH = ".agents/nmr/AGENTS.md";
7
+ const SOURCE_FILENAME = "AGENTS.md";
8
+ function getSourcePath() {
9
+ let dir = path.dirname(fileURLToPath(import.meta.url));
10
+ while (dir !== path.dirname(dir)) {
11
+ const candidate = path.join(dir, SOURCE_FILENAME);
12
+ if (existsSync(candidate)) return candidate;
13
+ dir = path.dirname(dir);
14
+ }
15
+ throw new Error(`Could not locate ${SOURCE_FILENAME} in any parent of ${fileURLToPath(import.meta.url)}`);
16
+ }
17
+ function getDestinationPath(destinationDir) {
18
+ return path.join(destinationDir, DESTINATION_RELATIVE_PATH);
19
+ }
20
+ function currentSourceStamp() {
21
+ return `${PACKAGE_NAME}@${VERSION}`;
22
+ }
23
+ const FRONTMATTER_REGEX = /^---\n((?:.*\n)*?)---\n/;
24
+ function stripFrontmatter(content) {
25
+ return content.replace(FRONTMATTER_REGEX, "");
26
+ }
27
+ function parseSourceStamp(content) {
28
+ const frontmatterMatch = FRONTMATTER_REGEX.exec(content);
29
+ if (!frontmatterMatch) return null;
30
+ const frontmatterBody = frontmatterMatch[1] ?? "";
31
+ const sourceMatch = /^source:\s*['"]([^'"]+)['"]\s*$/m.exec(frontmatterBody);
32
+ return sourceMatch?.[1] ?? null;
33
+ }
34
+ function sync(destinationDir) {
35
+ const sourcePath = getSourcePath();
36
+ const sourceContent = readFileSync(sourcePath, "utf8");
37
+ const body = stripFrontmatter(sourceContent);
38
+ const stamp = currentSourceStamp();
39
+ const output = `---
40
+ source: '${stamp}'
41
+ ---
42
+ ${body}`;
43
+ const destinationPath = getDestinationPath(destinationDir);
44
+ mkdirSync(path.dirname(destinationPath), { recursive: true });
45
+ writeFileSync(destinationPath, output, "utf8");
46
+ return { written: destinationPath, stamp };
47
+ }
48
+ function check(destinationDir) {
49
+ const destinationPath = getDestinationPath(destinationDir);
50
+ const expected = currentSourceStamp();
51
+ if (!existsSync(destinationPath)) {
52
+ return {
53
+ ok: false,
54
+ reason: `${DESTINATION_RELATIVE_PATH} is missing. Run \`nmr sync-agent-files\`.`
55
+ };
56
+ }
57
+ const content = readFileSync(destinationPath, "utf8");
58
+ const found = parseSourceStamp(content);
59
+ if (found === null) {
60
+ return {
61
+ ok: false,
62
+ reason: `Cannot parse version stamp in ${DESTINATION_RELATIVE_PATH}. Run \`nmr sync-agent-files\`.`
63
+ };
64
+ }
65
+ if (found !== expected) {
66
+ return {
67
+ ok: false,
68
+ reason: `${DESTINATION_RELATIVE_PATH} is out of sync (file: ${found}, installed: ${expected}). Run \`nmr sync-agent-files\`.`
69
+ };
70
+ }
71
+ return { ok: true, stamp: expected };
72
+ }
73
+ export {
74
+ check,
75
+ parseSourceStamp,
76
+ sync
77
+ };
@@ -5,6 +5,7 @@ const commonWorkspaceScripts = {
5
5
  clean: "pnpm exec rimraf dist/*",
6
6
  compile: "tsx ../../config/build.ts",
7
7
  fix: ["lint", "fmt"],
8
+ "fix:check": ["fmt:check", "lint:check"],
8
9
  fmt: "prettier --list-different --write .",
9
10
  "fmt:check": "prettier --check .",
10
11
  "generate-typings": "tsc --project tsconfig.generate-typings.json",
@@ -27,14 +28,16 @@ const standardTestScripts = {
27
28
  };
28
29
  const rootScripts = {
29
30
  audit: ["audit:prod", "audit:dev"],
30
- "audit:dev": "pnpm dlx audit-ci@^7 --config .config/audit-ci/config.dev.json5",
31
- "audit:prod": "pnpm dlx audit-ci@^7 --config .config/audit-ci/config.prod.json5",
31
+ "audit:dev": "pnpm exec audit-deps --dev",
32
+ "audit:prod": "pnpm exec audit-deps --prod",
32
33
  build: "pnpm --recursive exec nmr build",
33
34
  check: ["typecheck", "fmt:check", "lint:check", "test"],
34
- "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
35
+ "check:agent-files": "nmr-sync-agent-files --check",
36
+ "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage", "check:agent-files"],
35
37
  ci: ["build", "check:strict", "audit"],
36
38
  clean: "pnpm --recursive exec nmr clean",
37
39
  fix: ["lint", "fmt"],
40
+ "fix:check": ["fmt:check", "lint:check"],
38
41
  fmt: `sh -c 'prettier --list-different --write "\${@:-.}"' --`,
39
42
  "fmt:all": ["fmt", "fmt:sh"],
40
43
  "fmt:check": `sh -c 'prettier --check "\${@:-.}"' --`,
@@ -51,6 +54,7 @@ const rootScripts = {
51
54
  "root:lint:strict": "strict-lint --ignore-pattern 'packages/**' .",
52
55
  "root:test": "vitest --config ./vitest.root.config.ts",
53
56
  "root:typecheck": "tsgo --noEmit",
57
+ "sync-agent-files": "nmr-sync-agent-files",
54
58
  "sync-pnpm-version": "nmr-sync-pnpm-version",
55
59
  test: "nmr root:test && pnpm --recursive exec nmr test",
56
60
  "test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
@@ -1 +1 @@
1
- export declare const VERSION = "0.10.0";
1
+ export declare const VERSION = "0.12.0";
@@ -1,4 +1,4 @@
1
- const VERSION = "0.10.0";
1
+ const VERSION = "0.12.0";
2
2
  export {
3
3
  VERSION
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for PNPM monorepos",
6
6
  "keywords": [
@@ -30,9 +30,11 @@
30
30
  "ensure-prepublish-hooks": "bin/ensure-prepublish-hooks.js",
31
31
  "nmr": "bin/nmr.js",
32
32
  "nmr-report-overrides": "bin/nmr-report-overrides.js",
33
+ "nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
33
34
  "nmr-sync-pnpm-version": "bin/nmr-sync-pnpm-version.js"
34
35
  },
35
36
  "files": [
37
+ "AGENTS.md",
36
38
  "bin",
37
39
  "dist"
38
40
  ],