# Changelog

All notable changes to Madarx OS. Newest first.

## Unreleased

## [2.16.3] - 2026-06-05

### Added

- **Local MisoTTS-compatible voice fallback.** `voice_speak` and `/api/voice/tts` now support a `local` provider through `MADARX_LOCAL_TTS_URL`, with optional `MADARX_LOCAL_TTS_MODEL` / `MADARX_LOCAL_TTS_VOICE`, so self-hosted engines can speak replies without OpenAI or ElevenLabs. Added `scripts/misotts-local-server.py` as a minimal bridge for `MisoLabsAI/MisoTTS`.

## [2.16.2] - 2026-06-05

### Changed

- **Login brand motion polish.** Added the animated madarX orbit/mark lockup to the login hero, including reduced-motion fallbacks and mobile sizing, so the app entrance matches the public madarX brand language.

## [2.16.1] - 2026-06-05

### Added

- **Shared brand publishing groundwork.** Added a brand sharing API path, local ledger/schema support, Supabase migration scaffolding, cloud mapping/validation helpers, and tests for publishing/adopting brand profiles without leaking secrets.
- **Custom brand logos in release-note artifacts.** Release notes now use the inline madarX mark for the default profile and switch to a user-uploaded logo when a custom brand is configured, covered by artifact-format tests.
- **Marketplace and agent tool-category UX spec.** Captured the next presentation-layer pass for friendly tool collections, complete icon coverage, and non-technical Details affordances.

### Changed

- **Login, chat, sidebar, and brand surfaces refreshed.** Updated the login experience, chat prompt controls, sidebar avatar treatment, brand page controls, and shared UI primitives so the new sharing/tool UX can land cleanly.

## [2.16.0] - 2026-06-05

### Added

- **Edit an agent without leaving the simple page.** The `/agents/[id]` overview now lets you change the destination space, the engine + model, the schedule, and the toolset inline. Each control is a card with a pencil that opens a calm editor drawer (one at a time), so non-technical owners can retune an agent without opening Configuration.
- **Plain-English schedule editor.** Pick On demand / Hourly / Daily / Weekly / Monthly with a time and day picker and a live preview ("Runs daily at 7:30 AM"), with an Advanced field for raw expressions like `every 5m`.
- **Tools as icons + a simple picker.** The agent's tools show as brand icons (the same vocabulary as the agents grid). The picker is searchable, grouped by category, and tags tools that need approval or setup. Adding a tool enables it for real (read-only → auto, write/send → approval-required) while deep per-tool permission modes stay in Configuration.
- **Searchable activity feed.** Recent activity now merges executions and approvals into one list you can filter by text.
- **Business agent marketplace foundation.** Connection-kind marketplace items plus the connections layer that backs them: OAuth2 connector auth with automatic access-token refresh on expiry (refreshed tokens re-stored in the vault, never logged), a generic manifest-driven "Test connection" health-check verifier so any OAuth/Tool-Smith connection with a declared `health_check` works with no route edits, connection-manifest validation, and a `grantConnectionToAgents` path for handing a verified connection to agents at the right default risk mode.

### Changed

- The Simple/Details toggle is gone — the **Configuration** button is the single way into the advanced view, and the details view links back to the agent.

## [2.15.1] - 2026-06-04

### Changed

- **Agent detail is calmer and easier to operate.** The `/agents/[id]` page now uses a simpler Apple-minimal layout with softer panels, clearer status hierarchy, less visual clutter, and a cleaner details view while preserving the existing run/configuration controls and data flow.

### Fixed

- **NPM install/build metadata is clean again.** The lockfile now resolves the declared Playwright dependency used by the browser-session adapter, removing the Next build warning. Dependency audit cleanup also removes the vulnerable optional legacy `xlsx` parser path and pins `uuid` through overrides; modern `.xlsx` import remains handled by `exceljs`.

## [2.15.0] - 2026-06-04

### Added

- **Guided `madarx setup` wizard.** Setup is now an arrow-key experience (↑/↓ or j/k, Enter, number keys) that decides what shapes the whole install up front: **usage mode** — Personal (local-only) or Team (Supabase shared layer, with credential capture) — then the **main agent model** and the **default sub-agent model**, chosen from the engines actually available on your machine (it probes the Claude Code and Codex CLIs and a running Ollama, and always lists the Anthropic models). It then walks the secrets-vault master key, API keys (masked, and only flags Anthropic as *required* when your selected engine needs it), folder permissions, the WhatsApp toggle, and the auto-update policy. Re-running is safe — every step shows your current value as the default. New zero-dependency `lib/cli/prompt.js` (`select`/`confirm`).
- The sub-agent default model picked at setup is honored at runtime: `createAgent` now falls back to the `default_agent_engine` / `default_agent_model` config when a caller doesn't specify one.

### Fixed

- **WhatsApp could get stuck "logged out" with no way to re-pair.** When WhatsApp unlinked the device server-side (you removed it from Linked Devices, the number got flagged, or its anti-unofficial-client sweep caught a fresh Baileys session), the bridge left the now-invalid credentials on disk and stopped — which silently blocked a new pairing QR (Baileys only offers a QR on an empty session). The Pairing screen sat on "Waiting for QR" forever, and the only recovery control lived in the *paired* branch of the UI, so there was no visible button to reset. The logged-out state now shows a **Reset session** button (with a confirm step) that wipes the dead session and brings up a fresh QR — the same recovery the dashboard already offered after a deliberate disconnect.
- **`npm run whatsapp:reset` wiped the wrong directory on global installs.** It hardcoded `<cwd>/data/whatsapp/session`, so for a `npm i -g madarx` install (whose data lives under `$MADARX_HOME`, default `~/.madarx`) it deleted a repo-relative path that didn't exist and never touched the real session. It now resolves the session directory from `lib/paths.js`, honoring `MADARX_HOME` / `MADARX_DATA_DIR`, via the new `scripts/whatsapp-reset.mjs`.

## [2.14.0] - 2026-06-04

### Changed

- **One intake form, branded by default.** Starting a workflow case now always opens the branded, interactive intake artifact — a self-contained page with real form controls that submits straight to the engine. The old bare React intake form and its route are gone, and the `MADARX_BRANDED_INTAKE` feature flag is removed (branded is no longer optional). The Start button, the Share link, and direct case links all lead to the same single intake surface, so there is no longer a "two forms running side by side" split to reason about. Share links keep working exactly as before. (Workflow simplification Phase 3.)
- **Policy gates are now JSON, not a custom mini-language.** Workflow `policy_gate` rules move from a hand-written string expression grammar (`intake.x == 'y' && prior_stage('kyc').score < 0.5`) to a declarative JSON predicate tree (`{ "op": "and", "rules": [ { "op": "eq", "path": "intake.x", "value": "y" }, … ] }`). This removes a bespoke tokenizer/parser from a security-sensitive path, makes rules validatable at authoring time, and eliminates string-escaping edge cases. Operators: `and`/`or`/`not` and `eq`/`neq`/`lt`/`lte`/`gt`/`gte`/`in`/`nin`, over `intake.*`, `prior_stage.<slug>.*`, and `context.*` paths. New `lib/process/policy-predicates.js` with a 55-case unit test. The seeded mine workflows are migrated to the new format. (Workflow simplification Phase 4.)

### Deprecated

- **Legacy policy-gate string expressions.** The old string DSL still evaluates for one release (isolated in `lib/process/stages/policy-dsl-legacy.js`) so existing definitions keep working, but it is on a removal track. Author new gates as JSON predicates; the spec validator and `ai-docs/process-engine.md` document the canonical shape.

### Notes

