--- name: odc-instruments description: Work on instruments and the runtime they import — adding a built-in instrument, vendoring a library under vendor/, or changing instrument-bundler, runtime-bundler, runtime-core, runtime-internal, runtime-meta, instrument-interpreter, vite-plugin-runtime, instrument-library or runtime/v1. Use also when an instrument builds but never appears, an import of /runtime/v1/… does not resolve, or a build logs ResolverError. --- Everything in this area is a **published contract**. `packages/runtime-core` is the API instrument authors import, and `runtime/v1` is the library catalog their bundles fetch by URL at runtime — changing an exported type changes every instrument in the wild, including instruments in repositories this monorepo never sees. **Name the blast radius in your reply before the first edit.** Most of the paths below sit on `review-pr`'s automatic `REQUIRES_HUMAN_REVIEW` list — check yours against the roster in `.agents/skills/review-pr/REVIEW.md`. This is the area where a clean-looking diff still goes to the user. ## Which workspace owns the change The names collide far more than the jobs do. | The change | Workspace | | ------------------------------------------------------------------------------ | --------------------------------- | | The API authors write — `defineInstrument`, instrument types, `Translator` | `packages/runtime-core` | | Which libraries an instrument may import, and at which major version | `vendor/` **and** `runtime/v1` | | One author's own source becoming a bundle string | `packages/instrument-bundler` | | The libraries that source imports, built into `runtime/v1/dist` | `packages/runtime-bundler` | | The interactive iframe, its bootstrap and service worker, `evaluateInstrument` | `packages/runtime-internal` | | A built runtime directory mapped onto HTTP URLs | `packages/runtime-meta` | | Serving `/runtime/v1/...` to a Vite app | `packages/vite-plugin-runtime` | | Re-validating an evaluated bundle before it renders | `packages/instrument-interpreter` | | Validating an instrument definition at the API perimeter | `packages/schemas` | | A built-in instrument itself | `packages/instrument-library` | **Only `runtime/v1/dist` is generated, and there is no `src/`.** Changing what the runtime serves means editing `runtime/v1/runtime.config.js` or a wrapper under `vendor/`; `Bundler.bundle()` removes the outdir before writing (`packages/runtime-bundler/src/bundler.ts`), so anything placed in `dist/` by hand is gone on the next build. **A new field kind or variant in `runtime-core` lands in two more packages, and only one of them errors.** The `ts-pattern` matches in `packages/instrument-utils/src/translate.ts` end in `.exhaustive()`, so `pnpm lint` there stays red until the new case is handled. `packages/schemas/src/instrument/instrument.form.ts` hand-lists the same kinds twice — in `$StaticFieldKind` and in the `kind`-discriminated union — and its `satisfies z.ZodType<…>` does not catch an omitted member, so a kind missing there type-checks clean and surfaces as a 422 from `InstrumentsService.create` the first time someone uploads the instrument. ## Where it fails silently **A failed runtime build exits 0.** `runtime-bundler`'s CLI logs what `bundle()` threw and carries on (`packages/runtime-bundler/AGENTS.md`), so a broken runtime does not fail CI on its own. Read the build output: a run that never printed `Success!` failed. **Adding a library needs two files to agree, and only one direction errors.** An entry in `runtime/v1/runtime.config.js`'s `include` without the matching `runtime/v1/package.json` devDependency is a build-time `ResolverError`. The reverse is silent — the package installs and is never emitted — so confirm the emitted directory exists under `runtime/v1/dist` before believing the build. **A built instrument is not a loaded one.** The built-in catalog takes one hand-written default import and one `create` call per instrument in `apps/api/src/demo/demo.service.ts` (`.agents/docs/playbooks/add-instrument.md` is where that step lives). For an external repository, discovery scans `lib/{file,forms,interactive,series}` and returns `series` last, because a series is rejected unless the instruments it references are already stored — so a series whose items live in a _different_ repository still fails to import (`.agents/docs/architecture/instrument-pipeline.md`). **`/runtime/v1/zod@3.x` is the Zod v3 API.** `vendor/zod@3.x`'s `.` export re-exports `zod/v3`; the v4 API is a separate subpath, `/runtime/v1/zod@3.x/v4`. The repo-wide `no-restricted-imports` ban on bare `zod` (`eslint.config.js`) matches the bare specifier only and never sees a `/runtime/v1/...` URL, so in instrument source the subpath is yours to choose deliberately. ## Writing a `/runtime/v1/...` import Instrument source always writes `/runtime/v1/`, never a package name. Two other spellings address the same artifact and are not interchangeable with it — `#runtime/v1/*` in `apps/api`, `@opendatacapture/runtime-v1` for tooling that needs `dist` on disk. Full resolution rules, the `tsconfig` `paths` that type-check the first form, and the files that must stay in sync: `.agents/docs/architecture/runtime-and-vendor.md`. `instrument-bundler` marks any path starting with `/runtime/v1/` external without checking that it resolves (`packages/instrument-bundler/src/plugin.ts`), so a package that was never emitted, or a subpath the wrapper's `exports` does not define, bundles clean and 404s at render. In-repo instruments are caught by `tsc` through those `paths`; playground and external-repo instruments are not. All three spellings bottom out in `runtime/v1/dist`, which is gitignored — on a cold checkout an unresolvable import means `pnpm build` has not run yet (`.agents/skills/odc-run-locally/SKILL.md`). ## Where the procedure lives | Doing this | Open before you start | | ---------------------------------------- | -------------------------------------------------------------------------------------------- | | Adding a built-in instrument | `.agents/docs/playbooks/add-instrument.md` | | Vendoring a library | `.agents/docs/playbooks/add-vendor-package.md` — three registration points, the third silent | | Following a bundle from source to screen | `.agents/docs/architecture/instrument-pipeline.md` | | Telling the runtime workspaces apart | `.agents/docs/architecture/runtime-and-vendor.md` | | Authoring the instrument definition | `packages/instrument-guidelines/AGENTS.md` | The guidelines package's rules about the definition object apply to a built-in instrument; its file layout does not. A built-in is one flat directory, `packages/instrument-library/src///`, where a subdirectory fails the build with `Cannot infer loader` (`.agents/docs/playbooks/add-instrument.md`). ## Done when - Your reply named the surface and what an already-published instrument does differently after the change — stated before the first edit, and restated at the end against what actually changed. - The runtime build printed `Success!`, and any package you added has its own directory under `runtime/v1/dist`. - A field kind or variant that moved is handled in `packages/instrument-utils` (`pnpm lint` green there) **and** listed in `packages/schemas/src/instrument/` — nothing checks the second for you. - A new built-in instrument renders in a demo instance — `pnpm --filter @opendatacapture/instrument-library run available` listing it proves only that it built.