wardyn

Changelog

What shipped in Wardyn, week by week.

Week of September 7, 2026

`src/gateway/util/retry.py` — `retry_transient()`**: retries only transient errors (connection refused/reset, timeouts, Supabase connection family) with 0.5/1/2s backoff; `GATEWAY_RETRY_DELAYS` env override; auth/validation/logic errors fail fast. Wired into `validate_token` employee lookup, usage-log writes, and downstream mount construction. **Never wraps a dispatched tool call

double-execution risk. DB blips now self-heal BEFORE the 3-strike alert counters trip, killing false "persistent failure" alerts.

Verification

full suite `uv run pytest tests/ -q` → **1680 passed, 6 skipped** (46.8s) on `feat/aiw-367-370-retry-status-loud`.

`test_agent_reads_multifile_skill_resources` proves the agent-side leg

an MCP client holding a real install token reads a 5-file, 4-type mock skill (`SKILL.md`, `references/guide.md`, `scripts/run.py`, `examples/config.json`, `assets/logo.png` base64) end-to-end through the gateway resource layer: `gateway://skill/{key}/detail` returns the file index + version, then every `gateway://skill/{key}/file/{path}` read returns the exact uploaded content — text byte-for-byte, the binary PNG as its stored base64 string. The full real chain runs (token → `_load_preset_for_token` → registry `load_preset` → `get_pinned_bundle_file`); only the HTTP header extraction is faked for the in-process client. Unknown skill keys are denied. Closes the doc-11 Q4 gap: storage roundtrips were already proven; this proves the agent can actually ACCESS the files.

Verification

`uv run pytest tests/test_skill_resource_access.py -q` → 1 passed (1.83 s); sweep w/ context-bundles + full marketplace file: 57 passed; ruff + mypy clean.

Verification

focused: 1 passed (2.15 s); full file: 41 passed; mypy: 133 files clean; full suite: **1658 passed, 6 skipped** (36.88 s); pgTAP on fresh `supabase db reset --local`: **Files=20, Tests=414 PASS** (04_workspace_id_fks counts canonical-workspace rows — reset local DB before running pgTAP when integration tests have left rows behind). No production code changed — test/docs only.

`marketplace_install` results now carry a `warnings[]` list

propagates plugin-import warnings and adds one actionable entry per downstream server that ends up registered without an `auth_token`, naming the exact admin tools to fix it (`register_downstream_server` re-run with the same key + token, or `authorize_oauth_server` for OAuth) (doc-11 D4). Because publish strips all credentials from the catalog copy, cross-workspace installs always land credential-less and always get the warning; a re-install into a workspace that already stores a token for the server's derived key preserves it and stays quiet (the upsert omits `auth_token` when the payload has none, so an existing DB token is never clobbered). No schema, no redaction changes.

Tests

4 new integration tests in `tests/test_marketplace.py` (cross-workspace warns + names tools/server; pre-existing token on derived key → no warning; target_preset mode warns; existing result fields unchanged). Focused: 15 passed; full file: 40 passed.

`scripts/measure_publish_perf.py` (doc-11 D5 gate)

builds a synthetic 265-file skill bundle (1 SKILL.md ~3KB, 260 text files 1-2KB, 4 binary PNGs), uploads it via `upload_skill_files`, and times export / cold publish / warm publish / install with `time.perf_counter()`. Writes `tmp/publish_perf_results.json` + a human summary. Local-dev ONLY: the script reads `supabase status -o env` and overrides the connection env vars (exact conftest pattern), then refuses to run unless the URL is `127.0.0.1`/`localhost:54321`.

Measured (local dev, 2026-09-07)

export 0.06s · publish-cold 1.37s · publish-warm 0.10s · install 1.23s. **Decision: warm publish 0.10s ≪ 15s threshold → async publish jobs NOT built** (idempotent fast path + scan PASS-cache keep a 265-file publish synchronous). Recorded in doc-11.

Repeat publishes skip the SkillSpector subprocess for unchanged skills

the publish path now looks up the latest `skill_scans` row for `(content_hash, ruleset_version, scan_mode)` before spawning the CLI; a stored PASS (verdict that did NOT block) short-circuits the rescan (doc-11 D3). `ruleset_version` is the version string the CLI itself reports (`skillspector --version`) — a scanner upgrade invalidates every cached PASS, so no stale verdict is ever reused. Migration `20260907110000_skill_scans_ruleset_version.sql` adds the column (NOT NULL, backfilled sentinel `pre_versioned` for pre-versioning rows — one forced re-scan; the append-only trigger is disabled/re-enabled around the backfill), the composite index `skill_scans_cache_idx`, and re-asserts the 3-tier grants (anon denied, authenticated SELECT-only — cache cannot be poisoned — service_role full). Blocked content is never cached; blocking semantics and SKILLSPECTOR_MODE unchanged. `ruleset_version` is persisted on every new scan row (publish path, contributions path).

