<!-- source: openspec/changes/add-specialist-tool-composition/proposal.md -->

# Add specialist tool composition — per-archetype tool/skill/agent palettes in the chat

> **⚠ Schema-rename overlay — RAP-1170 (Wave 1, 2026-05-22).** This change was drafted against the pre-rename `canvas_*` table names. Before implementing, apply the rename map from `openspec/current/canvas.md § Naming clarification`: `canvas_sessions → ideas`, `canvas_messages → messages`, `canvas_files → files`, `canvas_invitees → spec_invitees`, and `canvas_ideas` / `canvas_attachments` / `canvas_instructions` / `canvas_tool_uses` / `canvas_stage_events` / `canvas_daily_costs` dropped — see the post-rename schema in `openspec/current/db/entities.md § Project → spec → idea hierarchy`. Inline `canvas_*` references in this folder's `proposal.md` / `design.md` / `tasks.md` / `verification.md` are deliberately left unmodified to preserve drafting context.

> **Status:** proposed.
> **Owner:** Pavel Rapoport <pavel@rapoport.studio>.
> **Linear issue:** RAP-1132 (epic RAP-1124 — Unpack studio product model).
> **Opened:** 2026-05-20.
> **Authority:** D-PM-6 value 5 (Specialist ecosystem in the chat) in `openspec/archive/2026-05-21-define-studio-product-model/design.md`; `_internal/platform/manifesto.md § Values § 5`; `openspec/_root/glossary.md § Curation § Cross-reference`.
> **Affects code:** new `openspec/brands/<slug>/archetypes/<role>/tools.yaml` (per-brand palette files); new `packages/muse/src/runtime/specialist-tools.ts` (palette loader + active-specialist gate); chat turn dispatch in `apps/studio/src/app/(auth-walled)/(canvas)/spec/[id]/turn/route.ts` (consumes the gate).
> **Affects spec at archive:** `openspec/current/canvas.md` § specialist runtime contract (new section); `openspec/current/muse.md` § Architect tool exposure (new section); `openspec/_root/glossary.md § Curation` — cross-reference resolves to this archived change.
> **Blast radius:** medium. New per-brand YAML files (additive — empty palette = current behaviour); new loader + gate in Muse runtime; turn-dispatch gains a tool-filter step. Existing canvases without specialist activation see no behaviour change.
> **Capability prefix:** `STC` (Specialist Tool Composition). Decisions ratify as `D-STC-N`.

---

## Why

Manifesto value 5 commits to a chat that is a runtime where curated specialists arrive with their own tools, research agents, skills, and vocabulary. Today the chat exposes a flat tool surface — every turn, regardless of which specialist is active, the LLM sees the same tool list. Three failure modes follow:

1. **Wrong tools surfaced.** A legal-counsel specialist gets offered `runPlaywrightTrace`, an architect specialist gets offered `lookupCaseLaw`. The LLM picks reasonably but the surface lies.
2. **No brand override.** A fintech client and a healthcare client share the same architect archetype; both archetypes need different tool palettes (fintech needs compliance-lookup, healthcare needs HIPAA-policy-check) but there is no per-brand seam.
3. **No enforcement.** Even if we curate via prompt-engineering ("legal-counsel should not call Playwright tools"), the LLM can still invoke any tool the runtime exposes. There is no server-side gate.

The product-model unpack ratified the seam (D-PM-6 value 5); this change builds it. Per-archetype YAML files live per-brand under `openspec/brands/<slug>/archetypes/<role>/tools.yaml`; a Muse runtime loader resolves the active specialist's archetype + brand to a palette; turn dispatch filters the tool list server-side so the LLM only sees palette-approved tools.

## Scope

Three pieces.

### Composition unit — per-archetype YAML

```yaml
# openspec/brands/<slug>/archetypes/architect/tools.yaml
archetype: architect
brand: <slug>
tools:
  - id: read_openspec
    description: Read OpenSpec capability docs
    scope: read
  - id: propose_change
    description: Author a proposal.md draft
    scope: write
skills:
  - id: spec_authoring
    bundled_from: '@rapoport-studio/muse/skills/spec-authoring'
research_agents:
  - id: scope_inquiry
    description: Scope a falsifiable thesis
    bundled_from: '@rapoport-studio/muse/research/scope'
vocabulary:
  ref: ../../identity.md#vocabulary
```