- `process_definitions.intake_form_schema_json` is intentionally retained — it is the live storage the engine reads to render the intake form. Dropping it is deferred to a future migration that first relocates the schema onto the first stage's config; intake stays backward compatible.
- Optional Phases 6 (settings-drawer preview endpoint) and 7 (audit-writer unification) are deliberately deferred — see `specs/active/simplification-dev-plan.md` for the reasons (cosmetic-vs-fragility for 6; the canonical writer the plan assumed doesn't exist and the real one is a distinct compliance channel for 7).

## [2.13.0] - 2026-06-04

### Added

- **Branded CLI experience.** `npm i -g madarx` now ends on an on-brand install splash (madarX wordmark, animated checks, "Designed in UAE" dateline), and launching `madarx` shows a Hermes-style hero banner: a 3D wordmark, a titled rule, the orbit mark (madar = "orbit"), and a two-column capability catalog. Sovereign-Mono palette, single Mars accent, zero new dependencies (embedded figlet art, pure-ANSI rendering). Honors `NO_COLOR`, non-TTY, and CI; guarded so it never slows or breaks an install. Preview with `npm run splash` / `npm run banner`.
- **Business-default skill pack.** A fresh agent now ships with nine ready-to-use business skills grouped by what most teams need first — research (market-research, competitor-watch), comms (inbox-triage, meeting-scheduler), documents (document-extraction, report-builder), data (database-q&a, spreadsheet-analysis), and web (web-automation). Each is backed by the default tools and stays within the usual approval gates. They seed as installed in the marketplace and are injected into every default agent's `skills.md`.

### Changed

- **Banner features business tools, not internals.** The boot banner highlights the toolsets a business agent actually reaches for (web, email, calendar, files, browser, data, automations, build-tools) with friendly labels; domain/installable packs (mines, travel, …) fold into "more toolsets". The default skills showcase replaces the engineering/design skills (design-shotgun, design-review, qa-only, cso-audit), which remain in the marketplace.

### Migration

- Existing installs' default agents (including the Caretaker) have their `skills.md` refreshed with the business-default skill pack on next launch. Idempotent and customization-safe: a user-edited `skills.md` is never overwritten.

## [2.12.1] - 2026-06-04

### Fixed

- **Agents can no longer fabricate a fake tool run and pass it off as real.** When you asked the main agent (on the Claude Code CLI) to run another agent, it sometimes role-played *being* that agent — writing a make-believe `<tool_call>`/`<tool_response>` web-search transcript with invented results — instead of actually dispatching it. The target agent never ran, but the fiction was delivered as the answer and saved as an artifact. Now: (1) the orchestrator detects a fabricated transcript, refuses to deliver or save it, re-prompts the CLI to emit a real `run_agent` action, and if it still won't, returns an honest "it did not run" message; (2) scheduled agent ticks on an engine that can't actually run tools (`ollama`/`hermes`) are stopped up front with a clear "switch to the Anthropic API engine" error instead of saving hallucinated output as a completed report; (3) a fabricated-transcript backstop blocks any such output from being persisted as a deliverable. New `lib/engine-capabilities.js` + `lib/fabricated-tool-output.js`, fully unit-tested.

## [2.12.0] - 2026-06-04

### Added

- **Tool-Smith: agents can now build the tools they need.** A dedicated Tool Smith agent (auto-seeded like the Caretaker) authors reusable capabilities and publishes them so any agent can use them, and the main agent can delegate a build with the new `request_capability` tool ("an agent needs Notion access it doesn't have" → Tool Smith builds it → it gets granted). Four authoring paths, none of which ever loads or runs LLM-written code:
  - **Composite tools** (`create_composite_tool`) — a declarative recipe chaining tools that already exist plus LLM steps (e.g. web_search → summarize → email). The composite inherits the *maximum* risk of its steps, and the interpreter re-enters the full gate pipeline on every sub-step, so no permission, approval, allowlist, or rate-limit check is skippable by composing.
  - **REST API connectors** (`create_connector`) — a new external API (Notion, Linear, Stripe, Airtable, …) becomes usable with **no code merge, no restart, and no build-time approval**: Tool Smith writes a declarative manifest (HTTPS-only, host allowlist, secret-by-reference auth, per-endpoint risk) and each endpoint becomes a callable `<service>.<endpoint>` tool the instant the owner drops the API key into the vault once. One bounded HTTP executor runs them all — host-allowlisted, SSRF-blocked, with secrets injected from the vault and never echoed.
  - **Skills** (`create_skill`) — author a SKILL.md instruction recipe + marketplace entry any agent can invoke.
  - **Propose tool** (`propose_tool`) — for genuinely non-REST integrations, generate a reviewable code scaffold into staging. It is inert and never callable until a human reviews, merges, and restarts.
- Authored irreversible-external connector endpoints **always** require human approval at run time, even for an agent on auto-trust — an LLM can never self-author a destructive capability and fire it unattended.

### Fixed

- **Orphaned in-process daemons no longer spam `fetch failed` forever.** When a `madarx run` supervisor died ungracefully (SIGKILL, terminal force-close, crash), its `next-server` web child was reparented to init and kept running — the in-process cron + heartbeat `setInterval` loops outlived the unref'd timer via next-server's own handles, while their HTTP poke target was gone, so each tick logged `[heartbeat:inproc] tick error: fetch failed` (every 5s) and `[cron:inproc] tick error` (every 30s) indefinitely. The loops now carry a circuit breaker: after ~30s of consecutive failures the server is treated as dead, the interval is cleared, the per-isolate start flag is reset, and the daemon lock is released so a live host owner can take over. An orphaned child now self-terminates its loop in ~30s instead of ticking for a day.

## [2.11.1] - 2026-06-04

### Fixed

- **A confirmation reply no longer wipes a designed artifact.** When the main agent finished a multi-step action and replied with a status line ("All six dispatched…"), the orchestrator's `autoArtifact()` fallback treated any ≥120-char plain-text reply as an iteration on the active artifact and ran a *full content replace* — overwriting a previously designed HTML artifact (e.g. the marketing-team roster) with raw conversational text that then rendered as unstyled prose on a blank canvas. The update path now requires the same `isDocumentLike` bar as artifact creation, so confirmations, acknowledgements, and status updates stay in chat (where they already appear) and never reach — or clobber — the canvas. Real documents still create and update normally.
- **Uninstalling the Mines module now actually removes it.** Clicking Uninstall reported success but left the module, its 6 agents, and the sidebar nav in place. `uninstallModule()` returned `{status:'blocked'}` whenever a `mines_projects` row existed, but the install toggle never handled `'blocked'` and fell through to the "uninstalled" toast — so the install row never flipped and the green toast lied. Teardown was also asymmetric with install, which seeds the agents. Uninstall now cascades in one transaction: it removes the provisioned agents, deletes `mines_projects` (FK `ON DELETE CASCADE` clears all child rows), and flips the install row off, with an audit row recording how many agents and projects were removed. (PR #43)

## [2.11.0] - 2026-06-04

### Added

- **Every fresh install ships with the madarX brand active by default.** `seedBrandProfile()` writes the `DEFAULT_BRAND_PROFILE` (Onyx/Platinum/Bone/Graphite/Mars palette, Inter/Cairo/IBM Plex Mono, voice + document types) into the `config.brand_profile` row on first boot — previously the brand was only resolved lazily at read time and never persisted, so a global install had no logo and null brand paths. The seed extracts the logo data URL + colors from `brand-assets/brand-guidelines.html` when present, falling back to the hard-coded profile. Idempotent and never overwrites a user-customized profile; the user edits it later via `/brand`, `PUT /api/brand`, or the `import_brand_guidelines` tool. `brand-assets/` now ships in the npm package so the default logo/guidelines exist on a global install.
- **Fresh installs land a CTO brief on the main space.** `seedCtoBriefArtifact()` reads the vendored `seed-assets/cto-brief.html` and creates an HTML artifact bound to the `default` ("main") dashboard space, so a brand-new install opens with a starting document instead of an empty canvas. Idempotency uses a `config.cto_brief_seeded` marker — delete-stays-deleted, so removing the brief doesn't bring it back on the next boot. `seed-assets/` ships in the npm package.
- **Release notes appear on the main space after every update.** On a version bump, `seedReleaseNotesArtifact()` drops a "What's new" artifact onto the main space, built from the matching `CHANGELOG.md` section for the now-running version. Boot-time version detection (`currentVersion()` vs a `config.last_release_noted` marker) means it fires regardless of how the upgrade landed — CLI `madarx update`, dashboard update, weekly auto-update, `npm i -g`, or a git pull + restart. A fresh install only sets the baseline (no note — nothing was "updated"); subsequent version bumps each produce one note. Per-version marker is the sole idempotency guard (delete-stays-deleted). `CHANGELOG.md` now ships in the npm package so the notes resolve on a global install.

## [2.10.1] - 2026-06-04

### Fixed

- **Team page no longer 500s during a Supabase auth hiccup.** In supabase auth mode the `supabase.auth.getUser()` call on the request hot path was unguarded, so a transient Supabase Auth failure (token refresh, rate limit, cold project, network blip, or a hung connection) threw an uncaught error. In `lib/auth.js` that surfaced as a bare HTTP 500 from any route's `requireSession`; in `middleware.js` — which gates every matched route — it 500'd whatever request was in flight, taking down unrelated endpoints (`/api/dashboard/spaces`, `/api/state`, `/api/events/stream`) together and dropping SSE streams as `CONNECTION_REFUSED`. Both call sites now wrap `getUser()` in try/catch plus a 4s timeout race and fail closed: `getSession` returns `null` (clean 401), and the middleware redirects to `/login`. No auth path can fail open.

## [2.10.0] - 2026-06-03

### Added

- **WhatsApp remembers the conversation.** The WhatsApp main agent now keeps multi-turn context across inbound messages from the same chat, so a follow-up like "add it to my main space" resolves against the prior turns and actually performs the action instead of re-asking. Since WhatsApp has no client to hold state, context is reconstructed server-side from `whatsapp_messages`, scoped by `chat_id` and bounded to a session window. New `whatsapp_sessions` table (`chat_id` PK, `session_started_at` cutoff) — history replay only includes rows at or after the cutoff, and artifact-attachment outbound rows are excluded. A **`/new` command** short-circuits the LLM and advances the cutoff to now, starting a fresh session without deleting the audit log.
- **Self-serve team console + link join.** New `/settings/team` console (org name, members, invites, create-org) replaces the admin-only invites panel. Opening an invite link now survives the Supabase sign-in redirect: the bearer token is stashed in a short-lived httpOnly cookie (`madarx_pending_invite`, 10 min) via `POST /api/auth/join-bootstrap`, then redeemed by `GET /api/orgs/join-complete` after sign-in. `ensureCloudOrg()` mirrors the local org into the cloud `organizations` table before invites/memberships are written, so cloud `org_invites` and the redeem RPC no longer FK-fail against a missing org row.
- **madarX favicon in browser tabs.** Added `app/icon.png` so the App Router emits a real favicon link — tabs no longer show the default globe. Bold self-contained mark (rounded graphite background, bone X + orbit ring, Mars Red bead) readable at 16px on light and dark tab bars.

### Fixed

- New-agent identity hint copy: "used by org chart" → "used by agent chart".

## [2.9.18] - 2026-06-03

### Added

- **Disconnect WhatsApp from the dashboard.** The WhatsApp section now has a Disconnect control (with a confirm step) that truly unlinks the paired device server-side, wipes the local bridge session, and re-offers a QR for re-pairing. New `POST /api/whatsapp/disconnect` (session-gated, writes a `whatsapp.disconnect` audit row) drives a new bridge `POST /logout` endpoint: it calls `sock.logout()` so the device disappears from WhatsApp's Linked Devices, then clears in-memory state, removes `SESSION_DIR`, and restarts the socket. `logoutWhatsApp()` added to `lib/whatsapp.js`.

### Chore

- `.inproc-token` runtime artifact is now git-ignored (was surfacing as an untracked file under `data/`).

## [2.9.17] - 2026-06-03

### Added

- **Attach files to the main agent chat.** The composer now takes file uploads three ways: drag-and-drop anywhere over the chat panel (a blur overlay reads "Drop files to attach"), a paperclip button, and paste (Cmd+V) for images. Attachments show as chips (image thumbnails inline, file icon + name otherwise) with one-click removal, and ride on the message into the agent's context. Attachment-only sends (no text) are allowed.
  - **Storage** — `lib/chat-uploads.js` persists uploads under `DATA_DIR/uploads/` keyed by an opaque 32-char hex id, with a sidecar metadata file. The read path validates the id against a strict `^[0-9a-f]{32}$` pattern so a client can never point it at an arbitrary path (no traversal). 20 MB per-file cap, 8 files per message. New endpoint `POST /api/orchestrator/upload` (multipart, session-gated, type + size validated).
  - **Per-engine handling** — the orchestrator (`app/api/orchestrator/stream/route.js`) feeds attachments to whichever engine is active: the Anthropic path gets native content blocks (images as `image`, PDFs as `document`, text files inlined), while CLI engines (Claude Code / Codex) and Ollama get text files inlined plus a note for images/PDFs to switch to an Anthropic model for vision. Supported types: images (png/jpeg/gif/webp), PDF, and text/code files.

## [2.9.16] - 2026-06-03

### Fixed

- **Heartbeat/cron ticks still 401'd after v2.9.14 because the poke loop ran in a DIFFERENT process than the web server.** The in-process cron/heartbeat loop is owned by whichever process first wins the machine-wide daemon lock after opening the DB. In a normal `madarx run` that turned out to be the **WhatsApp daemon** (`scripts/whatsapp-daemon.mjs` calls `getDb()`), not the web tier — so it POSTed the web server's `/api/heartbeat/worker` from a separate process whose token never matched. v2.9.14's `globalThis`-pinned token only shares within one process, so it couldn't help. Two fixes:
  - **`run.js` now spawns the WhatsApp daemon with `MADARX_DISABLE_INPROC_DAEMON=1`** so only the web child owns the poke loop (the poke targets the web tier's own process).
  - **The inproc token is now persisted to a `0600` `.inproc-token` file under `DATA_DIR`**, making it install-stable across processes (race-safe via `O_EXCL`). Any legitimate cross-process poker (hermes job runner, an orphaned web server left by a prior `npm i -g` upgrade, split-process deploys) now resolves the same token. `MADARX_INPROC_TICK_TOKEN` still overrides; falls back to a process-local token if `DATA_DIR` is unwritable. Threat model matches `MADARX_MASTER_KEY` / the auth recovery token (both `0600` files in `DATA_DIR`).

## [2.9.15] - 2026-06-03

### Fixed

- **Uploaded/created artifacts never appeared on the project page even though the files existed on disk** (`~/.madarx/workspace/projects/<slug>/artifacts/`). The artifacts table stores `path` via `relToRoot()` = `path.relative(ROOT, abs)`. `ROOT` is derived from `import.meta.url`, which Next.js bakes at **build** time — so in the standalone web bundle on a global install `ROOT` is the CI builder's path. Artifact files live under `WORKSPACE` (`$MADARX_HOME/workspace`), **outside** that baked `ROOT`, so stored paths became `../../../../../Users/.../.madarx/workspace/...` traversals that resolve to nothing locally. `getProjectWorkspaceSummary`'s ghost filter (`fs.existsSync(path.resolve(row.path))`) resolved them against `process.cwd()`, the path doubled, `existsSync` returned false, and **every** artifact was dropped as a non-existent "ghost". Rendering reads DB `content`, so the data was always intact — only the disk-existence gate hid it.
  - **New `resolveStoredPath()` (`lib/paths.js`)** re-anchors the `workspace/` or `data/` tail of a stored path onto the runtime `WORKSPACE`/`DATA_DIR`, recovering both build-baked rows and dev rows. No data migration needed; old rows resolve at read time.
  - Applied at the ghost filter (`lib/projects.js`), `deleteArtifact`'s on-disk unlink (`lib/runtime.js` — previously orphaned files on a global install), and the share-link disk reads (`app/api/share/[token]/route.js` — encrypted + plaintext; the path-traversal guard is preserved and now runs on the correctly re-anchored absolute path).

### Tests

- `tests/resolve-stored-path.test.js` — simulates a build-baked `ROOT` traversal and asserts re-anchoring; covers absolute, dev-relative, and empty inputs.

## [2.9.14] - 2026-06-03

### Fixed

- **In-process heartbeat/cron ticks 401'd every 5s (`[heartbeat:inproc] tick error: route /api/heartbeat/worker returned 401`).** The inproc tick token was a module-scoped singleton, but Next.js bundles the daemon (`lib/db.js`) and each route segment into separate module instances even in one process — so the daemon minted one random token and the route resolved a different one. `verifyInprocToken` never matched, the tick fell through to `requireSession`, and the daemon (no session cookie) got 401 on every poke. The same path gated `/api/cron/tick`. Pinned the token to `globalThis.__madarxInprocToken` so every bundled copy in the process shares one value. `lib/inproc-token.js` only; public API unchanged.

## [2.9.13] - 2026-06-03

### Fixed

- **Supabase-mode `membership_revoked` self-lockout when sharing to an org.** On a fresh local install (a new `$MADARX_HOME`, a wiped data dir, or a second machine) the first Supabase sign-in had no local `org_memberships` row, so `_bootstrapFirstOrg` invented a brand-new "phantom" org with no corresponding cloud row. The session then carried that phantom org, but `requireFreshMembership`'s live-check queried the authoritative cloud `org_memberships` table, found no matching row, and locked the user out with `membership_revoked` on any sensitive action (e.g. share project folder → org). The cloud is authoritative for org membership in supabase mode; the local bootstrap never consulted it.
  - **New `mirrorCloudMembershipsForUser` (`lib/shared-cloud.js`)** mirrors the user's existing cloud memberships (+ org names) into local SQLite. Prefers the service-role admin client; falls back to the member's own JWT through RLS. Additive and idempotent; returns `[]` and never throws when the cloud is unconfigured/unreachable.
  - **`getSession` now reconciles against the cloud before trusting the local table** and prefers a cloud-confirmed org over any local-only row (`_pickPrimaryMembership`). On an admin/owner instance (service-role key present) it reconciles on **every** login, so a phantom org created during a fresh install or a transient cloud blip **self-heals** instead of sticking. Member instances stay lazy (reconcile only when there is no local membership) to avoid an extra cloud round-trip per login.
- **`smart-search` crash on synthetic keydown events with no `event.key`.** `event.key.toLowerCase()` threw when `event.key` was `undefined`, surfacing in the built `layout-*.js` chunk. Guarded with `(event.key || '')` — an empty key matches no shortcut, so real-key behavior is unchanged.

### Tests

- **`mines-import-roots.test.js` no longer leaks the developer's real `data/` DB into the suite.** Its `lib/` imports were static, so ESM hoisting evaluated `lib/paths.js` (which resolves `DATA_DIR` at module load) before the test assigned `VILLA515_DATA_DIR` — binding the run to the real database. Harmless on a fresh CI checkout (defaults to local mode) but flips to real supabase-mode state on a configured dev machine, falsely failing the admin-import gate test. Converted to dynamic imports after env setup, matching the rest of the suite.

## [2.9.12] - 2026-06-02

### Fixed

- **Two more packaged-install spawns used the build-time-baked `ROOT` as their working directory — same bug class as 2.9.11's agent-tick fix.** Any `lib/` module pulled into the standalone web bundle gets an `import.meta.url` that Next.js bakes at build time, so `ROOT`/`PROJECT_ROOT` resolves to the CI builder's path (`/home/runner/work/madarx-os/madarx-os`) — absent on the user's machine. Using it as a spawn `cwd` fails with `spawn <cmd> ENOENT`. Fixed both remaining sites:
  - **Browser harness tools** (`browser_open`, `browser_extract`) spawned with `cwd: ROOT` and silently degraded to a structured failure on every call in packaged installs — the tools never actually ran. Now spawn from a directory that exists at runtime.
  - **Hermes background job runner** spawned `scripts/hermes-job-runner.mjs` with `cwd: ROOT` *and* a path relative to that non-existent dir — doubly broken. The runner script is now resolved to an absolute path against the real install dir, and the spawn uses an existing `cwd`.
- **New runtime package-root resolution.** The `madarx` CLI now passes the real install directory to the web server as `MADARX_PKG_ROOT`, and `lib/paths.js` exposes it as `PKG_ROOT` (falling back to the baked `ROOT`, which is correct in dev). A `pickExistingDir()` helper picks a guaranteed-existing spawn `cwd`. This closes the whole class of "baked CI path used at runtime" spawn failures.

## [2.9.11] - 2026-06-02

### Fixed

- **`claude-code` engine agents failed every scheduled/manual run with `spawn claude ENOENT` in packaged installs.** The agent tick spawns the `claude` CLI with `cwd: PROJECT_ROOT`, where `PROJECT_ROOT` is derived from `import.meta.url`. Next.js bakes that path at **build time**, so in the published standalone bundle it points at the CI builder's directory (`/home/runner/work/madarx-os/madarx-os`) — a path that does not exist on the user's machine. Node's `spawn` reports a missing `cwd` with the exact same `spawn <cmd> ENOENT` error as a missing binary, so the failure looked like "claude isn't installed" even when it was on `PATH`. `runAgentEngine` now resolves a working directory that actually exists **at runtime** — the `$MADARX_HOME` workspace, falling back through the data dir to `process.cwd()` — and passes only existing directories as `--add-dir` targets. The spawned binary is also resolved to an absolute path (via login-shell lookup or common install locations, overridable with `MADARX_CLAUDE_BIN`) so execution no longer depends on the server process inheriting the right `PATH`. Affected every published install, not just one machine; ollama/anthropic engines were unaffected because they use `fetch`, not a subprocess.

## [2.9.10] - 2026-06-02

### Fixed

- **Self-lockout when saving Supabase auth config failed midway.** `setAuthConfig()` flipped `auth_mode` to `supabase` in the config table *before* writing the `SUPABASE_URL`/`SUPABASE_ANON_KEY` secrets to the vault. If the secret write then threw — e.g. the master key wasn't loaded (the 2.9.9 bug) — the mode was already committed, leaving the app stranded in supabase mode with no credentials: every route redirected to `/login?error=missing_supabase_env` with no way back in through the UI. `setAuthConfig` now (1) writes the vault secrets first, and (2) refuses to switch to supabase mode unless a `SUPABASE_URL` + `SUPABASE_ANON_KEY` resolve (from the just-written secrets or existing ones), so the auth gate can never be enabled without working credentials. Recovery for an already-stranded install is unchanged: the local recovery token (`data/.auth-recovery-token`) resets the mode via the login page.

## [2.9.9] - 2026-06-02

### Fixed

- **Web server didn't load the secrets-vault master key from the data home, breaking vault writes (e.g. saving Supabase auth config).** `madarx setup` writes `MADARX_MASTER_KEY` to `$MADARX_HOME/.env.local`, and the standalone daemons load it via `loadProjectEnv()` — but the Next standalone web server only auto-loads `.env.local` from the package directory, never from `MADARX_HOME`. So any vault write from the dashboard failed with *"MADARX_MASTER_KEY is not set but the secrets vault has stored credentials."* The `madarx` run supervisor now calls `loadProjectEnv()` right after `applyHomeEnv()` (reading `MADARX_ENV_FILE` = `$MADARX_HOME/.env.local`) before spawning the web tier, so the master key and any other vault keys propagate to the server and the daemons through inherited env. No more manual `export MADARX_MASTER_KEY` in a shell rc just to use the dashboard.

## [2.9.8] - 2026-06-02

### Changed

- **Account submenu (sidebar footer) shows version / update status instead of a theme picker.** The bottom dropdown's non-functional Theme `<select>` is replaced with a live version row driven by `GET /api/system/update`: when a newer release exists it renders an "Update available · v<latest>" link in brand mars (`var(--accent-600)`); otherwise it shows the current version. Completes the update-awareness surface alongside the dashboard banner.

## [2.9.7] - 2026-06-02

### Added

- **Boot banner now shows the catalog counts** — `catalog · 73 tools · 16 skills`. `lib/tools/registry.js` exports a new lean `MARKETPLACE_TOOL_COUNT`, and the skill count derives from the marketplace seed slug lists (`PHASE1_SKILL_SLUGS` + `OPEN_DESIGN_SKILL_SLUGS`). Both are pulled via a best-effort dynamic import in the CLI supervisor — the modules build their catalogs at eval time and never open the database, so the supervisor stays DB-free, and any import failure silently omits the counts rather than blocking boot.

## [2.9.6] - 2026-06-02

### Added

- **Branded terminal banner on `madarx` boot and `madarx setup`.** New `lib/cli/banner.js` renders a block-letter MADARX wordmark plus a boxed info panel (version, data home, dashboard URL, default engine, up-to-date / update-available line) in the product palette — mars red `#c2452a` with a gradient through `#de6a53`/`#7c2b19`, gold `#d4b77f` rules, sand `#f6f3ed` values — using 24-bit truecolor ANSI. Dependency-free (node builtins only), so it's safe to import from the CLI supervisor without touching the runtime or the database. Honors `NO_COLOR` and non-TTY output (plain text) and `FORCE_COLOR`; suppress entirely with `--quiet` or `MADARX_NO_BANNER=1`. Replaces the plain `madarX OS — setup` header.

## [2.9.5] - 2026-06-02

### Fixed

- **Update banner claimed every install was out of date ("you have v0.0.0").** `currentVersion()` read `<module dir>/../package.json`, which works for the CLI and standalone daemons but misses inside the Next standalone web server, where the module is bundled into a server chunk and `import.meta.url` points into `.next/server/…`. The failed read silently degraded to `'0.0.0'`, and the in-process update-check heartbeat wrote that into the `.update_check` ledger — so the dashboard always showed an update available, even right after upgrading. Now resolved defensively: a `MADARX_VERSION` env override (set by the CLI supervisor when it spawns the web tier) first, then a list of validated candidate `package.json` paths (module-relative, cwd, and the Next standalone stub). `GET /api/system/update` also derives `behind` live from `latest` vs the current version instead of trusting the ledger's stored flag, so a banner pinned by an older bad ledger clears immediately on upgrade.

## [2.9.4] - 2026-06-02

### Changed

- **Fresh installs now ship an empty `/workflows` page.** The module workflow definitions (ADGM Entity Setup, HAIL Engagement Lifecycle, HAIL Quarterly Review, and the four Mine workflows) are no longer seeded by default — they are opt-in behind `MADARX_SEED_DEFAULT_WORKFLOWS=1`, mirroring the existing `MADARX_SEED_DEFAULT_AGENTS` gate for the Mines live agents. The agent templates and the `kind:'workflow'` marketplace catalog still seed regardless, so any of these workflows can still be installed on demand from the marketplace. Fresh-install defaults are now just the **Caretaker** agent plus the infrastructure heartbeat tasks (`update_check`, `tool_rate_limit_gc`). The 19 workflow/HAIL/Mines test suites set the flag in their setup so they still exercise the definitions.

## [2.9.3] - 2026-06-02

### Fixed

- **Global install rendered with no CSS, no JS chunks, and a broken logo.** `next build` with `output: 'standalone'` emits `.next/standalone/server.js` but deliberately does **not** copy `.next/static` or `public/` into the standalone directory — and the standalone server resolves both relative to its own location. The published tarball therefore shipped a server with no static assets beside it. Added `scripts/postbuild-standalone.mjs`, wired as a `postbuild` npm hook, which copies `.next/static` → `.next/standalone/.next/static` and `public/` → `.next/standalone/public/` after every build. Since `prepublishOnly` runs `npm run build`, every future publish ships a self-serving bundle. Found dogfooding a fresh `npm i -g madarx`.

## [2.9.2] - 2026-06-02

### Fixed

- **`madarx update` is now robust against a stale npm cache.** The global install runs with `--prefer-online`, so `@latest` always resolves against fresh registry metadata. Without it, updating right after a new release could fail with `npm error notarget No matching version found` when npm's local packument cache predated the new version. Found while validating the self-update pipeline.

## [2.9.1] - 2026-06-02

### Maintenance

- First patch release after the `madarx` CLI launch — exercises the end-to-end self-update pipeline (`madarx update` / weekly check → `npm i -g madarx@latest`).
- docs: `specs/active/share-projects-with-permissions.md` gains the required `## Team Orchestration` section so it passes the active-spec format gate.

## [2.9.0] - 2026-06-02

### feat(cli): one-command install, run & auto-update — madarX OS is now an installable npm package

Install with `npm i -g madarx` (or the `curl … install.sh | bash` one-liner) and run the whole stack — web dashboard, cron scheduler, durable heartbeat worker, and the WhatsApp bridge — with the single command **`madarx`**. Modeled on how Hermes installs and runs. (The brand stays "madarX OS"; npm forbids uppercase, so the published name is the lowercase `madarx`.)

- **`madarx` CLI** (`bin/madarx.mjs`): `run` (default — boots everything and opens the dashboard), `setup`, `status`, `doctor`, `update`, `uninstall`, `version`, `help`.
- **`setup` wizard** — configures the secrets-vault master key, the folder permissions the agent may read/write/run in (`MADARX_ALLOWED_ROOTS`), API keys (stored encrypted), the WhatsApp toggle, and the weekly auto-update preference.
- **`$MADARX_HOME` data home** (default `~/.madarx/`) — DB, workspace, secrets, and update ledger live outside the package dir, so upgrades never wipe your data. The web tier ships as a prebuilt Next.js **standalone** bundle, so a global install has no build step.
- **Self-update** — a weekly check writes `~/.madarx/.update_check`; `madarx update` (or the dashboard banner) applies it, and an opt-in hands-off mode auto-applies. An agent can never silently update: the `self_update` tool is `irreversible_external` and always requires owner approval.
- **`uninstall`** keeps your data by default (`--purge` removes the home). One-liner installers (`scripts/install.sh`, `scripts/install.ps1`) and an npm publish workflow (`.github/workflows/publish.yml`) ship it.
- **Branding** — the data/workspace env overrides are now `MADARX_DATA_DIR` / `MADARX_WORKSPACE`; the legacy `VILLA515_*` names remain as deprecated aliases.

Plan: `specs/active/npm-installable-distribution.md`. Full guide: `docs/install.md`.

## Unreleased

### feat(sharing): per-principal project share ACL + Supabase-mode auth fixes (2026-06-02)

Whole **projects** can now be shared into the org with **per-member permissions** (viewer/editor/admin), not just coarse `org|team|private` visibility — the space-level mirror of the existing artifact ACL. Plus three Supabase-auth-mode fixes surfaced while diagnosing a project-share 403.

#### Project share ACL (Phases 1–3)

- **Cloud schema** `supabase/migrations/0005_shared_space_acl.sql` — `shared_space_acl` (mirror of `shared_artifact_acl`, keyed on `shared_space_id`; `principal_type ∈ user|team|org`, `permission ∈ read|write|admin`; RLS select=`is_org_member`, write=`org_role in (owner,admin)`; `unique(shared_space_id, principal_type, principal_id)`). **Operator must apply this migration to the cloud DB.**
- **`lib/shared-cloud.js`** — `grantSpaceAccess` / `revokeSpaceAccess` / `listSpaceAccess` / `resolveSpacePermission` (read-time enforcement) + a pure, unit-tested `decideSpacePermission` (visibility + grants → effective permission; highest wins). Service-role writes, principal/permission validation, compliance audit on every grant/revoke, must-be-published guard. `team` principal is in the cloud CHECK domain for forward-compat but **rejected by the writers** (no teams model yet — deferred to Phase 4) so no dead grants.
- **API** `app/api/shared/spaces/[id]/acl/route.js` — GET (list) / PUT (upsert) / DELETE (revoke). Auth chain `requireSession → requireFreshMembership → writer`; mutations additionally require org owner/admin **or** an `admin` grant on the space (`requireSpaceAdmin`), matching the cloud RLS write policy.
- **UI** — `SharePanel` now branches on `source_type:'project'` into a new `project-share-controls.jsx`: publish-to-org CTA, visibility selector, member grant editor (picker + role dropdown + revoke), with loading skeleton, cloud-not-ready notice, and empty/error states. A **Share** button on the project header opens it. Enforcement is read-time and metadata-only — file bodies never leave the machine. `use-share-data.js` gains `readProjectShareData`; `share-scope-selector.jsx` gains a `kind` prop.
- Tests: `tests/shared-space-acl.test.js` (13 — decision matrix, validation guards, team-rejection, not-configured/unpublished short-circuits, mocked publish→grant→list→resolve→revoke) and `tests/api-shared-space-acl.test.js` (4 — route happy path + guards). Build green (`npm run build` exit 0).

#### Supabase-mode auth fixes

- **Project-share 403 (`forbidden`) root cause** — a Supabase-authenticated user with no local `org_memberships` row resolved `role:null`, failing the writer gate. Session role comes from local SQLite, not the JWT; added `scripts/grant-cloud-membership.mjs` (idempotent) to provision a membership for an authenticated user, and documented the local⇄cloud membership requirement.
- **`middleware.js`** — loopback daemon ticks (`/api/cron/tick`, `/api/heartbeat/worker`) carrying `x-inproc-token` were redirected to `/login` (HTML), breaking the in-process cron/heartbeat loops with `Unexpected token '<'`. Middleware now lets those token-bearing requests through to the route, which performs the real constant-time token verification.
- **`app/login/page.js`** — awaited `searchParams` (Next 15 async dynamic API) to clear the sync-access warnings.

### feat(orgs): multi-tenant invite & join — bring members onto a shared Supabase project without ever sharing the service-role key

An **owner** configures Supabase once; **members** join the same project from their own machines and read/write under **RLS with their own JWT** — the `SUPABASE_SERVICE_ROLE_KEY` stays only on the owner's machine. Two invite kinds, one table, one local-first source of truth that mirrors to the cloud.

- **Email pre-invite** — an owner/admin invites an address from `/admin`; when that person signs in (verified email only), they are claimed into the **inviting** org instead of being bootstrapped into a brand-new personal org. `getSession` runs the claim only on the no-membership path (cloud RPC under the member's JWT, mirrored locally).
- **Shareable invite link** — owner/admin mints a link whose token is a 256-bit CSPRNG secret shown **once**; only its SHA-256 hash is stored. Any signed-in user redeems it once (subject to expiry / max-uses / revoke) via `POST /api/orgs/join`. Redemption is atomic; re-redeeming as an existing member is a no-op that consumes no use.
- **Invite blob + `/join` trust screen** — a member pastes one link and is connected → signed in → joined, never seeing a key. `join-bootstrap` is local-mode-only, never accepts or persists a service-role key, runs the SSRF guard before any write, and `/join` shows the **target host pill** for explicit verification (anti-phishing). The token rides in the URL fragment, never a query string.
- **Create-or-join onboarding** — a signed-in user with no org lands on `/onboarding/org` (Create org / Join with code), with any pending email invites surfaced for explicit Accept.
- **Member writes via RLS** — `publishArtifact`/`unpublishArtifact` select the service-role admin client (owner) or a user-JWT client (member) automatically; members publish through RLS. `share_links` writes tightened to owner/admin (the one justified RLS change).
- **Security**: cloud redemption runs through two hardened `SECURITY DEFINER` RPCs (pinned `search_path`, `auth.uid()`/verified-email asserts, EXECUTE locked to `authenticated`, no client-supplied org/role/user/use-count). The granted role is bounded by the creator's role, enforced by a DB trigger on insert **and** role-changing update. An independent adversarial review ran against the diff; its P1/P2 findings (insert-only ceiling, unqualified `digest()`, revoke-by-id no-op, rate-limit dodge) were fixed.
- Also fixes a supabase-mode `/login` redirect loop on server-guarded pages (server-component cookie resolution).
- Tests: `tests/org-invites.test.js` (22) + `tests/auth-server-component-cookies.test.js` (6) + Phase-6 client-selection cases in `tests/shared-cloud.test.js`. Full suite **1109 passing**; build green. Cloud RPC/RLS paths are integration-verified against a live project (migrations `0003_org_invites.sql`, `0004_tighten_share_links.sql`).

### feat(hail): HAIL operating-model layer — kickoff specs + Phases 1–6 (foundation → pilot/managed-layer + quarterly review)

Started **HAIL** (Human-to-Agent Intelligence Layer): the operating-model configuration layer that interviews a principal, produces the operating-model artifacts, and — only after human review gates — configures madarX OS from the approved blueprint. Delivered through madarX OS; preserves local-first, artifact-first, approval-gated, and the process-engine sole-writer invariant.

#### Phase 6 — Pilot + Managed Layer + quarterly review (added)

Completes the engagement lifecycle and opens the ongoing Managed Layer.

- **`lib/hail/pilot.js`** — when the pilot-launch gate is approved, the gate listener marks **only provisioned** blueprint agents `live` (the applied setup package is the prerequisite — un-applied agents can't go live). Renders the **artifact-first pilot test report** (idempotent via `dedupe_key`); `syncPilotState`/`getPilotStatus` for the cockpit/API. Writes only artifacts — no runtime mutation, never `process_cases.*`.
- **Managed Layer** — the `managed-layer-activation` gate moves the engagement to `managed_layer` (and the engine completes the lifecycle case).
- **`lib/hail/quarterly-review.js`** + **`hail-quarterly-review` workflow** (5 stages, seeded + published) — the ongoing Managed Layer review is a **separate case linked to its engagement by project**. `synthesizeReviewDelta` builds a **dry-run delta** setup package (re-proposing the current config + any refinement) that routes through the **same approval → apply path**: the `reconfig-approval` gate (`irreversible_external`) flips the delta to `approved` via the generalized gate listener, then the **existing `setup-applier`** provisions it (idempotent no-op for already-applied items). Re-synthesis never resets live/provisioned blueprint statuses.
- **Generalized gate listener** — `review-gates.js` now resolves both HAIL workflows (`HAIL_WORKFLOW_GATE_STAGES`) and runs engine-non-invasive side effects on approval: setup-package flip, pilot-go-live, managed-layer advance — exception-safe, no spurious effects for non-HAIL approvals.
- **API**: `GET/POST /api/hail/engagements/[id]/pilot` and `…/quarterly`; **UI**: a cockpit Pilot & Managed Layer panel (live-agent status + start quarterly review) — all practitioner-gated.
- **Hardening (reviewer):** enforce engagement↔project **1:1** (partial unique index + a `createEngagement` guard) so a quarterly case always resolves to the right engagement; safer `requires_approval` carry-forward in re-proposed delta items.
- Tests: `tests/hail-pilot.test.js` (4), `tests/hail-quarterly-review.test.js` (5), and the extended `tests/hail-lifecycle-e2e.test.js` (pilot-live → managed-layer → quarterly-start). Independent security + tester review applied (1 major + nits fixed; 10/10 hostile probes held). Full suite **1073 passing**; build green.

#### Phase 5 — Setup applier (the single write path) (added)

Promotes an **approved** setup package into live madarX OS configuration — the only path from a HAIL blueprint to runtime, idempotent and per-item-gated.

- **`lib/hail/setup-applier.js`** — `applySetupPackage(package_id, {user_id})`: **refuses unless the package is `approved`** (403 otherwise — nothing applies before the gate). Iterates the item ledger idempotently by `ref`; each item routes through its **existing** runtime write surface (`createAgent` / `upsertSchedule` / marketplace `setItemInstalled` / `toolAllowlists.addEntry` / `upsertDefinition` / `setAgentToolPermission`). Per-item gating mirrors the 7-gate model: **`irreversible_external` always creates its own approval even under trust=auto**; `blocked` is recorded `skipped` and never applies; `high`/`medium` gate only in `approval_required` mode; `low` applies. **`secret` items are a collection request only — a value is never set, carried, or stored** (`awaiting_secret`). Item + package audit rows with `user_id` provenance; the applier is re-runnable (no duplicate agents/schedules/allowlist rows) and **never writes `process_cases.*`**. It is the **only** `lib/hail` module that calls runtime mutators.
- **Gate → approved flip** — `lib/hail/review-gates.js` flips the engagement's dry-run package to `approved` when the `setup-package-approval` gate is approved (a scoped SQL update, engine-non-invasive, exception-safe). The applier is then operator-triggered (auto-apply is a future flag).
- **API**: `POST /api/hail/setup-packages/[id]/apply` (practitioner-gated; 403 unless approved; returns the per-item ledger). **UI**: a "Promote to live config" / "Resume apply" action on `/hail/[id]/setup` with per-item apply-status badges. `getSetupPackage` now surfaces the package through its whole lifecycle (dry_run → approved → applied).
- Tests: `tests/hail-setup-applier.test.js` (6) and `tests/hail-lifecycle-e2e.test.js` (1) — refuse-before-approval, idempotent re-apply, irreversible-gates-under-auto, blocked-skipped, secret-awaiting (no value), audit/provenance, the malformed-item defense-in-depth guard, and a **full intake→managed-layer e2e** proving zero pre-approval mutation, the gate-driven flip, applier provisioning, and case completion. Independent security + tester review applied (3 minor robustness fixes: consume-result gating, package-flip scoping, agent-id assertion). Full suite **1063 passing**; build green.

#### Phase 4 — Blueprint synthesis (added)

From the approved diagnostic, designs the governed agent operating model and a **dry-run** setup package — **zero runtime mutation** at this phase.

- **`lib/hail/blueprint.js`** — `synthesizeBlueprint({ engagement_id, engine, payload })` designs the **governance matrix** (each rule through `validateGovernanceRule`), the **agent roster** (each blueprint through `validateAgentBlueprint`, mapped onto `createAgent` fields), a **classification framework**, and a **dry-run setup package** (through `validateSetupPackage`). Persists `hail_governance_rules`/`hail_agent_blueprints` and renders three artifacts. Transaction-wrapped; idempotent (replace, not duplicate); the whole synthesis is rejected if any rule/agent/item is invalid. Demo mode deterministic; real mode calls the blueprint agent with a wall-clock timeout. **Creates no agents/schedules/secrets/tools** and **never writes `process_cases.*`.** Plus `getBlueprintDrafts`, `listGovernanceRules`, and `replaceGovernanceRules` (validated, transactional matrix editing).
- **`lib/hail/setup-package.js`** — authors the dry-run package: `validateSetupPackage` forces `dry_run:true` and rejects any secret value; persists the package + a per-item ledger (`pending`), superseding prior drafts (and cleaning up their item rows). `getSetupPackage` reads the current package. The actual apply is Phase 5.
- **`lib/hail/redaction.js`** — the restricted-value scrub is now shared by diagnostic + blueprint (deduped); applied to the full blueprint payload (governance, agents, setup package, classification, summary) before validate/persist/render.
- **Blueprint sign-off gate** — reuses the lifecycle gate mirroring; deny routes the case back to `blueprint-design` (revision). Covered by an end-to-end engine test.
- **API**: `GET/POST /api/hail/engagements/[id]/blueprint`, `GET/PUT …/governance`, `GET …/setup-package`, `POST /api/hail/setup-packages/validate` (dry-run, no write) — all practitioner-gated. **UI**: cockpit blueprint panel, governance matrix editor (`/hail/[id]/governance`, inline validation), and dry-run setup review (`/hail/[id]/setup`).
- **Security hardening (reviewer):** all `/hail` **server pages and `'use server'` actions** are now practitioner-gated via a shared `app/hail/hail-guard.js` (the API guard only covered the routes — this closes read/mutate access for non-practitioners in multi-tenant mode). Also fixed a `role_id`→`role_slug` field mismatch between the governance validator and the schema, and orphaned setup-item ledger rows on re-synthesis.
- Tests: `tests/hail-blueprint.test.js` (8) — records + artifacts, createAgent-field mapping, dry-run/no-secret-value, **zero runtime mutation**, inconsistent-model rejection, restricted scrub, governance edit, ledger cleanup, and the deny→revision / approve→advance gate flow. Independent security + tester review applied. Full suite **1056 passing**; build green.

#### Phase 3 — Diagnostic synthesis (added)

Turns the classified interview into the three operating-model deliverables, gated by diagnostic sign-off.

- **`lib/hail/diagnostic.js`** — `synthesizeDiagnostic({ engagement_id, engine, payload })` reads the engagement's interview responses **through the engine projection** (restricted content masked for any non-local engine; `ollama` is the only fully-local engine, matching `lib/disk.js`/`lib/agent-memory.js`), synthesizes the operating profile, role/capacity map (with prohibited overlaps), and friction map, then persists records (`hail_operating_profiles`/`hail_roles`/`hail_role_boundaries`/`hail_frictions`, transaction-wrapped, idempotent — supersede + replace) and renders four artifacts. Demo mode (`MADARX_PROCESS_FAKE_AGENT=1`) is deterministic; real mode calls the diagnostic agent with a wall-clock timeout. **Never writes `process_cases.*`.** `getDiagnosticDrafts` reads the current drafts for the cockpit.
- **Restricted never echoed verbatim** — a defense-in-depth scrub sanitizes any restricted answer value the agent (or an injected payload) echoes back, before validate/persist/render, so the guarantee holds for every engine and for arbitrary LLM output. Observation is kept separate from inference (`confidence_notes`, `needs_confirmation` flags).
- **`lib/hail/artifacts.js`** — markdown renderers (operating profile, role map, friction map, diagnostic report) through `createArtifact`; they render the *derived* payload only and markdown-escape all interpolated values.
- **`validateDiagnosticPayload`** (`lib/hail/validators.js`) — strict shape gate before persistence.
- **Diagnostic sign-off gate** — `lib/hail/review-gates.js` now mirrors engine `human_approval` decisions for the lifecycle gate stages into `hail_review_gates` (pending via `syncLifecycleGates`, resolved via the approval-decision listener — both engine-non-invasive and exception-safe). Deny routes the case back to `diagnostic-synthesis` (revision); both paths are covered by an end-to-end engine test.
- **API**: `GET/POST /api/hail/engagements/[id]/diagnostic` (practitioner-gated; 422 on invalid payload, 502 on agent error). **Cockpit**: a diagnostic panel (operating-profile version, role/friction counts, top frictions, sign-off status) with a synthesize action.
- Also hardened in Phase 3: `refused` answers now store no `evidence` either (column-level "stores no value").
- Tests: `tests/hail-diagnostic.test.js` (10) — records + artifacts, cloud-engine masking, local-engine no-echo, injected-payload scrub, idempotency, invalid-payload rejection, and the deny→revision / approve→advance gate flow. Independent security + tester review applied (the verbatim-scrub closed a real ollama-path leak). Full suite **1048 passing**; build green.

#### Phase 2 — Configuration Interview (added)

The first principal-facing surface: a structured, resumable interview with **classification at capture**.

- **`lib/hail/interview.js`** — `startInterview` / `recordResponse` / `pauseInterview` / `resumeInterview` / `setSectionCursor` / `completeInterview` / `getTranscript` / `renderTranscriptArtifact`, over the 11 PRD interview sections. Three rules enforced in code: (1) every answer **requires** a valid sensitivity class (rejected with HTTP 422 otherwise); (2) `refused` answers store **no value** (NULL); (3) `restricted` answers stay local and are **never rendered verbatim** — the transcript, the client projection, and the API responses all substitute a redaction marker. Practitioner notes (`is_practitioner_note`) are kept separate and excluded from the principal transcript. `recordResponse` updates in place per `(interview, question_key, note-flag)` for resumable editing. Audit rows record the classification + key, never the answer. **Never writes `process_cases.*`.**
- **`toDisplayResponse` / `listDisplayResponses`** — a client-safe projection that drops the raw `answer`/`evidence` entirely so restricted/refused cleartext can never cross into a browser payload or API response.
- **API** (`app/api/hail/**`): `engagements` (GET/POST), `engagements/[id]` (GET), `engagements/[id]/interview` (GET/POST), `interviews/[id]` (GET transcript, PATCH pause/resume/cursor/complete), `interviews/[id]/responses` (GET redacted, POST 422-without-sensitivity). All reads **and** writes require a practitioner (admin/owner) via a shared `app/api/hail/guard.js` — closes the "any caller on the port reads confidential content" gap in multi-tenant mode.
- **UI** (`app/hail/**`): engagements list + new-engagement intake (`/hail`), engagement cockpit with lifecycle rail (`/hail/[id]`), and the sectioned, resumable interview with a per-answer **sensitivity selector** (default confidential), practitioner-note toggle, and visually distinct restricted/refused/redacted states (`/hail/[id]/interview` + client `interview-form.jsx`). HAIL styles added to `app/globals.css` (Mars-red reserved for required/restricted). *(The Phase-2 "USER APPROVAL of the interview UX" gate remains open — pending operator sign-off.)*
- Tests: `tests/hail-interview.test.js` (11) — classification-required, refused-stores-NULL, restricted-never-rendered, pause/resume, practitioner-note separation, update-in-place, audit-no-leak, client-projection-no-leak, transcript-artifact-no-leak.
- Independent security + tester review applied: fixed a leak where the POST response and the server→client props carried restricted cleartext (now redacted server-side); gated all HAIL GET routes. Full suite **1038 passing**; build green.

#### Phase 1 — schema, domain, lifecycle wiring (added)

- **Schema:** 12 HAIL tables in `lib/schema.js` (`hail_engagements`, `hail_interviews`, `hail_interview_responses`, `hail_operating_profiles`, `hail_roles`, `hail_role_boundaries`, `hail_frictions`, `hail_governance_rules`, `hail_agent_blueprints`, `hail_setup_packages`, `hail_setup_package_items`, `hail_review_gates`) + indexes. `engagement_id` FKs cascade; `process_case_id` is a deliberate non-FK pointer (the engine owns case lifecycle).
- **Domain:** `lib/hail/engagements.js` (`createEngagement` provisions a project, starts the `hail-engagement-lifecycle` case via `engine.startCase`, persists the engagement — never writes `process_cases.*`; requires a named accountable human) and `lib/hail/review-gates.js` (gate-decision ledger that mirrors engine `human_approval` decisions into `hail_review_gates`, registered as an approval-decision listener at DB-open time).
- **Process engine:** two `kind:'agent'` templates `hail-diagnostic-analyst` + `hail-blueprint-architect` (`lib/process/seeds/hail-agent-templates.js`); the `hail-engagement-lifecycle` definition is now `published` and seeded via `lib/process/seeds/hail-engagement-lifecycle.js`. Seed order in `lib/seeds.js` puts the HAIL agent templates before the definition so the validator resolves the `agent_template_slug` references.
- **State:** `getState()` gains a safe `hail` slice (active engagement counts by lifecycle stage).
- Tests: `tests/hail-schema.test.js`, `tests/hail-engagements.test.js` (incl. a gate-mirror test proving HAIL never mutates engine case state). Full suite **1028 passing**; production build green.

#### Kickoff — specs + pure scaffolding (added)

#### Added

- **Specs** (`specs/active/`): `hail-operating-model.md` (product + platform spec — 12-table data model, library/API/UI surface, the setup-package→runtime mapping, acceptance criteria, team orchestration), `hail-development-plan.md` (8 phased gates with file paths/tests/validation commands), `hail-workflow-definition.md` (Diagnostic → Blueprint → Pilot → Managed Layer on the process engine with review/revision gates). Source notes in `specs/research/hail-source-notes.md`.
- **`lib/hail/constants.js`** — canonical enums: sensitivity classes (incl. `refused`), lifecycle stages, governance risk tiers (mirroring the runtime `low/medium/high/irreversible_external/blocked` model), agent authorities, artifact kinds, review-gate types, reviewer roles, setup-item types, confidence levels. Pure module.
- **`lib/hail/validators.js`** — strict pure validators `validateGovernanceRule`, `validateAgentBlueprint`, `validateSetupPackage` (the shape gate between a HAIL blueprint and any runtime apply; enforces dry-run-only packages, no secret values, `irreversible_external` cannot auto-execute).
- **`lib/process/seeds/specs/hail-engagement-lifecycle.json`** — 9-stage lifecycle workflow draft (status `draft`, **unwired** from `seedDefaults`); the setup-package gate is `irreversible_external` so no agent acts in the principal's name until approved.
- Tests: `tests/hail-validators.test.js` (11) — validators + a structural check that the workflow spec passes `validateWorkflowSpec`.

#### Notes

- CPO decisions recorded in the specs: HAIL lives inside `madarx-os` for v1; the classified interview lives in dedicated HAIL tables (never in process intake payloads); the lifecycle is one terminal process case with separate quarterly-review cases; a single idempotent, per-item-gated setup applier is the only path from an approved blueprint into runtime config and never writes case state.

### feat(spaces): Share a space — public links + org publish, from the space right-click menu

Spaces now have a **Share** action in the space-chip context menu (alongside Rename and Delete). Sharing covers **every artifact in the space, including ones created or moved in later** — the public viewer live-queries `artifacts WHERE space_id = <space>` at view time, so the shared set is always current.

#### Added

- **Public space links** (`/share/space/<token>`): no login, works in local mode. Per-share expiry, max views, and an optional email-OTP gate — mirrors the existing artifact share-link controls. New `space_share_links` + `space_share_link_otps` tables; domain in `lib/space-acl.js`.
- **Shared crypto helper** `lib/share-secret.js`: HMAC secret resolution, timing-safe compare, and the public-route IP rate limiter are now shared by artifact and space links (`artifact-acl.js` refactored to import them) so both use one `MADARX_SHARE_LINK_SECRET`.
- **APIs**: `POST/GET /api/dashboard/spaces/[id]/share-link`, `DELETE …/[linkId]`, and public `GET /api/share/space/[token]` (+ `/otp`, `/verify`) — all mirroring the audited artifact-share routes (HMAC token, timing-safe verify, IP rate limit, view-cap auto-revoke, signed httpOnly OTP cookie).
- **Public viewer** `app/share/space/[token]/page.js` (chromeless gallery) + OTP gate, with a shared render block `app/share/artifact-content.jsx` (escape-first markdown, sandboxed iframes for HTML artifacts).
- **Org publish** wired through the existing `publishDashboardSpace` / `/api/shared/spaces/*`; the Share modal's "Org workspace" section appears only when shared cloud is configured. `publishDashboardSpace` now also indexes the space's artifact metadata.
- **UI**: `space-share-modal.jsx` (portaled to `document.body`, viewport-centered, scrolls) launched from the space menu. Deleting a space revokes its share links.
- Tests: `tests/space-acl.test.js` — create/verify/revoke, expiry, view-cap auto-revoke, OTP verify, and the "future/moved artifact is included" guarantee.

#### Security

- The space token is the grant: the public gallery intentionally bypasses per-artifact ACLs (the operator shared the whole space and its future contents) — documented in `lib/space-acl.js`.

### feat(auth): UI-driven Supabase auth configuration — no `.env` editing, no restart

Auth mode and Supabase credentials are now configured entirely from **Settings → Account → Authentication**. `MADARX_AUTH_MODE` is no longer a load-time constant; `getAuthMode()` resolves at request time from the `config` table (explicit env still wins as a backstop).

#### Added

- **`lib/auth-config.js`**: single resolver for auth mode + Supabase URL/anon/service-role key. Mode lives in `config`; credentials in the encrypted secrets vault under the canonical `SUPABASE_*` names (shared with cloud sync + tool adapters). Includes a connection test and a filesystem **recovery token**.
- **`POST/GET /api/auth/config`** (public GET for middleware + the login form; never returns the service key) and the `<AuthConfigForm>` settings UI (test connection, switch mode, service-role key entry).
- **First-login org bootstrap**: enabling Supabase mode and signing in on a fresh install makes that user the owner of a personal org.
- Lockout recovery (both): a localhost-gated **Reset to local mode** control on `/login` requiring the `data/.auth-recovery-token` (0600) secret, plus the `MADARX_AUTH_MODE` env override.
- Tests: `tests/auth-config.test.js` (precedence, vault placement, recovery-token verification).

#### Security (addressed in review)

- Recovery-to-local is gated on a filesystem token (timing-safe compare), **not** the spoofable `Host` header.
- Edge middleware resolves config via the public route and **fails closed** (last-known-good, else gate) — never fail-open to local.

### fix(ui): center the Share-space modal via a body portal

The modal rendered inside `.artifact-dock`, where an ancestor's `transform`/`filter` reparented its `position: fixed` box — causing an off-center, bottom-clipped dialog. Now portaled to `document.body`, viewport-centered, height-capped, and internally scrollable; body scroll locked while open.

### feat(shared): Supabase-backed org collaboration for spaces, project folders, and artifacts

MadarX OS now has a production-safe shared-collaboration layer on the dedicated Supabase project (`hgxzlitmoemkjebcjfek`) while preserving the local-first privacy boundary. The `madarx` org is seeded with confirmed Supabase Auth users (`ak@madarx.io` owner, `dbd@madarx.io` admin) and an org-visible `MadarX Shared Workspace` (`local_space_id: madarx-shared`).

#### Added

- **Server-only shared-cloud module** (`lib/shared-cloud.js`) for explicit publish/unpublish flows. It resolves Supabase credentials only on the server, routes egress through audit logging, and never imports service-role secrets into client code.
- **Shareable dashboard spaces** via `publishDashboardSpace`, `getSharedSpaceStatus`, `listSharedSpaces`, and `unpublishSpace`. A local dashboard space maps to `shared_spaces.space_type = 'dashboard_space'` only after an explicit publish action.
- **Shareable project folders** via `publishProjectFolder`. A local project maps to `shared_spaces.space_type = 'project'` and publishes a metadata-only index of folders, files, and artifacts into `shared_space_resources`.
- **Metadata mappers and guards**: `mapDashboardSpaceToSharedSpaceMetadata`, `mapProjectToSharedSpaceMetadata`, `mapProjectResourceMetadata`, `validateSharedSpaceMetadata`, plus artifact-to-cloud helpers.
- **Shared-space API surface**: `GET /api/shared/spaces`, `GET /api/shared/spaces/status`, `POST /api/shared/spaces/publish`, and `POST /api/shared/spaces/unpublish`.
- **Local-only publication ledgers** in SQLite: `shared_publications` for artifacts and `shared_space_publications` for dashboard/project spaces. No row means local/private and never shared.
- **Supabase migrations**: `0001_shared_layer.sql` establishes orgs/memberships/shared artifacts/spaces; `0002_shared_spaces_resources.sql` adds `space_type`, `local_project_id`, and `shared_space_resources` with RLS policies.
- Tests: `tests/shared-cloud.test.js` and `tests/shared-spaces.test.js` cover private-by-default behavior, explicit publish/unpublish, metadata-only resource sharing, artifact association with shared spaces, and malformed visibility/type rejection.

#### Privacy/security guarantees

- Personal/local work remains local by default. Sharing is explicit, audited, and recorded locally.
- Project folder sharing uploads resource metadata only by default — never file bodies.
- `disk_files.local_only = 1` resources are excluded by default and require a double opt-in to publish even metadata; bodies still never upload.
- Artifact payload sharing remains opt-in (`includePayload`); default artifact publish is metadata-only and can associate with an already-shared project/space.

#### Validation

- `node --check` clean for changed shared-cloud/API JavaScript files.
- `node --no-warnings --test tests/shared-cloud.test.js tests/shared-spaces.test.js` — 21 / 21 passing.
- `npm test` — 988 / 988 passing.
- `npm run build` — production build completed successfully. Existing optional `playwright` resolution warning remains non-blocking.

### feat(mines): engine-agnostic re-architecture — runs on the Claude Code CLI subscription OR the Anthropic API

The Mines module now runs on **either engine** and is triggerable from both the mines page and the main agent chat. `engine` is a pure cost/scale tier (personal = Claude Code CLI subscription, production = Anthropic API key), not a capability gate. A CLI-subscription user with **no Anthropic API key** can now generate diligence reports — previously every LLM path (fact extraction, narrative authoring, report generation) structurally required the API key, so picking the CLI engine produced a "credit balance is too low" error from an unfunded API account the user never opted into.

The fix is architectural: **mines is a workflow, not an autonomous agent.** The pipeline dispatches its tools itself as **direct gated calls** (`executeRegistryTool` under an agent identity — every tier/allowlist/rate-limit/approval/audit gate still applies) and uses the engine only for the two genuine LLM steps through an engine-abstracted helper that both the CLI and the API satisfy.

#### Added

- **`lib/engine-author.js` (`authorJson`)** — engine-abstracted structured-output helper. API engine → forced-tool emit; CLI/local engines → fenced-JSON with strict schema validation and bounded corrective retries, throwing on exhaustion (never ships a half-authored report). Keeps codex/ollama extensible.
- **`lib/schema-validate.js`** — dependency-free JSON-shape validator (required keys, primitive types, array-of-string) backing `authorJson`. No new npm dependency.
- **`lib/mines/storage.js`** — dataroom storage interface (`listFolder`/`readDocument`/`statFile`) with a local-disk implementation that enforces the import-root allowlist; blob-swappable later, no local-disk assumptions leak to callers.
- **`lib/marketplace-modules.js`** — `installModule`/`uninstallModule`: idempotent provisioning + an uninstall teardown guard that blocks while active mines projects reference the module (mirrors the workflow uninstall guard), with audit rows on every transition.
- **`mines_run_report` main-agent tool** (`lib/orchestrator-tools.js`) — one-action chat trigger ("assess Batamote PFS readiness") that authors the narrative on the configured engine and renders via the gated report tool. Works on any main-agent engine.
- **`runSingleReport`** (`lib/mines/diligence.js`) — single-report path for the mines page and the chat trigger, including `pfs_readiness_report` via the Feasibility Lead.
- Tests: `tests/engine-author.test.js`, `tests/mines-engine-agnostic.test.js` (pipeline + single report on **both** engines, provenance preserved, no API key), `tests/mines-storage.test.js`, `tests/marketplace-module-install.test.js`.

#### Changed

- **`runDiligencePipeline`** no longer force-flips mines agents to the `anthropic` engine. It grants the workflow's tools (no engine change), resolves the engine per run (default `claude-code`), replaces the agent-tick extraction/report steps with **direct gated** `executeRegistryTool` calls, threads `engine` + `user_id`/`org_id`, and adds `pfs_readiness_report` to the report set.
- **`mines_extract_facts`** routes its LLM step through `authorJson({engine})` (the existing injected-`chat` test path is preserved); citation validation — uncited facts rejected — is unchanged and remains the provenance guard on both engines.
- **Diligence route** (`POST /api/mines/projects/[id]/diligence`) drops the hard Anthropic-API-key 400 in favor of a per-engine availability check (`assertEngineAvailable`); accepts optional `engine` and `report_kind` in the body.
- **`mines_report_generate`** now number-scrubs the investor-presentation HTML deck's LLM-authored prose slots (they were rendered via `esc()` without `stripProseNumbers`, unlike the markdown builders) — closing a provenance gap that the new CLI authoring made more likely to trigger.
- **`executeRegistryTool`** forwards an `engine` tier hint to engine-aware adapters; it runs after every gate and never relaxes a check. The orchestrator chat path now threads `user_id`/`org_id` into tool context so chat-initiated mines work attributes audit rows to the real tenant.
- **`setItemInstalled`** gains a `module` branch delegating to the new provisioning/teardown path.

This intentionally softens the earlier "the agent is the sole tool-dispatcher" design (commit 3290515) to "the workflow dispatches; the LLM authors." Provenance (numbers ↔ cited rows, number-free prose) and the `executeRegistryTool` gate are preserved; only the dispatcher changed.

### feat(mines): PFS-readiness assessment report + Feasibility Lead agent

A new report kind that measures an asset against the Pre-Feasibility Study standard instead of pretending to be one. Built because the Batamote dataroom — 9,730 documents, drilling/trenching summaries, targets, comparables, indicative valuations — is **exploration-stage with no declared Mineral Resource**, so a real PFS cannot honestly be produced (template §15 reserve conversion is the defining step; checklist gate G3 requires Measured & Indicated only).

#### Added

- **`pfs_readiness_report` report kind** (`lib/tools/adapters/mines/report-generate.js`). A gap assessment, never a PFS. Inventories what exists (SQL-grounded, cited), maps it to the NI 43-101 body sections and the G0–G5 decision gates, scores the PFS contract checklist, lists the ordered critical-path gaps, and frames the work program to reach PFS. Rubric: `docs/mines/pfs-best-practice-template.md` + `pfs-best-practice-checklist.md`.
- **`mines-feasibility-lead` agent** ("Feasibility Lead", 6th Mines agent in `lib/mines/seeds.js`). Authors only the prose slots (`verdict`, `gap_intro`, `work_program`, `recommendation`); every gate/section status and count is rendered by the tool from cited DB rows. System prompt hard-rules: no PFS without a declared reserve; a conceptual/operator target is not a resource.
- **`tests/mines-pfs-readiness.test.js`** — asserts an exploration asset is reported NOT PFS-feasible with the resource gate as the hard blocker, a resource+reserve asset flips the verdict, prose figures are number-scrubbed, and unknown projects error cleanly.

#### Changed

- **Resource detection is fact-only** (`assessPfsReadiness`). A declared Mineral Resource requires an explicit resource-class extracted fact (QP-accepted block model) — valuation scenarios (`base`/`downside`/`upside` T1 in-situ runs and `operator_*` targets) are indicative figures on conceptual tonnage and **never** count as a resource. Prevents an exploration asset from reading as resource-backed.
- **`mines_report_generate` schema** (`lib/tools/registry.js`): `pfs_readiness_report` added to the `report_kind` enum; narrative slots `verdict`/`gap_intro`/`work_program`/`recommendation` documented.

### feat(mines): agent+LLM-only diligence reports, no deterministic fallback

The Mines diligence pipeline now generates **every** report through the agent + LLM only — all deterministic fallbacks removed — while figures stay SQL-grounded so numbers can never be hallucinated. Shipped in commit `a0de0b5`.

#### Changed

- **Agent is the sole executor** (`lib/mines/diligence.js`, new). Removed the direct `minesExtractFacts` extraction fallback and the deterministic `minesReportGenerate` report loop. For every report kind the LLM authors number-free prose (forced-tool call), then the `mines-report-packager` agent calls `mines_report_generate`. Success is detected by a **new artifact id (DB truth)**, not the agent's own claim — so a confabulated "success" or "rate-limit" message can't fool the pipeline.
- **No up-front artifact wipe.** End-of-run dedup keeps newest-per-title, so a report the agent fails to produce keeps its last-good version instead of emptying the space. Failures surfaced via `failed_kinds`; pipeline returns `status: 'partial'`.
- **LLM authors prose for every report** (`report-generate.js`), not just the investor deck. Narrative slots added to the data-quality, interpretation, and due-diligence builders; hardcoded exec-summary / thesis / work-program prose replaced with LLM slots. Legal disclaimers (conceptual-target, limitations) stay deterministic.

#### Added

- **`stripProseNumbers` provenance guard** (`report-generate.js`): redacts any figure / currency / percent / decimal / comma-thousands that leaks into LLM prose (`[see figures]`); bare years and word-numbers survive. All numbers render only from cited SQL rows — the load-bearing guarantee that the LLM cannot state a grade/tonnage/value the source never contained.
- **Retry/backoff for Claude calls** (`lib/claude.js`): SDK `maxRetries` 2→4 plus an outer retry (default 3) on 429/408/409/5xx (incl. **529 overloaded**) and connection/timeout errors; honors `Retry-After`, else exponential + jitter. Non-transient errors (e.g. 404) throw immediately. Covers both the agent tool-loop and narrative authoring — burst resilience for the diligence pipeline. Tunable via `MADARX_ANTHROPIC_MAX_RETRIES` / `MADARX_ANTHROPIC_OUTER_RETRIES`.

#### Fixed

- **Duplicate artifact title.** The dashboard artifact window already renders the title in its chrome bar, while `renderDocumentHtml` also emits a `<h1 class="doc-title">` — showing the title twice. `artifact-utils.js` now hides the in-document title inside the dashboard iframe only; standalone / raw / download views keep it. Also removed the redundant leading `#` heading from the mines markdown builders.

#### Notes

- Verified end-to-end on real Batamote: all 4 reports (investor presentation, data quality, interpretation memo, due-diligence memo) agent-generated with LLM-authored prose + SQL-grounded numbers, single title each.
- The "5/5 calls / rate limit" failure seen mid-run was the report-packager **LLM confabulating** a transient API hiccup (real account limits are 4,000 req/min · 2M input-tok/min). The DB-truth success check caught it; the retry/backoff now absorbs the transient burst error.

### fix(mines): render investor/package HTML artifacts as HTML, not raw text

The investor presentation and project-package artifacts store a full HTML document under their own kinds (`mines_investor_presentation`, `mines_project_package`). Every artifact renderer outside the results page gated HTML-vs-markdown on `kind === 'html' || 'document'`, so these fell through to the Markdown/`text/plain` path and dumped raw `<!doctype html>…` source as text.

#### Fixed

- **Dashboard artifact renderers** (`artifact-board.jsx`, `artifact-fullscreen.jsx`, `artifact-dock.jsx`, `use-artifact-layout.js`): now route HTML-bearing kinds through the sandboxed iframe (and `.html` download). Introduced a single shared `isHtmlArtifact(kind)` helper + `HTML_ARTIFACT_KINDS` set in `artifact-utils.js` so the kind list has one source of truth.
- **`GET /api/artifacts/[id]/raw`**: serves `text/html` for `mines_investor_presentation` / `mines_project_package` so the `raw ↗` link renders instead of showing source.
- **Public share flow** (`app/api/share/[token]/route.js`, `app/share/[token]/page.js`, `app/share/[token]/otp-gate.jsx`): shared mines presentations/packages now render in a sandboxed iframe with `text/html` content-type, matching `html`/`ui` artifacts.

### feat(mines): investor presentation + dashboard-space publishing + ingestion fixes

Investor-facing presentation surface for Mines, integration of mines artifacts into the dashboard space/window system, and ingestion-correctness fixes surfaced by a clean Batamote re-import. Shipped in commit `d4dd292`.

#### Added

- **Investor presentation** (`mines_report_generate` `report_kind: 'investor_presentation'`): a self-contained single-scroll microsite artifact (`mines_investor_presentation`), the accessible counterpart to the technical diligence package. Every figure renders from cited DB rows; investor prose is supplied via optional LLM-authored `narrative` slots (`hero_headline`, `hero_subhead`, `opportunity`, `findings_lead`, `upside_lead`, `risk_lead`, `the_ask`) with data-driven fallbacks. The `mines-report-packager` agent (system_prompt + expected_output) authors the narrative and produces it. New `lib/tools/adapters/mines/investor-presentation.js`.
- **Per-project default dashboard space** (`mines_projects.default_space_id`). `mines_report_generate` resolves a target space (explicit `space_id` override or the project default) and pins the generated artifact (`pinned_to_space_at`) so it surfaces as a movable/resizable window on the dashboard board regardless of the active project.
- **`project_package` report kind** (`mines_project_package`): renders the whole diligence results page as one dashboard artifact.
- **`POST /api/mines/projects/[id]/publish`**: sets the project default space, generates the `project_package`, and pins every artifact for the project into the space.
- **Results-page artifact modal** (`app/mines/[projectId]/results/artifacts-panel.jsx`): artifacts open in a draggable/resizable modal window; adds a space picker + publish-to-space toolbar.
- **`scripts/mines-drain.mjs`**: concurrent heartbeat-queue drainer (the stock worker processes one task per 30s; default concurrency 2 to avoid OOM on large spreadsheet parses).
- **Native Playwright browser-session adapter** (`lib/tools/adapters/browser-session.js`, `browser_*` tools) + `tests/browser-tools.test.js`; `playwright`/`playwright-core` added to `serverExternalPackages`.

#### Fixed

- **Legacy `.xls` ingestion** (`lib/mines/tabular-loaders.js`): read binary `.xls` via SheetJS (`xlsx`) — exceljs only handles the zip-based `.xlsx` and threw on OLE-compound `.xls`.
- **Copper unit corruption**: `Cu_ppm` columns were stored into the `cu_pct` percent column (10,000× too high — 1,170 rows showed impossible >100% Cu). Now converted ppm→pct at load; re-import corrected the data (max `cu_pct` 1.0).
- **Tabular mis-classification**: dropped greedy single-letter `x/y/z` aliases that matched geochem element columns (`Y_ppm` → `collar_northing`), forcing assay tables to mis-detect as drill; added explicit DB-export aliases (`coordsorigx`, `east`, `rl`, `azimuthorig`, `tbldhsample sampleid`, …) and an assay-first detection rule.
- **Missing document parsers**: `pdf-parse`, `mammoth`, `officeparser` were never real dependencies (only phantom nested copies), so ESM `import()` from the project root failed and every PDF/DOCX/PPTX extraction hit `parser_unavailable`. Added them (and `xlsx`) as direct dependencies; a clean re-import now yields **0 failed docs** (was 313).

#### Notes

- The investor deck and the technical diligence package read the same cited data; the deck never fabricates figures. Disclaimers (conceptual exploration target, decision-support not investment advice) are preserved.
- `npm run check` green (syntax + tests + production build) before push.

### feat(mines): real-data Batamote diligence package + due-diligence memo

End-to-end run of the Mines pipeline on the **real Batamote NI 43-101** technical report and project archive, producing a client-grade investment diligence package. Demonstrates the agent/artifact-first flow on genuine data while preserving the no-fabrication guarantee (exploration-stage asset → no invented resource or valuation).

#### Added

- **`due_diligence_memo` report kind** (`mines_report_generate`): comprehensive, provenance-driven client deliverable — executive summary, project overview, geology/mineralization, drill + trench results, comparable-asset benchmark, valuation framework + operator scenario, data inventory + QA/QC, risk register, recommended work program, and disclaimers. Generic and reusable: it composes whatever facts, comparables, targets, and scenario rows exist; every numeric claim traces to a fact id, SQL aggregate, or persisted scenario row.
- **Operator conceptual exploration-target scenario**: persisted `mines_valuation_scenarios` rows (`operator_low/base/high`) plus a `mines_valuation_scenario_report` artifact. Tonnage/grade are an operator-supplied conceptual exploration target expressed as a range, carrying the CIM disclaimer (conceptual; insufficient exploration to define a resource; not a mineral resource/reserve). In-situ values are an order-of-magnitude exploration-optionality indication only.
- **Mines results dashboard** (`app/mines/[projectId]/results/page.js`): minimalist white client-facing view — ingestion stats, drill collars/intercepts, trench geochemistry, comparables, targets, operator scenario, valuation readiness, risk register, recommended program, and every generated artifact rendered inline. Curated tables are scoped to the 43-101 source so background-loaded archive rows do not pollute the deliverable.
- **Batamote orchestration script** (`scripts/mines-batamote-demo.mjs`): idempotent pipeline driving the real adapters — approve import root, create project, import, load real structured data (28 Teck collars, 19 Bata Sur trench assays, 10 Table-4 drill intercepts tied to holes), record provenance-linked facts (deposit type, four targets, La Caridad/Cananea/La Colorada comparables, work program, QA/QC), live Cu/Ag prices, document extraction, operator scenario, canonical readiness valuation, and the three report artifacts.

#### Fixed

- **Document extraction parser compatibility** (`mines_document_extract`): now supports both `pdf-parse` v1 (function) and v2 (`PDFParse` class, real per-page text + page numbers), and both `officeparser` v4 (`parseOfficeAsync`) and v5 (`parseOffice`). Real extraction of the Batamote archive now works — the 43-101 yields 55 chunks across 47 pages; 850 documents extracted (~1,065 chunks). Without a local embedder, extraction returns `embedding_failed` (chunks created, RAG falls back to text search) rather than failing.

#### Notes

- Batamote is exploration-stage with no declared resource, so the canonical valuation correctly produces a **readiness** report (no dollar figure); the operator scenario is the only $-bearing output and is explicitly labelled not-a-resource. Comparable-asset figures (La Caridad, Cananea, La Colorada) are transcribed from §13 / the S-K 1300 reports referenced in §15. Metal prices are dated market snapshots.
- Full mines suite **139 / 139 passing** after the changes.

### feat(mines): P3 synthetic valuation engine foundation

#### Added

- **Tier selector** (`lib/mines/valuation/tier-selector.js`): chooses T1 in-situ vs T2 DCF from project stage and valuation facts, with hard blocking for exploration / Batamote-like assets that lack reserve, opex, and production-schedule inputs.
- **T1 in-situ engine** (`lib/mines/valuation/t1-insitu.js`): contained Au/Ag/Cu metal, gross metal value, discounted in-situ value, default recoveries/factors, and comparable $/oz / $/lb / $/tonne cross-checks.
- **T2 DCF seam** (`lib/mines/valuation/t2-dcf.js`): annual revenue/cost/free-cash-flow/NPV model, only reachable after the tier gate allows DCF.
- **Sensitivity engine** (`lib/mines/valuation/sensitivity.js`): commodity price ±20%, grade ±2σ, recovery, and in-situ-factor tables.
- **Valuation adapters** (`mines_valuation_run`, `mines_sensitivity_run`): persist base/downside/upside `mines_valuation_scenarios`, write a `mines_valuation_report` artifact row, and update scenario sensitivity JSON.
- **Polished valuation/readiness artifacts**: generated reports now include executive conclusion, selected tier, scenario range, sensitivity tables, assumptions/provenance, data gaps, T2 blocking reasons, limitations, recommended next actions, and a parseable JSON payload. Missing resource-tonnage runs create a `mines_valuation_readiness_report` instead of pretending a valuation exists.
- **Artifact-first mine valuation workflow copy**: `mine-valuation` now instructs the valuation analyst to return the generated artifact, keep DCF blocked unless prerequisites are present, and package produced artifacts for approval.
- **Synthetic valuation fixtures** under `tests/fixtures/mines/` for non-confidential Batamote-like, resource-stage, partial-production, and T2-capable projects.
- Tests: `tests/mines-tier-selector.test.js`, `tests/mines-t1-valuation.test.js`, `tests/mines-sensitivity.test.js`, `tests/mines-valuation-adapter.test.js`, `tests/mines-valuation-workflow.test.js`.

#### Notes

- This ships the synthetic P3 engine/adapters and automated gate. The real Batamote NI 43-101 reconciliation remains a manual/data gate until confidential resource figures are available.

## [2.7.0] - 2026-05-28

### feat(mines): P2 Batamote vertical slice — full ingestion pipeline

The Mines module now ingests a raw directory tree end to end: documents become RAG-retrievable chunks, tabular files become SQL-queryable assays and drill holes, GIS/CAD files become a spatial-asset inventory, and the data-interpreter can emit provenance-cited report artifacts.

#### Added

- **Bulk data import** (`mines_data_import`): recursive `node:fs` tree walk, 23-category folder-taxonomy classification, and per-file routing to extraction / tabular-load / spatial-index heartbeat tasks (photos catalogued, unknown types marked unsupported). Idempotent and resumable on `(project_id, original_path)`; allowlist-gated before any DB write.
- **Document extraction** (`mines_document_extract`): PDF/DOCX/PPTX parsing, scanned-PDF detection with OCR fallback, ~600-token chunking with 100-token overlap and page tracking, chunk embedding, and `mines_document_chunks` writes. Spanish-first language detection.
- **New library modules** under `lib/mines/`: `embeddings.js` (local multilingual embedder with cosine + serialize helpers), `ocr.js` (tesseract.js seam + scanned detection), `rag.js` (cosine retrieval with text-search fallback), `tabular-loaders.js` (assay/drill loaders with Ensayes junk-header + Spanish-column + comma-decimal handling), `spatial-reader.js` (`.prj`/`.shp` header reader), `data-quality.js` (per-category completeness + import progress).
- **Assay/drill SQL loaders** and `detectTabularType` column-signature classifier.
- **Valuation readiness** (`mines_assumption_validate`): T1/T2 gap analysis and recommended tier.
- **Report artifacts** (`mines_report_generate`): `interpretation_memo` and `data_quality_report`, with the provenance invariant enforced — numeric claims trace to SQL rows/aggregates, narrative claims cite `DocumentChunk.id` + page.
- **API routes**: `GET/POST /api/mines/projects/[id]/import` (progress + trigger) and `GET /api/mines/projects/[id]/chunks` (RAG retrieval).
- **Data tab** at `/mines/[projectId]?tab=data`: per-category completeness table, status breakdown, spatial-asset count, overall completeness score, and a live import-progress bar with a Run-import control.
- Heartbeat-worker dispatch for `mines.extract_document`, `mines.load_tabular`, and `mines.index_spatial` task types.
- Tests: `tests/mines-data-import.test.js`, `tests/mines-tabular-loaders.test.js`, `tests/mines-rag.test.js` (synthetic fixtures + mock embeddings; no confidential data).

#### Notes

- Heavy parsers/embedders (`pdf-parse`, `mammoth`, `officeparser`, `tesseract.js`, `@xenova/transformers`) are **optional** runtime dependencies, loaded via bundler-ignored dynamic imports so `next build` never resolves them. The pipeline degrades gracefully when they are absent (`failed` / `ocr_skipped` / `embedding_failed`, with RAG falling back to text search). Install them to run the manual Batamote dataset gates.

## [2.6.0] - 2026-05-23

### refactor(workflow): Phases 1, 2, 5 of workflow-module simplification

Ships three of seven phases from `specs/active/workflow-module-simplification.md`. Each phase landed independently with green tests. Phases 3 (bare-intake fork removal) and 4 (policy-gate DSL → JSON predicates) are still open; phase 3 needs a product decision, phase 4 is queued.

#### Phase 1 — Helper consolidation + dead-weight removal

Six new helper modules under `lib/process/` consolidate duplicated logic that was scattered across the engine, stage handlers, definition layer, and UI:

- `lib/process/util.js` — `safeJson`, `pickBrandAccent`, `loadKnownAgentTemplates`
- `lib/process/template.js` — `applyTemplate` (mustache-lite token substitution)
- `lib/process/redact.js` — `redactSensitiveId`
- `lib/process/aggregate-case-artifacts.js` — dedupes `intake_form_artifact_id` with execution `artifact_ids` while preserving display order
- `lib/process/case-artifacts-query.js` — `listArtifactsByIds` with RSC-safe row spreading
- `lib/process/status-tokens.js` — `STATUS_TONES`, `STATUS_LABELS`, `EXEC_STATUS_TONES`, `TERMINAL_CASE_STATES`, `TERMINAL_EXECUTION_STATES` (source-of-truth; engine.js now imports terminal sets from here so client components don't pull the engine into the bundle)

Consumers migrated: `engine.js`, `definitions.js`, `definition-settings.js`, `artifacts.js`, `stages/{agent-run,human-approval,package,intake-form}.js`, `app/workflows/{page,[caseId]/page,[caseId]/case-detail-client}.js[x]`, `app/api/processes/{[id],definitions/validate}/route.js`.

`lib/process/stages/intake-form.js` reduced to a defensive throw — the engine handles intake via `submitIntake()` directly and the heartbeat dispatcher should never see an `intake_form` stage. Hitting this throw means something has skipped the engine's intake routing; the throw makes the bug visible.

Acceptance:
- `grep '^function safeJson|applyTemplate|pickBrandAccent|loadKnownAgentTemplates|redactSensitiveId' lib/` returns one hit per name.
- `case-detail-client.jsx` uses `TERMINAL_CASE_STATES.has(c.status)` instead of inline `['complete','rejected','failed','cancelled']` arrays.
- `BRAND_DEFAULTS` alias + `BRAND_DEFAULT` export removed from `definition-settings.js`.

#### Phase 2 — Marketplace workflow specs → JSON files

The 3 starter marketplace workflows (`vendor-onboarding`, `contract-review`, `support-escalation`) now live as `lib/process/seeds/specs/*.json` — mirroring the `adgm-entity-setup.json` pattern. `lib/process/seeds/marketplace-workflows.js` collapsed from 342 → 61 lines: it now holds only marketplace-card metadata + the `seedMarketplaceWorkflows` writer; the spec bodies are loaded from disk at module init.

Acceptance:
- All 3 JSON specs pass `validateWorkflowSpec()` with full known-agent-template visibility.
- `tests/marketplace-workflow-kind.test.js` — 8 / 8 still passing.
- `getDefinition('vendor-onboarding')` returns identical normalized shape before/after.

#### Phase 5 — Engine internals tidy

`runStageExecution` (`lib/process/engine.js`) split into three pieces:

- `runStageExecution` — 10-line orchestration shell, public signature + return shape preserved exactly (heartbeat-worker contract unchanged).
- `dispatchStageHandler` — 23 lines, loads `case` + `stage` + `execution`, runs preflight checks, resolves the per-kind handler, invokes it.
- `applyStageOutcome` — 53 lines, single place that handles `approval_required`, fail-routing (`failureStageAfter` → terminal `rejected`), and pass-routing (`nextStageAfter` → terminal `complete`).

`seedIntakeArtifact` helper extracted from `startCase` so the env-flag + artifact creation aren't inlined into the case-creation transaction path.

Other tightening:

- `lib/process/stages/agent-run.js`: `isDemoMode()` no longer infers from `NODE_ENV` or `VILLA515_DATA_DIR` — only the explicit `MADARX_PROCESS_FAKE_AGENT=1` activates the synthetic-output path. `tests/process-agent-run.test.js` sets the flag explicitly. This stops tests silently running synthetic output when they thought they were exercising the real engine.
- `findAgentForTemplate(slug, project_id)` now requires `project_id` and never falls back to cross-project agents. A new isolation test asserts an install in project A is invisible to a case in project B.
- `app/api/processes/[id]/stages/[stageId]/override/route.js`: 73 → 28 lines. Single `transitionTo` call with a computed `{next_stage}` / `{terminal_status}` branch — no more four-way manual pass/fail × next/terminal dispatch.
- `lib/process/spec-validator.js`: `schema_version` is now reserved-for-future-use. Unknown integers are accepted (no migration exists yet); only non-integer values are rejected as malformed. `on_outcome` cross-reference check moved into `validateAgentRun` (was duplicated in the per-stage routing-refs loop). `lib/process/definitions.js::upsertDefinition` no longer writes the `schema_version` column — the schema's `DEFAULT 1` still populates it.

### Tests

- New: `tests/process-agent-run.test.js` — cross-project isolation test (a project-A install must not resolve for a project-B case).
- Updated: `tests/process-spec-validator.test.js` test 15 split into 15a (unknown integer accepted) and 15b (non-integer rejected).
- Validation: `npm test` **780 / 780 passing**; `npm run build` succeeds.

### Cumulative diff (phases 1+2+5)

- `marketplace-workflows.js`: -281 JS, +207 JSON (specs externalized)
- `engine.js`: net unchanged line-count but `runStageExecution` is now 10 lines instead of 125; complexity moved to two ≤60-line helpers
- `definition-settings.js`, `definitions.js`, `artifacts.js`, all stage handlers: helper duplicates removed; -188 lines across existing files
- New helpers (+150 lines, but each ≤40 lines and reused 2–5× across the codebase)

### Known limitations (Phase 3 + 4 still open)

- `MADARX_BRANDED_INTAKE` env flag still forks five files. Phase 3 will collapse this once the product decision lands on whether to default branded.
- `lib/process/stages/policy-gate.js` still hand-rolls its tokenizer/parser. Phase 4 will replace with JSON predicates + a TDD evaluator. In-flight cases are unaffected (rule definitions live on `process_stages.config_json`, not on `process_cases.context_json`).

## [2.5.0] - 2026-05-21

### feat(workflow): Phase 3 — marketplace `kind:'workflow'` + agent-authored definitions

Ships Phase 3 of `specs/active/json-workflow-authoring.md`. Workflows are now first-class marketplace items, agents can create and update them through gated tools, and three starter templates are seeded out of the box.

- **Marketplace `kind:'workflow'`.** Browse, install, and uninstall process workflows alongside tools, skills, bundles, agents, and connections. Each card shows the stage shape (`intake → agent → approval → package`), version, author, and risk summary. Install writes through `upsertDefinition` — the SOLE writer for `process_definitions` — so the validator, version-bump enforcement, and audit trail all apply. Uninstall soft-archives the definition and is blocked while non-terminal cases reference it. New tokens `--mkt-workflow-tint`/`--mkt-workflow-ink`; the stats row is now 7 columns.
- **Three curated starter workflows** (`lib/process/seeds/marketplace-workflows.js`). `vendor-onboarding` (intake → automated risk review → buyer sign-off → onboarding pack), `contract-review` (intake → automated clause review → counsel approval → review report), `support-escalation` (intake → automated triage → supervisor sign-off → resolution pack). All three pass the strict validator at install time and follow the same human-approval discipline as ADGM.
- **Three new agent templates** in the marketplace catalog (`vendor-risk-reviewer`, `contract-reviewer`, `support-triage-agent`) so the curated workflows resolve their `agent_template_slug` references. Each prompt emits `outcome:'pass' | 'reject'` so the existing `agent_run` outcome handler routes correctly without per-slug hardcoding.
- **Agent-authored workflows via `create_workflow_definition` + `update_workflow_definition`.** Both tools are risk `medium`, `approval_required`, rate-limited 20/hr. They call `upsertDefinition` directly — the gates fire upstream in `executeRegistryTool` (irreversible_external, approval, allowlist, rate-limit), the single-write-path invariant holds at the `upsertDefinition` boundary, and no HTTP / inproc-token plumbing is required. `acting_on_behalf_of_user_id` now propagates through `executeRegistryTool` to adapters, so `created_by_user_id` is populated on agent-authored writes.
- **Workflow install flow** (`app/marketplace/workflow-card.jsx`). Stage-shape summary on the card, new toast states `already_present` (slug already exists; the existing definition is left untouched) and `in_flight_cases` (uninstall blocked, cases still running).
- **Audit hygiene.** Adapters return `status:'error'` on structured-error paths so the audit log distinguishes refusals (`spec_invalid`, `slug_in_use`, `version_bump_required`, `not_found`) from successful writes — previously every refused write was logged as `status:'complete'`.

### Tests

- New: `tests/marketplace-workflow-kind.test.js` (7) — kind shape, seed presence, install/uninstall correctness, `already_present`, FK-safe uninstall while a case is in flight.
- New: `tests/agent-create-workflow-tool.test.js` (7) — tool registration, approval gating in `approval_required` mode, write-through after approval, `spec_invalid` structured errors, `slug_in_use`, `not_found`, `version_bump_required`.
- Updated: `tests/marketplace-listing.test.js` — 56 seeded tools (+2), 3 workflow templates, workflow item shape assertions.
- Validation: `npm run check` green — 776/776 tests pass, build succeeds.

### Known limitations (documented for follow-up)

- Workflow agents (`vendor-risk-reviewer`, etc.) are seeded into the catalog but not auto-installed per project. ADGM has the same shape today — operators install agents into their project before running a real (non-demo) case. Auto-install on workflow install is a follow-up.
- Marketplace install state is currently keyed on slug existence in `process_definitions`. A user-authored definition at the same slug will appear "installed" on the marketplace card; clicking Uninstall would archive their definition. Adding `marketplace_install_id → definition_id` ownership tracking is a follow-up.
- Re-installing an archived definition bypasses the version-bump check — acceptable as a "restore" semantic, but worth tightening once edit-then-re-install becomes a common flow.

## [2.4.1] - 2026-05-21

### feat(artifacts): main-agent artifact control + dashboard spaces

- Exposed `read_artifact` to the main-agent tool schema so Hermes can load full artifact content before editing or summarizing. Document artifacts return markdown source when available, falling back to rendered content, while the dashboard continues to display the polished rendered HTML artifact.
- Added main-agent dashboard space controls: `list_spaces`, `create_space`, `rename_space`, `set_active_space`, `delete_space`, and `move_artifact_to_space`.
- Extended `create_artifact` with `project_id`, `space_id`, and `dedupe_key` fields so the agent can intentionally place artifacts in the right project/dashboard tab and collapse retried writes when needed.
- Extended `find_artifacts` with `project_id`/slug and `space_id` filters, and now returns project/space metadata for agent follow-up actions.
- Verified `update_artifact` preserves the existing artifact row and path, updates markdown source, and re-renders the displayed HTML in place rather than creating duplicates.

### Tests

- Added `tests/main-agent-artifact-control.test.js` for update-in-place behavior, tool-schema parity, project/space-scoped create/search/read, and moving artifacts into/out of dashboard spaces.
- Validation: focused artifact/space/project suite **36 / 36 passing**; `npm test` **762 / 762 passing**; `npm run build` passed.

## [2.4.0] - 2026-05-21

Release status: merged to `main` via PR #18 (`63ad63a`) on 2026-05-21. GitHub Actions `npm run check` passed before merge.

### feat(workflow): JSON workflow authoring MVP — validator, persisted source spec, CRUD APIs, JSON editor

Implements Phase 1 + Phase 2 of `specs/active/json-workflow-authoring.md`. Process workflow definitions are now agent-authorable JSON artifacts — no code edit, no dev-server restart.

- **Strict validator** (`lib/process/spec-validator.js`). Pure function `validateWorkflowSpec(spec, opts) → { ok, errors, warnings, normalized }` enforces all 8 rule groups from the spec: top-level required fields, slug regex, schema_version, stage uniqueness, first stage MUST be `intake_form`, per-kind config validators (intake_form / agent_run / policy_gate / human_approval / package), routing references + cycle detection, 64 KB size cap, 8000-char prompt cap. Validator never touches the DB; takes an optional `knownAgentTemplates` set so the agent_template_slug existence check is testable in isolation.
- **Persisted source spec.** Four new columns on `process_definitions`: `source_spec_json`, `schema_version`, `created_by_user_id`, `created_by_agent_id`. `lib/db.js` ships idempotent `ALTER TABLE` migrations so existing installs upgrade in place. Re-seeds populate `source_spec_json` so the engine's row-set is no longer the only home of authoring intent.
- **ADGM seed extracted to pure JSON.** `lib/process/seeds/specs/adgm-entity-setup.json` is now the source of truth. `lib/process/seeds/adgm-entity-setup.js` shrunk to a 33-line shim. Engine reads (engine.js, stage handlers) unchanged — they still see the same `process_stages` row-set.
- **`upsertDefinition` is now validator-backed and provenance-aware.** Accepts `opts.source_spec_json`, `opts.created_by_user_id`, `opts.created_by_agent_id`. Backfills `source_spec_json` from `JSON.stringify(spec)` if not provided. Refuses to overwrite a published definition with structurally different stages unless `version` is bumped. New `archiveDefinition(slug)` helper soft-archives (status='archived') and refuses when in-flight cases exist (FK from `process_cases` is RESTRICT).
- **Public CRUD + validate API.**
  - `POST /api/processes/definitions` — create. Returns 201 on success, 400 with structured `errors[]` on schema rejection, 409 on slug-already-exists or version_bump_required.
  - `GET /api/processes/definitions/[slug]` — returns `definition` + `stages` + `source_spec` + `intake_form_schema`.
  - `PUT /api/processes/definitions/[slug]` — update (slug forced to match path so an agent can't rename out from under itself).
  - `DELETE /api/processes/definitions/[slug]` — soft-archive. 409 with `in_flight_cases` count if there are running cases.
  - `POST /api/processes/definitions/validate` — dry-run validate, no DB write. Powers the editor's live preview and the agent retry loop.
  - All write paths require `requireSession()` + admin/owner role.
- **JSON workflow editor at `/workflows/new/definition`.** Lightweight `<textarea>`-based editor (no Monaco bundle in the main app). Live JSON parse error display, "Validate" button that hits the validate endpoint, "Publish" button that POSTs to the create endpoint. Sidebar shows live spec summary (slug / name / version / status / stage count). Errors and warnings render inline with field paths.
- **"+ New workflow" CTA** on `/workflows` next to "Process definitions". Routes to the editor.
- **Tests.** `npm test` passes with 758 total tests on `main`; the JSON authoring work adds dedicated validator, upsert/archive, and API route coverage.
  - `tests/process-spec-validator.test.js` — 20 cases covering happy path, slug regex, schema_version, per-kind configs, routing, cycles, 64 KB cap, orphans.
  - `tests/process-definitions-upsert.test.js` — 8 cases covering source_spec_json persistence, validator integration, version-bump enforcement, FK-safe archive.
  - `tests/api-process-definitions.test.js` — 13 HTTP-layer cases: create/update/get/archive/validate, 409 slug-in-use, 400 schema rejection, 404, slug-mismatch.
  - `tests/process-definitions.test.js` rewritten against the new strict contract (longer slugs, real agent templates, version bumps where structural change is involved).
- **`/workflows` minimalist UI** (preserved from working tree) is unchanged — quiet white cards, minimal metadata, aligned with Projects / Marketplace.

Out of scope (Phase 3 — separate spec): marketplace `kind: 'workflow'`, `create_workflow_definition` agent tool, curated workflow templates. The validator and APIs above are the foundation those build on.

## [2.3.1] - 2026-05-21

### refactor(workflow): brand inherits from /settings/brand + redesigned UI

- **Workflow brand inherits from the global brand profile** (`lib/brand.js`).
  - `lib/process/definition-settings.js` adds `resolveBrandPresentation({ settings })` that merges the global brand (logo, accent, fonts, voice, company_name) with per-(definition, project) overrides (sender name, accent override, logo override, cover image, cover copy).
  - Engine snapshots the MERGED brand into `process_cases.context_json.definition_settings_snapshot.brand` at `startCase` so cases are immune to subsequent edits to either global brand or workflow overrides.
  - Branded artifact no longer requires a settings row — falls back to the global brand directly. Operators only need to configure a workflow when they want WORKFLOW-SPECIFIC presentation (cover image, sender override).
  - Settings drawer dropped its duplicate Brand section. Replaced with a `Brand` summary row showing the inherited sender / accent / fonts + a direct link to `/settings/brand`. New `2 · Presentation` section keeps the workflow-specific fields (cover image URL, cover copy, optional sender / accent / logo overrides under a disclosure).
  - `GET /api/processes/definitions/[slug]/settings?include_brand=1` returns `resolved_brand` alongside the row so the drawer can render the merged preview without a second round-trip.

- **`/workflows` page redesigned.**
  - Dark editorial stat bar (Onyx bg, Platinum/Bone numerals, Mars accent on the "in flight" cell) replaces the generic four-stat row.
  - Definitions: featured hero card + sidebar grid. Featured chosen by usage count (most cases started); singleton mode collapses to one full-width card. Initials chip on the cover and a usage-count pill in the head replace the small uppercase tile.
  - Recent cases switched from a dense table to a vertical card list with a left-edge accent stripe coded to status (Mars for failed/rejected, sky for running, amber for awaiting_approval, sage for complete).
  - All workflow UI reads the global accent from `getBrandProfile()` via a `--wf-accent` CSS variable so flipping the brand at `/settings/brand` re-tints the page.

- **Tests** (`tests/process-definition-settings.test.js`)
  - `resolveBrandPresentation` falls back to global brand when no overrides.
  - `resolveBrandPresentation` applies workflow overrides on top of global.
  - Engine snapshot at `startCase` contains the MERGED brand (override + global flow-through).
  - Total: 715 → 717 passing.

No backwards-incompatible API changes. Existing settings rows still read the same shape; the snapshot just contains richer values.

## [2.3.0] - 2026-05-21

### feat(workflow): Phase 1 + Phase 2 — branded intake-artifact UX (flagged)

Implements the design described in `specs/active/workflow-intake-artifact-ux.md` §10 phases 1 & 2.

**Phase 1 — settings drawer, redesigned card, engine snapshot (always on)**

- **New table `process_definition_settings`** (`lib/schema.js`). Single row per `(definition_id, project_id)` UNIQUE. Stores brand JSON (logo, accent, sender, cover image, cover copy), agent overrides, auto-approve rules, share-link toggle + default expiry, default-for-project flag, and a `template_version` integer that bumps on `template_html` change.
- **`lib/process/definition-settings.js`** CRUD module: `getSettings`, `getSettingsBySlug`, `listSettingsForProject`, `upsertSettings`, `deleteSettings`. Structural validation rejects malformed brand / agent_overrides / auto_approve_rules / expiry-days payloads.
- **Engine snapshot at `startCase`.** `lib/process/engine.js` reads `(definition, project)` settings at start time and writes them into `process_cases.context_json.definition_settings_snapshot`. In-flight cases bind to the snapshot — editing settings later affects only NEW cases (versioning rule, spec §5.2).
- **`/workflows` card redesign.** New `app/workflows/definition-card.jsx` client component with cover image, Start / Configure / Share actions. `app/workflows/settings-drawer.jsx` right-edge drawer with 6 sections: Project & defaults, Brand (logo URL, accent picker + presets, cover image, cover copy, live preview iframe), Form template (placeholder for Phase 3), Agents (per-stage agent template overrides JSON), Auto-approval (declarative rules JSON), Sharing (toggle + 1–90 day expiry).
- **API routes (new).** `GET/PUT /api/processes/definitions/[slug]/settings`, `POST /api/processes/start-and-route` (one-shot create + redirect URL), `GET/POST /api/processes/[id]/share-link`.

**Phase 2 — branded artifact + HMAC share-link tokens (behind `MADARX_BRANDED_INTAKE=1`)**

- **`renderIntakeFormHtml` interactive mode.** `lib/process/artifacts.js` gains a branded renderer with real `<input>` / `<select>` / `<textarea>` / shareholders-repeater controls, locked `name` attrs straight from `schema.fields[].name`, vanilla-JS submit handler (no innerHTML — DOM built via createElement), client-side validation mirror, fetch POST to a configurable `submit_endpoint`, and optional `submit_token`. Branding (accent color, logo, sender, cover image + copy) flows in from settings.
- **`lib/share-token.js` — HMAC share-link tokens.** Mint / verify / consume / revoke. SHA-256 of the token stored in `share_link_consumptions` (raw token never persisted). Single-use enforced by `UPDATE … WHERE consumed_at IS NULL` race guard. Default expiry 14 days, max 90. HMAC secret reuses `MADARX_MASTER_KEY` (no second key to manage). Tokens auto-revoke when the engine advances past the intake stage (spec §13 Q1 default).
- **Extended `POST /api/processes/[id]/stages/[stageId]/submit`.** Accepts `?token=` query param or `body.token` for share-link submissions. Token is consumed atomically before validation; verified `c` and `s` claims must match the case + current stage.
- **`/workflows/new/[caseId]` — branded intake page** (server-rendered chrome around an iframe pointing at `/api/artifacts/[id]/raw`). Reached automatically by `start-and-route` when the flag is on.
- **`/workflows/share/[caseId]?token=…` — anonymous share-link landing.** No admin chrome. Verifies the token server-side, re-renders the artifact with the token baked into the submit handler, serves it full-viewport.

**Backwards compatible.** Existing `/workflows/new` bare React form stays as the default (flag off). Existing `POST /api/processes/{id}/stages/{stage}/submit` still accepts session-cookie payloads with no token. No engine API contracts changed.

**Tests** (`tests/process-definition-settings.test.js`, `tests/share-token.test.js`)

- Settings round-trip + brand defaults merge
- `template_version` bumps only on `template_html` change
- Structural validation rejects malformed payloads
- Engine snapshot frozen at `startCase` (mutating settings does NOT change in-flight cases)
- Mint + verify + consume round-trip
- Single-use replay rejects
- Tampered token signature fails verify
- Expired token fails verify and consume
- Mint rejects expiry beyond max 90-day window
- `revokeShareLinksForCaseStage` invalidates outstanding tokens
- Engine auto-revokes share links on `submitIntake`

**Total: 704 → 715 passing** (+11 new). 0 failures.

**Rollout.** Off by default. Enable Phase 2 by setting `MADARX_BRANDED_INTAKE=1` and configuring `process_definition_settings` for the `(definition, project)` pair via the Configure drawer. Both forms run side-by-side for QA, per spec §10. Phase 3 (AI-generated form templates) deferred.

## [2.2.0] - 2026-05-21

### feat(workflow): branded intake-artifact UX spec + workflow engine overview

- **Spec.** `specs/active/workflow-intake-artifact-ux.md` — design for the artifact-IS-the-form rollout: case-first start flow, per-definition settings drawer (default project, branding, agent overrides, auto-approve rules), AI-generated branded form wrappers with locked field bindings, share-link tokens via HMAC, phased migration. Includes `specs/active/workflow-engine-overview.html` stakeholder snapshot.

### feat(marketplace): install/uninstall toast feedback

- **Post-action toast banner on `/marketplace`.** Toggling an item now redirects with toast / slug / kind query params; a new client component `app/marketplace/install-toast.jsx` renders a tone-coded banner (success / info / error) with 3.8s fade and URL scrub so a refresh doesn't re-show the same status.
- **`InstallToggle` carries slug + kind hints** so the redirect deep-links back to the correct tab.

### fix(process): in-process daemon ticks survive Next dev bundler

- **Cron + heartbeat in-process daemons now POST to `/api/cron/tick` and `/api/heartbeat/worker` instead of dynamic-importing modules inside `setInterval`.** Under Next.js dev's bundler, the dynamic import resolved to a stale module snapshot; the first claimed `process_stage` task was marked complete with `{}` payload while the execution row stayed `queued` forever.
- **`lib/inproc-token.js` — shared in-process token.** Daemon-side `pokeRoute` sends `X-Inproc-Token` on every tick; routes verify with `timingSafeEqual` and skip the session gate on match. Remote attackers without the token (and without a session) get 401. Per-process random token; `MADARX_INPROC_TICK_TOKEN` env override for diagnostic scripts.
- **`pokeRoute` aborts after `MADARX_INPROC_POKE_TIMEOUT_MS` (default 5 s)** so a hung route never stalls the daemon event loop.
- **NaN-guard on `/api/cron/tick` body.interval_ms** — a malformed payload no longer silently disables the lookback window.
- **`lib/process/stages/agent-run.js`** records `result_json` + `created_at` + `updated_at` on the `agent_runs` row so the runs table reflects process-engine outputs end-to-end.
- **`getAgentTemplate`** now resolves process agent templates (`PROCESS_AGENT_TEMPLATES`) when the slug is not in the legacy catalog, normalized to the legacy template shape.
- **`completeness-agent` prompt** tightened: enumerates every required field, forbids inventing ADGM regulatory checks beyond the schema.

### fix(perf): drop artifact bodies from case-detail polling JSON

- **`/api/processes/[id]` no longer returns `artifact.content`** — only `id, title, kind, path, created_at`. The case-detail page polls every 4 s; previously each poll re-sent every artifact's full HTML.
- **New `GET /api/artifacts/[id]/raw`** serves the body with the correct `Content-Type` and a private 5-minute cache so the iframe re-fetches only when its `src` changes.
- **`case-detail-client.jsx`** swaps `srcDoc={artifact.content}` for `src=\`/api/artifacts/<id>/raw\`` on both `ArtifactCard` and `IntakeArtifactFrame`.
- **`install-toast.jsx`** drops unstable `router` / `pathname` / `searchParams` identities from its `useEffect` dependency array; captures a `queryString` snapshot so the toast no longer re-arms on every render.

### fix(security): shareholders length cap + inproc auth gate

- **`validateIntakePayload`** caps shareholders at `INTAKE_MAX_SHAREHOLDERS = 50` — defense-in-depth alongside the existing 256 KB payload / 10 KB per-string caps. An unbounded array could slip past JSON size checks while still slowing the percentages-sum-to-100 math + downstream artifact rendering.
- **`/api/cron/tick` and `/api/heartbeat/worker`** now require either a valid session OR the inproc token (see above). Previously cron was unauthenticated and heartbeat required session; the new daemon-routed flow needs the token to call heartbeat at all in `MADARX_AUTH_MODE=supabase`.

### Tests

- `tests/inproc-token.test.js` (4 cases) — random generation, env override, header match / mismatch / length-mismatch, malformed-req safe reject.
- **Total: 704 / 704 passing** (was 700 / 700 on prior commit).

### feat(process): artifact-first workflow touchpoints

- **New product invariant — artifact-first workflows.** Every workflow touchpoint a user or colleague sees is now an HTML artifact, not a bare dashboard form / JSON panel. Three new artifact kinds are produced as a side-effect of engine state transitions:
  - **Intake form artifact** — generated at `startCase` when the first stage is `intake_form`. An HTML page rendering the form schema; id stored on `process_cases.context_json.intake_form_artifact_id`.
  - **Intake submission artifact** — generated at `submitIntake`, PII-redacted (passport / national ID / SSN / tax ID masked via `redactSensitiveId`). Linked to the intake `process_stage_executions.artifact_ids_json` alongside the form artifact.
  - **Analysis artifact** (per `agent_run` execution, pass and fail) — completeness check, KYC/risk score, drafter output, etc. Linked to that stage execution.
- **All artifacts use `lib/runtime.js::createArtifact`** so they land in the project library automatically and inherit existing ACL / encryption / share-link behavior. Implementation: new `lib/process/artifacts.js` module with self-contained HTML renderers (no external assets).
- **Engine-as-sole-state-writer preserved.** `lib/process/artifacts.js` writes only to the `artifacts` table + disk; it never touches `process_cases` columns. Artifact-write failures are silently swallowed so a presentation error cannot abort a transition — engine state remains correct even when an artifact fails to render.
- **Case detail UI now artifact-first.** `/workflows/[caseId]` shows the intake form artifact in-iframe before submission, the submission artifact in-iframe after, and a "Case artifacts" gallery section beneath the stage grid. `case-detail-client.jsx` switched to `srcDoc` rendering (HTML content comes through the `/api/processes/[id]` response) since there is no `GET /api/artifacts/[id]` route.
- **Tests.** New `tests/process-artifacts.test.js` covering: intake-form artifact creation + linkage on `startCase`, intake-submission artifact creation + dual-linkage on `submitIntake`, analysis artifact creation + linkage for completeness/kyc-risk stages, passport redaction in submission HTML (regression: no full passport number leaks to disk artifacts).
- **Docs.** `ai-docs/process-engine.md` gains an "Artifact-first presentation invariant" section; `specs/active/process-workflow-engine.md` gains an "Artifact-first invariant (binding)" section under Vocabulary.

### feat(process): agentic process / workflow engine + ADGM entity-setup demo

- **New Workflows surface.** Added `/workflows` (list of process definitions + live cases), `/workflows/new` (definition picker + JSON-schema-driven intake form with shareholders math), and `/workflows/[caseId]` (vertical stage timeline + structured outputs + inline approval CTA, polled every 4s while a case is active).
- **State-machine engine with a load-bearing invariant.** `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 and return an outcome envelope; every transition goes through `transitionTo`, which is transaction-wrapped and asserts the expected `from_stage_id` (stale transitions throw `PROCESS_TRANSITION_CONFLICT`). Stage executions are append-only once terminal; retries create new rows with `attempt += 1`.
- **5 stage kinds.** `intake_form` (form submitted via UI), `agent_run` (LLM-backed verifier with strict JSON validation + safe-default-fail on missing/unknown outcomes — closes the LLM trust gap on the KYC gate), `policy_gate` (declarative rule evaluator with a small expression dialect: `==`, `!=`, comparisons, `&&`, `||`, `IN`, parens, `prior_stage('slug').field`), `human_approval` (creates an approval, pauses the heartbeat task, advances on approve / fails + routes via `on_fail_stage_slug` on deny — denied approvals no longer orphan the heartbeat task), and `package` (renders a markdown bundle into an artifact; passport numbers are redacted on disk).
- **Heartbeat integration.** Engine-driven stages run as `type='process_stage'` heartbeat tasks; `lib/heartbeat-worker.js` dispatches to cached handlers. Stage handler resolution is now cached at module-load instead of dynamically importing on every dispatch.
- **ADGM Entity Setup demo seeded.** 5-stage flow: applicant intake → completeness verifier → KYC/risk verifier → compliance human approval → final package (cover sheet + advisory MOA draft). Three supporting agent templates seeded: `completeness-agent`, `kyc-risk-verifier`, `regulatory-drafter`. Demo mode synthesizes structured outputs so the full pipeline runs without an LLM round-trip.
- **API.** 7 routes under `/api/processes/**`: list/start cases, case detail, cancel, submit intake, retry stage, admin override, list/detail definitions. Server-side intake validation enforces a 256 KB payload cap, 10 KB per string, type sanity, and shareholders-sum-to-100; business validation stays with the completeness agent.
- **Audit + activity.** Every transition writes an `audit_logs` row (`action_target_type='process_case'`) and emits `process.case.created`, `process.stage.entered`, `process.stage.completed`, `process.case.complete` live events. Failures now log to stderr rather than silently dropping. New `processes` slice in `getState` powers the dashboard.
- **Tests.** Added `tests/process-engine.test.js` (9), `tests/process-adgm-demo.test.js` (4 — happy path, approval landing, denial routing, incomplete-intake rejection), `tests/process-policy-gate.test.js` (18 — tokenizer, evaluator, escape hatch), `tests/process-agent-run.test.js` (7 — completeness, kyc shape + determinism, regulatory-drafter, unknown-template fallback), `tests/process-definitions.test.js` (12 — validation error cases, atomic re-upsert, stage routing), `tests/process-api-routes.test.js` (18 — full HTTP layer with 200/201/400/404 paths).
- **Docs.** New `ai-docs/process-engine.md` deep reference (lifecycle, kinds, allowed transitions, idempotency, authoring guide); pointers added to `CLAUDE.md`, `ai-docs/api-surface.md`, `ai-docs/key-constraints.md`.
- **Validation.** Full `npm run check` passed with **692/692 tests** (+68 new) and a successful Next production build.

### refactor(projects): complete P4 component file splits

- **Split the project album into focused components.** `album-view.jsx` now orchestrates album state/actions while toolbar, breadcrumb, folder grid, item cards, migration banner, preview body, and shared album helpers live in focused modules under `app/projects/[slug]/`.
- **Split the share panel.** Share scope selection, share-link management, and access-log rendering now live in separate components. The panel keeps using cached share data from `ProjectAlbumProvider`.
- **Moved share-panel inline styles into `globals.css`.** The share panel now renders with stable class names instead of large inline style objects.
- **Added P4 regression coverage.** `tests/projects-p4-splits.test.js` asserts the focused modules exist, share-panel inline styles stay out, and project component files remain below the 500-line ceiling.
- **Validation.** Full `npm run check` passed with **624/624 tests passing** plus successful Next production build. Browser smoke on `/projects/personal` passed, including opening the album preview and share panel with no console errors.

### refactor(projects): complete P3 live library deltas

- **Added project live-delta flag.** `MADARX_PROJECTS_LIVE_DELTAS=1` now enables project detail pages to subscribe to `/api/events/stream`; the flag is default-off for soak.
- **Added project library resync endpoint.** `/api/projects/[slug]/library` returns the current project library items/folders so the client can resync only the Projects library slice instead of forcing a full Server Component refresh.
- **Expanded live event slices.** The shared state bus now accepts `state.disk.changed`, `state.acl.changed`, and `state.share_links.changed` in addition to existing artifact events. Disk folder/file mutations, artifact move/delete/protect, ACL updates, share-link create, and share-link revoke all publish live state changes.
- **Removed project mutation `router.refresh()` round-trips.** Album delete/move/protect patch local state immediately; uploads and folder creation call the targeted library resync endpoint. The provider keeps a 60s safety refresh and foreground visibility resync while live deltas are enabled.
- **Cleaned local build warnings.** Installed the Supabase SSR dependency tree, suppresses the local-auth production warning during `next build` while preserving the runtime warning, suppresses Node's experimental SQLite warning in `npm run build`, and deduped `.env.local` to one `MADARX_MASTER_KEY` entry on this machine.
- **Tests.** Added `tests/projects-p3-live-deltas.test.js` for the feature flag, event fan-out, disk mutation emits, SSE provider wiring, refresh removal, and library endpoint invariants. Full `npm run check` — **621/621 tests passing** plus successful Next production build.

### fix(dashboard): eliminate space + project flicker on first paint

- **Server is now the single source of truth for artifact → space binding.** `lib/runtime.js::recordArtifact` defaults `space_id` to `'default'` when no explicit value and no agent default is set, instead of writing NULL. `lib/dashboard-spaces.js::ensureSeed` runs an idempotent backfill (`UPDATE artifacts SET space_id = 'default' WHERE space_id IS NULL`) on first DB open and on every spaces-list read. `lib/seeds.js` invokes `ensureSeed` from `seedDefaults` so the backfill is not gated on first dashboard fetch.
- **Removed the client-side orphan reconciler.** `app/components/dashboard/use-spaces.js` no longer tracks `artifactIds` per space and no longer reassigns "orphan" artifacts into the active space on hydrate. The hook now owns space metadata only (`id`, `name`, `is_active`). Per-space membership is derived in the caller from `artifact.space_id`. The drag/drop `moveArtifactToSpace` path now does an optimistic local patch through a passed-in callback, persists via `/move`, then triggers a refresh.
- **`app/components/dashboard/use-dashboard-state.js` derives `activeSpaceArtifactIds`, `visibleArtifacts`, and `dockedArtifacts` directly from `(a.space_id || 'default') === activeSpaceId`.** Legacy NULL rows collapse onto `default` so the UI is never empty during the brief window between mount and backfill. `artifact-dock.jsx` and `voice-panel.jsx` count per-space cards from the same derivation rather than from a separate `space.artifactIds` array.
- **Project filter applied on the very first poll.** `selectedProjectId` now hydrates synchronously from `localStorage` (`madarx:selected-project`), defaulting to the canonical `'personal'` slug. `lib/runtime.js::getState` resolves slug → id server-side so the slug works before `state.projects` has hydrated client-side. The auto-select effect upgrades the slug to the real project id once `state.projects` arrives. Without this, the dashboard fetched `/api/state` unfiltered on mount and then refetched with `?project_id=personal`, briefly showing artifacts from other projects in `main` before the filter narrowed the list.
- **Tests.** Updated `tests/agent-default-space.test.js` to assert the new `default` fallback contract (was: NULL). Added `tests/dashboard-spaces-backfill.test.js` covering the NULL → `default` backfill, default-space appearance in `listSpacesWithArtifactIds`, and the `recordArtifact` default. Full `npm run check` — **614/614 tests passing** plus successful Next production build.

### refactor(projects): complete P1/P2 modularity + bundle split

- **Completed P1 state extraction** by moving album filtering/folder/search/selection state into `use-album-state.js` and share ACL/link/audit loading into `use-share-data.js`.
- **Completed P2 bundle split** by dynamically loading the floating artifact window, share panel, privacy modals, and markdown renderer instead of keeping those heavier modules in the initial album bundle.
- **Kept privacy flow centralized** in `ProjectAlbumProvider`, removing page-local event coupling while preserving setup/unlock behavior across header actions and album cards.
- **Added P1/P2 regression coverage** for provider wiring, event-coupling removal, hook extraction, dynamic imports, and keeping `react-markdown` out of `album-view.jsx`.

Validation: `node --no-warnings --test tests/projects-p1-context.test.js tests/projects-p1-event-coupling.test.js tests/projects-p1-p2-completion.test.js` passed; `npm run check` passed with 611/611 tests and a successful Next build; browser smoke on `/projects/personal` passed.

### refactor(projects): P0 security + correctness checkpoint

- **Hardened HTML artifact previews.** Project artifact preview iframes no longer include `allow-same-origin`, preserving script execution for previews while preventing same-origin access to parent/session storage privacy state.
- **Fixed share URLs.** Share-panel links now use the signed `link.token` value instead of the internal share-link row ID, and the UI avoids rendering unusable `/share/null` URLs for legacy rows without a token.
- **Protected share-link token listing.** `GET /api/artifacts/[id]/share-link` now performs a fresh membership check before listing bearer tokens and requires `share` access. `canShare()` now requires member+ role plus artifact readability, preventing users from listing tokens for private artifacts they cannot read.
- **Gated artifact audit search.** `/api/audit/search` now requires artifact-targeted searches to provide both `action_target_type=artifact` and `action_target_id`, checks artifact read access before search, preserves auth error statuses, and avoids mixing non-target-aware fallback sources into target-scoped results.
- **Cleaned stale closure hazards.** Project drop-zone upload guarding now reads live busy state, privacy hydration has explicit hook dependencies, header action event listeners list dependencies, and protect toggles refresh/error-report after mutation.
- **Tests added.** New regression coverage for share-link listing authorization, `canShare` authorization, audit route authz ordering, target-scoped audit filtering, and share-token persistence/listing. Verified with targeted Projects P0 tests (**32/32 passing**) and full `npm run check` (**605/605 passing** plus successful Next build).

### refactor(dashboard): Phase 5 — memoization + JSX (dashboard refactor complete)

- **`app/page.js`: `createElement(...)` → JSX.** The shell, the chat-restore bubble, and the four overlays (`ConfigPanel`, `TrustModeConfirmModal`, `VoicePanel`) all render through JSX now. Behavior is byte-identical to the prior `createElement` form; this is purely a readability + tooling win.
- **`React.memo` on the four heavy children.** `ChatPanel`, `CanvasSection`, `ConfigPanel`, and `VoicePanel` now `export default memo(...)`. The win is realized once the provider also stops re-rendering on the keystroke path (see below).
- **Composer prompt lifted into `ChatPanel` local state.** `useDraftPersist()` no longer runs inside `useMainAgentStream` → `useDashboardState`. It runs in `ChatPanel` itself. The provider therefore does not re-run on every keystroke. `useStreamCommand.command(text)` now requires an explicit `text` argument; `clearSavedDraft` is imported directly from `use-draft-persist.js` instead of being threaded through three hooks. `chat-panel.jsx` owns a stable `send()` callback (`useCallback`) that reads its own prompt, clears it, and calls `mainAgent.command(text)`. Same draft-restore semantics, same localStorage key — the hook just moved one level down the tree.
- **`useMemo` on derived arrays in `use-dashboard-state.js`.** `artifacts`, `visibleArtifacts`, `dockedArtifacts`, `frontArtifactId`, `activeSpaceIdSet`, `approvalRun`, `activeHermesJobs`, and `zForArtifact` are now memoized against their real dependencies. Visible/docked filtering merged into a single pass per side.
- **`useMemo` on `grouped` + `useCallback` on `pickModel` in `use-model-picker.js`.**
- **Acceptance — typing in the composer triggers zero `CanvasSection` renders.** Realized by the architectural change above: the keystroke path does not cross the provider boundary, so context-consumer children stay idle. Build green, all 582 tests pass.
- **Spec closed.** `specs/active/dashboard-refactor.md` moved to `specs/archive/`. Phases P0–P5 all shipped.

### refactor(dashboard): Phase 3 — SSE deltas replace 5s poll

- **Feature flag `MADARX_DASHBOARD_LIVE_DELTAS=1`, default off.** Spec P3 ships behind a soak flag for one week before flip. When off, the dashboard keeps the existing 5s `/api/state` poll. When on, the dashboard does one hydration request on mount, then subscribes to `/api/events/stream` and refetches `/api/state` on demand. `/api/state` now returns `live_deltas_enabled` so the client knows which mode it is in.
- **New event channel — `state.<slice>.changed`.** `lib/live-events.js` exports `publishStateChanged(slice)`. Slices that fan out: `agents`, `runs`, `artifacts`, `heartbeat`, `approvals`, `connectors`, `schedules`, `sessions`, `local_actions`, `local_processes`, `hermes_jobs`, `projects`. Unknown slice names are dropped.
- **Auto-fan-out from `appendAudit`.** Every audit-log row whose `type` maps to a known slice (`agent.*`, `run`, `artifact*`, `approval*`, `heartbeat.*` / `cron.*` / `schedule.*`, `connector*`, `local.process*`, `local.tool*` / `local.terminal*`, `hermes*`) now publishes a `state.<slice>.changed` event. Direct emits added at non-audited mutation sites: `lib/heartbeat-queue.js` (claim/lease/complete/fail/approval/recover), `lib/approvals.js` (create/decide/consume), `lib/missions.js` (insert + completion).
- **Client subscription debounced + tab-resync.** `app/components/dashboard/use-dashboard-state.js` opens an `EventSource`, coalesces bursts of `state.*` events with a 250 ms debounce, runs a 60 s safety-net refresh, and force-refetches once on `visibilitychange → visible`. The 5 s `setInterval` is dropped when SSE is on.
- **Seed-time emits suppressed.** `lib/seeds.js` toggles `globalThis.__madarxSuppressStateEmits` around the seed block so first-boot audits (Caretaker creation, schedule upsert, allowlist seed) do not generate SSE noise. The dashboard reads the post-seed snapshot from `/api/state` on mount.
- **Tests:** `tests/dashboard-p3-live-deltas.test.js` (13 tests) covers the helper, audit-type mapping, mutation-site emits, and feature-flag env parsing.

### refactor(dashboard): Phase 1 — context + hook consolidation

- **`app/page.js` shrunk from 653 → 57 lines.** All 19 `useState` hooks and 12 handler functions moved into a React context + four leaf hooks. Children (`ChatPanel`, `CanvasSection`, `ConfigPanel`, `VoicePanel`) now consume `useDashboard()` instead of receiving 28–50-prop bags each. Zero behavior, visual, or API surface changes.
- **New files:** `app/components/dashboard/dashboard-context.jsx` (provider + `useDashboard` hook), `use-dashboard-state.js` (top-level composer), `use-config-panel.js`, `use-trust-mode.js`, `use-model-picker.js`.
- **All three P0 fixes preserved verbatim** — `onArtifactMessage` origin/WeakSet guard, `projectId` threaded into `useMainAgentStream`, ref-based stale-closure pattern in the mount-only poll effect. P0 mount-effect refs now extended to cover `configPanel`/`trustMode`/`modelPicker` hook returns so the effect's `[]` deps remain legitimate without `eslint-disable`.
- **Out of P1 scope (per spec, deferred to later phases):** JSX conversion (P5), `React.memo` / `useMemo` / `useCallback` (P5), `next/dynamic` lazy imports (P2), SSE-driven state with poll removal (P3), file splits >500 lines (P4). Spreading leaf-hook objects into one context value means every leaf-hook state change re-renders all consumers — addressed when memoization lands in P5.

### fix(dashboard): block iframe postMessage escape; thread projectId to orchestrator; fix stale poll URL after project switch

- **Security fix — `onArtifactMessage` origin/source guard.** The dashboard's `madarx:prompt` bridge accepted `postMessage` from any source with no origin check, allowing a hostile or compromised artifact iframe to drive the main agent. The handler now rejects messages unless `ev.origin === window.location.origin` AND `ev.source` is in a `WeakSet` of trusted artifact iframe windows. Iframes register via a React `ref` callback at mount — synchronously, BEFORE any srcdoc inline script can call `postMessage` — so registration cannot race with early-fire messages.
- **Bug fix — `projectId` threaded into `useMainAgentStream`.** Previously `useMainAgentStream` was called without `projectId`, so orchestrator commands always defaulted to `'personal'` even after the user switched projects in the dropdown. The page now passes `projectId: selectedProjectId` so the active project follows the dropdown.
- **Bug fix — stale `statePath` closure in the 5s poll effect.** The poll effect captured the initial `selectedProjectId` via `[]` deps, so after a project switch the poll still hit `/api/state` without the new `project_id` (or with the previous one). Latest-value `useRef`s for `selectedProjectId`, `mainAgent`, `sessions`, and `openConfig` are now synced in the render body; the poll effect reads through the refs and stays mount-only. Same pattern applied to `onArtifactMessage` so it always sees the current `mainAgent.busy` / `mainAgent.command`.
- **Cleanup — `eslint-disable react-hooks/exhaustive-deps` removed.** Both suppressions in `app/page.js` are gone now that closures are read via refs.
- **No change to `/api/events/stream`.** Confirmed it already enforces `requireSession` (no auth regression).

### fix(scheduler): backfill missed schedule windows on wake; auto-start cron+heartbeat in-process; add catchup_policy column

- **Bug fix — `markScheduleRan` wallclock skip:** `markScheduleRan` now accepts an optional `from` argument (defaults to `now`). The backfill loop passes the last missed window as `from`, so `next_run_at` advances from the actual last window rather than skipping ahead from `now`.
- **Bug fix — single-pass enqueue:** `runDueSchedules` replaced with a backfill loop that collects all missed windows (up to `MADARX_BACKFILL_MAX_WINDOWS`, default 10) and enqueues a deterministic `heartbeat_task` per window. Tasks use `INSERT OR IGNORE` so re-running never double-queues.
- **Bug fix — cron daemon never auto-starts:** `lib/db.js::getDb()` now auto-starts both the cron and heartbeat worker as in-process `setInterval` loops. Set `MADARX_DISABLE_INPROC_DAEMON=1` to opt out (for deployments using separate daemon processes).
- **New: `catchup_policy` column on `schedules`:** Controls missed-window behavior per schedule. Values: `fire_all` (default for daily/weekly/monthly), `fire_once` (default for `every Nm/Nh`), `skip`. `upsertSchedule` infers the default from the schedule string. Migration is idempotent (`ALTER TABLE … DEFAULT 'fire_all'` handles existing rows).
- **New: `cron.schedule.backfilled` audit row:** Written whenever more than one window was missed or any windows were skipped/truncated. Fields: `missed_count`, `fired_count`, `truncated_count`, `policy`, `oldest`, `newest`.
- **New: `cronDaemonTick()` shared tick body** exported from `lib/runtime.js`. Both the in-process auto-starter and `scripts/cron-daemon.mjs` call it. Includes wake-after-sleep recovery: if `Date.now() - lastTickAt > 2 × intervalMs`, calls `recoverStaleHeartbeatTasks()` before `runDueSchedules`.
- **New env vars:** `MADARX_BACKFILL_MAX_WINDOWS` (default 10), `MADARX_DISABLE_INPROC_DAEMON` (set to `1` to skip in-process auto-start), `HEARTBEAT_WORKER_INTERVAL_MS` (default 5000).

## 2026-05-17 — v2.1.0 Auth + Privacy Foundation

Full-stack security upgrade across four independently-shippable phases plus a standalone privacy module. Every API route is now gated with an identity check. Artifacts can be protected by passphrase, scoped to an org, shared via signed links, and encrypted on disk. A compliance-grade append-only audit trail and an admin console complete the package.

### Privacy module

- **Per-artifact passphrase protection.** The all-or-nothing blur toggle is replaced with an `is_protected` flag per artifact (`artifacts.is_protected`). The passphrase never leaves the browser — the server stores only a salt, KDF parameters, and an HMAC verification token.
- **5-attempt lockout with 15-minute timeout.** Failed unlock attempts are counted in `sessionStorage`; on the fifth failure within 5 minutes a 15-minute lockout begins. Lockout state and every attempt are written to `audit_logs`.
- **Web Worker for PBKDF2.** The PBKDF2-SHA256 derivation (600,000 iterations) runs in a dedicated Web Worker (`app/projects/[slug]/privacy.worker.js`) so the UI never freezes during key derivation.
- **Passphrase reset flow.** Clears `is_protected` on every artifact in the project and writes a `privacy.passphrase.reset` audit row with an `artifacts_cleared` count.
- **Audit trail.** Every protect, unprotect, unlock attempt (success/failure), passphrase set, passphrase change, and reset event appends an `audit_logs` row.
- **Modal entry points.** `privacy-passphrase-modal.jsx` (set/change passphrase) and `privacy-unlock-modal.jsx` (unlock session) are wired into the album toolbar lock chip and double-click on locked cards.

### Multi-tenant auth (Phase 1)

- **Organizations, users, memberships.** New tables `organizations`, `users`, `org_memberships` with role hierarchy: owner / admin / member / viewer. `teams` and `team_memberships` tables are stubbed for Phase 2 expansion.
- **Pluggable auth mode.** Set `MADARX_AUTH_MODE=local` for zero-config single-user pass-through (development default) or `MADARX_AUTH_MODE=supabase` for federated SSO via Supabase Auth (production required).
- **All API routes now gated.** `lib/auth.js::requireSession(req)` is called at the top of every route handler. Local mode returns a synthetic owner session; Supabase mode validates the JWT.
- **Login page.** `app/login/page.js` renders email/password and magic-link flows. Only shown when `MADARX_AUTH_MODE=supabase`.
- **Auth callback route.** `app/api/auth/callback/route.js` handles Supabase OAuth / magic-link code exchange.
- **First-boot migration.** `scripts/migrate-auth-v1.mjs` seeds the default org, assigns the `personal` project to it, and creates the synthetic owner user row. Idempotent; runs automatically on first boot via `lib/seeds.js`.

### Artifact ACLs and sharing (Phase 2)

- **Four sharing scopes.** Each artifact has a `sharing_scope` column: `private` (default) / `team` / `org` / `link`. A per-artifact `artifact_acls` row holds the resolved scope and optional `team_id`.
- **Signed share links.** `artifact_share_links` table; tokens are HMAC-SHA256 signed with `MADARX_SHARE_LINK_SECRET`. Each link supports expiry (`expires_at`), a view cap (`max_accesses`), and explicit revocation (`revoked=1`).
- **Per-org email-OTP gate for share links.** Recipients enter their email address; an OTP is delivered and verified before the artifact is streamed. Toggle per org via `require_share_email_verification`. OTPs are posted in the request body, never in the URL.
- **Public share viewer.** `app/share/[token]/page.js` renders a read-only artifact view with an `otp-gate.jsx` interstitial when the org requires email verification.
- **Compliance audit trail.** `lib/compliance-audit.js` writes append-only rows on every share link create, access, revocation, and OTP event. SQLite triggers block UPDATE and DELETE on `audit_logs` at the database layer.
- **Bulk revocation.** Admin console lets owners revoke all share links for an artifact or all links across an org in one action.

### At-rest encryption (Phase 3)

- **Per-org HKDF-derived DEK.** Each organization's artifact blobs are encrypted with a key derived via HKDF from the master key and the org ID. The DEK is never stored — derived on demand in `lib/artifact-crypto.js::deriveOrgDek`.
- **AES-256-GCM with random IV per write.** Disk format: `[12-byte IV][16-byte auth tag][ciphertext]`. New `artifacts.encrypted` and `artifacts.encryption_key_id` columns track per-artifact state. Existing plaintext artifacts continue to work; the column is the per-artifact gate.
- **Boot-time disk-encryption health check.** `scripts/check-disk-encryption.mjs` detects FileVault / LUKS / BitLocker state, writes an `audit_logs` row (`type: disk_encryption.health`), and emits a stderr warning in `NODE_ENV=production` when encryption is off.
- **Toggle via env var.** `MADARX_ARTIFACT_ENCRYPTION=1` enables encryption for new writes. Default `0` preserves the existing plaintext behavior.

### Admin console (Phase 4)

- **`/admin` route.** Org settings, member management (invite, role change, remove), and an audit log viewer with column-level filtering.
- **CSV and NDJSON audit export.** `GET /api/admin/audit-export` streams the filtered audit log in either format; gated to owner/admin.
- **Bulk share-link revocation.** `DELETE /api/admin/share-links` accepts an array of link IDs or a wildcard `artifact_id`.
- **Service-principal propagation.** Agent runs now carry `acting_on_behalf_of_user_id` threaded from `schedules.created_by_user_id` through heartbeat tasks to `lib/agent-runner.js` and every `audit_logs` row the run writes.

### Hybrid membership freshness (RD-1)

- **Read paths cache for 5 minutes.** `lib/authz.js` caches the last-known membership result to avoid a Supabase round-trip on every request.
- **Sensitive paths call Supabase live.** Sharing, downloads, admin, and `irreversible_external` tool calls bypass the cache and verify membership in real time.
- **Revoked members detected within seconds** on any protected action, regardless of cache state.

### Security hardening

- **Append-only audit log.** SQLite triggers (`BEFORE UPDATE` and `BEFORE DELETE` on `audit_logs`) raise an error at the database layer, making the audit trail tamper-evident without application-layer trust.
- **Path-traversal guard.** Share-link resolution in `app/api/share/[token]/route.js` validates the resolved artifact path stays inside the project artifacts directory before streaming.
- **OTP codes in request body.** The email-OTP verification endpoint (`POST /api/share/[token]/verify`) reads the code from `req.body`, not from query params, to prevent accidental logging in access logs or browser history.
- **Production-mode warning.** Starting the server with `MADARX_AUTH_MODE=local` in `NODE_ENV=production` emits a console warning. `local` mode is intended for development only.

### New env vars

| Variable | Default | Description |
|----------|---------|-------------|
| `MADARX_AUTH_MODE` | `local` | `local` (dev pass-through) or `supabase` (production) |
| `MADARX_SHARE_LINK_SECRET` | — | 32+ random bytes; required when creating share links |
| `MADARX_ARTIFACT_ENCRYPTION` | `0` | Set to `1` to encrypt new artifact writes |
| `MADARX_MEMBERSHIP_LIVE_CHECK` | `on` (supabase mode) | Set to `off` only for testing; never in production |
| `MADARX_OWNER_EMAIL` | — | Local-mode synthetic owner email |
| `NEXT_PUBLIC_SUPABASE_URL` | — | Supabase project URL (supabase mode) |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | — | Supabase anon key (supabase mode) |
| `SUPABASE_SERVICE_ROLE_KEY` | — | Supabase service role key (supabase mode, server-only) |

### New command

```bash
npm run check:disk-enc   # boot-time disk-encryption health check; writes audit row
```

### Verified

- New test files: `tests/privacy-module.test.js`, `tests/privacy-crypto.test.js`, `tests/privacy-no-audit-route.test.js`, `tests/auth-local-mode.test.js`, `tests/artifact-acl.test.js`, `tests/artifact-crypto.test.js`, `tests/audit-append-only.test.js`, `tests/service-principal.test.js`, `tests/share-link-otp.test.js`. Full suite: **499/499 passing**.
- `npm run build` green.

### Out of scope (this entry)

- SAML 2.0 / ADFS federation (Supabase OIDC covers enterprise SSO; SAML parsing is not implemented).
- FIPS 140-2 validated hardware security modules.
- FedRAMP Authorization to Operate — the controls here support a future ATO; the authorization program is a separate engagement.
- Fine-grained field-level column encryption (artifact blobs only; `artifacts.content` and `artifacts.source_md` remain plaintext).
- Team-scoped sharing beyond stub tables (teams are created but not populated in Phase 1).

---

## Unreleased — Voice main-agent tool parity (2026-05-17)

### Changed
- **Voice IS the main agent.** `lib/voice/tools.js` no longer maintains a curated 12-tool whitelist for the Realtime model. `getVoiceToolsForRealtime()` and `getVoiceToolCatalog()` now derive their output from `getTools()` (the same function the typed dashboard agent uses), so the Realtime session sees every `CORE_TOOLS` entry plus every enabled marketplace tool. Schema is converted in one place from Anthropic-style `input_schema` to OpenAI Realtime `parameters`; everything else (trust mode, approval gating, allowlists, rate limits, audit, egress) is unchanged. Voice gets no privileged path — `executeVoiceTool` still routes through `executeTool` → `executeRegistryTool` exactly as before.
- **Voice now exposes `generate_ui` by default.** The Realtime model picks `generate_ui` for any request the operator expects to look designed (list pages, briefs, snapshots, restaurant lists, reports, presentations) and `create_artifact` (markdown) for plain notes. Closes the regression where voice-created HTML artifacts shipped as unstyled fragments (serif headings, plain bullets) because the Realtime model produced bare HTML without inheriting the brand system.

### Why
- The Realtime model was previously asked to inline the brand tokens into a hand-written HTML fragment. It can't reliably reproduce the full design system that `generate_ui` applies via Claude + `DESIGN_RULES` + `brandInstructionText`. Result: voice artifacts looked basic; typed-chat artifacts looked branded. Same agent, different output. This fixes that gap.

### Verified
- Smoke test: `getVoiceToolCatalog()` returns 38 tools (was 12), including `generate_ui`, `create_agent`, `whatsapp_send`, marketplace tools. `send_to_hermes` correctly hidden when Hermes is unavailable on the host (same filter the typed agent uses).
- All voice tool calls continue to flow through `executeTool`; `irreversible_external` tools called from voice still return `approval_required` rather than auto-executing.

### Docs
- `lib/voice/context.js` system prompt rewritten: instructs the Realtime model to route visual outputs to `generate_ui`, written notes to `create_artifact` (markdown).

---

## Unreleased — Voice (Jarvis) console: providers, context, tool surface, UI

### Added
- **Provider abstraction (`lib/voice/providers.js`).** Single source of truth for which voice lanes are usable on this install: `realtime.openai`, `tts.{openai,elevenlabs,local}`, `stt.{openai,elevenlabs,local}`, `llm.{anthropic,openai}`. Defaults pick the best available mode (`realtime` → `tts` → `local` → `off`) and respect `MADARX_VOICE_DEFAULT_PROVIDER`. Pure: no network, no DB writes.
- **Context builder (`lib/voice/context.js`).** `buildVoiceContext({ project_id, agent_id })` produces the system-style instruction block the Realtime model (and Anthropic fallback) receive. Includes today's ISO date, active project, brand voice, open tasks, recent artifacts, pending approvals, durable memory rows (`local_only` redacted), available tool names, and an explicit no-secrets-aloud rule. Capped at 8000 chars.
- **Curated voice tool surface (`lib/voice/tools.js`).** `create_artifact`, `update_artifact`, `delete_artifact`, `find_artifacts`, `get_state`, `web_search`, `web_fetch`, `voice_speak`. `executeVoiceTool` always routes through `executeTool` → `executeRegistryTool`, so trust-mode, approval gating, allowlists, rate limits, egress logging, and audit rows are preserved exactly as for typed chat. Voice never gets a special trust level.
- **`delete_artifact` orchestrator tool.** Added to `lib/orchestrator-tools.js` so voice and typed chat can both remove artifacts through one well-gated entrypoint.
- **API surface.** `GET /api/voice/providers`, `GET /api/voice/realtime/context`, `GET /api/voice/realtime/tools`. Session mint route now accepts `project_id`/`agent_id` and hydrates instructions + tools from the context builder unless the caller overrides. Tool-result relay accepts `project_id` so artifact tools land in the right scope.
- **Jarvis UI (`app/components/dashboard/voice-panel.jsx` + `voice-orb.jsx`).** Polished overlay opened from the chat panel mic button. Madarx-branded — Onyx surface, Bone/Graphite text, Mars Red reserved for active CTA + speech peaks + approval banners. Modes auto-hide based on configured providers. Persists mode + TTS choices in `localStorage`.
- **WebRTC hook (`app/components/dashboard/use-realtime-voice.js`).** Owns RTCPeerConnection, mic stream, data channel, audio-level meter for the orb, and the function-call dispatch loop. Transcript array is capped to prevent unbounded growth.
- **Operator docs + env names.** `docs/voice/jarvis.md` covers the provider matrix, BYO-keys setup, what the agent knows about Madarx context, the curated tool catalog, every preserved security gate, the API surface, and a troubleshooting table. `.env.example` now lists the safe voice variable names as comments only.

### Verified
- 3 new test files: `tests/voice-providers.test.js` (6), `tests/voice-context.test.js` (4), `tests/voice-tools.test.js` (5). Pass: 15/15.
- Full suite: **396/396 passing**. `npm run build` green; new routes appear in the route manifest.
- Security invariants confirmed by tests: no API keys in instruction blocks; `local_only` memory redacted before leaving the machine; irreversible_external tools (e.g. `gh_pr_merge`) called from voice return `approval_required` and never auto-execute.

### Out of scope (this entry)

- Per-agent voice personalities. The voice panel always speaks as the Main Agent today.
- Local TTS / STT server adapters. We provide the URL slots and capability detection; the URLs are passed through to the browser, but a local-lane WebRTC bridge is a follow-up.
- Streaming partial TTS playback in the typed-chat lane (today's `tts` mode synthesizes one final reply).

### Docs
- `README.md` gains a "Voice (Jarvis)" section.
- `CLAUDE.md` voice-tools constraint expanded to cover providers, fallback order, curated tool exposure, and the new `delete_artifact` tool.
- `specs/active/open-design-voice-realtime.md` Delta section captures the Jarvis pass.

---

## Unreleased — Tasks workspace, project-scoped artifacts, flight_search free fallback

### Added
- **Tasks workspace.** Reworked `/tasks` into a multi-pane workspace (`app/tasks/tasks-workspace.jsx`) with grouped board, watchers, recurrence chips, and an expanded `new-task-modal.jsx` (all 8 task fields: title, description, assignee, watchers, due_at, priority, status, recurrence). The modal now lifts `assignee` to state, disables `in_progress` when no assignee is set, auto-coerces back to `todo` if assignee is cleared mid-edit, and wraps `createAction` in try/finally so the modal can't get stuck on adapter failures.
- **Task checklists.** `lib/delegation.js` exposes `listTaskChecklist`, `addChecklistItem`, `updateChecklistItem`, `deleteChecklistItem`; backed by a new `task_checklist_items` table in `lib/schema.js`. UI lives at `app/tasks/[id]/checklist-editor.jsx`; REST surface at `GET/POST /api/tasks/[id]/checklist`, `PATCH/DELETE /api/tasks/[id]/checklist/[itemId]`.
- **Task attachments.** `POST /api/tasks/[id]/attachments` links artifacts and disk files to a task. Inline list rendering on `/tasks/[id]`.
- **Task recurrence.** `lib/delegation.js` rollover supports `daily`, `every Nh`, `every Nm`, `weekly`, `monthly` specs. Unknown specs are no-ops (logged via `console.warn` instead of silent `catch {}`). The recurrence guard reads `task.recurred_from_task_id` post-update for clearer semantics.
- **Project-scoped artifacts.** `lib/artifact-paths.js` resolves artifact storage under `workspace/projects/<slug>/artifacts/` per project; `lib/artifact-migration.js` + `scripts/migrate-artifacts-to-projects.mjs` move legacy artifacts into their owning project on first run (idempotent, gated by `data/.artifacts-migrated-v1`). `lib/runtime.js::createArtifact` accepts a `project_id`; unknown ids resolve to the `personal` project for both the file path and the DB row (validated via `listProjects().find()` before write).
- **Project library view.** `app/projects/[slug]/library-view.jsx` renders disk-file previews inline (md, html, text up to 512 KiB, capped at 1200 char preview), with `render_kind` (html / markdown / text / binary) returned from `getProjectWorkspaceSummary` so the UI can pick the right renderer.
- **Project picker on agent creation.** `app/agents/new-agent-modal.tsx` and `/agents` page now pass the active project context through to agent creation. Single-project installs hide the picker; multi-project installs surface a `<select name="project_id">`.
- **`flight_search` dual-provider routing.** `lib/tools/adapters/google-flights.js` implements a key-less fallback that posts to Google's `batchexecute` RPC (`H4cFwe`) and curates offers into the same shape Amadeus returns. `lib/tools/adapters/travel.js::flightSearch` is now a router (Amadeus when both creds resolve, Google otherwise; honors `MADARX_FLIGHT_SEARCH_NO_FALLBACK=1`). `required_env` on the registry entry is now `[]` so the tool is always available. `lib/flight-cache.js` is a 15-minute LRU (cap 200, sha1-keyed) that bypasses `wrapFetch` and rate-limit accounting on hits (`source.cache_hit=true`). System prompt updated in `lib/agent-config.js` to drop the "if Amadeus configured" qualifier.

### Verified
- New tests: `tests/flight-search-fallback.test.js` (8 — encoder golden, parser+curator on canned `tests/fixtures/google-flights-dxb-lhr-2026-05-18.txt`, router selection, Google fallback, `NO_FALLBACK` error path, cache hit), `tests/artifact-project-scoping.test.js`. Full suite green: **381/381 passing**. `npm run build` green.

### Out of scope (this entry)
- Live smoke tests against real Google Flights / Amadeus (F9/F10 in the flight spec) — pending operator dogfood.
- Multi-city / open-jaw flight routing.

### Docs
- `CLAUDE.md` gains a bullet documenting the dual-provider flight_search behavior, the cache, and the `MADARX_FLIGHT_SEARCH_NO_FALLBACK` opt-out.

---

## Unreleased — Open Design corpus, voice (TTS + Scribe), Browser Harness activation

### Added
- **Open Design corpus (vendored).** `scripts/vendor-open-design.mjs` mirrors a pinned commit of `nexu-io/open-design` into `workspace/skills/open-design/` (craft notes, skills, design systems, design templates, prompt templates). `NOTICE.md` preserves Apache-2.0 attribution and enumerates per-resource LICENSE files; `INDEX.md` ships a machine-readable JSON catalog. Run `npm run vendor:open-design` to refresh.
- **Open Design domain layer.** `lib/open-design.js` exposes `listOpenDesignSkills`, `getOpenDesignSkill`, `listDesignSystems`, `listDesignTemplates`, `listCraftNotes`, `listPromptTemplates`, plus `listAllEntries({ kind, scenario, query })` and `readEntryContents()`.
- **Open Design marketplace surface.** `GET /api/marketplace/open-design?kind=&scenario=&q=&include_body=1` returns the vendored catalog. `lib/marketplace-seeds.js` seeds 12 curated Open Design skill items + 1 `bundle:open-design` (author `nexu-io`, Apache-2.0). Re-runs are idempotent.
- **Cross-engine corpus injection.** `lib/agent-open-design-skills.js` produces a short catalog block for non-Claude-Code engines (Anthropic / Hermes / Codex). Claude Code engines inherit the corpus natively via `--add-dir workspace`.
- **TTS layer.** `lib/tts.js` (`synthesizeSpeech`) supports `openai` (default, `tts-1`) and `elevenlabs`. Vault keys via `OPENAI_API_KEY` / `ELEVENLABS_API_KEY` (with `process.env` fallback). API keys are masked in errors and never recorded in audit rows. Egress logged through `wrapFetch`.
- **`voice_speak` marketplace tool.** Category `voice`, risk `low`, rate-limited 60/hour. Returns base64 audio; optionally writes a Disk artifact when called with `save_to_disk=true` and a `project_id`. Disabled by default.
- **`POST /api/voice/tts` route.** Thin wrapper around `lib/tts.js` that streams audio bytes back with the right content-type for direct UI playback. Independently rate-limited from the agent-facing tool path.
- **OpenAI Realtime server bridge.** `lib/realtime-openai.js`, `POST /api/voice/realtime/session`, and `POST /api/voice/realtime/tool-result` mint short-lived OpenAI Realtime browser tokens and relay voice-side tool calls through `executeRegistryTool`, preserving rate limits, allowlists, and irreversible approval gates.
- **ElevenLabs Scribe transcription.** `lib/transcription.js` adds an `elevenlabsScribe` provider. Default provider unchanged (`openai` Whisper). When `provider: 'elevenlabs'` is requested without a key, falls back to Whisper with a warning. `local_only` audio is always blocked from cloud providers — gate enforced before any network call.
- **Voice schema.** New `voice_sessions` and `voice_tool_calls` tables (observation-only) under `CREATE TABLE IF NOT EXISTS` in `lib/schema.js`.
- **Browser Harness tool.** Disabled-by-default `browser_harness_run` Marketplace tool (preserved from the prior Unreleased entry).
- **cli-printing-press decision record.** `docs/integrations/cli-printing-press.md` captures the deferral rationale + promotion criteria. An off-by-default companion skill lives at `workspace/skills/cli-printing-press-companion/SKILL.md` (flag: `OPEN_PRINTING_PRESS_COMPANION=1`).

### Verified
- New tests: `tests/open-design-loader.test.js` (7), `tests/tts.test.js` (7), `tests/transcribe-elevenlabs.test.js` (4), `tests/voice-realtime.test.js` (2). All pass.
- `tests/marketplace-listing.test.js` updated for the new tool/skill/bundle counts; full suite green for that file.
- Browser Harness tool registry test untouched; preservation contract upheld.

### Out of scope (this entry)
- Full browser WebRTC dashboard voice UI / push-to-talk panel (server-side Realtime session + tool relay are in place; UI lands next).
- ElevenLabs Conversational AI signed-URL path (stretch).
- TTS playback hook in the dashboard chat panel (lands with the Realtime UI track).

## 2026-05-15 — `ee0f1a3` wip: connections subsystem + agent UX polish

Multi-thread WIP pushed to `main` as a single commit (38 files, +3414/-199). Not a PR — captures three concurrent threads to lock the work into git history.

### Added
- **Marketplace connections layer.** Connection-kind Marketplace items and supporting domain code (`lib/connections.js`, `lib/connection-status.js`, `lib/connection-seeds.js`) for services that need setup before agents can use them.
- **WhatsApp Marketplace connector.** Configurable Marketplace connection with QR-pairing setup copy and a deep link back to `/whatsapp` operations.
- **Hermes Agent Marketplace connector.** Configurable connection with local checkout/path detection; `HERMES_AGENT_DIR` now stored in the local secrets vault.
- **API-key setup flows.** Guided setup metadata and verification support for Anthropic, OpenAI, and Kimi-style providers.
- **Connection setup UI components.** `app/marketplace/connection-card.jsx`, `connection-setup-modal.jsx`, and step components for API key, QR pairing, and local binary/path detection. `ConnectionCard` owns its modal state via `'use client'` — closes the "Configure didn't open the modal" QA finding from the WIP-prep pass.
- **Connection verification API.** `/api/connections/[service]/verify` plus connection disconnect/status helpers so the Marketplace can verify credentials and mark services configured without echoing raw secrets.
- **Agent UX polish.** New `app/agents/schedule-picker.jsx`, rewritten `new-agent-modal.tsx` (+187 lines), `tool-picker.jsx` tweaks, and a chat-panel icon refresh (Conversations / Minimize / Settings switched to cleaner SVGs).
- **Tests:** `tests/connections.test.js`, `tests/connection-verify.test.js`, `tests/schedule-parser.test.js`, plus updates to `tests/agent-detail-format-helpers.test.js` and `tests/marketplace-listing.test.js`.

### Changed
- Marketplace counts and filters now include connection and agent kinds alongside tools, skills, and bundles.
- Marketplace setup copy positions credential entry as a guided activation flow instead of a raw env-var task.
- `lib/hermes.js` resolves `HERMES_AGENT_DIR` from the local secrets vault as well as process env and passes the resolved path through to Hermes execution.
- `lib/schedule.js` parser hardened and now covered by `tests/schedule-parser.test.js`.

### Fixed
- **Sidebar nav transition.** Removed `cursor: progress` (macOS spinning beach-ball) and the small spinning red dot from the pending-navigation state in `app/globals.css:711`. The immediate active-style swap is enough responsive feedback; the prior visuals made the wait feel frozen.

### Verified
- `npm run check` — green: **317/317 tests passing** and a clean Next.js production build.
- Working tree clean after the commit.

### Cleanup
- Deleted 3 stale local branches whose PRs were already merged: `feat/simplify-agents-ux`, `feature/main-agent-trust-mode`, `feature/smart-search-shortcuts`.
- Moved 3 shipped specs from `specs/active/` → `specs/done/`: `simplify-agents-ux.md`, `design-system-unification.md`, `oss-readiness-and-console-completeness.md`.

---

## [Unreleased] — 2026-05-15 (Open-source readiness + admin console completeness)

### Added
- **MIT `LICENSE`** at the repo root. Project is now formally MIT-licensed; `package.json` carries the same.
- **`CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`.** Workflow, vulnerability reporting (`ak@madarx.io`), and a Contributor Covenant 2.1 code of conduct.
- **GitHub Actions check workflow.** `.github/workflows/check.yml` runs `npm run check` on pull requests and pushes to `main` against Node 22.x. No deploy steps — this repo is local-first.
- **Route smoke script.** `scripts/smoke-routes.mjs` plus `npm run smoke:routes` pings every admin route (Overview, Projects, Agents, Tasks, Approvals, Disk, Missions, Audit, Schedules, Activity, Reports, Marketplace, Help, Settings, Runtime) and asserts HTTP 200 + an HTML body.
- **Help page Advanced views section.** Surfaces the previously-orphan routes — `/architecture`, `/brand`, `/federated-ai`, `/onboarding`, `/org`, `/runtime`, plus the legacy `/tools` alias — alongside the primary 12 sidebar routes with one-line descriptions.

### Changed
- **`package.json` is now OSS-publishable.** Removed `"private": true`. Added `license`, `author`, `repository`, `homepage`, and `bugs` fields. Added `smoke:routes` script.
- **`README.md` rewrite.** Replaced the hardcoded `/Users/kareemo/...` quick-start path with a clone-and-`cd` flow. Added Prerequisites (Node ≥ 22.5), Documentation links to `CLAUDE.md`, a self-host Deployment section that frames Vercel as one option among many, and a License footer.
- **Sidebar active state.** `app/components/ui/sidebar-with-submenu.jsx` now reads `usePathname()` and visually highlights the active nav item with `aria-current="page"`, `data-active`, and a restrained Mars Red active ring.
- **`.env.example` documentation.** Added commented vault-managed credential names with no real values and replaced machine-specific sample paths with generic `/path/to/...` placeholders.

### Verified
- `npm run check` — green: script syntax checks, full test suite, Next production build.
- Manual visual check: sidebar highlights the current route on every admin page.
- `npm run dev` + `npm run smoke:routes` — 15/15 routes pass.

### Notes
- Retroactive note: the design-system unification commit `b63c87e` (2026-05-15) and the non-dashboard styling pass `cc4e86d` (2026-05-15) ship under the V1 personal workforce unreleased block below; this is the explicit changelog cross-reference.

---

## [Unreleased] — 2026-05-15 (V1 personal workforce integrations + missions)

### Added
- **Google read integrations for the personal workforce.** Added Gmail and Google Calendar read adapters with shared Google auth handling, curated response shapes, secrets-vault credential lookup, and focused adapter tests.
- **Disk transcription pipeline.** Added transcription support with transcript metadata (`engine`, `language`, `duration_seconds`), OpenAI/local-path behavior, Disk transcript artifact persistence, and regression coverage for local-only audio boundaries.
- **Project-scoped missions and approvals.** Mission runs now enqueue project-scoped heartbeat work, heartbeat workers complete/fail linked mission rows, approval requests can be created/listed by `project_id`, and `/api/state` can return a project-filtered runtime snapshot.
- **Focused V1 regression tests.** Added Gmail, Calendar, transcription, and mission/project-filter test files to lock the new behavior.

### Changed
- Runtime state now includes project metadata and can scope agents, tasks, runs, audit/activity, schedules, artifacts, heartbeat tasks, and approvals to a selected project.
- Mission heartbeat payloads now carry explicit mission/task/agent/project context so workers can execute the correct installed agent and preserve audit lineage.

### Verified
- `node --no-warnings --test tests/gmail-adapter.test.js tests/calendar-adapter.test.js tests/transcription.test.js` — passing.
- `node --no-warnings --test tests/missions-project-filter.test.js` — passing.
- `npm run check` — green: script checks, 260/260 tests, and Next production build.

### Follow-up
- V1.0g final operator dogfood review remains before V1 closure.

---

## [Unreleased] — 2026-05-15 (Marketplace foundation — Phase 1 of 4)

### Added
- **Tools page reframed as a Marketplace.** `/tools` is now a single operator-facing
  surface — the Marketplace — for discovering and installing every kind of agent
  capability: **tools** (the existing 40 API adapters), **skills** (instruction
  packs / slash commands), and **bundles** (curated packs of both). All 40 tools
  toggle exactly as before, with the same approval-mode and risk-tier surfacing.
- **/marketplace route.** Server-rendered shell at `/marketplace` with kind tabs
  (All / Tools / Skills / Bundles — **no Agents tab**), category chips, a Cmd+K
  smart search that ranks slug/title prefixes above description matches, and a
  responsive row layout that scales from 1180px down to 760px. `/tools` redirects
  to `/marketplace?kind=tool` so existing bookmarks still work.
- **5 seeded marketplace items.** 4 skills (`design-shotgun`, `design-review`,
  `qa-only`, `cso-audit`) and 1 bundle (`gstack` by garrytan · MIT). Phase 1
  catalogs these so the UI shape is real on day one. Skill / bundle file
  installation lands in Phase 2.
- **Brand identity at `ai-docs/marketplace-brand.md`.** Wordmark, color tokens
  (`--mkt-tool-*`, `--mkt-skill-*`, `--mkt-bundle-*`, plus reserved
  `--mkt-agent-*` for the future Agent Marketplace), typography scale, and a
  primitive inventory. Tokens are declared in `:root` for DevTools introspection.
- **Marketplace primitives at `app/components/ui/marketplace/`.** `<KindChip>`,
  `<KindBadge>`, `<RiskDot>`, `<InstallToggle>` — all server-renderable.
  `<SkillIcon>` and `<BundleIcon>` added to `tool-icons.tsx` (no emoji).
- **REST surface at `/api/marketplace`.** `GET` lists with optional `?kind=`,
  `?category=`, `?q=`. `PUT /api/marketplace/[id]` toggles install — tool-kind
  proxies to `setToolEnabled`, skill-kind writes `marketplace_installations`,
  bundle-kind returns `{ status: 'deferred' }`. `kind=agent` rejected with 400
  as a Phase 1 boundary check.
- **Two new tables.** `marketplace_items` (id/kind/slug/title/description/
  category/author/source/version/manifest_json/risk_summary/tags) and
  `marketplace_installations` (item_id/installed/installed_at/settings_json).
  Tool-kind items continue to live in `tool_installations` — there is exactly
  one row per tool, in one place, no drift between stores.
- **Domain library `lib/marketplace.js`.** `listMarketplaceItems({ kind,
  category, query })`, `getItem`, `setItemInstalled`, `searchItems`,
  `getKindCounts`, `listCategoriesForKind`. Single write path for installs,
  dispatched on kind. Agent kind is intentionally rejected here too.

### Out of scope (Phase 1 boundary)
- **No agent personas.** Hermes, OpenClaw, gstack-ceo, etc. land in a separate
  Agent Marketplace product. The agent brand tokens are reserved in CSS so the
  two surfaces stay siblings.
- **No GitHub URL importer.** "Import from GitHub" opens an informational modal
  that audits the click. Real importer is Phase 2.
- **No bundle install.** Clicking install on the gstack bundle opens the same
  Phase 2 modal with a deeper "what install will do" explanation.

### Verified
- `npm test` — 24 new marketplace tests pass (14 listing + 10 API), 250 total
  in the suite. One pre-existing flaky Hermes ordering test is independent of
  this change (passes in isolation).
- `next build` — green. `/marketplace` route is 4.34 kB.
- Manual: `/marketplace` shows correct counts (40 tools, 4 skills, 1 bundle,
  45 total). Tab filters narrow correctly. Search "design" surfaces both design
  skills with slug-prefix ranking. `/tools` redirects to the tool tab.

---

## [Unreleased] — 2026-05-12 (agents UI distilled, engine→model coupling fixed)

### Changed
- **Agents list page rewritten as scannable rows.** Each agent is now a single row showing name, status pill, mission line clamped to 1 line, and a muted meta line of `engine · model · schedule · last-run`. Skills editor and Memory pruner are removed from the list page (they already live on `/agents/[id]`); secondary actions (Run / Engine / Delete) cluster on the right. Sidebar's New-agent form keeps only Name + Mission + Engine visible; Schedule, Tools, and Approval move behind an `<details>` "Advanced" disclosure. Hermes panel reduces to one line when installed; install steps move behind `<details>` when not.
- **Agent detail page restructured for a single workbench column.** Skills editor sits alone in the left column, sticky-positioned, sized to fill the visible viewport (`calc(100vh - 180px)`, capped at 86vh, min 360px) so it stays anchored while you read the right column. Everything else — At a glance, Tool permissions, Memory pruning, Runs history, Audit history, and Recent artifacts — stacks in the right column with the existing 14px gap, fixing the previous "Tool Permissions touched Memory Pruning" spacing bug. The inline engine-edit form is now collapsed behind a "Change engine" disclosure that opens a popover with the shared picker.

### Fixed
- **Engine → model dropdown now filters by selected engine.** Added a shared client island (`app/agents/engine-model-picker.jsx`) used on both the list page (inside a "Engine" disclosure per row) and detail page (in the "Change engine" popover) and the New-agent sidebar form. The model select shows only the chosen engine's models; for engines with no model list (Hermes, Codex) the model becomes a free-text input. Previously the model was a free-text `<input list="agent-model-options">` whose datalist was defined only in the sidebar and mashed all engines' models together, making the per-agent footer model field a void.

### Data
- All 9 Villa 515 agents switched to `engine=claude-code`, `model=sonnet` via SQL UPDATE.

### Verified
- `npm test` — 68/68 passing.
- `npm run build` — green.
- `/agents` and `/agents/villa515-main-orchestrator` return 200; rendered HTML inspection confirms (a) 9 distinct `.agent-row` blocks on the list, (b) the model select per row contains only the active engine's models (`sonnet` for claude-code), (c) all five right-column panels stack with `gap:14px` on the detail page.

---

## [Unreleased] — 2026-05-12 (Hermes stuck-job auto-sweep)

### Fixed
- **Stale Hermes jobs are now swept automatically.** `markStaleHermesJobs()` already existed but was never called outside of tests, so any Hermes job that died mid-execution stayed at `status='running'` forever and lit up the dashboard's Hermes badge with phantom activity. Wired the sweep into the heartbeat worker's pre-claim cycle next to the existing `recoverStaleHeartbeatTasks()`. After one worker pass the dashboard toolbar correctly shows zero running Hermes jobs; a stuck "Brix CRM" row from two days prior was retired to `status='stale'` during verification.

---

## [Unreleased] — 2026-05-12 (scheduled outputs render as branded HTML)

### Changed
- **Scheduled agent reports are now branded HTML documents.** `runAgentTick`, `competitorReport`, and `marketingPlan` now run their markdown output through the same `stripEmoji` + `renderDocumentHtml` pipeline the chat composer uses for `create_artifact`. Each artifact record now stores `kind: 'document'`, the full styled HTML in `content`, and the raw markdown in `source_md` (so download stays intact). The dashboard canvas already renders `kind === 'document'` in an iframe with brand colors, logo, fonts, and design-style tokens — scheduled reports now look identical to chat-created artifacts instead of raw markdown text. The on-disk `.md` files under `workspace/reports/` are preserved unchanged.

### Verified
- `npm test` — 68/68 passing.
- Smoke run of `competitorReport()` produced an artifact with `kind=document`, 8977-byte HTML body starting with `<!DOCTYPE html>`, and the raw 1940-byte markdown preserved in `source_md`.

---

## [Unreleased] — 2026-05-12 (operator command center hardening)

### Added
- **Operator attention snapshots for tasks.** Added `getTaskLastRun`, `getTaskLastFailure`, and `getTaskOperatorSnapshot` in `lib/delegation.js` so task surfaces can show active run state, last failure reason, checkout/execution locks, and a single `needs_action` signal.
- **Task list/detail action surfacing.** `/tasks` now highlights blocked, approval-waiting, or failed tasks with a “needs action” badge, last failure text, run state, and checkout metadata. `/tasks/[id]` now has an operator alert panel plus last-failure/execution metadata next to the existing checkout release control.
- **Operator Command Center quick links.** The dashboard runtime popover is now labeled as an Operator Command Center and links directly to Approvals, Tasks, Activity, and Runtime so an operator can jump from live signals to detail pages.
- **Villa 515 delegated-work demo reset.** `scripts/setup-villa515-poc.mjs` now also cleans/reseeds deterministic `villa515-poc` demo tasks: a root owner briefing, a completed competitor child task, a WhatsApp approval-waiting run, and a blocked/failed image-provider task. The script emits the seeded demo task/run IDs and exits cleanly after printing JSON.

### Tests
- Added coverage for operator snapshots surfacing approval waits, last failures, and released checkout metadata.

### Verified
- Targeted: `node --check scripts/setup-villa515-poc.mjs` and `node --test tests/delegation-and-activity.test.js` — passing.
- Temp-data POC reset smoke — created 9 agents, 4 demo tasks, one approval-waiting run, and one failed run without touching the local project DB.
- `git diff --check` and `npm run check` — 68/68 tests and production build passing after clearing stale `.next` build artifacts.

---

## [Unreleased] — 2026-05-12 (dashboard controller hooks)

### Changed
- **Dashboard controller logic split into hooks.** Reduced `app/page.js` again from 1,073 lines to a thin 336-line composition layer by extracting artifact window layout, chat sessions, Main Agent streaming/background activity, and runtime approval/worker actions into `useArtifactLayout`, `useDashboardSessions`, `useMainAgentStream`, and `useRuntimeActions` under `app/components/dashboard/`.

### Verified
- `git diff --check` — passed.
- `npm run check` — syntax checks, 67/67 tests, and production build passing.
- Browser smoke — dashboard loaded at `http://127.0.0.1:8800/`, `/api/state` returned 200, Agent Configuration modal opened, and browser console reported no JavaScript errors.

---

## [Unreleased] — 2026-05-12 (dashboard page refactor)

### Changed
- **Dashboard page split into focused modules.** Reduced `app/page.js` from 2,117 lines to a controller-sized 1,073-line page by extracting the chat surface, canvas/runtime/artifact surface, configuration modal, shared icons/timers, API helper, tool-step labels, artifact iframe/wheel guards, and artifact layout math into `app/components/dashboard/**`. This is behavior-preserving groundwork for safer delegated UI work.

### Verified
- `npm test` — 67/67 passing during the refactor.
- `npm run build` — production build passing after extraction.

---

## [Unreleased] — 2026-05-12 (task UI controls, worker env loading, failure cleanup)

### Added
- **Task creation and management in the UI.** `/tasks` now has a "New task" panel (title, description, assignee, priority, optional parent task id). `/tasks/[id]` has a "Manage task" form (status, priority, assignee, blocked reason), a conditional "Release checkout" button when a stale lock is held, and a "Post comment" composer below the comments thread. All flows use server actions and revalidate the relevant paths so the UI refreshes without manual reload.
- **Settings page realignment.** Rewrote `app/settings/page.js` to use the established admin design-system classes (`admin-panel`, `admin-panel-head`, `admin-card-list`, `admin-record`, `admin-button`) after discovering it referenced classes that didn't exist in `globals.css`. The Save button now matches the rest of the app's primary button style.

### Fixed
- **Daemon scripts now load `.env.local`.** `npm run dev` auto-loaded env via Next.js, but `npm run daemon`, `npm run heartbeat:worker`, `npm run heartbeat:once`, `npm run daemon:once`, and `npm run hermes:job` did not — so `ANTHROPIC_API_KEY` (and any other key) was missing whenever a delegated task ran through the worker. Added Node 22's native `--env-file-if-exists=.env.local` to each script entry in `package.json` so the worker and daemon see the same env the dev server does.
- **Task checkout is released on agent run failure.** `runAgentTick` previously had no try/catch around the engine call. When Claude/Ollama threw (credit balance exhausted, network error, model rejected), the `agent_runs` row stayed at `status=running` and the parent task kept its `checkout_run_id` pointing at the dead run. Every retry then failed with the misleading "Task checkout failed" because `checkoutTask` refused to overwrite an existing lock. Wrapped the body in try/catch — on error, the run is marked `failed` via `markAgentRunFinished` (which already releases the task), a `failed` live event is published, then the error is rethrown so the heartbeat worker still records the failure and retries. The next retry now claims the task cleanly and surfaces the real underlying error.

### Verified
- `npm test` — 67/67 passing.
- End-to-end run: created a task in the UI, assigned to a Codex-CLI-backed agent, watched the heartbeat worker claim it, execute, write an artifact under `workspace/reports/`, post a comment with the artifact link, and move the task to `in_review`. Confirmed the checkout-release fix by reproducing the original failure path (out-of-credit Anthropic agent) and observing the task become re-claimable after each retry instead of dead-lettering.

---

## [Unreleased] — 2026-05-12 (paperclip-style delegation, durable runs, activity replay)

### Added
- **Delegation contracts and schema.** Added canonical task/run/live-event enums in `lib/delegation-contracts.js` and expanded SQLite with org, task, comment, task-event, agent-run, agent-run-event, runtime-state, task-session, and `activity_log` tables. Added derived reporting views: `agent_workload_view`, `manager_delegation_view`, `task_lifecycle_view`, `run_failure_view`, and `approval_latency_view`.
- **Org layer.** Agents now support reporting lines and org metadata: `reports_to`, `role`, `title`, `status_reason`, `autonomy_mode`, monthly budget/spend, and metadata/permissions JSON. Added org queries and APIs: `GET /api/org`, `GET /api/agents/:id/chain`, `GET /api/agents/:id/reports`.
- **Task layer.** Added first-class tasks, comments, immutable task timeline, child-task relationships, terminated-agent assignment guards, lifecycle transitions, reassignment lock clearing, and worker wakeups via the heartbeat queue. Added task APIs under `/api/tasks/**`.
- **Execution layer.** Added durable `agent_runs` and `agent_run_events`, `agent_runtime_state`, and `agent_task_sessions`. Delegated task execution now creates durable runs and per-run events instead of relying only on legacy `runs`.
- **Activity replay.** Added DB-backed business activity persistence and cursor replay via `GET /api/events/since/:cursor`. Typed event names now include `activity.logged`, `task.updated`, `task.comment.added`, `task.assigned`, `task.reassigned`, `agent.run.queued`, `agent.run.status`, `agent.run.event`, and `agent.run.log`.
- **New operator screens.** Added `/org`, `/tasks`, `/tasks/[id]`, and `/activity`. Task detail now includes comments, event timeline, child tasks, active run widget, and historical runs. Reports now exposes operational/audit views based on the new SQL views.

### Changed
- **Live events are durable-first.** `publishLiveEvent()` now dual-writes to `activity_log` before fanout, and the in-memory subscriber registry is hoisted to `globalThis` to avoid HMR orphaning in dev.
- **Heartbeat worker is task-aware.** The durable worker can now execute `agent-task` queue payloads, routing delegated work through the same lease/retry/approval model as other queued jobs.
- **Agent detail surface expanded.** `/agents/[id]` now shows active tasks, current run, reports count, autonomy mode, budget spend, and status reasons.
- **Sidebar navigation expanded.** Added direct entry points for Org, Tasks, and Activity.

### Fixed
- **SSE backpressure guard.** `/api/events/stream` now checks stream pressure and drops pathological slow subscribers instead of growing the queue indefinitely.
- **Build-safe migrations.** Reporting views and the `reports_to` index now apply after idempotent `ALTER TABLE` migrations so older local databases upgrade cleanly.

### Tests
- Added `tests/delegation-and-activity.test.js` covering org hierarchy, task lifecycle, durable activity rows, replay, and terminated-agent assignment rejection.
- `npm test` — 67/67 passing.
- `npm run build` — passed on 2026-05-12.

## [Unreleased] — 2026-05-12 (Paperclip-style live activity)

### Added
- **Live event bus.** New in-memory pub/sub at `lib/live-events.js` lets background work (scheduled agent ticks, sub-agents spawned by the Main Agent) surface live to any subscriber.
- **Global SSE channel.** New `GET /api/events/stream` fans out live events to the dashboard so you can watch what background agents are doing in real time, even when you're not actively chatting. Capped at 32 simultaneous subscribers; auto-cleans up on client disconnect.
- **Dashboard live activity.** The main chat feed now renders inline "tool steps" with status icons (running, done, error) as the Main Agent uses tools. A "busy" indicator on the runtime button shows when sub-agents are working in the background, with hover-tip count.
- **Hermes status panel.** The Agents page now shows whether the Hermes worker is installed and where to put it if not. Hermes-only tools (`send_to_hermes`, `start_hermes_job`) are hidden from the Main Agent when Hermes is unavailable, so it doesn't try to call tools that don't exist.
- **Artifact continuity.** New `update_artifact` tool keeps follow-up turns updating the same artifact instead of spawning new ones. The chat now passes the active `session_artifact_id` to the orchestrator so refinements land on the original.
- **CLI engine streaming.** `runClaudeCodeAgent` and `runCodexAgent` accept an `onChunk` callback so the chat can stream subprocess output progressively instead of showing a frozen progress bar.
- **CLI synthesis pass.** When the Claude Code or Codex engines call web tools, results are now fed back to an Anthropic synthesis pass that composes a complete formatted answer. If Anthropic synthesis is unavailable, falls back to a readable markdown summary of raw search results.

### Fixed
- Settings page no longer accepts arbitrary model IDs — model selection is validated against the live model catalog again.
- Tool inputs from LLM tool-use blocks are no longer forwarded raw over SSE. Only a short whitelisted summary (search query, fetched hostname, artifact title, etc.) is broadcast.
- The 30-second cleanup timer for completed background agents no longer wipes a new run that started for the same agent during the drop window.
- The SSE keepalive interval now clears on graceful client disconnect, not only on abort.
- Out-of-order tool result events no longer silently drop — if `tool_result` arrives before `tool_start`, the step is recorded after the fact.
- `updateArtifact` now re-validates that the on-disk path stays inside the artifacts/reports directories before writing.

### Tests
- New `tests/live-events.test.js` — covers subscribe/publish/unsubscribe, throwing-subscriber isolation, and subscriber counting. 5 new tests, 64/65 passing (1 pre-existing failure unrelated to this work).

---

## [Unreleased] — 2026-05-12 (AI onboarding wizard)

### Added
- **AI onboarding wizard plan.** Saved the implementation plan at `.hermes/plans/2026-05-12_102417-ai-onboarding-wizard.md`.
- **Deterministic onboarding engine.** Added `lib/onboarding.js` to convert business-owner inputs into a launchable AI operating-system blueprint with business context, recommended agents, schedules, skills, memory, tool permissions, autonomy policy, voice-intelligence opportunities, and a first-week plan.
- **Onboarding launch flow.** Added `POST /api/onboarding/blueprint` and `POST /api/onboarding/launch`; launch creates/updates agents, schedules, skills, memory, and approval-gated tool permissions using existing Madarx runtime helpers.
- **Onboarding UI.** Added `/onboarding`, a three-panel account setup surface with business inputs, goal/channel chips, autonomy selection, live blueprint preview, OpenAI Realtime voice readiness notes, and “Create my AI team” launch CTA.
- **Sidebar entry.** Added Onboarding to the primary navigation.

### Tests
- `node --no-warnings --test tests/onboarding.test.js` — passes, covering Villa515-style blueprint recommendation and launch behavior.

---

## [Unreleased] — 2026-05-12 (Villa 515 POC agent reset)

### Added
- **Villa 515 POC setup script.** Added `scripts/setup-villa515-poc.mjs` to safely delete only current agent/artifact rows and exact generated files, then recreate a Villa 515 business-owner agent roster with schedules, skills, memory, and per-agent tool permissions.
- **Villa 515 specialist roster.** The script creates 9 POC agents: Main Orchestrator, Competitor Intelligence, Marketing Strategist, Creative Image Studio, WhatsApp Concierge, Workshop & Events, Commerce & Gifting, Website SEO & QA, and Reputation & Influencer PR.
- **Image-generation provider tools.** The script registers POC tool entries for local/open-source ComfyUI FLUX.1-schnell, Google Imagen/Gemini image generation, Alibaba Tongyi Wanxiang, ByteDance Seedream, Kuaishou Kolors, and OpenAI GPT Image.

### Verified
- `npm run check` — 58/58 tests passing and Next production build passing.
- Browser/curl smoke: `/agents`, `/agents/villa515-creative-image-studio`, `/tools`, `/schedules`, and `/reports` return 200 after the reset.

---

## [Unreleased] — 2026-05-12 (presentation-first output + Claude Code / Codex chat engines)

### Added
- **Claude Code and Codex as chat engines.** The Main Agent chat now routes through `claude` CLI (Claude Code) or `codex` CLI when those engines are selected in the model picker. `buildCliSystemPrompt()` gives them a focused prompt that supports `generate_ui`-style HTML code fences and `madarx-action` agent-creation blocks. HTML artifacts are extracted and saved automatically.
- **Live model discovery.** New `getModels()` async function in `lib/agent-config.js` queries the Ollama tags API and probes CLI availability (`claude`, `codex`) on every call, returning a unified list of Anthropic, Ollama, Claude Code, and Codex models. The model picker in the chat header now reflects the live list.
- **`GET /api/models` endpoint.** Exposes the `getModels()` result over HTTP so the frontend always has the current model catalog without a full config reload.
- **Engine ping.** New `POST /api/runs/engine-ping` route and `<EnginePingButton>` component let the Agents page test engine connectivity inline, surfacing availability errors before a run is dispatched.
- **Auto-artifact promotion.** Any substantive response (headings or 350+ words) that is not already an artifact is automatically promoted to a styled `document` artifact by the stream route — for all three engine paths (Anthropic, Ollama, CLI). This ensures every report, analysis, or structured reply appears in the canvas as a designed document rather than inline chat text.
- **`artifacts` SSE event client handler.** The dashboard now listens for the `artifacts` event emitted by auto-promotion and immediately calls `bringToFront` on the first created artifact, so it appears selected in the canvas without waiting for the polling refresh.

### Changed
- **Presentation-first system prompt.** The Main Agent instruction set was rewritten around a "think about presentation first" rule:
  - Results with visual structure (news, feeds, card collections, timelines, comparisons, leaderboards, dashboards, schedules) → `generate_ui` with an explicit layout description.
  - Dense written content (long-form reports, analyses, plans) → `create_artifact` (rendered as styled HTML document).
  - Hard rule: any response longer than 2-3 sentences must be an artifact or UI; chat replies are 1-2 sentences describing what was created.
- **Per-agent engine selection UI.** The Agents page was rebuilt with per-agent engine and model dropdowns, letting each agent independently target Anthropic, Ollama, or Claude Code engines.
- **`lib/agent-engines.js`** gained `runClaudeCodeAgent` and `runCodexAgent` helpers, completing the four-engine runtime (Anthropic, Ollama, Claude Code, Codex).
- **Stream route agentic loop** now tracks `artifactCreated` and `totalAgentText` across all tool-use turns so the post-loop auto-promotion fallback operates on the full accumulated response, not just the final turn.

### Tests
- `npm run check` — passes with all changes applied.

---

## [Unreleased] — 2026-05-11 (Ollama default + Developer Agent)

### Added
- **Per-agent engine registry.** `lib/agent-engines.js` exposes Anthropic API, Claude Code CLI, Ollama, Hermes, and Codex as selectable agent engines with default models, labels, availability checks, and routing helpers.
- **Claude Code Developer Agent.** Added/persisted a `developer-agent` profile using engine `claude-code`, model `sonnet`, on-demand scheduling, human-in-loop approval, and repo-focused tools: `file_read`, `file_search`, `directory_list`, `git_status`, `terminal_run`, `claude_code`, and `browser_qa`.
- **Claude Code scheduled-run path.** Agent ticks can route through the authenticated local `claude` CLI in print mode for developer/repo analysis, with read-only defaults unless the prompt explicitly requests mutation.
- **Engine tests.** Added coverage that the engine catalog exposes Anthropic, Claude Code, Ollama, Hermes, and Codex; `createAgent` persists selected engine/model; Ollama ticks work without Anthropic; Claude Code engine uses the local CLI provider path.

### Changed
- **Local-first default model.** Main Madarx configuration and new-agent defaults now use Ollama `qwen3.5:latest` / **Ollama · Qwen 3.5** instead of Anthropic by default.
- **Existing agent routing.** Ops Agent and existing local agents were switched to the Ollama engine with `qwen3.5:latest`; Developer Agent remains Claude Code powered for coding workflows.
- **Product status docs.** `project-update.md` now documents Phase 9, the active Developer Agent, Ollama default state, Claude Code availability verification, and current 51-test quality gate.

### Tests
- `npm run check` — passed after switching the default app/agent model to Ollama.
- `npm test` — **51/51 passing** after adding the Developer Agent / Claude Code configuration.
- `npm run build` — passed after adding the Developer Agent / Claude Code configuration.
- Manual verification: `claude --version` reports Claude Code `2.1.138`; `listAgentEngines()` marks `claude-code.available=true`; `developer-agent` persists with engine `claude-code`, model `sonnet`, and generated heartbeat file.

---

## [Unreleased] — 2026-05-11 (later session)

### Added
- **Local machine tool registry.** New `lib/tools/registry.js` plus adapters (`files`, `terminal`, `processes`, `web`, `supabase`, `vercel`) drive a marketplace of bounded local tools that the Main Agent can enable/disable per install. Enabled tools are merged into the Anthropic tool schema dynamically.
- **Tool safety layer.** `lib/tools/safety.js` enforces allowed roots (`MADARX_ALLOWED_ROOTS`), blocks secret/system paths (`.env*`, `.ssh`, `/etc`, `/private`, `/System`, `/Library`), forbids `sudo`/`rm -rf`/`git reset --hard`, caps and redacts output previews, and writes `local.*` audit rows for every call.
- **Generic single-use approvals.** New `approval_requests` table + `lib/approvals.js` issue durable approvals for risky local actions (file writes, patches, dangerous commands, process kills, deploys, git commits/pushes). Approvals are consumed exactly once by the matching action type.
- **Durable heartbeat queue.** New `heartbeat_tasks` and `heartbeat_task_events` tables, `lib/heartbeat-queue.js` (claim/lease/retry/backoff/recover-stale), and `lib/heartbeat-worker.js` + `scripts/heartbeat-worker.mjs` run-loop. The cron daemon now queues one task per schedule/window into SQLite; the worker owns execution, retries, and approval-paused states.
- **`local_processes` registry.** Madarx-owned subprocesses started by the terminal/process adapters are tracked with pid, args, cwd, status, stdout/stderr previews, and exit code, exposed via `/api/state.local_processes`.
- **`agent_tool_permissions` table.** Per-agent tool modes (default `approval_required`) so individual agents can opt into auto-run for low-risk tools without changing global defaults.
- **New API surface.**
  - `GET/POST /api/approvals` — list or create approval requests.
  - `GET/PATCH /api/approvals/[id]` — read, approve, or deny a single approval.
  - `GET /api/tools` — tool marketplace metadata + connector readiness.
  - `PUT /api/tools/[name]` — enable/disable a marketplace tool; `POST /api/tools/[name]/approve` records per-agent approval state.
  - `GET /api/config/env` + writer in `lib/env-file.js` — surface and persist allowlisted env keys (`OPENAI_API_KEY`, `SUPABASE_*`, `VERCEL_TOKEN`, `SEARXNG_URL`, …) without exposing existing values.
- **Open-source web backends.** `web_search`, `web_extract`, and `web_fetch` now resolve through Brave/SearXNG/no-key fallbacks. System prompt is augmented with explicit "use `web_search` then `web_fetch`, cite URLs" instructions whenever internet tools are enabled (`lib/agent-config.js` → `withDynamicToolInstructions`).
- **Connector readiness.** `connectors` gained `type`/`capabilities`/`metadata` columns; the marketplace surfaces required-env state so the settings UI can prompt for missing keys.
- **Settings UI extensions.** The dashboard config panel now lists marketplace tools with toggle switches, lets the owner save allowlisted env keys without echoing values back, and renders a generic approval decision card for pending `approval_requests`.
- **Specs.**
  - `specs/local-tools-and-durable-heartbeat-plan.md` (902 lines) — full implementation plan for the layers above.
  - `specs/open-source-web-backends-plan.md` (203 lines) — strategy for Brave/SearXNG-backed internet tools.

### Changed
- **Schedule execution model.** `runDueSchedules()` no longer executes inline; it now enqueues one durable `heartbeat_tasks` row per schedule/window and records a `cron.schedule.queued` audit entry. `schedules` gained `last_queued_at` / `last_queued_window` columns so the cron daemon stays idempotent across restarts.
- **`getState()`** now returns `heartbeat_tasks`, `approvals`, `pending_approvals`, `local_processes`, and a `local_tool_audit` slice so the runtime view + dashboard can render the new state without extra round trips.
- **Tool dispatcher fallthrough.** `executeTool()` falls through to `executeRegistryTool()` for any name not in `CORE_TOOLS`, so adapter-backed tools share the same orchestrator code path.
- **`npm run check`** now also syntax-checks `scripts/heartbeat-worker.mjs`.

### Frontend (this session)
- **macOS-style artifact focus.** Clicking any visible artifact now selects it and raises it to the front. Inactive HTML/document iframe artifacts get a transparent focus shield, so the first click is captured by the parent window and updates z-order instead of being swallowed by the iframe.
- **Artifact iframe scaling.** HTML/document artifacts scale their iframe content from a 1280px logical width, keeping generated pages usable and inspectable when artifact windows are resized narrower.
- **Runtime activity popover.** Replaced the always-visible runtime strip with a compact toolbar button and popover showing pending approvals, heartbeat queue, local processes, and local audit rows.
- **Toolbar/session polish.** Moved chat session dropdown styling out of inline styles, converted the layout reset action to an icon button, and kept the canvas toolbar above board content.
- **Dock hover preview.** Hovering a minimized artifact in the bottom dock now pops a 300×220 floating preview above the card with the artifact title, kind badge, and a live thumbnail (scaled iframe for HTML/document kinds, rendered markdown otherwise). Pointer-events on the preview are disabled so the click still toggles minimize.
- **Fullscreen ↔ dock layout.** When at least one artifact is docked, the fullscreen overlay shrinks to `bottom: 64px` (via the new `artifact-fs--docked` modifier) so the dock stays visible and reachable.
- **Dock stacking.** Bumped `.canvas-dock` to `z-index: 9000` (and the preview to `9001`) so the hover preview floats above every artifact window — including any that climbed past the default stacking order via `bringToFront` — and above the fullscreen overlay.

### Tests
- `tests/local-tools-heartbeat.test.js` — covers heartbeat queue claim/lease/retry/recover, approval consumption, and local process registry.
- `tests/tools-registry.test.js` — covers tool marketplace listing, enable/disable persistence, env-file writer, connector token wiring, and web backend fallbacks (Brave / SearXNG / no-key).
- Total: **37/37 passing.** `npm run check` (syntax + tests + production build) green on 2026-05-11. Latest artifact stacking/focus update was verified with `npm test` and `npm run build`.

### Ignored
- `.hermes/` added to `.gitignore` (local Hermes plan/session state, parallel to `.herenow/`).

---

## [Unreleased] — 2026-05-11 (earlier)

### Added
- **Phase 2 Claude Code approval path.** Added `/api/runs/approve-claude-run`, strict Claude command parsing in `lib/claude-runner.js`, `which claude` preflight, `spawn('claude', args, { shell: false })` execution, SSE stdout/stderr streaming, run status updates, audit logs, and output artifacts.
- Added dashboard approval UI for `approval-needed` Claude Code dry-runs so the user can approve and stream local execution from the app.
- **Hermes pluggable backend jobs.** Added `send_to_hermes` and `start_hermes_job` orchestrator tools, queued/background Hermes job persistence, live-updating dashboard artifacts, stale-job marking, and a job runner script.
- Added Hermes job API routes under `/api/hermes/**` and tested queued/running/complete/failed job artifact updates.
- Added draggable/resizable artifact windows with z-index focus behavior and active Hermes job status in the canvas.
- **Real cron ticks (Cron → Claude).** Due `agent-tick` schedules now run through `lib/agent-runner.js`, call Claude, save markdown reports under `workspace/reports/`, record full run output, update heartbeat files, and append capped per-agent memory.
- Added idempotent SQLite migrations for `agents.system_prompt`, `agents.memory`, and `runs.output`.
- Added `MAX_TOKENS_PER_TICK` support for scheduled Claude ticks, defaulting to 1024 output tokens.
- Agent creation through the Claude tool path now asks Claude for a focused system prompt, with a deterministic safe fallback for direct runtime/API calls.
- Added mocked tick test coverage for report artifacts, run output persistence, and memory cap behavior.
- **Skills and memory APIs.** Added `GET/PUT /api/agents/[id]/skills` and `GET/DELETE /api/agents/[id]/memory`.
- Main Agent stream context now includes the last 10 memory entries when the user references an agent by name or id.
- Added test coverage for skills read/write, memory pruning, and referenced-agent memory context.
- **Chat history persistence with token tracking.** Every Main Agent turn is now saved to SQLite.
  - New `chat_sessions` table — id, title, model, created_at, last_message_at, message_count, and per-session totals: `total_input_tokens`, `total_output_tokens`, `total_cache_read_tokens`, `total_cache_creation_tokens`, `last_context_tokens`.
  - New `chat_messages` table — id, session_id (FK with `ON DELETE CASCADE`), role, content, `tool_calls` JSON, model, per-message tokens, timestamp.
  - Indexes: `idx_chat_sessions_recent` (last_message_at DESC) and `idx_chat_messages_session` (session_id, at ASC).
- **Anthropic prompt-cache token capture.** Stream route now reads `cache_read_input_tokens` and `cache_creation_input_tokens` from `message_start` events and emits a richer `usage` SSE event including `context` (last input-window size).
- **Sessions UI.** The chat header's old "Clear chat" trash icon is replaced with a Sessions dropdown:
  - Lists recent sessions with title + token total.
  - "+ New chat" item creates a fresh session.
  - Per-row delete (×) removes a session and cascades its messages.
  - On page load, the most recent session is auto-hydrated into the chat feed.
- **Context-window meter.** The model pill now displays live context usage as `XK · YY%` against the model's input window. Hover for exact input + cache breakdown.
- **Chat session API.**
  - `GET /api/chat/sessions` — list (most-recent-first, limit 50).
  - `POST /api/chat/sessions` — create (returns id; usually unnecessary — stream auto-creates).
  - `GET /api/chat/sessions/[id]` — session + messages array.
  - `PUT /api/chat/sessions/[id]` — rename (`{ title }`).
  - `DELETE /api/chat/sessions/[id]` — delete + cascade.
- **`.env.local`** committed for cross-machine dev parity (private repo only — keys rotated as needed).

### Fixed
- **Artifact iframe navigation containment.** Clicking a link inside an HTML artifact (e.g., a generated landing page) no longer loads the full Madarx OS app over the artifact (the "two sidebars" bug).
  - Injected guard now sits at the **start** of the artifact's `<head>` so it pre-empts any page script.
  - Click handler calls `stopImmediatePropagation()` after `preventDefault()`, neutralizing inline `onclick="location='/'"` handlers.
  - New capture-phase `submit` listener blocks `<form action="/...">` from reloading the iframe with the parent app.
  - JS-driven navigation primitives are overridden inside the iframe: `window.open`, `window.location.assign`, `window.location.replace`, `history.pushState`, `history.replaceState`. External URLs are routed to a new tab via `window.top.open`.
  - `<base target="_blank">` injected as defense-in-depth so any link the click handler somehow misses still escapes the iframe.
  - Iframe sandbox now includes `allow-popups allow-popups-to-escape-sandbox` so `window.open(_, '_blank')` actually opens a tab.
  - Removed duplicate click handler from `fixIframe` — the injected guard owns navigation now.

### Notes
- Existing chat history is empty (the table is new). Older conversations from prior sessions were never persisted.
- Token totals only accrue from the moment this update is deployed; reset/clear by deleting a session.

---

## 2026-05-10 — `185c8e4` chore: add .env.local for team access

### Added
- `.env.local` checked into the repo for dev parity across machines (private repo).

### Notes
- Repo is private; keys are rotated as needed. Do not adopt this pattern in public repos.

---

## 2026-05-10 — `3f0b2df` feat: Madarx OS — orchestrator, artifacts, config API, and iframe navigation fix

### Added
- Real Anthropic SDK wired (`@anthropic-ai/sdk`) with `lib/claude.js` (stream + non-stream helpers).
- Orchestrator tool definitions (`lib/orchestrator-tools.js`) for `create_agent`, `create_schedule`, `run_competitor_report`, `run_marketing_plan`, `claude_dry_run`, `list_agents`, `list_schedules`, `create_artifact`, `generate_ui`.
- SSE stream endpoint `/api/orchestrator/stream` with full agentic tool-use loop (max 8 turns).
- `artifacts` table now stores `title` and `content` directly (idempotent migration).
- `config` table for app-level settings (`designStyle`, default `model`, custom Main Agent instructions, enabled tools).
- `/api/config` endpoint and Settings panel.
- HTML artifact rendering via `srcDoc` iframe with first-pass navigation containment.

### Fixed
- Initial pass at iframe link interception (later hardened in this changelog's first entry).

---

## 2026-05-10 — `c524074` chore: rebrand product as Madarx OS
- Project renamed from Villa 515 / Client Workspace prototype to Madarx OS.
- DB file renamed to `data/madarx.sqlite`.

---

## 2026-05-10 — `5e316fa` feat: add hover-expand side navigation
- Far-left rail expands on hover to reveal labels.

---

## 2026-05-10 — `24a7203` chore: remove legacy static prototype files
- Deleted `site/index.html`, `runtime-dashboard/index.html`, the old `server.js`.
- Active product is now only the Next.js stack.

---

## 2026-05-10 — `0a494ad` fix: reset Villa 515 workspace to fresh dashboard
- Removed all seeded/demo data. Workspace now starts blank.
- `lib/seeds.js` is a no-op. Tests updated for fresh-start behavior.

---

## 2026-05-10 — `ea1877b` feat: rebuild Villa 515 as Next agent product
- Migrated from native Node.js POC to Next.js 15 App Router + React.
- SQLite runtime persistence (`node:sqlite` `DatabaseSync`).
- Background cron daemon (`scripts/cron-daemon.mjs`).
- API routes: state, orchestrator/command, agents/create, runs/competitor-report, runs/marketing-plan, runs/claude-code-dry-run, heartbeat/tick, schedules/upsert.

---

## Earlier — POC era
- `dac33e7` wire visual workspace to local agent runtime
- `2badd44` render visual workspace as standalone dashboard
- `d34375d` open Villa 515 visual workspace by default
- `dbea858` add Villa 515 local agent runtime POC
- `0a14fda` add Villa 515 Local Agent OS prototype