Tests

new `tests/test_skill_scans_cache.py` (6 tests: cache hit skips guard, miss scans + persists ruleset, ruleset change invalidates, blocked never short-circuits, version probe stable + cached, missing CLI falls back to `unknown`); 2 existing marketplace scan tests hardened against cross-run cache rows (unique ruleset per run). pgTAP `20_skill_scans_ruleset.test.sql` (11 tests: column shape, index, grants, default, multi-ruleset coexist, append-only still enforced). Suite: 1653 passed; pgTAP Files=20 Tests=414 PASS.

`marketplace_publish` no longer double-publishes on retry

migration `20260907100000_marketplace_dedupe.sql` (doc-11 D2) first dedupes live rows on the 4-column key — (publisher_workspace_id, source_preset_key, content_hash, visibility) — keeping the newest (published_at, id) per dup set (cleans the 2 live prod dup pairs audited 2026-09-07), then creates the partial unique index `marketplace_items_live_dedupe` over live rows only. `_insert_item` is now check-then-insert: a live row with the same key returns `already_published: true` with the same `marketplace_id`; a lost race raises 23505, which is caught and resolved by re-fetching the winner. Republish after an edit (new content hash) or after an unpublish (soft-deleted rows never block) still creates a new row. `visibility` is a key column — the same content may legitimately be live at both `public` and `workspace` visibility (AIW-359 edge, proven by the existing visibility test contract).

Tests

4 new integration tests in `tests/test_marketplace.py` (republish→same id, edit→new row, unpublish→new row, 23505 race simulation→winner returned); pgTAP `19_marketplace_dedupe.test.sql` (6 tests: index exists, dup raises, different hash inserts, soft-deleted never block, workspace-scoped, visibility coexist). Suite: 36 passed; pgTAP Files=19 Tests=403 PASS.

One-off hygiene script

`supabase/snippets/purge_old_unpublished_marketplace_items.sql` (doc-11 D6) deletes soft-deleted `marketplace_items` rows (`unpublished_at` older than 90 days). Live-DB audit 2026-09-07: 80 total / 32 unpublished / 2 purgeable — ran once against remote, removing the 2 orphaned rows. Idempotent; not a migration.

Week of August 31, 2026

The marketplace hop now ships whole skill bundles, not just SKILL.md

`export_preset_as_plugin` reads the preset's pinned capability versions (`preset_capability_versions`, latest-version fallback for unpinned capabilities) and embeds every `bundle_items` row into the package as `skill_files: {slug: {path: {content, encoding}}}` — two queries, no N+1; base64 binaries stay as their stored base64 string; `out_dir` export writes the full file tree. `marketplace_install` re-materializes the files under `skills/<slug>/` (base64 decoded to bytes, `rel_path` re-validated against traversal/absolute/backslash before every write) before the plugin import ingests them, so the target workspace's `bundle_items` receives the complete file set. Old packages without `skill_files` install unchanged.

Tests

6 new unit tests in `tests/test_plugins_export.py` (pinned export, base64 marker, determinism, unpinned fallback, workspace isolation, out_dir bytes) + 8 in `tests/test_marketplace.py` (3-file roundtrip incl binary, 265-file bundle roundtrip, old-shape backward compat, traversal rejection ×4, base64 materialize). Full suite 1643 passed, 6 skipped.

`AGENT_ENDPOINTS` constant in `gateway/onboard.py`

the eight agent-facing endpoint paths (`/robots.txt`, `/llms.txt`, `/llms-full.txt`, `/connect`, `/.well-known/mcp.json`, `/doctor`, `/skill/{name}`, `/mcp`) live in one typed constant; `_build_llms_txt` renders the Endpoints section from it, so the index and the routes can never drift apart again.

Drift-guard tests

`tests/test_agent_surface_sync.py` (4 tests) assert every AGENT_ENDPOINTS path is registered as a route in `__main__.py`, appears in the rendered llms.txt, skills glob mirrors the HTTP skills dir, and docs cover the surface. Recovered from a commit stranded after PR #121 merged (landed via PR #129). Suite: 4 passed; mypy 133 files clean.

The gateway now speaks plain HTTP to agents and crawlers