* **Per-archetype, not per-specialist.** Specialists inherit their archetype's palette; the archetype is the unit of composition. This means a brand can have 3 architect specialists and they all share one tools.yaml.
* **Brand can override.** The platform ships defaults under `openspec/_methodology/specialist-tool-palettes/`; per-brand files at `openspec/brands/<slug>/archetypes/<role>/tools.yaml` override.
* **Diff-friendly.** Adding/removing a tool is one YAML line in one file, reviewable in a normal PR.

### Hosting — per-brand override, platform defaults

```
openspec/
  _methodology/
    specialist-tool-palettes/
      architect/tools.yaml          ← platform default
      designer/tools.yaml
      legal-counsel/tools.yaml
      ...
  brands/
    <brand-slug>/
      identity.md
      archetypes/
        architect/tools.yaml        ← brand override (optional)
        legal-counsel/tools.yaml
```

The resolver walks brand → platform; first match wins. If a brand has no override file, the platform default applies. This pattern mirrors the existing brand-mirroring schema (RAP-1126 / `openspec/archive/2026-05-20-add-brand-mirroring-schema/`).

### Permission/scope model — active-specialist gate at turn dispatch

Turn dispatch reads `canvas_sessions.active_specialist_id`, resolves the specialist → archetype + brand, loads the palette, and filters the tool list passed to Anthropic. Out-of-palette tool invocations rejected server-side before the LLM ever sees them.

```ts
// apps/studio/src/app/(auth-walled)/(canvas)/spec/[id]/turn/route.ts
const palette = await loadPalette({
  archetype: specialist.archetype,
  brand: canvas.brand_slug,
});
const filteredTools = ALL_TOOLS.filter((t) =>
  palette.tools.some((p) => p.id === t.id)
);
// pass `filteredTools` (not `ALL_TOOLS`) to anthropic.messages.create
```

The chat surface also reads the palette to render specialist-specific suggestion chips, status indicators, and tool-call previews.

## Out of scope

* Per-specialist (not per-archetype) palette overrides — covered by the archetype unit per D-STC-1; specialist-level overrides are a future change if the rule-of-three trigger fires.
* Runtime palette mutation by the LLM (self-extending toolset) — explicit anti-feature; palettes are author-controlled.
* Audit-only mode (allow all tools, log out-of-palette calls) — D-STC-3 picks hard-block; audit-only is OOS.
* Permission model on shared tools (e.g., `read_openspec` available to all archetypes) — sharing is expressed by listing the tool in multiple palette files; no shared-pool concept.
* Cross-brand specialist mobility — a specialist active on brand A's canvas cannot inherit brand B's palette.
* UI for editing palettes inside the studio — palette editing happens in the repo via normal PRs (matches the "spec-as-PR" pattern).

## Risks

* **Palette drift across brands.** Brand-overrides diverge from platform defaults over time; new tools added to the platform default don't propagate. Mitigation: a lint rule under `tools/check-palette-drift.mjs` reports per-brand palettes that are >3 months out of date relative to the platform default and surfaces the diff in CI.
* **Loader cost on hot path.** Resolving a palette on every turn adds I/O. Mitigation: palette resolution is memoised per `(brand, archetype)` key with an in-process LRU (size 100, no TTL); files are loaded at app start in dev and at first hit in prod.
* **YAML parse-time errors.** A malformed brand YAML file crashes turn dispatch. Mitigation: loader validates against a Zod schema at startup; a schema violation surfaces as a typed `palette-invalid` error (handled by the P5 error taxonomy from RAP-1094) rather than a stream crash.
* **Tool-id sprawl.** Tool ids in YAML must match `ALL_TOOLS` ids exactly; renames are a coupled change. Mitigation: lint rule `tools/check-palette-tool-ids.mjs` asserts every YAML tool id exists in `ALL_TOOLS`; renaming requires updating both.

## Verification

See `verification.md` — file-existence + lint/test gates + reviewer-confirms for the four risk points above.

## Lessons consulted

* `openspec/archive/2026-05-20-add-brand-mirroring-schema/` — same per-brand override pattern.
* `forge_plan_staleness_on_issue_update.md` — Linear edits before forge plan.
* `feedback_muse_architect_naming.md` — "Muse Architect" full form on user-facing copy describing the runtime; substrate-refs stay "Muse".
* `feedback_openspec_folder_naming.md` — slug-only folder name while active.
