From 2a241711184d5f58438237ed60540ae37513f237 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 15:08:19 -0700 Subject: [PATCH 1/7] tooling: Stop compiling workspace packages during pnpm install `pnpm install` no longer compiles workspace packages; building is reached through `nmr build` alone. The removed `prepare` hooks ran concurrently with pnpm's own bin linking and intermittently invoked a `.bin` shim before it was executable, failing the install with `sh: 1: nmr: Permission denied`. --- packages/kb/package.json | 3 +-- packages/lifecycle/package.json | 3 +-- packages/mcp/package.json | 3 +-- packages/run-core/package.json | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/kb/package.json b/packages/kb/package.json index 79454d5c..a65b64c8 100644 --- a/packages/kb/package.json +++ b/packages/kb/package.json @@ -76,8 +76,7 @@ "!dist/esm/test-utils" ], "scripts": { - "build": "nmr compile", - "prepare": "nmr compile" + "build": "nmr compile" }, "dependencies": { "picomatch": "4.0.5", diff --git a/packages/lifecycle/package.json b/packages/lifecycle/package.json index fa25b811..f259134d 100644 --- a/packages/lifecycle/package.json +++ b/packages/lifecycle/package.json @@ -16,8 +16,7 @@ "dist" ], "scripts": { - "build": "nmr compile", - "prepare": "nmr compile" + "build": "nmr compile" }, "devDependencies": { "esbuild": "0.28.1" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index bb8b5b31..0a5b8093 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -12,8 +12,7 @@ "dist" ], "scripts": { - "build": "nmr compile", - "prepare": "nmr compile" + "build": "nmr compile" }, "dependencies": { "@codeassembly/run-core": "workspace:*", diff --git a/packages/run-core/package.json b/packages/run-core/package.json index 113699fc..8d0731de 100644 --- a/packages/run-core/package.json +++ b/packages/run-core/package.json @@ -35,8 +35,7 @@ "dist" ], "scripts": { - "build": "nmr compile", - "prepare": "nmr compile" + "build": "nmr compile" }, "dependencies": { "yaml": "2.9.0", From 9cfeb338f73a771b1efc034f805897ea265d8c9e Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 15:09:09 -0700 Subject: [PATCH 2/7] root|tooling: Add a bootstrap script and remove the postinstall hook `pnpm run bootstrap` builds every package, giving a fresh clone or worktree one command to reach a usable state after `pnpm install`. The root `postinstall` hook is gone. Its `pnpm.overrides` warning is moving into nmr's default check composites (williamthorsen/node-monorepo-tools#511), where it reaches every repo rather than this one. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 01613896..683ae4a3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "scripts": { "agents:install": "tsx packages/agents/src/cli.ts install", "agents:install-launchers": "bash packages/agents/scripts/install-launchers.sh", + "bootstrap": "nmr build", "ci:post": "pnpm --filter agents run test:sh", "prepare": "lefthook install" }, From 4c57b6c69886a5d706f6d27a19c05f4c8e46b154 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 15:09:46 -0700 Subject: [PATCH 3/7] root|tooling: Route worktree creation, CI, and the README at bootstrap Worktree creation, the CI job, and the documented clone sequence all reach a built tree through `pnpm run bootstrap`. The build now runs in CI's own `Bootstrap` step rather than inside the check command, so a build break turns that step red instead of surfacing as a check failure. Worktrunk gates changed hook commands behind approval, so the first worktree created after this lands prompts once for `pnpm run bootstrap`. --- .config/wt.toml | 2 +- .github/workflows/code-quality.yaml | 2 +- README.md | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.config/wt.toml b/.config/wt.toml index 916fc033..1b4a2750 100644 --- a/.config/wt.toml +++ b/.config/wt.toml @@ -3,4 +3,4 @@ [post-create] direnv = "direnv allow" deps = "pnpm install" -build = "nmr build" +bootstrap = "pnpm run bootstrap" diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index e5bf20ea..2a964469 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -25,4 +25,4 @@ jobs: code-quality: uses: williamthorsen/.github/.github/workflows/code-quality-pnpm-workflow.yaml@v7 with: - check-command: pnpm exec nmr build && pnpm exec nmr check:strict + check-command: pnpm exec nmr check:strict diff --git a/README.md b/README.md index 6b49658a..396a3988 100644 --- a/README.md +++ b/README.md @@ -10,5 +10,6 @@ See [`.agents/PROJECT.md`](.agents/PROJECT.md) for project structure, commands, ```bash pnpm install -nmr check # Run all checks (typecheck, format, lint, test) +pnpm run bootstrap # Build every package; required before running the MCP server or CLI bins +nmr check # Run all checks (typecheck, format, lint, test) ``` From ade8f44305e5759b07ba82f5862ad2c83c56dc93 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 15:11:02 -0700 Subject: [PATCH 4/7] tooling: Point the build-missing hint at the bootstrap script A bin invoked before the build now names a command that exists. `codeassembly-agents`, `codeassembly-mcp`, `codeassembly-runs`, and `kb` told the reader to run `pnpm run build`, which the repository root does not define, so following the advice failed. `.agents/PROJECT.md` and the run-core README document `pnpm run bootstrap` as the step between install and a usable tree. --- .agents/PROJECT.md | 1 + packages/agents/bin/codeassembly-agents.js | 2 +- packages/kb/bin/kb.js | 2 +- packages/mcp/bin/codeassembly-mcp.js | 2 +- packages/run-core/README.md | 4 ++-- packages/run-core/bin/codeassembly-runs.js | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.agents/PROJECT.md b/.agents/PROJECT.md index 94bbed88..9f7057c2 100644 --- a/.agents/PROJECT.md +++ b/.agents/PROJECT.md @@ -214,6 +214,7 @@ The package README documents the `kb.yaml` configuration schema and merge semant **Root-level development (via `@williamthorsen/nmr`):** - `pnpm install` - Install all dependencies +- `pnpm run bootstrap` - Build every package; required after install before the MCP server or CLI bins will run - `nmr check` - Run typecheck, format check, lint check, and tests - `nmr check:strict` - Strict checks including coverage and audit - `nmr ci` - Full CI pipeline (strict checks + build) diff --git a/packages/agents/bin/codeassembly-agents.js b/packages/agents/bin/codeassembly-agents.js index 02293471..92ea0c71 100755 --- a/packages/agents/bin/codeassembly-agents.js +++ b/packages/agents/bin/codeassembly-agents.js @@ -11,7 +11,7 @@ const entryPoint = new URL('../dist/esm/cli.js', import.meta.url); // module in the graph, so keying the build-first message off the error code would also // fire when the build is present and one of its imports is missing. if (!existsSync(entryPoint)) { - process.stderr.write('codeassembly-agents: build output not found — run `pnpm run build` first\n'); + process.stderr.write('codeassembly-agents: build output not found — run `pnpm run bootstrap` first\n'); process.exit(1); } diff --git a/packages/kb/bin/kb.js b/packages/kb/bin/kb.js index 2824bd3d..a9b49a07 100755 --- a/packages/kb/bin/kb.js +++ b/packages/kb/bin/kb.js @@ -11,7 +11,7 @@ const entryPoint = new URL('../dist/esm/cli/index.js', import.meta.url); // module in the graph, so keying the build-first message off the error code would also // fire when the build is present and one of its imports is missing. if (!existsSync(entryPoint)) { - process.stderr.write('kb: build output not found — run `pnpm run build` first\n'); + process.stderr.write('kb: build output not found — run `pnpm run bootstrap` first\n'); process.exit(1); } diff --git a/packages/mcp/bin/codeassembly-mcp.js b/packages/mcp/bin/codeassembly-mcp.js index fd7e0a62..8abe44a0 100755 --- a/packages/mcp/bin/codeassembly-mcp.js +++ b/packages/mcp/bin/codeassembly-mcp.js @@ -11,7 +11,7 @@ const entryPoint = new URL('../dist/esm/cli.js', import.meta.url); // module in the graph, so keying the build-first message off the error code would also // fire when the build is present and one of its imports is missing. if (!existsSync(entryPoint)) { - process.stderr.write('codeassembly-mcp: build output not found — run `pnpm run build` first\n'); + process.stderr.write('codeassembly-mcp: build output not found — run `pnpm run bootstrap` first\n'); process.exit(1); } diff --git a/packages/run-core/README.md b/packages/run-core/README.md index a5a53950..484fdeb3 100644 --- a/packages/run-core/README.md +++ b/packages/run-core/README.md @@ -29,8 +29,8 @@ codeassembly-runs --path # override the base projects directory ### Bin wrapper pattern -The `bin` field in `package.json` points to `bin/codeassembly-runs.js`, a committed wrapper script that dynamically imports the build output at runtime. Do not point `bin` entries directly into `dist/` — pnpm creates bin symlinks during install, before lifecycle scripts like `prepare` run, so the target won't exist in a fresh worktree and `pnpm install` will emit confusing "Failed to create bin" warnings. +The `bin` field in `package.json` points to `bin/codeassembly-runs.js`, a committed wrapper script that dynamically imports the build output at runtime. Do not point `bin` entries directly into `dist/` — pnpm creates bin symlinks during install, and nothing compiles until `pnpm run bootstrap` runs afterward, so the target won't exist in a fresh worktree and `pnpm install` will emit confusing "Failed to create bin" warnings. -If invoked before building, the wrapper finds the entry file absent and tells the user to run `pnpm run build`. It checks for the file directly rather than keying off `ERR_MODULE_NOT_FOUND`, which Node raises for any unresolved module in the graph — including a missing dependency of a build output that is present, where advising a rebuild would be wrong. Any other load failure is reported verbatim. +If invoked before building, the wrapper finds the entry file absent and tells the user to run `pnpm run bootstrap`. It checks for the file directly rather than keying off `ERR_MODULE_NOT_FOUND`, which Node raises for any unresolved module in the graph — including a missing dependency of a build output that is present, where advising a rebuild would be wrong. Any other load failure is reported verbatim. Any new `bin` entry in this monorepo should follow the same pattern. See `bin/codeassembly-runs.js` for the template, and the `@williamthorsen/node-monorepo-tools` packages for the original rationale. diff --git a/packages/run-core/bin/codeassembly-runs.js b/packages/run-core/bin/codeassembly-runs.js index 6bb21c80..115b2222 100755 --- a/packages/run-core/bin/codeassembly-runs.js +++ b/packages/run-core/bin/codeassembly-runs.js @@ -11,7 +11,7 @@ const entryPoint = new URL('../dist/esm/cli.js', import.meta.url); // module in the graph, so keying the build-first message off the error code would also // fire when the build is present and one of its imports is missing. if (!existsSync(entryPoint)) { - process.stderr.write('codeassembly-runs: build output not found — run `pnpm run build` first\n'); + process.stderr.write('codeassembly-runs: build output not found — run `pnpm run bootstrap` first\n'); process.exit(1); } From e2199c8a3c6fdc78154adbfb15587e158660b120 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 16:05:38 -0700 Subject: [PATCH 5/7] mcp|fix: Start the MCP server by resolving SDK subpaths under Node `codeassembly-mcp` starts again. It failed with `ERR_MODULE_NOT_FOUND` on `@modelcontextprotocol/sdk/server/stdio`, leaving the server unavailable to any client launching it. The SDK maps unlisted subpaths through a `"./*"` export that appends no extension, so `server/stdio`, `server/mcp`, `client/stdio`, and `inMemory` need explicit `.js`. Vite resolves the extensionless form and Node does not, so the test suite exercised these imports without observing the failure. --- packages/mcp/src/__tests__/protocol-stdio.test.ts | 2 +- packages/mcp/src/__tests__/protocol.test.ts | 2 +- packages/mcp/src/__tests__/staleness.test.ts | 2 +- packages/mcp/src/cli.ts | 2 +- packages/mcp/src/server.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mcp/src/__tests__/protocol-stdio.test.ts b/packages/mcp/src/__tests__/protocol-stdio.test.ts index abf3219b..9740085c 100644 --- a/packages/mcp/src/__tests__/protocol-stdio.test.ts +++ b/packages/mcp/src/__tests__/protocol-stdio.test.ts @@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Client } from '@modelcontextprotocol/sdk/client'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { describe, expect, it } from 'vitest'; import { isErrorResult, parseAndGetString } from './helpers.ts'; diff --git a/packages/mcp/src/__tests__/protocol.test.ts b/packages/mcp/src/__tests__/protocol.test.ts index 72119c11..3e703c9b 100644 --- a/packages/mcp/src/__tests__/protocol.test.ts +++ b/packages/mcp/src/__tests__/protocol.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { v3RunIndexSchema } from '@codeassembly/run-core'; import { Client } from '@modelcontextprotocol/sdk/client'; -import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { afterEach, describe, expect, it } from 'vitest'; import { createServer } from '../server.ts'; diff --git a/packages/mcp/src/__tests__/staleness.test.ts b/packages/mcp/src/__tests__/staleness.test.ts index f0a97e33..b3defe0f 100644 --- a/packages/mcp/src/__tests__/staleness.test.ts +++ b/packages/mcp/src/__tests__/staleness.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { Client } from '@modelcontextprotocol/sdk/client'; -import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { isBuildStale } from '../staleness.ts'; diff --git a/packages/mcp/src/cli.ts b/packages/mcp/src/cli.ts index d24a41e9..bde01877 100644 --- a/packages/mcp/src/cli.ts +++ b/packages/mcp/src/cli.ts @@ -1,6 +1,6 @@ import process from 'node:process'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { createServer } from './server.ts'; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 8d1ab5ff..dd2377a9 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -1,4 +1,4 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { isBuildStale } from './staleness.ts'; From beb80c97df31690f4758130d71495bf36d7f09a0 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 21:49:07 -0700 Subject: [PATCH 6/7] root|docs: Give the quick start a check command that runs without direnv The quick start's check step now runs on a fresh clone. It named the bare `nmr` binary, which resolves only after direnv is configured, so a reader running the three commands in order hit `command not found` on the third. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 396a3988..5c4aa6a5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,6 @@ See [`.agents/PROJECT.md`](.agents/PROJECT.md) for project structure, commands, ```bash pnpm install -pnpm run bootstrap # Build every package; required before running the MCP server or CLI bins -nmr check # Run all checks (typecheck, format, lint, test) +pnpm run bootstrap # Build every package; required before running the MCP server or CLI bins +pnpm exec nmr check # Run all checks (typecheck, format, lint, test) ``` From 3b26758c6706d178f0f6de52e2b63db4c354cfe7 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 21:56:54 -0700 Subject: [PATCH 7/7] run-core|docs: Reduce the bin-wrapper section to its actionable rules The bin-wrapper section now carries only what a new `bin` entry must honor and the pointer to the template. How the wrapper behaves when the build output is missing is documented on the wrapper itself, which every new bin copies. --- packages/run-core/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/run-core/README.md b/packages/run-core/README.md index 484fdeb3..cafa0a8c 100644 --- a/packages/run-core/README.md +++ b/packages/run-core/README.md @@ -31,6 +31,4 @@ codeassembly-runs --path # override the base projects directory The `bin` field in `package.json` points to `bin/codeassembly-runs.js`, a committed wrapper script that dynamically imports the build output at runtime. Do not point `bin` entries directly into `dist/` — pnpm creates bin symlinks during install, and nothing compiles until `pnpm run bootstrap` runs afterward, so the target won't exist in a fresh worktree and `pnpm install` will emit confusing "Failed to create bin" warnings. -If invoked before building, the wrapper finds the entry file absent and tells the user to run `pnpm run bootstrap`. It checks for the file directly rather than keying off `ERR_MODULE_NOT_FOUND`, which Node raises for any unresolved module in the graph — including a missing dependency of a build output that is present, where advising a rebuild would be wrong. Any other load failure is reported verbatim. - Any new `bin` entry in this monorepo should follow the same pattern. See `bin/codeassembly-runs.js` for the template, and the `@williamthorsen/node-monorepo-tools` packages for the original rationale.