no MCP handshake needed to discover or verify the service. New GET endpoints: `/robots.txt` (crawl rules + sitemap pointer), `/llms.txt` (llmstxt.org-format index: install paths, per-client commands, endpoint list, skill catalog), `/llms-full.txt` (index plus full text of every served skill), `/connect` (JSON mirror of the `gateway://connect` resource: mcp_url, auth pattern, per-client commands including the Codex two-liner), `/.well-known/mcp.json` (`{mcp_servers: {wardyn: {url, transport: streamable-http}}}`), and `/doctor` (always JSON: anonymous gets status/db_reachable/version/mcp_url; `?token=<valid>` adds token_valid, employee display_name, granted_servers; invalid token adds only `token_valid: false` — no employee data leak).

Two new HTTP skills

`mcp_clients.md` (connect Claude Code, Claude Desktop, opencode, and Codex to Wardyn with exact commands) and `troubleshooting.md` (token invalid/revoked, gateway unreachable, env vars, AIW-346 symptom with pending-deploy note, instant revocation by design).

Codex install path everywhere agents land

landing page setup widget gains a 4th `codex` tab (`export WARDYN_INSTALL_TOKEN=<token>` + `codex mcp add wardyn --url <gateway>/mcp --bearer-token-env-var WARDYN_INSTALL_TOKEN`), the `_install_block` used by signup success and welcome pages gains a Codex path (with the note that Codex does not run embedded commands — paste each line manually), and a manual-section Codex snippet with `codex mcp get wardyn` verification. Landing setup-copy now lists Codex among supported clients, plus an agent-first endpoints link row (llms.txt · connect · doctor · skills).

GATEWAY_PUBLIC_URL-aware base URLs

all new endpoints build absolute URLs via `_gateway_public_url`, so Cloudflare-tunnel deployments render correct links.

Tests

`tests/test_onboard_llms.py` (18 tests: llms index/full, robots, connect, well-known, doctor anonymous/valid/invalid/degraded); regression sweeps `-k 'onboard or landing or view or template or install or signup or welcome'` 213 passed.

Removed stale `.opencode/opencode.json`

the file shadowed the correct global opencode config and pinned subagents (`build`, `general`, `explore`, and others) to dead models (`opencode/mimo-v2.5-free`, `opencode/nemotron-3-ultra-free`), causing `Subagent model resolution failed` errors during task delegation. Agents now resolve `tokenrouter/z-ai/glm-5.3-free` from the global config.

/admin/employees now opens with a Role Guide section

a three-tier comparison table (Employee / Preset Admin / Workspace Admin) showing who can use capabilities, request access, submit contributions, upload without approval, approve contributions, publish to marketplace, and manage employees/invites/tokens/presets/servers. Plus a per-preset note: one person can be preset admin of one preset and plain employee of another; only workspace admins appoint or remove preset admins. Built for new workspace admins landing on the page for the first time.

Rendering

static section in `_render_employees_page` (views/admin.py), first section on the page, reuses existing `admin-section`/`admin-table` CSS variables. No JS, no new function.

Tests

`test_role_guide_renders_three_tiers` (test_admin_ui.py) asserts the guide + all three tier headers render; full suite 53 passed.

The site /changelog now parses the repo CHANGELOG.md

dated `[Unreleased] — Title (date)` sections render as week-grouped cards (Monday-of-date labels), newest first, HTML-escaped. The hardcoded marketing.py list is now fallback-only (page never 500s when the file is unreadable) and still supplies pre-September history the md doesn't cover.

Parser accepts dated versioned releases too

`## [2.6.0] — 2026-09-04` headers now parse (previously only dated `[Unreleased]` did; versioned meant "old history the hardcoded list covers" — no longer true once releases carry new content). Sections are grouped by Monday-of-week, so multiple releases in one week merge into a single card group instead of duplicating the week label.

Docker image ships the file

runtime stage COPYs CHANGELOG.md; parser resolves repo root / /app / cwd.

Tests

9 parser/page tests (real-file parse, fallback, undated/versioned section skip, bad-date skip, HTML escaping, no duplicate week labels); page suite 39 green.

Sync test: registered tools ⟷ seed lists

new `tests/test_tool_mounting_sync.py` builds the real FastMCP server (dynamic HTTP mode, DB mocked) and asserts every registered tool is covered by a mount source (UNIVERSAL/ADMIN/ADMIN_ONLY seed keys or BUILTIN/BOOTSTRAP/ADMIN_SCOPE hardcoded sets). Any future tool added without a seed entry fails CI with the exact uncovered names + fix hint. Red path verified by monkeypatching a name out.

