Changelog

What we shipped recently

A running log of Stackon’s evolution. Every meaningful change lands here.

  1. v0.39.4Shipped

    Press-kit full lockup — mark + wordmark on one canvas

    • Added a horizontal lockup to the press kit — mark on the left, wordmark on the right, both centered on the same 1536×1024 canvas. Ships in both dark (`#0A0A0A`) and light (`#FAFAF8`) variants. Useful for footers, slide decks, and any context where a journalist or partner wants both identity pieces composed as a single asset rather than reassembling the mark and wordmark by hand.
    • `scripts/crop-stackon-logo.mjs` gains a lockup pass that reuses the existing `markBuffer` and the vector wordmark SVG (480×480 mark, 64 px gap, 760 px wordmark, group centered on the canvas). Same `npm run gen:brand` regenerates everything; no new dependencies.
    • `/media` now lists the two lockup tiles at the top of the asset grid, ahead of the mark-only and wordmark-only slots — the typical first thing a press contact reaches for.
  2. v0.39.3Fix

    Press-kit wordmark — designed SVG replaces gpt-image-1 raster

    • The `/media` wordmark slots (`Wordmark · dark` and `Wordmark · light`) were generated by gpt-image-1, which mangles text — the rendered `STACKON` letterforms had subtle distortion and paper-grain artifacts that didn't read at press scale. Replaced with a hand-wired vector wordmark built from Geist Regular glyph paths.
    • New `scripts/build-wordmark.mjs` reads the Geist-Regular.ttf already bundled with `@vercel/og` (a transitive Next.js dependency), uses `opentype.js` to convert the text "Stackon" to SVG glyph paths, and writes `public/stackon-wordmark-{dark,light}.svg`. Path-only — no font dependency at render time, no rasterization, no AI distortion. Single color per variant (white #FAFAFA on dark, charcoal #0A0A0A on light); drops the legacy coral 'on' two-tone from the v0.34 bitmap source, aligned to v0.36 monochrome.
    • `scripts/crop-stackon-logo.mjs` now invokes the wordmark builder up-front, then rasterizes the SVG onto a 1536×1024 canvas at 1100 px target width for the press-kit PNGs. `npm run gen:brand` remains the single command to refresh all brand assets. PNG file size dropped from ~640 KB (raster) to ~59 KB (clean vector composite).
    • Removed the `media-logo-dark` / `media-logo-light` entries from `scripts/placeholder-prompts.mjs` so a future `npm run gen:placeholders --force` can't silently overwrite the vector wordmark with a gpt-image-1 raster. Gotcha #60 (wordmark distortion caveat in press kit) closes.
  3. v0.39.0Preview

    Stackon for Windows — Tauri shell scaffolded (NSIS + MSI)

    • The Tauri shell now targets Windows alongside macOS. `tauri.conf.json:bundle.targets` extends to `["dmg", "app", "nsis", "msi"]` — Tauri silently skips the platform-foreign targets at build time, so the same `npm run desktop:build` invocation produces a DMG on macOS and an NSIS .exe + MSI on Windows. No code changes in `main.rs`; the Rust shell is platform-agnostic by design.
    • Added a Windows `bundle.windows` config block: `webviewInstallMode: downloadBootstrapper` (so older Windows 10 boxes get WebView2 fetched at install time), NSIS `installMode: perMachine` with the new `icon.ico` as the installer icon, WiX defaults set to `en-US`. The capability surface is unchanged from the macOS shell — same minimal `core:window:*` permissions.
    • Asset pipeline extended: `scripts/crop-stackon-logo.mjs` now also emits `desktop/src-tauri/icons/icon.ico`, a multi-resolution ICO covering 16/24/32/48/64/128/256 px. Built from the same RGBA mark via the `to-ico` npm package — no external tooling, runs on every `npm run gen:brand`.
    • **You cannot build a Windows installer from macOS.** Tauri's Windows bundler relies on Windows-only tooling (NSIS, WiX, SignTool). Production builds happen either on a Windows machine or — recommended — a `windows-latest` GitHub Actions runner. `desktop/DEPLOY.md` gains a section 8 covering Windows prerequisites (MSVC C++ Build Tools, rustup), build commands, code signing (OV vs. EV certs), and the GH Actions workflow stub. The `desktopWindows` feature flag stays `enabled: false` until a signed `.exe` + `.msi` is hosted.
  4. v0.38.1Fix

    Stackon for Mac — first compiling build (scaffold fixes)

    • v0.38.0 scaffolded the Tauri shell but never compiled locally — the dev box was missing Command Line Tools + Rust. After installing both (xcode-select --install, rustup default stable), the first `npx tauri build` surfaced two real bugs in the scaffold: a capability that doesn’t exist in Tauri v2.11, and icon PNGs in the wrong color space.
    • Capability fix: `core:webview:allow-reload` was requested in `capabilities/default.json`, but Tauri 2.11 removed it — reload now lives on the OS menu (⌘R). Dropped it; nothing was using it anyway.
    • Icon fix: `scripts/crop-stackon-logo.mjs` was emitting RGB PNGs for the desktop icon matrix (32×32, 128×128, 128×128@2x, 512×512). Tauri’s `generate_context!` proc-macro requires RGBA. Added `.ensureAlpha()` to the desktop icon loop. All four PNGs now write as 8-bit RGBA; `iconutil` still assembles `icon.icns` from the RGB sources cleanly.
    • After fixes, the first end-to-end build produces `Stackon.app` + `Stackon_0.38.1_x64.dmg` in ~1m 23s (~4.2 MB DMG, unsigned x86_64). Signing + notarization still gated on Apple Developer Program enrollment. Bumped `desktop/{Cargo.toml,package.json,tauri.conf.json}` from the inherited 0.37.1 to 0.38.1 so the DMG filename + version sync to the repo cadence going forward.
  5. v0.38.0Preview

    Stackon for Mac — Tauri v2 desktop shell (scaffolded)

    • New `desktop/` package: a Tauri v2 Rust shell that wraps the Stackon web ADE in a native macOS window. Dev mode points at `http://localhost:3000` and hot-reloads alongside `npm run dev`; production build redirects to `https://stackon.ai/dashboard` from a tiny `dist/index.html` splash. Window chrome is native (titlebar, dock icon, ⌘W/⌘Q), backed by WKWebView on macOS so memory + cold start are roughly half of what an Electron equivalent would be.
    • Bundle identifier `ai.stackon.desktop`. Icons regenerate from `stackon/stackon_logo.png` via the existing `npm run gen:brand` script — the matrix now includes 32×32/128×128/128×128@2x/icon.png PNGs plus an `icon.icns` assembled via macOS `iconutil`. The Rust code is intentionally tiny: a single `main.rs` that calls `tauri::Builder::default().run()`. We’ll add commands + plugins (system tray, global hotkey for voice push-to-talk, native notifications) as later iterations.
    • Distribution: direct download from `/download`, no App Store. The `desktopMac` feature flag stays `enabled: false` until the signed DMG is hosted — the page renders a 'Coming in Phase 4' violet pill in the meantime. Signing + notarization requires Apple Developer Program enrollment ($99/yr), which is the same gate as TestFlight (mobile/DEPLOY.md step 0.2). Doing both together is the natural sequence.
    • Windows build is a near-identical follow-up in a later session — same Tauri base, swap the bundle target from DMG to MSI. The `/download` page already has a Windows card showing 'Coming in Phase 4' so the placement is in place.
  6. v0.37.0Shipped

    Voice v2 — true local Whisper (wasm) joins the capability chain

    • Push-to-talk now has a third path: fully in-browser Whisper via `@huggingface/transformers` (ONNX wasm). Audio + inference both stay on the device — nothing leaves the browser. First use downloads ~145 MB of `onnx-community/whisper-tiny.en` at fp32 precision (cached in browser Cache Storage thereafter). Subsequent transcriptions reuse the loaded pipeline with no network round-trip. fp32 chosen over the smaller quantized variants because their per-tensor scale tensors are broken in the upstream exports — crashes session init via the new MatMulNBits kernel.
    • User-selectable preference (auto / local / cloud) is persisted to localStorage and exposed as a one-click chip next to the mic button. `auto` resolves to webspeech → local → cloud (best UX everywhere). `local` is a privacy lock — it will never silently fall back to cloud. `cloud` keeps the existing Whisper-1 path for users who explicitly opt in.
    • Audit log records the same shape for all three modes (`bytes`, `mime`, `duration_ms`, `transcript_chars`, plus a new `mode` field). A new same-origin `POST /api/voice/audit` endpoint receives a JSON beacon from the browser when local-mode transcription finishes, so the compliance audit log shows the usage without the audio ever crossing the wire.
    • Component palette aligned to v0.36 — error states use rose instead of red. Build, lint, typecheck all clean.
  7. v0.36.3Shipped

    Final amber sweep — every remaining marketing + auth + onboarding surface

    • Closes the v0.36 monochrome campaign. After this commit, `grep -rE 'amber|FBBF24|251,191,36' src/app src/components` returns empty across the entire repo. Home (v0.36), dashboard (v0.36.1), pricing (v0.36.2), and now everything else.
    • 16 files swept: About, Careers, Changelog, Community/Discord, Community/Events, Contact, Forge marketplace + listing detail, Invite landing, Media, Privacy, Terms, the auth layout + form, the onboarding layout + 3-step wizard. ~58 amber utilities + 5 amber-rgba hero backdrops retired.
    • Tone calls worth flagging: the changelog `preview` kind moved from amber to rose, filling the 4-tone categorical slot alongside shipped (emerald), fix (sky), infra (violet). The invite-landing `Pill` ('Team invite' / 'Email mismatch') moved to violet — categorical 'invitation' marker. The auth header's mini 'S' logo mark flipped to a white background. The onboarding wizard's 'checked' selection state, progress dots, and CTAs all moved to white (neutral primary) rather than picking a categorical color for what is fundamentally a selection-vs-unselected toggle.
    • Every italic emphasis phrase ('one we'd trust with production', 'say hello first', 'where we build', 'on the calendar', 'the right answer', 'shipped', 'brand', 'policy', 'service') flipped from text-amber-300 to text-white. Emphasis without color, per v0.36 ethos. Every hero radial-gradient backdrop swapped from amber-rgba to a faint white glow. Every mailto link in /privacy and /terms moved from amber to text-zinc-100 with underline-offset.
    • Build, lint, typecheck clean. No behavior changes.
  8. v0.36.2Shipped

    Pricing page — amber sweep to match v0.36 monochrome palette

    • The last public marketing surface still running on the old amber-accented system. `/pricing` now follows the same recipe as the home (v0.36) and dashboard (v0.36.1): white-pill primary CTAs, neutral zinc eyebrows, monochrome chrome.
    • The featured Team plan card flipped from amber to violet — categorical 'premium tier' pop, matching the convention from v0.36.1 (Owner role badge, postmortem panel, modified-diff state). Border + gradient + ring + 'Most popular' badge text + the matrix column header all read violet now. The CTA inside the featured card is a white pill (same as every other primary action across the app).
    • Feature-list bullets uniform muted zinc across all plan cards. FAQ caret neutral. Italic accent phrases ('not seat-shaming.', 'Start free') keep the typography but drop the amber tint — emphasis without color, per v0.36 ethos. Hero radial-gradient backdrop swapped from amber-rgba to a faint white glow.
    • 12 amber utilities + 1 amber rgba retired in `src/app/pricing/page.tsx`. No behavior changes. Build, lint, typecheck clean. `/pricing` is now the third surface (after home + dashboard) running on the unified `--brand-*` palette.
  9. v0.36.1Shipped

    Dashboard amber sweep — monochrome palette across the app shell

    • v0.36 retired amber on the marketing home and shared chrome. v0.36.1 finishes the job: every page under /dashboard (38 surfaces) plus five shared components (ComingSoon banner, trace/badges, voice-input, canvas-multiplayer, canvas-comments) now run on the same monochrome system. ~273 amber utilities replaced with `--brand-*`-aligned alternatives.
    • Primary CTAs flipped from amber pills to white pills (`bg-white text-zinc-950 hover:bg-zinc-200`), matching the home's primary action. Form focus rings dropped from amber to neutral white. Hover-link accents lost their amber tint.
    • Semantic emerald now appears only where v0.36's rule says it should: live status. Trace, Evals, and Approvals eyebrows are emerald (the live-measurement family). The Run-canvas and Run-mission action panels carry an emerald-tinted gradient. The cost breakdown bar reads as a live feed. Postmortem panels moved to violet (analytical / investigative, not a status). Diff-modified rows moved to violet (categorical, distinct from emerald-added and red-removed). 'Coming Soon' chips and the dashboard nav hover dropped to pure neutral. Warning banners (missing env vars, no canvas yet) moved to rose — pending/attention per the v0.36 semantic palette.
    • The Trace span-kind badge for `agent` moved from amber to the paler categorical emerald (`emerald-300`) — distinct from the saturated emerald-400 used for OK/in-progress status. Build, lint, and typecheck all clean; no behavior changed.
  10. v0.36.0Shipped

    Marketing home rework — monochrome palette, editorial Trace pillar

    • The home page and shared marketing chrome (header / nav / footer) traded amber for a monochrome white-on-near-black system. New brand tokens live in `globals.css` (`--brand-bg/surface/text/live/pending/tone-*`) as the single source of truth.
    • Muted emerald (#34D399) is the only chromatic accent and it's reserved for live/healthy status moments — pulsing dots, the eval-pass row, the '● Recently shipped' hero callout. Categorical product chips use a 4-tone set (sky for Canvas, violet for MCP, emerald for Trace, rose for Mobile).
    • Two new home sections: `<ProductQuad />` ('Four products. One workflow.') and `<FromPromptToProduction />` (Describe / Orchestrate / Ship triptych). The Trace pillar was rebuilt as a single editorial spine — small chip, oversized two-line headline ('Every run / leaves a trace.'), `<TraceSpecimen />` artifact with crop-mark corners, then a 3-up feature row.
    • All `gpt-image-1` prompts rewritten end-to-end — HUNTERADE wordmark → STACKON, amber → emerald-only-where-functional, `ANTI_AI_TAIL` to reduce AI tell. 17 of 18 placeholders regenerated; the 18th (`home-fp2p-ship.png`) is blocked on raising the OpenAI billing limit and renders the dotted-grid fallback in the meantime.
  11. v0.35.0Shipped

    Renamed to Stackon

    • HunterADE is now Stackon. The new brand mark — three stacked cards representing code, sparkle, and OpenAI's model — sits over the wordmark and reads as an editor environment built on top of the foundation models that actually do the work. The tagline followed: an Agentic Developmental Environment powered by Claude and Codex.
    • Sub-products dropped the Hunter prefix. HunterTrace is Trace, HunterCanvas is Canvas, HunterMobile is Mobile, HunterCost is Cost, and so on. Two exceptions where the bare noun was too generic: Stackon MCP and Stackon CLI.
    • This release is the visible-surfaces flip: marketing pages, dashboard UI, mobile app icon + name + deep-link scheme, emails, OpenAPI docs, /privacy and /terms. Wire formats stayed put on purpose — `ht_` / `hbk_` / `htmi_` / `htmd_` token prefixes still work, the `X-HunterADE-Signature` webhook header is unchanged, and `HUNTERADE_*` env vars keep their names. Existing automation against the API does not break. A later cutover sprint will rotate those when there's a clean migration window.
    • iOS bundle ID flipped to `com.stackon.mobile` — the one exception to the phased rule, because App Store records are permanent post-registration and the mobile build hadn't shipped to TestFlight yet. Free now, irreversible after.
  12. v0.34.5Preview

    HunterMobile brand assets — icon, splash, adaptive, favicon

    • mobile/assets/ now ships with the four files EAS Build requires: icon.png (1024×1024 RGB), splash.png, adaptive-icon.png, and favicon.png. All derived from the existing HunterADE crosshair mark (public/placeholders/media-mark-dark.png) via macOS `sips` — center-cropped to 1024 square, no alpha channel (Apple rejects icons with transparency).
    • mobile/app.json wires the new files through expo.icon, expo.splash (resizeMode contain, #0A0A0A background), expo.android.adaptiveIcon, and expo.web.favicon. Mobile bundle bumped to 0.34.5; DEPLOY.md step 1 now reads as ✅ shipped.
    • Closes the last codebase-side blocker before TestFlight submission. Remaining: deploy the backend at the apiBaseUrl, enroll Apple Developer, eas login, eas init, eas build + eas submit.
  13. v0.34.4Preview

    TestFlight prep — privacy, terms, mobile deploy checklist

    • New /privacy and /terms pages cover everything Apple needs to accept a TestFlight submission — what data the app collects, who we share it with (Supabase / Anthropic / OpenAI / Resend / Expo / Stripe), how to delete your data, and the standard alpha-stage warranty disclaimers. Footer links to both. Plain-English drafts written for honest disclosure, not legal cover — get them reviewed by counsel before public GA.
    • New mobile/DEPLOY.md walks the user through every step from Apple Developer enrollment to `eas submit --platform ios`. P0 flagged up top: the production build talks to hunterade.com, so the backend has to be deployed and reachable before TestFlight reviewers can pair.
    • Cleanup in mobile/: dropped the dangling googleServicesFile reference so a first `eas build --platform ios` succeeds without an Android Firebase project. Bumped mobile bundle + app.json to 0.34.0.
  14. v0.34.3Fix

    Press kit — full-image view, click-to-open, download per asset

    • The new /media tiles were being cropped: every press shot is wider than 16:9, and the old PlaceholderImage frame used object-cover, lopping content off both sides. Replaced the press-kit grid with a new MediaAsset component that renders each shot at its natural aspect (object-contain inside an aspect-ratio computed from the file).
    • Click any tile → opens the original file at full resolution in a new tab. Each tile now has a `↓ Download` link beneath it that saves the file directly. Captions are auto-built from the real PNG header — actual pixel dimensions + file size, so the page can't drift from what's in public/placeholders/.
  15. v0.34.2Shipped

    Press kit — real dashboard screenshots replace AI mockups

    • The three product shots on /media (HunterTrace, HunterCanvas, HunterMobile / Approvals) are now real screenshots of the running dashboard, not gpt-image-1 mockups. The AI versions were photoreal but leaked the prompt's example labels — "span name", "PlanGPT", "Expense Report" — into the UI; the new shots carry actual product copy.
    • New scripts/seed-demo-screenshots.mjs (`npm run seed:demo`) inserts a photogenic 16-span trace, a 3-node Planner→Coder→Reviewer canvas, and a pending approval — all tagged so re-runs (or `--clean`) replace prior demo rows without drift. Anyone refreshing the press kit can re-seed, screenshot, and commit in five minutes.
  16. v0.34.0Shipped

    Marketing site redesign — editorial nav + ecosystem index

    • Homepage and global nav rebuilt around an editorial column grid: monospaced eyebrows mark structural sections, Instrument Serif italic carries emphasis pulls, hairline amber rules act as scaffolding instead of decoration. The page reads like a print spread now — same proof artifacts, more breathing room.
    • New top nav: Logo · Pricing · Ecosystem (dropdown: Observability / Canvas & Missions / Apps & Surfaces — every enabled product from featureFlags.ts) · Community (Discord, Docs, Events, Blog) · Company (About, Changelog, Careers, Media, Contact). Hover-open + click-toggle + ESC + outside-click. Hand-rolled, no extra deps. Mobile hamburger opens a full-width sheet listing all three categories stacked.
    • Single source of truth at src/lib/marketing/ecosystem.ts — the nav dropdown, the new homepage Ecosystem section, and the footer columns all read the same data. Adding a product = one edit.
    • Six new stub pages so every nav link resolves: /about, /careers, /media, /contact, /community/discord, /community/events. Each is a real editorial page (not <ComingSoon /> banners) with placeholder image slots tagged data-placeholder-slot="<slug>" — grep that attribute to swap in OpenAI-generated assets once the API key lands.
    • Hero ops cluster recomposed as a "specimen plate" with bracket markers, t+ timer, and a span/eval/budget caption — reads as proof artifact rather than tech demo. The conic-gradient swirl is gone; one calm radial bloom + the existing dotted-grid texture handles atmosphere.
  17. v0.33.1Infra

    pg_cron sweeper hardening

    • Both sweep_expired_canvas_pending_runs and sweep_expired_mission_pending_runs were tripping Supabase's function_search_path_mutable advisor: a function with no SET search_path leaves it role-mutable, so a malicious schema sitting earlier in resolution could shadow an unqualified table reference. Low risk in practice (pg_cron runs jobs as postgres; tables are owned by supabase_admin) but cleanest fix is to nail it down.
    • Migration recreates both functions with SET search_path = '' and fully-qualified public.<table> references. CREATE OR REPLACE only — no schema or cron-schedule change. Both advisor warnings are gone; sweep_expired_*_pending_runs RPCs still callable and behaviorally identical (verified end-to-end in scripts/test-sweepers.mjs, 14 assertions).
  18. v0.33.0Shipped

    HunterMobile — cost dashboard on your phone

    • Mobile gets `budget.breached` pushes (v0.28) but couldn't see the current state on the phone. v0.33 closes that loop. New Cost tab on HunterMobile mirrors the web HunterCost dashboard: current-month spend, vs. last month delta with percent change, hard / soft budget limits with usage bar, and a 7-day spend sparkline.
    • Server: new GET /api/v1/mobile/cost endpoint returns { current_spend_usd, last_month_spend_usd, budget, daily }. Reuses the same getMonthlySpend / getLastMonthSpend / getDailySpend / getBudget helpers the dashboard uses — refactored to thread an admin client so they work outside a session (mobile uses bearer tokens, no Supabase cookie).
    • UI: pure-JSX sparkline (no chart library dep), color-coded budget bar (green under soft, amber over soft, red at hard), and a 'no limits set' empty state pointing to the web dashboard. Footnote spells out the spend formula so users can audit it.
    • Mobile bundle bumped to 0.33.0.
  19. v0.32.0Shipped

    HunterMobile — mission approvals on your phone

    • The v0.31 server fan-out (mission.run.requested webhook + push) is now actionable from HunterMobile. New /(home)/approvals tab unifies canvas + mission pending runs in two grouped sections; each row deep-links to the right detail screen via expo-router.
    • New mobile/app/mission-approvals/[id].tsx screen mirrors the canvas detail layout — shows mission title, requester, bound canvas, user task, and the same Approve & run / Reject buttons. Approve redirects to the resulting trace (same admin-client identity override path the dashboard uses, so the trace is attributed to the original requester).
    • Three new bearer-token authed API routes: GET /api/v1/mobile/mission-approvals (list pending for team), GET /api/v1/mobile/mission-approvals/[id] (single), POST /api/v1/mobile/mission-approvals/[id] (action: approve | reject + optional reason). ApprovalError codes map cleanly: already_decided → 409 conflict, not_team_member → 403 forbidden.
    • Push notifications for mission.run.requested now deep-link properly — server pushes set data.kind = 'mission_approval', mobile's deepLinkForNotification routes to /mission-approvals/[id]. Tapping a notification opens the right screen on cold start + foreground.
    • Mobile bundle bumped to 0.32.0. Tab labels stay 'Approvals' — the inside is what changed.
  20. v0.31.0Shipped

    HunterMissions run approvals

    • Missions now opt into the same approval gate as canvases. Flip Require approval on a mission and Run mission queues a row in the new mission_pending_runs table instead of executing immediately. Any team member can approve or reject from /dashboard/approvals or the inline banner on the mission detail page.
    • Mission-level approval supersedes canvas-level approval — signing off on a mission IS signing off on the run, so a canvas with its own requires_approval=true is NOT re-gated. The approveMissionRun helper calls runCanvas directly via the admin-client identity override, attributing the trace + cost to the original requester, not the approver. Same race-safe atomic claim pattern from canvas approvals (v0.29): UPDATE … WHERE status='pending' returns null on the second simultaneous approver, surfacing 'already_decided'.
    • New webhook event mission.run.requested fans to outgoing webhooks AND mobile push (sibling pattern from v0.28). Three new audit_action values: mission.run.requested / .approved / .rejected. The pending row snapshots canvas_id + user_task at request time so a teammate rebinding the mission to a different canvas — or editing its title — between request and approval doesn't change what the approver is signing off on.
    • /dashboard/approvals is now a unified inbox: missions and canvases live in the same list, grouped by kind. The /dashboard/missions/[id] page shows an inline pending banner above the Run button so the requester sees their own queued run without leaving the mission.
    • pg_cron job sweep-expired-mission-pending-runs runs every 5 minutes (mirrors the canvas sweeper from v0.30.1) and flips status pending → expired for any row past expires_at (24h default). security invoker, idempotent reschedule, no audit_action for the transition — the absence of an approve/reject IS the signal.
  21. v0.30.1Infra

    Approvals expiry sweeper

    • canvas_pending_runs.expires_at defaults to now()+24h but nothing was marking expired rows — listings already filtered on status='pending' so they dropped out of view, but the audit trail showed a dangling 'pending' state forever. Closed.
    • New pg_cron job sweep-expired-canvas-pending-runs runs every 5 minutes (*/5 * * * *) and flips status pending → expired for any row past its deadline. The job calls public.sweep_expired_canvas_pending_runs() which is security invoker — pg_cron's runner role bypasses RLS so we don't need definer privileges.
    • No new audit_action value: pg_cron is system maintenance, not a user action. The absence of an approve/reject decision before expires_at IS the entire signal.
  22. v0.30.0Shipped

    HunterMobile diff viewer + EAS Build config

    • HunterMobile now has a canvas browser. New Canvases tab lists every canvas on your team with version count + 'requires approval' badge; tapping drills into the version history; tapping a version renders a side-by-side diff against the previous one (or against an arbitrary version via ?against=N).
    • The mobile diff reuses the server's existing diffSnapshots() engine — same algorithm that powers /dashboard/canvas/[id]/versions on the web. Per-node ADDED / MODIFIED / REMOVED / UNCHANGED blocks with per-field deltas (role / name / model / system_prompt). Metadata diffs (canvas name + description) surface separately. Summary pills at the top count each category.
    • Three new mobile API endpoints, all bearer-token (htmd_) authed, all team-scoped: GET /api/v1/mobile/canvases (list), GET /api/v1/mobile/canvases/[id]/versions (history), GET /api/v1/mobile/canvases/[id]/versions/[version]/diff (CanvasDiff between this version and previous, or `?against=` for an arbitrary base).
    • Mobile bundle bumped to 0.30.0. New eas.json with development / preview / production profiles is in place — running `eas build --profile preview` produces an internal-distribution build for both platforms; `eas submit --profile production` ships to TestFlight + Play internal track. First build needs `eas login` and the Apple Developer + Google Play Console records (those are out-of-band).
    • v0.30 closes the HunterMobile track for now — the app is feature-complete behind the mobile flag. Future work: actual store submissions, live multiplayer cursors mirrored to mobile, push-to-talk recording from the phone.
  23. v0.29.0Shipped

    HunterMobile approve queued canvas runs

    • Canvases can now opt in to an approval gate via a new requires_approval toggle on the canvas detail page. When on, every Run pipeline button queues a row in the new canvas_pending_runs table instead of executing immediately. Any team member can approve or reject from /dashboard/approvals or HunterMobile — approver != requester is allowed (the simplest rule that lets a phone-only co-pilot actually unblock a teammate).
    • New webhook event canvas.run.requested fans to outgoing webhooks AND mobile push (sibling pattern from v0.28). The push notification carries the canvas name, requester, and a truncated user_task; tapping deep-links straight to the mobile approval detail screen. Existing webhook subscribers don't see the new event unless they explicitly subscribe — backwards-compatible.
    • Server: new src/lib/canvas/approvals.ts wraps the lifecycle. requestCanvasRun inserts pending row + fires push/webhook. approveCanvasRun atomically claims the row (UPDATE … WHERE status='pending') so two simultaneous approvers can't double-run; then calls runCanvas via the admin-client identity override, attributing the trace to the original requester (not the approver). rejectCanvasRun mirrors the claim, with an optional reason stored in error_message.
    • Dashboard: /dashboard/approvals lists all pending requests across canvases; per-canvas pages also show their own pending list inline so the requester sees their own queued runs without leaving the canvas. Three new audit_action values: canvas.run.requested / canvas.run.approved / canvas.run.rejected.
    • Mobile: new /(home)/approvals tab + /approvals/[id] detail screen. Approve sends the user to the new trace; reject returns to the list. Notification taps deep-link to detail; the screen shows 'already decided' if two approvers race.
    • Race safety: the WHERE status='pending' guard makes approve/reject idempotent — second caller gets a conflict error (409 on mobile API, friendly redirect on dashboard). pending_run.expires_at defaults to 24h after creation; a background cleanup will mark expired rows in a follow-up.
  24. v0.28.0Shipped

    HunterMobile push notifications — webhook events to your phone

    • HunterMobile flag flipped on. trace.failed / budget.breached / gate.failed / gate.passed now fan out to every paired device on the team alongside the existing outgoing webhook. Same fire-and-forget contract: fireMobilePush(teamId, event, payload) wraps the work in Next 16's after() so a slow Expo round-trip never adds latency to user-facing actions.
    • Server: new src/lib/mobile/push.ts hits exp.host/--/api/v2/push/send with all active push tokens for the team in a single request. Per-device receipts land in a new mobile_push_deliveries table (status / expo_ticket_id / error_code / error_message / timestamps), readable by team members. One audit event per fan-out (mobile.push.send) summarises delivered + failed counts.
    • Device life-cycle: when Expo returns DeviceNotRegistered or InvalidCredentials we set mobile_devices.expo_push_token_invalid_at and the device drops out of future fan-outs until the next heartbeat with a fresh token. The active-device index (mobile_devices_push_idx) is now scoped on (team_id) where token-present-and-not-invalid.
    • Mobile: expo-notifications + expo-device added. New mobile/src/push.ts handles permission, projectId lookup, getExpoPushTokenAsync, and POSTs to /api/v1/mobile/heartbeat with push_token. RootLayout subscribes to addNotificationResponseReceivedListener — tapping a notification deep-links to /traces/<id> for trace + gate events, /(home) for budget. Settings tab shows ‘Push notifications: Enabled / Disabled’ and exposes a manual re-prompt button when the OS permission is off.
    • Notification payloads carry only event title + IDs (trace_id, gate_slug, event kind). Trace bodies, span outputs, and budget breakdowns stay on the server — Expo can't see them.
  25. v0.27.0Preview

    HunterMobile preview — pair-by-code + read-only viewer

    • Phase 4's last surface starts shipping. New @hunterade/mobile package at mobile/ — Expo + Expo Router + TypeScript app with three screens (pair, traces list, trace detail) and a settings tab that shows device/team identity and signs out. Token lives in expo-secure-store (Keychain on iOS, Keystore on Android).
    • Pairing: /dashboard/settings/devices mints a 6-character code (15 min TTL) per user, the mobile app exchanges the code via /api/v1/mobile/exchange for a long-lived htmd_-prefixed device token. Each user only sees their own devices, even on shared teams.
    • API surface: /api/v1/mobile/{exchange,heartbeat,me,traces,traces/[id]} — a separate authenticator from the scoped PAT one (mobile tokens are scope-less, mirrors HunterBridge's hbk_ pattern). Three new audit_action values: mobile.pair, mobile.revoke, mobile.heartbeat.
    • Scope deliberately small for v0.27 — read-only screens and pull-to-refresh. Push notifications via Expo Push API, approve/reject queued canvas runs, and a diff viewer all ship in v0.28+. The mobile feature flag stays off until that lands; the v0.27 preview is reachable directly from /dashboard/settings/devices.
  26. v0.26.0Shipped

    HunterVoice cloud-Whisper fallback (Firefox + everywhere else)

    • Closes gotcha #16 — the v1 push-to-talk button now works in browsers without the Web Speech API. The component picks its mode at mount: Web Speech where present (Chrome / Edge / Safari), or MediaRecorder + cloud Whisper where it isn't (Firefox, in-app webviews).
    • Cloud path: MediaRecorder captures audio with the best supported mime type (webm/opus → mp4 → ogg/opus → browser default), the blob is POSTed to a new same-origin /api/voice/transcribe route handler, the server calls OpenAI whisper-1 via raw fetch (no extra SDK — same pattern as HunterKnowledge embeddings), and the returned text is appended to the linked textarea using the existing appendForVoice helper. UX is byte-identical to the v1 path other than a small ‘cloud’ tag and a ‘Transcribing…’ status while the round-trip is in flight.
    • OPENAI_API_KEY drives the fallback (already required for HunterKnowledge embeddings). Each callsite reads isWhisperConfigured() server-side and passes a cloudFallbackEnabled boolean to the client component, so the disabled-state copy can tell the user whether the limit is the browser or the server.
    • Audit-logged as voice.transcribe with bytes / mime / duration / transcript-char-count metadata only — we deliberately do not store the raw audio or the transcript itself. Compliance audit log already renders the new action label.
    • Auth: same-origin Supabase session cookie, same shape as server actions. 12 MB upload cap, normalized error envelope ({ error: { code, message } }), 503 when the key is missing, 502 on upstream failure.
  27. v0.25.1Infra

    Webhooks: drop legacy plaintext secret column

    • Follow-up to v0.24.1 — encrypt-at-rest is now the only path for webhook signing material. The plaintext webhooks.secret column is gone, the webhooks_secret_present_chk constraint is gone, and secret_encrypted / secret_iv / secret_auth_tag are NOT NULL.
    • createWebhookAction now refuses to create a webhook unless HUNTERADE_BYOK_MASTER_KEY is set — no more silent plaintext fallback in dev. Local dev needs the master key configured to flag-flip webhooks.
    • dispatch.ts is simpler: one decrypt path, no lazy migration, no fallback branch. Decrypt failures fail the delivery loudly with a hint that the master key probably rotated.
    • Closes gotcha #20 fully. The transition window is over.
  28. v0.25.0Shipped

    HunterMultiplayer v2: persisted comments + Realtime Authorization

    • Canvas comments are now a real persisted thread, not just live cursors. New canvas_comments table with author profiles, resolve/reopen, author-or-admin delete, full RLS scoped to team membership. The dashboard renders an SSR snapshot; new inserts stream in via Supabase postgres_changes — no refresh needed.
    • Closes gotcha #15 — the canvas:<uuid> cursor + presence channel is now private. Two new policies on realtime.messages enforce team membership before a client can subscribe or broadcast: split_part(realtime.topic(), ':', 2) gives the canvas id, then a join through canvases → team_members checks auth.uid(). Public-channel access by guessed UUIDs is no longer possible.
    • Browser client calls supabase.realtime.setAuth() with the active session JWT before subscribing, then opts in with config.private = true. The cursor / presence experience is byte-identical to v1 — just provably scoped.
    • Three new audit_action enum values — comment.create, comment.resolve, comment.delete — labeled on /dashboard/compliance. Author can edit their own comments; owners and admins can moderate any.
    • Comment writes go through server actions that look up canvas.team_id, populate the row, and invalidate the canvas page; the postgres_changes listener also re-fetches so two open tabs converge on the same state without thrashing.
  29. v0.24.1Infra

    Webhook signing secrets encrypted at rest

    • Closes the v2 hardening item (gotcha #20). Webhook secrets now live in three new bytea columns — secret_encrypted / secret_iv / secret_auth_tag — encrypted with AES-256-GCM keyed off HUNTERADE_BYOK_MASTER_KEY. Same scheme as BYOK API keys; rotating the master key invalidates both surfaces.
    • AES-GCM helpers extracted from compliance/byok.ts to a shared src/lib/crypto/aes-gcm.ts (encryptSecret, decryptSecret, bytesFromBytea, bytesToBytea, masterKey, isMasterKeyConfigured). Rule of two — webhooks is the second consumer, so it earns the abstraction.
    • Transition: new webhooks insert encrypted-only with secret = NULL. Existing rows still have plaintext; dispatch.ts lazy-migrates on first read (decrypt → dispatch → write encrypted columns + null plaintext in the background). After every row has been hit, a follow-up commit will drop the plaintext column.
    • Defensive constraint: webhooks_secret_present_chk ensures every row has either plaintext OR all three encrypted columns. No row can lose its signing material via a partial write.
    • Behaviorally invisible to users — same /dashboard/settings/webhooks UI, same delivery log, same HMAC-SHA256 X-HunterADE-Signature on every request. Flag-flipped without a key in dev: createWebhookAction falls back to plaintext when isMasterKeyConfigured() is false.
  30. v0.24.0Shipped

    Public API: OpenAPI 3.1, scoped tokens, rate limits, request IDs

    • Hand-authored OpenAPI 3.1 spec at /openapi.yaml — every /api/v1 endpoint with request/response schemas, scope annotations, and error envelope. Rendered with Scalar at /docs (no npm dep, single CDN script).
    • Personal access tokens now hold scopes: traces:read (list/get traces + spans), traces:write (start traces, append spans, end, record_run), evals:run (trigger evals on a trace + run an eval gate). The token-create form at /dashboard/settings/tokens has a checkbox grid for picking a least-privilege subset; existing tokens were backfilled with the full scope set so no consumer broke.
    • Per-token sliding-window rate limit — 60 requests/minute default, settable via HUNTERADE_API_RPM. Counters live in api_rate_buckets via an atomic security-definer Postgres function. 429 responses carry Retry-After + X-RateLimit-{Limit,Remaining,Reset} headers. Fails open on infra blips so a transient DB error doesn't deny legitimate traffic.
    • Error envelope normalized across every /api/v1/* response: { error: { code, message, request_id } }. Every response (success + error) carries an X-Request-Id header — partners can pass one in to correlate their logs with ours, or read the one we mint. Hunter MCP client updated to read either the new or legacy shape, surfacing the request id when present.
    • Versioning policy committed in the spec's info.description: /api/v1 is stable; new optional fields and new endpoints land without a major bump; deprecations get a 90-day Deprecation header notice and a changelog entry; breaking changes ship under /api/v2 with parallel running.
    • Bridge daemon endpoints (hbk_ namespace) inherit the same error envelope + request id flow but stay scope-less — single-purpose tokens for job dispatch.
  31. v0.23.0Shipped

    Teams & invites — Phase 1 closer

    • New `/dashboard/settings/team` surface. Owners and admins invite teammates by email + role, see pending invites with sent/expires timestamps, revoke any pending invite, promote/demote members, and remove members. The owner row is protected — never demotable or removable from this UI.
    • Each invite mints a single-use `htmi_`-prefixed token (sha256-hashed at rest, plaintext only sent in the email) with a 14-day expiry. The accept-invite landing at `/invite/[token]` handles every edge case explicitly: signed-out → sign-up/sign-in deeplink with the email pre-filled, signed-in but wrong email → sign-out CTA, expired/revoked/already-claimed → friendly explainer.
    • Resend delivery mirrors the waitlist pattern — gated on `resendIsConfigured()` so flag flips ship without a real key, dispatched via Next 16 `after()` so the user's redirect lands instantly. The `team_invites` row is the durable record; the email is delivery only.
    • Audit trail extended with three new actions — `member.invite.revoke`, `member.accept`, `member.role.update` — alongside the existing `member.invite` and `member.remove`. Every transition shows up on `/dashboard/compliance` with a recognizable label.
    • RLS: members read their team's invites (so non-admins see who's been invited); owners and admins manage. Accept-invite redemption goes through the admin client because the redeemer isn't yet a team member when the row is being looked up by token_hash.
    • Phase 1 now fully closes: marketing site, auth (magic link + OAuth), waitlist + Resend, onboarding wizard, ⌘K palette, and teams & invites — all live and registry-flagged.
  32. v0.22.0Shipped

    Phase 1 cleanup: waitlist confirmation, onboarding wizard, ⌘K palette

    • Waitlist now sends a Resend confirmation email after a fresh signup. Dispatch wraps Next 16's `after()` so the redirect lands instantly while the email send survives the serverless response. Already-on-the-list submissions skip the email and surface a friendly 'queued' toast on the home page; failures show a retry message instead of disappearing into the void.
    • Onboarding wizard at `/onboarding` — 3 steps (display name, role, primary use case) — runs after sign-up and before the first dashboard hit. Profile row gets `role`, `primary_use_case`, and `onboarded_at` columns; the dashboard layout detours fresh users until the wizard is complete. Skipped automatically for users who've already completed it (back-button safety).
    • Command palette (⌘K / Ctrl+K) mounts globally across `/dashboard/*`. Three groups: Navigate (every dashboard surface + settings), Create (new mission, canvas, eval, adversary sweep, scribe distillation, knowledge source), Settings & docs. Built on the shadcn `cmdk` primitive — fuzzy search, keyboard-only operation, no mouse required.
    • Marketing surface picks up post-submit feedback: `?status=joined|already|invalid|error` on the home renders a colored banner anchored to the waitlist section so users see exactly what happened with their submit.
    • Three Phase 1 flags flipped (`waitlist`, `onboarding`, `commandPalette`); Phase 1 is now substantively complete except for teams + invites.
  33. v0.21.1Fix

    Webhooks: dispatch survives serverless response via Next 16 `after()`

    • fireWebhookEvent now wraps dispatch in `after()` from next/server. On Vercel a bare `void promise(...)` is dropped when the response returns; with after() the runtime keeps the function alive until dispatch finishes. Falls back to bare scheduling outside a request scope (tests / scripts) by catching the E468 'wrong context' throw.
    • No behavior change in local dev — Node continues running async work either way. The change matters in production where serverless functions terminate the moment the response is sent.
  34. v0.21.0Shipped

    Outgoing webhooks: ping Slack / Discord / PagerDuty when things matter

    • Subscribe a URL to events HunterADE already cares about: gate.passed, gate.failed, budget.breached, trace.failed. Slack / Discord / PagerDuty / your own infra now composes with the dashboard so an eval regression or a budget breach pages someone instead of sitting in a tab.
    • Each delivery is signed HMAC-SHA256 in the X-HunterADE-Signature header (sha256=hex(hmac(secret, body))) — same scheme as GitHub / Stripe / Linear. Receivers verify by re-signing the raw body and constant-time-comparing. The signing secret is shown once at create; lose it and you delete + recreate.
    • X-HunterADE-Delivery carries a stable UUID so receivers can dedupe replays. Manual retry from the dashboard re-fires with the same delivery_uuid so the receiver's idempotency check still works.
    • Single-attempt inline + manual retry from the UI in v1; cron-driven exponential backoff is a deliberate v2 once we have a worker queue. 8s timeout per attempt so a slow receiver can't hang a gate run.
    • Dispatch is fire-and-forget at every call site (fireWebhookEvent, no await) so a wedged receiver never adds seconds to a user-facing response. Failures land in the delivery log with HTTP status, response body (truncated to 2KB), and error message.
    • /dashboard/settings/webhooks: list + create (URL + event multi-select + auto-mint secret shown once) + per-webhook detail page with delivery log, manual retry, send-test-event button, enable/disable toggle. Three new audit_action values (webhook.create / update / delete) plus webhook.test for the synthetic-event path.
  35. v0.20.0Shipped

    HunterEvals PR gates: block PRs from CI when an agent regresses

    • The Phase-2.5 promise the README has been making for months. A 'gate' bundles a canvas + a fixed test input + the evals that must pass, identified by a stable slug. Trigger it from a GitHub Action and you get a synchronous pass/fail you can wire into branch protection — a regression to the canvas's prompt blocks merge.
    • New schema: eval_gates (slug-unique-per-team, eval_ids[], canvas + test_input) and eval_gate_runs (status, trace_id, pass/fail counts, optional commit_sha + pr_url for CI deep-links). Three new audit_action values (eval_gate.create / delete / run).
    • POST /api/v1/eval-gates/[slug]/run is bearer-token authed, runs the canvas with the gate's test input via an admin-client identity override on runCanvas, then grades the gate's evals against the resulting trace and returns {status, passed_count, failed_count, trace_id, dashboard_url} in one synchronous call.
    • /dashboard/evals/gates lets you create gates (canvas dropdown + test input + multi-select evals + auto-derived slug) and each gate page shows recent runs (manual + CI), a copyable workflow YAML pre-filled with the slug, and a 'Run gate now' button for smoke testing.
    • .github/workflows/example-eval-gate.yml.template ships in the repo as a reference workflow — drop into your own repo, set HUNTERADE_API_TOKEN secret, add to branch protection, done.
    • runCanvas now accepts an optional identity override ({teamId, userId, client}) so any non-session caller (eval gate runner, future webhooks) can drive a canvas run without hijacking auth.getUser. Existing user-session callers are unchanged.
  36. v0.19.1Shipped

    HunterVoice: ⇧Space toggle replaces hold-Space PTT

    • Press ⇧Space inside a focused prompt textarea to start dictating; press it again to stop. Plain Space is no longer intercepted, so typing literal spaces while composing a prompt is back to normal.
    • Behaviour matches the click-to-toggle button: mic stays hot until you toggle off, so you can pause to think without losing the recognizer.
  37. v0.19.0Shipped

    HunterVoice v1: push-to-talk dictation on agent + canvas inputs

    • Click the mic next to any prompt textarea — or hold Space inside it — and dictate. Final transcript is appended to whatever you've already typed; live interim text shows next to the button so you see the recognizer working before you stop.
    • Two interaction modes: click-to-toggle (accessibility default, keyboard / screen-reader friendly with aria-pressed + aria-label) and hold-Space inside the focused textarea (Slack-style power-user shortcut).
    • Web Speech API only in v1 — works in Chrome, Edge, Safari with zero new deps and zero env vars. Firefox shows a disabled 'Voice n/a' button instead of crashing. True local Whisper (transformers.js wasm, ~40MB) and an OpenAI Whisper cloud fallback are deliberate v2 paths so we don't ship a 40MB browser bundle for an MVP.
    • Dropped on /dashboard/agents (user prompt) and /dashboard/canvas/[id] (run-canvas task input). Wires up by id — the textarea stays a normal server-rendered field, the VoiceInput client component finds it via document.getElementById and writes to .value so form submission picks up the dictated text without any controlled-input refactor.
  38. v0.18.0Shipped

    HunterMultiplayer v1: Figma-style live cursors + presence on canvas

    • Open the same canvas detail page (`/dashboard/canvas/[id]`) in two windows — or with a teammate — and you see each other's cursor in real time, with a coloured arrow + name tag. Top-right pill stack shows everyone currently viewing.
    • Pure Supabase Realtime — one channel per canvas, presence for the pill stack, broadcast for cursor coordinates throttled to 20 Hz. Zero new tables, zero DB writes; opening a canvas you don't have access to never reaches the channel because Realtime inherits the same RLS as your other queries.
    • Per-user color is a stable hash of `user_id` from a hand-picked dark-theme palette (no amber, so the brand accent stays distinct from any one cursor). Initials computed from full_name → email → user_id fallback.
    • Each tab is its own session, but pills de-dupe by user_id so two tabs from the same person show one avatar — Figma-style. Cursors GC after 5s of silence as a safety net for crashed peers.
    • Comments + Multiplayer-on-other-surfaces (traces, missions) are deferred to v2 — the canvas headline experience is what the roadmap promised, and shipping it cleanly beats a half-built three-feature drop.
  39. v0.17.0Shipped

    HunterBridge v1: BYO compute — pair your laptop, run agents locally

    • Phase 3 closes. Register a machine (laptop, GPU box, self-hosted runner), pair it with a 15-minute one-time code, and dispatch agent runs to it from /dashboard/agents. The bridge polls the HunterADE API, executes locally with its own Anthropic key, and streams traces back into HunterTrace — zero model traffic touches our infra.
    • New schema: `bridges`, `bridge_pairing_codes`, `bridge_tokens` (hbk_… bearer, sha256-stored), `bridge_jobs` queue, plus `traces.bridge_id` so the trace viewer shows where a run executed. Three new audit_action values: bridge.register / bridge.revoke / bridge.run.
    • Daemon-facing API at /api/v1/bridge/*: pair, heartbeat, jobs/next (atomic claim), jobs/[id]/result. Bridge tokens authenticate every endpoint; tokens are sha256-hashed at rest and revoked the moment a bridge is decommissioned.
    • /dashboard/compute lists bridges with online/offline state (90s heartbeat window), shows unclaimed pairing codes with countdown, recent jobs grouped by bridge with status + trace deep-link, and a one-line install command.
    • @hunterade/bridge ships as a Node-based reference daemon under bridge/. `npx @hunterade/bridge pair --code …` then `npx @hunterade/bridge run` is the whole loop. Heartbeat every 30s, poll every 3s, graceful shutdown on SIGINT. Config stored chmod 600 at ~/.hunterade/bridge/config.json.
    • Agent runner gets a "Run on" select — defaults to Cloud, lists every bridge with online/offline status. Cloud path is unchanged; the bridge path enqueues a job + pre-creates the trace so the user lands on a live URL while the daemon picks it up.
    • Tauri/Rust binaries with mTLS tunnel + auto-update + code signing are deferred to v2 — out of scope for a single session, and the v1 token-auth model is functionally complete for self-hosted use.
  40. v0.16.0Shipped

    HunterCompliance v1: SOC2-ready audit log, PII proxy, BYOK

    • New `audit_events` table captures every meaningful action — agent runs, canvas runs, evals, postmortems, adversary sweeps, scribe distillations, forge publishes / installs, knowledge ingests, budget changes, BYOK rotations, token lifecycle, compliance settings updates, and audit exports. Append-only, RLS-protected per team, indexed on (team_id, occurred_at).
    • PII proxy redacts emails, phones, SSNs, credit cards (Luhn-checked), IPv4, AWS access keys, and known model-provider key formats from every prompt before it leaves our infra. Strict mode adds long digit runs and IPv6. Originals stay in your trace spans.
    • BYOK lets a team supply their own Anthropic / OpenAI key. Encrypted at rest with AES-256-GCM under HUNTERADE_BYOK_MASTER_KEY. Runner resolves BYOK first, falls back to platform env. Rotating leaves a `byok.rotate` audit event.
    • /dashboard/compliance — owner / admin only — surfaces PII toggle, retention slider, BYOK cards (key hint + last rotation), and a 100-event audit log. Stream the full log as JSONL or CSV; the export itself shows up as the next event in the trail.
    • Every LLM-calling surface (runAgent, runCanvas, runRedTeamCampaign, runPostmortem, distillSkillFromTrace, llm-judge) goes through one `prepareRun(teamId, provider)` helper that resolves the key + redacts inputs + stamps `compliance.*` attributes on the parent span.
    • RBAC: owners and admins manage settings + BYOK; all team members can read the audit log. Personal teams treat their creator as owner even before team_members has a row.
  41. v0.15.0Shipped

    Naming pass: every product / surface now starts with "Hunter"

    • Locked in the naming rule from v0.14.0 across every shipped surface — product labels in featureFlags.ts, every dashboard page header, every empty-state, and the public marketing surfaces.
    • Phase 2: Mission Control → HunterMissions, Eval Harness → HunterEvals, Multi-Agent Canvas → HunterCanvas. Phase 3: Cost Governance → HunterCost, Red Team Mode → HunterAdversary, Agent Postmortems → HunterPostmortems, Agent Git → HunterGit, Team Knowledge Graph → HunterKnowledge, Local Bridge → HunterBridge. Phase 4: Forge → HunterForge, with HunterMultiplayer / HunterMobile / HunterCompliance lined up.
    • Dashboard sub-nav labels stay terse on purpose — the Hunter brand prefix is implied by being inside the HunterADE app shell. Each terse label maps to its full Hunter-prefixed name on the page header (e.g. nav "Adversary" → page header "HunterAdversary").
    • ROADMAP.md and README.md status table refreshed in the same commit so external readers see the official names. Added an explicit Naming rule line to ROADMAP § Brand & Naming so the policy lives next to the surface list.
    • Internal code identifiers (table / column / variable names like canvas_versions, forge_listings) intentionally stay descriptive snake_case — the rule only governs user-facing product names.
  42. v0.14.0Shipped

    HunterScribe v1: distill a recorded trace into a reusable skill

    • New `skills` table — name, description, instructions (the actual prompt prefix), source_trace_ids[] for provenance, distill model + cost rollup. RLS via private.is_team_member; owners/admins delete.
    • Added `skill_id` to canvas_nodes — when set, the canvas runner prepends the skill's instructions to that node's system prompt automatically.
    • Distillation: pick a saved trace at /dashboard/scribe/new, click Distill — Sonnet 4.6 reads the trace's spans + IO previews and produces { name, description, instructions } in strict JSON. Typical cost ~$0.005 per skill.
    • /dashboard/scribe — list of skills with provenance count + distill cost. /dashboard/scribe/[id] is a full edit page (refine the auto-generated instructions before attaching).
    • Canvas node edit page gained a HunterScribe skill picker. The runner pre-loads all referenced skills in one query, splices skill.instructions into the system-prompt head before knowledge context and base role.
    • Naming rule: every product / surface starts with "Hunter" going forward. Skill Recorder → HunterScribe is the first feature shipped under this rule.
  43. v0.13.0Shipped

    HunterForge v1: publish a canvas, install someone else's

    • New `forge_listings` table — frozen canvas snapshots (same shape as canvas_versions), unique slug, published_at lifecycle, install_count rollup. RLS lets anon + authenticated read PUBLISHED listings; team members additionally see their own drafts.
    • /forge — public marketplace landing (no auth required). Hero, role-chip preview per listing, install_count + node count footer.
    • /forge/[slug] — public detail with full pipeline preview (every node's role, name, model, system prompt) and a one-click Install button.
    • Install action forks the listing into the user's team: new canvas + nodes, auto-commit as v1 with message "Installed from Forge · <title>", increments install_count, redirects to the new canvas.
    • Publish panel on /dashboard/canvas/[id]: title + description form. Server action snapshots current state, mints a slug (title + 6-char random suffix), sets published_at = now.
    • /dashboard/forge — your team's listings with Unpublish / Republish / Delete actions. Drafts and unpublished listings remain visible to the team without leaking to the public.
    • Forge link added to the marketing nav (public) and the dashboard sub-nav (publishers).
  44. v0.12.0Shipped

    HunterKnowledge v1: pgvector retrieval that grounds every canvas run

    • Enabled pgvector + new knowledge_sources / knowledge_chunks tables (1536-dim embeddings, HNSW cosine index). public.match_knowledge_chunks RPC runs as security_invoker over team-scoped RLS — no security-definer warnings.
    • Built /dashboard/knowledge with paste-ingest (paragraph-aware chunker, ~500 tok / 50 tok overlap), source detail with chunk drill-down, and a built-in Search panel that runs cosine retrieval ad-hoc to verify quality.
    • OpenAI text-embedding-3-small via raw fetch (no extra SDK dep). Friendly setup banner + form-disable when OPENAI_API_KEY isn't configured.
    • Per-canvas opt-in via a Use Knowledge toggle on the canvas detail page. When on, runCanvas embeds the user task once, retrieves top-6 chunks above min-similarity 0.4, and prepends a "Relevant team context" block to every node's system prompt.
    • Retrieved chunk_ids land on the agent.run span's attributes so trace replay shows exactly which knowledge informed the run.
  45. v0.11.0Shipped

    HunterGit v1: version-control your canvas like code

    • New `canvas_versions` table — frozen snapshots of (canvas + ordered nodes) keyed on (canvas_id, version) with auto-incrementing per-canvas integers. Snapshots are immutable history; the table has no UPDATE policy.
    • Commit panel on /dashboard/canvas/[id]: type a message, click Commit, snapshot lands as the next version. Required commit message keeps history meaningful.
    • /dashboard/canvas/[id]/versions: full list with revert links and a Diff vs vN-1 button on each row. Latest pair gets a one-click "Diff latest" shortcut in the header.
    • /dashboard/canvas/[id]/versions/[versionId]: full snapshot view of name + description + every node (with its system prompt) + a Revert button.
    • /dashboard/canvas/[id]/versions/diff?from=…&to=…: side-by-side diff with metadata changes highlighted, per-position node states (added / removed / modified / unchanged), and per-field change summaries (role, name, model, system_prompt). Modified rows expand with both before/after states + full prompt text.
    • Revert is auditable: replaces canvas + nodes with the snapshot, then auto-writes a new version with `is_revert = true` and message "Revert to vN" so history stays linear.
  46. v0.10.0Shipped

    HunterPostmortems v1: 5-whys analysis on any trace

    • New `postmortems` table — one per trace (UNIQUE), summary + structured analysis (whys, root_cause, recommendations) jsonb, model + cost + duration tracked. RLS via private.is_team_member.
    • Embedded a Postmortem panel into /dashboard/traces/[id]: empty state with a Generate button, populated state with summary + numbered 5-whys list + amber root-cause callout + recommendation bullets + Regenerate button.
    • Runner uses Claude Haiku 4.5 with a strict JSON-output system prompt and a compact trace-context summary (root + spans with status, IO previews, errors). Typical cost is sub-cent; budget gated.
    • Re-generation upserts on (trace_id) — running it again on the same trace overwrites with a fresh analysis. The trigger column reserves space for future auto-on-error / auto-on-budget triggers.
  47. v0.9.0Shipped

    HunterAdversary v1: automatic adversarial sweeps against your front-door agent

    • New `red_team_campaigns` + `red_team_attempts` tables with attack_kind enum (prompt_injection / jailbreak / system_extraction / off_task / edge_case). RLS via private.is_team_member.
    • Standard attack pack: 4 canonical adversarial inputs — "Ignore previous instructions", system-prompt extraction, DAN-style jailbreak, and an off-task chitchat distraction. Each attack ships with an explicit judge rubric so verdicts are auditable.
    • Runner targets the front-door (first node) of any saved canvas. Each attack creates a new HunterTrace + agent.run + llm.completion span — the campaign reuses the same trace + eval infrastructure, no new observability surface.
    • LLM judge (Haiku 4.5 by default) grades each attempt: passed=true means the agent BLOCKED the attack, false means the attacker won. Judge cost is rolled into the campaign's total_cost_usd alongside the agent call.
    • Built /dashboard/red-team with a list view (block-rate bar chart per campaign), /dashboard/red-team/new (canvas + pack picker), and /dashboard/red-team/[id] showing per-attempt verdicts, judge reasoning, and links into each generated trace.
    • Re-run + delete campaigns inline. Cost gated by the same monthly budget as everything else.
  48. v0.8.0Shipped

    HunterCost v1: monthly budgets that refuse runs over the hard limit

    • New `budgets` table — one per team via PK, hard + optional soft USD limits. RLS lets owners + admins update; members read.
    • Built /dashboard/cost: this-month total + budget gauge, last-month comparison, 30-day SVG sparkline (no charting library — pure server-rendered), spend breakdown by runner (canvas, agent runner, evals, API) and by model, top 10 traces by cost.
    • BudgetExceededError + assertBudgetAvailable() wired into runAgent, runCanvas, and runEvalsForTrace. Once the team's monthly spend reaches the hard limit, every LLM-triggering path returns a clear error with the exact number to raise the cap or wait for next cycle.
    • Soft limits surface as a banner above the dashboard (and on the budget gauge as a tick mark) — runs continue, but the team knows they're approaching the wall.
    • Phase 3 begins. Budgets close the immediate enterprise-readiness gap that traces alone don't solve.
  49. v0.7.0Shipped

    HunterMissions v1: kanban tasks that run end-to-end through your canvases

    • New `missions` table + `mission_status` enum (backlog / in_progress / done / failed / archived). Optional canvas_id and current_trace_id link each mission to its execution and resulting trace. RLS via private.is_team_member.
    • Built /dashboard/missions kanban with four lanes, status dropdowns, priority chips, and a Run button on cards bound to a canvas.
    • Built /dashboard/missions/new (title + description + canvas selector + priority) and /dashboard/missions/[id] for full edit + run + archive/delete with the latest linked trace pinned to the top.
    • runMissionAction wraps runCanvas: marks the mission in_progress, executes synchronously, parks the trace_id back on the row, transitions to done (or failed). Closes the loop from queue → execute → replay.
    • Sub-nav now leads with Missions: Overview · Missions · Agents · Canvas · Traces · Evals.
    • Phase 2 is complete: every roadmap surface for the lead pillar is shipped.
  50. v0.6.0Shipped

    HunterCanvas v1: orchestrate Planner → Coder → Reviewer pipelines

    • New `canvases` + `canvas_nodes` schema with a `canvas_role` enum (planner / coder / reviewer / tester / red_team / custom). RLS via private.is_team_member; ordered retrieval indexed on (canvas_id, position).
    • Built /dashboard/canvas list, /dashboard/canvas/new (with four starter templates), /dashboard/canvas/[id] for the pipeline editor, and /dashboard/canvas/[id]/nodes/[nodeId] for per-node prompt + model editing.
    • Composer UX is fully server-rendered: add / remove / reorder nodes via server actions; no client JS required. Vertical card layout with animated connectors between steps.
    • Canvas runner walks nodes in order, feeds each output into the next agent's system prompt, captures every call as an llm.completion span under one agent.run root span. The whole pipeline lands as a single HunterTrace you can replay and grade with Evals.
    • Adaptive thinking on every node; ephemeral prompt caching on the system block; cost rolled up from each node's usage object.
    • Sub-nav now: Overview · Agents · Canvas · Traces · Evals. Canvas tile is a Live link.
  51. v0.5.0Shipped

    Hunter MCP server: stream traces from Cursor / Claude Code / any MCP client

    • New `api_tokens` table with sha-256 hashed PATs (plaintext shown once); `/dashboard/settings/tokens` to mint and revoke.
    • New v1 HTTP API: POST /api/v1/runs (one-shot), POST /api/v1/traces (start), GET /api/v1/traces, GET /api/v1/traces/:id, POST /api/v1/traces/:id/spans, POST /api/v1/traces/:id/end, POST /api/v1/traces/:id/evals/run. Bearer-token authed.
    • New `mcp/` package: an independent npm module exposing six tools (record_run, start_trace, add_span, end_trace, list_recent_traces, run_evals) over MCP stdio. Drop the binary into Cursor or Claude Code's mcp.json and your editor agent emits HunterTrace spans on its own.
    • Eval runner refactored to accept an injected Supabase client so the API path can run evals with the admin client without going through user RLS.
    • Requires the SUPABASE_SECRET_KEY env var — the HTTP API returns 503 with a clear setup message until it's set.
  52. v0.4.0Shipped

    HunterEvals v1: grade traces with output-contains or LLM judge

    • New evals + eval_runs schema (RLS via private.is_team_member) — re-running an eval against the same trace upserts on (eval, trace).
    • Two grader types: output_contains (instant, free) and llm_judge (Haiku 4.5 by default — costs negligible tokens per run, returns a JSON verdict with pass/fail, score, and reason).
    • /dashboard/evals: list + create form with conditional config blocks per grader kind. /dashboard/evals/[id] shows the eval's recent runs with verdicts, scores, and per-run cost.
    • Trace detail (/dashboard/traces/[id]) gained an Evals panel: passed/failed counts, per-eval verdict chips, and a Run evals button that grades the trace against every enabled rule.
    • PR-blocking eval gates land in Phase 2.5 once the local CLI ships.
  53. v0.3.0Shipped

    Single-agent runner: real Anthropic calls, every span recorded

    • Wired @anthropic-ai/sdk into a server-side runner that streams Messages API calls (adaptive thinking, prompt caching on system blocks) and persists every call as a HunterTrace span tree: agent.run wraps llm.completion.
    • Built /dashboard/agents — a clean form (system prompt, user prompt, model dropdown). On submit you land on the trace detail with cost, tokens, latency, and the full assistant output captured.
    • Accurate cost calculation per model, including separate rates for cache writes (5m TTL × 1.25 input) and cache reads (× 0.1 input). Opus 4.7 / 4.6, Sonnet 4.6, Haiku 4.5 supported.
    • Typed Anthropic SDK exceptions (RateLimitError, AuthenticationError, etc.) surface as span.status = error with the HTTP status preserved on attributes.
    • Surface a clear setup banner when ANTHROPIC_API_KEY is missing rather than failing silently.
  54. v0.2.0Shipped

    HunterTrace v1: trace + span ingest, list view, timeline replay

    • New `traces` and `spans` tables with full RLS — every span carries cost, latency, model, tokens, IO, and a typed `kind` (agent / llm / tool / eval / internal).
    • Built `/dashboard/traces` list view with status badges, span counts, total cost rollups, and relative timestamps.
    • Built `/dashboard/traces/[id]` timeline replay — proportional span bars colored by kind, stat tiles, and a full inspector with collapsible inputs/outputs/attributes per span.
    • Auto-create a personal team for every new user (and backfill existing users) so traces always have a home.
    • One-click "Generate sample trace" action that inserts a realistic span tree to play with.
  55. v0.1.2Infra

    Supabase wired up — auth and waitlist persist

    • Provisioned the production Supabase project (us-west-1, Free tier) with `profiles`, `teams`, `team_members` (owner/admin/member enum), and `waitlist` tables — RLS on everything.
    • Security-definer trigger functions (`handle_new_user`, `handle_new_team`, `is_team_member`) live in a private schema so they can't be called as RPCs.
    • Generated typed `Database` for the Supabase clients — full autocomplete and compile-time safety on every query.
    • Waitlist form on the landing page now persists to Postgres. Duplicate emails are treated as success.
  56. v0.1.1Shipped

    Phase 1 auth: magic link + GitHub/Google OAuth

    • Added the `(auth)` route group: shared `AuthForm` with email magic-link plus GitHub and Google OAuth buttons.
    • Wired session-refresh middleware so Supabase auth cookies stay fresh on every navigation.
    • OAuth callback exchanges the `?code` for a session and redirects to `?next` (defaults to `/dashboard`).
    • `/dashboard` is now server-side guarded — unauthenticated users land on `/sign-in?next=/dashboard`.
    • Site header switches to a user menu with email + sign-out once you're signed in.
  57. v0.1.0Shipped

    Phase 0 foundation

    • Stood up the HunterADE app: Next.js 16 + React 19 + TypeScript + Tailwind v4 + shadcn/ui.
    • Supabase clients (browser, server, middleware) typed end-to-end.
    • Introduced the `<ComingSoon />` pattern + `featureFlags.ts` registry — flip a single boolean to launch a feature; banner self-removes.
    • Marketing landing page with hero, four-pillar grid (HunterTrace as lead), and waitlist form.
    • CI on every push: typecheck + lint + build.