# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Deep references live under `ai-docs/` — load them on demand:
- [`ai-docs/api-surface.md`](ai-docs/api-surface.md) — every `app/api/**/route.js` endpoint
- [`ai-docs/key-constraints.md`](ai-docs/key-constraints.md) — secrets vault, allowlists, rate limits, trust mode, tool adapters, voice, WhatsApp, sub-agent engines
- [`ai-docs/agent-security-model.md`](ai-docs/agent-security-model.md) — 4 risk tiers, 7-gate execution order
- [`ai-docs/auth-privacy.md`](ai-docs/auth-privacy.md) — orgs, ACLs, share links, encryption, audit
- [`ai-docs/v1-personal-workforce.md`](ai-docs/v1-personal-workforce.md) — projects, Disk, Team, missions
- [`ai-docs/marketplace-brand.md`](ai-docs/marketplace-brand.md) — marketplace tokens
- [`ai-docs/process-engine.md`](ai-docs/process-engine.md) — workflow / process engine (definitions, stages, cases, engine-as-sole-state-writer invariant)
- [`ai-docs/tool-smith.md`](ai-docs/tool-smith.md) — Tool-Smith: agent-authored capabilities (skills, composite tools, REST connectors, propose_tool), dynamic dispatch + per-step/per-endpoint gating invariants

## Commands

```bash
npm run dev          # dev server at http://127.0.0.1:8787/
npm run build        # Next.js production build
npm start            # production server at http://127.0.0.1:8787/
npm test             # Node built-in test runner (tests/*.test.js)
npm run daemon       # continuous cron daemon; queues due schedules into heartbeat_tasks
npm run daemon:once  # one-shot cron queue check (for testing)
npm run heartbeat:worker # continuous durable heartbeat worker
npm run heartbeat:once   # one-shot heartbeat worker pass
npm run whatsapp:bridge  # continuous WhatsApp gateway daemon (Baileys bridge + inbound poller)
npm run whatsapp:once    # one-shot: install bridge deps, spawn, wait for pairing, exit
npm run whatsapp:reset   # wipe data/whatsapp/session/ to force re-pairing
npm run check:disk-enc   # boot-time disk-encryption health check; writes audit_logs row
npm run check        # syntax check + tests + build (full pre-ship gate)
```

App runs on port **8787**, not the default Next.js port. Runtime diagnostics at `/runtime`.

## Distribution / CLI

Shipped as an installable npm package with a `madarx` bin (`bin/madarx.mjs` → `lib/cli/`). Install via `npm i -g madarx` or `scripts/install.sh`; published by `.github/workflows/publish.yml` on a `v*` tag (`npm run check` → build → `npm publish --provenance`). User guide: [`docs/install.md`](docs/install.md).