Found 54 unseeded tools on first run

3 universal experience commands (`submit_capability_experience`, `get_capability_experiences`, `my_capability_experiences`) + 51 admin tools (webhooks/alerts/templates family, audit export family, dashboard/usage analytics, schema drift family, marketplace admin suite, OAuth authorize, catalog install, prompt allowlist, session terminate, workspace deletion request). They were registered in `admin/register_tools.py`/`tools/commands.py` over time but never seeded → invisible to every preset including admin.

Seed lists + seed.sql

`UNIVERSAL_CAPABILITIES` 10→13, `ADMIN_CAPABILITIES` 65→116 with verbatim docstring descriptions (AST-extracted); seed.sql rows (uuids 0311-0313, 0271-02a3) + key-based wiring for CS and admin presets.

Live verification

admin employee tools/list 99→148; previously-invisible `get_dashboard_stats`, `marketplace_install`, `submit_capability_experience` all mounted and callable.

Tests

sync test green (red path proven); pytest selection (seed/capabilit/preset/mounting_sync): 213 passed; `test_regressions_aiw67.py` baselines updated 83→137 caps / 178 assignments. pgTAP 18 files / 397 tests green.

Fix: marketplace skill installs broke whole workspaces with P0001

`persist_skill_bundle` (the skill-import RPC) inserted `preset_capabilities` rows WITHOUT `preset_capability_versions` pins. `resolve_employee_pinned_presets` (0057) raises P0001 "Capability assignment unavailable" if ANY preset of an employee has an unpinned assignment — so one orphaned install killed tools/list for every member of that workspace. Root-caused via prod census: 20 orphans, all in `cs_ops_shared_test` (ws-e9fbniog), all created by the AIW-306 identical-content reuse path.

persist_skill_bundle v5

now pins at BOTH assignment sites: reuse path pins the existing version, fresh path pins the new version (`on conflict do update`). No other logic touched.

Orphan repair

idempotent do-block (proven 20260903160000 pattern): ensure `capability_versions` per orphan cap, then `set_preset_capability_pin` per unpinned assignment. Prod: 15457 assignments, orphans 20→0.

Bootstrap writer fixed

