diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0fac53db1..4c536b4e8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,10 +33,18 @@ jobs:
working-directory: packages/types
run: pnpm build
+ - name: Build artifact-canvas package (+ build-smoke)
+ working-directory: packages/artifact-canvas
+ run: pnpm build && pnpm build:smoke
+
- name: Run core unit tests
working-directory: packages/core
run: pnpm test
+ - name: Run artifact-canvas unit tests
+ working-directory: packages/artifact-canvas
+ run: pnpm test
+
- name: Copy skeleton for unit tests
working-directory: packages/codev
run: pnpm copy-skeleton
diff --git a/.gitignore b/.gitignore
index 900735c1e..f9a8afb02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ packages/codev/dist/
packages/codev/skeleton/
packages/types/dist/
packages/core/dist/
+packages/artifact-canvas/dist/
packages/vscode/dist/
packages/vscode/out/
*.vsix
diff --git a/codev/plans/945-build-foundational-reusable-pa.md b/codev/plans/945-build-foundational-reusable-pa.md
new file mode 100644
index 000000000..1d87c865a
--- /dev/null
+++ b/codev/plans/945-build-foundational-reusable-pa.md
@@ -0,0 +1,428 @@
+---
+approved: 2026-06-10
+approval_note: >-
+ Approved at the SPIR plan-approval gate (status.yaml: approved_at 2026-06-10). 5 plan consult
+ iterations — Claude APPROVE x5; Codex REQUEST_CHANGES x5 (progressively smaller build/test/release
+ wiring items, the last two self-inflicted by revisions); Gemini lane unavailable (agy). Per the
+ human decision recorded in the Consultation Log below, the final two Codex items were fixed and the
+ plan taken to the gate rather than a 6th consult round.
+validated: [claude] # plan iter-5 APPROVE; codex REQUEST_CHANGES resolved pre-gate; gemini lane unavailable
+---
+
+# Plan: Foundational reusable package `@cluesmith/codev-artifact-canvas`
+
+## Metadata
+- **ID**: plan-2026-06-09-945-build-foundational-reusable-pa
+- **Status**: approved
+- **Specification**: [codev/specs/945-build-foundational-reusable-pa.md](../specs/945-build-foundational-reusable-pa.md)
+- **GitHub Issue**: [#945](https://github.com/cluesmith/codev/issues/945)
+- **Created**: 2026-06-09
+
+## Executive Summary
+
+Build the shared library `@cluesmith/codev-artifact-canvas` (Approach A in the spec: one
+React package + per-host adapter seams). The work splits into four committable phases:
+(1) package skeleton + dual-format build + locked interfaces + theme tokens; (2) the
+markdown renderer with `data-line` mapping + D7 sanitization; (3) the comment overlay
+(intent-only) + v1 marker rendering + adapter wire-up + auto-refresh; (4) the smoke-test
+host + README + cross-cutting tests. No host integration ships here (that's #859 / the
+dashboard route / mobile).
+
+This plan also **resolves the five items deferred from the spec consult** (the plan-gate
+acceptance criteria) — see the **Deferred-Item Resolutions** section, which maps each to the
+phase that closes it.
+
+## Locked plan-level decisions (closing spec Open Questions §3/§4)
+
+- **Build tool = `tsup`** (closes spec Open Q §3). It emits CJS + ESM + `.d.ts` from one
+ config with minimal setup, handles TSX, and can bundle/copy the stylesheet. Vite library
+ mode and raw esbuild were the alternatives; tsup is the lightest path to the spec's required
+ dual-format output. The build-smoke test (a CJS `require()` + an ESM `import()`) guards it.
+- **`default-theme.css` ships as a separate export path** (closes spec Open Q §4):
+ `@cluesmith/codev-artifact-canvas/default-theme.css`. Explicit, host-overridable, not
+ auto-injected — hosts opt in via ``/import and override the `--codev-canvas-*` vars.
+
+## Success Metrics
+- [ ] All spec acceptance criteria met (functional + non-functional).
+- [ ] All 5 deferred items resolved or consciously decided (see Deferred-Item Resolutions).
+- [ ] Package source has zero `vscode` / `node:*` / direct `fs`/`fetch` imports (import-boundary test green).
+- [ ] Dual CJS+ESM bundle + `.d.ts` builds; build-smoke test green.
+- [ ] New package `test` script green and wired into the monorepo build graph.
+- [ ] No regression to #857 (editor-side review flow untouched).
+
+## Phases (Machine Readable)
+
+```json
+{
+ "phases": [
+ {"id": "phase_1", "title": "Package skeleton, dual-format build, locked interfaces, theme tokens"},
+ {"id": "phase_2", "title": "Markdown renderer: data-line mapping + DOMPurify sanitization"},
+ {"id": "phase_3", "title": "Comment overlay (intent-only) + v1 marker rendering + adapter wire-up"},
+ {"id": "phase_4", "title": "Smoke-test host, README, cross-cutting tests"}
+ ]
+}
+```
+
+## Deferred-Item Resolutions (plan-gate acceptance criteria)
+
+| # | Source | Item | Resolution | Phase |
+|---|---|---|---|---|
+| 1 | Codex | D2 "injectable logger" claim has no matching prop | **Drop the injectability claim.** Internal diagnostics go to `console`; the host-facing hook is the existing `onError?(err)` prop in `ArtifactCanvasProps`. No logger prop added. Spec D2 text adjusted accordingly during this phase's doc pass. | P1 (contract) |
+| 2 | Codex | `ThemeAdapter.resolve` token format ambiguous | **Pin to the full custom-property name** — `resolve("--codev-canvas-foreground")`, 1:1 with the D4 vocabulary, no hidden bare-name mapping. Documented on the interface + README. | P1 (contract) |
+| 3 | Codex | Sanitization test doesn't exercise DOMPurify (`html:false` neutralizes ``, a `javascript:` URL) renders
+with that content neutralized — no script executes and no event-handler attribute survives.
+
+## Solution Approaches (alternatives considered)
+
+### Approach A — Shared React package + per-host adapters *(chosen)*
+Build the renderer, overlay, and adapter seams once; hosts implement three adapters.
+- **Pros:** one renderer/overlay to maintain; UX parity across surfaces by construction;
+ makes #859 thin and the dashboard route + mobile cheap; the adapter seam is the natural
+ test boundary.
+- **Cons:** introduces the repo's first React-component-library package and first dual-format
+ build; the contract must be right up front (mitigated by this spec + the smoke-test host).
+- **Complexity:** Medium. **Risk:** Medium (contract lock-in) — addressed by SPIR's gates.
+
+### Approach B — Framework-agnostic Web Components *(rejected)*
+- **Pros:** host-framework-neutral; embeddable anywhere.
+- **Cons:** throws away the dashboard's React investment; React↔custom-element interop and
+ styling/theming friction; the team's component idioms are React. The issue explicitly
+ rejects this.
+
+### Approach C — Build per surface, no shared package *(rejected — the status quo trap)*
+- **Pros:** each surface optimal in isolation; no new package.
+- **Cons:** three renderers + three overlays to maintain; UX divergence; every #858–#863
+ feature implemented up to three times. This is exactly what the issue exists to prevent.
+
+### Approach D — Extend VSCode's built-in markdown preview *(rejected — infeasible)*
+#859 already established the built-in preview's contribution points are render-only with no
+host back-channel, so comment-from-preview is impossible without owning the surface.
+
+## Open Questions
+
+### Critical (blocks progress) — none
+All decisions needed to begin are resolved above.
+
+### Important (affects design)
+
+1. **REVIEW marker format reconciliation.** The issue body states the marker form is
+ `` and calls it "the existing convention from #857".
+ **Codebase verification shows that is not the current convention** — #857 writes positional
+ `` (line implied by file position; regex captures author +
+ text only). **Proposed resolution (per D3):** the package stays serialization-agnostic; the
+ in-memory `ReviewMarker` carries `line` (derived from position on read) and `raw` (for
+ round-tripping). The VSCode host preserves the positional #857 form (satisfying the
+ "no regression" AC); a host that wants explicit `line=N`/`lines=N-M` may opt in without the
+ package mandating it. *This will be raised with the architect at the spec-approval gate so
+ the "existing convention" wording can be confirmed or corrected.*
+
+2. **Smoke-test host form.** Issue leaves it to the implementer: a Vite dev-server route or a
+ minimal VSCode webview. **Proposed:** a Vite dev-server harness inside the package
+ (`examples/`), since it exercises the ESM build and the React components without VSCode
+ tooling, runs in CI headlessly, and doubles as living adapter-implementation documentation.
+
+### Nice-to-know (optimization)
+
+3. **Build tool for the dual bundle** (`tsup` vs Vite library mode vs raw esbuild). A plan
+ decision; the spec only requires the CJS+ESM+types+CSS output.
+4. **Whether `default-theme.css` ships as a separate import path** (`.../default-theme.css`)
+ vs auto-injected. Leaning separate import (explicit, tree-shakeable, host-overridable).
+
+## Success Criteria / Acceptance Criteria
+
+Functional (MUST):
+- [ ] `packages/artifact-canvas/` exists; `package.json` declares
+ `@cluesmith/codev-artifact-canvas`, peer-deps on `react`/`react-dom`, deps on
+ `markdown-it` and `dompurify`.
+- [ ] Renderer produces HTML with `data-line` attributes on block tokens (paragraphs,
+ headings, list items, code blocks, blockquotes, tables); a unit test covers the
+ attribution.
+- [ ] A comment-overlay component renders a hover-`+` on rendered blocks; clicking invokes the
+ **`onAddComment(line: number)`** prop (the canonical intent seam, `ArtifactCanvasProps`);
+ the package does **not** call `MarkerAdapter.add` (text-input + write-back are host-owned,
+ D6). A unit test asserts the intent-prop contract.
+- [ ] The hover-`+` affordance is **keyboard-accessible** — reachable via keyboard focus and
+ activatable with Enter/Space (not hover-only), with an accessible label for screen
+ readers; a test covers keyboard activation. (iter-2 Claude)
+- [ ] The public API exports the three adapter interfaces (`FileAdapter`, `MarkerAdapter`,
+ `ThemeAdapter`), the data types `ReviewMarker` and `Disposable`, and the component props
+ `ArtifactCanvasProps`; the package has zero direct filesystem, `fetch`, or VSCode-API
+ imports (import-boundary test).
+- [ ] Theming via CSS custom properties; the package ships a default stylesheet defining a
+ fallback for **each v1 `--codev-canvas-*` token (D4)**; hosts override by setting the
+ variables; documented host override examples. `ThemeAdapter.resolve()` is JS-side only
+ (D4, Model A) and is not on the v1 render path.
+- [ ] A smoke-test host demonstrates end-to-end: load sample markdown → render → hover →
+ click `+` → adapter receives the call → marker round-trips.
+- [ ] Build produces a **CJS + ESM** bundle (with type declarations) consumable by both a
+ VSCode webview and the dashboard's Vite/ESM pipeline.
+- [ ] **Text-as-source-of-truth invariant test**: no affordance produces output that isn't
+ either a source-markdown text mutation or a clearly delimited adjacent text artifact.
+ (For v1 concretely: the comment overlay's only output channel is the `onAddComment`
+ intent event — no side-channel writes.)
+- [ ] **HTML-sanitization (D7)**: markdown-it runs with `html: false` and output is
+ DOMPurify-sanitized before render; rendered HTML contains **no executable script
+ content** even when the input markdown attempts to embed it (`
+