- **`next.config.mjs` `output: 'standalone'`** — the web tier ships as `.next/standalone/server.js` (prebuilt, in the tarball via the `files` whitelist + `prepublishOnly`), so a global install has no build step. `lib/cli/run.js` boots it (falling back to `next start`), supervises the WhatsApp daemon, and relies on `getDb()`'s in-process auto-start for cron + heartbeat.
- **`$MADARX_HOME` data home** — default `~/.madarx/` (`data/`, `workspace/`, `.env.local`, `.update_check`). Lives outside the package dir so upgrades never wipe it. `lib/cli/home.js` resolves it and sets `MADARX_HOME` + `MADARX_ENV_FILE`; `lib/paths.js` derives `DATA_DIR`/`WORKSPACE` from it.
- **Env var rename** — the data/workspace overrides are now `MADARX_DATA_DIR` / `MADARX_WORKSPACE`. `VILLA515_DATA_DIR` / `VILLA515_WORKSPACE` are deprecated aliases (still honored; one-time warning for non-temp paths). The explicit override (either name) doubles as the test-isolation signal that disables the in-proc daemons + disk-enc check; `MADARX_HOME` does **not** (a real install must run them). Helper: `hasExplicitDataDirOverride()` in `lib/paths.js`.
- **Subcommands** — `run` (default), `setup` (wizard: master key, `MADARX_ALLOWED_ROOTS`, vault keys, WhatsApp toggle, auto-update prefs), `doctor`, `status`, `update`, `uninstall` (`--purge` deletes the home), `version`, `help`. Management commands set `MADARX_DISABLE_INPROC_DAEMON=1`.
- **Self-update** — `lib/update-check.js` writes the `.update_check` ledger; `lib/cli/update.js::applyUpdate()` runs `npm i -g madarx@latest`. A weekly `update_check` heartbeat task (seeded in `lib/seeds.js`, dispatched in `lib/heartbeat-worker.js`, recurring +7d) refreshes the ledger and, only when the operator opted into hands-off at setup (`auto_update_mode=weekly` + `auto_update_apply=auto`), applies directly. The **`self_update` tool** (`lib/tools/adapters/self-update.js`, risk `irreversible_external`, `enabled_default:false`) is the agent-facing path and is **always** approval-gated — an agent can never silently replace the binary. Human/system paths (CLI `update`, dashboard `POST /api/system/update`, opted-in weekly job) call `applyUpdate()` directly because consent is explicit. Dashboard surface: `app/components/update-banner.jsx` (renders only when behind).

## Architecture

**Local-first** Next.js 15 App Router product. Entire stack runs on the developer's machine — no cloud dependencies.

### Frontend (`app/`)

- `app/page.js` — main dashboard shell (57 lines), `"use client"`. Mounts `<DashboardProvider>` and renders `ChatPanel`, `CanvasSection`, `ConfigPanel` (conditional), `VoicePanel` (conditional), `TrustModeConfirmModal`. State lives in `app/components/dashboard/use-dashboard-state.js` exposed via context (`dashboard-context.jsx`). Leaf hooks: `use-config-panel.js`, `use-trust-mode.js`, `use-model-picker.js`. Polls `/api/state` every 5s by default. Set `MADARX_DASHBOARD_LIVE_DELTAS=1` to switch to SSE — one hydration call on mount, then `state.<slice>.changed` events on `/api/events/stream` drive a debounced refetch (250 ms), with a 60 s safety-net refresh and a `visibilitychange` resync.
- `app/runtime/page.js` — diagnostics view. **All technical surfaces for the agents UX (engine counts, worker counts, Hermes runtime, raw IDs) live here**, linked from `/agents` via footer.
- `app/agents/page.tsx` — Apple-style simplified list view. Header has only `New agent`; agent grid + footer link to `/runtime`. Caretaker (id `ops-agent`) auto-seeded on first launch.
- `app/agents/[id]/page.js` — two-mode detail page driven by `?view=` query param. **Simple** (default): status, schedule, last-run, Run button, recent activity. **Details** (`?view=details`): skills editor, memory pruning, tool permissions, audit history, runs table, recent artifacts, engine swap. Server-rendered, bookmarkable.
- `app/agents/[id]/format-helpers.js` — pure unit-tested formatters.
- `app/globals.css` — all styles.
- API routes at `app/api/**/route.js` — full list in [`ai-docs/api-surface.md`](ai-docs/api-surface.md).

### Runtime library (`lib/`)

Business logic shared between API routes and the cron daemon:

| File | Purpose |
|------|---------|
| `lib/db.js` | Singleton `DatabaseSync` via `node:sqlite`. Initialises schema + seeds on first access. `resetDbHandle()` for tests. |
| `lib/schema.js` | `CREATE TABLE IF NOT EXISTS` for agents, schedules, runs, connectors, audit logs, artifacts, approvals, local process rows, heartbeat tasks/events, agent tool permissions, secrets vault, tool allowlists, rate limit events, egress log. |
| `lib/runtime.js` | Domain logic: `createAgent`, `handleOrchestratorCommand`, `competitorReport`, `marketingPlan`, `heartbeatTick`, `createArtifact`/`updateArtifact`, schedule-to-heartbeat queueing, `getState`. |
| `lib/live-events.js` | In-memory pub/sub bus for live activity. `publishLiveEvent()` fans out + persists to `activity_log`. Process-local; cross-process visibility via `lib/live-events-bridge.js`. |
| `lib/secrets.js` | Encrypted secrets vault. `getSecret`/`setSecret`/`rotateSecret`/`deleteSecret`/`listSecrets`. AES-256-GCM at rest; in-process cache keyed by name+scope. `process.env` fallback during migration. |
| `lib/master-key.js` | Resolves AES master key from `MADARX_MASTER_KEY`. On first install with empty secrets table, generates a random key and writes to `.env.local` with a warning. No Keychain integration. |
| `lib/tool-allowlists.js` | Per-tool destination allowlists. `assertAllowed`, `addEntry`, `removeEntry`, `listAll`, `listForTool`. |
| `lib/tool-rate-limits.js` | Sliding-window rate limits. `checkLimit`, `recordCall`. Per-tool windows and maxes defined in `MARKETPLACE_TOOLS`. Daily GC heartbeat task. |
| `lib/egress.js` | `wrapFetch(toolName, agentId, fetchFn)` records every external API call into `tool_egress` (host, method, status, bytes, duration). Observation-only — DB write failures are silent. |
| `lib/live-events-bridge.js` | Cross-process event bridge. Every published event is also written to `activity_log`. SSE endpoint starts a singleton 500ms tailer (`ensureBridgeStarted`) that polls `activity_log` for foreign rows (WhatsApp/cron/heartbeat daemons) and republishes them. De-dupes against an LRU of locally-published event IDs. |
| `lib/paths.js` | Central path constants. `DATA_DIR` and `WORKSPACE` overridable via `VILLA515_DATA_DIR` / `VILLA515_WORKSPACE` (tests). |
| `lib/schedule.js` | `computeNextRunAt` — parses `"daily 07:30"`, `"every 5m"` into ISO timestamps. |
| `lib/seeds.js` | Called by `getDb()` on first open. Wires `seedDefaultEmailAllowlist` (git user.email → recipient_domain entry) and `seedMarketplaceItems`. No agent / connector / run seeds. |
| `lib/marketplace.js` | Unified marketplace domain. `listMarketplaceItems`, `getItem`, `setItemInstalled`, `searchItems`, `getKindCounts`, `listCategoriesForKind`. Single write path; rejects `kind: 'agent'`. |
| `lib/marketplace-seeds.js` | Idempotent skill + bundle seeds. 4 skills (`design-shotgun`, `design-review`, `qa-only`, `cso-audit`) and 1 bundle (`gstack` by garrytan · MIT). |

### Background daemons

**In-process auto-start (default):** `lib/db.js::getDb()` auto-starts both the cron scheduler and heartbeat worker as in-process `setInterval` loops after the first DB open. `npm run dev` / `npm start` automatically run scheduled agents — no separate daemon processes required.

- Cron interval: `CRON_DAEMON_INTERVAL_MS` (default 30000 ms)
- Heartbeat interval: `HEARTBEAT_WORKER_INTERVAL_MS` (default 5000 ms)
- Disable: `MADARX_DISABLE_INPROC_DAEMON=1`
- Guarded by `globalThis.__madarxCronStarted` / `globalThis.__madarxHeartbeatStarted` (survives HMR)
- Skipped in tests (`NODE_ENV=test` or `VILLA515_DATA_DIR` set)

**Standalone daemons** (separate-process deployments): `scripts/cron-daemon.mjs` and `scripts/heartbeat-worker.mjs`. Set `CRON_DAEMON_ONCE=1` / `HEARTBEAT_WORKER_ONCE=1` for one-shot.

