Reborn Extension Surfaces
The top-level product object is always an extension. A channel is not a
sibling product type: it is one capability surface an extension's manifest
declares, exactly like tools and auth. Runtime (wasm / mcp /
first_party) is implementation only and never taxonomy. The retired
vocabulary (connectable channels, slack_bot, slack_personal, extension
kind strings) is pinned at zero by
crates/app/ironclaw_architecture_tests/tests/reborn_retired_taxonomy.rs — if your
change trips that gate, you are re-introducing a deleted model.
Schema is reborn.extension_manifest.v3 — v2 plus explicit [channel]
and [auth.*] sections. The design is law: docs/internal/reborn/extension-runtime/
(overview.md §3 the manifest, §4 the adapters, §5 the flows, §6 lifecycle).
The worked example for every section below is the live Slack manifest —
read it first: crates/extensions/packages/slack/manifest.toml.
The model in one diagram
extension (one manifest.toml, one installed identity, e.g. `slack`)
[[tools]] → tool surfaces (model-callable), each with [[tools.credentials]]
[channel] → channel surface (AT MOST ONE per extension; inbound/outbound)
[auth.<vendor>] → auth surface: one recipe per vendor (oauth2_code | api_key)
[mcp] → hosted-MCP extension: discovered tools INSTEAD of [runtime]+[[tools]]
[runtime] first_party|wasm|mcp → how the adapter LOADS (implementation only, never taxonomy)
ExtensionId (slack, github, gmail) — product/installed identity.
VendorId (slack, github, google) — credential authority; several
extensions may share one (gmail + google-drive + calendar + … share
google). The manifest field is vendor = "...". It is not the
extension id. (Renamed from ProviderId/RuntimeCredentialAccountProviderId
in this train — overview §2; stored id strings are unchanged.)
CapabilitySurfaceKind (crates/contracts/ironclaw_extension_contracts/src/surface.rs) — tool,
channel, auth (+ reserved trigger, file).
- Surfaces are derived from the resolved manifest — never store a parallel
taxonomy. The manifest compiles once per install into a typed
ResolvedExtensionManifest (+ manifest_digest); all production projection
reads the resolved record, never re-parsed TOML (overview §3.3).
Adapters implement behavior only (overview §4): they never report ids,
schemas, effects, scopes, routes, or credentials — the resolved manifest is the
sole authority. Trait homes:
ToolAdapter — crates/contracts/ironclaw_extension_contracts/src/tool_adapter.rs;
ChannelIngress/ChannelDelivery — crates/contracts/ironclaw_extension_contracts/src/channel_adapter.rs;
ReplySink — crates/contracts/ironclaw_extension_contracts/src/reply.rs;
ExtensionEntrypoint/ExtensionBindings —
crates/extensions/ironclaw_extension_host/src/entrypoint.rs. Auth has no adapter
trait — it is one host engine driving manifest recipes (overview §4.3).
Where a bundled package lives
Every first-party integration is a self-contained package:
crates/extensions/packages/<id>/ (manifest + schemas +
prompts + any WASM) beside one module
crates/extensions/ironclaw_extension_support/src/packages/<id>.rs (embeds via
include_str!/include_bytes!, onboarding copy, trust effects). A collector
concatenates them; add a line to PACKAGES in .../src/packages/mod.rs.
Composition and the CLI consume these as opaque bundles and never name a
package (overview §3). Do NOT register assets in composition — the old
available_extensions.rs::*_assets() home (now crates/extensions/ironclaw_extension_host/src/available_extensions.rs) is being dissolved.
Re-verify the module list: grep -n 'ID,' crates/extensions/ironclaw_extension_support/src/packages/mod.rs.
Adding a tool surface
- Declare each capability as a
[[tools]] entry (id, description, effects,
default_permission, visibility, input_schema_ref, optional
prompt_doc_ref) with a [[tools.credentials]] block naming its vendor,
audience, and injection. Copy the plain schema-declaring shape from
crates/extensions/packages/github/manifest.toml; for standard_op-bound
messaging tools the exemplar is
crates/extensions/packages/slack/manifest.toml, whose tools are all
standard-op-bound (count entries with grep -c '^\[\[tools\]\]' <manifest> rather than trusting a written number).
- Schemas and prompt docs are package assets (
schemas/…, prompts/…)
embedded by the package module — not composition.
- Model-visible tool wording is product surface: if a tool acts as the user
(delegated authority), its description and prompt doc must say so — and must
say the tool is for side effects inside a job, never for delivering the final
answer (the host delivers final replies on the outbound channel surface —
overview §5.4). Exemplar:
crates/extensions/packages/slack/prompts/slack/send_message.md.
- A messaging-shaped tool (send/read/react over a conversation) binds
standard_op = "<op_name>" instead of declaring its own schemas — a
closed, host-owned vocabulary with host-canonical input/output schemas.
See docs/internal/reborn/extension-runtime/standard-operations.md for the full
vocabulary and binding rules.
Adding a channel surface
- Add a
[channel] section (at most one per extension) with id,
display_name, inbound/outbound bools, and required
conversation_model (continuous | isolated, overview §3). Then its
subsections, all worked in crates/extensions/packages/slack/manifest.toml:
[channel.ingress] (route_suffix, method, body limit),
[channel.ingress.verification] (declarative recipe the host executes —
hmac_sha256 segment list or shared_secret_header; signing secrets never
reach the adapter), [channel.connection] (connection strategy),
[[channel.egress]] (host allowlist + credential handle), and
[channel.presentation]. Operator/deployment setup fields are a separate
top-level [admin_configuration] section (there is no [channel.config]);
the host renders the generic form from it.
- Direction is the
inbound/outbound bools, which project to
channel { inbound, outbound } on the extensions wire. A run's own final
reply is never an agent decision — it rides the runtime-owned delivery
coordinator automatically (lane 1, overview §5.4). The agent DOES get one
generic, host-built-in tool for reaching any other target explicitly,
builtin.outbound_deliver (lane 2) — it is not a per-channel tool an
extension declares.
- Behavior lives in the extension's channel adapter:
ChannelIngress::receive
(parse → normalized outcome), ChannelDelivery::deliver (render+send a
discrete message), and — for a declared [channel.reply] — one
ReplySink::reconcile (crates/contracts/ironclaw_extension_contracts/src/reply.rs)
that converges the vendor's presentation onto the run's reply document
(transport = "stream" hears every revision, "message" the terminal
one; the host publishes, the sink renders; verify with
rg -n "impl ReplySink for" crates/extensions/packages) — see each trait
doc for the method contract. Vendor-side webhook wiring is manifest data,
not adapter code: the [channel.ingress.registration] /
[channel.ingress.deregistration] recipes the host executes (worked
example: rg -n "channel.ingress.registration" crates/extensions/packages/telegram/manifest.toml).
Additional activation-time vendor calls (e.g. registering a native command
menu) are [[channel.ingress.activation_calls]] /
[[channel.ingress.deactivation_calls]] recipes — never per-vendor host
code (worked example: Telegram's setMyCommands in the same manifest).
The binary
supplies the adapter to composition through the
RebornHostBindings::with_channel_extension_bindings seam
(crates/app/ironclaw_composition/src/input.rs, ChannelExtensionBinding);
composition iterates it by extension_id and never names a concrete crate.
- Conversation/actor binding is data, not per-channel code: the
conversation_model value + the identity resolver drive it. Contract:
docs/internal/reborn/contracts/conversation-binding.md. The actor→user resolver is
ProviderIdentityActorResolver
(crates/extensions/ironclaw_extension_host/src/provider_identity.rs, re-exported by
composition),
parameterized by (vendor, adapter id, actor kind) — not a per-channel
resolver (the retired-taxonomy gate hunts the old pattern).
- Connect affordance is derived (overview §6.4): installation state +
[admin_configuration] completeness + the auth account state. The WebUI channels
tab renders every channel surface with the same generic components — there is
no channel registry to update (frontend helpers:
crates/product/ironclaw_webui/frontend/src/pages/extensions/lib/extensions-schema.ts,
hasChannelSurface). Editing [channel.connection] while Active runs an
automatic deactivate → reactivate cycle; there is no separate reconfigure
state or channel-setup activation gate.
Adding / sharing an auth provider
- Add one
[auth.<vendor>] recipe per vendor the extension needs — the
section key is the vendor id. method = "oauth2_code" (endpoints,
scope_param, PKCE, client_credentials handles, [auth.<vendor>].token_response
[auth.<vendor>].identity JSON-pointer maps) or method = "api_key" (form
fields + optional validation probe). Worked example: [auth.slack] in
crates/extensions/packages/slack/manifest.toml; the full recipe vocabulary is overview §4.3 +
implementation.md §7. There is no auth adapter trait and no extension code
in an auth flow — the host engine (crates/domains/ironclaw_auth) runs each method
once over the recipe data.
- Share a
vendor across extensions when the credential authority is the same
(google across gmail/drive/calendar/docs/sheets/slides). Recipes for one
vendor must be identical except scopes/display_name, or activation fails
with a conflict; scopes union across active extensions (overview §3.2).
- Renaming any persisted identity (vendor id, extension id) requires a one-time
forward data migration, never a runtime alias. There is currently no
in-tree migration exemplar: the retired-identity boot migration this skill
used to cite was deleted 2026-08-04 by owner ruling, and its identifiers
(
RETIRED_SLACK_USER_EXTENSION_ID, remove_retired_internal_installation)
are now on the banned list in
crates/app/ironclaw_architecture_tests/tests/reborn_retired_taxonomy.rs — do not
reintroduce them. The behavioral pin is
restore_special_cases_no_extension_id_and_leaves_every_uncatalogued_row_intact
(crates/extensions/ironclaw_extension_host/tests/lifecycle_restore_contract.rs):
boot restore must leave uncatalogued rows intact, never delete or re-key
them.
Hosted-MCP extensions
An extension whose tools are discovered from a server declares one [mcp]
section (server, namespace, max_tools, effects, [[mcp.credentials]]) instead
of [runtime] (and normally instead of [[tools]] + [channel]), plus its
[auth.<vendor>] recipe. A hosted-MCP package MAY additionally pin static
[[tools]] entries beside [mcp] — guaranteed model-visible from first boot
and on the bundled-manifest fallback; a successful tools/list discovery
replaces the static set with the server's live catalog (worked example:
nearai.web_search in crates/extensions/packages/nearai-mcp/manifest.toml).
The MCP loader owns discovery; past activation a discovered tool is an ordinary
tool surface (overview §3.1). Worked example:
crates/extensions/packages/notion-mcp/manifest.toml.
Testing surfaces
- Manifest projection (v3):
crates/extensions/ironclaw_extension_registry/tests/manifest_v3_contract.rs;
channel ingestion through the real contract:
crates/extensions/ironclaw_extension_registry/tests/product_adapter_manifest_ingestion.rs
(drives parse_product_adapter_manifest_record in
crates/extensions/ironclaw_extension_registry/src/host_api/product_adapter.rs). Extend
these rather than adding parallel suites.
- Adapter behavior: the exported conformance suites — channel in
crates/contracts/ironclaw_extension_contracts/src/test_support/conformance.rs
(run_channel_adapter_conformance, consumed by
crates/extensions/packages/{slack,telegram}/tests/channel_conformance.rs),
standard-messaging schema conformance in
crates/contracts/ironclaw_host_api/src/test_support/messaging_conformance.rs, and auth
in crates/domains/ironclaw_auth/src/test_support/conformance.rs. Run by every
applicable extension crate + the acme
fixture (tests/fixtures/extensions/acme-messenger/). Real extensions add one
end-to-end integration proof each (tests/integration/).
- Frontend:
pnpm --dir crates/product/ironclaw_webui/frontend test.
- Always finish with
cargo test -p ironclaw_architecture_tests — the specificity,
dependency-direction, and retired-taxonomy gates are the machine reviewers.
Generic code naming your extension trips
reborn_extension_specificity.rs: put the name in the package/CLI, not
generic crates.
Sibling skills
reborn-feature (wiring a feature through the layers) ·
ironclaw-reborn-architecture-review (boundaries) ·
ironclaw-reborn-testing (tiers).
1---2name: reborn-extension-surfaces3description: Use when adding or changing a Reborn integration — a new extension, a channel surface, model-callable tools, or a shared auth provider — or when deciding whether something is "a channel", "an extension", or "a tool". Maps the unified extension model (NEA-25) to the exact manifest sections, crates, seams, and tests.4---56# Reborn Extension Surfaces78The top-level product object is always an **extension**. A channel is not a9sibling product type: it is one **capability surface** an extension's manifest10declares, exactly like tools and auth. Runtime (`wasm` / `mcp` /11`first_party`) is implementation only and never taxonomy. The retired12vocabulary (connectable channels, `slack_bot`, `slack_personal`, extension13`kind` strings) is pinned at zero by14`crates/app/ironclaw_architecture_tests/tests/reborn_retired_taxonomy.rs` — if your15change trips that gate, you are re-introducing a deleted model.1617**Schema is `reborn.extension_manifest.v3`** — v2 plus explicit `[channel]`18and `[auth.*]` sections. The design is law: `docs/internal/reborn/extension-runtime/`19(`overview.md` §3 the manifest, §4 the adapters, §5 the flows, §6 lifecycle).20The worked example for *every* section below is the live Slack manifest —21read it first: `crates/extensions/packages/slack/manifest.toml`.2223## The model in one diagram2425```text26extension (one manifest.toml, one installed identity, e.g. `slack`)27 [[tools]] → tool surfaces (model-callable), each with [[tools.credentials]]28 [channel] → channel surface (AT MOST ONE per extension; inbound/outbound)29 [auth.<vendor>] → auth surface: one recipe per vendor (oauth2_code | api_key)30 [mcp] → hosted-MCP extension: discovered tools INSTEAD of [runtime]+[[tools]]31 [runtime] first_party|wasm|mcp → how the adapter LOADS (implementation only, never taxonomy)32```3334- `ExtensionId` (`slack`, `github`, `gmail`) — product/installed identity.35- `VendorId` (`slack`, `github`, `google`) — credential authority; several36 extensions may share one (gmail + google-drive + calendar + … share37 `google`). The manifest field is `vendor = "..."`. It is **not** the38 extension id. (Renamed from `ProviderId`/`RuntimeCredentialAccountProviderId`39 in this train — overview §2; stored id strings are unchanged.)40- `CapabilitySurfaceKind` (`crates/contracts/ironclaw_extension_contracts/src/surface.rs`) — `tool`,41 `channel`, `auth` (+ reserved `trigger`, `file`).42- Surfaces are **derived** from the resolved manifest — never store a parallel43 taxonomy. The manifest compiles **once** per install into a typed44 `ResolvedExtensionManifest` (+ `manifest_digest`); all production projection45 reads the resolved record, never re-parsed TOML (overview §3.3).4647Adapters implement **behavior only** (overview §4): they never report ids,48schemas, effects, scopes, routes, or credentials — the resolved manifest is the49sole authority. Trait homes:50`ToolAdapter` — `crates/contracts/ironclaw_extension_contracts/src/tool_adapter.rs`;51`ChannelIngress`/`ChannelDelivery` — `crates/contracts/ironclaw_extension_contracts/src/channel_adapter.rs`;52`ReplySink` — `crates/contracts/ironclaw_extension_contracts/src/reply.rs`;53`ExtensionEntrypoint`/`ExtensionBindings` —54`crates/extensions/ironclaw_extension_host/src/entrypoint.rs`. Auth has **no** adapter55trait — it is one host engine driving manifest recipes (overview §4.3).5657## Where a bundled package lives5859Every first-party integration is a self-contained package:60`crates/extensions/packages/<id>/` (manifest + schemas +61prompts + any WASM) beside one module62`crates/extensions/ironclaw_extension_support/src/packages/<id>.rs` (embeds via63`include_str!`/`include_bytes!`, onboarding copy, trust effects). A collector64concatenates them; add a line to `PACKAGES` in `.../src/packages/mod.rs`.65Composition and the CLI consume these as **opaque bundles** and never name a66package (overview §3). Do NOT register assets in composition — the old67`available_extensions.rs::*_assets()` home (now `crates/extensions/ironclaw_extension_host/src/available_extensions.rs`) is being dissolved.68Re-verify the module list: `grep -n 'ID,' crates/extensions/ironclaw_extension_support/src/packages/mod.rs`.6970## Adding a tool surface71721. Declare each capability as a `[[tools]]` entry (id, description, effects,73 default_permission, visibility, `input_schema_ref`, optional74 `prompt_doc_ref`) with a `[[tools.credentials]]` block naming its `vendor`,75 `audience`, and `injection`. Copy the plain schema-declaring shape from76 `crates/extensions/packages/github/manifest.toml`; for `standard_op`-bound77 messaging tools the exemplar is78 `crates/extensions/packages/slack/manifest.toml`, whose tools are all79 standard-op-bound (count entries with `grep -c '^\[\[tools\]\]'80 <manifest>` rather than trusting a written number).812. Schemas and prompt docs are **package assets** (`schemas/…`, `prompts/…`)82 embedded by the package module — not composition.833. Model-visible tool wording is product surface: if a tool acts *as the user*84 (delegated authority), its description and prompt doc must say so — and must85 say the tool is for side effects inside a job, never for delivering the final86 answer (the host delivers final replies on the outbound channel surface —87 overview §5.4). Exemplar: `crates/extensions/packages/slack/prompts/slack/send_message.md`.884. A **messaging-shaped** tool (send/read/react over a conversation) binds89 `standard_op = "<op_name>"` instead of declaring its own schemas — a90 closed, host-owned vocabulary with host-canonical input/output schemas.91 See `docs/internal/reborn/extension-runtime/standard-operations.md` for the full92 vocabulary and binding rules.9394## Adding a channel surface95961. Add a `[channel]` section (**at most one per extension**) with `id`,97 `display_name`, `inbound`/`outbound` bools, and **required**98 `conversation_model` (`continuous` | `isolated`, overview §3). Then its99 subsections, all worked in `crates/extensions/packages/slack/manifest.toml`:100 `[channel.ingress]` (route_suffix, method, body limit),101 `[channel.ingress.verification]` (declarative recipe the *host* executes —102 `hmac_sha256` segment list or `shared_secret_header`; signing secrets never103 reach the adapter), `[channel.connection]` (connection strategy),104 `[[channel.egress]]` (host allowlist + credential handle), and105 `[channel.presentation]`. Operator/deployment setup fields are a separate106 top-level `[admin_configuration]` section (there is no `[channel.config]`);107 the host renders the generic form from it.1082. Direction is the `inbound`/`outbound` bools, which project to109 `channel { inbound, outbound }` on the extensions wire. A run's own final110 reply is never an agent decision — it rides the runtime-owned delivery111 coordinator automatically (lane 1, overview §5.4). The agent DOES get one112 generic, host-built-in tool for reaching any other target explicitly,113 `builtin.outbound_deliver` (lane 2) — it is not a per-channel tool an114 extension declares.1153. Behavior lives in the extension's channel adapter: `ChannelIngress::receive`116 (parse → normalized outcome), `ChannelDelivery::deliver` (render+send a117 discrete message), and — for a declared `[channel.reply]` — one118 `ReplySink::reconcile` (`crates/contracts/ironclaw_extension_contracts/src/reply.rs`)119 that converges the vendor's presentation onto the run's reply document120 (`transport = "stream"` hears every revision, `"message"` the terminal121 one; the host publishes, the sink renders; verify with122 `rg -n "impl ReplySink for" crates/extensions/packages`) — see each trait123 doc for the method contract. Vendor-side webhook wiring is manifest data,124 not adapter code: the `[channel.ingress.registration]` /125 `[channel.ingress.deregistration]` recipes the host executes (worked126 example: `rg -n "channel.ingress.registration" crates/extensions/packages/telegram/manifest.toml`).127 Additional activation-time vendor calls (e.g. registering a native command128 menu) are `[[channel.ingress.activation_calls]]` /129 `[[channel.ingress.deactivation_calls]]` recipes — never per-vendor host130 code (worked example: Telegram's `setMyCommands` in the same manifest).131 The binary132 supplies the adapter to composition through the133 `RebornHostBindings::with_channel_extension_bindings` seam134 (`crates/app/ironclaw_composition/src/input.rs`, `ChannelExtensionBinding`);135 composition iterates it by `extension_id` and never names a concrete crate.1364. Conversation/actor binding is **data, not per-channel code**: the137 `conversation_model` value + the identity resolver drive it. Contract:138 `docs/internal/reborn/contracts/conversation-binding.md`. The actor→user resolver is139 `ProviderIdentityActorResolver`140 (`crates/extensions/ironclaw_extension_host/src/provider_identity.rs`, re-exported by141 composition),142 parameterized by (vendor, adapter id, actor kind) — not a per-channel143 resolver (the retired-taxonomy gate hunts the old pattern).1445. Connect affordance is **derived** (overview §6.4): installation state +145 `[admin_configuration]` completeness + the auth account state. The WebUI channels146 tab renders every channel surface with the same generic components — there is147 no channel registry to update (frontend helpers:148 `crates/product/ironclaw_webui/frontend/src/pages/extensions/lib/extensions-schema.ts`,149 `hasChannelSurface`). Editing `[channel.connection]` while `Active` runs an150 automatic deactivate → reactivate cycle; there is no separate reconfigure151 state or channel-setup activation gate.152153## Adding / sharing an auth provider1541551. Add one `[auth.<vendor>]` recipe per vendor the extension needs — the156 section key **is** the vendor id. `method = "oauth2_code"` (endpoints,157 `scope_param`, PKCE, `client_credentials` handles, `[auth.<vendor>].token_response`158 + `[auth.<vendor>].identity` JSON-pointer maps) or `method = "api_key"` (form159 `fields` + optional `validation` probe). Worked example: `[auth.slack]` in160 `crates/extensions/packages/slack/manifest.toml`; the full recipe vocabulary is overview §4.3 +161 implementation.md §7. There is **no auth adapter trait and no extension code162 in an auth flow** — the host engine (`crates/domains/ironclaw_auth`) runs each method163 once over the recipe data.1642. Share a `vendor` across extensions when the credential authority is the same165 (`google` across gmail/drive/calendar/docs/sheets/slides). Recipes for one166 vendor must be identical except `scopes`/`display_name`, or activation fails167 with a conflict; scopes union across active extensions (overview §3.2).1683. Renaming any persisted identity (vendor id, extension id) requires a one-time169 forward data migration, never a runtime alias. There is currently **no170 in-tree migration exemplar**: the retired-identity boot migration this skill171 used to cite was deleted 2026-08-04 by owner ruling, and its identifiers172 (`RETIRED_SLACK_USER_EXTENSION_ID`, `remove_retired_internal_installation`)173 are now on the banned list in174 `crates/app/ironclaw_architecture_tests/tests/reborn_retired_taxonomy.rs` — do not175 reintroduce them. The behavioral pin is176 `restore_special_cases_no_extension_id_and_leaves_every_uncatalogued_row_intact`177 (`crates/extensions/ironclaw_extension_host/tests/lifecycle_restore_contract.rs`):178 boot restore must leave uncatalogued rows intact, never delete or re-key179 them.180181## Hosted-MCP extensions182183An extension whose tools are discovered from a server declares one `[mcp]`184section (server, namespace, max_tools, effects, `[[mcp.credentials]]`) **instead185of** `[runtime]` (and normally instead of `[[tools]]` + `[channel]`), plus its186`[auth.<vendor>]` recipe. A hosted-MCP package MAY additionally pin static187`[[tools]]` entries beside `[mcp]` — guaranteed model-visible from first boot188and on the bundled-manifest fallback; a successful `tools/list` discovery189replaces the static set with the server's live catalog (worked example:190`nearai.web_search` in `crates/extensions/packages/nearai-mcp/manifest.toml`).191The MCP loader owns discovery; past activation a discovered tool is an ordinary192tool surface (overview §3.1). Worked example:193`crates/extensions/packages/notion-mcp/manifest.toml`.194195## Testing surfaces196197- Manifest projection (v3): `crates/extensions/ironclaw_extension_registry/tests/manifest_v3_contract.rs`;198 channel ingestion through the real contract:199 `crates/extensions/ironclaw_extension_registry/tests/product_adapter_manifest_ingestion.rs`200 (drives `parse_product_adapter_manifest_record` in201 `crates/extensions/ironclaw_extension_registry/src/host_api/product_adapter.rs`). Extend202 these rather than adding parallel suites.203- Adapter behavior: the exported conformance suites — channel in204 `crates/contracts/ironclaw_extension_contracts/src/test_support/conformance.rs`205 (`run_channel_adapter_conformance`, consumed by206 `crates/extensions/packages/{slack,telegram}/tests/channel_conformance.rs`),207 standard-messaging schema conformance in208 `crates/contracts/ironclaw_host_api/src/test_support/messaging_conformance.rs`, and auth209 in `crates/domains/ironclaw_auth/src/test_support/conformance.rs`. Run by every210 applicable extension crate + the `acme`211 fixture (`tests/fixtures/extensions/acme-messenger/`). Real extensions add one212 end-to-end integration proof each (`tests/integration/`).213- Frontend: `pnpm --dir crates/product/ironclaw_webui/frontend test`.214- Always finish with `cargo test -p ironclaw_architecture_tests` — the specificity,215 dependency-direction, and retired-taxonomy gates are the machine reviewers.216 Generic code naming your extension trips217 `reborn_extension_specificity.rs`: put the name in the package/CLI, not218 generic crates.219220## Sibling skills221222`reborn-feature` (wiring a feature through the layers) ·223`ironclaw-reborn-architecture-review` (boundaries) ·224`ironclaw-reborn-testing` (tiers).