Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .agents/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ MCP server for orchestrated run management. Wraps run-core capabilities as five

**Package:** `@codeassembly/mcp` (private)

**Bin:** `codeassembly-mcp` — the stdio server entry point. `.claude/settings.json` launches the server through `packages/mcp/bin/codeassembly-mcp.js`.

### Agents (`packages/agents/`)

The agents package is a CLI tool (`codeassembly-agents`) that installs reusable AI skills and subagent definitions into harness-specific directories. It also serves as the canonical home for all skill and subagent content.
Expand Down Expand Up @@ -258,12 +260,14 @@ The package README documents the `kb.yaml` configuration schema and merge semant

### Build system

- Uses esbuild via custom `config/build.ts` for TypeScript packages
- Intelligent caching based on content hashes
- Automatic `.ts` to `.js` extension rewriting
- Alias resolution support (`~src/` -> `src/`)
- Uses `nmr-compile` (from `@williamthorsen/nmr`) for TypeScript packages, emitting `.js` and `.d.ts` in a single TypeScript pass
- Intelligent caching based on content hashes, keyed under `node_modules/.cache/nmr-compile/`
- Automatic `.ts` to `.js` extension rewriting, in compiled output and emitted declarations alike
- Alias resolution support for `~/`-prefixed imports
- Factory uses Vite with `@vitejs/plugin-react` for the web app

Deleting `dist/` does not force a rebuild. The build cache lives outside it and is keyed on inputs alone, so a rebuild after a manual delete skips and leaves `dist/` empty. Clear `node_modules/.cache/nmr-compile/` too. Tracked upstream at williamthorsen/node-monorepo-tools#470.

### TypeScript

- Strict mode across all packages (`strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitReturns`)
Expand Down
4 changes: 2 additions & 2 deletions .agents/nmr/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: '@williamthorsen/nmr@0.15.0'
source: '@williamthorsen/nmr@0.18.1'
---

