--- id: PX-PLAN-RMJ8BIOX type: plan title: Built Extension Packages spec: PX-SPEC-DZLVGRW4 status: approved --- ## Outcome Pi loads every extension from a self-contained generated package under `dist/` produced by Rolldown from the extension's own source. The repository root stops being a Pi package. Development builds are on-demand, focused, atomic, and reloadable. Verified current state: root `package.json` carries `pi.extensions` globbing `extensions/*/index.ts`; user settings list `~/Workspace/pi-ext` as a package; Pi loads TypeScript through jiti; no bundler dependency; no `dist/`. ## Behaviors Order is delivery value; each behavior crosses build, manifest, settings, and verification. ```mermaid flowchart TB b1["1. ultra loads from dist/ultra"] --> b2["2. Focused rebuild replaces one package atomically"] b1 --> b3["3. the-system bundles third-party deps"] b2 --> b4["4. Whole collection loads from explicit dist entries; root not a package"] b3 --> b4 b4 --> b5["5. Every package loads relocated with zero unexpected diagnostics"] ``` 1. Pilot `extensions/ultra`: Rolldown build emits `dist/ultra/` with `package.json`, entry chunk, package-private lazy chunks for `implementation.ts`, worker chunk for `dynamic-validator-worker.ts`, copied `prompts/`, and a generated `pi` manifest declaring only the entry and prompts. User settings gain one explicit local package entry for `dist/ultra`; the root package entry stays until behavior 4. Proof: Pi with only `dist/ultra` configured runs `run_workflow` and dynamic validation. 2. Build task `//:build-ext ` (Mise → root npm script → Node build driver) writes into a staging directory, then swaps into `dist/` only after all chunks and resources exist. Interrupted builds leave the previous package intact. Content-addressed chunk names let a loaded extension finish lazy imports across a swap; stale chunks are pruned on the next successful build. Development build keeps sourcemaps and no minification. 3. Pilot `extensions/the-system`: bundle `mermaid`, `unified`, `rehype-*`, `remark-*`, `minisearch`, `dagre`, `yaml` into package-private chunks; copy `skills/`; declare `pi.skills` explicitly; keep Node built-ins, `@earendil-works/*`, and `typebox` external. Any dependency left external requires a recorded incompatibility note in the build config. Proof: `dist/the-system` renders `/system view` and `/system book` outside the checkout with no `node_modules` ancestor. 4. Extend the build driver to all extensions; `pi.extensions`, `pi.skills`, `pi.prompts`, `pi.themes` removed from root `package.json`; `scripts/extensions.mjs` static check inverted to forbid root Pi keys and require per-extension declared resources. Codegen adds a settings snippet or `//:settings-entries` task that prints the explicit `dist/` package list; the human applies it to `~/.pi/agent/settings.json`. Resources: `firefox-bidi/skills`, `pfui/skills`, `mockup/guidance` (internal, not exposed), `strata/assets`, executable helpers from `pi-ext-executable` targets. 5. Relocation gate: copy each `dist/` to a temp directory without ancestors, load through Pi on Node, assert zero extension errors, working lazy first use, worker start, and manifest-declared resources only. Run as `//:verify-packages`; per-extension via `//extensions/:verify-package`. ## Module structure ```mermaid flowchart LR src["extensions/name/*.ts + resources"] --> driver["scripts/build/driver (Rolldown API)"] policy["scripts/build/policy: externals, resources, entries"] --> driver driver --> staging["dist/.staging/name"] -->|atomic swap| pkg["dist/name"] pkg --> manifest["dist/name/package.json pi.*"] pkg --> settings["user settings packages[]"] pkg --> verify["scripts/verify-packages: relocate + Pi load"] ``` Touched: `package.json` (add `rolldown` devDependency, remove `pi.*`), `mise.toml` (`build-ext`, `build-all`, `verify-packages`), `scripts/build/*`, `scripts/extensions.mjs` static check, each `extensions//package.json` (declared resources, `main` stays source for tests), `tsconfig.json` include for build scripts, `.gitignore` for `dist/`. ## Constraints - Pi's jiti importer stays; packages are plain ESM JavaScript that jiti passes through. - No lifecycle scripts in generated `package.json`. - Generated manifest derives from extension `package.json` declarations; the source manifest remains the single authority. - `extensions/*/package.json` keep `main: index.ts` for source tests; generated packages set their own `main`. - Watcher (optional) calls the same build; no Pi protocol. ## Verification - [ ] `dist/ultra` alone runs a workflow with a dynamic extension after relocation. - [ ] Interrupting `build-ext` mid-write leaves the previous `dist/` loadable. - [ ] `dist/the-system` has no external third-party import and renders Markdown with Mermaid outside the checkout. - [ ] Root `package.json` has no `pi` key; static check fails if it returns. - [ ] `verify-packages` passes for all extensions and for the full collection with no identity, tool, command, flag, or resource conflicts. - [ ] Focused `//extensions/:check` builds only that extension.