`setup.py:596` legacy path inserted assignments without versions/pins; now creates a `capability_versions` row and pins via the RPC. No bare pin-less INSERT remains in `src/` (AC#1 grep).

seed.sql

wiring + pin inserts wrapped in one transaction (the new deferred trigger fires at seed time too).

Live verification

marketplace install via the exact orphan-generating path (reuse, no target preset): isError=false, pin created seconds later, global orphan census 0, no P0001 anywhere.

Tests

pgTAP `18_pin_invariant_on_skill_persist.test.sql` (8 tests: bare insert throws at commit via `set constraints immediate`, pinned paths lives_ok, pin/version parity after both persist paths). Local: `supabase db reset` + `test db` = 18 files, 397 tests all green. pytest selection (setup/bundle/pin/capabilit/preset): 279 passed, 0 failed.

Creator-only workspace admin lock

DB triggers on `admin_scopes` + `employee_roles` (anchored on `workspaces.creator_employee_id`) now enforce: only the workspace creator may hold workspace-admin scope; the creator's own row cannot be updated or deleted. Cascade deletes and legacy null-creator workspaces pass. Applied to prod (supabase migration `20260904130000_creator_only_workspace_admin_lock`).

pgTAP CI re-enabled

seed.sql had 15 colliding uuids (marketplace block reused 0230-0249 from the admin block) which made `supabase db reset` fail; renumbered to 0256-0270. CS wiring for the 6 AIW-348 universal commands is now key-based (literal ids were skipped when the earlier backfill migration had already inserted those keys with generated ids → FK violation). `supabase test db`: 389/389 green. ci.yml `pgtap` job un-disabled (was `if: false` since the seed error).

Prod cleanup

removed `testuser@bayzat.com` admin-preset membership (visibility leak only; no admin_scopes row, 0 tokens).

Fix: 10 registered tools never mounted for any preset

added to commands.py over time without seed entries, so they were invisible to `_allowed_tool_names` (mounting) AND to `list_capabilities` (catalog). Employees saw `Access denied. Tool 'X' is not available for your workspace preset.`

Seed lists

`UNIVERSAL_CAPABILITIES` 4→10, `ADMIN_CAPABILITIES` 62→65 in `src/gateway/db/seed_capabilities.py`; matching rows in `supabase/seed.sql` (fixed uuids 0305-0310, 0253-0255); admin-preset select list extended

Live verification

prod tools/list 90→99 tools; `whoami` tools/call returns full identity (alam@bayzat.com, admin, ws-o9irbued); `provision_team_mate` mounted

Tests

new `tests/test_seed_capability_lists.py` (membership + field completeness, red→green); `test_regressions_aiw67.py` count assertions updated 74→83 caps / 66+3×6 → 75+3×12 assignments

CLI onboard now configures Codex CLI

`npx bayzat-gateway onboard` adds `codex mcp add wardyn --url <gateway>/mcp --bearer-token-env-var WARDYN_INSTALL_TOKEN`, removes stale entries (`bayzat-alam`, `company-workspace`), and idempotently upserts `export WARDYN_INSTALL_TOKEN=<token>` into `~/.zshrc` (Codex reads the env var at launch, never a literal token in config.toml). Standard install prompt now suffices for Codex — no manual steps. Verified: `codex mcp get wardyn` shows correct config; `codex exec` reaches the gateway (auth OK; tools mount once the AIW-346 fix deploys) (AIW-347)

Fix: tools/list crashed on re-list (warm process)

`ToolAnnotationTransform` merged existing annotation fields via `ToolAnnotations(title=..., **model_dump())`; any second list (reconnect, `tools/list_changed`) passed the previously stamped `title` twice → `got multiple values for keyword argument 'title'` → whole tools/list died → clients showed "Failed to get tools". Transform now merges fields into ONE dict (explicit overrides win) before constructing — idempotent, verified by regression test `tests/test_tool_annotations_idempotent.py` (red → green) (AIW-346)

Fix: tools/list crashed on live prod

Railway's fresh build resolved `fastmcp>=3.4.4` to 4.0.2 (breaking major); `ToolAnnotationTransform` mutated `ToolAnnotations` in place, which fastmcp 4.x exposes getter-only (`property 'getter' of 'ToolAnnotations' object has no setter`). Every tools/list errored; clients showed "Failed to get tools" with zero tools. Transform now constructs new `ToolAnnotations` objects (merging existing fields) — compatible with 3.x and 4.x

Pin policy

all 10 runtime deps upper-bounded at locked major in pyproject (`fastmcp>=3.4.4,<4`, `anthropic<1`, `arize-phoenix<20`, etc.); `uv.lock` re-locked under bounds (no drift)

Reproducible Docker builds

Dockerfile now installs from `uv export --frozen` requirements (exact lockfile versions) instead of re-resolving pyproject floors; `--no-deps` project install

Fix: `src/gateway/__main__.py` IndentationError

`from gateway.onboard import (...)` block lost its leading indent (paste damage), breaking `gateway.__main__` import and `test_admin_subpage_routes_are_registered`; restored indent, test green

Known local-DB drift (not fixed here)

`tests/test_multi_preset.py::test_admin_preset_loaded` + `test_cs_employee_multi_preset_merge` fail against local Supabase (admin preset lacks `import_skill`/`create_preset` rows); pre-existing before these changes, verified via stash

Deduped CLAUDE.md rules

[CHANGELOG-UPDATE]/[CHANGELOG-COMMIT] each appeared twice; now once (AIW-344)

Docs Structure tree moved to reference

full tree + naming rules now live in `docs/reference/docs-structure.md`; CLAUDE.md keeps a 3-line pointer (progressive disclosure)

Language Primers table removed

primer pointers preserved in docs-structure.md; Hard Rules still reference primer paths directly

RTK.md merged into AGENTS.md

global `~/.claude/CLAUDE.md` is now empty of rules; RTK section lives in project AGENTS.md

Skills audit performed

top-30 skill curation recommended from OpenCode DB usage evidence (AIW-344)

Commit-time ruff gate

tracked `.githooks/pre-commit` runs ruff on staged Python files; `git config core.hooksPath .githooks` wires it. Undefined names (F821) and similar can no longer be committed while a session claims success (AIW-340)

Evidence-based DoD defaults

new backlog tasks now carry four Definition-of-Done items requiring pasted verification output (ruff, mypy, symbol-existence grep, test counts), closing the AIW-332 "claimed but never written" gap (AIW-340)

Fix: repair broken AIW-332/334 commits

12 undefined names and 3 duplicate config fields from the milestone/feedback commits now resolve: wired missing imports (`get_downstream_schema_drift` via `asyncio.run`, `set_downstream_server_enabled`, `get_capability_experience_count`), replaced `db_client()` with the closure `db` client in experience tools, and wrote the two never-written scheduler checkers `_check_and_send_milestone_emails` + `_check_and_send_team_collaboration_prompt` per AIW-332 spec (workspace-scoped active-user counts, one-time stamp columns, no retry-spam on failed sends) (AIW-339)

Agent experience sharing system

Agents can share how capabilities helped them using a guided template, with experiences visible to peers in the same workspace (AIW-333)

Experience-aware capability listing

list_capabilities now shows experience counts (e.g., "💡 5 agent experiences") to surface valuable capabilities

Context bundle renamed

context_generic renamed to context_wardyn with enhanced Wardyn-specific guidance including experience sharing tools

New experience API tools

submit_capability_experience, get_capability_experiences, and my_capability_experiences for sharing and discovering peer experiences

Feedback follow-up system

Automated emails when feedback is approved, denied, or implemented (AIW-334)

Milestone achievement emails

200 calls and 5 active users milestones with branded notifications (AIW-332)

Team collaboration prompts

Emails sent when workspace has solo usage for 2+ days to encourage collaboration (AIW-332)

Enhanced email template support

All email types now support Resend templates with branded HTML fallbacks (AIW-331)

Feedback follow-up system

Automated emails when feedback is approved, denied, or implemented (AIW-334)

Milestone achievement emails

200 calls and 5 active users milestones with branded notifications (AIW-332)

Team collaboration prompts

Emails sent when workspace has solo usage for 2+ days to encourage collaboration (AIW-332)

Enhanced email template support

All email types now support Resend templates with branded HTML fallbacks (AIW-331)

Auto-heal DB connection pool

When database connection fails, automatically resets cached Supabase client and retries the query before escalating to error state (AIW-323)

Auto-revoke expired tokens

Background scheduler revokes install tokens older than 90 days to improve security (AIW-319)

Auto-accept safe schema drift

Background scheduler automatically acknowledges schema drifts that are only additive (backward-compatible changes) (AIW-321)

Auto-clean stale sessions/JWKS

Background scheduler runs gateway_repair periodically to clear stale sessions and reset JWKS cache (AIW-322)

Self-healing config validation

New health check validates critical environment variables and reports misconfiguration in diagnostics (AIW-329)

Auto-enable/restart unhealthy downstream servers

When health checks detect unreachable downstream servers, automatically attempts to re-enable them if found disabled in DB (AIW-320)

Auto-quarantine poisoned tools

Background scheduler runs tool poisoning scans and automatically disables tools when poisoning is detected (AIW-324)

Garbage-collect orphaned resources

Background scheduler periodically cleans up unused capabilities, dangling webhook subscriptions, and stale feature flags (AIW-326)

Health-based circuit breaker

Background scheduler implements circuit breaker pattern for repeatedly failing downstream servers, temporarily stopping traffic and testing recovery after cool-down (AIW-327)

Preset capability flows fixed

assign/remove resolve capabilities scoped to the admin's workspace; the cross-workspace key collision that caused silent no-op removes and "Capability assignment unavailable" errors is gone (AIW-295).

Preset rename + context save fixed

`rename_preset` resolves the preset first and checks preset-scoped admin rights, so preset admins (not just workspace admins) can rename presets and save context; update uses `maybe_single()` (no PGRST116 500s) (AIW-295).

Capability dropdown

the preset detail page assigns capabilities via a dropdown of the workspace's unassigned capabilities instead of a free-text key field (AIW-295).

"Preset context"

the landing-intro field is relabeled "Preset context" to reflect what it is: context fed to agents (AIW-295/296).

Marketplace browse page

`/admin/marketplace` lists catalog items visible to the workspace (public + own); Marketplace and Connect/onboard buttons in the admin sidebar (AIW-297).

Preset context feeds agent instructions

stdio initialize instructions now include the preset's `landing_markdown` and its `kind='context'` bundle contents (capped 4k/bundle, 12k total; truncated tails point to `gateway://context/<key>`); dynamic-HTTP agents get a session-start directive to read `gateway://preset` and `gateway://context/<key>` (AIW-296). No schema change — the RPC already returned `landing_markdown` (migration 0057).

Benefit-led copy

hero, pipeline, features, and setup sections lead with impact on the reader's work life; technical detail demoted to supporting copy. Structure, links, and pricing untouched (AIW-298).

Cross-workspace duplicate-email isolation, test-proven

signup with an email from another workspace provisions a fully independent workspace; A-side rows unmutated; bound session tokens resolve only their workspace; the login chooser rejects foreign workspace IDs with no bind attempt. 14 new tests, row-filtering fake DB makes a dropped `workspace_id` filter fail the suite (AIW-299).

Button/route audit

automated test extracts every form action and link from every page and resolves it against the `__main__.py` route table; dead buttons fail the build. Removed one dormant dead button (unrouted per-employee credential form) (AIW-300).

Week of August 24, 2026

Welcome email

new employees receive a branded install prompt email on workspace join, with a one-click "Start asking" link (AIW-266).

Email scheduler

background scheduler runs daily to check for inactive employees and trigger lifecycle emails (AIW-269).

Re-engagement nudge

employees inactive for 7+ days receive a personalized nudge email via the `wardyn-reengagement` Resend template (AIW-269).

Weekly usage digest

admins receive a Monday morning email summarizing top tools, total cost, and active employees per workspace. Uses `SUM(cost_cents)` from `usage_events` (AIW-272).

Resend templates

all transactional emails (`workspace-welcome`, `wardyn-reengagement`, `wardyn-digest`) now use Resend managed templates with brand styling.

Context bundles

workspaces can define reusable context bundles (prompts + skills + resources) that install with the gateway (AIW-270).

Export/import

workspace state (presets, capabilities, bundles) can be exported as JSON and imported into another workspace (AIW-271).

General page

Settings renamed to General; shows workspace facts (employee count, preset count, created date).

Duplicate workspace conflict

login page detects existing workspace and offers "Log in" or "Report a problem" buttons instead of silently failing.

Workspace deletion

admins can delete workspaces via email confirmation flow; surgical delete by admin email (AIW-262).

request_templates

new table for predefined server-access request templates (GitHub, Jira, etc.) (AIW-139).

Usage summary

replaced the old cost page with a real-time usage summary showing tool calls, cost, and active employees (AIW-192).

Feedback system

submit_feedback tool + workspace feedback button + admin inbox with themed detail pages (AIW-232).

Skill filters

preset and kind filters on the skills page (AIW-213).

Preset admin uploads

preset admins can upload skills directly; workspace-wide promotion requires approval (AIW-227).

Error pages

themed 401/404/500 error pages across admin, dashboard, and OAuth flows.

Prompt capability kind

gateway exposes prompt templates as a first-class capability kind (AIW-198).

Resource templates

MCP resource templates with parameterized URIs for skills, presets, and contexts (AIW-214).

Tool annotations

tools carry annotations (readOnlyHint, destructiveHint) for client-side UX (AIW-215).

Per-preset prompts

prompts scoped to specific presets, not broadcast to all employees (AIW-216).

Session tracking

gateway tracks connected sessions with protocol version, transport type, and last active timestamp (AIW-217).

Progressive skill delivery

skills are bundled and delivered progressively during gateway init, not all at once (AIW-218).

Self-serve mounting

employees can mount downstream servers to their own workspace without admin intervention (AIW-219).

Skill lifecycle telemetry

`skill_usage_stats` tool exposes discovered/executed/pinned metrics per capability (AIW-243).

Export/import tools

`export_workspace_state` and `import_workspace_state` admin tools for full workspace portability.

Plugin manifest

Agent Plugins v1.0.0 shared contract for cross-platform preset portability.

Plugin export

export any preset as a standalone agent plugin (JSON manifest + bundled skills).

Plugin import

import an agent plugin as a new preset with automatic capability resolution.

Gateway-controlled login

Resend magic-link login now orchestrated by the gateway, not the client (AIW-254.1).

ward_ token prefix

install tokens use `ward_` prefix for easy identification (AIW-193).

CSRF consent

OAuth consent flow includes CSRF protection (AIW-107.1).

RLS hardening

tenant-scoped RLS policies on all new tables; service_role grants for admin operations.

Per-workspace email uniqueness

employees cannot have duplicate emails within the same workspace (AIW-259).

Signup validity

confirmation pages now state 24h token validity (AIW-253.1).

Railway deployment

production gateway deployed on Railway with auto-deploy from master.

Cloud Supabase

migrations compatible with Supabase Cloud; clean seed for local dev.

CI pipeline

ruff linting, mypy type checking, 1348+ tests (unit + integration + pgTAP).

Schema contract

automated test asserting all 44 public tables exist with expected columns.

Week of August 10, 2026

Landing redesign

new marketing landing page with combined login/register flow.

Admin dashboard

responsive control center with status cards, quick actions, and employee list.

Per-preset landing

each preset gets its own landing page with tailored install instructions (AIW-186).

Admin polish

HIG-aligned dashboard, consistent spacing, centered flow diagram (AIW-184/185).

Pricing page

`/pricing` served by the gateway with tier breakdown.

FAQ page

`/faq` with common questions.

Changelog page

`/changelog` for release notes.

Week of August 3, 2026

Plugin manifest

shared contract for cross-platform preset portability.

Export preset

serialize any preset as a standalone agent plugin.

Import plugin

deserialize an agent plugin into a new preset.

Admin tools

`export_agent_plugin` and `import_agent_plugin` wired into the admin skill set.

Bundle schema

skills stored as atomic bundles with content hash, version pin, and metadata.

Pinned resolution

gateway resolves skill versions at init time, not per-request.

Encoded assets

bundles support base64-encoded design assets (images, fonts).

Factory reset

`factory_reset_workspace` tool for tenant data cleanup.

Capability versioning

immutable version pins on skill updates.

Skill scans

intent-scoped skill content scanning for quality enforcement.

Week of July 27, 2026

MCP Gateway

company-controlled MCP provisioning service for Claude Code.

Auth middleware

SHA-256 token validation against live Supabase state.

Capability registry

preset-based tool/skill/resource filtering per employee role.

Supabase backend

PostgreSQL with RLS, 37 tables, pgTAP test suite.

Admin dashboard

employee management, preset configuration, server mounting.

Employee portal

self-serve tool access, feedback submission, workspace discovery.

Downstream proxy

N8N integration with 33 tools, per-server auth tokens.

Week of August 17, 2026

Usage summary replaces the cost page.

Real-time tool calls, cost, and active employees, scoped per workspace.

Feedback: submit_feedback tool, a workspace feedback button, and an admin inbox.

Employees rate tools and send comments; admins review them in a themed inbox.

Prompts, resource templates, tool annotations, and per-preset prompts.

The gateway serves parameterized resources and read-only/destructive hints so clients render them correctly.

Session tracking and self-serve server mounting.

The gateway tracks connected sessions and lets employees mount downstream servers themselves.

Skill lifecycle telemetry.

Discover, open, execute, and outcome events are recorded per capability for adoption metrics.

Week of July 20, 2026

Self-service signup: create a workspace, become its admin.

create_workspace_with_admin() provisions a new tenant with an owner-scoped admin in one step.

Invite governance: multi-use invites, revocation, and TTLs.

Invites carry workspace_id, open_enrollment flags, and expiry; admins revoke them from the UI.

Welcome flow: /welcome page and Claude Desktop remote-URL install.

Invited employees redeem an email invite, land on /welcome, and get a Claude Desktop config block.

Offboarding retires the employee: magic-link blocked, Supabase user banned.

Workspace-scoped token recovery with an admin MCP tool.

Recovery replaces Slack-webhook routing; the workspace handle scopes every lookup.

Downstream servers scoped workspace-wide or per-preset.

Universal capabilities and workspace handles.

Capabilities dedupe by handle across workspaces, so presets share skills without name collisions.

Conversational provisioning via /help and a live tool-call event feed.

Admins bootstrap setup by chatting; the dashboard streams live calls as they happen.

Starter presets and rename tools replace seeded mock data.

Week of July 13, 2026

The gateway ships: repo skeleton, Supabase schema, toolchain green.

P0 and P1 land together — a full FastMCP gateway with tools, middleware, and 23 unit tests.

Client dialects: Claude Code, Claude Desktop, and OpenCode detected and normalized.

OAuth 2.1 compatibility for Claude Desktop connectors.

Authorization-server metadata, consent flow, PKCE, and a /token endpoint that speaks streamable HTTP.

One-command installer and self-service onboarding (Phase 6).

Invite codes, /onboard page, CLI setup, and a one-liner install for Claude Code and Claude Desktop.

Downstream MCP proxy with hot-reload and tool discovery.

Register a server once and its tools become namespaced and governed; schema changes surface immediately.

Governance pipeline: cost metering, sessions, reports, audit export, webhooks.

Budget controls stop overspend before a call runs; live sessions, usage reports, and Slack webhooks ship.

Employee portal, community context library, and feedback.

Employees get a self-service portal, can contribute context, and rate capabilities.

Wardyn adoption: identity spine, JWKS token verification, pgTAP, CI.

Workspace-scoped composite keys, real ES256 JWT verification against Supabase, and a database test harness.

CRM connectors: HubSpot and Salesforce, then pivoted to downstream proxies.

Custom connectors were tried and removed — the downstream MCP registry won as the integration path.

Wardyn-branded landing page and Railway deploy config.