**Schedule backfill (`runDueSchedules`):** When overdue by multiple windows, the cron enqueues all missed windows up to `MADARX_BACKFILL_MAX_WINDOWS` (default 10). Behavior per schedule controlled by the `catchup_policy` column:

| Policy | Behavior | Default for |
|--------|----------|-------------|
| `fire_all` | Queue every missed window | `daily`, `weekly`, `monthly` |
| `fire_once` | Queue only the most recent missed window | `every Nm/Nh` |
| `skip` | Advance silently, queue nothing | (explicit opt-in only) |

An audit row `cron.schedule.backfilled` is written whenever >1 window was missed or any were skipped.

`scripts/whatsapp-daemon.mjs` owns the WhatsApp gateway lifecycle (Baileys bridge subprocess + inbound poller). The bridge **must run as a separate process** because Next.js hot-reload kills the persistent Baileys WebSocket. Details: [`ai-docs/key-constraints.md`](ai-docs/key-constraints.md#whatsapp-gateway).

### Workspace files (`workspace/`)

`runtime.createAgent()` writes `AGENTS.md`, `skills.md`, `tools.md`, `heartbeat.json` under `workspace/agents/<agent-id>/`. Report artifacts land in `workspace/reports/`. Git-ignored.

### Database

SQLite via `node:sqlite` (`DatabaseSync`). DB file at `data/madarx.sqlite` (git-ignored). `node:sqlite` emits an experimental warning — expected. `next.config.mjs` lists `node:sqlite` under `serverExternalPackages` so Next.js does not bundle it.

Security-foundation tables: `secrets`, `tool_allowlists`, `tool_rate_limit_events`, `tool_egress`, `marketplace_items`, `marketplace_installations`. Auth+privacy tables documented in [`ai-docs/auth-privacy.md`](ai-docs/auth-privacy.md).

## Testing

Tests use `node:test`. Each file sets `VILLA515_DATA_DIR` and `VILLA515_WORKSPACE` to a `tmp` dir before importing `lib/runtime.js`, so tests are isolated from real `data/` and `workspace/`. Always call `resetDbHandle()` in `test.after` to close the DB and allow cleanup.

Single test by name:
```bash
node --no-warnings --test --test-name-pattern="your test name" tests/api-behavior.test.js
```

## Key constraints (short list — deep details in [`ai-docs/key-constraints.md`](ai-docs/key-constraints.md))

- **ESM throughout** — `"type": "module"`. `.mjs` for scripts.
- **Node ≥ 22.5** required for `node:sqlite`.
- **Claude Code dry-run only** — `claudeDryRun()` never shells out; external actions always approval-gated.
- **Fresh-start by default** — only seed is the canonical **Caretaker** agent (id `ops-agent`, daily 08:00, safe toolset). Idempotent.
- **Local tools are bounded** — file/terminal/process tools must use `lib/tools/safety.js`, stay inside allowed roots, block secret/system paths, cap/redact output, write audit rows.
- **Approvals are one-time** — risky local actions create `approval_requests`; approved requests consumed once per matching action type.
- **Heartbeat is durable** — cron queues work into SQLite; worker owns execution, leases, retries, approval pauses, stale recovery.
- **Secrets vault** — credentials encrypted in `secrets` SQLite table (AES-256-GCM). Master key from `MADARX_MASTER_KEY`. `process.env` fallback during migration. Migrate with `node scripts/migrate-env-to-secrets.mjs`.
- **Per-tool destination allowlists** — `tool_allowlists` table. Default-deny for most write tools; default-allow for `web_search` / `web_fetch`.
- **Per-tool rate limits** — sliding-window quotas per tool defined in `MARKETPLACE_TOOLS`. `recordCall` only after successful adapter execution.
- **Tool egress logging** — every external call via `wrapFetch` recorded in `tool_egress` (host/method/status/bytes/duration). No bodies.
- **Marketplace tools** — canonical defs in `MARKETPLACE_TOOLS` (`lib/tools/registry.js`). All new integration tools `enabled_default: false`.
- **Marketplace foundation (Phase 1)** — `/tools` → `/marketplace?kind=tool`. Three kinds: tool / skill / bundle. Agent personas are out of scope (future product). Single write path: `lib/marketplace.js::setItemInstalled`.
- **Open Design corpus** — vendored at `workspace/skills/open-design/`. Refresh via `npm run vendor:open-design`. Do not hand-edit.
- **Main agent trust mode** — `executeRegistryTool()` gates risky calls. Default `approval_required`. `auto` skips gate for `high` and below — `blocked` and `irreversible_external` never auto-approved.
- **`irreversible_external` risk tier** — always creates an approval request even in trust-mode `auto`. Load-bearing guarantee that an LLM on auto-trust cannot make irreversible external writes without a human in the loop. Tiers: `blocked` → refuse | `irreversible_external` → always gate | `high`/`medium` → gate in `approval_required` mode | `low` → always execute.
- **Tool adapters** — Resend email, GitHub (11 tools), Supabase writes (6 tools), `flight_search` dual-provider routing (Amadeus → Google Flights fallback). Details and credentials in [`ai-docs/key-constraints.md`](ai-docs/key-constraints.md#tool-adapters).
- **Voice tools (Jarvis)** — provider abstraction (`realtime` → `tts` → `local` → `off`). **Voice IS the main agent.** Default `realtime` brain is **transport-only**: OpenAI Realtime does STT (and the TTS lane speaks); cognition + tool calls run through the SAME `/api/orchestrator/stream` path as typed chat — same model, same live `getTools()` catalog, same trust/approval/allowlist/rate-limit gates. No per-session tool snapshot, so a tool enabled mid-conversation is usable on the next turn. Legacy in-browser-brain path is behind `localStorage madarx:voice-brain='realtime'` for rollback. Realtime ephemeral secrets never persisted. Brand: Onyx/Bone/Graphite, Mars Red only on active speech + approval banners.
- **WhatsApp gateway** — Baileys sub-package, separate-process daemon. **Pair only a dedicated WhatsApp number, not personal account.** Router enforces allowlist + 5-msg/min per-sender rate limit.
- **Sub-agent engines** — five engines in `lib/agent-engines.js`: `claude-code` (**default**), `anthropic`, `ollama`, `codex`, `hermes`. CC spawns the local CLI with `--permission-mode bypassPermissions`; **the `deny` list in `.claude/settings.json` is the load-bearing safety boundary**.
- **Process engine ownership of state** — `lib/process/engine.js` is the SOLE writer of `process_cases.current_stage_id`, `current_stage_slug`, `status`, and `context_json`. Stage handlers receive a read-only case snapshot; they never mutate case state. Every transition flows through `transitionTo`, which is transaction-wrapped and asserts the expected `from_stage_id`. Full details: [`ai-docs/process-engine.md`](ai-docs/process-engine.md).
- **JSON workflow authoring** — process definitions are agent-authorable JSON. `lib/process/spec-validator.js::validateWorkflowSpec(spec, opts)` is the single source of truth for spec shape (strict; all 8 rule groups). `upsertDefinition` is the sole writer and routes every call through the validator. Source-of-truth JSON persists in `process_definitions.source_spec_json` (4 columns: `source_spec_json`, `schema_version`, `created_by_user_id`, `created_by_agent_id`). Public surface: `POST /api/processes/definitions` (create), `PUT/DELETE /api/processes/definitions/[slug]` (update/archive), `POST /api/processes/definitions/validate` (dry-run, no DB write). Admin UI at `/workflows/new/definition` (textarea-based JSON editor; no Monaco). All writes require admin/owner role. Slug regex `^[a-z][a-z0-9-]{2,63}$`. First stage must be `intake_form`. Spec hard-cap 64 KB; `agent_run.prompt` hard-cap 8000 chars. Soft-archive only (FK-safe — `process_cases.definition_id` is RESTRICT). **Phase 3 shipped:** marketplace `kind:'workflow'` (3 starter templates seeded in `lib/process/seeds/marketplace-workflows.js` — `vendor-onboarding`, `contract-review`, `support-escalation`), `create_workflow_definition` + `update_workflow_definition` agent tools (`lib/tools/adapters/workflow-definitions.js`, risk='medium', approval_required, direct call to `upsertDefinition`), and `app/marketplace/workflow-card.jsx` install flow that writes through the same sole-writer path. Install soft-archives the definition on uninstall; uninstall is blocked while non-terminal cases reference it.
- **Tool-Smith — agent-authored capabilities** — a dedicated `tool-smith-agent` (auto-seeded like the Caretaker) authors reusable capabilities; the main agent delegates via the `request_capability` core tool (dedupe + rate-limit → delegation task). Four paths, none of which loads/executes LLM-authored code: **skills** (`create_skill`/`update_skill` → `lib/skills/definitions.js`), **composite tools** (`create_composite_tool` → recipe chaining existing primitives; risk = MAX over steps; the `composite_tool_run` interpreter RE-ENTERS `executeRegistryTool` per step so no gate is skippable via composition), **REST connectors** (`create_connector` → inert manifest: https-only, `allowed_hosts`, secret-by-`*_ref` auth, per-endpoint risk ceiling; each endpoint becomes a callable `<slug>.<endpoint>` tool backed by the ONE generic `http_api_call` executor — host allowlist + `assertPublicHttpUrl` SSRF block + vault-secret injection never echoed), and **`propose_tool`** (Option B: stages an INERT code scaffold under `workspace/tool-proposals/`, callable only after human merge + restart). Composites/connectors surface dynamically via `listMarketplaceTools()` (`source.kind` routes dispatch); `http_api_call`/`composite_tool_run` are intentionally NOT in `MARKETPLACE_TOOLS` so per-endpoint/per-composite risk can't be bypassed. Authored irreversible_external capabilities ALWAYS gate for agents (the agent-path irreversible gate fires when `tool.source` is set). Tables: `composite_tools`, `api_connectors`, `tool_proposals`. Authoring tools are `medium`/`approval_required`/owner-admin role-gated (mirrors `create_workflow_definition`). Full reference: [`ai-docs/tool-smith.md`](ai-docs/tool-smith.md).

## V1 personal workforce

V1 adds the project-scoped personal workforce product. `projects` is the top-level scope with `id`, `slug`, `name`, `color`, `description`, `status`, `created_at`. Planify is seeded as the first dogfood project.

- Disk files under `data/disk/<project-slug>/` tracked by `disk_folders` / `disk_files`. `local_only` flag is a hard content boundary: non-local engines receive a redacted marker from `readFileForEngine`.
- Memory in `agent_memory` with `scope = agent | project | global`. `recallForAgent(agent_id, project_id, query, engine)` requires explicit engine so local-only memory is masked for cloud engines.
- Secret resolution order: `agent:<id>` → `project:<id>` → `global` → `process.env`.
- Marketplace includes `kind: 'agent'` templates. `/marketplace?kind=agent` discovery; `/team` management.

Full reference: [`ai-docs/v1-personal-workforce.md`](ai-docs/v1-personal-workforce.md).

## Auth + Privacy Foundation

`feature/auth-privacy-foundation` (v2.1.0, 2026-05-17). Adds pluggable auth, multi-tenant org model, artifact ACLs, at-rest encryption, per-artifact passphrase protection, compliance-grade audit.

Key shape:
- **Auth modes** — `MADARX_AUTH_MODE=local` (dev) or `supabase` (production).
- **Orgs / roles** — `organizations`, `users`, `org_memberships` (`owner`/`admin`/`member`/`viewer`). Hybrid membership cache: 5-min TTL for reads; live Supabase check on sensitive paths.
- **Artifact ACLs** — `artifact_acls.scope`: `private` / `team:<id>` / `org` / `link`.
- **Share links** — HMAC-signed tokens, 7-day expiry, view-cap, revoke, IP rate limit, per-org email-OTP toggle.
- **Encryption** — `MADARX_ARTIFACT_ENCRYPTION=1` enables AES-256-GCM on new artifact disk writes. Per-org DEK derivation. Disk format: `[IV][tag][ciphertext]`. DEK never stored.
- **Audit triggers** — `BEFORE UPDATE`/`BEFORE DELETE` on `audit_logs` raise SQLite errors. `lib/compliance-audit.js` is the canonical writer; INSERT only.
- **Gate 0** — `executeRegistryTool()` accepts `user_id`; threaded into every `approval_requests` and `audit_logs` row.
- **Service-principal propagation** — `schedules.created_by_user_id` → `heartbeat_tasks.acting_on_behalf_of_user_id` → audit rows.
- **Pre-production** — full-disk encryption required (FileVault/LUKS/BitLocker). `MADARX_AUTH_MODE=supabase`. Run `npm run check:disk-enc` at boot. See `docs/deploy/production-checklist.md`.

Full reference (tables, columns, routes, env vars, binding decisions): [`ai-docs/auth-privacy.md`](ai-docs/auth-privacy.md).

## Hybrid local-first sharing (Supabase shared layer)

Personal work stays **local by default**: local SQLite + the filesystem are the source of truth for private artifacts and local runs. Supabase is the **cloud coordination layer** for org collaboration only — auth/orgs/members, explicitly shared spaces, shared-artifact metadata, ACLs, share links. The full local runtime (agents, runs, heartbeat, secrets, egress) is **not** mirrored to the cloud.

- **Cloud schema** — `supabase/migrations/0001_shared_layer.sql` (RLS on every table; `is_org_member()` / `org_role()` helpers; service-role bypasses RLS for server-side publish). Apply via Supabase CLI or dashboard SQL editor. See [`supabase/README.md`](supabase/README.md).
- **Server-only library** — `lib/shared-cloud.js`. `isSharedCloudReady()` (readiness, never returns the secret), `getSharedCloudAdmin()` (lazy PostgREST service-role client), pure mappers `mapArtifactToCloudMetadata` / `mapSpaceToCloudMetadata` (body omitted unless `includePayload`), `validateSharedArtifactMetadata`, and explicit audited `publishArtifact` / `unpublishArtifact`. `assertServerOnly()` blocks client import.
- **Nothing leaves implicitly** — publishing is the ONLY path artifact data goes to the cloud, always explicit + audited. The local-only `shared_publications` ledger records publish/sync status; `getPublishStatus()` is a network-free local read that distinguishes local vs shared. An artifact with no ledger row is private and never uploaded.
- **Independent of auth mode** — shared-cloud readiness keys off `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY`, NOT `MADARX_AUTH_MODE`. Publishing works while auth stays `local`; the product never has to flip globally to supabase mode.

## Skill routing

When the user's request matches an available skill, ALWAYS invoke it using the Skill tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.

Key routing rules:
- Product ideas, brainstorming → invoke `office-hours`
- Bugs, errors, "why is this broken", 500 errors → invoke `investigate`
- Ship, deploy, push, create PR → invoke `ship`
- QA, test the site, find bugs → invoke `qa`
- Code review, check my diff → invoke `review`
- Update docs after shipping → invoke `document-release`
- Weekly retro → invoke `retro`
- Design system, brand → invoke `design-consultation`
- Visual audit, design polish → invoke `design-review`
- Architecture review → invoke `plan-eng-review`
- Save progress, checkpoint, resume → invoke `checkpoint`
- Code quality, health check → invoke `health`
