build: separate runtime artifacts and supervise command ownership

This commit is contained in:
morluto committed 2026-10-09 20:11:17 +08:00
1 parent c8544b1574
commit abc4f47683
32 files changed
+793 -784

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@ jobs:
with: with:
name: rea-dist name: rea-dist
path: dist path: dist
- name: Restore generated outputs from downloaded build - name: Generate test artifacts from the downloaded runtime
run: | run: |
node scripts/generate-mcp-tool-catalog.mjs node scripts/generate-mcp-tool-catalog.mjs
node scripts/generate-skill-metadata.mjs node scripts/generate-skill-metadata.mjs
+1 -4
View File
@@ -58,11 +58,8 @@ Thumbs.db
!/website/.dev.vars.example !/website/.dev.vars.example
!/website/.env*.example !/website/.env*.example
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
src/generatedMcpToolCatalog.ts
# Derived catalogs, portable conformance projections, and packaged skill bundles # Derived catalogs, portable conformance projections, and packaged skill bundles
# (regenerated from this checkout by `npm run build:cached`; never commit) # (regenerated by `npm run docs:generate`; never commit)
/skills/ /skills/
/docs/public/product-catalog.json /docs/public/product-catalog.json
/docs/verification/managed-conformance-manifest.json /docs/verification/managed-conformance-manifest.json
+4 -4
View File
@@ -24,15 +24,15 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points. - `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters. - `src/process/` owns shared process lifecycle primitives, not provider wire protocols. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks. - `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections. - `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run docs:generate`; never commit derived catalog digests or portable conformance projections.
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`. - `.cache/mcp-tool-catalog.json` is generated test metadata. Generate it with `npm run mcp-catalog:generate`; source checking and runtime builds do not consume it.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- `npm ci`: install the locked dependencies. - `npm ci`: install the locked dependencies.
- `npm run build:cached`: build the CLI and MCP server. - `npm run build:cached`: compile the CLI/MCP runtime and bundle its authored skill. Test catalogs and documentation/conformance artifacts have separate tasks.
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status. - `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests. - `npm run test:focused -- PATH...`: run exact test files, preparing runtime and generated artifacts required by the selected tests.
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`). - `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
- `npm run check:fast`: run cached typecheck and lint checks. - `npm run check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time. - `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
+24 -6
View File
@@ -42,9 +42,10 @@ npm run check:fast
`npm ci` installs the exact dependencies and prepares the Husky hooks without `npm ci` installs the exact dependencies and prepares the Husky hooks without
building the project. Run `npm run build:cached` when you need the standalone building the project. Run `npm run build:cached` when you need the standalone
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by CLI or MCP server; it compiles the runtime and bundles the authored skill.
Turbo. Turbo caches deterministic builds and static checks across Git `npm run build` forces those tasks to rerun. Neither command generates test
worktrees. After a package, lockfile, or managed-skill version change, run catalogs, documentation projections, or managed verification evidence. Turbo
caches deterministic builds and static checks across Git worktrees. After a package, lockfile, or managed-skill version change, run
`npm run metadata:generate` before building. `npm run metadata:generate` before building.
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`. Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
@@ -96,7 +97,22 @@ Packaging, setup, installation, or distribution changes also require
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes `npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
require the matching real-provider `verify:*` lane. require the matching real-provider `verify:*` lane.
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure. `check:fast` runs cached source/test typecheck and lint without compiling or
running generators. `npm run test:prepare` prepares all artifacts consumed by
the complete suite; focused tests prepare only their runtime and declared
artifact dependencies. The test-only MCP catalog lives at
`.cache/mcp-tool-catalog.json` and can be regenerated independently with
`npm run mcp-catalog:generate`.
Build and generation commands hold one SQLite exclusive transaction around
their whole Turbo graph, including cache restoration. The persistent database
in `.cache/rea-command-locks/` is a lock identity, not a stale-file marker;
the operating system releases ownership when the command supervisor exits.
On POSIX, that supervisor retains the transaction and process-group identity
through cancellation, including loss of its parent runner. Windows cancellation
waits for the command to complete. Do not remove the database while commands
are running.
`check` adds formatting, dead-code, and package-metadata freshness checks. `check` adds formatting, dead-code, and package-metadata freshness checks.
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
the same configuration. Pre-commit formats and lints staged files; pre-push runs the same configuration. Pre-commit formats and lints staged files; pre-push runs
@@ -104,8 +120,10 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs
`docs:check` builds and validates generated metadata for the current checkout. `docs:check` builds and validates generated metadata for the current checkout.
The product catalog (`docs/public/product-catalog.json`), portable managed The product catalog (`docs/public/product-catalog.json`), portable managed
conformance projections (`docs/verification/managed-conformance-*.json`), and conformance projections (`docs/verification/managed-conformance-*.json`), and
packaged skill (`skills/`) are ignored build outputs. Edit skill instructions packaged skill (`skills/`) are ignored generated outputs. `docs:generate`
and references in `skill-src/`; the build adds catalog-dependent metadata to prepares documentation and managed evidence; `evidence:generate` prepares only
the managed commitment and its runtime/skill dependencies. Edit skill
instructions and references in `skill-src/`; the skill task adds metadata to
the packaged copy without rewriting authored files. The generated manifest the packaged copy without rewriting authored files. The generated manifest
commits to that exact packaged skill bundle. It is a portable projection of commits to that exact packaged skill bundle. It is a portable projection of
the deterministic managed verifier, not a record of optional real-provider runs. the deterministic managed verifier, not a record of optional real-provider runs.
+1 -1
View File
@@ -298,7 +298,7 @@ For exact options, prerequisites and result contracts:
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine. - [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses. - [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations. - [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands. - [Tool catalog](docs/mcp-contracts.md#generated-catalog): generated inventory of tools, providers and CLI commands.
- [Roadmap](docs/roadmap.md): planned work and capability trackers. - [Roadmap](docs/roadmap.md): planned work and capability trackers.
Report vulnerabilities through [SECURITY.md](SECURITY.md). Report vulnerabilities through [SECURITY.md](SECURITY.md).
+11
View File
@@ -647,6 +647,17 @@ The dry-run option reports the chosen merge base, scope and build prerequisite
without executing tests or building. A missing Git base reports how to fetch without executing tests or building. A missing Git base reports how to fetch
it or select another revision. it or select another revision.
Source typechecking needs no compiled runtime or generated test metadata.
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
`npm run test:prepare` also generates the MCP contract test catalog, product
catalog, and portable managed evidence used by the complete suite. Focused
tests prepare those extra outputs only when their selected files consume them.
The MCP test catalog is JSON in `.cache/mcp-tool-catalog.json`; the tracked
test loader supplies its types without making generation a source-check
prerequisite. Run `npm run mcp-catalog:generate` to refresh it independently.
Documentation generation and managed conformance run through their separate
`docs:generate` and `evidence:generate` task graphs.
Changed selection can miss runtime registration, generated data, shell Changed selection can miss runtime registration, generated data, shell
entrypoints, bridges, or other relationships absent from the import graph. entrypoints, bridges, or other relationships absent from the import graph.
An empty changed selection means no tests were selected, not verified An empty changed selection means no tests were selected, not verified
+36 -28
View File
@@ -80,27 +80,26 @@
], ],
"scripts": { "scripts": {
"deps:check": "node scripts/check-dependency-install.mjs", "deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs", "metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check", "metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs", "mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached", "mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check", "mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built",
"prebuild": "npm run deps:check && npm run metadata:check", "prebuild": "npm run deps:check",
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked", "build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs",
"precheck": "npm run deps:check", "precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check", "check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check", "precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint --output-logs=errors-only", "check:fast": "turbo run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check", "precheck:changed": "npm run deps:check",
"check:changed": "npm run check:fast && npm run test:changed", "check:changed": "npm run check:fast && npm run test:changed",
"precheck:test": "npm run deps:check", "precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run", "check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check", "precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos", "check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check", "precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos", "check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "node scripts/check-dependency-install.mjs && turbo run build", "build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
"format": "oxfmt .", "format": "oxfmt .",
"format:check": "oxfmt --check .", "format:check": "oxfmt --check .",
"lint": "oxlint && npm run verify:module-boundaries", "lint": "oxlint && npm run verify:module-boundaries",
@@ -110,33 +109,33 @@
"scan:todos": "node scripts/scan-todos.mjs", "scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp", "start": "node scripts/rea.mjs mcp",
"test": "npm run test:with-build", "test": "npm run test:with-build",
"test:with-build": "npm run build:cached && vitest run", "test:with-build": "npm run test:prepare && vitest run",
"test:run": "vitest run", "test:run": "vitest run",
"test:local": "node scripts/run-development-tests.mjs local", "test:local": "node scripts/run-development-tests.mjs local",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation", "test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
"pretest:boundary": "npm run build:cached", "pretest:boundary": "npm run test:prepare",
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global", "test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "npm run build:cached", "pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
"test:mcp": "vitest run --project mcp-boundary", "test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached", "pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance", "test:acceptance": "vitest run --project acceptance",
"test:changed": "node scripts/run-development-tests.mjs changed", "test:changed": "node scripts/run-development-tests.mjs changed",
"pretest:integration": "npm run build:cached", "pretest:integration": "npm run test:prepare",
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance", "test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached", "pretest:coverage": "npm run test:prepare",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8", "test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml", "test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"pretest:ci:shard": "npm run build:cached", "pretest:ci:shard": "npm run test:prepare",
"test:ci:shard": "npm run test:ci:shard:run", "test:ci:shard": "npm run test:ci:shard:run",
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob", "test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8", "test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
"pretest:verbose": "npm run build:cached", "pretest:verbose": "npm run test:prepare",
"test:verbose": "vitest run --reporter=verbose", "test:verbose": "vitest run --reporter=verbose",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed", "test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached", "pretest:watch:all": "npm run test:prepare",
"test:watch:all": "vitest --changed", "test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs", "verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs", "verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs", "verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs", "verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
@@ -181,18 +180,17 @@
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs", "probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
"verify:published": "node scripts/verify-published-package.mjs", "verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs", "config:print": "node scripts/print-mcp-config.mjs",
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs", "evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence",
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check", "evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built",
"predocs:dev": "npm run docs:generate", "predocs:dev": "npm run docs:generate",
"docs:dev": "vitepress dev docs", "docs:dev": "vitepress dev docs",
"predocs:build": "npm run docs:generate", "predocs:build": "npm run docs:generate",
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked", "docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-error-schema.mjs", "docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run docs:check:built",
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built", "docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check", "docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs", "prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs", "prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs", "verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
@@ -217,7 +215,17 @@
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs", "verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs", "verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs", "verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs" "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
"artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs",
"artifacts:error-schema": "node scripts/generate-error-schema.mjs",
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
"evidence:check:built": "node scripts/generate-completion-ledger.mjs --check",
"docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema"
}, },
"lint-staged": { "lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [ "*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
+1 -4
View File
@@ -4,7 +4,4 @@ import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await Promise.all([ await rm(join(root, "dist"), { recursive: true, force: true });
rm(join(root, "dist"), { recursive: true, force: true }),
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
]);
+9 -103
View File
@@ -1,13 +1,12 @@
import { mkdir } from "node:fs/promises";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js"; import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server"; import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js"; import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js"; import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs"; import { ensureGeneratedFile } from "./lib/generated-file.mjs";
@@ -17,10 +16,9 @@ for (const argument of arguments_)
throw new Error(`Unknown MCP tool catalog option: ${argument}`); throw new Error(`Unknown MCP tool catalog option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const root = join(dirname(fileURLToPath(import.meta.url)), "..");
// This build-generated source supports schema and provider conformance tests; // This generated metadata supports schema conformance tests;
// production MCP tools are registered from their canonical contracts directly. // production MCP tools are registered from their canonical contracts directly.
// Keep the test metadata portable and regenerate it with `npm run build`. // Keep the test metadata portable and regenerate it with `npm run mcp-catalog:generate`.
const CATALOG_PLATFORM = "darwin";
const sessionToolNames = new Set([ const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map( ...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
({ name }) => name, ({ name }) => name,
@@ -87,10 +85,6 @@ async function sdkToolCatalog() {
await Promise.allSettled([client.close(), server.close()]); await Promise.allSettled([client.close(), server.close()]);
} }
} }
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
CATALOG_PLATFORM,
).map(({ identity, capabilities }) => ({ identity, capabilities }));
const canonicalizeJson = (value) => { const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson); if (Array.isArray(value)) return value.map(canonicalizeJson);
if (value === null || typeof value !== "object") return value; if (value === null || typeof value !== "object") return value;
@@ -101,101 +95,13 @@ const canonicalizeJson = (value) => {
); );
}; };
const payloadJson = JSON.stringify( const payloadJson = JSON.stringify(canonicalizeJson({ catalog }), null, 2);
canonicalizeJson({ catalog, auxiliaryProviders }), const outputPath = join(root, ".cache/mcp-tool-catalog.json");
null, if (!arguments_.has("--check"))
2, await mkdir(dirname(outputPath), { recursive: true });
);
const source = await formatGeneratedFile(
"src/generatedMcpToolCatalog.ts",
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {
CapabilityDescriptor,
ProviderIdentity,
} from "./application/AnalysisProvider.js";
import type { ToolKind } from "./contracts/toolContractTypes.js";
import type { ToolEffects } from "./contracts/toolEffects.js";
interface GeneratedTool {
readonly name: string;
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
readonly title: string;
readonly description: string;
readonly kind: ToolKind;
readonly requiresSession: boolean;
readonly inputSchema: Readonly<Record<string, unknown>>;
readonly outputSchema: Readonly<Record<string, unknown>>;
readonly annotations: ToolAnnotations;
readonly effects: ToolEffects;
}
interface GeneratedProvider {
readonly identity: ProviderIdentity;
readonly capabilities: readonly CapabilityDescriptor[];
}
interface GeneratedPayload {
readonly catalog: readonly GeneratedTool[];
readonly auxiliaryProviders: readonly GeneratedProvider[];
}
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
/** Generated from TOOL_CONTRACTS; do not edit. */
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
return (
isRecord(value) &&
Array.isArray(value.catalog) &&
value.catalog.every(isGeneratedTool) &&
Array.isArray(value.auxiliaryProviders) &&
value.auxiliaryProviders.every(isGeneratedProvider)
);
}
function isGeneratedTool(value: unknown): value is GeneratedTool {
return (
isRecord(value) &&
typeof value.name === "string" &&
(typeof value.analysisOperation === "string" ||
value.analysisOperation === null) &&
typeof value.title === "string" &&
typeof value.description === "string" &&
typeof value.kind === "string" &&
typeof value.requiresSession === "boolean" &&
isRecord(value.inputSchema) &&
isRecord(value.outputSchema) &&
isRecord(value.annotations) &&
isRecord(value.effects)
);
}
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
return (
isRecord(value) &&
isRecord(value.identity) &&
Array.isArray(value.capabilities)
);
}
function isRecord(
value: unknown,
): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
);
await ensureGeneratedFile({ await ensureGeneratedFile({
path: join(root, "src/generatedMcpToolCatalog.ts"), path: outputPath,
source, source: `${payloadJson}\n`,
check: arguments_.has("--check"), check: arguments_.has("--check"),
generateCommand: "npm run mcp-catalog:generate", generateCommand: "npm run mcp-catalog:generate",
}); });
+4 -6
View File
@@ -200,7 +200,7 @@ export const providerCatalog = (sources) => {
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
}, },
{ {
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY, identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: [ contracts: [
...sources.browserContracts.BROWSER_TOOL_CONTRACTS, ...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS, ...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
@@ -208,19 +208,17 @@ export const providerCatalog = (sources) => {
}, },
{ {
identity: identity:
sources.browserScenarioProvider sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
contracts: contracts:
sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS, sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
}, },
{ {
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY, identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY,
contracts: observationContracts, contracts: observationContracts,
}, },
{ {
identity: identity:
sources.electronActiveProvider sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
contracts: activeContracts, contracts: activeContracts,
}, },
{ {
+1 -4
View File
@@ -75,10 +75,7 @@ const SOURCE_PATHS = {
idaProvider: "dist/ida/IdaProviderCapabilities.js", idaProvider: "dist/ida/IdaProviderCapabilities.js",
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js", nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js", artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js", browserIdentities: "dist/browser/providerIdentities.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js", v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js", evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js", evidenceBundle: "dist/domain/evidenceBundle.js",
+2 -1
View File
@@ -11,11 +11,12 @@ export function parseDevelopmentTestRequest(
arguments_: readonly string[], arguments_: readonly string[],
): DevelopmentTestRequest; ): DevelopmentTestRequest;
/** Plan source feedback or a strict explicit run with its build prerequisite. */ /** Plan source feedback or explicit tests with runtime/artifact prerequisites. */
export function developmentTestPlan( export function developmentTestPlan(
request: DevelopmentTestRequest, request: DevelopmentTestRequest,
baseCommit?: string, baseCommit?: string,
): { ): {
readonly needsBuild: boolean; readonly needsBuild: boolean;
readonly artifactTasks: readonly string[];
readonly vitestArguments: readonly string[]; readonly vitestArguments: readonly string[];
}; };
+18 -1
View File
@@ -49,7 +49,20 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
return { mode, paths: [...new Set(paths)], base, dryRun }; return { mode, paths: [...new Set(paths)], base, dryRun };
}; };
/** Plan source feedback or a strict explicit run with its build prerequisite. */ // Test-only outputs are declared beside the test planner rather than becoming
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
const artifactConsumers = {
"artifacts:mcp-catalog": [
"tests/boundary/mcp/contractPresentation.test.ts",
"tests/boundary/mcp/toolSchemaValidity.test.ts",
],
"artifacts:product-catalog": ["tests/boundary/cli/productCatalog.test.ts"],
"artifacts:managed-evidence": [
"tests/boundary/filesystem/setupSkill.test.ts",
],
};
/** Plan source feedback or explicit tests with their runtime/artifact prerequisites. */
export const developmentTestPlan = (request, baseCommit) => { export const developmentTestPlan = (request, baseCommit) => {
const explicit = request.paths.length > 0; const explicit = request.paths.length > 0;
if ( if (
@@ -60,6 +73,10 @@ export const developmentTestPlan = (request, baseCommit) => {
const projects = request.mode === "focused" ? [] : sourceProjects; const projects = request.mode === "focused" ? [] : sourceProjects;
return { return {
needsBuild: request.paths.some((path) => !sourceTest(path)), needsBuild: request.paths.some((path) => !sourceTest(path)),
artifactTasks: Object.entries(artifactConsumers).flatMap(
([task, consumers]) =>
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
),
vitestArguments: [ vitestArguments: [
"run", "run",
...projects.flatMap((project) => ["--project", project]), ...projects.flatMap((project) => ["--project", project]),
-13
View File
@@ -14,14 +14,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"inspector", "inspector",
"javascript", "javascript",
]); ]);
const PROVIDER_ROOTS = new Set([
...MIGRATED_PROVIDER_ROOTS,
"artifacts",
"browser",
"dotnet",
"native",
]);
const repositoryPath = (root, path) => const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/"); relative(root, path).replaceAll("\\", "/");
@@ -88,11 +80,6 @@ const failedBoundary = (file, target) => {
/^src\/(?:cli|main)\./u.test(target)) /^src\/(?:cli|main)\./u.test(target))
) )
return "artifact-acquisition"; return "artifact-acquisition";
if (
PROVIDER_ROOTS.has(layer) &&
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
)
return "provider-generated-catalog";
if (PURE_LAYERS.has(layer)) { if (PURE_LAYERS.has(layer)) {
if ( if (
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) || (/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
+3 -6
View File
@@ -44,7 +44,6 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const { const {
renderCliOutputArgumentError, renderCliOutputArgumentError,
renderEmptyFilteredCliOutput, renderEmptyFilteredCliOutput,
sanitizeCliOutput,
validateCliOutputArguments, validateCliOutputArguments,
} = await import("../dist/cliOutput.js"); } = await import("../dist/cliOutput.js");
const outputArguments = validateCliOutputArguments(args); const outputArguments = validateCliOutputArguments(args);
@@ -57,13 +56,11 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
await createCli(process.env, resultOutput).serve(args, { await createCli(process.env, resultOutput).serve(args, {
stdout: (output) => { stdout: (output) => {
if (resultOutput?.handled) { if (resultOutput?.handled) {
if (resultOutput.failed) if (resultOutput.failed) process.stderr.write(output);
process.stderr.write(sanitizeCliOutput(output));
return; return;
} }
const sanitized = sanitizeCliOutput(output); if (output.length > 0) wroteOutput = true;
if (sanitized.length > 0) wroteOutput = true; process.stdout.write(output);
process.stdout.write(sanitized);
}, },
}); });
if (!wroteOutput && !resultOutput?.handled) if (!wroteOutput && !resultOutput?.handled)
+17 -8
View File
@@ -47,15 +47,24 @@ try {
? `Focused tests: ${request.paths.join(", ")}` ? `Focused tests: ${request.paths.join(", ")}`
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`, : `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
); );
if (plan.needsBuild) { if (plan.needsBuild || plan.artifactTasks.length > 0) {
if (!process.env.npm_execpath) const dependencies = await run(process.execPath, [
throw new Error("Run compiled tests through npm run test:focused"); "scripts/check-dependency-install.mjs",
const built = await run(process.execPath, [
process.env.npm_execpath,
"run",
"build:cached",
]); ]);
if (built.code !== 0) process.exitCode = built.code; if (dependencies.code !== 0)
throw new Error(
"Test prerequisites require the locked dependency installation",
);
const prepared = await run(process.execPath, [
"scripts/run-exclusive.mjs",
"artifacts",
process.execPath,
resolve("node_modules/turbo/bin/turbo"),
"run",
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
]);
if (prepared.code !== 0) process.exitCode = prepared.code;
} }
if (!process.exitCode && !shutdownSignal) { if (!process.exitCode && !shutdownSignal) {
const tested = await run(process.execPath, [ const tested = await run(process.execPath, [
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { DatabaseSync } from "node:sqlite";
const [, , lockName, ...command] = process.argv;
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let database;
let child;
let forceTimer;
let shutdownSignal;
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGTERM", () => handleSignal("SIGTERM"));
process.on("disconnect", handleDisconnect);
process.on("message", (message) => {
if (message === "SIGINT" || message === "SIGTERM") handleSignal(message);
});
if (!process.connected) {
// The public wrapper can disappear before ESM startup installs listeners.
// A disconnected supervisor has no authority to start a new command.
process.exitCode = signalExitCodes.SIGTERM;
} else if (
typeof lockName !== "string" ||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
command.length === 0
) {
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
process.exitCode = 64;
disconnectParent();
} else {
await run();
}
async function run() {
try {
await mkdir(lockRoot, { recursive: true });
// SQLite owns acquisition and OS-backed lock release, including process
// death. Keep the database pathname stable; deleting it would split owners.
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
try {
database.exec("BEGIN EXCLUSIVE");
} catch (cause) {
if (isBusyError(cause))
throw new Error(
`Another ${lockName} command is already running. Wait for it to finish.`,
{ cause },
);
throw cause;
}
if (shutdownSignal === undefined) {
const result = await runCommand();
process.exitCode = result.code ?? 1;
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
} finally {
database?.close();
clearTimeout(forceTimer);
if (shutdownSignal !== undefined)
process.exitCode = signalExitCodes[shutdownSignal];
disconnectParent();
}
}
function runCommand() {
return new Promise((resolveResult, reject) => {
child = spawn(command[0], command.slice(1), {
cwd: process.cwd(),
env: process.env,
shell: process.platform === "win32",
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => {
// During POSIX cancellation the group leader must stay alive, retaining
// both its group identity and SQLite ownership until the entire group is
// killed. A completed command can still have uncooperative descendants.
if (shutdownSignal === undefined || process.platform === "win32")
resolveResult({ code });
});
});
}
function disconnectParent() {
process.off("disconnect", handleDisconnect);
if (process.connected) process.disconnect();
}
function handleDisconnect() {
handleSignal("SIGTERM");
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child === undefined || process.platform === "win32") return;
// Establish cancellation before signalling the group. This supervisor is
// its live leader, so the group identifier cannot be reused during cleanup.
process.kill(-process.pid, signal);
forceTimer = setTimeout(() => process.kill(-process.pid, "SIGKILL"), 1_000);
}
function isBusyError(cause) {
return (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ERR_SQLITE_ERROR" &&
"errcode" in cause &&
(cause.errcode === 5 || cause.errcode === 6)
);
}
+30 -145
View File
@@ -1,155 +1,40 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url";
import { join, resolve } from "node:path";
const [, , lockName, ...command] = process.argv;
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
const lockPath = join(lockRoot, `${lockName ?? "invalid"}.lock`);
const ownerPath = join(lockPath, "owner.json");
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 }; const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let lockHeld = false;
let child;
let forceTimer;
let shutdownSignal; let shutdownSignal;
const supervisor = spawn(
process.execPath,
[
fileURLToPath(new URL("./run-exclusive-command.mjs", import.meta.url)),
...process.argv.slice(2),
],
{
detached: process.platform !== "win32",
stdio: ["inherit", "inherit", "inherit", "ipc"],
},
);
process.on("SIGINT", () => handleSignal("SIGINT")); for (const signal of Object.keys(signalExitCodes))
process.on("SIGTERM", () => handleSignal("SIGTERM")); process.on(signal, () => {
if (
typeof lockName !== "string" ||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
command.length === 0
) {
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
process.exitCode = 64;
} else {
await run();
}
async function run() {
try {
await acquireLock();
const result = await runCommand();
process.exitCode =
shutdownSignal === undefined
? (result.code ?? 1)
: signalExitCodes[shutdownSignal];
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
} finally {
await releaseLock();
}
}
async function acquireLock() {
await mkdir(lockRoot, { recursive: true });
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
await mkdir(lockPath);
await writeFile(
ownerPath,
JSON.stringify({
pid: process.pid,
command: command.join(" "),
started_at: new Date().toISOString(),
}),
"utf8",
);
lockHeld = true;
return;
} catch (cause) {
if (!isAlreadyExistsError(cause)) throw cause;
const owner = await readOwner();
if (owner === null || isProcessAlive(owner.pid))
throw new Error(
`Another ${lockName} command is already running (pid ${String(owner?.pid ?? "unknown")}). Wait for it to finish or inspect ${lockPath}.`,
);
await rm(lockPath, { force: true, recursive: true });
}
}
throw new Error(`Could not acquire the ${lockName} command lock`);
}
async function releaseLock() {
if (!lockHeld) return;
lockHeld = false;
try {
const owner = await readOwner();
if (owner?.pid === process.pid)
await rm(lockPath, { force: true, recursive: true });
} catch (cause) {
console.error(
`Could not remove the ${lockName} command lock: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
}
function runCommand() {
return new Promise((resolveResult, reject) => {
child = spawn(command[0], command.slice(1), {
cwd: process.cwd(),
env: process.env,
shell: process.platform === "win32",
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => {
clearTimeout(forceTimer);
resolveResult({ code });
});
});
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return; if (shutdownSignal !== undefined) return;
shutdownSignal = signal; shutdownSignal = signal;
if (child !== undefined && child.exitCode === null) { if (supervisor.connected)
child.kill(signal); supervisor.send(signal, (cause) => {
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000); if (cause !== null)
forceTimer.unref?.(); console.error(`Could not forward cancellation: ${cause.message}`);
} });
} });
async function readOwner() { supervisor.once("error", (cause) => {
try { console.error(cause.message);
const parsed = JSON.parse(await readFile(ownerPath, "utf8")); process.exitCode = 1;
const pid = });
typeof parsed === "object" && parsed !== null supervisor.once("close", (code) => {
? Reflect.get(parsed, "pid") process.exitCode =
: undefined; shutdownSignal === undefined
if (typeof pid !== "number" || !Number.isInteger(pid)) return null; ? (code ?? 1)
return { pid }; : signalExitCodes[shutdownSignal];
} catch (cause) { });
if (isMissingFileError(cause)) return null;
throw cause;
}
}
function isProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (cause) {
return !isMissingProcessError(cause);
}
}
function isAlreadyExistsError(cause) {
return isNodeError(cause) && cause.code === "EEXIST";
}
function isMissingFileError(cause) {
return isNodeError(cause) && cause.code === "ENOENT";
}
function isMissingProcessError(cause) {
return isNodeError(cause) && cause.code === "ESRCH";
}
function isNodeError(cause) {
return typeof cause === "object" && cause !== null && "code" in cause;
}
+2 -6
View File
@@ -42,12 +42,8 @@ export async function verifyPackagePack({ root, workspace }) {
throw new Error( throw new Error(
"package included authored skill sources instead of only the generated bundle", "package included authored skill sources instead of only the generated bundle",
); );
if ( if (packedFiles.some((path) => path.startsWith("package/.cache/")))
packedFiles.some((path) => throw new Error("package included development cache or test metadata");
/^package\/(?:dist|src)\/generatedMcpToolCatalog\./u.test(path),
)
)
throw new Error("package included the test-only generated MCP catalog");
const skill = ( const skill = (
await exec("tar", [ await exec("tar", [
"-xOf", "-xOf",
@@ -1,63 +0,0 @@
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { z } from "zod";
import { GENERATED_MCP_TOOL_CATALOG } from "../../src/generatedMcpToolCatalog.js";
const execute = promisify(execFile);
const fixture = fileURLToPath(
new URL("../fixtures/contractPresentation.mjs", import.meta.url),
);
const resultSchema = z.object({
catalog: z.array(
z.object({
name: z.string(),
inputSchema: z.record(z.string(), z.unknown()),
outputSchema: z.record(z.string(), z.unknown()),
}),
),
responses: z.array(
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
),
});
it.each(["original", "reversed"])(
"preserves advertised schemas and handler meaning with %s presentation",
async (presentation) => {
const { stdout } = await execute(
process.execPath,
[fixture, presentation],
{
timeout: 20_000,
maxBuffer: 16 * 1024 * 1024,
},
);
const result = resultSchema.parse(JSON.parse(stdout));
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
);
for (const advertised of result.catalog) {
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
({ name }) => name === advertised.name,
);
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
}
for (const { name, response } of result.responses) {
if (name === "reconcile_javascript_runtime") {
expect(response.isError).not.toBe(true);
expect(response.structuredContent).toMatchObject({
evidence: { operation: name },
});
} else {
expect(response.isError, name).toBe(true);
expect(response.structuredContent, name).toMatchObject({
error: { details: { operation: name } },
});
}
}
},
);
@@ -0,0 +1,56 @@
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { z } from "zod";
import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js";
const execute = promisify(execFile);
const fixture = fileURLToPath(
new URL("../../fixtures/contractPresentation.mjs", import.meta.url),
);
const resultSchema = z.object({
catalog: z.array(
z.object({
name: z.string(),
inputSchema: z.record(z.string(), z.unknown()),
outputSchema: z.record(z.string(), z.unknown()),
}),
),
responses: z.array(
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
),
});
it("preserves advertised schemas and handler meaning when presentation order changes", async () => {
const { stdout } = await execute(process.execPath, [fixture, "reversed"], {
timeout: 20_000,
maxBuffer: 16 * 1024 * 1024,
});
const result = resultSchema.parse(JSON.parse(stdout));
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
);
for (const advertised of result.catalog) {
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
({ name }) => name === advertised.name,
);
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
}
for (const { name, response } of result.responses) {
if (name === "reconcile_javascript_runtime") {
expect(response.isError).not.toBe(true);
expect(response.structuredContent).toMatchObject({
evidence: { operation: name },
});
} else {
expect(response.isError, name).toBe(true);
expect(response.structuredContent, name).toMatchObject({
error: { details: { operation: name } },
});
}
}
});
+23 -3
View File
@@ -7,7 +7,7 @@ import { z } from "zod";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js"; import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js";
import { processScenarioSchema } from "../../../src/domain/process/processScenario.js"; import { processScenarioSchema } from "../../../src/domain/process/processScenario.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js"; import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js";
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
interface ToolSchemas { interface ToolSchemas {
@@ -94,6 +94,23 @@ function expectKnownAuthorityHints(tools: readonly ToolSchemas[]): void {
idempotentHint: false, idempotentHint: false,
openWorldHint: false, openWorldHint: false,
}, },
export_web_scripts: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
capture_process_scenario: {
openWorldHint: true,
},
export_evidence_bundle: {
readOnlyHint: false,
destructiveHint: true,
},
import_evidence_bundle: {
readOnlyHint: false,
destructiveHint: false,
},
} as const; } as const;
for (const [name, annotations] of Object.entries(expected)) for (const [name, annotations] of Object.entries(expected))
@@ -383,8 +400,11 @@ describe("MCP root input schemas", () => {
for (const contract of TOOL_CONTRACTS) { for (const contract of TOOL_CONTRACTS) {
const inputSchema = advertised.get(contract.name)!.inputSchema; const inputSchema = advertised.get(contract.name)!.inputSchema;
expect(inputSchema.type, contract.name).toBe("object"); expect(inputSchema.type, contract.name).toBe("object");
expect(inputSchema.properties, contract.name).toBeDefined(); expect(
expect(inputSchema.anyOf, contract.name).toBeUndefined(); inputSchema.properties !== undefined ||
Array.isArray(inputSchema.anyOf),
contract.name,
).toBe(true);
const validate = ajv.compile(inputSchema); const validate = ajv.compile(inputSchema);
for (const example of contract.examples) for (const example of contract.examples)
expect( expect(
@@ -1,240 +0,0 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { McpServer } from "@modelcontextprotocol/server";
import { Ajv2020 } from "ajv/dist/2020.js";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js";
import { webPageInspectionSchema } from "../../../src/domain/browserObservation.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js";
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
function resolveLocalReference(root: unknown, reference: string): unknown {
if (reference === "#") return root;
if (!reference.startsWith("#/")) return undefined;
let current = root;
for (const part of reference.slice(2).split("/")) {
const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
if (!isRecord(current) || !Object.hasOwn(current, key)) return undefined;
current = current[key];
}
return current;
}
// Count object/array and composition levels, following references per path.
// Definitions, property maps, and literal example/default data are not levels.
function inputDepth(
schema: unknown,
root: unknown = schema,
activeReferences: readonly string[] = [],
): number {
if (!isRecord(schema)) return 0;
let referenceDepth = 0;
if (typeof schema.$ref === "string") {
if (activeReferences.includes(schema.$ref)) return Number.POSITIVE_INFINITY;
const target = resolveLocalReference(root, schema.$ref);
if (target === undefined) return Number.POSITIVE_INFINITY;
referenceDepth = inputDepth(target, root, [
...activeReferences,
schema.$ref,
]);
}
const childDepths: number[] = [];
for (const key of ["properties", "patternProperties", "dependentSchemas"]) {
const group = schema[key];
if (isRecord(group))
for (const child of Object.values(group))
childDepths.push(inputDepth(child, root, activeReferences));
}
for (const key of [
"items",
"additionalProperties",
"contains",
"not",
"if",
"then",
"else",
"propertyNames",
"unevaluatedProperties",
"unevaluatedItems",
])
childDepths.push(inputDepth(schema[key], root, activeReferences));
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
const children = schema[key];
if (Array.isArray(children))
for (const child of children)
childDepths.push(inputDepth(child, root, activeReferences));
}
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
const container = types.includes("object") || types.includes("array") ? 1 : 0;
const composition = ["anyOf", "oneOf", "allOf"].some((key) =>
Array.isArray(schema[key]),
)
? 1
: 0;
return Math.max(
referenceDepth,
container + composition + Math.max(0, ...childDepths),
);
}
function inputDepthViolations(
tools: readonly { name: string; inputSchema: unknown }[],
) {
return tools.flatMap(({ name, inputSchema }) => {
const depth = inputDepth(inputSchema);
return depth > 10 ? [{ name, depth }] : [];
});
}
async function withCatalog(
verify: (client: Client, calls: ReadonlyMap<string, number>) => Promise<void>,
): Promise<void> {
const server = new McpServer({ name: "wire-limits", version: "0" });
const calls = new Map<string, number>();
for (const contract of TOOL_CONTRACTS)
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => {
calls.set(contract.name, (calls.get(contract.name) ?? 0) + 1);
return { content: [], isError: true };
},
);
const client = new Client({ name: "wire-limits", version: "0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await verify(client, calls);
} finally {
await Promise.allSettled([client.close(), server.close()]);
}
}
describe("complete catalog input compatibility profile (#919)", () => {
it("follows shared and escaped references without treating data as schemas", () => {
const diamond = {
type: "object",
properties: {
left: { $ref: "#/$defs/a~1b~0c" },
right: { $ref: "#/$defs/a~1b~0c" },
},
$defs: {
"a/b~c": { type: "object", properties: { id: { type: "string" } } },
unused: { $ref: "#/$defs/unused" },
},
examples: [{ $ref: "#" }],
default: { $ref: "#" },
};
expect(inputDepth(diamond)).toBe(2);
expect(
inputDepth({ type: "object", properties: { child: { $ref: "#" } } }),
).toBe(Number.POSITIVE_INFINITY);
expect(
inputDepth({
type: "object",
properties: {
values: {
anyOf: [
{ type: "array", items: { type: "object" } },
{ type: "null" },
],
},
},
}),
).toBe(4);
});
it("keeps every live and generated input within ten structural levels", async () => {
await withCatalog(async (client) => {
const { tools } = await client.listTools();
expect(tools.map(({ name }) => name).sort()).toEqual(
TOOL_CONTRACTS.map(({ name }) => name).sort(),
);
expect(inputDepthViolations(tools)).toEqual([]);
expect(inputDepthViolations(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
});
});
it("describes capture round trips without altering domain or scenario schemas", () => {
const contract = TOOL_CONTRACTS.find(
({ name }) => name === "compare_web_captures",
);
if (contract === undefined)
throw new Error("Missing capture comparison contract");
const original = z.toJSONSchema(browserCaptureComparisonInputSchema, {
io: "input",
});
const projected = z.toJSONSchema(contract.inputSchema, { io: "input" });
expect(inputDepth(original)).toBeGreaterThan(10);
expect(inputDepth(projected)).toBeLessThanOrEqual(10);
expect(isRecord(original.properties)).toBe(true);
expect(isRecord(projected.properties)).toBe(true);
if (!isRecord(original.properties) || !isRecord(projected.properties))
throw new Error("Missing comparison properties");
for (const key of ["normalization"])
expect(projected.properties[key], key).toEqual(original.properties[key]);
expect(z.toJSONSchema(webPageInspectionSchema).properties).toHaveProperty(
"network",
);
for (const example of contract.examples)
expect(contract.inputSchema.parse(example.input)).toEqual(
browserCaptureComparisonInputSchema.parse(example.input),
);
const example = contract.examples[0];
if (example === undefined) throw new Error("Missing scenario example");
for (const malformed of [
{ before_scenario: {}, after_scenario: {} },
{
...example.input,
before: { inspection: {} },
after: { inspection: {} },
},
{ ...example.input, __unexpected_root_key__: true },
]) {
const originalResult =
browserCaptureComparisonInputSchema.safeParse(malformed);
const projectedResult = contract.inputSchema.safeParse(malformed);
expect(originalResult.success).toBe(false);
expect(projectedResult.success).toBe(false);
if (!originalResult.success && !projectedResult.success)
expect(projectedResult.error.issues).toEqual(
originalResult.error.issues,
);
}
});
it("rejects incomplete round-trip payloads before invoking the handler", async () => {
await withCatalog(async (client, calls) => {
const tool = (await client.listTools()).tools.find(
({ name }) => name === "compare_web_captures",
);
if (tool === undefined)
throw new Error("Missing advertised comparison tool");
const incomplete = {
before: { inspection: {} },
after: { inspection: {} },
};
const validate = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(tool.inputSchema);
expect(validate(incomplete)).toBe(true);
expect(
browserCaptureComparisonInputSchema.safeParse(incomplete).success,
).toBe(false);
const result = await client.callTool({
name: tool.name,
arguments: incomplete,
});
expect(result.isError).toBe(true);
expect(calls.get(tool.name) ?? 0).toBe(0);
});
});
});
@@ -3,58 +3,6 @@ import { expect, it } from "vitest";
import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js"; import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js";
import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js"; import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { GENERATED_AUXILIARY_PROVIDERS } from "../../../src/generatedMcpToolCatalog.js";
it.each(["darwin", "linux", "win32"] as const)(
"preserves canonical auxiliary metadata and host facts on %s",
async (platform) => {
const declarations = auxiliaryAnalysisProviderDeclarations(platform);
expect(declarations.map(({ identity }) => identity)).toEqual(
GENERATED_AUXILIARY_PROVIDERS.map(({ identity }) => identity),
);
const operations = new Set<string>();
for (const declaration of declarations) {
const canonical = GENERATED_AUXILIARY_PROVIDERS.find(
({ identity }) => identity.id === declaration.identity.id,
);
if (canonical === undefined)
throw new Error("Missing canonical provider");
const expected = canonical.capabilities.map((capability) => {
if (platform === "darwin") return capability;
if (capability.provider.id === "native-macos")
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Native macOS utilities require macOS.",
};
if (capability.operation === "inspect_asset_catalog")
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Apple asset catalogs require macOS assetutil.",
};
return capability;
});
expect(declaration.capabilities).toEqual(expected);
const first = await declaration.load();
const second = await declaration.load();
expect(first).not.toBe(second);
expect(first.identity()).toEqual(declaration.identity);
expect(first.capabilities()).toEqual(declaration.capabilities);
expect(second.capabilities()).toEqual(declaration.capabilities);
for (const capability of declaration.capabilities) {
expect(operations.has(capability.operation)).toBe(false);
operations.add(capability.operation);
expect(Object.isFrozen(capability)).toBe(true);
expect(Object.isFrozen(capability.effects)).toBe(true);
expect(Object.isFrozen(capability.limitations)).toBe(true);
}
}
expect(operations.has("inspect_native_dispatch_metadata")).toBe(true);
},
);
it("keeps declaration discovery and client creation lazy and independently owned", async () => { it("keeps declaration discovery and client creation lazy and independently owned", async () => {
const target = await parseBinaryTarget(process.execPath); const target = await parseBinaryTarget(process.execPath);
@@ -40,6 +40,25 @@ describe("development test selection", () => {
expect(plan.vitestArguments).not.toContain("--changed"); expect(plan.vitestArguments).not.toContain("--changed");
}); });
it("prepares test catalogs and managed evidence only for their consumers", () => {
const ordinary = developmentTestPlan(
parseDevelopmentTestRequest("focused", [
"tests/boundary/mcp/sessionMcp.test.ts",
]),
);
expect(ordinary.artifactTasks).toEqual([]);
const consumers = developmentTestPlan(
parseDevelopmentTestRequest("focused", [
"tests/boundary/mcp/toolSchemaValidity.test.ts",
"tests/boundary/filesystem/setupSkill.test.ts",
]),
);
expect(consumers.artifactTasks).toEqual([
"artifacts:mcp-catalog",
"artifacts:managed-evidence",
]);
});
it("uses the resolved branch merge base for committed source changes", () => { it("uses the resolved branch merge base for committed source changes", () => {
const request = parseDevelopmentTestRequest("changed", ["--base", "main"]); const request = parseDevelopmentTestRequest("changed", ["--base", "main"]);
const plan = developmentTestPlan(request, "a".repeat(40)); const plan = developmentTestPlan(request, "a".repeat(40));
-10
View File
@@ -159,18 +159,8 @@ describe("incremental module import boundaries", () => {
["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false], ["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false],
["src/server/probe.ts", "../composition/android.js", true], ["src/server/probe.ts", "../composition/android.js", true],
["src/application/probe.ts", "../browser/CdpCaptureValues.js", true], ["src/application/probe.ts", "../browser/CdpCaptureValues.js", true],
["src/hopper/probe.ts", "../generatedMcpToolCatalog.js", false],
["src/ghidra/probe.ts", "../generatedMcpToolCatalog.ts", false],
["src/ida/probe.ts", "../generatedMcpToolCatalog.js", false],
["src/artifacts/probe.ts", "../generatedMcpToolCatalog.js", false],
["src/browser/probe.ts", "../generatedMcpToolCatalog.js", false],
["src/ghidra/probe.ts", "../generatedPackageMetadata.js", true], ["src/ghidra/probe.ts", "../generatedPackageMetadata.js", true],
["src/hopper/probe.ts", "../contracts/officialToolContracts.js", true], ["src/hopper/probe.ts", "../contracts/officialToolContracts.js", true],
[
"src/application/binary/probe.ts",
"../../generatedMcpToolCatalog.js",
true,
],
])("checks %s importing %s", async (file, dependency, allowed) => { ])("checks %s importing %s", async (file, dependency, allowed) => {
const temporary = await createTestTempDirectory("rea-module-boundary-"); const temporary = await createTestTempDirectory("rea-module-boundary-");
const fixture = join(temporary, file); const fixture = join(temporary, file);
+2 -1
View File
@@ -1,3 +1,4 @@
import { silentLogger } from "../../dist/logger.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js"; import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js";
@@ -22,7 +23,7 @@ if (process.argv[2] === "reversed") {
const { createServer } = await import("../../dist/server/createServer.js"); const { createServer } = await import("../../dist/server/createServer.js");
const config = parseConfig({}); const config = parseConfig({});
if (!config.ok) throw config.error; if (!config.ok) throw config.error;
const session = createBinarySession(config.value); const session = createBinarySession(config.value, silentLogger, {});
const server = createServer(session, session); const server = createServer(session, session);
const client = new Client({ name: "contract-presentation", version: "1" }); const client = new Client({ name: "contract-presentation", version: "1" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+65
View File
@@ -0,0 +1,65 @@
import { readFileSync } from "node:fs";
import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type { CapabilityDescriptor } from "../../src/application/AnalysisProvider.js";
import type { ToolKind } from "../../src/contracts/toolContractTypes.js";
import type { ToolEffects } from "../../src/contracts/toolEffects.js";
interface GeneratedTool {
readonly name: string;
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
readonly title: string;
readonly description: string;
readonly kind: ToolKind;
readonly requiresSession: boolean;
readonly inputSchema: Readonly<Record<string, unknown>>;
readonly outputSchema: Readonly<Record<string, unknown>>;
readonly annotations: ToolAnnotations;
readonly effects: ToolEffects;
}
interface GeneratedPayload {
readonly catalog: readonly GeneratedTool[];
}
const generatedPayload: unknown = JSON.parse(
readFileSync(
new URL("../../.cache/mcp-tool-catalog.json", import.meta.url),
"utf8",
),
);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError(
"Generated MCP catalog payload is invalid; run npm run mcp-catalog:generate",
);
/** Canonical schemas advertised through the pinned MCP SDK. */
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
return (
isRecord(value) &&
Array.isArray(value.catalog) &&
value.catalog.every(isGeneratedTool)
);
}
function isGeneratedTool(value: unknown): value is GeneratedTool {
return (
isRecord(value) &&
typeof value.name === "string" &&
(typeof value.analysisOperation === "string" ||
value.analysisOperation === null) &&
typeof value.title === "string" &&
typeof value.description === "string" &&
typeof value.kind === "string" &&
typeof value.requiresSession === "boolean" &&
isRecord(value.inputSchema) &&
isRecord(value.outputSchema) &&
isRecord(value.annotations) &&
isRecord(value.effects)
);
}
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+206 -40
View File
@@ -1,58 +1,224 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { access } from "node:fs/promises"; import { readFile, utimes, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { setTimeout as delay } from "node:timers/promises";
import { expect, it } from "vitest"; import { expect, it } from "vitest";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const runnerPath = fileURLToPath(
new URL("../../scripts/run-exclusive.mjs", import.meta.url),
);
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"escalates cancellation when an owned command ignores SIGTERM", "holds ownership until an uncooperative command has finished cancellation",
async () => { async () => {
const lockName = `runner-cancel-${String(process.pid)}`; const directory = await createTestTempDirectory("rea-runner-cancel-");
const lockPath = join(".cache", "rea-command-locks", `${lockName}.lock`); const runner = launch(
const runner = spawn( directory,
process.execPath,
[
"scripts/run-exclusive.mjs",
lockName,
process.execPath,
"-e",
'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);', 'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);',
],
{ stdio: ["ignore", "pipe", "pipe"] },
); );
let output = "";
const ready = new Promise<void>((resolveReady) => {
runner.stdout.on("data", (chunk) => {
output += chunk.toString();
if (output.includes("ready")) resolveReady();
});
});
const closed = new Promise<{
code: number | null;
signal: NodeJS.Signals | null;
}>((resolveClosed) => {
runner.once("close", (code, signal) => resolveClosed({ code, signal }));
});
try { try {
await withTimeout(ready, "runner child did not start"); await runner.ready;
runner.kill("SIGTERM"); runner.child.kill("SIGTERM");
const result = await withTimeout( expect(await runner.closed).toMatchObject({ code: 143, signal: null });
closed, const replacement = launch(directory, 'process.stdout.write("ready");');
"runner did not finish cancellation", expect(await replacement.closed).toMatchObject({ code: 0, signal: null });
);
expect(result).toEqual({ code: 143, signal: null });
} finally { } finally {
if (runner.exitCode === null) runner.kill("SIGKILL"); if (runner.child.exitCode === null) runner.child.kill("SIGKILL");
await withTimeout(closed, "runner cleanup did not finish"); await runner.closed;
} }
expect(output).toContain("ready");
await expect(access(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
}, },
); );
it.runIf(process.platform !== "win32")(
"retains the lock when the command exits but a descendant ignores cancellation",
async () => {
const directory = await createTestTempDirectory("rea-runner-descendant-");
const pidPath = join(directory, "descendant.pid");
const activityPath = join(directory, "activity.log");
const descendant = `
const fs = require("node:fs");
process.on("SIGTERM", () => {});
fs.writeFileSync(${JSON.stringify(activityPath)}, "active\\n");
fs.writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));
setInterval(() => fs.appendFileSync(${JSON.stringify(activityPath)}, "active\\n"), 20);
`;
const runner = launch(
directory,
`
const fs = require("node:fs");
require("node:child_process").spawn(process.execPath, ["-e", ${JSON.stringify(descendant)}], { stdio: "ignore" });
const ready = setInterval(() => {
if (fs.existsSync(${JSON.stringify(pidPath)})) {
clearInterval(ready);
process.stdout.write("ready");
}
}, 10);
setInterval(() => {}, 1000);
`,
);
let descendantPid: number | undefined;
try {
await runner.ready;
descendantPid = Number(await readFile(pidPath, "utf8"));
runner.child.kill("SIGTERM");
const contender = launch(
directory,
'process.stdout.write("unexpected");',
);
expect(await contender.closed).toMatchObject({
code: 1,
stderr: expect.stringContaining("already running"),
});
expect(await runner.closed).toMatchObject({ code: 143 });
const stopped = await readFile(activityPath, "utf8");
await delay(100);
expect(await readFile(activityPath, "utf8")).toBe(stopped);
expect(
await launch(directory, 'process.stdout.write("done");').closed,
).toMatchObject({ code: 0 });
} finally {
if (runner.child.exitCode === null) runner.child.kill("SIGTERM");
await runner.closed;
// Also clean up when this regression is run against the broken runner.
if (descendantPid !== undefined) stopIfPresent(descendantPid);
}
},
);
it("does not treat an old database timestamp as permission to replace a live owner", async () => {
const directory = await createTestTempDirectory("rea-runner-live-");
const runner = launch(
directory,
'process.stdout.write("ready"); setTimeout(() => {}, 1500);',
);
try {
await runner.ready;
const old = new Date("2000-01-01T00:00:00Z");
await utimes(
join(directory, ".cache/rea-command-locks/test.sqlite"),
old,
old,
);
const contender = launch(directory, 'process.stdout.write("unexpected");');
expect(await contender.closed).toMatchObject({
code: 1,
stderr: expect.stringContaining("already running"),
stdout: "",
});
expect(await runner.closed).toMatchObject({ code: 0 });
} finally {
if (runner.child.exitCode === null) runner.child.kill("SIGKILL");
await runner.closed;
}
});
it.runIf(process.platform !== "win32")(
"recovers after abrupt owner death and keeps simultaneous contenders exclusive",
async () => {
const directory = await createTestTempDirectory("rea-runner-recover-");
// Killing the public runner disconnects its supervisor, which retains
// ownership while cancelling the group before automatic OS lock release.
const interrupted = launch(
directory,
'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);',
);
await interrupted.ready;
interrupted.child.kill("SIGKILL");
expect(await interrupted.closed).toMatchObject({
code: null,
signal: "SIGKILL",
});
const activityPath = join(directory, "active");
const logPath = join(directory, "activity.log");
await writeFile(logPath, "");
const command = `
const fs = require("node:fs");
const active = ${JSON.stringify(activityPath)};
const log = ${JSON.stringify(logPath)};
try { fs.closeSync(fs.openSync(active, "wx")); }
catch { process.stderr.write("commands overlapped"); process.exit(77); }
fs.appendFileSync(log, "start\\n");
setTimeout(() => {
fs.appendFileSync(log, "end\\n");
fs.unlinkSync(active);
}, 250);
`;
const results = await Promise.all(
Array.from({ length: 8 }, () => launch(directory, command).closed),
);
expect(results.some(({ code }) => code === 0)).toBe(true);
for (const result of results) {
expect([0, 1]).toContain(result.code);
if (result.code === 1) expect(result.stderr).toContain("already running");
expect(result.stderr).not.toContain("commands overlapped");
}
const events = (await readFile(logPath, "utf8")).trim().split("\n");
expect(events.length).toBeGreaterThan(0);
for (let index = 0; index < events.length; index += 2)
expect(events.slice(index, index + 2)).toEqual(["start", "end"]);
expect(
await launch(directory, 'process.stdout.write("done");').closed,
).toMatchObject({ code: 0, stdout: "done" });
},
);
function stopIfPresent(pid: number): void {
try {
process.kill(pid, "SIGKILL");
} catch (cause) {
if (
!(cause instanceof Error) ||
!("code" in cause) ||
cause.code !== "ESRCH"
)
throw cause;
}
}
function launch(directory: string, command: string) {
const child = spawn(
process.execPath,
[runnerPath, "test", process.execPath, "-e", command],
{ cwd: directory, stdio: ["ignore", "pipe", "pipe"] },
);
let stdout = "";
let stderr = "";
let notifyReady: (() => void) | undefined;
const ready = withTimeout(
new Promise<void>((resolveReady) => {
notifyReady = resolveReady;
}),
"runner child did not start",
);
// Some scenarios intentionally never print a ready marker.
void ready.catch(() => {});
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
if (stdout.includes("ready")) notifyReady?.();
});
child.stderr.on("data", (chunk) => (stderr += chunk.toString()));
const closed = withTimeout(
new Promise<{
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}>((resolveClosed, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => {
notifyReady?.();
resolveClosed({ code, signal, stdout, stderr });
});
}),
"runner did not finish",
);
return { child, ready, closed };
}
function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> { function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined; let timeout: NodeJS.Timeout | undefined;
return Promise.race([ return Promise.race([
-3
View File
@@ -1,14 +1,11 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"incremental": true,
"rootDir": "src", "rootDir": "src",
"tsBuildInfoFile": ".cache/tsconfig.build.tsbuildinfo",
"types": ["node"] "types": ["node"]
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": [ "exclude": [
"src/generatedMcpToolCatalog.ts",
"src/**/*.fixture.ts", "src/**/*.fixture.ts",
"src/**/*.test.ts", "src/**/*.test.ts",
"tests", "tests",
-2
View File
@@ -15,8 +15,6 @@
"rootDir": ".", "rootDir": ".",
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"incremental": true,
"tsBuildInfoFile": ".cache/tsconfig.typecheck.tsbuildinfo",
"target": "ES2023", "target": "ES2023",
"types": ["node"], "types": ["node"],
"verbatimModuleSyntax": true "verbatimModuleSyntax": true
+138 -29
View File
@@ -1,42 +1,24 @@
{ {
"$schema": "https://turborepo.dev/schema.json", "$schema": "https://turborepo.dev/schema.json",
"tasks": { "tasks": {
"build": { "compile": {
"dependsOn": ["metadata:check"],
"inputs": [ "inputs": [
".nvmrc", ".nvmrc",
"package.json", "package.json",
"package-lock.json", "package-lock.json",
"scripts/check-dependency-install.mjs", "scripts/check-dependency-install.mjs",
"scripts/clean-build-output.mjs", "scripts/clean-build-output.mjs",
"scripts/generate-mcp-tool-catalog.mjs",
"scripts/generate-package-metadata.mjs", "scripts/generate-package-metadata.mjs",
"scripts/generate-skill-metadata.mjs",
"scripts/generate-product-catalog.mjs",
"scripts/generate-completion-ledger.mjs",
"scripts/lib/**/*.mjs",
"scripts/verify/managed/**",
"scripts/fixtures/managed/**",
".oxfmtrc.json",
"skill-src/**",
"src/**/*.ts", "src/**/*.ts",
"!src/**/*.test.ts", "!src/**/*.test.ts",
"!src/**/*.fixture.ts", "!src/**/*.fixture.ts",
"!src/generatedMcpToolCatalog.ts",
"tsconfig.json", "tsconfig.json",
"tsconfig.build.json" "tsconfig.build.json"
], ],
"outputs": [ "outputs": ["dist/**"]
"dist/**",
".cache/tsconfig.build.tsbuildinfo",
"src/generatedMcpToolCatalog.ts",
"skills/**",
"docs/public/product-catalog.json",
"docs/verification/managed-conformance-manifest.json",
"docs/verification/managed-conformance-ledger.json"
]
}, },
"typecheck": { "typecheck": {
"dependsOn": ["build"],
"inputs": [ "inputs": [
".nvmrc", ".nvmrc",
"package.json", "package.json",
@@ -46,9 +28,10 @@
"tsconfig.json", "tsconfig.json",
"tsconfig.build.json", "tsconfig.build.json",
"vitest.config.ts", "vitest.config.ts",
"scripts/**/*.d.mts" "scripts/**/*.d.mts",
"docs/.vitepress/config.ts"
], ],
"outputs": [".cache/tsconfig.typecheck.tsbuildinfo"] "outputs": []
}, },
"metadata:check": { "metadata:check": {
"inputs": [ "inputs": [
@@ -74,7 +57,7 @@
}, },
"format:check": {}, "format:check": {},
"knip": { "knip": {
"dependsOn": ["build"], "dependsOn": ["compile"],
"inputs": [ "inputs": [
".nvmrc", ".nvmrc",
"knip.json", "knip.json",
@@ -106,7 +89,12 @@
] ]
}, },
"test:run": { "test:run": {
"dependsOn": ["build"], "dependsOn": [
"artifacts:mcp-catalog",
"artifacts:product-catalog",
"artifacts:managed-evidence",
"artifacts:skills"
],
"inputs": [ "inputs": [
".nvmrc", ".nvmrc",
"package.json", "package.json",
@@ -115,11 +103,25 @@
"tests/**", "tests/**",
"scripts/**", "scripts/**",
"bridge/**", "bridge/**",
"vitest.config.ts" "vitest.config.ts",
{
"mode": "dependencyOutputs",
"globs": [
".cache/mcp-tool-catalog.json",
"skills/**",
"docs/public/product-catalog.json",
"docs/verification/managed-conformance-manifest.json",
"docs/verification/managed-conformance-ledger.json"
]
}
] ]
}, },
"docs:check:built": { "docs:check:built": {
"dependsOn": ["build"], "dependsOn": [
"artifacts:product-catalog",
"artifacts:managed-evidence",
"artifacts:skills"
],
"inputs": [ "inputs": [
".nvmrc", ".nvmrc",
"package.json", "package.json",
@@ -134,9 +136,116 @@
"src/generated*.ts", "src/generated*.ts",
"src/contracts/**/*.ts", "src/contracts/**/*.ts",
"docs/**", "docs/**",
"!docs/public/product-catalog.json",
"!docs/verification/managed-conformance-*.json",
"skill-src/**", "skill-src/**",
"skills/**/SKILL.md", "README.md",
"README.md" {
"mode": "dependencyOutputs",
"globs": [
"skills/**",
"docs/public/product-catalog.json",
"docs/verification/managed-conformance-manifest.json",
"docs/verification/managed-conformance-ledger.json"
]
}
]
},
"artifacts:mcp-catalog": {
"dependsOn": ["compile"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-mcp-tool-catalog.mjs",
"scripts/lib/generated-file.mjs"
],
"outputs": [".cache/mcp-tool-catalog.json"]
},
"artifacts:skills": {
"dependsOn": ["compile"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-skill-metadata.mjs",
"scripts/lib/generated-file.mjs",
"skill-src/**"
],
"outputs": ["skills/**"]
},
"artifacts:product-catalog": {
"dependsOn": ["compile"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-product-catalog.mjs",
"scripts/lib/**/*.mjs",
".oxfmtrc.json"
],
"outputs": ["docs/public/product-catalog.json"]
},
"artifacts:managed-evidence": {
"dependsOn": ["artifacts:skills"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-completion-ledger.mjs",
"scripts/lib/generated-file.mjs",
"scripts/verify/managed/**",
"scripts/fixtures/managed/**"
],
"outputs": [
"docs/verification/managed-conformance-manifest.json",
"docs/verification/managed-conformance-ledger.json"
]
},
"artifacts:error-schema": {
"dependsOn": ["compile"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-error-schema.mjs",
"scripts/lib/generated-file.mjs",
"scripts/lib/format-generated-file.mjs",
".oxfmtrc.json"
],
"outputs": ["docs/error-contract.schema.json"]
},
"mcp-catalog:check:built": {
"dependsOn": ["artifacts:mcp-catalog"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-mcp-tool-catalog.mjs",
"scripts/lib/generated-file.mjs",
{
"mode": "dependencyOutputs",
"globs": [".cache/mcp-tool-catalog.json"]
}
]
},
"evidence:check:built": {
"dependsOn": ["artifacts:managed-evidence"],
"inputs": [
".nvmrc",
"package.json",
"package-lock.json",
"scripts/generate-completion-ledger.mjs",
"scripts/lib/generated-file.mjs",
"scripts/verify/managed/**",
"scripts/fixtures/managed/**",
{
"mode": "dependencyOutputs",
"globs": [
"docs/verification/managed-conformance-manifest.json",
"docs/verification/managed-conformance-ledger.json"
]
}
] ]
} }
} }