# nmr: agent guidance
Expand Down Expand Up @@ -38,7 +38,7 @@ Some root scripts (e.g. `lint`, `typecheck`, `test`) expand to `nmr root:X && pn

## Managed build

The default `compile` script runs `nmr-compile`, a standalone bin that esbuild-compiles a package's `src` to `dist/esm`, rewriting `~/` (package-root) import aliases and `.ts`→`.js` specifiers, and skipping work when inputs are unchanged. There is no repo-local build script to maintain; `nmr build` runs `compile` then `generate-typings`. To find or debug the build, look to `nmr-compile`, not a `config/build.ts` in the consuming repo.
The default `compile` script runs `nmr-compile`, a standalone bin that uses the TypeScript compiler API to emit a package's `src` to `dist/esm` as `.js` and `.d.ts` in one pass, rewriting relative `.ts`→`.js` specifiers and tsconfig `paths` aliases in both outputs, and skipping work when inputs are unchanged. `typescript` is a peer dependency (`>=5.7.0`). There is no repo-local build script to maintain, and no separate typings step; `nmr build` runs `compile` alone. To find or debug the build, look to `nmr-compile`, not a `config/build.ts` in the consuming repo.

## Override behaviors

Expand Down
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"mcpServers": {
"codeassembly": {
"command": "node",
"args": ["packages/mcp/dist/esm/cli.js"]
"args": ["packages/mcp/bin/codeassembly-mcp.js"]
}
}
}
106 changes: 0 additions & 106 deletions config/build.ts

This file was deleted.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@
"@vitest/coverage-v8": "4.1.10",
"@vitest/eslint-plugin": "1.6.23",
"@williamthorsen/eslint-config-typescript": "5.17.6",
"@williamthorsen/nmr": "0.16.0",
"@williamthorsen/nmr": "0.18.1",
"@williamthorsen/release-kit": "8.0.0",
"@williamthorsen/strict-lint": "6.3.2",
"dotenv": "17.4.2",
"esbuild": "0.28.1",
"eslint": "9.39.5",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest-dom": "5.5.0",
Expand Down
21 changes: 15 additions & 6 deletions packages/agents/bin/codeassembly-agents.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
#!/usr/bin/env node

import { existsSync } from 'node:fs';

// Thin wrapper so pnpm can symlink the bin at install time, before `dist/`
// exists. The real entry point loads at runtime from the build output.
// See packages/run-core/README.md ("Bin wrapper pattern") for details.
const entryPoint = new URL('../dist/esm/cli.js', import.meta.url);

// Gate on the entry file itself: Node raises ERR_MODULE_NOT_FOUND for any unresolved
// 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.exit(1);
}

try {
await import('../dist/esm/cli.js');
await import(entryPoint.href);
} catch (error) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
process.stderr.write('codeassembly-agents: build output not found — run `pnpm run build` first\n');
} else {
process.stderr.write(`codeassembly-agents: failed to load: ${error.message}\n`);
}
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`codeassembly-agents: failed to load: ${message}\n`);
process.exit(1);
}
3 changes: 2 additions & 1 deletion packages/agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
},
"devDependencies": {
"@hyperjump/json-schema": "1.17.7",
"@williamthorsen/toolbelt.strings": "3.1.4"
"@williamthorsen/toolbelt.strings": "3.1.4",
"esbuild": "0.28.1"
},
"engines": {
"node": ">=24"
Expand Down
22 changes: 15 additions & 7 deletions packages/kb/bin/kb.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
#!/usr/bin/env node

import { existsSync } from 'node:fs';

// Thin wrapper so pnpm can symlink the bin at install time, before `dist/` exists.
// The real entry point loads at runtime from the build output.
// See packages/run-core/README.md ("Bin wrapper pattern") for details.
const entryPoint = new URL('../dist/esm/cli/index.js', import.meta.url);

// Gate on the entry file itself: Node raises ERR_MODULE_NOT_FOUND for any unresolved
// 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.exit(1);
}

try {
await import('../dist/esm/cli/index.js');
await import(entryPoint.href);
} catch (error) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
process.stderr.write('kb: build output not found — run `pnpm run build` first\n');
} else {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`kb: failed to load: ${message}\n`);
}
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`kb: failed to load: ${message}\n`);
process.exit(1);
}
4 changes: 2 additions & 2 deletions packages/kb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@
"!dist/esm/test-utils"
],
"scripts": {
"build": "nmr compile && nmr generate-typings",
"prepare": "nmr compile && nmr generate-typings"
"build": "nmr compile",
"prepare": "nmr compile"
},
"dependencies": {
"picomatch": "4.0.5",
Expand Down
13 changes: 0 additions & 13 deletions packages/kb/tsconfig.generate-typings.json

This file was deleted.

24 changes: 24 additions & 0 deletions packages/mcp/bin/codeassembly-mcp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env node

import { existsSync } from 'node:fs';

// Committed launch path for the stdio server, so `.claude/settings.json` targets a file that
// exists before `dist/` is built. The real entry point loads at runtime from the build output.
// See packages/run-core/README.md ("Bin wrapper pattern") for details.
const entryPoint = new URL('../dist/esm/cli.js', import.meta.url);

// Gate on the entry file itself: Node raises ERR_MODULE_NOT_FOUND for any unresolved
// 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.exit(1);
}

try {
await import(entryPoint.href);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`codeassembly-mcp: failed to load: ${message}\n`);
process.exit(1);
}
7 changes: 7 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
"private": true,
"description": "MCP server for orchestrated run management",
"type": "module",
"bin": {
"codeassembly-mcp": "bin/codeassembly-mcp.js"
},
"files": [
"bin",
"dist"
],
"scripts": {
"build": "nmr compile",
"prepare": "nmr compile"
Expand Down
2 changes: 1 addition & 1 deletion packages/run-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ codeassembly-runs --path <dir> # 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, 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.

If invoked before building, the wrapper detects `ERR_MODULE_NOT_FOUND` and tells the user to run `pnpm run build`.
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.

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.
21 changes: 15 additions & 6 deletions packages/run-core/bin/codeassembly-runs.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
#!/usr/bin/env node

import { existsSync } from 'node:fs';

// Thin wrapper so pnpm can symlink the bin at install time, before `dist/`
// exists. The real entry point loads at runtime from the build output.
// See packages/run-core/README.md ("Bin wrapper pattern") for details.
const entryPoint = new URL('../dist/esm/cli.js', import.meta.url);

// Gate on the entry file itself: Node raises ERR_MODULE_NOT_FOUND for any unresolved
// 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.exit(1);
}

try {
await import('../dist/esm/cli.js');
await import(entryPoint.href);
} catch (error) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
process.stderr.write('codeassembly-runs: build output not found — run `pnpm run build` first\n');
} else {
process.stderr.write(`codeassembly-runs: failed to load: ${error.message}\n`);
}
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`codeassembly-runs: failed to load: ${message}\n`);
process.exit(1);
}
4 changes: 2 additions & 2 deletions packages/run-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"dist"
],
"scripts": {
"build": "nmr compile && nmr generate-typings",
"prepare": "nmr compile && nmr generate-typings"
"build": "nmr compile",
"prepare": "nmr compile"
},
"dependencies": {
"yaml": "2.9.0",
Expand Down
13 changes: 0 additions & 13 deletions packages/run-core/tsconfig.generate-typings.json

This file was deleted.

Loading
Loading