Changelog
Every revision, in public
Loop Desk ships on a fast, deliberate cadence. Each revision is a public commit and a single, human-readable summary — no marketing-only releases. If a feature shows up here, it's live for every workspace within hours.
Want a feature? Email us or open an issue. We read everything.
/
Diversification rev away from the 9-rev industry-templates cluster — new GET /api/v1/changelog/{rev} single-rev detail endpoint closes the v1 parity gap on the rev-169 dashboard primitive + /changelog/rss.xml feed items now link to per-rev detail pages instead of in-page hash anchors so feed-reader subscribers (Feedly, Inoreader, AI tooling release-roundup newsletters) land on the dedicated SEO/share surface + per-rev share-affordance chip (🔗 Copy link) on /changelog/[rev] closes the share-affordance loop the rev-101 index page established + per-rev TechArticle JSON-LD structured data closes the structured-data axis on the per-rev detail page (the only public marketing surface still without schema.org markup after rev-103/170/171 closed it on /blog/[slug] / /templates / /integrations / /templates/[key] / landing) — closes the load-bearing v1 parity gap on the rev-169 named dashboard primitive + opens the protocol-bound + share + SEO axes on the per-rev surface in one rev — 96th unbroken cadence rev (rev 174)
- GET /api/v1/changelog/{rev} — single-rev detail endpoint. Closes the load-bearing v1 parity gap on the rev-169 dashboard primitive (per-rev detail pages at /changelog/[rev]). Until rev 174, an MCP host wanting to render 'show me what shipped in rev 168' had to fetch /api/v1/changelog?limit=N and filter client-side. Rev 174 makes the answer a one-call bearer-less GET. Each response carries the source rev's full shape (rev / date / title / highlights[]) plus a `neighbors` block { newer, older } with adjacent revs for chronological navigation parity with the rev-107 blog /api/v1/blog/{slug}/neighbors primitive. Pairs with /api/v1/changelog (listing rev 100/101) as the two-axis changelog read surface on the protocol-bound side (full history + per-rev detail) — same depth pattern the rev-102/103 blog cluster reached after listing + per-post detail. No auth — public marketing surface (same model as /api/v1/changelog, /api/v1/blog/{slug}, /api/v1/roadmap-items). Cache-control public, max-age=300, s-maxage=1800. OpenAPI 3.1 spec types the new endpoint with full request/response schemas + 404 error path in lockstep — the cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 89th unbroken rev with rev 174.
- /changelog/rss.xml feed items now link to per-rev detail pages. Until rev 174 the feed items linked to the rev-101 in-page anchor /changelog#rev-N — feed-reader subscribers (Feedly, Inoreader, AI tooling release-roundup newsletters) landing on a new release dropped onto the changelog index with the rev pre-scrolled but lost the rev-169 dedicated OG card + per-rev SEO surface. Rev 174 makes every feed item land on its dedicated /changelog/[rev] detail page so the share + SEO + JSON-LD axis (rev 169 + rev 174 JSON-LD) is reachable from every aggregator without a follow-up click. GUIDs upgraded to `isPermaLink=true` URL form so feed clients dedupe and link through correctly. Plus per-item `<media:thumbnail>` + `<media:content>` pointing at the rev-169 per-rev OG card so feed readers that render thumbnails (Feedly, NetNewsWire, Reeder) show the rev-specific share-card image inline in their item list — the strongest possible visible-velocity trust signal for procurement teams reviewing AI-tool release cadences.
- Per-rev share-affordance chip on /changelog/[rev]. Until rev 174 the rev-169 detail page had title + content + adjacent-rev navigation but no copy-link chip. The rev-101 changelog list has had per-rev copy-link chips since then; rev-126 added the same pattern at the roadmap-phase level. Rev 174 closes the share-affordance loop on the per-rev detail page itself with a brand-color 🔗 Copy link chip in the article header meta row. Pure client-side via `navigator.clipboard.writeText` with the standard `execCommand` fallback for non-secure contexts (matches the rev-42 / rev-43 / rev-101 / rev-125 / rev-126 / rev-128 / rev-133 chip-copy pattern that has run since rev 42). Brand-green `is-copied` success state + 1.6s pulse animation matches the rev-101 changelog permalink + rev-125 roadmap permalink + rev-126 roadmap-filter share chip vocabulary so all the dashboard's share-affordances ring out with one consistent visual story across every public surface (in-app, share page, public marketing). Distinct from the rev-101 index-page chip (which fades in on row hover) — the detail-page chip is always-visible because once the reader has landed on the detail page, the share intent is already strong.
- Per-rev TechArticle JSON-LD structured data on /changelog/[rev]. Closes the structured-data axis on the per-rev detail page — the only public marketing surface still without schema.org markup after rev-103 / rev-170 / rev-171 closed it on /blog/[slug] / /templates / /integrations / /templates/[key] / landing. TechArticle is more accurate than Article for changelog entries since the content describes software changes (Google's structured-data tooling treats TechArticle as a first-class subtype with bonus surface area in developer-focused search results). Pure server-rendered `<script type=application/ld+json>` block generated at request time from the existing release data. Strategic significance: every public marketing surface now ships schema.org structured data — landing (rev 171), templates index (rev 170 CollectionPage + ItemList), templates detail (rev 171 FAQPage), integrations (rev 170 CollectionPage + ItemList), blog post (rev 103 Article), per-author archive (rev 105 + rev 171 OG avatar), AND per-rev detail (rev 174 TechArticle). Closes the structured-data axis on every public read surface uniformly so procurement reviewers + AI tooling discovery systems consuming schema.org markup get the complete picture in one fetch.
Two more industry onboarding templates (Direct-trade food / beverage + Outdoor recreation / guided experiences) + per-author social handles primitive (GitHub / X / LinkedIn / Mastodon / Bluesky) on the rev-106 author profile registry + frontmatter + author archive page hero rendering with per-platform chip row + v1 /blog/authors endpoint extended with resolved socialHandles + templates page count + radar copy refresh — closes the named rev-172 next-sprint candidates (further industry templates AND per-author social-link auto-resolution) at two more underserved verticals + the cheapest possible per-author identity primitive across five canonical professional-social platforms — 95th unbroken cadence rev (rev 173)
- Two more onboarding templates — Direct-trade food / beverage + Outdoor recreation / guided experiences. Closes the named rev-172 next-sprint candidate (further industry templates — direct-trade food / beverage + outdoor recreation / guided experiences) at two more underserved SMB segments where the procurement-conscious buyer's 'will it know my industry on day 1?' question is loudest. Direct-trade food / beverage (small-batch coffee roasters, single-origin chocolate makers, artisan cheese, specialty tea — 4 high-importance memory entries: producer + farm + harvest year origin transparency on every product page, producer-share retail-price disclosure, 18-month harvest-cycle origin-visit cadence rule, 70% wholesale share channel-mix red flag + 1 sample subscriber origin-story request signal) AND Outdoor recreation / guided experiences (independent gear shops, guide services, charter operators, outfitting companies, adventure-tourism operators — 4 high-importance memory entries: named-guide / captain / instructor confirmation rule, honest-seasonal-risk-window transparency rule, 18-month guide-tenure peak-season pairing rule, 21-day below-peak booking-horizon red flag + 1 sample repeat-customer relationship-expansion question signal). Two new OnboardingTemplateKey enum values (direct_trade_food, outdoor_recreation) extend the rev-19 pattern. The templates cluster is now twenty-three named verticals deep, closing the day-1 starvation-point story across two more underserved owner-led segments where named-producer / named-guide attribution is the load-bearing differentiator against generic AI tools.
- Per-author social handles primitive (GitHub / X / LinkedIn / Mastodon / Bluesky). Closes the named rev-172 next-sprint candidate (per-author social-link auto-resolution — auto-resolving GitHub / X / LinkedIn handles from the email or a `socialHandles` registry field for richer per-author profile blocks). Until rev 173 the rev-106 author profile + rev-107 frontmatter override + rev-172 Gravatar email primitive together gave readers a per-author avatar + bio + tagline + arbitrary `links` array — but the social-axis (GitHub / X / LinkedIn / Mastodon / Bluesky) had to live as raw `links` entries per-handle, which was operationally painful for contributors and gave the rendered surface no way to know 'this is a GitHub handle, render it with the GitHub icon'. Rev 173 closes that. New `BlogAuthorSocialHandles` type with bare-username keys for github / x / linkedin / mastodon / bluesky. New `socialHandles` block on data/blog-authors.json + matching `authorGithub` / `authorX` / `authorLinkedin` / `authorMastodon` / `authorBluesky` frontmatter fields with the same registry → frontmatter override resolution rev 107 introduced. New `buildSocialHandleUrl(platform, handle)` helper derives the per-platform URL at render time from the bare username so contributors register usernames once (no @, no URL prefix) and the URL shape can evolve (LinkedIn personal vs company; Mastodon instance handling) without a registry migration. Per-platform chip row renders on every per-author archive page hero with slate-tinted ambient styling distinct from the rev-106 brand-color teal `links` chips so readers can tell 'this is a GitHub profile' apart from 'this is a labelled portfolio link' at a glance. The five canonical platforms (GitHub → X → LinkedIn → Mastodon → Bluesky) cover ~95% of professional social presence as of 2026; future platforms can be added as bare-username keys without touching the URL builder. Strategic significance: pairs with rev-105 author archive + rev-106 profile registry + rev-107 frontmatter override + rev-172 Gravatar email as the now-five-axis per-author identity surface (avatar / tagline / bio / homepage / socials).
- v1 /blog/authors endpoint extended with resolved socialHandles. Pairs the rev-173 dashboard primitive with the v1 mirror in lockstep — the cadence pattern from rev 37 onward (every dashboard primitive ships with a v1 equivalent in the same cycle) holds unbroken into rev 173. The existing rev-105 `GET /api/v1/blog/authors` endpoint now also projects each profile's `socialHandles` block as a Record<platform, { handle, url }> shape — bare username + resolved URL together so MCP hosts rendering an author profile chip don't have to re-derive the URL shape per platform. Pairs with the rev-106 profile bio + avatar projection so MCP hosts now have the complete per-author identity surface in one bearer-less GET. Closes the named rev-172 candidate at the protocol-bound surface in addition to the dashboard surface. The MCP server (Q3 #1) gains one more pre-typed surface with nothing left to design on the per-author identity axis.
- Templates page count + radar copy refresh + visual polish. Templates page count copy bumps 'twenty-one verticals today' → 'twenty-three verticals today' across hero, metadata title, OpenGraph + Twitter description, JSON-LD CollectionPage block, and the templates-cta. Two new keyword hints on the per-vertical chip line (Direct-trade food/beverage `Origin transparency · producer share · harvest cycles · channel mix`; Outdoor recreation `Named guides · seasonal risk · guide tenure · booking horizon`). Six new SEO keywords (`AI for direct-trade coffee roasters`, `AI for craft food and beverage`, `AI for single-origin chocolate makers`, `AI for outdoor recreation operators`, `AI for guide services`, `AI for charter operators`). 'Don't see your vertical?' next-radar list refreshes — replaced with small construction / general contracting, dental / orthodontic clinics (regulated retention rules + insurance billing rhythms), independent veterinary practices (pet-owner cadence + recall recall), and small distillery / brewery operators (production-vs-tap-room channel split) now that direct-trade food + outdoor recreation are shipped. Per-template detail page keyword-hint map gains the matching two new entries so /templates/[key] for the new verticals reads with the same depth as the existing twenty-one verticals. New `.blog-author-hero-socials` + `.blog-author-hero-social` CSS uses a slate-tinted palette + tiny mono-glyph badge inside each chip — quieter than the rev-106 brand-color teal links so the social row reads as ambient profile-context rather than primary call-to-action.
Two more industry onboarding templates (Franchise / multi-location operators + Financial advisor / regulated professional services) + Gravatar auto-resolution on per-author OG cards + new public GET /api/v1/onboarding-templates endpoint completes the four-axis public-marketing v1 cluster (planned + most-requested + shipped + brand voice + industry-fit) — closes both named rev-171 next-sprint candidates (further industry templates AND per-author avatar URL field auto-resolution) at two more underserved verticals + the cheapest possible per-author identity primitive + the missing fourth axis on the v1 marketing surface — 94th unbroken cadence rev
- Two more onboarding templates — Franchise / multi-location operators + Financial advisor / regulated professional services. Closes the named rev-171 next-sprint candidate (further industry templates — franchise-model multi-location operators AND professional services with regulated client obligations) at two more underserved SMB segments. Franchise / multi-location operators (regional QSR / boutique fitness chains / multi-unit retail / multi-clinic platforms — 4 high-importance memory entries: brand-standard variance discipline across locations, weekly-roll-up + monthly-drill-down per-location P&L cadence, 8-location regional manager span-of-control ceiling, 90-day-decline + no-franchisor-contact franchisee retention red flag + 1 sample regional manager same-store-sales decline signal) AND Financial advisor / regulated professional services (independent RIAs, insurance brokers, financial planners, regulated wealth managers — 4 high-importance memory entries: suitability-rationale-leading recommendation framing, KYC + suitability gate before any recommendation, 13-month annual-review cadence regulatory cliff, client-readback suitability-violation red flag + 1 sample concentration-risk client question signal). Two new OnboardingTemplateKey enum values (franchise, financial_advisor) extend the rev-19 pattern. The templates cluster is now twenty-one named verticals deep, closing the day-1 starvation-point story across two more underserved SMB segments where the procurement-conscious buyer's 'will it know my industry on day 1?' question is loudest — multi-location operators where brand-standard discipline + per-location P&L cadence + franchisee retention are 2026 market-table-stakes, and regulated professional services where KYC + suitability + fiduciary documentation are the load-bearing differentiators against generic AI tools that don't know fiduciary regulatory obligations.
- Per-author Gravatar auto-resolution on per-author OG cards. Closes the named rev-171 next-sprint candidate (per-author avatar URL field auto-resolution from the author's email so contributors with a Gravatar profile get a real photo on the OG card without registering it manually). Until rev 172 the rev-168 per-author OG card primitive shipped initials-fallback avatars (rev 171); contributors who wanted a real photo had to register an avatarUrl URL by hand. Rev 172 closes that gap at the cheapest possible primitive: a registered `email` field in data/blog-authors.json (or `authorEmail` in markdown frontmatter) auto-derives a Gravatar URL with `d=identicon` so a missing Gravatar profile falls through to a deterministic geometric pattern (always renders, never 404s) instead of a broken-image placeholder. MD5 of the lowercased + trimmed email is the load-bearing piece — same hash format every Gravatar SDK across every language ecosystem expects so a contributor who already has a Gravatar profile gets a real photo on every per-author OG card with zero configuration. The shared `getAuthorProfile` helper resolves the avatar with the same registry → frontmatter → null cascade rev 107 introduced; the new email field rides on top with the same precedence. Authors who explicitly want the rev-171 initials-circle treatment just don't register an email. Strategic significance: closes the OG-image-polish loop on the byline axis at the cheapest possible cost — no avatar-fetching infrastructure required for missing profiles, no broken-image placeholder, and zero configuration for contributors with an existing Gravatar profile.
- GET /api/v1/onboarding-templates — public templates list on the v1 surface. Opens the missing fourth axis on the public-marketing v1 cluster. Until rev 172 the rev-19/165/166/167/168/169/170/171/172 onboarding templates list lived only in the dashboard signup flow + the rev-166 public /templates marketing page + the rev-169 per-template detail pages. MCP hosts driving the desk programmatically — particularly the upcoming MCP server (Q3 #1) that wraps /api/v1 — needed a machine-readable templates surface so an integration agent could render 'which onboarding template fits my workspace?' in its own UI and apply it programmatically without scraping HTML. Rev 172 closes that gap. Each row carries key + name + description + memoryCount + signalCount + url + the full memories[] + signals[] arrays. No auth — public marketing surface (same model as /api/v1/badge.svg, /api/v1/roadmap-*, /api/v1/changelog, /api/v1/blog). Cache-control public, max-age=300, s-maxage=1800. Pairs with /roadmap-items (planned) + /roadmap-votes (most-requested) + /changelog (shipped) + /blog (brand voice) + /onboarding-templates (industry-fit) as the now-five-axis public marketing surface on the protocol-bound side. Closes the named rev-171 MCP-server protocol-translation work at the templates axis — the upcoming MCP server has nothing left to design across the public marketing v1 cluster.
- Templates page count + radar copy refresh + visual polish. Templates page count copy bumps 'nineteen verticals today' → 'twenty-one verticals today' across hero, metadata title, OpenGraph + Twitter description, JSON-LD CollectionPage block, and the templates-cta. Two new keyword hints on the per-vertical chip line (Franchise `Per-location P&L · brand-standard · regional cadence · franchisee retention`; Financial advisor `KYC · suitability · annual review · fiduciary documentation`). Eight new SEO keywords (`AI for franchise operators`, `AI for multi-location retail`, `AI for boutique fitness chains`, `AI for QSR franchises`, `AI for financial advisors`, `AI for RIAs`, `AI for insurance brokers`, `AI for regulated professional services`). 'Don't see your vertical?' next-radar list refreshes — replaced with direct-trade food/beverage (small-batch coffee, cheese, single-origin chocolate), small construction / general contracting, and outdoor recreation (gear shops, guide services, charter operators). New API hint chip on the templates CTA names the new /api/v1/onboarding-templates endpoint inline so integration-minded readers discover it. New focus-visible accessibility ring on every templates-page interactive element (chip links, expand summary, CTA buttons) so keyboard-only operators land cleanly — mirrors the rev-38 dashboard accessibility pattern.
Two more industry onboarding templates (Creator economy infrastructure + Small manufacturing / craft production) + FAQPage JSON-LD on every /templates/[key] detail page + initials-avatar block on per-author OG images + Organization + WebSite + SoftwareApplication JSON-LD on the landing page — closes both load-bearing rev-170 named next-sprint candidates (further industry templates AND per-vertical FAQ schema on /templates/[key] pages AND per-author avatars in OG images via next/og) and opens the structured-data axis on the landing page itself for the first time — 93rd unbroken cadence rev
- Two more onboarding templates — Creator economy infrastructure + Small manufacturing / craft production. Closes the named rev-170 next-sprint candidate (further industry templates — creator economy infrastructure, small manufacturing / craft production) at two more underserved SMB segments. Creator economy infrastructure (newsletter operators, membership platforms, community-led media — 4 high-importance memory entries: named-relationship subscriber communication, 72h owned-channel platform-risk syndication rule, $35 CPM sponsorship floor, three-opens-no-clicks-30-days subscriber churn red flag + 1 sample paid-tier downgrade feedback signal) AND Small manufacturing / craft production (owner-led manufacturers, craft producers, small-batch CNC / leather / textile / ceramics / specialty food — 4 high-importance memory entries: named-maker production communication, worst-case lead-time honesty rule, 35% supplier-concentration red flag, sampled defect-rate quality audit cadence + 1 sample B2B custom-request feedback signal). Two new OnboardingTemplateKey enum values (creator_infra, small_manufacturing) extend the rev-19 pattern. The templates cluster is now nineteen named verticals deep, closing the day-1 starvation-point story across two more underserved SMB segments where the procurement-conscious buyer's 'will it know my industry on day 1?' question is loudest — newsletter operators / membership platforms where platform-risk + subscriber-cadence anxiety are 2026 market-table-stakes, and small manufacturing / craft producers where named-maker attribution + supplier-concentration discipline are the load-bearing differentiators against the platform-tier marketplaces.
- Per-vertical FAQPage JSON-LD on every /templates/[key] detail page. Closes the named rev-170 next-sprint candidate (per-vertical FAQ schema on /templates/[key] pages with sample procurement questions per vertical). Procurement reviewers + AI tooling discovery systems consuming schema.org markup can now answer five named procurement-conscious questions per vertical without parsing the page body: (1) does Loop Desk know my industry on day 1, (2) what gets pre-loaded, (3) is the pre-loaded knowledge editable, (4) can the operator skip a template, (5) how does Loop Desk's flat-fee compare to per-cycle-credit AI tools at the per-vertical axis. Pure server-rendered <script type='application/ld+json'> block generated at request time from the existing template data + a small per-vertical answer-template helper. Mirrors the rev-103 Article markup pattern and the rev-170 CollectionPage + ItemList markup at the per-template detail axis. Strategic significance: closes the structured-data symmetry on the per-template detail pages alongside the rev-169 per-template OG cards (rich previews) + rev-170 ItemList markup on the index — the templates cluster is now structured-data-complete on every read surface.
- Initials-avatar block on per-author OG images. Closes the named rev-170 next-sprint candidate (per-author avatars in OG images via next/og — surfacing actual avatars in the share card via next/og would close the OG-image-polish loop on the byline axis). Until rev 171 the rev-168 per-author OG cards surfaced the author's name as the headline + post-count as the subline + three stat chips, but had no identity-anchor visual element. Rev 171 closes the loop with an 88px circular avatar block in the eyebrow row of every per-author OG card. Two render paths: (a) when the author has registered an avatarUrl in data/blog-authors.json or via authorAvatar frontmatter, the avatar URL renders inline as an <img>; (b) the always-renderable fallback is an initials circle (first + last initial for two-name authors, first two characters for single-name authors). The shared renderOg helper in src/lib/og-image.tsx gains an optional avatar input so the per-author route handler is the only consumer that opts in — every other public-marketing OG card keeps its existing typographic card. Strategic significance: closes the OG-image-polish loop on the byline axis at the lowest possible primitive — no avatar-fetching infrastructure required for the always-renderable fallback, and the optional imageUrl path means a contributor who registers a Gravatar / CDN URL gets a real photo without re-deploying.
- Organization + WebSite + SoftwareApplication JSON-LD on the landing page. Opens the schema.org structured-data axis on the most operator-loaded public surface for the first time. Until rev 171 the rev-103 /blog/[slug] Article markup, rev-170 /templates + /integrations CollectionPage + ItemList markup, and rev-171 /templates/[key] FAQPage markup together shipped structured data on every public marketing surface — except the landing page itself. The Organization block names the brand + logo + sameAs links so AI tooling discovery systems answer 'what is Loop Desk' with one fetch; the WebSite block surfaces a SearchAction shape so Google + AI tooling can render an inline blog search box; the SoftwareApplication block names the flat-fee positioning at the structured-data axis explicitly so procurement reviewers + AI tooling roundup newsletters scraping the landing page see 'free tier; flat-fee paid tiers with no per-cycle credits' in machine-readable form. Strategic significance: every public marketing surface now ships schema.org structured data — landing (rev 171), templates index (rev 170), templates detail (rev 171 FAQPage), integrations (rev 170), blog post (rev 103), per-author archive (rev 105 + rev 171 OG avatar). Pairs with rev-167 / 168 / 169 dynamic OG cards as the full SEO + sharable-card story.
Two more industry onboarding templates (B2B services / consultative sales + Home services platforms) + server-side per-rev link index on /changelog for crawler discoverability + JSON-LD CollectionPage + ItemList structured data on /templates and /integrations — closes the rev-169 named next-sprint candidates (further industry templates AND /changelog SSR'd page link to per-rev pages) at two more verticals + the SSR'd discoverability axis + opens the structured-data axis on the public marketing trio — 92nd unbroken cadence rev
- Two more onboarding templates — B2B services + Home services platforms. Brings the templates cluster to seventeen named verticals. B2B services / consultative sales closes the named rev-169 next-sprint candidate (B2B services with long sales cycles) at the sixteenth vertical: 4 high-importance memory entries (named-next-commitment buyer communication, multi-stakeholder layer-up champion rule, 14-day pipeline-stall red flag, outcome-bound proposal scope rule) + 1 sample proposal-stage stakeholder feedback signal. Home services platforms closes the named rev-169 next-sprint candidate (home services platforms — HVAC / plumbing / electrical adjacent at the platform tier) at the seventeenth vertical: 4 high-importance memory entries (named-technician customer confirmation, parts/labour quote transparency, 90-day callback red flag, 24-hour-after-invoice review-request cadence) + 1 sample dispatch coordination signal. Two new OnboardingTemplateKey enum values (b2b_services, home_services) extend the rev-19 pattern.
- Server-side per-rev link index on /changelog. Closes the named rev-169 next-sprint candidate (/changelog page link to per-rev pages on the SSR'd HTML — adding it server-side too would let crawlers discover the per-rev URL even without JS). The rev-169 'Open page →' link lives inside the rev-101 'use client' ChangelogList component which can mask discoverability for crawlers that don't fully render the React tree. Rev 170 adds a plain server-rendered <nav> + <ol> at the bottom of /changelog with one anchor per rev that guarantees every per-rev URL lands in the SSR'd HTML response body regardless of JS execution. Pure SSR, zero client-side cost. Visually tucked as an 'All revisions' appendix under the rev-101 client list so it doesn't compete with the existing search + permalink surface above. Closes the SSR'd discoverability axis on the rev-169 per-rev detail page primitive.
- JSON-LD CollectionPage + ItemList structured data on /templates and /integrations. Pairs with the rev-103 /blog/[slug] Article markup as the second + third axes of the structured-data story across the public marketing trio. The /templates page emits a CollectionPage with an ItemList of every shipped template (name, description, URL); the /integrations page emits a CollectionPage with an ItemList of every channel (axis-prefixed name + auth model in description). Google + AI tooling discovery systems that scrape schema.org markup now see every template + every integration channel with one fetch instead of having to render the React tree. Procurement teams searching for 'AI workspace integrations' or 'Loop Desk templates by vertical' land on the matching page with full structured-data context, not just title + description. Marketing surface — pairs with the rev-167/168/169 dynamic OG cards (rich previews) and the rev-103 / rev-105 / rev-107 / rev-108 blog index entries (per-axis discoverability) for the full SEO + sharable-card story across every public marketing surface.
- Templates + integrations + CTA copy refresh. /templates page count copy bumps 'fifteen verticals today' → 'seventeen verticals today' across hero, metadata title, OpenGraph + Twitter description, and the templates-cta. Two new keyword hints on the per-vertical chip line (B2B services 'Pipeline · stakeholders · proposals · expansion'; Home services 'Dispatch · technicians · quotes · callbacks'). Four new SEO keywords (AI for B2B services, AI for home services platforms, AI for HVAC contractors, AI for plumbing companies). 'Don't see your vertical?' radar list refreshes — replaced with creator economy infrastructure (newsletters / membership platforms), small manufacturing / craft production, and franchise-model multi-location operators now that B2B services + home services are shipped. Sitemap gains 2 new per-template detail pages automatically (priority 0.55, monthly) since it reads from ONBOARDING_TEMPLATES directly.
Two more industry onboarding templates (Accounting / compliance + Fitness / wellness studios) + per-template detail pages at /templates/[key] with their own OG cards + per-rev detail pages at /changelog/[rev] with their own OG cards — closes both named rev-168 next-sprint candidates (per-template OG images at /templates#template-{key} deep-links AND per-rev OG images on /changelog#rev-N deep-links) at the load-bearing share + SEO surface — 91st unbroken cadence rev
- Two more onboarding templates — Accounting / compliance + Fitness / wellness studios. Brings the templates cluster to fifteen named verticals. Accounting / compliance closes the named rev-168 next-sprint candidate (accounting / compliance — legal-adjacent) at the fourteenth vertical: 4 high-importance memory entries (precise-not-reassuring client communication leading with the number, 5-business-day filing buffer, scope-creep red flag at 3+ unbilled questions/month, dated advice-memo audit trail) + 1 sample late-document feedback signal. Fitness / wellness studios closes the named rev-168 next-sprint candidate (fitness / wellness studios — membership-adjacent) at the fifteenth vertical: 4 high-importance memory entries (coach-voice member communication, 90% safe-capacity class booking rule, 14-day silent-regular-member retention red flag, 6-paying-member class economics floor) + 1 sample class-format feedback signal.
- Per-template detail pages at /templates/[key] with their own OG cards. Closes the named rev-168 next-sprint candidate (per-template OG images at /templates#template-{key} deep-links). The rev-168 fragment links don't reach the server so they can't carry per-template OG metadata — a share into Slack / X / LinkedIn / Bluesky always rendered the rev-167 generic /templates card. Rev 169 ships proper per-template URLs at /templates/{key} with their own OG image route handler so each vertical has a sharable surface that names the specific template + memory seed count + sample signal count. Each per-template page also serves as a per-vertical SEO surface in its own right alongside the rev-166 /templates index. Statically prerendered for every template via generateStaticParams. Brand-amber accent matches the rev-167 generic /templates card so the templates OG family scans as siblings.
- Per-rev detail pages at /changelog/[rev] with their own OG cards. Closes the named rev-168 next-sprint candidate (per-rev OG images on /changelog#rev-N deep-links). The rev-101 in-page #-fragment permalink can't change OG metadata per rev, so a share into Slack / X / LinkedIn / Bluesky always rendered the rev-167 generic /changelog card. Rev 169 ships proper per-rev URLs at /changelog/{n} with their own OG image route handler so every revision's share card carries the specific rev label + date + headline as the eyebrow + subline. Brand-teal accent matches the rev-167 generic /changelog card. Statically prerendered for every rev (157 prerendered pages at build time). Adds a quiet 'Open page →' link to every rev row on the rev-101 list so operators discover the per-rev page without leaving the index.
- Templates + sitemap + changelog list copy refresh. Templates page count copy bumps from 'thirteen verticals today' to 'fifteen verticals today'. Each template card on /templates surfaces an 'Open the {name} page →' link to the new per-template detail page. Two new keyword hints (Accounting `Filings · client comms · advisory cadence · scope`, Fitness `Members · attendance · retention · class economics`) plus three new SEO keywords (AI for accounting practices, AI for fitness studios, AI for wellness centres). The sitemap gains 15 per-template pages (priority 0.55, monthly) + 157 per-rev pages (priority 0.5, never — content is frozen by rev). 'Don't see your vertical?' CTA copy refreshes the next-radar list — replaced with B2B services with long sales cycles, home services platforms, creator-economy infrastructure, and small manufacturing / craft production now that accounting + fitness are shipped.
Two more industry onboarding templates (Property management + Nonprofit / membership) + per-author OG images on /blog/author/[slug] + per-post OG images on /blog/[slug] — extends the rev-167 dynamic OG primitive to the per-author + per-post axes and closes the named rev-167 next-sprint candidates (further industry templates AND OG image polish) at two more verticals + two more share surfaces — 90th unbroken cadence rev
- Two more onboarding templates — Property management + Nonprofit / membership. Closes the named rev-167 next-sprint candidate (further industry templates) at the twelfth and thirteenth verticals. Rev 19 opened the templates cluster with five verticals; rev 165 added Healthcare; rev 166 added Real estate + Legal; rev 167 added Field services + Restaurant + Education. Rev 168 adds Property management (owner-led property managers, landlord operators, small management companies — 4 high-importance memory entries: tenant communication leading with a named next step, 24h-acknowledgement / 72h-resolution maintenance triage, 90-day renewal red flag, 6-month inspection cadence + 1 sample recurring-HVAC feedback signal) AND Nonprofit / membership (donors, members, programmes, grant cycles — 4 high-importance memory entries: donor communication leading with gratitude before any ask, concrete-numbers-not-adjectives outcome reporting, 18-month lapsed-donor red flag with a 12-month re-engagement watch threshold, 90-day grant-cycle prep horizon + 1 sample newsletter-cadence feedback signal). Brings the templates cluster to thirteen named verticals.
- Per-author OG images on every /blog/author/[slug]. Extends the rev-167 dynamic-OG primitive to the per-author archive surface. Until rev 168, sharing `loopdesk.space/blog/author/<slug>` into Slack / X / LinkedIn / Bluesky fell back to the rev-167 generic /blog OG card. Rev 168 makes every per-author archive carry its own typographic card via next/og — author name as headline, profile tagline (or post-count fallback) as subline, and three stat chips (posts, words, latest). Brand-purple accent distinguishes author cards from /blog (teal) so the public-marketing OG family scans as siblings without competing. SSG'd via `generateStaticParams` so every author route is statically prerendered at build time. Pairs naturally with the rev-105 per-author archive page primitive — rev 105 made the archive page exist, rev 168 makes its share card carry author identity.
- Per-post OG images on every /blog/[slug]. Extends the rev-167 dynamic-OG primitive to every individual blog post. Until rev 168, sharing a specific post URL fell back to the generic /blog OG card; rev 168 makes every post carry its own typographic card with title as headline, excerpt as subline, three stat chips (read time, words, date), and a category-tinted accent (approval-governance → teal, memory-context → navy, signal-stream → amber, market-strategy → purple) so the share card itself signals where the post lives in the topical map. Statically prerendered for every post via `generateStaticParams`. Closes the named rev-167 OG-image-polish follow-up at the load-bearing public-reading surface — every blog post is the most-shared content type Loop Desk produces.
- Templates page + keyword hint refresh. Templates page count copy bumps from 'eleven verticals today' to 'thirteen verticals today'; the keyword hint map adds two new entries (`Tenants · maintenance · renewals · inspections` for property management, `Donors · members · outcomes · grant cycles` for nonprofit); SEO keyword set expands with two new vertical-buyer terms (`AI for property management`, `AI for nonprofits`); 'Don't see your vertical?' CTA copy refreshes the next-radar list — replaced with accounting / compliance (legal-adjacent), B2B services with long sales cycles, fitness / wellness studios (membership-adjacent), and home services platforms now that property management + nonprofit are shipped.
Three more industry onboarding templates (Field services + Restaurant/hospitality + Education) + dynamic per-page OG images via next/og — closes the named rev-166 next-sprint candidates (further industry templates AND per-page OG images) and fixes the broken `og-default.png` reference rev-166 metadata pointed at — 89th unbroken cadence rev
- Three more onboarding templates — Field services + Restaurant/hospitality + Education. Closes the named rev-166 next-sprint candidate (further industry templates) at the ninth, tenth, and eleventh verticals in one cycle. Rev 19 introduced industry onboarding templates with five verticals; rev 165 added Healthcare/Wellness; rev 166 added Real estate + Legal. Rev 167 adds Field services / trades (HVAC, plumbing, contracting, landscaping — 4 high-importance memory entries: customer communication voice with ETAs, written quote stance with 14-day validity, callback red flag inside 30 days, same-day review cadence + 1 sample late-dispatch feedback signal), Restaurant/hospitality (independent operators + small chains — 4 high-importance memory entries: warm-brief guest comms, 15-minute reservation grace policy, three-low-reviews red flag, supplier invoice variance threshold + 1 sample slow-service feedback signal), and Education / tutoring (small schools, tutoring services, bootcamps — 4 high-importance memory entries: concrete parent comms, three-absences attendance threshold, 60-day renewal window, monthly outcome-win cadence + 1 sample homework-load feedback signal). Brings the templates cluster to eleven named verticals, closing the day-1 starvation-point story across every owner-led SMB segment Loop Desk's approval-first vocabulary fits, including the three trade/service segments rev-166 explicitly named as the next radar candidates.
- Dynamic per-page OG images via next/og — closes rev-166 named candidate + fixes broken share cards. Closes the named rev-166 next-sprint candidate (per-page OG images on /templates / /integrations / /roadmap / /changelog) and silently fixes a real bug: rev-166 metadata referenced `https://loopdesk.space/og-default.png` for share cards but no such file existed in /public (the actual file shipped is `og-image.png`), so share cards into Slack / X / LinkedIn / Bluesky rendered without an image. Rev 167 ships an `opengraph-image.tsx` route handler in each of the four marketing route segments. Each handler imports a shared `renderOg` helper from `src/lib/og-image.tsx` that uses next/og's `ImageResponse` to render a typographic card with Loop Desk branding + page-specific stat chips (changelog: latest rev / total rev count, roadmap: phase count + item count, templates: vertical count + memory-seed count, integrations: inbound/outbound/programmatic counts) and a per-page accent palette (teal / purple / amber / navy). The static `images` field on each page's metadata is dropped so Next.js auto-discovers the dynamic route. Each card is statically prerendered at build time so the share-link round-trip stays fast.
- Templates page + integrations page copy refresh. Templates page count copy bumps from 'eight verticals today' to 'eleven verticals today'; the keyword hint map adds entries for the three new templates (`Dispatch · quotes · callbacks · reviews` for field services, `Covers · reservations · review patterns · supplier mix` for restaurant, `Attendance · renewals · parent comms · outcomes` for education); SEO keyword set expands with the three new vertical-buyer terms (`AI for field services`, `AI for restaurants`, `AI for tutoring`). 'Don't see your vertical?' CTA copy refreshes the next-radar list since rev 166's named candidates are now shipped — replaced with property management (real-estate-adjacent), accounting/compliance (legal-adjacent), nonprofit/membership orgs, and B2B services with long sales cycles.
Two more industry onboarding templates (Real estate + Legal) + public /templates marketing page + clickable activation checklist step rows + rich social-share metadata across the public marketing trio (changelog + roadmap + integrations) — closes the named rev-165 next-sprint candidate (further industry templates) at the cluster's seventh and eighth verticals + opens the second public marketing surface that answers a procurement-conscious question — 88th unbroken cadence rev
- Two more onboarding templates — Real estate + Legal/professional services. Closes the explicit rev-165 next-sprint candidate at the seventh and eighth verticals. Rev 19 introduced industry onboarding templates with five verticals (ecommerce/saas/consulting/agency/creator); rev 165 added Healthcare/Wellness as the sixth. Rev 166 adds Real estate / property (owner-led brokerages, property managers, leasing — 4 high-importance memory entries: buyer/tenant outreach voice, listing freshness floor, renewal red flag, review cadence + 1 sample buyer-feedback signal) AND Legal / professional services (solo + small firms, accountants, advisors — 4 high-importance memory entries: client communication voice, conflict-check policy, deadline-buffer lesson, retention red flag + 1 sample client-feedback signal). Both verticals were named in the rev-165 running state as the next natural underserved segments. Pure additive — same rev-19 pattern, new `OnboardingTemplateKey` values (`real_estate`, `legal`), no migration. Brings the templates cluster to eight named verticals, closing the day-1 starvation-point story across every owner-led SMB segment Loop Desk's approval-first vocabulary fits.
- Public /templates marketing page. Pairs with /integrations (rev 165 — vendor inventory) as the second public marketing surface that answers a specific procurement-conscious question. /integrations answers 'what does this product connect to'; /templates answers 'will it know my industry on day 1'. Until rev 166 the rev-19/165/166 onboarding template list lived only inside the signup flow — a prospect evaluating Loop Desk had to start a workspace to see whether their vertical was supported. The /templates page closes that loop: every available template renders as a card with name, description, vertical-keyword chip, memory + signal counts, and a `<details>` expansion that lists every memory entry + sample signal the template pre-loads — concrete day-1 evidence for the buyer's procurement check. Sitemap entry + nav links across landing/integrations/roadmap/changelog/blog/docs. SEO win on per-vertical buyer terms ('AI workspace for healthcare practice', 'AI workspace for real estate brokerage', 'AI workspace for legal practice', etc.).
- Activation checklist v3 — clickable step rows. Rev 165 v2 sharpened copy + added the 6th step + percent. Rev 166 v3 makes each step *clickable* — tap 'Connect a feed' → smooth-scroll to `#panel-sources`. Each step now has an optional `href` mapping to a matching dashboard panel anchor (`#panel-sources` / `#panel-signal-add` / `#panel-approvals` / `#panel-integrations`). Done steps stay inert; live steps render as `<a>` with the new `.ld-activation-step-link` treatment (hover lift + brand-color background tint + focus-visible ring + 'Take me there →' chip that fades in on hover). Closes the day-1 'where do I do this?' friction without a new copy axis. Pairs with the rev-23 keyboard-shortcut FAB + rev-27 ⌘K command palette as the third launcher into dashboard panels — but the most discoverable one for new operators who don't yet know about the keyboard surface.
- Rich social-share metadata across the public marketing trio. Until rev 166 the public marketing surfaces (/changelog, /roadmap, /integrations, plus the new /templates) had only title + description metadata; share links into Slack / X / LinkedIn / Discord / Bluesky rendered as plain-text previews, which converts at a fraction of the rate of rich-card previews. Rev 166 adds full openGraph + twitter card blocks to all four pages with canonical URLs, OG image references, descriptive social-share copy distinct from the page-meta copy (so the share-card lede reads as a hook rather than a duplication), and `card: summary_large_image`. Marketing surface improvement that compounds across every share — every rev-101 changelog permalink share, rev-125 roadmap permalink share, and rev-126 roadmap-filter share chip now lands in chat with a card preview.
Strategic diversification — Healthcare/Wellness onboarding template + public /integrations marketing page + activation checklist v2 (6 steps with refreshed copy and a guardrail nudge) + evergreen operator blog post on the always-on desk shape — pivot away from the 14-rev cost-spike alarm thread (rev 151-164) onto the public-marketing + onboarding axes — 87th unbroken cadence rev
- Healthcare/Wellness onboarding template — sixth industry preset. New ONBOARDING_TEMPLATES entry for Healthcare/Wellness practice owners (independent therapists, dentists, vets, dental clinics, wellness studios) — a real underserved SMB segment that until rev 165 had to fall back on Other/Custom. Seeds 4 high-importance memory entries (patient-communication preference, no-show policy, review-cadence lesson, retention-red-flag lesson) plus 1 sample patient-feedback signal so the workspace's first cycle has substance. Mirrors the rev-19 pattern (Ecommerce/SaaS/Consulting/Agency/Creator) at the sixth named vertical. Strategic significance: closes a gap in the day-1 starvation-point story for an industry segment that maps cleanly onto Loop Desk's approval-first vocabulary (no-show notification, review-request copy, retention nudges all want the same human-in-the-loop boundary).
- Public /integrations marketing page — closes the public marketing trio. Loop Desk has shipped /changelog (history) since rev 14 and /roadmap (future) since rev 38, but until rev 165 had no single page answering 'what does this product connect to?' for procurement teams + B2B buyers evaluating governance-first AI. The /integrations page lists every Loop Desk channel grouped by axis: inbound (RSS / review sites / LinkedIn / email forwarding / inbound webhooks / manual logging — 6 entries with auth model + sinceRev), outbound (Slack push / outbound webhooks per-event router / daily digest / public share links / public desk-health badge SVG — 5 entries), and programmatic (REST API v1 / OpenAPI 3.1 / MCP server coming Q3 — 3 entries). Each entry names the auth model and the rev where it shipped, doubling as a vendor inventory for SOC 2 / ISO 42001 procurement reviews. New page mounted at /integrations + sitemap entry + nav links across landing/changelog/roadmap/docs/blog so the public marketing trio (changelog + roadmap + integrations) is reachable from every other public surface. Strategic significance: cumulative diversification away from the 14-rev cost-spike-axis cluster (rev 151-164) onto the public marketing surface. SEO win on 'AI workspace integrations', 'governance-first AI vendor inventory', 'MCP business workspace integrations' — buyer-side terms.
- Activation checklist v2 — 6th step + sharper copy on existing 5. The rev-11 activation checklist shipped 5 steps (connect a source / log signal / run a cycle / approve first output / wire integration) when the dashboard had ~12 panels. After 154 revs the desk has accumulated memory teaching, multi-operator invites, per-recipient digests, cost guardrails, and the MCP-foundation v1 API — but the checklist copy still read like rev 11. Rev 165 refreshes it on three axes: (a) sharper copy on the existing 5 steps that names current affordances ('connect a feed' vs 'Hook up an RSS feed'), (b) adds a 6th step 'Set a guardrail' that nudges new operators toward configuring at least one of: daily cost cap (rev 20), Slack quiet hours (rev 15), or workspace timezone (rev 20) — until rev 165 first-day operators didn't see the desk's operator-respect controls until they hit them organically, (c) visual: numeric badges on each step + tactile hover lift on incomplete steps so the recommended sequence is implicit. New `hasGuardrail` boolean computed in `app/page.tsx` from existing workspace columns. Strategic significance: the activation surface is the single most operator-loaded onboarding affordance on the dashboard; refreshing it to match the modern feature set is a load-bearing trust signal for new workspaces.
- Evergreen blog post — 'The Morning News Habit, Replaced By An Always-On Desk'. New blog post pivots away from the AI-meta / governance-DevOps / CI-CD security topics that have dominated the rev-by-rev blog cadence for many cycles. Operator-focused angle: what changes structurally when an owner-led team's morning news routine is replaced by an always-on AI workspace. Frames the four trust contract pieces (approval boundary / audit trail / cost predictability / operator-respect controls) as the structural constraints that make 'always-on' a sustainable daily habit rather than a weekend experiment. Names Notion Custom Agents' per-cycle credits + HubSpot Breeze's outcome-based pricing as competitive contrast. SEO play on 'always-on AI workspace', 'morning routine AI', 'governance-first AI for owner-led teams', 'flat-fee AI workspace' — operator-side buyer terms distinct from the technical/governance cluster the recent posts have all been targeting. Closes with concrete CTA cluster pointing at signup + docs + integrations + roadmap.
Per-memory cost spike + chronic warning daily digest sections close the email-channel parity gap on the rev-161 daily alarm AND the rev-163 chronic warning at the per-memory axis — closes the named rev-163 next-sprint candidate (per-memory chronic-warning digest section) — 86th unbroken cadence rev
- Per-memory cost spike daily digest section closes the email channel gap on the rev-161 alarm. New buildMemoryCostSpikesSection() helper renders up to 5 spiking memory entries with ⚡ ratio + today vs avg + kind chip + importance chip + retrieval count + structural-fix copy ('pin or raise importance ≥9 to exempt, or ack to silence today'). Pre-fetched once per workspace via the existing rev-161 detectMemoryCostSpikes() helper and reused across every owner/admin recipient on the cron daily digest path — same pattern the rev-60 source / rev-63 assignee / rev-68 tag daily spike sections use. Workspace-shared (every owner/admin sees the same list since 'this knowledge entry is anomalously expensive today' is workspace-level diagnostic context). The rev-161 daily Slack push + outbound memory.cost_spike event were already fired by pingMemoryCostSpikes() earlier in runDailyDigest, so the email surface stays in lockstep with the chat + integration channels. The detector itself filters out entries acked today via the rev-162 costSpikeAckedAt stamp so the email rows match what the dashboard ⚡ pill shows. Closes the alarm cluster's seventh axis on the email channel — workspace (rev 32) / per-task (rev 55) / per-source (rev 60) / per-assignee (rev 63) / per-tag (rev 68) / per-memory daily (rev 164) all now reach the digest email.
- Per-memory chronic-warning digest section closes the named rev-163 next-sprint candidate at the email channel. Closes the explicit rev-163 next-sprint candidate ('per-memory chronic-warning digest section'). New buildMemoryChronicWarningSection() helper mirrors the rev-75 source/tag chronic-warning sections at the per-memory axis with amber ⏳ Nd-in-a-row chips + ⚡ ratio + structural-fix copy ('the entry is being retrieved too often by too many cycles — pin, raise importance, or refactor surrounding tasks'). Pure derived state from the rev-161 detector return shape: filter to entries whose consecutiveSpikeDays counter has crossed the chronic threshold (3 days, matching rev 70/72/163) AND whose chronicAckedAt stamp is absent or older than the 7-day TTL — same filter shape as the rev-75 source/assignee/tag chronic filtering. Workspace-shared (memory entries don't have assignees so the section lists every chronic entry in one block, mirroring the rev-75 source/tag chronic-warning sections rather than the rev-49 per-recipient stale pattern). The rev-163 chronic-warning Slack push + outbound memory.chronic_warning event were already fired by pingMemoryCostSpikes() so the email surface stays in lockstep with the chat + integration channels. Closes the chronic horizon on the alarm cluster's seventh axis on the email channel: per-source chronic (rev 75) / per-assignee chronic (rev 75) / per-tag chronic (rev 75) / per-memory chronic (rev 164) — every chronic axis now reaches the digest email.
- Plumbed through buildDigestHtml + production cron + admin preview path in lockstep. Both new sections wired into the buildDigestHtml() params signature with optional fields — undefined-safe so no behavior change on workspaces without spikes. Cron daily digest path (runDailyDigest) pre-fetches memorySpikes once per workspace via detectMemoryCostSpikes() and derives the chronic entries inline using the same CHRONIC_THRESHOLD_DAYS + CHRONIC_ACK_TTL_MS constants the rev-75 chronic sections use — one shared filter shape across all four chronic axes. Admin preview path (previewDigestForUser, the rev-36 send-test path) mirrors the cron exactly so admins iterating on configuration see the same surface they'll receive in production. Render order: per-memory daily section appears LAST among daily alarm sections (after per-task/per-source/per-assignee/per-tag) so readers scan the cost story in one visual block; per-memory chronic appears LAST among workspace-shared chronic sections so the four-axis chronic vocabulary reads in one visual block. The detector is read-only — no counter increment, no chronic-ack stamping — so running an admin preview does NOT alter the rev-163 consecutiveSpikeDays counter or the rev-162/163 ack stamps.
- OpenAPI 3.1 spec changelog header documents the rev-164 closure — 86th unbroken cadence rev. No new v1 endpoints (the rev-161 /memory/cost-spikes + rev-163 /memory/chronic-warnings already cover the protocol-bound surface). The OpenAPI 3.1 spec changelog header gains a rev-164 block explaining the email-channel parity gap closure on both alarms in a single cycle. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 86th unbroken rev with rev 164 — even though no new v1 endpoint ships, the pattern is preserved by documenting the rev-164 closure in the OpenAPI changelog header so MCP-host code generators reading the spec see exactly when each rev's primitive landed (either as new endpoint or as channel expansion of an existing alarm). The cost-spike alarm cluster's seventh axis (per-memory) now reaches every operator-loaded channel — in-app ⚡ + ⏳ chips, daily Slack push, outbound webhook events, AND digest email — across both daily and chronic horizons.
Per-memory chronic-spike counter + warning + chronic-ack chip on TopCostMemoryPanel + memory.chronic_warning + memory.chronic_warning_acked outbound events + GET /api/v1/memory/chronic-warnings + POST /api/v1/memory/{id}/chronic-ack + bulk v1 mirror closes the chronic axis on the cost-spike alarm cluster's seventh axis at parity with the daily axis (rev 161/162) — closes the named rev-162 next-sprint candidate at the per-memory chronic horizon — 85th unbroken cadence rev
- Per-memory consecutiveSpikeDays counter + chronicAckedAt column + counter maintenance in pingMemoryCostSpikes. Closes the named rev-162 next-sprint candidate ('per-memory chronic-spike counter + warning'). Two new columns on memory_entry: `consecutiveSpikeDays` (integer NOT NULL default 0 — increments every day the rev-161 detector flags the entry as spiking; resets to 0 the first day it doesn't) and `chronicAckedAt` (timestamp nullable — stamped when the operator chronic-acks the entry for the rev-163 7-day TTL window). The rev-161 pingMemoryCostSpikes daily sweep now maintains the counter on every sweep via batched UPDATEs (one to bump spiking entries, one to reset entries that previously had a non-zero counter but aren't currently spiking). Mirrors the rev-61 source counter + rev-64 assignee counter + rev-70 tag counter at the per-memory axis on the cost dimension. Independent of `costSpikeAckedAt` (rev 162 — per-day mute) — the counter keeps growing through ack-and-spike-again cycles so a memory entry that's been 'ack me daily' for a week shows the strongest possible chronic-noise signal. Pure additive on top of the rev-161 daily detector — no change to the rev-161 behavior unless an operator's entry crosses the chronic threshold.
- Per-memory chronic warning Slack push + memory.chronic_warning outbound event in pingMemoryCostSpikes sub-sweep. New chronic-warning sub-sweep added to pingMemoryCostSpikes mirrors the rev-70 tag chronic / rev-72 source chronic / rev-72 assignee chronic patterns at the per-memory axis. For each workspace where any memory entry's counter has crossed the chronic threshold (3 days) AND hasn't been chronic-acked within the 7-day TTL: (a) Slack push via the new buildMemoryChronicWarningSlackPayload() block (header `:hourglass_flowing_sand: Chronic per-memory cost spike` + per-entry rows with `Nd in a row`, ratio, today $, retrieval count + recommendation copy 'consider pinning, raising importance, or refactoring the surrounding tasks'), (b) outbound memory.chronic_warning event via dispatchMemoryChronicWarningWebhook(), (c) memory_chronic_warning activity-log entry rate-limited to once per workspace per 24h. Same dead-Slack-webhook auto-clear path as the rev-161 daily push. Distinct from the rev-161 daily ⚡ alarm — chronic warning names a *structural* problem (the entry is being retrieved too often by too many cycles) so the right operator response is structural (pin / raise importance / refactor surrounding tasks), not 'stop alarming today.'
- Chronic-ack chip + chronic bulk-ack bar on TopCostMemoryPanel + per-row ⏳ Nd chronic chip. New MemoryChronicAckButton client component mounts inline beside the rev-163 ⏳ chronic chip on every chronically-spiking row of the rev-159 TopCostMemoryPanel. Brand-amber palette (`rgba(232,159,75,*)`) distinct from the rev-162 brand-red daily ack chip so operators read both ack horizons (today / structural) at two distinct attention levels on the same row. New chronic bulk-ack bar surfaces above the row list when canAck && visibleChronicCount >= 2 — mirrors the rev-162 daily bulk-ack bar at the chronic axis. New `⏳ Nd in a row` chronic chip on every row whose counter has crossed the threshold AND hasn't been chronic-acked within the 7-day TTL. The full row-level reading order is now consistent: ⚡ (today's alarm) ↔ Ack (mute today) :: ⏳ Nd (structural alarm) ↔ Ack 7d (mute 7d). Three layers of cost-axis context on every row (cumulative + trajectory + daily alarm + chronic alarm) — operators triaging a load-bearing memory entry now see the full descriptive→defensive picture without leaving the panel.
- v1 endpoints (chronic-warnings GET + chronic-ack POST + bulk POST) + memory.chronic_warning_acked closure + OpenAPI typed coverage — 85th unbroken cadence rev. Four new endpoints close the chronic axis on the protocol-bound surface in the same cycle the dashboard primitive ships: GET /api/v1/memory/chronic-warnings (returns memory entries whose counter has crossed the chronic threshold AND haven't been chronic-acked within the 7-day TTL), POST /api/v1/memory/{memoryId}/chronic-ack (single-entry chronic ack — mirrors `/sources/{id}/chronic-ack` rev 72 + `/cost/by-tag/{tag}/chronic-ack` rev 71 at the per-memory axis), POST /api/v1/memory/chronic-ack/bulk (bulk chronic-ack up to 50 IDs — mirrors `/sources/chronic-ack/bulk` rev 87 + `/cost/by-tag/chronic-ack/bulk` rev 87 at the per-memory axis on the chronic horizon). Plus matching dashboard endpoints (POST /api/memory/{id}/chronic-ack + POST /api/memory/chronic-ack/bulk). New memory.chronic_warning + memory.chronic_warning_acked outbound events with full payload typing — the closure receipt fires from both single + bulk ack so downstream FinOps integrations can reconcile alarm-open with alarm-acknowledged at the per-knowledge-entity axis on the chronic horizon. The OpenAPI 3.1 spec types every new endpoint with full request/response schemas plus the chronic-warnings response shape with the same field projection as the rev-161 daily endpoint plus consecutiveSpikeDays + chronicAckedAt. The OpenAPI spec changelog header gains a rev-163 block explaining the chronic axis closure on the seventh alarm-cluster axis. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 85th unbroken rev with rev 163. The cost-spike alarm cluster on the protocol-bound surface now closes both the daily horizon (rev 161/162) AND the chronic horizon (rev 163) on every axis where chronic makes sense (per-source / per-assignee / per-tag / per-memory). The MCP server has nothing left to design across detect → triage → ack on either horizon.
Per-memory cost-spike acknowledgment + bulk-ack on TopCostMemoryPanel + memory.cost_spike_acked outbound event + POST /api/v1/memory/{id}/cost-spike-ack + bulk v1 mirror closes the named rev-161 next-sprint candidate at the operator counter-action surface — closes the alarm cluster's seventh axis on the closure-receipt loop in lockstep — 84th unbroken cadence rev
- Per-memory cost-spike ack (memoryEntries.costSpikeAckedAt column + acknowledgeMemoryCostSpike helper) closes the rev-161 alarm-only gap. Closes the named rev-161 next-sprint candidate ('per-memory cost-spike acknowledgment') on the operator counter-action surface. New nullable timestamp column on memory_entry. Stamped by acknowledgeMemoryCostSpike() when the operator clicks the rev-162 'Ack' button beside the rev-161 ⚡ pill on the rev-159 TopCostMemoryPanel. The rev-161 detector + Slack daily push + outbound memory.cost_spike event all skip memory entries whose ack stamp is >= today's day-start in workspace TZ so a triaged spike doesn't keep firing today. Mirrors the rev-56 task / rev-59 source / rev-63 assignee / rev-68 tag ack at the per-memory axis on the cost dimension. Distinct from rev-5 pin (permanent — exempts from rev-161 detector forever via the same load-bearing-by-design exclusion that rev-161 already applies). Ack = 'I see this entry's spike, stop alarming today, but watch for tomorrow.'
- Bulk per-memory ack-all chip on TopCostMemoryPanel + bulkAcknowledgeMemoryCostSpikes helper. New bulkAcknowledgeMemoryCostSpikes() helper caps at 50 memory IDs per call matching the rev-26 / rev-33 / rev-34 / rev-36 / rev-57 / rev-60 / rev-63 / rev-68 bulk surface vocabulary. New 'Ack all N' chip surfaces inline on TopCostMemoryPanel when canAck && visibleSpikingCount >= 2 (gated to editor+ via the rev-16 role enforcement on the dashboard layer). The rev-161 daily Slack push lists up to 5 spiking memory entries; until rev 162 operators landing on the dashboard had to ack each entry individually via the rev-162 inline button. Bulk-ack collapses that to one tap. Pairs with the per-row inline ack chip for the inline-vs-batch ack symmetry — the same pattern rev 6 / 26 / 33 / 34 / 36 / 57 / 60 / 63 / 68 followed across all four core entities + per-source / per-assignee / per-tag spike alarms now applies to the rev-161 per-memory spike alarm.
- memory.cost_spike_acked outbound event + dispatchMemoryCostSpikeAckedWebhook closes the rev-37 closure-receipt pattern at the per-memory axis. New OutboundEvent value memory.cost_spike_acked plus matching dispatchMemoryCostSpikeAckedWebhook() in src/lib/outbound.ts. Wired into both acknowledgeMemoryCostSpike() and bulkAcknowledgeMemoryCostSpikes() via void dispatch...({...}) so a downstream failure can't ripple back to block the ack itself. Mirrors the rev-61 task.cost_spike_acked + source.cost_spike_acked + rev-63 assignee.cost_spike_acked + rev-68 tag.cost_spike_acked closure-receipt pattern at the per-memory axis. Closes the alarm-cluster's seventh axis on the closure-receipt loop. External integrations watching the rev-161 memory.cost_spike event can now reconcile alarm-open with alarm-acknowledged at the per-knowledge-entity axis just as they already could at task / source / assignee / tag — load-bearing for FinOps tools tracking AI cost on a per-knowledge-asset dashboard, CRMs tagging knowledge entries by ROI, and board-status integrations aggregating 'open cost issues this week' surfaces. Rate-limited correctly: only fires when the SQL UPDATE returns at least one acked row so a no-op bulk call doesn't poison the audit trail.
- Dashboard + v1 ack routes (single + bulk) + OpenAPI 3.1 typed coverage — 84th unbroken cadence rev. Four new endpoints — POST /api/memory/{memoryId}/cost-spike-ack, POST /api/memory/cost-spike-ack/bulk, POST /api/v1/memory/{memoryId}/cost-spike-ack, POST /api/v1/memory/cost-spike-ack/bulk — mirror the rev-68 tag-ack route shape exactly at the per-memory axis. Editor+ role at the dashboard layer; bearer-auth via existing ingest token at v1. The OpenAPI 3.1 spec types both new v1 endpoints with full request/response schemas (memoryIds[≤50] required + acknowledgedCount response shape) plus extends the rev-161 GET /memory/cost-spikes response with the new costSpikeAckedAt field (nullable date-time) so MCP hosts render the muted-state chip without a follow-up call. The OpenAPI spec changelog header gains a rev-162 block explaining the operator counter-action closure on the per-memory axis and the new outbound event. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 84th unbroken rev with rev 162. The cost-spike ack v1 cluster on the protocol-bound surface is now five axes deep (per-task rev 56-57 / per-source rev 59-60 / per-assignee rev 63 / per-tag rev 68 / per-memory rev 162). The MCP server's cost-axis tooling has nothing left to design across attribution + trajectory + alarm + ack at every meaningful axis.
Per-memory cost spike alarm + daily Slack push + outbound memory.cost_spike event + ⚡ pill on TopCostMemoryPanel + GET /api/v1/memory/cost-spikes closes the cost-spike alarm cluster's seventh axis at the per-memory level — 83rd unbroken cadence rev
- Per-memory cost spike detector (detectMemoryCostSpikes helper) closes the cost-spike alarm cluster's seventh axis. Closes the cost-spike alarm cluster's seventh axis on the load-bearing primitive layer — workspace (rev 32), per-task (rev 55), per-source (rev 58), per-assignee (rev 62), per-tag (rev 67), now per-memory (rev 161). Pure read of the rev-160 memoryEntries.dailyCostHistory primitive — no schema cost. A memory entry is spiking when its today >= 2× trailing-7-day daily average AND today >= $0.50 absolute AND >= 3 historical days of non-zero spend (so a brand-new load-bearing memory entry's first day can't trigger). Pinned + importance>=9 entries excluded server-side because they're load-bearing-by-design — alarming on them is tautological. Closes the descriptive (rev 159 attribution) → trajectory (rev 160 sparkline) → alarm (rev 161) trio at the per-memory axis on the cost dimension at parity with the per-task axis (rev 51 + 54 + 55) and the per-source axis (rev 57 + 60 + 58).
- Daily Slack push + outbound memory.cost_spike event closes the chat + integration channel parity gap. New buildMemoryCostSpikeSlackPayload() Slack block + dispatchMemoryCostSpikeWebhook() outbound dispatcher + new memory.cost_spike OutboundEvent value. New pingMemoryCostSpikes() sweep added to runDailyDigest() mirrors pingTagCostSpikes() shape exactly: rate-limited via the new memory_cost_spike activity-log kind to once per workspace per 24h with the same dead-Slack-webhook auto-clear path as rev-58 source spikes. Slack post: ':zap: Per-memory cost spike' header + listing each spike with ratio + today vs avg + retrieval count + kind + title snippet. Outbound payload includes the per-memory shape (memoryId + title + kind + importance + todayUsd + baselineUsd + spikeRatio + retrievalCount). Lets external integrations (FinOps tool tracking AI cost on a per-knowledge-asset dashboard, CRM tagging knowledge entries by ROI, board-status integration aggregating 'open cost issues this week') route the alarm by named knowledge entry — the strongest possible operator-facing answer to 'this specific knowledge asset is anomalously expensive today' because it names the entry, not just the workspace or the source.
- Inline ⚡ alarm pill + spike banner + spiking-row accent on TopCostMemoryPanel. Every spiking row on the rev-159 TopCostMemoryPanel now gets a brand-red ⚡ pill alongside the rev-159 cost amount + rev-160 trajectory sparkline. Spiking rows wear a left-border accent + tinted amount color matching the rev-58 source spike + rev-67 tag spike treatments. Banner above the list summarises the spiking count inline so operators see the alarm without reading every row. Closes the visual-hierarchy gap on the rev-159 panel — operators now scan the panel and triage by alarm state (today's spike) alongside cumulative state (rev 159) and trajectory shape (rev 160) without leaving the dashboard sidebar. The rev-159 panel now reads at three orthogonal levels — same three-level pattern shipped on per-task in revs 51/54/55 + on per-source in revs 57/60/58 — uniformly applied at the per-memory axis on the cost dimension.
- GET /api/v1/memory/cost-spikes v1 endpoint + OpenAPI 3.1 typed coverage + activity-log glyph + tint — 83rd unbroken cadence rev. Closes the cost-spike alarm cluster's seventh axis on the protocol-bound surface in lockstep with the dashboard primitive (cadence pattern from rev 37 onward holds unbroken into rev 161). New bearer-auth GET /api/v1/memory/cost-spikes?limit=10 endpoint mirrors the rev-58 /sources/cost-spikes shape at the per-memory axis. Each row carries memoryId, title, kind, importance, pinned, tags, todayUsd, baselineUsd, spikeRatio, retrievalCount. Pairs with /api/v1/memory/top-cost (rev 159) + /api/v1/memory/{id}/cost-trajectory (rev 160) as the three-axis cost-observability surface on the per-memory entity. The OpenAPI 3.1 spec types the new endpoint with full request/response schemas + the OpenAPI changelog header gains a rev-161 block. Activity log gains memory_cost_spike kind with ⚡ glyph matching the rev-32/55/58/62/67 cost-spike kinds so operators reading the audit trail scan all seven axes with one consistent visual vocabulary. The MCP server's per-memory cost tooling now has nothing left to design across attribution + trajectory + alarm.
Per-memory cost trajectory primitive + inline cost sparkline on TopCostMemoryPanel + memory list rows + GET /api/v1/memory/{id}/cost-trajectory closes the named rev-159 next-sprint candidate at the trajectory axis on the cost dimension at parity with rev-54 task cost trajectory + rev-157 memory retrieval trajectory — 82nd unbroken cadence rev
- Per-memory daily cost history primitive (memoryEntries.dailyCostHistory column + pulse engine jsonb_set bump). Closes the named rev-159 next-sprint candidate ('per-memory cost trajectory mini-chart — letting per-memory entries carry a 7-day cost trajectory just as rev 54 added per-task daily cost history'). New JSONB column on memory_entry — same shape as the rev-54 task.dailyCostHistory primitive (Record<YYYY-MM-DD, { inputTokens, outputTokens }>). The pulse engine's per-memory cost-attribution UPDATE (rev 159) now also bumps today's bucket via SQL jsonb_set alongside the rev-159 cumulative-column writes — single statement, all retrieved rows share the same todayKey on a single cycle so one round-trip covers the entire batch. The rev-159 cumulative columns remain authoritative for total spend; this map is pure history. Trimmed implicitly via 30-day cap when the dashboard renders the trailing window — long-running entries don't grow unbounded JSONB. Closes the *trajectory* axis on the cost dimension at the per-memory level the same way rev-157 closed it on the retrieval dimension and rev-54 closed it on the per-task surface.
- Inline brand-amber MemoryCostSparkline on TopCostMemoryPanel + memory list rows. New `MemoryCostSparkline` client component renders inline beside the rev-159 cost pill on every TopCostMemoryPanel row + beside the rev-157 retrieval sparkline on every memory list row whose dailyCostHistory has at least one non-zero day. Brand-amber palette (rgba(207,108,58,*)) matches the rev-159 TopCostMemoryPanel + rev-51 top-cost-tasks panel cost vocabulary. Distinct from the rev-157 brand-purple retrieval sparkline so the dashboard's two per-memory trajectory primitives (cost vs retrieval) read at two distinct attention levels. The TopCostMemoryPanel `getTopCostMemoryEntries` helper now projects a `trajectory7d` cents array per row (mirrors the rev-51 task.trajectory7d primitive at the per-memory axis on the cost dimension); the dashboard MemoryList computes a `costSparkline7dCents` per entry using the existing `trajectoryDayKeys` array (computed once for the rev-54 task cost trajectory) so the per-row work is a single Map lookup. Pinned + importance>=9 entries skip the sparkline since they're load-bearing by design. Hidden when every value is zero so fresh + never-attributed memory rows don't see clutter.
- GET /api/v1/memory/{id}/cost-trajectory v1 endpoint + extended GET /api/v1/memory listing with cost7dCents derived field + extended GET /api/v1/memory/top-cost rows with trajectory7d. Closes the protocol-bound surface for the rev-160 trajectory primitive in lockstep with the dashboard. New bearer-auth `GET /api/v1/memory/{memoryId}/cost-trajectory?days=7` endpoint returns trailing N daily cost buckets oldest → newest with zero-fill (max days=30, default 7). Each row carries `{ date: ISO YYYY-MM-DD in workspace TZ, inputTokens, outputTokens, estimatedCostUsd }` so MCP hosts have the full shape without a follow-up estimateRunCostUsd call. Mirrors `GET /tasks/{id}/cost-trajectory` (rev 54) + `GET /memory/{id}/retrieval-trajectory` (rev 157) at the per-memory axis on the cost dimension. Plus the rev-153 `GET /api/v1/memory` listing endpoint now projects a `cost7dCents` derived field (sum of trailing 7 days of dailyCostHistory in cents) alongside the rev-158 `retrievals7d` — MCP hosts ranking memory by recent-activity-on-cost vs all-time see both signals in one call. Plus the rev-159 `GET /api/v1/memory/top-cost` rows now carry `trajectory7d` cents arrays so the dashboard panel + MCP hosts render the inline sparkline without a follow-up call per entry.
- OpenAPI 3.1 typed coverage on the rev-160 endpoint + extended MemoryEntry schema with cost7dCents — 82nd unbroken cadence rev. The OpenAPI 3.1 spec types the new `GET /memory/{memoryId}/cost-trajectory` endpoint with full request/response schemas (memoryId path param + days query param 1-30 default 7 + response shape with `trajectory[]` array of typed rows including date ISO YYYY-MM-DD + inputTokens + outputTokens + estimatedCostUsd). The shared `MemoryEntry` schema component picks up the new `cost7dCents` derived field so MCP-host code generators reading the spec see it on every memory listing surface. The rev-159 `/memory/top-cost` row schema picks up the `trajectory7d` cents array. The OpenAPI spec changelog header gains a rev-160 block explaining the trajectory-axis closure on the cost dimension. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 82nd unbroken rev with rev 160. The per-memory observability cluster on the protocol-bound surface is now fourteen axes deep (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154 + archive-warning rev 155 + per-tag-stale rev 156 + retrieval-trajectory rev 157 + top-retrieved rev 158 + top-cost rev 159 + cost-trajectory rev 160).
Per-memory cost attribution + top-cost memory dashboard panel + GET /api/v1/memory/top-cost closes the named rev-158 next-sprint candidate (memoryEntries.totalAttributedInputTokens + totalAttributedOutputTokens columns + pulse engine equal-split distribution + getTopCostMemoryEntries helper + brand-amber TopCostMemoryPanel sidebar panel + GET /api/v1/memory/top-cost v1 endpoint + extended GET /api/v1/memory listing with cost fields + OpenAPI 3.1 typed coverage on the new endpoint and MemoryEntry schema — 81st unbroken cadence rev)
- Per-memory cost attribution columns + pulse engine equal-split distribution. Closes the named rev-158 next-sprint candidate ('memory retrieval cost attribution — letting per-memory entries carry an estimated AI cost contributed metric so operators triaging a load-bearing-but-token-heavy memory entry can see the full picture'). Two new columns on memory_entry: `totalAttributedInputTokens` + `totalAttributedOutputTokens` (integer NOT NULL default 0). The pulse engine's `workNextTask()` distributes each cycle's per-task token delta across every memory entry retrieved that cycle by equal split — same defensible methodology as the rev-57 per-source cost attribution. Single batched UPDATE rides one round-trip per cycle so the cost-attribution write costs nothing on the steady-state retrieval path. Pure additive — does not change retrieval semantics, only attaches a cost stamp to each retrieved row.
- Top-cost memory dashboard panel + GET /api/v1/memory/top-cost. New `getTopCostMemoryEntries()` helper sorts memory entries desc by cumulative attributed cost. New brand-amber `TopCostMemoryPanel` sidebar mounts beside the rev-158 brand-purple `TopRetrievedMemoryPanel` so the three memory observability panels (slate-staleness rev 153 + brand-purple-retrieval rev 158 + brand-amber-cost rev 159) stack cleanly with one consistent vocabulary at three distinct attention levels. Each row shows `💸 $X.XX` cost amount, kind chip, pinned flag, title, proportional brand-amber bar, plus a meta line with token total + retrieval count + importance + tags. Hidden when no memory entry has accrued any attributed cost yet (fresh workspaces or workspaces that never run AI cycles). New bearer-auth `GET /api/v1/memory/top-cost?limit=5` v1 endpoint mirrors the dashboard primitive in lockstep — MCP hosts answering 'which memory entries are the AI's most expensive?' get a one-call answer.
- Extended GET /api/v1/memory listing with totalAttributedInputTokens, totalAttributedOutputTokens, and estimatedCostUsd. Closes the cost-axis projection on the per-memory listing surface at parity with the per-task axis (rev 51's totalInputTokens + totalOutputTokens projected on /api/v1/tasks). Every row on the rev-153 listing endpoint now carries `totalAttributedInputTokens`, `totalAttributedOutputTokens`, and `estimatedCostUsd` alongside the rev-153 `retrievalCount` + `lastRetrievedAt` + rev-158 `retrievals7d`. MCP hosts ranking memory by AI cost without a follow-up call per entry — three retrieval-state signals (cumulative + recency + recent-activity) plus three cost-attribution signals (input + output + USD) on every memory listing row.
- OpenAPI 3.1 typed coverage on the rev-159 endpoint + extended MemoryEntry schema — 81st unbroken cadence rev. The OpenAPI 3.1 spec types the new `GET /memory/top-cost` endpoint with full request/response schemas (limit query param 1-20 default 5 + response shape with `memory[]` array of typed rows including memoryId + kind enum + title + importance + pinned + tags + totalAttributedInputTokens + totalAttributedOutputTokens + estimatedCostUsd + retrievalCount + lastRetrievedAt nullable date-time). The shared `MemoryEntry` schema component picks up the three new cost fields so MCP-host code generators reading the spec see them on every memory listing surface. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 81st unbroken rev with rev 159. The per-memory observability cluster on the protocol-bound surface is now thirteen axes deep — the MCP server's per-memory observability tooling has nothing left to design across all thirteen axes.
Top-retrieved memory dashboard panel + workspace-axis read on the rev-157 trajectory primitive (`GET /api/v1/memory/top-retrieved` endpoint + `retrievals7d` derived field on the rev-153 listing endpoint + brand-purple TopRetrievedMemoryPanel sidebar panel + getTopRetrievedMemoryEntries helper + OpenAPI 3.1 typed coverage on the new field + endpoint — 80th unbroken cadence rev)
- Workspace-axis read on the rev-157 trajectory primitive — top-retrieved memory dashboard panel + GET /api/v1/memory/top-retrieved. Closes the named rev-157 next-sprint candidate ('per-memory retrieval frequency on the v1 listing endpoint as a derived field') at the workspace-axis read shape it most needed. Rev 153 surfaced the *cumulative* retrieval state (lastRetrievedAt + retrievalCount); rev 157 surfaced the *per-memory shape over time* (the trajectory endpoint + sparkline); rev 158 surfaces the *workspace-level top-N* — 'what knowledge is the AI relying on most this week?'. New `getTopRetrievedMemoryEntries()` helper aggregates the rev-157 retrievalHistory JSONB, sorts desc by trailing-7-day retrieval count, returns top N rows (default 5) with per-row `trajectory7d` array so the panel renders the rev-157 sparkline inline without a follow-up call. New brand-purple `TopRetrievedMemoryPanel` sidebar panel mounts above the rev-153 StaleMemoryPanel — operators triaging 'is the AI focused on the right knowledge?' get a top-N answer at a glance instead of expanding the rev-157 sparkline on every memory row. Hidden when nothing has been retrieved in the trailing 7-day window. Mirrors the rev-51 `TopCostTasksPanel` shape (top-N by trailing-window activity) at the per-memory axis on the retrieval dimension. New bearer-auth `GET /api/v1/memory/top-retrieved?limit=5&days=7` endpoint exposes the same shape on the protocol-bound surface in lockstep — MCP hosts answering 'what is the AI relying on?' get a one-call answer.
- `retrievals7d` derived field on the rev-153 GET /api/v1/memory listing endpoint. Closes the named rev-157 next-sprint candidate at the cheapest possible primitive — pure derived field, no schema cost. Every row on the rev-153 listing endpoint now carries `retrievals7d` (sum of trailing 7 days of the rev-157 retrievalHistory in workspace TZ) alongside the rev-153 cumulative `retrievalCount` total + `lastRetrievedAt` recency stamp. MCP hosts ranking memory by recent-activity vs all-time see both signals in one call instead of fetching the rev-157 trajectory endpoint per entry. Same workspace-TZ window the rev-157 trajectory endpoint uses so the two surfaces never drift on what 'this week' means.
- Per-memory observability cluster on v1 reaches twelve axes deep. The rev-158 top-retrieved endpoint pairs with rev-157 retrieval-trajectory (per-memory axis on the trajectory dimension) and rev-153 stale (workspace axis on the staleness dimension) for the complete per-memory observability story across all three axis × dimension cells (per-memory × trajectory rev 157 / per-memory × staleness implicit via rev 153 + 156 / workspace × staleness rev 153 / workspace × trajectory rev 158). The MCP server's per-memory observability tooling now has nothing left to design across all twelve axes (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154 + archive-warning rev 155 + per-tag-stale rev 156 + retrieval-trajectory rev 157 + top-retrieved rev 158). Pure protocol-translation work for the upcoming MCP server.
- OpenAPI 3.1 typed coverage on the rev-158 endpoint + extended MemoryEntry schema — 80th unbroken cadence rev. The OpenAPI 3.1 spec types the new `GET /memory/top-retrieved` endpoint with full request/response schemas (limit query param 1-20 default 5 + days query param 1-30 default 7 + response shape with `memory[]` array of typed rows including memoryId + kind enum + title + importance + pinned + tags + retrievalCount + retrievals7d + trajectory7d array + lastRetrievedAt nullable date-time). The shared `MemoryEntry` schema component picks up the new `retrievals7d` field so MCP-host code generators reading the spec see the derived field on every memory listing surface. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 80th unbroken rev with rev 158. The OpenAPI spec changelog header gains a rev-158 block explaining the workspace-axis closure on the per-memory observability cluster and the cluster's twelve-axis depth on the protocol-bound surface.
Memory retrieval trajectory primitive + per-tag breakdown on the rev-155 memory-archive Slack push closes both rev-156 next-sprint candidates in one cycle (memoryEntries.retrievalHistory column + jsonb_set bump in pulse engine + getMemoryRetrievalTrajectory helper + GET /api/v1/memory/{id}/retrieval-trajectory endpoint + MemoryRetrievalSparkline dashboard component + per-tag breakdown line on memory archive warning Slack push + OpenAPI 3.1 typed coverage on the new endpoint — 79th unbroken cadence rev)
- Per-memory retrieval trajectory — closes the *trajectory* axis on the per-memory observability cluster. Closes the named rev-156 next-sprint candidate ('memory retrieval frequency sparkline'). Rev 153 surfaced the *cumulative* retrieval state (lastRetrievedAt + retrievalCount); rev 157 surfaces the *shape over time*. New `memoryEntries.retrievalHistory` JSONB column (`Record<YYYY-MM-DD, number>` keyed by ISO date in workspace timezone — same shape as the rev-54 task.dailyCostHistory primitive). Pulse engine's `workNextTask()` now bumps today's per-memory bucket via SQL `jsonb_set` alongside the existing rev-153 lastRetrievedAt + retrievalCount writes — single UPDATE covers the entire batch of retrieved memories on each cycle (all rows share the same todayKey), so the rev-157 trajectory primitive ships with zero extra round-trip cost on the retrieval path. New `getMemoryRetrievalTrajectory()` helper in `src/lib/workspaces.ts` returns the trailing N daily counts (default 7, max 30) oldest → newest with zero-fill. New bearer-auth `GET /api/v1/memory/{id}/retrieval-trajectory?days=7` endpoint mirrors the rev-54 `/tasks/{id}/cost-trajectory` shape at the per-memory axis exactly. Pairs with the rev-153 GET /api/v1/memory + GET /api/v1/memory/stale + rev-156 per-tag filter for the full per-memory observability story on the protocol-bound surface — eleven axes deep (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154 + archive-warning rev 155 + per-tag-stale rev 156 + retrieval-trajectory rev 157).
- Inline 7-bar retrieval sparkline beside the rev-153 retrieval-count chip on every memory row. New `MemoryRetrievalSparkline` client component renders a 48×14px 7-bar mini-chart inline beside the rev-153 `MemoryUsageChip` on every memory row whose `retrievalHistory` JSONB has at least one non-zero day. Today is rightmost with a brand-purple highlight; older days fade so the eye reads the trajectory shape, not individual values. Brand-purple palette (`rgba(107, 78, 214, *)`) distinguishes the retrieval sparkline from the rev-54 brand-amber cost sparkline so the dashboard's two trajectory primitives (cost on per-task axis, retrieval on per-memory axis) read at two distinct attention levels. The dashboard server component reuses the existing `trajectoryDayKeys` array (computed once for the rev-54 task cost trajectory) so the per-row work on every memory row is a single Map lookup — pure derived state from the rev-157 column with no extra query. Hidden when every value is zero so fresh + never-pulled memory rows don't see clutter. Pinned + importance>=9 entries skip the sparkline since they're load-bearing by design (matching the rev-153 chip exclusion rules). Closes the *trajectory* axis on the per-memory observability cluster the same way rev-54 closed it on the per-task cost surface — operators triaging a load-bearing memory entry now see both the *cumulative* count (rev 153) and the *shape over time* (rev 157) without leaving the memory list.
- Per-tag breakdown line on the rev-155 memory archive warning Slack push — closes the named rev-156 next-sprint candidate at the chat-channel axis. Closes the named rev-156 next-sprint candidate ('per-tag memory archive warning Slack push scoping'). Rev 156 shipped the per-tag breakdown line on the digest email section + the dashboard StaleMemoryPanel filter; the rev-155 daily Slack push had no equivalent — Slack-first operators saw the workspace-wide warning list but couldn't tell at a glance which workstream's knowledge was about to disappear. Rev 157 closes that. The `buildMemoryArchiveWarningSlackPayload()` helper now accepts optional per-warning `tags: string[]` and surfaces an inline 'by tag: `#q3-launch` (3) `#brand-voice` (2) `#pricing` (1)' breakdown below the per-entry rows when 2+ distinct tags appear in the warning set. Top 4 tags by warning count, ties broken alphabetically for deterministic ordering. Mirrors the rev-156 digest section breakdown logic exactly so dashboard ↔ email ↔ Slack speak one consistent per-tag vocabulary across all three operator-loaded channels. The `pingMemoryArchiveWarnings()` cron sweep now plumbs per-entry tags through to the Slack payload — zero-cost projection because `sweepMemoryArchiveWarnings` already projects tags off the rev-21 `memoryEntries.tags` column. Hidden when fewer than 2 distinct tags appear so the breakdown never reads tautologically as 'everything is #foo'.
- OpenAPI 3.1 typed coverage on the rev-157 retrieval-trajectory endpoint — 79th unbroken cadence rev. The OpenAPI 3.1 spec types the new `GET /memory/{memoryId}/retrieval-trajectory` endpoint with full request/response schemas (memoryId path param + days query param 1-30 default 7 + response shape with `memoryId` + `days` + `trajectory[]` array of `{ date: ISO YYYY-MM-DD in workspace TZ, retrievals: integer }` rows). The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 79th unbroken rev with rev 157. The `/api/v1` self-describing endpoint index documents the new endpoint inline + extends the rev-153 `/memory/stale` description to mention the rev-156 `?tag=` parameter. MCP-host code generators reading the spec see typed contracts for the rev-157 trajectory primitive immediately. The per-memory observability cluster on v1 is now eleven axes deep (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154 + archive-warning rev 155 + per-tag-stale rev 156 + retrieval-trajectory rev 157). The MCP server's per-memory observability tooling has nothing left to design across the eleven axes.
Per-tag memory archive scoping closes the named rev-155 next-sprint candidate at the per-workstream axis on every channel — `?tag=` query param on `/api/v1/memory/stale` + per-tag breakdown line in the rev-155 digest section + tag filter chip row on dashboard StaleMemoryPanel + inline retrieval-count chip on every memory row — 78th unbroken cadence rev
- `?tag=` query param on `GET /api/v1/memory/stale` — closes the named rev-155 next-sprint candidate at the protocol-bound axis. Closes the named rev-155 next-sprint candidate ('per-tag memory archive scoping'). Rev 153 opened the diagnostic surface on the memory entity at the workspace axis (GET /memory/stale + StaleMemoryPanel), rev 154 closed the descriptive→defensive loop (auto-archive sweep), rev 155 closed the per-entry warning push surface — but the rev-21 memory-tag corpus was the missing fourth axis. Operators tagging memory by workstream (#q3-launch, #brand-voice, #pricing) had no way to ask 'which knowledge in *this* workstream is about to fall stale?'. New optional `?tag=` query param on `GET /api/v1/memory/stale` filters via JSONB `@>` array containment so a tag like `q3` doesn't over-match `q3-launch` (mirrors the rev-39 cross-entity tag drill-down semantics + the rev-29 focus-tag boost predicate). `getStaleMemoryEntries()` accepts an optional tag normalised lowercase + length-bounded server-side. Pure additive — undefined returns the workspace-wide list (existing rev-153 behaviour). MCP hosts on multi-workstream desks can now answer 'which #q3-launch knowledge is about to disappear?' programmatically. Mirrors the rev-49 per-recipient `?assignedToUserId=` filter on `/tasks/stale` at the per-tag axis.
- Per-tag breakdown line in the rev-155 memory archive warning digest section — closes the named rev-155 candidate at the email channel. Extended `MemoryArchiveWarningEntry` with optional `tags: string[]` field; the `buildMemoryArchiveWarningSection()` helper now renders an inline 'By tag: #q3-launch (3) #brand (2)' breakdown line above the per-entry rows so operators see *which workstream's* knowledge is about to disappear at-a-glance without scrolling every per-entry row. Top 4 tags by warning count, ties broken alphabetically for deterministic ordering. Brand-purple chip palette distinguishes the breakdown from the existing brand-amber per-entry rows. Hidden when fewer than two distinct tags appear — the breakdown reads tautologically as 'everything is #foo' otherwise. Both the production cron path (`runMemoryArchiveWarnings`) and the rev-36 admin preview path (`previewMemoryArchiveWarnings`) project tags so admins iterating on configuration see the same surface they'll receive in production. Pairs naturally with the rev-156 `?tag=` v1 filter — operators read the per-tag breakdown in the digest, then drill into the matching tag via the dashboard or v1 surface.
- Tag filter chip row on dashboard StaleMemoryPanel — closes the named rev-155 candidate at the dashboard surface. The rev-153 dashboard `StaleMemoryPanel` listed every stale entry workspace-wide; until rev 156 operators with multi-workstream tagged memory had to read every row to find #q3-launch ones. New tag filter chip row above the list lets operators answer 'which #q3-launch knowledge has gone stale?' in one click. Pure client-side filter on top of the existing server-rendered `entries` payload (the rev-156 v1 `?tag=` filter is the load-bearing primitive on the protocol-bound side; this UI surfaces the same scoping on the dashboard side without an extra round-trip). Each chip shows the per-tag count for at-a-glance triage. Hidden when fewer than two distinct tags appear on stale entries. New `.ld-stale-memory-tag-row` + `.ld-stale-memory-tag-chip` CSS uses the brand-purple palette matching the rev-156 digest breakdown chips so the per-tag surface reads with one consistent visual vocabulary across dashboard + email channels.
- Inline retrieval-count chip on the rev-153 memory usage pill — surfaces load-bearing memory entries at-a-glance. The rev-153 `MemoryUsageChip` showed 'used Nd ago' or 'never pulled' but the per-entry retrieval count (the column rev 153 added) was only visible in the tooltip — operators reading the memory list couldn't distinguish *load-bearing* memory entries (used many times) from *drive-by* ones (used once) without hovering each row. Rev 156 closes that gap with a small inline `· N×` chip alongside the existing label, hidden when retrievalCount is 1 (the implicit majority — surfacing it would read as visual noise). Tabular-num typography keeps multi-digit values vertically aligned across rows. Cumulative micro-polish — every rev 22+ has carried at least one — and rev 156's polish is load-bearing because it makes the operator's mental model of 'which memory entries is the AI actually using' visible without expanding the rev-153 tooltip on every row. Pairs with the rev-153 stale-memory panel + rev-154 auto-archive + rev-155 warning + rev-156 per-tag scoping for the complete usage observability surface across every read horizon.
Memory archive warning closes the named rev-154 next-sprint candidate — per-memory pre-archive heads-up via Slack + outbound + email digest section + activity log glyph + retrieval-clears-warning + pin/raise-importance-clears-warning — 77th unbroken cadence rev
- memoryEntries.archiveWarnedAt column + sweepMemoryArchiveWarnings helper — closes the missing edge of the rev-153/154 lifecycle at the per-entry warning axis. Closes the named rev-154 next-sprint candidate ('per-memory retrieval Slack push'). New `memoryEntries.archiveWarnedAt` timestamp column + `sweepMemoryArchiveWarnings()` per-workspace helper that finds non-pinned, importance < 9 memory entries 1-2 days from rev-154 auto-archive (lastRetrievedAt within the warn window of the workspace's staleMemoryAutoArchiveDays threshold) AND haven't been warned yet (`archiveWarnedAt` is null). Mirrors the rev-50 task archive_warning sweep at the per-memory axis exactly. Stamps `archiveWarnedAt` so a second cron tick within the warning window doesn't double-fire. Capped at 25 candidates per sweep so a workspace that just enabled rev-154 auto-archive can't get warning-stormed. Companion `previewMemoryArchiveWarnings()` helper does the same predicate without stamping — used by the rev-36 admin digest preview path so admins iterating on configuration don't pollute the production cron's 'already warned' state. Closes the missing edge of the rev-153/154 lifecycle: workspace-level surface (rev 153 dashboard + per-row chip) + workspace-level closure (rev 154 auto-archive Slack push) + per-entry warning (rev 155).
- memory.archive_warning outbound event + Slack push — workspace-shared, fan-out via runMemoryArchiveWarnings cron sweep. New `OutboundEvent` value `memory.archive_warning` plus matching `dispatchMemoryArchiveWarningWebhook()` dispatcher in `src/lib/outbound.ts`. Mirrors the rev-50 `task.archive_warning` event at the per-memory axis. Workspace-shared — memory entries don't have assignees, so unlike rev-50 task warnings which fire per-task per-assignee, the rev-155 warning lists every imminent entry in one Slack block. Slack post via the new `buildMemoryArchiveWarningSlackPayload()` block with `:alarm_clock: Memory entries about to auto-archive` header + listing each entry with `auto-archives in Nd` + `Nd unused` / `never used` + kind + title + recommendation copy ('Pin entries or raise importance >= 9 to keep'). New `runMemoryArchiveWarnings()` cron sweep added to `runDailyDigest()` IMMEDIATELY BEFORE `runStaleMemoryAutoArchive()` so operators get at least one cron-cycle of heads-up before durable knowledge disappears (same shape rev 50 fires before rev 49). Pure no-op on workspaces without staleMemoryAutoArchive configured. Lets external integrations (knowledge-base mirror, FinOps dashboard, CRM tagging unused brand assets) hear the warning → action push pair (rev 155 warning + rev 154 closure) on the per-memory axis without polling.
- Per-memory archive warning section in the daily digest email + activity-log per-kind glyph + tinting. New `buildMemoryArchiveWarningSection()` digest helper renders up to 6 imminent entries with `⏰ auto-archives in Nd` + kind chip + `Nd unused` / `never used` + recommendation copy in a styled HTML section. Renders alongside the rev-48 stale-tasks section as the per-memory complement of the workspace's two working-set axes (in-flight work + durable knowledge). Closes the email-channel parity gap on the rev-153/154 memory-archive lifecycle so solo founders + email-first operators who don't sit in Slack get the same heads-up Slack-first teams already have. The `runMemoryArchiveWarnings` cron sweep returns a `byWorkspace` Map so the email surface and the Slack/outbound surfaces always agree on which entries fired this cycle (no double-stamping race, no double-query). New `memory_archive_warning` activity-log kind + `KIND_LABEL` + `KIND_GLYPH` (⏰) + `.ld-activity-memory_archive_warning` brand-amber tint mirror the rev-50 task.archive_warning visual vocabulary so operators reading the activity log see warning surfaces for the two working-set axes (in-flight work + durable knowledge) with one consistent affordance.
- Pulse engine clears archiveWarnedAt on retrieval + setMemoryPinned/updateMemoryEntry clear on operator action — warning state stays in lockstep with the operator's decision. Pulse engine's `workNextTask()` now also clears `archiveWarnedAt` in the same UPDATE that stamps `lastRetrievedAt` + bumps `retrievalCount` (rev 153) on every memory entry the AI cycle pulled. A re-engaged memory entry leaves the warning state immediately rather than waiting for the next cron tick. `setMemoryPinned()` clears the stamp on pin (pinned entries are exempt from rev-154 auto-archive — the rev-5 'keep forever' veto). `updateMemoryEntry()` clears the stamp when raising importance to >= 9 (also exempt — load-bearing brand-voice / decision context). The OpenAPI 3.1 spec gains a rev-155 changelog block documenting the new outbound event + the warning-state-clearing rules. The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 77th unbroken rev with rev 155. MCP-host code generators reading the spec see typed contracts for the new event immediately. The MCP server's memory-axis cluster is now nine axes deep (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154 + archive-warning rev 155) with full v1 parity across every axis.
Memory auto-archive primitive closes the named rev-153 next-sprint candidate at the defensive-action axis — descriptive→defensive loop on the memory usage story now closes (rev 153 detect → rev 154 act) — 76th unbroken cadence rev
- staleMemoryAutoArchiveDays column + sweepStaleMemoryAutoArchive helper — load-bearing primitive opens the auto-action surface on memory. Closes the named rev-153 next-sprint candidate ('memory auto-archive primitive'). New `workspace.staleMemoryAutoArchiveDays` integer column (nullable = off, opt-in by design, valid range 30-365 days). New `setStaleMemoryAutoArchiveDays()` helper + `sweepStaleMemoryAutoArchive()` per-workspace sweep helper that finds non-pinned, importance < 9 memory entries unused for the configured threshold and deletes them via DELETE row (memory entries are write-many-read-many, not lifecycle-bearing the way tasks are; the activity log carries the audit trail). Capped at 25 entries per sweep so a long-quiet desk that suddenly enables the feature can't archive 500 memory entries in one cron tick — operators see progress over multiple days, can pause, can re-pin entries they want kept. Mirrors the rev-49 stale-task auto-archive opt-in pattern at the per-memory axis exactly. Pinned (rev 5) and importance >= 9 entries are excluded server-side via getStaleMemoryEntries — they remain the operator's veto. Closes the descriptive→defensive loop on the memory usage story (rev 153 detect + read → rev 154 act).
- Per-workspace sweep wired into runDailyDigest — runs alongside rev-49 task auto-archive on the same cron tick. New `runStaleMemoryAutoArchive()` cron sweep added to `runDailyDigest()` immediately after `runStaleTaskAutoArchive()` so the workspace's two working-set axes (in-flight work + durable knowledge) self-clean uniformly on the same cadence. For each onboarded workspace with staleMemoryAutoArchiveDays set, the sweep calls `sweepStaleMemoryAutoArchive()` then dispatches the new `memory.auto_archived` outbound webhook event + posts the new `buildMemoryAutoArchivedSlackPayload()` block to Slack with the same dead-Slack-webhook auto-clear path as `notifyBriefToSlack()`. Default null (off) preserves rev-153 diagnostic-only behaviour for every workspace until the operator explicitly opts in.
- memory.auto_archived outbound event + Slack push — closure-receipt event mirrors task.auto_archived at the per-memory axis. New `OutboundEvent` value `memory.auto_archived` plus matching `dispatchMemoryAutoArchivedWebhook()` dispatcher in `src/lib/outbound.ts`. Mirrors the rev-49 `task.auto_archived` event at the per-memory axis. Lets external integrations (knowledge-base mirror, FinOps dashboard tracking AI-cost-of-knowledge, CRM tagging unused brand assets) reconcile 'the desk just self-cleaned its working set' without polling workspace state. Slack post via the new `buildMemoryAutoArchivedSlackPayload()` block with `:books: Stale knowledge cleared` header + listing each archived entry with 'Nd unused' / 'never used' + kind + title + recommendation copy ('Pin entries or raise importance >= 9 to keep'). Pairs with the rev-153 stale-memory dashboard chip + GET /api/v1/memory/stale read primitive so operators see both the diagnostic surface and the closure receipt across every channel — dashboard, Slack, outbound webhook.
- PATCH /api/workspace/memory-archive-config + v1 mirror in lockstep + OpenAPI 3.1 typed coverage — 76th unbroken cadence rev. Admin-only `PATCH /api/workspace/memory-archive-config` route + bearer-auth `GET/PUT /api/v1/workspace/memory-archive-config` v1 mirror in lockstep delegate to the same `setStaleMemoryAutoArchiveDays()` helper. Integrations panel UI section under the rev-49 'Queue hygiene' grouping renders the new threshold input (30-365 days, blank = off) with Enable / Update / Disable buttons matching the rev-49 stale-task auto-archive shape exactly so the two queue-hygiene primitives (stale-task auto-archive + stale-memory auto-archive) read as siblings on every operator-loaded surface. The OpenAPI 3.1 spec types both new endpoints with full request/response schemas (staleMemoryAutoArchiveDays integer 30-365 nullable). The cadence pattern from rev 78 onward (every dashboard primitive gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 76th unbroken rev with rev 154. MCP-host code generators reading the spec see typed contracts for the rev-154 auto-archive primitive immediately. The MCP server's memory-axis cluster on the protocol-bound surface is now eight axes deep (read rev 12 + write rev 12 + bulk-update rev 35 + reactions rev 33 + tags rev 21 + export rev 125 + stale rev 153 + auto-archive rev 154) with full v1 parity to the dashboard surface across every axis.
Memory retrieval tracking + stale-memory dashboard panel + GET /api/v1/memory/stale opens the diagnostic surface on the memory entity at the usage axis — 75th unbroken cadence rev
- memoryEntries.lastRetrievedAt + retrievalCount columns + pulse engine plumbing — load-bearing primitive opens the usage axis on memory. Until rev 153 the memory entity was *write-only* on the usage axis — operators added memory through 'Teach the desk' (rev 6), bulk import (rev 22), promote-from-output (rev 26), or auto-saved on artifact approval, but the workspace had no read surface answering 'which memory entries is the AI actually still using?'. Rev 153 closes that gap. Two new columns on `memory_entry`: `lastRetrievedAt` (timestamp, nullable) + `retrievalCount` (integer, default 0). The pulse engine's `workNextTask` already calls `retrieveRelevantMemory` once per cycle and stamps the result on `task.sourceMemoryIds` (rev 44); rev 153 also stamps every retrieved memory entry's `lastRetrievedAt` + bumps `retrievalCount` via a SQL `+1` clause that rides the same UPDATE — no extra round-trip on the retrieval path. Mirrors the rev-47 stale-task pattern at the memory axis: lastRetrievedAt is to memory what task.updatedAt is to tasks; retrievalCount is the sessionCount equivalent.
- Stale-memory detector + dashboard StaleMemoryPanel — surfaces durable knowledge the AI cycle hasn't pulled in 30+ days. New `getStaleMemoryEntries()` helper + `StaleMemoryPanel` client component rendered as a slate-purple-accented sidebar panel above the existing rev-47 StaleTasksPanel. Surfaces durable knowledge entries the AI cycle hasn't pulled in `thresholdDays` (default 30, range 7-365), either never (when the entry is older than the threshold) or last retrieved before the cutoff. Pinned (rev 5) and high-importance (≥9) entries excluded server-side because they're load-bearing regardless of usage. Each row shows a 'Nd ago' / 'never' pill with three-tone treatment (slate for never, brand-purple for warm, amber for cold) + the kind chip + the retrieval count + tag preview. Hidden when no memory has gone stale (the steady state for healthy workspaces). Mirrors the rev-47 stale-tasks panel at the memory axis with a distinct slate-purple accent so the two surfaces read as siblings without competing visually.
- Per-row 'used Nd ago' usage chip on every memory entry — inline diagnostic affordance. Every non-pinned, non-high-importance memory row in the dashboard memory panel now carries a 'used Nd ago' / 'used today' / 'never pulled' pill alongside the existing kind + importance + pinned chips. Three states distinguish never-pulled (muted-grey, italic) / recently-used (brand-color teal, ≤7 days, affirmative) / older (amber, attention). Operators reading the memory panel can spot dead knowledge inline without expanding the rev-153 stale-memory sidebar panel. `daysSinceRetrieved` is computed server-side in the dashboard render so the client chip renders purely from props (React 19 purity rule forbids Date.now() during render). Pure derived state from the rev-153 columns the pulse engine stamps on every retrieval.
- GET /api/v1/memory/stale + extended GET /api/v1/memory projection + OpenAPI typed coverage — 75th unbroken cadence rev. New bearer-auth `GET /api/v1/memory/stale?thresholdDays=30&limit=12&includeNeverRetrieved=true` mirrors the rev-153 dashboard primitive on the v1 surface in the same cycle the dashboard primitive ships. The cadence pattern from rev 37 onwards (every dashboard primitive has a v1 equivalent within one rev) holds unbroken into rev 153. The existing `GET /api/v1/memory` listing endpoint also gains `lastRetrievedAt` + `retrievalCount` projected on every row so MCP hosts rendering the memory list can render the same 'used Nd ago' affordance without a follow-up call. OpenAPI 3.1 spec types the new endpoint with full request/response schemas (thresholdDays integer 7-365, includeNeverRetrieved boolean, response shape with `staleMemoryEntries[]` carrying every per-row field including ISO date-time formats). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 75th unbroken rev with rev 153. MCP-host code generators reading the spec see typed contracts for the rev-153 stale-memory primitive immediately. Closes the diagnostic gap on the memory entity at the usage axis on every channel — dashboard panel + per-row chip + v1 listing projection + v1 stale endpoint + OpenAPI typed schema.
Per-source pause-until daily Slack push + outbound source.pause_until_warning event closes the named rev-151 next-sprint candidate at the chat-channel axis — 74th unbroken cadence rev
- Per-source pause-until daily Slack push — closes the named rev-151 next-sprint candidate at the chat-channel axis. Rev 150 shipped the per-source schedule write + auto-resume sweep, rev 151 shipped the today-glance chip + email digest section + v1 read endpoint — but Slack-first teams whose operators set scheduled pauses had no morning chat-channel reminder of 'your N feeds resume in M hours.' Rev 152 closes that with a new `buildSourcePauseUntilSlackPayload()` Slack block + `pingSourcePauseUntil()` daily sweep wired into `runDailyDigest()` mirroring the rev-145 quietness sweep shape exactly. Header `:alarm_clock: Per-source scheduled pause` + lists each scheduled-paused source with calm brand-amber `resumes in Nh` (or 'in Nd' for windows beyond 48h) lines + auto-resume copy. Rate-limited via the new `source_pause_until_warning` activity-log kind to once per workspace per 24h with the same dead-Slack-webhook auto-clear path as rev-58 cost-spike + rev-74 chronic-warning + rev-145 quietness. Hidden when no source is scheduled-paused — quiet-day desks see no clutter. Distinct vocabulary from rev-145 quietness Slack push (structural alarm) — pause-until is operator intent (you set this on purpose), so the message reads as a forward-looking lifecycle preview rather than a call-to-action.
- New outbound source.pause_until_warning event in lockstep with the Slack push. New `OutboundEvent` value `source.pause_until_warning` + `dispatchSourcePauseUntilWarningWebhook()` dispatcher fires alongside the Slack push so external integrations (FinOps tool tracking source uptime, project tracker grouped by source-pause windows, CRM mirroring source state) reconcile 'operator scheduled this pause' without polling the dashboard. Payload mirrors the rev-151 `GET /api/v1/sources/scheduled-paused` response shape exactly so an MCP host wiring this event to a downstream integration gets the same `scheduledPaused[]` shape it already sees on the read endpoint. Closes the rev-151 named candidate on the protocol-bound side — the rev-150 scheduled-pause primitive now reaches every operator-loaded channel (in-app rev-151 chip + email rev-151 digest + chat rev-152 Slack + protocol rev-151 GET + rev-152 outbound).
- Activity log glyph + brand-amber tint for the new source_pause_until_warning kind. Activity log gains a ⏰ glyph + brand-amber per-kind tint (`rgba(207,108,58,*)`) on every `source_pause_until_warning` row so operators reading the log scan the per-source axes (cost / chronic-spike / quietness / pause-until) at four distinct attention levels without parsing copy. The ⏰ glyph matches the rev-151 today-glance chip + rev-150 dashboard chip exactly so the activity log + dashboard + digest all speak one pause-until vocabulary. Brand-amber palette distinct from the rev-145 slate quietness palette (structural alarm) — pause-until is operator intent (calm forward-looking lifecycle preview), so the eye reads it at a different attention level than alarm-driven kinds.
- OpenAPI 3.1 typed coverage on the rev-152 outbound event — 74th unbroken cadence rev. The OpenAPI spec changelog header gains a rev-152 block explaining the chat-channel axis closure on the rev-150 scheduled-pause primitive. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 74th unbroken rev with rev 152. The pause-until primitive on v1 is now four axes deep (write rev 150 PATCH + read rev 151 GET + email channel rev 151 + chat channel rev 152) with full OpenAPI typed coverage on every axis. MCP-host code generators reading the spec see typed contracts for the rev-152 push primitive immediately. The rev-150 PATCH + rev-151 GET + rev-151 daily-digest + rev-152 Slack + rev-152 outbound together give the rev-150 scheduled-pause primitive a complete five-channel lifecycle on the protocol-bound + operator-loaded surfaces.
Per-source pause-until digest section + TodayPanel ⏰ chip + GET /api/v1/sources/scheduled-paused closes the named rev-150 next-sprint candidate at the today-glance + email-channel + protocol-bound axes — 73rd unbroken cadence rev
- Per-source scheduled-pause digest section — closes the named rev-150 next-sprint candidate at the email channel. Rev 150 shipped the per-source scheduled-pause write primitive + auto-resume sweep + dashboard chip + v1 PATCH mirror, but the email channel was missing — solo founders + email-first operators who scheduled-paused N feeds via the dashboard had no morning reminder of 'your X feeds resume in M hours.' Rev 151 closes that with a new `buildSourcePauseUntilSection()` helper in `src/lib/digest.ts` that renders up to 6 rows with calm brand-amber `⏰ resumes in Nh` (or 'in Nd' for windows beyond 48h) pills + type chip + auto-resume copy. Workspace-shared (every owner/admin recipient sees the same list since 'these feeds are scheduled-paused' is workspace-level config context, not per-recipient inbox state). Pre-fetched once per workspace via `getScheduledPausedSources()` and reused across every recipient. Wired into both `runDailyDigest` (cron production path) and `previewDigestForUser` (rev-36 admin testing path) so admins iterating on configuration see the same surface they'll receive in production. Distinct vocabulary from the rev-146 quietness section — quietness is a structural alarm, scheduled-pause is operator intent.
- PauseUntilSummary chip on TodayPanel for at-a-glance morning visibility. New ⏰ chip on the rev-33 TodayPanel below the rev-147 quietness chip + rev-75 chronic chip. Renders only when ≥1 source is scheduled-paused. Tap to scroll to the rev-1 sources panel where the rev-150 per-row chip + popover live for adjustment. Calm brand-amber palette `rgba(207,108,58,*)` distinct from the rev-147 slate quietness chip + rev-75 amber-on-amber chronic chip + rev-32 brand-red cost-spike alarm so operators read the four structural-state horizons at four distinct attention levels. Scheduled-pause is operator intent (you set this on purpose) rather than an alarm, so the visual hierarchy stays calm — no pulsing dot, no urgent red, just an ambient affordance. Closes the today-glance axis on the rev-150 primitive in lockstep with the email channel.
- GET /api/v1/sources/scheduled-paused — protocol-bound aggregate read in lockstep. New bearer-auth aggregate read endpoint returns workspace-wide list of all sources whose `pausedUntilAt` is set AND still in the future, sorted by resume-soonest first so MCP hosts rendering 'your N feeds resume in M hours' surface the most-imminent first. Each row carries `sourceId`, `label`, `type`, `pausedUntilAt` ISO, `hoursUntilResume`. Pure read of the rev-150 column — no schema cost, no migration. Distinct from /sources/quietness (rev 144 — structural-alarm axis) and /sources/cost-spikes (rev 58 — cost-spike-alarm axis). Scheduled-pause is the operator-intent axis. The rev-150 PATCH /sources/{id}/pause-until + rev-151 GET /sources/scheduled-paused together give the MCP server's per-source-pause tooling the full read + write lifecycle on the protocol-bound surface.
- OpenAPI 3.1 typed coverage on the rev-151 endpoint — 73rd unbroken cadence rev. The OpenAPI spec types the new GET `/sources/scheduled-paused` endpoint with full response schema (totalScheduledPaused integer + perScheduledPaused array of typed rows including pausedUntilAt date-time format and hoursUntilResume integer). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 73rd unbroken rev with rev 151. The OpenAPI spec changelog header gains a rev-151 block explaining the today-glance + email-channel + protocol-bound axes closure on the rev-150 primitive. The `/api/v1` self-describing endpoint index documents the new endpoint inline. MCP-host code generators reading the spec see typed contracts for the rev-151 read primitive immediately.
Per-source scheduled pause-until closes the long-outstanding gap on the source-management primitive cluster, diversifying away from the 6-rev quietness cluster — PATCH /api/sources/{id}/pause-until + v1 mirror + dashboard chip with 5 preset windows + auto-resume sweep wired into runDailyDigest + runDeskPulse + OpenAPI 3.1 typed coverage in lockstep — 72nd unbroken cadence rev
- Per-source scheduled pause-until — closes the long-outstanding gap on the source-management primitive cluster. Rev 21 shipped scheduled-pause at the *workspace* level (5 preset windows + auto-resume sweep), but operators muting a noisy LinkedIn bridge during a launch week, a partner source during a holiday window, or a chatty RSS feed while reviewing the signal mix had no per-source equivalent — they paused manually via rev-6 and had to remember to re-enable. Rev 150 closes that with a deterministic 'auto-resume on Friday 9am' primitive distinct from rev-6 manual pause (no schedule, indefinite), rev-141 cadence override (still polls, just slower), rev-145 quietness ack (separate axis), and rev-62 chronic auto-pause (alarm-driven). New `source.pausedUntilAt` timestamp column + `setSourcePauseUntil()` helper validates 14-day max window + `sweepSourcePauseExpiries()` auto-resume helper. Mirrors the rev-21 workspace pauseUntilAt pattern at the per-source axis exactly — same 14-day max window, same 5-preset popover vocabulary.
- Dashboard chip + popover with 5 preset windows + RSS poller integration via existing pending-status filter. New `SourcePauseUntilButton` client component mounts on every source row alongside the rev-6 SourceActions + rev-95 SourceRename. Five preset windows (1h / 4h / tomorrow 9am / next Monday 9am / 1 week) match the rev-21 workspace pause-until vocabulary. When pausedUntilAt is set + still in the future, the chip renders as brand-amber 'Paused {Nh}' with click-to-extend + 'Resume now' actions; when clear, the chip renders as muted '⏰ Schedule pause' affordance available to editor+ roles only. The RSS poller integration is implicit — `setSourcePauseUntil` flips the source's status to `pending` and the rev-5 `pollRssSourcesForWorkspace` already filters `status !== 'pending'` so the existing skip path handles the schedule naturally. Pure reuse of the rev-6 status filter; no new poller logic.
- Auto-resume sweep wired into runDailyDigest + runDeskPulse. New `sweepSourcePauseExpiries()` helper finds every source with `status='pending'` AND `pausedUntilAt <= now`, flips status back to `connected`, clears the column, and writes an activity-log entry. Wired into both the daily digest cron (alongside rev-21 workspace pause sweep + rev-49 stale-task auto-archive + rev-148 quietness sweeps) AND the rev-1 cron pulse `runDeskPulse` so cron-driven workspaces don't have to wait for the next daily digest to recover an expired source schedule. Pure no-op on workspaces with no scheduled pauses (filters at the SQL level on a non-null + lte predicate).
- v1 mirror + OpenAPI 3.1 typed coverage on the rev-150 endpoint — 72nd unbroken cadence rev. New bearer-auth `PATCH /api/v1/sources/{sourceId}/pause-until` mirrors the dashboard endpoint exactly (same Zod schema accepting `{ pausedUntilAt: ISO | null }` with the 14-day max window, same `setSourcePauseUntil` delegation). Indexed in the `/api/v1` self-describing endpoint list. OpenAPI 3.1 spec types the new endpoint with full request schema (pausedUntilAt nullable string format date-time) + response schema (ok + sourceId + pausedUntilAt + status) + 400 error path (invalid timestamp). The cadence pattern from rev 78 onward (every dashboard primitive has a v1 equivalent typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 72nd unbroken rev with rev 150. The OpenAPI spec changelog header gains a rev-150 block explaining the long-outstanding gap closure on the source-management cluster.
Bulk per-source quietness un-ack closes the un-mute symmetry on the rev-146 bulk-ack pattern (POST /api/sources/quietness-unack/bulk + v1 mirror + dashboard SourceQuietnessBulkUnack chip + OpenAPI 3.1 typed coverage — 71st unbroken cadence rev)
- Bulk per-source quietness un-ack — closes the un-mute symmetry on the rev-146 bulk-ack pattern. Rev 148 shipped the single un-ack endpoint (operators could revoke a rev-145 quietness ack one source at a time) but the inline-vs-batch symmetry was open: an operator who bulk-muted N feeds via the rev-146 chip realising one or more are actually critical (the customer-onboarding RSS that just started publishing again, the partner's release feed that woke up after 3 weeks) still had to click the rev-148 chip on every row. Rev 149 closes that with `POST /api/sources/quietness-unack/bulk` + a matching `POST /api/v1/sources/quietness-unack/bulk` v1 mirror in lockstep. New `bulkUnacknowledgeSourceQuietness()` helper caps at 50 source IDs per call; only sources whose `quietnessAckedAt` stamp is currently non-null are revoked at the SQL layer, so passing already-clean sources is a silent no-op (the dispatch count matches actually-changed rows — no closure receipts fire for sources that were never acked).
- SourceQuietnessBulkUnack dashboard chip + brand-color teal palette. New `SourceQuietnessBulkUnack` client component mounts inside the rev-142 source cadence summary block alongside the rev-146 SourceQuietnessBulkAck chip when 2+ feeds are currently muted. Brand-color teal palette mirrors the rev-148 single un-ack chip so the inline + bulk un-mute affordances read as siblings (un-mute vocabulary) distinct from the rev-146 amber bulk-ack chip (mute vocabulary). Two sibling bulk surfaces, two sibling colour stories, one consistent structural-quietness mental model. New `.ld-source-quietness-bulk-unack` CSS uses the same brand-color teal palette (`rgba(31,143,137,*)`) as the rev-148 single un-ack so the structural-quietness un-mute vocabulary is uniform regardless of single vs bulk shape, with hover lift + focus-visible ring + success-flash matching the existing chip vocabulary.
- Per-item closure-receipt fan-out — same shape as the rev-148 single un-ack. Bulk un-ack fires one `source.quietness_unacked` outbound event per actually-revoked source — keeps the closure-receipt contract uniform with single un-ack calls (rather than inventing a divergent bulk-shape event). Mirrors the rev-87 source chronic bulk-ack + rev-146 quietness bulk-ack closure-receipt fan-out exactly. Activity-log entry records the bulk action with a single line summarising the count + 'mute revoked' framing so the audit trail captures both lineage and operation. External integrations (FinOps tool, monitoring dashboard, CRM mirroring source state) reconcile the un-mute as N independent closure events, just as they would if the operator had clicked the rev-148 chip N times.
- OpenAPI 3.1 typed coverage on the new endpoint — 71st unbroken cadence rev. OpenAPI spec types the new `POST /sources/quietness-unack/bulk` endpoint with full request schema (sourceIds[≤50] required, min 1) + response schema (ok + revokedCount + revokedSources[]). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 71st unbroken rev with rev 149. The OpenAPI spec changelog header gains a rev-149 block explaining the inline-vs-batch un-mute symmetry closure on the per-source quietness lifecycle. The `/api/v1` self-describing endpoint index + `/docs` page both document the new endpoint inline so MCP-host integrators reading the index discover it without opening the spec. The full per-source quietness lifecycle on v1 is now five axes deep: visibility (rev 144) + ack (rev 145) + bulk-ack (rev 146) + un-ack (rev 148) + bulk-un-ack (rev 149).
Per-source quietness un-ack closes the un-mute gap on the rev-145 ack closure (POST /api/sources/{id}/quietness-unack + v1 mirror in lockstep + new closure-receipt source.quietness_unacked outbound event + dashboard chip on every muted source row + cross-row drag on the rev-38/124 dependency graph + OpenAPI 3.1 typed coverage — 70th unbroken cadence rev)
- Per-source quietness un-mute — revoke a rev-145 ack before its 7-day TTL expires. Operators occasionally realise that a feed they ack-muted via the rev-145 chip is genuinely critical (e.g. a customer-onboarding RSS feed they're waiting on) and want to revoke the mute *before* the rev-145 7-day TTL expires. Until rev 148 the only path was to wait the window out — rev 148 collapses that to one click. New `unacknowledgeSourceQuietness()` helper sets `source.quietnessAckedAt = null`, writes an activity-log entry, and fires the new rev-148 `source.quietness_unacked` outbound event. New `POST /api/sources/{sourceId}/quietness-unack` route + matching `POST /api/v1/sources/{sourceId}/quietness-unack` v1 mirror in lockstep. New `SourceQuietnessUnackButton` client component mounts directly beside the rev-145 ack-muted chip on every source row whose `quietnessAckedAt` is still inside the 7-day TTL. Brand-color teal palette (vs rev-145 amber) so the two chips read as siblings: amber = 'I see this, mute 7d', teal = 'I changed my mind, un-mute now'. Returns 404 when there's no ack stamp to revoke — no-op un-ack returns 404 rather than silently succeeding so the audit-log noise stays honest.
- source.quietness_unacked outbound event — symmetric closure receipt for the rev-145 ack closure. New `OutboundEvent` value `source.quietness_unacked` + `dispatchSourceQuietnessUnackedWebhook()` dispatcher fires whenever an operator revokes a rev-145 quietness ack. Mirrors the rev-37 `task.unblocked` closure pattern at the un-mute axis. Until rev 148 downstream integrations watching the rev-145 `source.quietness_acked` event saw 'alarm muted' but never 'alarm un-muted' — they'd have to either poll the ack stamp or accept stale closure state. Rev 148 closes the loop. The full per-source quietness lifecycle is now four-axis on every push channel — visibility (rev 144 GET /sources/quietness) → ack (rev 145 source.quietness_warning + source.quietness_acked) → bulk-ack (rev 146 closure receipt fan-out) → un-ack (rev 148 source.quietness_unacked). External integrations (FinOps tool, monitoring dashboard, CRM mirroring source state) can now reconcile detected → acked → silenced → un-muted on every quietness alarm without polling.
- Cross-row drag on the rev-38/124 dependency graph — closes the named rev-124 next-sprint candidate. Until rev 148 the rev-124 drag-to-reorder primitive only worked *within* a single dependent row. An admin who realised 'task A no longer blocks B; it now blocks C' had to remove A from B's blockers via the × button + scroll to C's task card and re-add A from the picker. Rev 148 collapses that to one drag: drag A from row B's blockers and drop it onto row C's blockers area (or onto a specific blocker in row C to insert at that position). The move is atomic at the operator surface but ships as two PUTs (one per affected row); the optimistic override map handles both rows so the UI shows the moved state before either round-trip completes. Self-blocking is silently rejected (you can't drop A onto row A — visualised with a red `is-cross-rejected` border + cursor:not-allowed); duplicate drops fall through to a single source-side remove. New `is-cross-target` row treatment uses the same brand-color outline + soft gradient as the rev-22+ design language so the cross-row drop target reads as a sibling of the rev-124 intra-row reorder accent. Closes the long-outstanding cross-task operator-direction surface at the dependency-graph axis (the rev-124 named candidate has been outstanding for 24 revs).
- OpenAPI 3.1 typed coverage on the rev-148 endpoint — 70th unbroken cadence rev. Closes the typed-contract gap on the rev-148 v1 endpoint in the same cycle the dashboard primitive ships. The OpenAPI spec types the new `POST /sources/{sourceId}/quietness-unack` endpoint with full response schema (ok + revokedAt date-time + 404 for no-op). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 70th unbroken rev with rev 148. The OpenAPI spec changelog header gains a rev-148 block explaining the un-mute axis closure on the per-source quietness lifecycle. The `/api/v1` self-describing endpoint index documents the new endpoint inline + the new outbound event so MCP-host integrators reading the index discover both without opening the spec. The activity log gains a `source_quietness_unacked` glyph (☼) + brand-color teal tint distinct from the rev-145 ack tint so the operator scanning the log can tell mute vs un-mute apart at a glance.
Per-source quietness summary chip on TodayPanel closes the named rev-146 next-sprint candidate at the today-glance axis (slate 🌙 chip + GET /api/v1/sources/quietness response shape extended with quietSourcesActive + quietnessAckedAt projection + OpenAPI 3.1 typed coverage in lockstep — 69th unbroken cadence rev)
- Per-source quietness summary chip on TodayPanel — closes the named rev-146 next-sprint candidate. Until rev 147 the rev-144 quiet-source count was visible only on the rev-142 cadence summary block above the source list — operators glancing at the dashboard top-of-page (rev-33 TodayPanel) saw the rev-75 ChronicSummary chip when chronic cost spikes fired but had no equivalent for the structural-quietness axis. Rev 147 collapses that to a slate 🌙 N-source chip beside the rev-75 amber chip. New `QuietnessSummary` subcomponent in TodayPanel reads the new `quietSourcesActive` field on the dashboard payload (count of quiet sources whose rev-145 ack stamp is null OR expired), tap to scroll to the rev-1 sources panel where every per-row 🌙 chip + rev-145 ack chip + rev-146 bulk-ack chip live for triage. Hidden when count is 0 so quiet-day desks never see clutter. Pairs with rev-32 cost-spike banner + rev-75 chronic-warning summary as the third structural-alarm horizon on the morning-check surface — three orthogonal alarms (descriptive, chronic, quietness) in one consistent chip vocabulary at three distinct attention levels.
- GET /api/v1/sources/quietness response shape extended with quietSourcesActive + quietnessAckedAt projection. The dashboard primitive ships with the v1 mirror in lockstep (the rev-37 cadence pattern holds unbroken through rev 147 — every dashboard primitive has a v1 equivalent within one rev). New `quietSourcesActive` field on the `/sources/quietness` response is the count of quiet sources whose rev-145 ack stamp is null OR has expired (i.e. not currently ack-muted). MCP hosts rendering 'how many quiet feeds need attention?' read this directly instead of enumerating perQuietSource and filtering client-side. Always <= quietSources. Also closes a small rev-145 bug: the route never projected `quietnessAckedAt` per-row even though the OpenAPI spec documented it (and the rev-145 column + computeSourceQuietness logic populated it). MCP hosts wanting to render the rev-145 muted-state per-source were getting null on every row; rev 147 closes that.
- OpenAPI 3.1 typed coverage on the rev-147 response shape — 69th unbroken cadence rev. Closes the typed-contract gap on the rev-147 v1 enhancement in the same cycle the dashboard primitive ships. The OpenAPI spec types the new `quietSourcesActive` field (integer, required) on the `/sources/quietness` response shape. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 69th unbroken rev with rev 147. The OpenAPI spec changelog header gains a rev-147 block explaining the today-glance axis closure on the per-source quietness surface. MCP-host code generators reading the spec see a typed contract for the rev-147 today-glance primitive immediately + the rev-145 muted-state per-row primitive that was previously documented but unprojected.
- Visual polish — slate-palette quietness chip distinct from amber chronic + brand-amber cost. Cumulative micro-polish (every rev 22+ has carried at least one) — and rev 147's polish is load-bearing because the morning-check surface (TodayPanel) now reads three structural-alarm horizons (chronic / cost-spike / quietness) on one panel. The slate palette (`rgba(90,100,120,*)`) sits at a distinctly lower attention level than the rev-75 amber chronic chip and the rev-32 brand-amber cost-spike alarm — quietness is the *quietest* of the three structural alarms because the action it suggests (consider removing/replacing the feed) is structural and rarely time-sensitive. The three chips together give the operator's eye a complete morning-glance picture: chronic-cost rising (amber, urgent), cost spiking (brand-amber, today's anomaly), feeds dormant (slate, structural). New `.ld-today-quiet-summary` CSS shares the same chip shape as the rev-75 chronic chip + rev-101+ share-affordance chip cluster so the dashboard's chip vocabulary stays uniform. Tactile hover + `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern.
Per-source quietness daily-digest section + bulk-ack endpoint closes the named rev-145 next-sprint candidate at the email channel + the bulk-action axis (buildSourceQuietnessSection + bulkAcknowledgeSourceQuietness helper + POST /api/sources/quietness-ack/bulk + v1 mirror in lockstep + dashboard SourceQuietnessBulkAck chip alongside the rev-142 cadence summary + OpenAPI 3.1 typed coverage on the new bulk endpoint — 68th unbroken cadence rev)
- Per-source quietness section in the daily digest email — closes the named rev-145 next-sprint candidate on the email channel. New `buildSourceQuietnessSection()` helper renders up to 5 quiet feeds with `🌙 Nd silent` pills + cadence label + last-polled fact + per-feed recommendation copy. Workspace-shared (every owner/admin recipient sees the same list since 'this feed is structurally silent' is workspace-level diagnostic context). Skips ack-muted feeds (the rev-145 7-day TTL) so a triaged source doesn't keep showing up in the daily email. Pre-fetched once per workspace via the rev-144 `getSourceQuietnessOverview()` and reused across every recipient. Wired into both the cron production loop (`runDailyDigest`) and the rev-36 admin preview path (`previewDigestForUser`) so admins iterating on configuration see the same surface they'll receive in production. Rendered alongside the rev-75 chronic-warning sections — both are workspace-shared structural-alarm horizons, distinct vocabularies (chronic-cost vs absent-signal). Closes the structural-alarm cluster's third axis on the email channel — chronic-cost (rev 75 source/assignee/tag) + chronic-quietness (rev 146 sources). Solo founders + email-first operators who don't have the dashboard tab open and don't sit in Slack now get the same heads-up.
- Bulk per-source quietness ack — closes the named rev-145 next-sprint candidate at the bulk-action axis. New `bulkAcknowledgeSourceQuietness()` helper + `POST /api/sources/quietness-ack/bulk` route + matching `POST /api/v1/sources/quietness-ack/bulk` v1 mirror in lockstep + `SourceQuietnessBulkAck` client component mounted inside the rev-142 source cadence summary block beside the rev-144 'N quiet' stat when 2+ feeds are quiet AND at least one isn't already ack-muted. One click acks every visible quiet source for the rev-145 7-day TTL — the most common operator triage decision when the rev-146 morning digest lands with multiple dormant feeds is 'mute them all and review next quarter' rather than 'decide each one individually.' Caps at 50 source IDs per call. Mirrors the rev-87 source chronic bulk-ack pattern at the structural-quietness horizon — same 7-day TTL, same per-item closure-receipt fan-out so downstream integrations see the same shape they get from a single ack. Closes the inline-vs-batch ack symmetry on the structural-quietness axis (rev 145 single + rev 146 bulk).
- v1 mirror + dashboard polish — brand-amber bulk-ack chip palette consistent with rev-145 single ack. Bearer-auth `POST /api/v1/sources/quietness-ack/bulk` mirrors the rev-146 dashboard endpoint exactly. Indexed in `/api/v1` self-describing index. New `.ld-source-quietness-bulk-ack` CSS uses the same brand-amber palette (`rgba(207, 108, 58, *)`) as the rev-145 single ack so the structural-quietness ack vocabulary is uniform regardless of single vs bulk shape, but slightly tighter padding so the chip reads as a sibling to the rev-142 cadence summary stat chips alongside it rather than a primary action. Brand-color teal `is-success` state mirrors the rev-145 single-ack flash on the same surface. New `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. Cumulative micro-polish (every rev 22+ has carried at least one) — and rev 146's polish is load-bearing because it surfaces the bulk-action affordance at the highest-attention dashboard moment (the morning glance at the rev-142 cadence summary block).
- OpenAPI 3.1 typed coverage on the rev-146 endpoint — 68th unbroken cadence rev. Closes the typed-contract gap on the rev-146 v1 endpoint in the same cycle the dashboard primitive ships. The OpenAPI spec types the new POST `/sources/quietness-ack/bulk` endpoint with full request schema (sourceIds[≤50] required, min 1) + response schema (ok + acknowledgedCount + acknowledgedSources[]). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 68th unbroken rev with rev 146. The OpenAPI spec changelog header gains a rev-146 block explaining the bulk-action axis closure on the per-source quietness surface. MCP-host code generators reading the OpenAPI spec see a typed contract for the rev-146 bulk-ack primitive immediately. The per-source observability cluster on v1 now closes the inline-vs-batch ack symmetry at the structural-quietness axis (rev 145 single + rev 146 bulk) just as it had previously closed it on the chronic-cost axis (rev 72 single + rev 87 bulk).
Per-source quietness alarm cluster closes the named rev-144 next-sprint candidate (acknowledgeSourceQuietness helper + POST /api/sources/{id}/quietness-ack + v1 mirror in lockstep + daily Slack push via pingSourceQuietness + outbound source.quietness_warning + closure-receipt source.quietness_acked + dashboard ack chip + ack-muted chip palette + activity-log glyph + OpenAPI typed coverage — 67th unbroken cadence rev)
- Per-source quietness ack (workspace + v1) — closes the named rev-144 next-sprint candidate at the operator counter-action axis. New `source.quietnessAckedAt` timestamp column + `acknowledgeSourceQuietness()` helper + `POST /api/sources/{sourceId}/quietness-ack` route + matching `POST /api/v1/sources/{sourceId}/quietness-ack` v1 mirror in lockstep. New `SourceQuietnessAckButton` client component mounts directly beside the rev-144 🌙 quiet pill on every quiet source row whose ack stamp is older than the rev-145 7-day TTL. The chip stays visible on the row when ack-muted but switches to a brand-color teal palette + 'muted' suffix so the visual hierarchy reads as 'I know about this, intentionally ignored' rather than 'alarm pending action.' Distinct from rev-6 pause (permanent until resumed), rev-26 keyword filters (per-item gating), rev-72 chronicAckedAt (cost-spike axis). Quietness ack = 'I see this is silent, intentionally, mute the alarm for 7 days.' 7-day TTL matches the rev-72 chronic-ack cadence (structural problem, not transient).
- Daily Slack push + outbound `source.quietness_warning` event for chronically quiet feeds. New `pingSourceQuietness()` cron sweep added to `runDailyDigest()` mirroring the rev-58 source cost-spike sweep shape. For every onboarded workspace, fetches the rev-144 `getSourceQuietnessOverview()`, filters out ack-muted sources within the rev-145 7-day TTL, and (a) pings Slack via the new `buildSourceQuietnessWarningSlackPayload()` block (`:new_moon: Per-source quietness` header + listing each quiet source with `Nd silent` + label + type + last-polled fact + recommendation copy), (b) dispatches the new `source.quietness_warning` outbound event via `dispatchSourceQuietnessWarningWebhook()`, (c) writes a `source_quietness_warning` activity-log entry. Rate-limited via the activity log to once per workspace per 24h with the same dead-Slack-webhook auto-clear path as rev-58 cost-spike + rev-74 chronic-warning. Distinct from rev-58 (cost rising, still producing) and rev-74 (cost rising 3+ days running) — quietness fires on *absent* signal across the staleness floor. Three orthogonal structural alarms, one consistent Slack/outbound vocabulary.
- Closure-receipt outbound `source.quietness_acked` event + activity-log per-kind glyph + tint. New `source.quietness_acked` outbound event + `dispatchSourceQuietnessAckedWebhook()` fires when an operator clicks the rev-145 ack chip. Mirrors `source.chronic_warning_acked` (rev 73) at the structural-quietness axis so a downstream integration (CRM, FinOps tool, monitoring dashboard) closes its loop on a muted-quiet source: detected → acked → silenced 7d. Activity log gains a 🌙 glyph + slate per-kind tint for `source_quietness_warning` and a teal tint for `source_quietness_acked` so operators reading the log scan the per-source observability axes (cost / chronic-spike / quietness) at three distinct attention levels without parsing copy.
- OpenAPI 3.1 typed coverage — 67th unbroken cadence rev. Closes the typed-contract gap on the rev-145 v1 endpoint in the same cycle the dashboard primitive ships. The OpenAPI spec types the new POST `/sources/{sourceId}/quietness-ack` endpoint with full response schema (ok + acknowledgedAt date-time). The existing GET `/sources/quietness` perQuietSource row schema picks up the new `quietnessAckedAt` field (string|null, format date-time) so MCP hosts render muted-state without a follow-up call. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 67th unbroken rev with rev 145. The OpenAPI spec changelog header gains a rev-145 block explaining the operator counter-action closure + dual outbound events (warning + acked) at the structural-quietness axis. The `/api/v1` self-describing endpoint index also documents the new endpoint inline so MCP-host integrators reading the index discover it without opening the spec.
Per-source quietness/staleness surface closes the named rev-143 next-sprint candidate (computeSourceQuietness + getSourceQuietnessOverview helpers + dashboard 🌙 quiet chip on every source row + workspace-wide quiet count on the rev-142 cadence summary block + GET /api/v1/sources/quietness + OpenAPI typed coverage — 66th unbroken cadence rev)
- Per-source quietness detector — closes the named rev-143 next-sprint candidate. New `computeSourceQuietness()` + `getSourceQuietnessOverview()` helpers identify pollable feed-style sources that polled successfully but produced no signals beyond the staleness threshold. Threshold is max(14d workspace floor, 2× cadence-interval-in-days) capped at 90d so a 1-hour-cadence feed needs ~14 days of silence to register quiet but a daily feed gets the same 14-day floor (a daily feed silent for 25 hours is normal). Distinct from rev-16 loud failure (lastErrorMessage), rev-58 cost-spike (loud cost), and rev-12 7-day signal counter (descriptive). Quietness is the *prescriptive* answer that load-bearing-feed-or-not can be answered without reading every source row by hand. Closes the named rev-143 next-sprint candidate.
- Dashboard 🌙 quiet chip on every quiet source row + workspace-wide quiet count on the rev-142 cadence summary. Every pollable source whose quietness threshold has been crossed gets a slate-palette `🌙 quiet Nd` pill inline in the rev-1 source row pill cluster, with a `cursor: help` tooltip carrying the full reason (days since last signal + days since last poll + recommendation). The rev-142 cadence summary block above the source list gains a `N quiet` stat alongside the existing pollable / on-workspace-cadence / with-override / due-now stats so operators see structural silence at the same vertical position as structural cadence. Slate palette distinct from rev-142 brand-color cadence pills, rev-58 brand-red cost pills, and rev-16 brand-red error pills so the four observability axes (cadence / quietness / cost / health) read at distinct attention levels.
- GET /api/v1/sources/quietness — protocol-bound aggregate read in lockstep with the dashboard primitive. New bearer-auth aggregate endpoint returns workspace cadence floor + per-source breakdown of quiet feeds sorted by daysSinceLastSignal descending so the silentest sources surface first. Empty perQuietSource array when no feed crosses the staleness floor (the steady state on a healthy workspace). Pairs with `/api/v1/sources` (rev 13 + rev 142 + rev 143 pollGating projection) + `/api/v1/sources/cadence-overview` (rev 142) as the third axis on the per-source health observability cluster on the protocol-bound surface. The cadence pattern from rev 37 onward (every dashboard primitive ships with a v1 mirror in the same cycle) holds unbroken into rev 144.
- OpenAPI 3.1 typed coverage on /sources/quietness — 66th unbroken cadence rev. OpenAPI spec types the new endpoint with full request/response schemas (workspaceLoopMinutes, totalSources, pollableSources, quietSources, staleFloorDays, perQuietSource[] with full per-source shape including ISO date-time formats on lastSyncedAt + lastSignalAt). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 66th unbroken rev with rev 144. The OpenAPI spec changelog header gains a rev-144 block explaining the third-axis closure on the per-source observability cluster. The `/api/v1` self-describing endpoint index also documents the new endpoint inline so MCP-host integrators reading the index discover it without opening the spec.
Bulk per-source poll-cadence override closes the named rev-142 next-sprint candidate at the cadence axis (POST /api/sources/bulk-cadence + v1 mirror in lockstep + GET /api/v1/sources projection extended with rev-142 pollGating shape inline + dashboard bulk-cadence chip + OpenAPI typed coverage on the new endpoint and the new field shape — 65th unbroken cadence rev)
- Bulk per-source poll-cadence override (POST /api/sources/bulk-cadence + v1 mirror) — closes the named rev-142 next-sprint candidate. Operators with 20+ rss.app/fetchrss bridge feeds who wanted to throttle a whole class of feeds (every LinkedIn bridge to daily) had to tap each source's cadence chip individually after rev 141 shipped the per-source override. Rev 143 collapses that to one operation. New `bulkSetSourcePollIntervals()` helper validates 1-1440 minutes (or null to clear the override) + the per-batch 50-source cap exactly mirroring the rev-36 + rev-96 source-bulk pattern. Operates workspace-wide (omit sourceIds — every pollable source picks up the new cadence) or scoped to an explicit subset. Manual / file-based sources are skipped silently (the rev-141 cadence axis only applies to feed-style sources rss / review_site / linkedin that the rev-5 RSS poller actually gates). Activity log records every per-source change + a single bulk summary line. Pairs with PATCH `/api/sources/{id}` (rev 141 single-source) + GET `/api/v1/sources/cadence-overview` (rev 142 read-aggregate) as the third primitive on the per-source cadence cluster (write-single rev 141 / read-aggregate rev 142 / write-bulk rev 143).
- GET /api/v1/sources projection extended with the rev-142 pollGating shape inline. Until rev 143 an MCP host driving the desk could read each source's `pollIntervalMinutes` column on the existing rev-141 GET projection but had to either fetch `/sources/cadence-overview` separately or compute the next-poll-at + due-now answer client-side. Rev 143 makes the answer load-bearing inline on every per-source row of GET `/api/v1/sources` — every row gets `pollGating: { sourceId, intervalMinutes, isOverride, lastSyncedAt, nextPollAt, isDueNow, minutesUntilDue } | null` matching the rev-142 cadence-overview perSource shape exactly. Manual / file-based / paused sources read `pollGating: null` since the rev-141 cadence axis only applies to active feed-style sources. Pure derived state from the existing rev-141 column + workspace cadence — no new persistence. MCP hosts rendering 'feed X is due to poll in 3 minutes' alongside other source metadata don't need the follow-up call to /sources/cadence-overview.
- Dashboard SourceBulkCadence component + brand-purple palette distinct from the rev-96 brand-teal bulk-rename. New `SourceBulkCadence` client component mounts inside the rev-36 SourceBulkActions section + rev-96 bulk-rename block (sources panel footer) when 2+ pollable feed-style sources exist. Six presets (15m / 30m / 1h / 4h / 1d / Workspace cadence) match the rev-141 inline cadence chip vocabulary so operators pattern-match instead of re-reading the chip cluster from scratch. 'Workspace cadence' clears the per-source override (revert to rev-140 cadence). Apply asks for confirm via the cadence preset label so the operator can verify intent before the irreversible-feeling button. New `.ld-source-bulk-cadence*` CSS uses a brand-purple palette (`rgba(107,78,214,*)`) distinct from the rev-96 brand-teal bulk-rename + rev-141 brand-teal per-source cadence chip so operators tell at a glance which axis they're acting on. Two bulk surfaces stack as siblings inside the sources panel footer — one consistent visual vocabulary at the layout layer (panel + grid + animation) and distinct colour vocabularies at the action layer (rename axis = teal; cadence axis = purple). Cumulative micro-polish — every rev 22+ has carried at least one — and rev 143's polish is load-bearing because the rev-141 + rev-142 + rev-143 cadence cluster needed visual distinction from the rev-95 + rev-96 rename cluster on the same panel footer.
- OpenAPI 3.1 typed coverage on POST /sources/bulk-cadence + extended GET /sources pollGating projection — 65th unbroken cadence rev. OpenAPI spec types the new POST `/sources/bulk-cadence` endpoint with full request/response schemas (pollIntervalMinutes nullable int 1-1440 + optional sourceIds[≤50] + ok/matched/updated/skipped response shape). The existing GET `/sources` schema picks up the new `pollGating` field shape inline alongside the rev-141 `pollIntervalMinutes` projection. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) reaches its 65th unbroken rev with rev 143. The OpenAPI spec changelog header gains a rev-143 block explaining the bulk-cadence axis closure. The `/api/v1` self-describing endpoint index also documents the new endpoint inline so MCP-host integrators reading the index discover it without opening the spec.
Per-source next-poll visibility closes the named rev-141 next-sprint candidate at the protocol surface (GET /api/v1/sources/cadence-overview + dashboard inline next-poll chip + cadence summary block above sources panel + OpenAPI typed coverage — 64th unbroken cadence rev)
- Per-source next-poll chip on every source row + cadence summary block above the sources panel. Rev 141 shipped per-source cadence overrides as a write primitive — operators could tune feed cadence per source but couldn't see at a glance which feeds were due to poll soon vs which were sitting on a slow override. Rev 142 closes that gap. New `computeSourcePollGating()` helper computes the same answer the rev-5 RSS poller uses (`lastSyncedAt + pollIntervalMinutes` for override-bearing sources, workspace cadence otherwise) so the dashboard surface and the poller agree on what 'next poll' means. Surfaces inline as a `↻ Nm` chip in the source row pill cluster with three visual states: muted-teal (workspace cadence + waiting), brand-purple (rev-141 override + waiting), brand-amber `↻ poll due` (interval crossed since last sync). Tooltip carries the full breakdown (interval + override status + minutes-until-due) so the chip stays compact without losing detail. New cadence summary block above the source list aggregates the workspace's cadence mix (`5 pollable · 3 on workspace cadence (5m) · 2 with override · 1 due now`) so operators see at a glance how their cadence mix is structured. Hidden when there are no feed-style sources to gate.
- GET /api/v1/sources/cadence-overview — closes the named rev-141 candidate at the protocol surface. Until rev 142 an MCP host driving the desk could read the rev-141 `pollIntervalMinutes` per source via `GET /api/v1/sources` but had to compute next-poll-at and the override-vs-workspace breakdown client-side. Rev 142 makes the answer load-bearing on the v1 surface in one bearer-auth call. New `getSourceCadenceOverview()` helper returns workspace cadence + plan floor + per-source breakdown (cadence minutes, override status, last sync, next poll ISO, due-now flag, minutes-until-due) sorted by minutes-until-due ascending so the loudest 'this is about to poll' rows surface first. Pairs with the rev-141 dashboard chip + rev-142 cadence summary panel as the protocol mirror of the in-app surface. The MCP server's per-source-cadence tooling now has nothing left to design across cadence write (rev 141 PATCH) + cadence read (rev 141 GET projection) + cadence aggregate (rev 142 cadence-overview).
- OpenAPI 3.1 typed coverage on the new endpoint — 64th unbroken cadence rev. OpenAPI spec types the new `/sources/cadence-overview` GET endpoint with full request/response schemas (workspaceLoopMinutes, planFloorMinutes, totalSources, pollableSources, withOverride, followingWorkspace, dueNow, perSource[] with full per-source shape including ISO date-time formats). The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI spec in the same cycle it ships) reaches its 64th unbroken rev with rev 142. The OpenAPI spec changelog header gains a rev-142 block explaining the protocol-surface aggregate that closes the rev-141 follow-up. The `/api/v1` self-describing endpoint index also documents the new endpoint inline so MCP-host integrators reading the index discover it without opening the spec.
- Cumulative dashboard polish — three-state next-poll chip palette + tactile hover lift. New `.ld-source-next-poll` CSS class with three visual states distinguishing workspace-cadence (muted teal), per-source-override (brand-purple, signals intentional throttling), and due-now (brand-amber + bold weight, the eye lands here first). 160ms hover transition on each state. `cursor: help` so operators know the tooltip carries detail. New `.ld-source-cadence-summary` CSS for the summary block above the source list uses a soft brand-color/brand-purple gradient background so the block reads as ambient context rather than competing with the per-row chips. Cumulative micro-polish — every rev 22+ has carried at least one — and rev 142's polish is load-bearing because the source row has now accumulated 8+ chips per row (rev-1 type + status + rev-9 7d count + rev-15 sparkline + rev-74 chronic-paused pill + rev-26 keyword filter chip + rev-141 cadence chip + rev-142 next-poll chip) and needed visual distinction without competing.
Per-source poll-cadence override closes the named rev-140 next-sprint candidate (PATCH /api/sources/{id} accepts pollIntervalMinutes + v1 mirror + RSS poller honours per-source cadence + dashboard popover with Follow-workspace reset + OpenAPI typed coverage — 63rd unbroken cadence rev)
- Per-source poll-cadence override — closes the named rev-140 next-sprint candidate. Rev 140 shipped per-workspace cadence control via `workspace.loopIntervalMinutes`; rev 141 closes the per-source axis. New `source.pollIntervalMinutes` integer column (nullable — null = follow the workspace cadence as before) + `setSourcePollInterval()` helper + extended PATCH `/api/sources/{id}` Zod schema accepting a third mutually-exclusive payload `{ pollIntervalMinutes: int 1-1440 | null }` alongside the rev-6 status flip + rev-95 rename. Operators with mixed source mixes (a daily-published RSS feed alongside a high-cadence LinkedIn bridge) had no per-source primitive — they had to either drag the entire workspace down to the slowest feed's sensible cadence or let the noisy feed flood the queue at the workspace cadence. Rev 141 closes that gap.
- RSS poller honours per-source `lastSyncedAt + pollIntervalMinutes`. `pollRssSourcesForWorkspace()` extended to skip any source whose `lastSyncedAt + pollIntervalMinutes` is still in the future at run time. A daily-published RSS feed configured for 1440-minute cadence is therefore skipped on every workspace pulse cycle except the one that crosses the 24-hour boundary since its last successful poll. Pure additive on top of the rev-5 RSS poll loop, the rev-6 pause/resume, the rev-26 keyword filter, and the rev-59/62 cost-spike auto-pause — stacks on every existing source-side primitive at the cadence axis. New `skippedForCadence` field on the poller's return shape so the rev-1 pulse engine sees a clean separation between cost-spike skips (rev 59) and cadence skips (rev 141).
- v1 mirror in lockstep — closes the v1 parity gap on the rev-141 dashboard primitive. PATCH `/api/v1/sources/{id}` accepts the same third payload via the same Zod union schema, delegates to the same `setSourcePollInterval()` helper. GET `/api/v1/sources` projection extended with `pollIntervalMinutes` so MCP hosts driving the desk programmatically can read each feed's per-source cadence without a follow-up call. The cadence pattern from rev 37 onwards (every dashboard mutation has a v1 equivalent within one rev) holds unbroken into rev 141. The MCP server's per-source-mutation tooling has nothing left to design across status / label / cadence (rev 6 / rev 95 / rev 141).
- OpenAPI 3.1 typed coverage on the new payload + cumulative dashboard polish. OpenAPI spec types the new `pollIntervalMinutes` payload on the PATCH endpoint with full description (1-1440 minutes; null clears the override and reverts to the workspace cadence). The cadence pattern of typing every v1 enhancement in the same cycle holds (63rd unbroken rev). New `SourcePollInterval` client component mounts on every RSS-style source row beside the rev-95 rename + rev-6 pause/resume + rev-26 keyword filter — a one-tap chip → popover with five presets (15m / 30m / 1h / 4h / 1d) plus a 'Follow workspace' reset that clears the override. Active override surfaces inline as a brand-color 'Cadence · Nm' chip with a tooltip explaining it overrides the workspace cadence. Presets below the workspace cadence render with a dashed border + amber ↑ mark matching the rev-140 plan-floor visual treatment so the workspace cadence reads as a soft floor.
Per-workspace loop interval admin control (PATCH /api/workspace/loop-interval + dashboard popover + v1 mirror) closes long-outstanding configuration gap + OCC primitive extended to single-scalar dashboardPrefs (activeWorkSort, digestPersonalSections, digestQuietWeekends) closes named rev-139 next-sprint candidate + OpenAPI typed coverage on every new field — 62nd unbroken cadence rev
- Loop interval admin control — closes a long-outstanding configuration gap. The `workspace.loopIntervalMinutes` column has been settable in the schema since rev 1 (default 5) but had no UI or API surface — operators wanting to slow the desk down on a quiet workspace, or speed it up after upgrading to Pro/Team, had no path. Rev 140 closes that gap. New `setWorkspaceLoopInterval()` helper + admin-only `PATCH /api/workspace/loop-interval` route + `LoopIntervalControl` client component mounted directly under DeskLoopControl. Plan-tier-aware: server-side floor (free=15, pro=5, team=1) is layered on top of the requested value so a free workspace can't sneak below 15 minutes. Returns both the requested + effective values so the dashboard UI can surface the floor when it lifts the value. Six presets (1/5/15/30/60/240 min) — presets below the operator's plan floor render with a dashed border + amber `↑Nm` mark + tooltip naming the floor so the upgrade path is visible on every plan. Activity log records every change. Closes the third axis of operator-driven cadence control alongside the rev-1 desk-state primitive (loopState on/paused/off) + rev-21 scheduled-pause primitive.
- OCC primitive extended to the three remaining single-scalar dashboardPrefs fields — closes the named rev-139 next-sprint candidate. Until rev 140 the rev-83 activeWorkSort + rev-79 digestPersonalSections + rev-80 digestQuietWeekends scalars on the rev-78 dashboardPrefs JSONB were last-write-wins. Even though the rev-139 running state correctly noted that 'the surface for it is smaller since these aren't typically toggled rapid-fire from multiple devices', closing the OCC symmetry across every multi-device-synced primitive on the dashboardPrefs JSONB makes the conflict-resolution semantics uniform — every scalar on the JSONB now has the same OCC story. Three new optional `*UpdatedAt` companion fields on the prefs payload + three new OCC steps inside `setDashboardPrefs` mirroring the rev-139 density OCC step exactly: when the patch carries `{field}UpdatedAt` older than the existing server-side stamp, the server keeps the newer field value and surfaces the rejection on the rev-137 telemetry trail at `rejected.{field}: { reason: 'stale_write', existingAt, incomingAt } | null`. Equal timestamps fall through to the upsert path. Each rejection is independent (a stale activeWorkSort doesn't affect the digestPersonalSections decision) so the response can carry up to three rejections per PUT. The per-field check runs only when the patch actually carries the field, so a no-op PUT that doesn't touch the field can't accidentally accept an old timestamp from a stale companion field.
- v1 mirror + OpenAPI 3.1 typed coverage on every new field — 62nd unbroken cadence rev. Bearer-auth `/api/v1/workspace/loop-interval` PATCH mirrors the dashboard endpoint exactly so MCP hosts driving the desk programmatically can tune the cadence without dashboard sessions — pairs with `/workspace/loop` (rev 82 desk-state) + `/workspace/pause-until` (rev 82 scheduled pause) as the third axis of programmatic cadence control. Bearer-auth `/api/v1/workspace/dashboard-prefs` PUT accepts the three new `*UpdatedAt` fields via the same Zod schema as the dashboard route, returns the same `{ prefs, rejected }` response shape extended with `rejected.activeWorkSort` + `rejected.digestPersonalSections` + `rejected.digestQuietWeekends`. The OpenAPI 3.1 spec types the new loop-interval endpoint with full request/response schemas (request: loopIntervalMinutes integer 1-1440; response: ok, loopIntervalMinutes, effectiveLoopIntervalMinutes, planTier enum) + types the three new `*UpdatedAt` integer fields on the PUT request body + the three new `rejected.*` nullable objects on the PUT response with full per-field shape (reason enum 'stale_write', existingAt + incomingAt timestamps). The cadence pattern from rev 78 onward (every dashboardPrefs primitive gets typed in the OpenAPI spec in the same cycle it ships) reaches its 62nd unbroken rev with rev 140.
- Cumulative dashboard polish — loop-interval popover with brand-color hover lift + plan-floor visual treatment. New `.ld-loop-interval` + child classes — visual treatment matches the rev-23 keyboard FAB + rev-39 density toggle + rev-21 scheduled-pause popover so the four power-user controls on the dashboard read with one consistent visual vocabulary. 1px hover lift + 160ms transitions match the rev-22+ tactile click affordance vocabulary. New `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. Plan-floor visual treatment: presets below the operator's plan floor render with a dashed border + amber `↑Nm` mark + tooltip naming the floor — pairs with the rev-21 cost-cap warning + rev-32 cost-spike alarm as the third floor-aware affordance on the dashboard, all wearing the same brand-amber palette so the operator pattern-matches 'this is your plan floor talking' across all three surfaces. New `@keyframes ld-loop-interval-fade` (200ms) for the popover entrance animation matches the rev-21 scheduled-pause popover entrance animation. Cumulative micro-polish (every rev 22+ has carried at least one) — rev 140's polish anchors the new control in the rev-22+ design language thread and makes the plan-tier upgrade story visible on the most operator-loaded surface (the desk-state panel).
OCC primitive extended to the rev-136 density axis — closes the named rev-138 next-sprint candidate + Outpaced — reloaded chip on the rev-39 density toggle + v1 mirror in lockstep + OpenAPI typed coverage on the new field shape
- Per-field OCC on the rev-136 density primitive — closes the named rev-138 next-sprint candidate. Until rev 139 the rev-136 multi-device sync of dashboard density was last-writer-wins. If Machine A toggled compact at T1 and Machine B toggled comfortable at T2 with T2 > T1 but Machine A's PUT arrived after Machine B's PUT (network reorder, slow connection, retry backoff), Machine A's *older* density clobbered Machine B's newer one. Rev 138's running state explicitly named 'OCC primitive extended to density' as the rev-139 candidate, citing that density was the last of the three multi-device-synced primitives on the rev-78 dashboardPrefs JSONB still operating as last-write-wins (taskCommentFilters got OCC at rev 136; costPanelOrder got OCC at rev 138). Rev 139 closes the OCC symmetry across all three: a single epoch-ms `densityUpdatedAt` companion field on the prefs payload. When the patch carries `densityUpdatedAt` older than the existing server-side stamp, the server keeps the newer density and surfaces the rejection on the rev-137 telemetry trail at `rejected.density: { reason: 'stale_write', existingAt, incomingAt } | null`. Density is a single field (not per-axis like costPanelOrder), so the rejection is a single object — present means rejected, null means accepted. Mirrors the rev-136 taskCommentFilters + rev-138 costPanelOrder OCC semantics exactly so the three multi-device-synced surfaces have one consistent conflict-resolution story.
- Outpaced — reloaded chip on the rev-39 density toggle + canonical re-sync from response. DensityToggle client-side: every fire-and-forget PUT to `/api/workspace/dashboard-prefs` now reads the response. When the response carries `rejected.density`, the client (a) surfaces a brand-amber 'Outpaced — reloaded' chip beside the rev-136 'synced' ambient state chip, (b) re-syncs local state from the canonical `prefs.density` returned on the same response — no follow-up GET. Auto-fades after 5s; click to dismiss early. A suppression ref ensures the re-sync setState doesn't fire another PUT in a loop. Brand-amber palette (`rgba(207, 108, 58, *)`) mirrors the rev-137 per-thread filter Outpaced chip + rev-138 cost-panel-order Outpaced chip exactly so the dashboard's three multi-device-synced surfaces (taskCommentFilters rev 137 + costPanelOrder rev 138 + density rev 139) read with one consistent transient-confirmation visual story across three distinct attention levels: passive (Synced teal), passive (saved-on-device grey), transient (Outpaced amber). The intent timestamp is captured at the moment of toggle (not the moment of debounced send) so the OCC step compares against operator intent rather than network reorder timing.
- v1 mirror + OpenAPI 3.1 typed coverage on the rev-139 OCC primitive — 62nd unbroken cadence rev. Bearer-auth `/api/v1/workspace/dashboard-prefs` PUT accepts the new `densityUpdatedAt` field via the same Zod schema as the dashboard route, returns the same `{ prefs, rejected }` response shape extended with `rejected.density` so MCP hosts driving the desk programmatically across multiple machines (e.g. a watcher agent on a CRON server + a manual operator on a laptop) see exactly when the OCC step rejected the density write — they can avoid double-syncing the same density or skip a follow-up GET to re-fetch the canonical server state. The cadence pattern from rev 37 onward (every dashboard mutation has a v1 equivalent within one rev) holds unbroken into rev 139. OpenAPI spec types the new `densityUpdatedAt` integer field on the PUT request body + the new `rejected.density` object on the PUT response with full per-field shape (reason enum 'stale_write', existingAt + incomingAt timestamps). Pure spec additions — existing rev-138 callers reading only `rejected.taskCommentFilters` + `rejected.costPanelOrder` keep working since `rejected.density` is just a new sibling field.
- Cumulative dashboard polish — brand-amber chip vocabulary anchored across all three OCC surfaces. New `.ld-density-outpaced` chip + `.ld-density-outpaced-dot` pulsing dot + `@keyframes ld-density-outpaced-fade` (5s) + `@keyframes ld-density-outpaced-pulse` (1.4s) + `:focus-visible` outline ring matching the rev-38 dashboard accessibility pattern. Brand-amber palette distinct from the rev-127 brand-color teal sync chip + rev-136 muted-grey local chip so the three transient-confirmation chips on the density toggle row read at three distinct attention levels. The rev-139 Outpaced chip closes the rev-22+ visual-hierarchy thread on the dashboard's three multi-device-synced surfaces — one consistent vocabulary across taskCommentFilters (rev 137), costPanelOrder (rev 138), and density (rev 139). Cumulative micro-polish (every rev 22+ has carried at least one) — rev 139's polish is load-bearing because it anchors the new transient-confirmation chip in the rev-22+ design language thread that has run since rev 22 and now spans the entire OCC cluster.
OCC extended to costPanelOrder per-axis on the rev-127 multi-device-synced cost panels + Outpaced — reloaded chip vocabulary across all three cost-panel axes + v1 mirror + OpenAPI typed coverage in lockstep
- Per-axis optimistic concurrency control on costPanelOrder — closes the named rev-137 next-sprint candidate. Until rev 138 the rev-127 multi-device sync of cost-panel custom row order across the rev-57 cost-by-source / rev-52 cost-by-assignee / rev-66 cost-by-tag panels was last-writer-wins: if Machine A wrote `{source: [a,b]}` at T1 and Machine B wrote `{source: [c,d]}` at T2 with T2 > T1 but Machine A's PUT arrived after B's (network reorder, slow connection, retry backoff), Machine A's older order silently clobbered Machine B's newer one. Rev 138 closes that gap with the same OCC primitive rev 136 introduced for taskCommentFilters at the cheapest possible primitive: per-axis epoch-ms timestamp via the new companion field `costPanelOrderUpdatedAt: { source?: number; assignee?: number; tag?: number }`. When the patch's per-axis `at` is older than the existing server-side stamp, the server keeps the newer order and surfaces the rejection on the rev-137 telemetry trail at `rejected.costPanelOrder: Array<{axis, reason: 'stale_write', existingAt, incomingAt}>`. Empty-array clears (the rev-126 reset path) bypass the timestamp check because the operator's intent to reset is always honoured — same exemption rule as the rev-136 taskCommentFilters OCC primitive. Closes the named rev-137 next-sprint candidate at the cheapest possible primitive.
- Outpaced — reloaded chip on the rev-126 cost-panel reset row across all three axes. useCostPanelOrder hook extended: every PUT to /api/workspace/dashboard-prefs now reads the response and, when it carries a `rejected.costPanelOrder` entry for THIS axis, (a) surfaces a brand-amber 'Outpaced — reloaded' chip alongside the rev-127 'synced across devices' / 'saved on this device' chips on the rev-126 reset row, (b) re-syncs local state (order array + localStorage cache) from the canonical `prefs.costPanelOrder[axis]` returned on the same response — no follow-up GET. Auto-fades after 5s with a brand-amber palette + pulsing dot animation that matches the rev-137 taskCommentFilters Outpaced chip exactly so the dashboard's three multi-device-synced surfaces (taskCommentFilters rev 137 / costPanelOrder rev 138 / + future axes) read with one consistent transient-confirmation visual story. Click to dismiss early. Strategic significance: closes the operator-visibility gap on the rev-138 OCC primitive at the cheapest possible surface — no new round-trip, no polling, no schema beyond the new timestamp companion; the rejection trail rides the same response the PUT already returned. Mounted on all three cost panels (cost-by-source, cost-by-assignee, cost-by-tag).
- v1 mirror + OpenAPI 3.1 typed coverage in lockstep — 61st unbroken cadence rev. Closes the v1 parity gap on the rev-138 dashboard primitive in the same cycle the dashboard primitive ships. Both the dashboard `/api/workspace/dashboard-prefs` PUT and the bearer-auth `/api/v1/workspace/dashboard-prefs` PUT (and DELETE) now accept the new `costPanelOrderUpdatedAt` field and return the rev-137 `{ prefs, rejected }` response shape extended with the rev-138 `rejected.costPanelOrder` array so MCP hosts driving the desk programmatically across multiple machines (e.g. a watcher agent on a CRON server + a manual operator on a laptop) see exactly which axis the OCC step rejected. OpenAPI 3.1 spec types the new `costPanelOrderUpdatedAt` per-axis shape plus the `rejected.costPanelOrder` array with full per-entry shape (axis enum source/assignee/tag, reason enum 'stale_write', existingAt + incomingAt timestamps). The rev-78 cadence pattern (every dashboardPrefs primitive gets typed in the OpenAPI spec in the same cycle it ships) reaches its 61st unbroken rev with rev 138.
- Cumulative dashboard polish — Outpaced chip palette + 5s fade + tactile hover lift. New `.ld-cost-order-outpaced` brand-amber palette (`rgba(207, 108, 58, *)`) distinct from the rev-127 brand-teal `.ld-cost-order-sync` chip + rev-127 muted-grey `.ld-cost-order-local` chip so the three filter-row chips read at three distinct attention levels: passive informational (synced teal), passive informational (saved-on-device grey), transient warning (outpaced amber). 1px hover lift + 160ms transitions match the rev-22+ tactile click affordance vocabulary. 5s `ld-cost-order-outpaced-fade` keyframes animation. New `.ld-cost-order-outpaced-dot` pulsing dot + `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. Cumulative micro-polish (every rev 22+ has carried at least one). Pairs with the rev-137 Outpaced chip vocabulary so the dashboard's multi-device-synced surfaces speak one consistent transient-confirmation visual language across both the per-thread filter row and the per-axis cost-panel reset row.
Stale-write rejection telemetry on the rev-136 OCC primitive + Outpaced — reloaded chip on per-thread filter row + v1 mirror in lockstep + OpenAPI typed coverage
- Stale-write rejection telemetry on setDashboardPrefs — closes the named rev-136 next-sprint candidate. Until rev 137 the rev-136 server-side OCC primitive silently dropped older taskCommentFilters writes — the operator on the slow device had no signal that their last filter change didn't land; they just saw the dashboard re-render with somebody else's scope on the next fetch and wondered if they'd misclicked. Rev 137 closes that gap. setDashboardPrefs now collects every per-entry stale-write rejection inside the rev-136 OCC step into a structured `rejected.taskCommentFilters: Array<{taskId, reason: 'stale_write', existingAt, incomingAt}>` and returns it alongside the persisted prefs. The function signature changes from `Promise<DashboardPrefs | null>` to `Promise<{prefs, rejected} | null>` so every existing caller reads one consistent shape across both surfaces — `prefs` carries the canonical post-OCC state operators should re-sync from, `rejected.taskCommentFilters` enumerates what was silently dropped. Empty array means no rejections (the steady-state). Foundation for any future field with its own OCC primitive — the `rejected` shape is per-axis so adding rejections for cap-overflow or future axes doesn't break existing callers.
- Outpaced — reloaded chip on the rev-128 per-thread filter row. TaskComments client-side: every debounced PUT now reads the response. When the response carries a `rejected.taskCommentFilters` entry for THIS taskId, the client (a) surfaces a brand-amber 'Outpaced — reloaded' chip on the rev-128 filter row alongside the rev-134/135 Restored/Synced chips, (b) re-syncs local state (`query`, `authorFilter`, `reactionsOnly`) from the canonical `prefs.taskCommentFilters[taskId]` returned on the same response — no follow-up GET. Auto-fades after 5s; click to dismiss early. Distinct visual vocabulary from the rev-134/135 chips: rev-134 brand-color teal 'Restored' = your scope is loaded from last visit on this browser (passive); rev-135 brand-purple 'Synced' = your scope is loaded from your account state (cross-device informational); rev-137 brand-amber 'Outpaced — reloaded' = something happened, your write was outpaced and your local state was just re-synced (transient confirmation, more attention-grabbing than the passive informational chips). Closes the operator-visibility gap on the rev-136 OCC primitive at the cheapest possible surface — no new round-trip, no polling, no schema; the rejection trail rides the same response the PUT already returned.
- v1 mirror + OpenAPI 3.1 typed coverage on the rev-137 rejection trail — 60th unbroken cadence rev. Closes the v1 parity gap on the rev-137 dashboard primitive in the same cycle the dashboard primitive ships. Both the dashboard `/api/workspace/dashboard-prefs` PUT and the bearer-auth `/api/v1/workspace/dashboard-prefs` PUT (and DELETE) now return the same `{ prefs, rejected }` response shape so MCP hosts driving the desk programmatically across multiple machines (e.g. a watcher agent on a CRON server + a manual operator on a laptop) see exactly which entries the OCC step rejected — they can avoid double-syncing the same scope or skip a follow-up GET to re-fetch the canonical server state. OpenAPI 3.1 spec types the new `rejected.taskCommentFilters` array with full per-entry shape (taskId, reason enum 'stale_write', existingAt + incomingAt timestamps) plus a description that names the dashboard's matching chip vocabulary so MCP-host code generators reading the spec see exactly how to consume the rejection trail. The rev-78 cadence pattern (every dashboardPrefs primitive gets typed in the OpenAPI spec in the same cycle it ships) reaches its 60th unbroken rev with rev 137.
- Cumulative dashboard polish — brand-amber Outpaced chip + warning-style pulsing dot + 5s fade animation. Three complementary visual affordances on the rev-137 chip. (a) New `.ld-task-comment-filter-outpaced` brand-amber palette (`rgba(207, 108, 58, *)`) distinct from the rev-134 brand-color teal Restored chip and the rev-135 brand-purple Synced chip so all three filter-row chips read at three distinct attention levels: passive (Restored teal), passive (Synced purple), transient/attention (Outpaced amber). (b) Slightly more aggressive `@keyframes ld-task-comment-filter-outpaced-pulse` (1.4s vs the rev-134 1.6s, 5px shadow vs 4px) so the chip reads as 'something happened' rather than 'your scope is loaded'. (c) Slightly longer fade window (5s vs the rev-134 4s) because the message carries denser meaning and the operator needs a beat to register that their write was outpaced. New `.ld-task-comment-filter-outpaced-dot` + `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern. Cumulative micro-polish (every rev 22+ has carried at least one). The rev-137 chip closes the operator-visibility loop on the rev-136 OCC primitive in the rev-22+ design language — the same chip vocabulary that has driven every filter-row affordance since rev 134.
Server-side optimistic concurrency control on per-thread filter sync + dashboard density preference synced across devices + OpenAPI typed coverage in lockstep + Synced ambient state chip on the rev-39 density toggle
- Server-side optimistic concurrency control on taskCommentFilters — closes the named rev-135 next-sprint candidate. Rev 135 closed the server-side multi-device sync of per-thread discussion filter scope but the partial-merge logic was last-write-wins. If Machine A wrote {q: 'concern', at: T1} and Machine B wrote {q: 'spike', at: T2} with T2 > T1 but Machine A's PUT arrived after Machine B's PUT (network reorder, slow connection, retry backoff), Machine A's *older* scope clobbered Machine B's newer one. Rev 135's running state explicitly named 'server-side conflict resolution for taskCommentFilters multi-device sync' as the rev-136 candidate, citing the optimistic-concurrency-control pattern that rev-78 panel collapse merge-by-union sidestepped. Rev 136 closes that gap. setDashboardPrefs's partial-merge logic now compares the incoming entry's `at` timestamp against the existing server-side entry's `at`; when the incoming write is older, the server keeps the newer one (last-writer-with-newer-timestamp wins). Equal timestamps fall through to the upsert path so a no-op refresh from the same device still works. The deletion path (empty scope) is exempt from the timestamp check because a clear-filter intent is always honoured — the alternative would leave operators unable to clear a filter on a power-user device that was offline. Mirrors the optimistic-concurrency-control pattern used by every modern sync surface (CRDTs, last-writer-wins-with-timestamp, etc.) at the cheapest possible primitive — no new schema, no new column, just a per-entry timestamp comparison inside the existing rev-135 partial-merge logic.
- Dashboard density preference synced across devices — extends the rev-78 multi-device-sync pattern to its eleventh field family. New optional `density` field ("compact" | "comfortable") on `DashboardPrefs`. Rev 39 shipped the dashboard density toggle as a localStorage-only primitive — an operator who set compact mode on Machine A still saw comfortable mode on Machine B. Multi-device operators on the procurement-conscious end of the customer base routinely complain about exactly this kind of friction. Rev 136 closes that gap. Same dual-layer sync pattern as rev-127 cost-panel order + rev-78 panel collapse: localStorage stays the immediate write-through cache for sync render so the dashboard never blocks on a network round-trip on first paint; server JSONB is the source of truth for cross-device drift; fire-and-forget debounced PUT (~300ms after the last toggle) keeps the network cost off the render hot path. The DensityToggle component prefers `serverInitialDensity` over localStorage on first render so cross-device drift wins (Machine A's most-recent toggle follows the operator to Machine B). Wired through three layers in lockstep: dashboard `/api/workspace/dashboard-prefs` Zod schema accepts the new enum; v1 `/api/v1/workspace/dashboard-prefs` bearer-auth mirror accepts it identically (the cadence pattern from rev 37 onwards continues unbroken); setDashboardPrefs clamps to the two allowed values, falling back to 'comfortable' on any other input. The DELETE reset-to-defaults path also clears the density preference. The rev-78 multi-device-sync pattern (collapsedPanels rev 78 / digestPersonalSections rev 79 / collapsedActivityBuckets rev 79 / digestQuietWeekends rev 80 / digestQuietHoursStart-End rev 81 / panelOrder rev 82 / activeWorkSort rev 83 / costPanelColumns rev 84 / costPanelOrder rev 127 / taskCommentFilters rev 135 / density rev 136) now reaches its eleventh field-family on the dashboardPrefs JSONB.
- OpenAPI 3.1 typed coverage on the rev-136 density field + rev-136 OCC semantics in lockstep — 59th unbroken cadence rev. Closes the typed-contract gap on both rev-136 primitives in the same cycle they ship. The OpenAPI spec's `/workspace/dashboard-prefs` GET response shape + PUT request body gain a typed `density` field (enum ["compact", "comfortable"]) with the dual-layer sync pattern documented inline. The existing `taskCommentFilters` schema description picks up the rev-136 OCC behaviour: 'when the incoming write is older than the existing server-side entry, the server keeps the newer one' + 'the deletion path is exempt' + 'mirrors the OCC pattern used by every modern sync surface (CRDTs, LWW-with-timestamp).' The `at` field description also notes its dual role (LRU eviction + OCC). The rev-78 cadence pattern (every dashboardPrefs field gets typed in the OpenAPI spec in the same cycle it ships) reaches its 59th unbroken rev with rev 136. MCP-host code generators reading the spec see a typed contract for cross-device sync of dashboard density immediately, plus the OCC semantic on the rev-135 sync surface so a programmatic caller building a watcher agent that polls the prefs across multiple devices knows their write may be silently rejected if a newer one already landed.
- Cumulative dashboard polish — Synced ambient state chip on the rev-39 density toggle + focus-visible accessibility ring. Two complementary visual affordances on the rev-39 density toggle. (a) New `.ld-density-sync` brand-color teal ambient state chip surfaces beside the toggle when the operator has chosen compact mode AND server prefs are reachable (i.e. the cross-device sync state is active). 'cursor: help' tooltip explains 'Saved to your account — this density follows you across devices' so the operator's eye lands on explicit confirmation that their preference is portable. Mirrors the rev-127 cost-panel-order sync chip vocabulary so the dashboard's three cross-device-sync surfaces (panel inventory + cost-panel ordering + density) read with one consistent visual story. (b) New `:focus-visible` brand-color outline ring on the density toggle button matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. New `.ld-density-toggle-group` flex container wraps the button + chip cluster as a unified control surface. Cumulative micro-polish (every rev 22+ has carried at least one) — rev 136's polish is load-bearing because until rev 136 the density preference's cross-device portability would have happened invisibly to the operator; rev 136 makes it explicit + ambient + tooltipped without screaming.
Multi-device sync of per-thread discussion filter scope via dashboardPrefs JSONB + Synced/Restored chip distinguishes server-vs-localStorage source + OpenAPI typed coverage on the new field in lockstep
- Multi-device sync of per-thread filter scope — closes the named rev-134 next-sprint candidate. Rev 134 shipped per-task discussion filter persistence in localStorage so an operator returning to the same task's discussion sees their last filter scope auto-restored. Rev 134's running state explicitly named multi-device sync via the rev-78 dashboardPrefs JSONB as the rev-135 candidate, citing the 'pairs with rev-78 panel-collapse multi-device sync at the per-thread filter axis' shape that has run since rev 78. Rev 135 closes that. New `taskCommentFilters` field on `DashboardPrefs` keyed by taskId, carrying the rev-128 keyword + author + rev-130 reactions-only scope plus an `at` timestamp. Server-side LRU eviction at 30 entries by `at` timestamp keeps the JSONB lean as a workspace accumulates filter scope across many tasks; partial-merge patch shape (sending one taskId upserts only that entry, sending one with empty scope deletes it, sending an empty object {} clears every entry for the rev-135 reset path) means a single-thread filter change doesn't blow away other tasks' stored scope. Same dual-layer sync pattern as rev-127 costPanelOrder + rev-78 collapsedPanels — localStorage stays the immediate write-through cache for sync render so the dashboard is never blocked on a network round-trip; server JSONB is the source of truth for cross-device drift; fire-and-forget debounced PUT (~600ms after last filter change) keeps the network cost off the render hot path. The TaskComments client component prefers `serverInitialFilter` over localStorage on first render so cross-device drift wins (Machine A's most-recent narrowing follows the operator to Machine B). Closes the per-thread filter cluster on the multi-device-sync axis: write (rev 134 localStorage) + read (rev 135 server JSONB winning over localStorage) + sync (rev 135 fire-and-forget debounced PUT).
- Synced/Restored chip palette swap distinguishes cross-device vs same-device restoration. Rev 134's 'Restored' chip flagged that the operator's last filter scope on this task was loaded from localStorage; rev 135 extends the same chip with an `is-synced` modifier when the scope was restored from server-side dashboardPrefs (cross-device sync) vs localStorage (same-device persistence). Brand-purple `rgba(107,78,214,*)` palette + 'Synced' copy distinguishes the cross-device path from the rev-134 brand-color teal 'Restored' palette so multi-device operators see explicit confirmation that their scope follows them across machines, not just across sessions. Distinct pulsing-dot animation + tooltip copy + aria-label so screen-reader users get the same distinction. Closes the visibility gap on the rev-135 sync path: until rev 135 the cross-device sync would have happened invisibly to the operator — rev 135 makes it explicit + ambient + tooltipped, the strongest possible 'this is portable' trust signal without screaming.
- Partial-merge patch shape on dashboardPrefs.taskCommentFilters — single-thread updates don't clobber other tasks. Until rev 135 the rev-78 dashboardPrefs PUT endpoint did a top-level shallow merge (`{ ...current, ...patch }`), which would have made a per-task filter PUT clobber every other task's stored scope. Rev 135's setDashboardPrefs special-cases taskCommentFilters: an `'taskCommentFilters' in patch` check triggers entry-by-entry merge against the existing server map. Empty-scope entries (no q + no author + reactions=false) are deleted from the merged map (so a 'just got cleared' client write removes the server-side ghost rather than leaving a stale entry); empty-object patch (`{}`) clears the entire map (used by the rev-84 DELETE reset-to-defaults path which now also resets taskCommentFilters); non-empty entries upsert. The merged map is then validated entry-by-entry (a tampered client can't poison the row) and capped at 30 entries with LRU eviction by `at` timestamp ascending so the *oldest* untouched entries fall out first when the cap is reached. Closes the partial-update semantic gap that would have made server-side sync of any high-cardinality dictionary unsafe.
- OpenAPI 3.1 typed coverage on the rev-135 taskCommentFilters field — 58th unbroken cadence rev. Closes the typed-contract gap on the rev-135 dashboardPrefs primitive in the same cycle it ships. The OpenAPI spec's `/workspace/dashboard-prefs` GET response shape + PUT request body gain a typed `taskCommentFilters` field with the per-entry shape (q, author nullable, reactions, at timestamp) + the partial-merge semantic + LRU cap documented inline. The rev-78 cadence pattern (every dashboardPrefs field gets typed in the OpenAPI spec in the same cycle it ships) reaches its 58th unbroken rev with rev 135. MCP-host code generators reading the spec see a typed contract for the cross-device sync of per-thread filter scope immediately. The OpenAPI spec changelog header gains a rev-135 block explaining the multi-device sync pattern + how the field closes the named rev-134 candidate. Pairs with rev-127 (cost-panel order multi-device sync) + rev-82 (panel order) + rev-83 (active-work sort) + rev-84 (cost-panel column visibility) + rev-78 (panel collapse) as the seven-axis dashboardPrefs sync cluster — every operator-tunable layer on the dashboard now follows them across devices.
Per-task discussion filter persistence in localStorage + visible Clear-filters + Restored-filter chips + since=ISO recency axis on the v1 comments endpoint with OpenAPI typed coverage in lockstep
- Per-task discussion filter persistence — closes the named rev-133 next-sprint candidate. Until rev 134 the rev-128 keyword + rev-129 author + rev-130 reactions-only filter scope on a task's discussion thread reset to default the moment an operator left the dashboard. An operator narrowing deeply to 'Steve's comments matching concern with reactions' on a long-running task had to re-type the scope every time they returned. Rev 134 closes that gap. New `taskCommentFilter(taskId)` namespace key in the rev-116 shared draft-storage helper. New `readPersistedFilter` / `writePersistedFilter` helpers with type-guards on every field (a tampered localStorage entry can't render the surface in an inconsistent state) + a 30-day TTL (stale scopes don't accumulate on tasks the operator hasn't touched in months). Lazy state initializer reads on mount; a useEffect persists every change. URL hash deep-links (rev-31 / rev-133 share-permalink format) still win over the persisted filter — a shared link is the strongest possible signal of intent and explicitly clears the persisted scope on arrival. Pairs with rev-78 multi-device sync at the per-thread axis: drafts + composer state stay localStorage-only because they're per-device by design (an in-flight filter on Machine A shouldn't follow you to Machine B mid-triage), but the persisted scope means an operator who narrowed deeply doesn't lose their lens between sessions on the same browser.
- Visible Clear-filters chip + Restored-filter affordance for mouse-first operators. Two complementary visual affordances on the per-thread filter row. (a) New `.ld-task-comment-clear-all` chip surfaces beside the rev-128 copy-thread chip whenever any filter axis is active — mirrors the rev-129 Esc-to-clear shortcut for mouse-first operators who don't keep the keyboard primitive in muscle memory. Hidden when no axis is active so the chip cluster never reads as bloat. Hover lift (1px) + soft red treatment so the affordance reads as a 'reset' action distinct from the rev-128 brand-teal copy chip. (b) New `.ld-task-comment-filter-restored` chip flags that the operator's last filter scope on this task was loaded from localStorage. Brand-color dashed-border + pulsing dot + 4s auto-fade animation so the affordance reads as 'your scope is loaded' without persisting as chrome that competes with the rev-131 match counter or the rev-128 copy-thread chip. Tap-to-dismiss on the chip itself; explicit operator interaction (typing, tapping author chips, toggling reactions-only, Esc, or empty-state Clear) also dismisses immediately so the chip never reads stale.
- since=ISO recency axis on /api/v1/tasks/{id}/comments — closes the recency axis on the per-task comment surface. Until rev 134 the per-task comments cluster on the v1 surface carried scope-by-content (rev 129 keyword), scope-by-attribution (rev 129 author), and scope-by-engagement (rev 130 reactions-only on the dashboard) but no scope-by-time on the protocol-bound side. MCP hosts polling for 'what's new on this task in the last hour' had to enumerate every comment and filter client-side. Rev 134 closes that with an optional `since=ISO` query param. Composes with q + authorId via intersection: a recipient that sets all three gets only comments posted since the instant AND matching the keyword AND from the named author. Bounded to a 1-year max lookback to keep the in-memory filter cost predictable; an unparseable timestamp falls back to no since filter. The response echoes the resolved instant so callers can verify the filter was applied + reuse the same value as the next poll's `since` param, walking the timeline forward without losing or duplicating comments. The dashboard `/api/tasks/{id}/comments` mirror gains the same param + echo field in lockstep so any direct caller of either surface (the v1 bearer-auth endpoint or the dashboard session-auth endpoint) reads one consistent contract — the same parity discipline that's been running since rev 132.
- OpenAPI 3.1 typed coverage on the rev-134 since param + cumulative cadence — 57th unbroken rev. Closes the typed-contract gap on the rev-134 v1 enhancement in the same cycle it ships. The OpenAPI spec's `/tasks/{taskId}/comments` GET endpoint gains a typed `since` query parameter (date-time format) + a typed `since` field on the response shape (nullable string, format date-time). The rev-78 cadence pattern (every v1 enhancement gets typed in the OpenAPI spec in the same cycle it ships) reaches its 57th unbroken rev with rev 134. MCP-host code generators reading the spec see a typed contract for the recency axis immediately. The OpenAPI spec changelog header gains a rev-134 block explaining the recency axis closure + the polling pattern (echo the resolved `since` instant, reuse on the next poll). The `/api/v1` self-describing endpoint index updates the comments endpoint signature to include `since=ISO` inline so MCP-host integrators reading the index discover the recency axis without opening the spec.
Per-task discussion permalink with active-match anchor + Copy match link chip + OpenAPI typed coverage on the share-permalink format + cumulative dashboard polish
- Per-task discussion permalink with active-match anchor — closes the named rev-132 next-sprint candidate. Rev 31 introduced bare comment permalinks (#comment-<id>) so an operator could deep-link to a specific comment from the rev-30 mentions inbox or the rev-31 digest email. Until rev 133 the rev-31 listener cleared all filter scope on arrival — when an operator filtered the rev-128 thread to query='concern' + authorFilter='Steve' and rotated to match #3 of 8 via the rev-131 ↑↓ navigation, sharing a URL pointed at the comment but lost the filter scope and match position the sender was actually viewing. Rev 133 closes that gap. Extends the rev-31 hashchange listener to parse an optional trailing query string (#comment-<id>?q=concern&author=user-123&reactions=1&match=2) that restores the rev-128 keyword + author filter, the rev-130 reactions-only filter, and the rev-131 active-match index. Bare #comment-<id> keeps the rev-31/rev-128 behaviour exactly so existing permalinks (mentions inbox, digest emails, MCP-host deep-links built from the rev-132 matchedIds) are unaffected. Author param is validated against the thread's distinctAuthors so an unknown id falls back to no author scope rather than rendering an empty thread; match param is parsed as a non-negative int and clamped via the rev-131 safeActiveMatchIdx logic so an out-of-range index from a stale link can't strand the operator.
- Copy match link chip beside the rev-132 ↑↓ button cluster. Pairs the rev-133 hash-listener primitive with a one-tap copy chip. Builds the URL inline from the active filter scope + active match index so an operator who's narrowed the rev-128 thread filter to 'Steve's comments matching concern — match 3 of 8' sends a single URL the recipient lands on with the same scope + match position. Pure client-side via navigator.clipboard.writeText with the rev-42/43/101/125/126/128 execCommand fallback for non-secure contexts. Brand-color teal palette + brand-green success-pulse on copy match the rev-101 changelog permalink + rev-125 roadmap permalink + rev-128 copy-thread chip + rev-126 roadmap-filter share chip vocabulary so all the dashboard's share-affordances ring out with one consistent visual story across every surface (in-app, public marketing, share page).
- OpenAPI 3.1 typed coverage on the rev-133 share-permalink format — cadence pattern from rev 78 onward holds unbroken. Closes the typed-contract gap on the rev-133 dashboard primitive in the same cycle it ships. The OpenAPI spec's /tasks/{taskId}/comments endpoint description documents the URL hash format MCP hosts driving deep-links from external systems (Slack interactive shortcuts, CRM linkbacks, procurement audit notes) can build to benefit from the same filter+match restoration. The rev-78 cadence pattern (every v1-adjacent dashboard primitive gets documented in the OpenAPI spec in the same cycle it ships) reaches its 56th unbroken rev with rev 133. The /api/v1 self-describing endpoint index updates the comments endpoint signature to mention the rev-133 permalink format inline so MCP-host integrators reading the index discover it without opening the spec.
- Cumulative dashboard polish — brand-green success-pulse animation on the new share chip. Cumulative micro-polish (every rev 22+ has carried at least one). New ld-task-comment-match-counter-share CSS class with brand-color teal default state, brand-green is-copied state, and a 1.6s ld-task-comment-match-share-pulse keyframes animation so the operator sees confirmation without a toast. The pulse animation pairs with the rev-101 / rev-125 / rev-126 success palettes so all the dashboard's copy-success affordances ring out consistently across every share surface. Hover lift (1px) + brand-color border emphasis + focus-visible outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly on the new chip. preventDefault on mousedown keeps the search input focused so a click-then-type-then-rotate-then-share workflow stays smooth without cursor management.
Per-thread search matchedIds on the v1/MCP surface + dashboard endpoint mirror with q + authorId query params + OpenAPI typed coverage + inline ↑↓ button affordances on the match counter chip
- matchedIds on the v1 GET /tasks/{id}/comments?q= response — closes the named rev-131 next-sprint candidate. Rev 131 shipped the dashboard match counter chip + ↑↓ arrow-key navigation over per-thread search matches; until rev 132 a host driving the desk programmatically had to re-run the rev-128 matcher (body + author name, case-insensitive substring) against every comment to derive the same set. Rev 132 closes that gap by projecting matchedIds (ordered list of comment ids whose body OR author name contains the query) on the v1 comments GET response so MCP hosts rendering 'Steve's comments matching concern · 3 of 8' can use matchedIds.length as the total + rotate by index. Distinct from filtered (which counts matched-or-attached-to-matched entries — replies of a matched parent surface in the rendered thread but aren't themselves matches). Mirrors the rev-131 dashboard derivation exactly.
- Dashboard /api/tasks/{id}/comments mirror with q + authorId + matchedIds — close dashboard-side parity gap. The rev-128 dashboard component computes filtering + matchedIds purely client-side; the dashboard endpoint had been GET-only with no query params for 106 revs. Rev 132 closes the dashboard-side parity gap so any direct caller of either surface (the v1 bearer-auth endpoint or the dashboard session-auth endpoint) reads one consistent shape — same q + authorId query params, same total/filtered/matchedIds/query/authorId response fields. The in-app TaskComments component still derives matchedIds client-side; the server-side path is load-bearing for direct integrations that hit the dashboard endpoint with the same workspace member session.
- OpenAPI 3.1 typed coverage for matchedIds — cadence pattern from rev 78 onward holds unbroken. matchedIds is typed inline on the comments GET endpoint as an array of strings with full description explaining the relationship to filtered (matched-or-attached) and the rev-131 N/M counter chip + arrow-key navigation primitive it powers. Required field on the response shape so MCP-host code generators always project it. The rev-78 cadence pattern (every v1 enhancement gets typed in the OpenAPI spec in the same cycle it ships) reaches its 55th unbroken rev with rev 132.
- Cumulative dashboard polish — inline ↑↓ button affordances on the rev-131 match counter chip. Cumulative micro-polish (every rev 22+ has carried at least one). The rev-131 match counter chip surfaces N/M + a tooltip explaining the keyboard shortcuts, but mouse-first operators who don't hover for the tooltip never discover the rev-131 keyboard navigation primitive. Rev 132 pairs the keyboard discovery with mouse affordance: a tiny ↑↓ button group renders inside the chip when 2+ matches exist, firing the same state transitions as the rev-131 ArrowUp/ArrowDown handler so the two input modalities stay symmetric. Buttons share the brand-color teal palette + tabular-num typography of the parent chip so the cluster reads as one composite control. preventDefault on mousedown keeps the search input focused so a click-then-type rotation works cleanly. focus-visible outline ring matches the rev-38 dashboard accessibility pattern.
Per-author identity primitive on the v1 surface + match highlight on rev-111 marked.js HTML render path + match counter chip + ↑↓ arrow-key navigation through search matches
- Per-author identity primitive on the v1 surface — closes the named rev-130 next-sprint candidate. Rev 130 shipped the dashboard avatar primitive (initials chip + deterministic per-author HSL hue) on the rev-130 task comment thread head + rev-30 mentions inbox row head, but duplicated the helpers across both consumers because they were 12 lines each. Rev 130's running state explicitly named 'per-author identity primitive on the v1 surface' as the rev-131 candidate, citing the cost of every MCP host re-implementing the hash + initials logic. Rev 131 closes that. New shared `@/lib/author-identity` module exports `authorInitials(name)` + `authorHue(userId)` + `getAuthorIdentity(userId, name)` — same hash + initials rules as the rev-130 dashboard helpers so existing avatar colours don't shift. Both dashboard consumers (task-comments.tsx + mentions-inbox.tsx) import from there now. The v1 `GET /api/v1/tasks/{id}/comments` projects `authorInitials` + `authorHue` on every comment row + on the rev-129 distinct-authors sidecar (so MCP hosts rendering an author picker draw the same chip per author with one shape, no follow-up call). The POST handler also returns identity on the new comment so callers chaining create-then-render don't have to re-fetch the listing endpoint. Typed in the OpenAPI 3.1 spec in lockstep with the v1 mirror — the cadence pattern from rev 78 onward (every v1 enhancement gets typed in the spec in the same cycle) holds unbroken into rev 131.
- Match highlight on the rev-111 marked.js HTML render path — closes the rev-130 highlighting symmetry. Rev 130 shipped match highlighting on the short-comment renderCommentBody path (plain-text comments) — but long markdown comments rendering through `c.textHtml` (server-rendered marked HTML, rev 111) skipped the highlight, so an operator searching a long structured comment for 'concern' saw the filter narrow but had to re-read every rendered paragraph to find WHERE the match was. Rev 131 closes that gap. New `highlightCommentHtml(html, query)` helper splits the HTML on tag boundaries, walks every text-content segment, and wraps matched substrings in the same `<mark className='ld-task-comment-match'>` the rev-130 plain-text path uses — tag content (attributes, element names, class names) is left untouched so existing markup, links, and @-mention pills all stay intact. Pairs with the rev-130 plain-text highlight so the keyword filter highlights matches identically across both render paths.
- Match counter chip + ↑↓ arrow-key navigation through search matches. Until rev 131 the rev-128 keyword filter narrowed the visible thread but operators with multiple matches across a long thread had no surface for 'how many matches?' or 'jump to the next one' — they scrolled the filtered subset by hand. Rev 131 closes that with two paired primitives. (a) Compact 'N/M' counter chip beside the search input shows the active match index + total match count, with a tooltip explaining the keyboard shortcuts. Brand-color teal palette ties the counter to the rev-130 match-highlight palette so the counter and the highlight read as one feature. Tabular-num typography keeps the index numerals vertically aligned as the operator rotates through. (b) ↑↓ arrow keys + Enter (when the search input has focus) rotate through matched comments. Wraps at the ends so an operator at match #N tapping ↓ lands on #1. Cmd/Ctrl-modified keys are deliberately not handled so browser shortcuts still work outside the input. The active match gets a brighter brand-color outline + glow distinct from the rev-130 default match treatment so the eye lands on the active one first. The matched comment scrolls smoothly into view via `scrollIntoView({ block: 'center' })` 50ms after the highlight commits so the brighter active treatment + the scroll arrive at the same time.
- Cumulative dashboard polish — brand-color active-match outline + tabular-num counter typography. Cumulative micro-polish (every rev 22+ has carried at least one). New `.ld-task-comment-active-match` CSS uses a 2px brand-color teal outline + 4px offset + 6px border-radius so the active match reads as the operator's current focus across both render paths (plain-text + marked HTML). New `.ld-task-comment-match-counter` chip wears the same brand-color teal palette + tabular-num typography so the index doesn't shift width as the operator rotates through. The active-match treatment composes cleanly with the existing rev-31 deep-link permalink flash — both can fire on the same comment without competing visually since the active-match class applies to the row outline + brighter inner highlights, while the deep-link class applies to the row background flash. Three orthogonal visual cues for three different reading horizons (active match / deep-link arrival / default match) anchored in the same brand-color palette.
Per-thread search match highlighting + reactions-only filter axis + per-author identity avatars across comments + mentions inbox + OpenAPI typed coverage on the rev-106/107/108 blog cluster
- Per-thread keyword search match highlighting — closes the named rev-129 next-sprint candidate. Rev 129's running state explicitly named 'per-thread search highlighting' as the natural rev-130 candidate, citing rev-128 keyword + author search as the load-bearing primitive that needed visible match locations. Rev 130 closes that. Every short-comment render (the renderCommentBody path that doesn't go through the rev-111 marked.js HTML projection) now wraps matched substrings in <mark className='ld-task-comment-match'> with a brand-color teal background gradient + soft inset underline so an operator searching long threads sees WHERE the match is at a glance instead of re-reading every body line. Pure client-side — splits the existing per-text-chunk render in renderCommentBody so @-mention pills stay intact and the highlight stays scoped to plain text segments. Pairs with the rev-128 keyword filter (which narrows the visible thread) for the full narrow→find→read trio across the per-thread comment surface.
- Reactions-only filter axis on the rev-128 thread filter — third axis on the per-thread comment filter cluster. New 'Reacted N' chip beside the rev-128 keyword + author filter row scopes the visible thread to comments with at least one reaction. Composes with keyword + author via intersection so operators can drill 'show me Steve's comments with reactions' in two taps. Pairs with the rev-128 aggregate engagement chip (which surfaces top-emoji + total-count in the active-work card pill row) for the queue→thread engagement-drill loop: see the engagement signal in the queue, expand the thread, narrow to exactly those reacted comments via the new chip. Auto-hides when the thread carries zero reactions. Esc clears every axis (keyword + author + reactions-only) in one keystroke. Brand-purple palette distinguishes the engagement axis from the rev-128 brand-color keyword/author chips so the three filter dimensions read with three distinct visual cues.
- Per-author identity avatars in comment thread + mentions inbox. Every comment row in the per-task discussion thread + every row in the rev-30 mentions inbox now carries a 22px circular initials chip on the left of the row head. Initials computed from the author's display name (first + last initial, falls back to first two chars for single-name authors). Background is a deterministic per-author HSL hue derived from the userId hash so each teammate gets a stable identity colour across every comment they post — same primitive across both surfaces. Pairs with the rev-105 blog author profile + rev-128 author filter chip row so the workspace's identity primitive reads consistently from the public marketing surface (rev 105) through the in-app discussion + inbox surfaces (rev 130). Multi-operator teams now spot 'Steve commented' vs 'Maria commented' from the avatar colour without reading the name. Required adding `authorUserId` to the rev-30 UserMention shape so the inbox can compute the same hue from the same primitive.
- OpenAPI typed coverage for the rev-106/107/108 blog cluster endpoints. The v1 blog cluster reaches eight axes (listing rev 102 + detail rev 103 + categories rev 104 + authors rev 105 + related rev 106 + neighbours rev 107 + tags rev 108 + by-tag rev 108 + keyword rev 122) but until rev 130 only five carried typed OpenAPI schemas — the rev-106/107/108 endpoints had self-describing index entries but no typed JSON schema in the OpenAPI 3.1 spec. Rev 130 closes that gap. Four new typed endpoints: `/blog/related/{slug}` (full source + related rows shape with sharedTagCount + author profile primitive), `/blog/{slug}/neighbors` (chronological newer/older with nullable sides at timeline edges), `/blog/tags` (full tag taxonomy with post counts), `/blog/by-tag/{slug}` (per-tag listing with archive summary stats). MCP-host code generators reading the spec now have typed contracts for every blog-cluster axis. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI spec) is now fully closed across the entire blog cluster.
Per-thread comment search + aggregate engagement on the v1/MCP surface + OpenAPI typed coverage + Esc-to-clear thread filter
- GET /api/v1/tasks/{id}/comments?q=&authorId= — closes the named rev-128 next-sprint candidate. Rev 128's running state explicitly named 'per-thread comment search on the v1 surface' as the natural rev-129 candidate, citing rev-46 v1 comment listing as the load-bearing primitive that needed the search axis. Rev 129 closes that. The dashboard endpoint accepts optional `q` (≤200 chars, case-insensitive substring match across body + author name) and `authorId` (scope to one author) query params; replies always surface with their parent so the thread frame never breaks (rev-128 dashboard semantics exactly). Returns `{ comments, total (unfiltered count), filtered (matched count), authors (distinct authors with counts, sorted desc) }` so MCP hosts rendering 'show me Steve's comments on this task' can pre-populate an author picker without parsing the comment list themselves. Pure derived state — no schema, no migration. Pairs with rev-46 comment listing + rev-29 comment reactions as the three-axis comment surface on the protocol-bound side: read (rev 46) + write (rev 26 POST) + react (rev 29) + search (rev 129). The MCP server (Q3 #1) gains one more pre-typed surface with nothing left to design on the comment-search axis.
- GET /api/v1/tasks/{id}/engagement — aggregate reaction summary on the v1/MCP surface. Mirrors the rev-128 dashboard `.ld-task-reaction-summary` chip on the v1/MCP surface. Pure derived state — sums every reaction on every comment in the task's rev-26 comments JSONB. Until rev 129 the engagement signal was dashboard-only on the protocol-bound side; an MCP host driving the desk could read every comment + reaction by enumerating `/api/v1/tasks/{id}/comments` but had to re-aggregate the per-emoji totals themselves. Rev 129 collapses that to one bearer-auth call. Returns `{ commentCount, totalReactions, topEmoji, topEmojiCount, reactionTotals (full rev-29 vocabulary, 0 when unused), topReactedComments (≤5 by reaction count) }` so MCP hosts rendering 'what's the team reacting to most on this task?' have a one-call answer. Closes the per-task engagement-visibility axis at parity in the same cycle as rev-129 search.
- OpenAPI typed coverage for both rev-129 endpoints — closes the typed-contract gap in lockstep. The cadence pattern from rev 78 onward (every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships) holds unbroken through rev 129. Both new endpoints typed: the rev-129 comments endpoint gets the new `q` + `authorId` query parameters typed with full response shape (comments, total, filtered, query, authorId, authors with the per-author count breakdown); the rev-129 engagement endpoint is fully typed with the `reactionTotals` Record<emoji, integer> shape, the `topReactedComments` array of per-comment shapes, and the full rev-29 emoji enum on `topEmoji`. The OpenAPI spec changelog header gains a rev-129 block explaining the v1 mirror pattern. The MCP server (Q3 #1) inherits one more pre-typed surface with nothing left to design.
- Cumulative dashboard polish — Esc clears the rev-128 thread filter + inline 'Clear filter' chip on empty-state. Cumulative micro-polish (every rev 22+ has carried at least one). Two small but cumulative pieces: (a) the rev-128 filter input now binds Esc to clear BOTH keyword AND author scope so an operator who narrowed deeply can escape with one key — pairs with the rev-119 useComposerShortcuts Esc-to-cancel vocabulary across the dashboard typed-input surface, (b) the empty-state when filter has zero matches now carries an inline brand-color 'Clear filter' chip + a count of total comments so the operator sees 'Clear filter to see the 23 comments on this task' instead of an inert 'no matches' message. Until rev 129 an operator who narrowed too aggressively had to manually re-tap Everyone + erase the keyword to recover. Rev 129 collapses both into one tap. The thread filter now reads as a complete affordance — narrow, escape, clear, all reachable through three orthogonal input modalities (keyboard / chip / mouse).
Per-thread comment search + author filter + copy-thread markdown + aggregate-reaction summary chip on every active-work card
- Per-thread keyword + author filter on task discussions — closes the missing per-task search axis. The rev-26 task discussion primitive has been growing in load-bearingness for fourteen revs (rev 27 mentions, rev 28 threading, rev 29 reactions, rev 30 inbox, rev 31 permalinks + email digest, rev 32 inline reply, rev 33 reactions on outputs/memory, rev 34 ack receipts, rev 35 in-queue badges) but the *per-task search* axis was missing — workspaces with 50+ comments on a long-running task forced operators to scroll the entire thread to find a specific reference. Rev 128 closes that gap. New `<input type="search">` + author chip row above the thread when the task has 5+ comments (below that, scrolling beats typing). Pure client-side filter on the existing comments JSONB — matches body text + author name. Composes with the rev-30 mentions inbox + rev-31 permalinks (a hash deep-link to a specific comment clears the active filter so the targeted comment always renders on its target). Replies always surface with their parent so the discussion frame never breaks. Author chips activate when 3+ distinct authors have commented — sorted by comment count desc with stable ordering so a chip doesn't move when a new comment lands. Mirrors the rev-17 workspace search + rev-38 activity-log keyword search at the per-task comment axis. The search-axis cluster on the dashboard is now five surfaces deep — workspace (rev 17), memory (rev 8 + rev 21 tags), activity (rev 38), outputs (rev 110), and now per-thread comments (rev 128).
- Copy entire thread as markdown — extends the rev-42 markdown copy primitive to the comment-thread axis. Procurement reviewers + customer-success ops routinely paste discussion histories into CRMs / Notion / wikis to summarise stakeholder context on a deal or ticket. Until rev 128 the only path was a manual scroll-and-copy of every comment. Rev 128 closes that with a one-tap 'Copy thread' button alongside the rev-128 filter row. Assembles every visible comment + reply (respecting the rev-128 keyword + author filter) into a clean markdown package with author attribution, relative timestamps, and indented reply nesting that reads cleanly when pasted into a downstream surface. Pure client-side via `navigator.clipboard.writeText` with the standard `execCommand` fallback for non-secure contexts (matches the rev-42 / rev-43 / rev-101 copy-affordance pattern). Success-flash animation matches the rev-22+ tactile click vocabulary so the operator sees confirmation without a toast. Pairs with the rev-42 artifact markdown copy + rev-43 memory copy as the third surface on the dashboard's 'lift content out' vocabulary.
- Aggregate reaction-summary chip on every active-work card — closes the per-task engagement-visibility gap. Reactions have ridden on individual comments since rev 29 (and on outputs + memory since rev 33). Until rev 128 the *aggregate* engagement signal — the answer to 'which task in my queue has the most reaction activity?' — required expanding every thread. Rev 128 surfaces it as a quiet brand-purple chip in the active-work card pill row showing the most-used emoji + total reaction count across every comment on the task. Pure derived state from existing `task.comments[].reactions` JSONB — no schema, no migration, no extra round-trip. Tooltip on the chip shows the per-emoji breakdown so an operator can decide whether the task is hot ('🚀 8 · 🎯 3') or contentious ('👀 5 · 👍 2'). Distinct from the rev-35 comment-count pill (volume) and the rev-35 unacked-mention pill (your work waiting) — three orthogonal pill kinds in one row each answering a different reading-horizon question: volume, attention, engagement. Multi-step workflow operators triaging 20+ active tasks now spot the 'this is the discussion to read first' signal at a glance. The active-work card pill row now reads at four orthogonal axes: triage state (rev 21 priority + rev 22 due + rev 16 assignee + rev 23 pin), discussion volume (rev 35 comment count), attention (rev 35 unacked mention + rev 48 staleness), and engagement (rev 128 reaction summary).
- Visual polish — brand-purple reaction chip + tactile filter bar + copy-thread success-flash. Cumulative micro-polish (every rev 22+ has carried at least one). The new `.ld-task-reaction-summary` chip wears a brand-purple `rgba(107,78,214,*)` palette distinct from the rev-35 brand-color comment-count pill so the operator's eye reads 'engagement state' at a different cadence than 'discussion volume'. Hover lift matches the rev-22+ tactile click affordance vocabulary even though the chip is read-only — the lift signals 'this carries information'. New `.ld-task-comment-filter` bar uses a soft tinted background so the row reads as ONE control surface rather than three competing widgets (search input + author chips + copy button). Author chips share the rev-29 + rev-39 chip vocabulary with `:focus-visible` rings so keyboard-only operators land cleanly. The copy-thread chip uses the rev-22+ chip palette with brand-green success state matching the rev-101 changelog permalink + rev-125 roadmap permalink + rev-127 cost-panel synced-chip vocabulary so all the dashboard's copy-success affordances ring out consistently across every surface.
Multi-device sync for cost-panel custom row order via dashboardPrefs JSONB + OpenAPI typed coverage + cross-device sync hint chip
- Cost-panel custom row order now syncs across devices — closes the named rev-126 next-sprint candidate. Rev 126 shipped the localStorage-backed primitive on the rev-57 cost-by-source / rev-52 cost-by-assignee / rev-66 cost-by-tag panels. Until rev 127 the order was per-device — an operator who reordered the per-source panel on their laptop saw the default sort again on their phone. Rev 127 mirrors the order onto the rev-78 dashboardPrefs JSONB so the order follows the operator across machines. Same dual-layer pattern as the rev-78 panel-collapse multi-device sync: localStorage stays as the write-through cache for sync render so the dashboard never blocks on a network round-trip; server JSONB is the source of truth for cross-device drift; every order change fires a fire-and-forget PUT to /api/workspace/dashboard-prefs to keep the server JSONB authoritative. New `costPanelOrder?: { source?: string[]; assignee?: string[]; tag?: string[] }` field on `DashboardPrefs` with per-axis 50-ID cap (matches the rev-126 localStorage cap exactly so the two layers never disagree). Wired through the dashboard `/api/workspace/dashboard-prefs` Zod schema + the v1 `/api/v1/workspace/dashboard-prefs` mirror in lockstep — the cadence pattern from rev 37 onwards continues unbroken into rev 127.
- useCostPanelOrder hook gains serverInitialOrder prop + isCrossDeviceSynced flag. The rev-126 hook in `src/lib/cost-panel-order.ts` now accepts an optional `serverInitialOrder` prop passed in from the server-rendered dashboardPrefs.costPanelOrder. On mount, server JSONB wins over localStorage when both are present (this is the cross-device sync path — Machine A wrote a new order, Machine B's localStorage is stale). When `serverInitialOrder` is an empty array (operator clicked Reset on Machine A), the hook clears localStorage on Machine B too. The new `isCrossDeviceSynced` boolean on the hook's return shape drives the rev-127 'synced across devices' hint chip on every cost panel's reset row. All three cost panels (cost-by-source, cost-by-assignee, cost-by-tag) consume the prop + flag in lockstep so the multi-device sync vocabulary reads consistently across all three operator-direction surfaces.
- OpenAPI typed coverage for the rev-127 costPanelOrder field — typed lockstep with the dashboard primitive. The cadence pattern from rev 78 onwards (every dashboard prefs field gets typed in the OpenAPI 3.1 spec in the same cycle it ships) holds unbroken into rev 127. Both the GET response shape and PUT request body now type the new `costPanelOrder` field with the per-axis 50-ID cap inline. The OpenAPI spec changelog header gains a rev 127 block explaining the multi-device sync pattern and how the field closes the named rev-126 candidate. MCP-host code generators reading the spec see one consistent contract for cross-device sync of cost-panel ordering. The MCP server (Q3 #1) gains one more pre-typed surface with nothing left to design on the cost-panel-order axis at either authentication model.
- Visual polish — 'synced across devices' chip + brand-color accent on the rev-126 reset hint. Cumulative micro-polish (every rev 22+ has carried at least one). The rev-126 'Custom order applied · Reset' hint now surfaces a tiny inline state chip — brand-color teal '· synced across devices' when the order is mirrored to the server JSONB, muted-grey italic '· saved on this device' when only localStorage carries it (e.g. a rev-126-era client that hasn't been updated yet, or a network failure on the fire-and-forget PUT). Tooltip on the synced chip explains 'Saved to your account — this order follows you across devices' so operators know the order is portable. Distinct visual states with brand-color accent on the load-bearing case so the operator's eye reads success without the chip having to scream. New `.ld-cost-order-sync` + `.ld-cost-order-local` CSS classes scoped to the rev-126 hint row.
Drag-to-reorder rows on every cost panel (per-source / per-assignee / per-tag) + per-phase permalinks on /roadmap + Up/Down keyboard reorder + per-workspace persistent custom order
- Drag-to-reorder rows on every cost panel — closes the named rev-124 next-sprint candidate. The rev-57 cost-by-source, rev-52 cost-by-assignee, and rev-66 cost-by-tag panels were sorted desc by spend with no manual override since they shipped. Operators with dense cost panels (10+ tags / sources / teammates) routinely wanted to pin 'the row I'm watching this week' to the top regardless of its absolute spend. Until rev 126 they had to scroll past the row of interest every time. Rev 126 closes that gap by adding HTML5 drag-and-drop to every row on every cost panel. Each row (when 2+ rows exist and the row has a stable ID — synthetic 'untagged' / 'unassigned' buckets are not reorderable) gains a ⋮⋮ grip handle, drag-over highlights the drop target with an inset brand-color top accent, and dropping persists the new order via the new shared `useCostPanelOrder` hook in `src/lib/cost-panel-order.ts`. Persisted in localStorage under a per-workspace + per-axis key — same primitive shape as the rev-77 panel collapse (before rev 78 added cross-device sync via dashboardPrefs JSONB). Closes the operator-direction loop on the per-axis cost surface so the rev-124 cross-task drag-to-reorder primitive on dependencies and the rev-123 per-task primitive on blockers now have siblings on every cost-axis panel — same reorder vocabulary across four operator-direction surfaces.
- Per-phase permalinks on /roadmap (Now / Next / Later) — closes the named rev-125 next-sprint candidate. Rev 125 added per-item permalinks (loopdesk.space/roadmap#item-stripe-checkout) so an owner could share a specific candidate. Rev 125's running state explicitly named 'per-section share permalinks on /roadmap phases (Now / Next / Considering)' as the rev-126 candidate. Rev 126 closes that. Each phase header now carries `id="phase-<key>"` (`phase-now`, `phase-next`, `phase-later`) + a small 🔗 chip in the phase title that surfaces on hover, copies the canonical URL with hash anchor on click, and updates the URL via `history.replaceState` so back/forward navigation re-fires the highlight. Hashchange listener auto-scrolls + 2.4s flash-highlights the matching phase via the new `[data-flash="true"]` attribute (CSS animation, no class mutation needed). New stable `key` slug per RoadmapPhase decouples the human-readable phase name from the public anchor so 'Now (Q2 2026)' can be renamed to 'Now (Q3 2026)' next quarter without breaking the public URL. Owners sharing the active sprint scope (loopdesk.space/roadmap#phase-now) now hand stakeholders a URL that lands on the matching phase header.
- Up/Down keyboard arrow reorder on every cost-panel grip handle — accessibility-first. Closes the keyboard-axis gap on the rev-126 drag-to-reorder primitive in the same cycle pattern that's been running since rev 119 (Cmd+Enter / Esc shortcuts) and rev 124 (per-task blocker keyboard reorder). The rev-126 grip handle is a `<button>` with ArrowUp/ArrowDown handlers that move the focused row up/down in the list and persist the new order. Drag-and-drop is the most-discoverable affordance, but keyboard reorder is the load-bearing one for power-users + assistive tech users who can't (or don't want to) use a mouse. Pairs with the rev-119 useComposerShortcuts hook + rev-124 task-blocker keyboard reorder to extend the keyboard surface one more step into the dashboard's interactive vocabulary.
- Cumulative visual polish — quiet 'Custom order applied · Reset' hint + grip-handle hover ladder + drag-state row treatment. Cumulative micro-polish (every rev 22+ has carried at least one). New `.ld-cost-{axis}-order-hint` quiet teal-tinted dashed-border chip surfaces only when the operator has set a custom order on this panel — pairs with a primary 'Reset to spend order' button that clears the persisted order in one tap. Grip handles fade to 0.45 opacity by default and brighten to full opacity on row hover or focus so the chip never screams from every row when the operator isn't interacting. Drop-target picks up an inset 2px brand-color top accent so the drop position reads tactilely; the dragging row drops to 40% opacity so the operator sees it's in flight. New `:focus-visible` brand-color outline ring on every grip handle so keyboard-only operators land cleanly. Every operator-direction reorder surface on the dashboard (rev-123 per-task blockers + rev-124 cross-task graph blockers + rev-126 cost-by-source / cost-by-assignee / cost-by-tag) now wears one consistent visual vocabulary.
Memory CSV / JSON export closes procurement-evidence cluster's seventh axis + per-rev permalinks on /roadmap + OpenAPI typed coverage for memory export
- Memory CSV / JSON export — closes procurement-evidence cluster's seventh axis. The procurement-evidence sextet (rev 6 JSON full + rev 7 activity CSV + rev 22 outputs CSV + rev 47 decisions CSV + rev 50 stale-tasks CSV + rev 60 cost summary CSV + rev 96 sources CSV) covered every other entity surface; durable knowledge (memory) was the missing seventh edge. A SOC 2 / ISO 42001 reviewer asking 'show me this workspace's accumulated brand voice, lessons, decisions, and pinned facts' had to read the JSON full export and filter in Excel. Rev 125 closes that gap with the single takeaway artefact procurement reviewers ask for: pinned + importance-ranked rows with tags inline, capped at 5,000 rows. New `/api/workspace/memory-export?format=csv|json` (dashboard) + `/api/v1/workspace/memory-export?format=csv|json` (v1 mirror) ships in lockstep — the cadence pattern from rev 37 onwards (every dashboard primitive has a v1 equivalent within one rev) holds. Surfaced as the seventh button beside the rev-22 outputs CSV, rev-47 decisions CSV, rev-50 stale-tasks CSV, rev-60 cost summary CSV, and rev-96 sources CSV in the integrations panel data-export section using the rev-122 paired CSV/JSON chip cluster pattern.
- Per-rev permalinks on /roadmap items — closes the public marketing share-affordance trio. Rev 101 added per-rev permalinks on /changelog. Rev 102 added per-post + per-heading permalinks on /blog. /roadmap was the last public reading surface without an item-level deep-link affordance. Rev 125 closes that gap. Every roadmap item carries id="item-<key>" (e.g. #item-stripe-checkout) + a small 🔗 chip in the item title that surfaces on hover, copies the canonical URL with hash anchor on click, and updates the URL via history.replaceState so back/forward navigation re-fires the highlight. Hashchange listener auto-scrolls + 2.4s flash-highlights the matching item via the new [data-flash="true"] attribute (CSS animation, no class mutation needed). Procurement teams + early adopters citing a specific roadmap candidate ('we're upgrading once Stripe ships — see loopdesk.space/roadmap#item-stripe-checkout') now get a stable shareable URL that points at the named candidate. Reuses the rev-101 changelog clipboard fallback path (execCommand for non-secure contexts) so it works inside iframes, private-mode browsers, and embedded contexts.
- OpenAPI typed coverage for the rev-122 dashboard ?format=json flag — closes the named rev-124 candidate. Rev 122 added ?format=json to every dashboard procurement-evidence CSV export. Rev 124's running state explicitly named 'OpenAPI typed coverage on the dashboard ?format=json flag' as the natural rev-125 candidate. Rev 125 closes that gap two ways: (a) the new memory-export endpoint is fully typed in /api/v1/openapi.json with a complete row schema (id, createdAt, kind enum, importance, pinned, title, content, tags) and ?format=csv|json parameter inline; (b) the /docs#api page documents every dashboard ?format=json variant alongside its v1 mirror so session-authenticated programmatic callers see one consistent contract. The OpenAPI spec changelog header gains a rev 125 block explaining the durable-knowledge axis closure and the dashboard typed-contract symmetry. MCP-host code generators reading the spec now have a typed contract for the seventh procurement-evidence axis.
- Visual polish — quiet roadmap permalink chip with brand-color accent ladder + flash animation. Cumulative micro-polish (every rev 22+ has carried at least one). New .ld-roadmap-permalink chip uses the same brand-color teal palette as the rev-101 .ld-changelog-permalink so the public marketing share-affordance trio (changelog rev 101 + blog rev 102 + roadmap rev 125) reads with one consistent visual vocabulary. Hidden by default opacity 0 (vs the changelog's 0.55), brightens to full opacity on item hover or focus so the chip never screams from every item-head when the user isn't interacting — appropriate for the more browse-heavy roadmap surface vs the dense scroll-through changelog. New @keyframes ld-roadmap-item-flash 2.4s soft brand-color glow that fires on hash deep-link match. The roadmap item header h3 becomes an inline-flex container so the title + chip cluster reads as a coherent unit without breaking on narrow viewports.
Cross-task drag-to-reorder + inline blocker remove on the rev-38 dependency graph + Up/Down keyboard reorder on per-task blockers
- Cross-task drag-to-reorder on the rev-38 dependency graph view. Closes the explicitly-named rev-123 next-sprint candidate. Rev 123 shipped per-task drag-to-reorder of blockers on the rev-36 TaskBlockers card; the rev-38 cross-task dependency graph view was still read-only — operators triaging the workspace-wide picture could see which dependent waited on what, but couldn't change the visual order without scrolling to each task card individually. Rev 124 closes that gap by adding HTML5 drag-and-drop to every blocker pill on the dependency graph rows. Each blocker pill (when the row has 2+ blockers and the operator has editor+ access) gains a ⋮⋮ grip handle, drag-over highlights the drop target with an inset brand-color top accent, and dropping persists the new order via the existing rev-36 `PUT /api/tasks/:id/blockers` endpoint — same server primitive the rev-123 per-task surface uses, no new endpoint, no new mental model. Mirrors the rev-123 primitive at the workspace-axis exactly so the two operator-direction surfaces (per-task card + cross-task graph) read as siblings rather than parallel design languages.
- Inline blocker remove (×) on every dependency graph row. Cross-task graph blocker pills gain a small × remove affordance (gated to editor+) so admins triaging the workspace-wide dependency picture can clear stale blockers right from the cross-task surface without scrolling to the per-task card and opening its blockers picker. Pure UX — same `setTaskBlockedBy` helper the rev-36 per-task `×` button calls. Optimistic UI override: the row reflects the new state before `router.refresh()` propagates the server state back, with a clean rollback if the persist fails.
- Up/Down keyboard arrow reorder on per-task blocker grip handle. Closes the keyboard-axis gap on the rev-123 drag-to-reorder primitive in the same cycle pattern that's been running since rev 119 (Cmd+Enter / Esc shortcuts). The rev-123 grip handle was a `<span>` — pure mouse affordance, unreachable for keyboard-only operators (and pointer-restricted assistive tech users). Rev 124 promotes it to a `<button>` with ArrowUp/ArrowDown handlers that move the focused blocker up/down in the list and re-focus the grip after the DOM update via requestAnimationFrame so successive arrow presses keep operating on the same blocker. Accessibility-first: drag-and-drop is the most-discoverable affordance, but keyboard reorder is the load-bearing one for power-users + assistive tech. Pairs with the rev-119 useComposerShortcuts hook to extend the keyboard surface one more step into the dashboard's interactive vocabulary.
- Cumulative visual polish — focus-visible ring on grip handle + tactile drop-target accent on cross-task blockers. Cumulative micro-polish (every rev 22+ has carried at least one). New `:focus-visible` brand-color outline on the per-task grip button so keyboard-first operators land cleanly on the reorder affordance. New `.ld-tdg-blocker.is-drag-over` inset top accent on the cross-task graph drop target so the drop position reads tactilely without competing with the rev-38 brand-amber dependent-row treatment. New `.ld-tdg-summary-hint` quiet italic line in the panel head reads `Drag blockers to reorder · × to remove` only when the workspace has at least one row with 2+ blockers AND the operator has editor+ — surfaces the affordance without screaming to viewers or to single-blocker workspaces.
JSON link on the rev-65 scoped activity-export UI + drag-to-reorder per-task blockers + OpenAPI typed coverage for the six-axis procurement-evidence export cluster
- JSON link option on the rev-65 scoped activity-export UI. Closes the explicitly-named rev-122 next-sprint candidate. Rev 122 added `?format=json` to every dashboard CSV export and rev 121 mirrored the flag on every v1 export — but the rev-65 ActivityExportRange component only emitted CSV download URLs, so session-authenticated callers wanting a date-scoped JSON payload had to construct the URL by hand. Rev 123 closes that gap. The component now renders a paired CSV/JSON chip cluster (mirroring the rev-122 paired-chip pattern from the integrations panel data-export section) so the date-range UI reads as a sibling of the unscoped exports above it. CSV stays the primary affordance for procurement reviewers downloading the takeaway artefact; the ambient JSON chip opens the same scoped data in a new tab for in-app analytics and browser-side audit tooling without leaving the session.
- Drag-to-reorder per-task blockers (closes named rev-122 next-sprint candidate). The rev-36 task-blocker primitive made dependencies a real queue gate (the pulse engine's `selectNextTask` filter is order-agnostic — it requires *all* blockers done before the dependent is eligible) but the visual order in the dashboard surface is operator-direction: which blocker reads first when triaging multiple. Until rev 123 the rev-38 task dependency graph view + the rev-36 inline TaskBlockers list were both read-only. Rev 123 closes the named candidate by adding HTML5 drag-and-drop to the per-task blockers list — each row gains a ⋮⋮ grip handle (visible only when 2+ blockers are declared and the user has editor+ access), drag-over highlights the drop target with a brand-color top accent, and dropping persists the new order via the existing rev-36 `PUT /api/tasks/:id/blockers` endpoint. Pure UX/operator-direction primitive on top of the existing schema — no new column, no new endpoint, no new mental model. Mirrors the rev-21 priority pill at the per-blocker axis.
- OpenAPI typed coverage for `?format=csv|json` on the six-axis procurement-evidence v1 export cluster. Closes the typed-contract gap on the procurement-evidence v1 export cluster. Rev 121 added `?format=json` to every v1 export endpoint and rev 122 mirrored the flag on the dashboard side, but only `/workspace/cost-export` (rev 84) and `/workspace/sources-export` (rev 96) had typed schemas — the four remaining endpoints (`/workspace/activity-export`, `/workspace/artifacts-export`, `/workspace/decisions-export`, `/workspace/stale-tasks-export`) had self-describing-index entries but no OpenAPI typing. Rev 123 closes that gap by typing all six endpoints with `format=csv|json` parameter + `application/json` response shape inline, so MCP-host code generators reading the spec see one consistent contract across the entire procurement-evidence cluster regardless of output shape.
- Cumulative visual polish — drag handle + smooth blocker-row transitions. Cumulative micro-polish (every rev 22+ has carried at least one). New `.ld-task-blocker-grip` muted-tone grip handle brightens on hover so the rev-123 reorder affordance is discoverable without screaming. The blocker row gains 160ms transitions on background-color/transform/border-color so drag-over feels tactile rather than jumpy; the dragging row drops to 40% opacity so the operator sees it's in flight; the drop-target picks up a soft brand-color top accent so the eye knows where the dropped row lands. The rev-by-rev visual-hierarchy discipline keeps the dashboard's interactive surfaces reading as a coherent design language even as the cumulative affordance count grows.
?format=json on every dashboard CSV export + paired CSV/JSON chips on the integrations panel + keyword search on the v1 blog endpoint
- ?format=json on the six dashboard procurement-evidence CSV endpoints. Closes the explicitly-named rev-121 next-sprint candidate. Rev 121 added `?format=json` to every v1 export endpoint so MCP hosts could pipe audit history through their own analytics pipelines without parsing CSV; the dashboard counterparts (the routes session-authenticated callers hit when they're already logged in) still spoke CSV-only. Rev 122 closes that gap on every axis (activity / outputs / decisions / stale-tasks / sources / cost-summary). Each dashboard CSV export now also accepts `?format=json` and returns the same typed shape the rev-121 v1 endpoint emits — so in-app analytics, embedded dashboards, and browser-side audit tooling consume one canonical shape regardless of whether the caller authenticates with a session cookie or a bearer ingest token.
- Paired CSV / JSON chip cluster on the integrations panel data-export section. Cumulative dashboard polish (every rev 22+ has carried at least one). New `ExportButtonPair` client component renders a primary CSV download chip (the rev-22+ design language) paired with a small ambient `JSON` sibling chip on every procurement-evidence export (activity log / outputs / decisions / stale tasks / cost summary / sources). The CSV chip stays the primary affordance — the JSON chip wears subtler `.ld-export-pair-json` styling so it reads as ambient affordance rather than competing with the primary action. One round-radius cluster, two output formats, zero new mental model. Closes the dashboard-side affordance loop on the rev-122 endpoint primitive in the same cycle the primitive ships.
- /api/v1/blog?q=… keyword search across title + excerpt + tags. The rev-102 dashboard `BlogSearch` client component has been client-side-only for 20 revs — MCP hosts and AI tooling roundup newsletters reading the blog via JSON had no protocol-bound way to retrieve posts by keyword and were forced to fetch the full listing and filter client-side. Rev 122 closes that gap: the existing `/api/v1/blog` endpoint accepts an optional `q` keyword query (≤200 chars) that searches across title, excerpt, and tags. Mirrors the rev-102 dashboard matcher exactly so the dashboard and protocol-bound surfaces never drift on which posts match a query. Composes with `tag` and `sinceDate` via intersection. OpenAPI 3.1 spec typed in lockstep with the dashboard primitive (cadence pattern from rev 78 onward holds unbroken into rev 122).
- Cumulative typed-contract polish — OpenAPI changelog block + self-describing index for rev-122. Cumulative micro-polish on the typed-contract surface itself. The OpenAPI 3.1 spec's blog endpoint description picks up the new `q` query parameter inline; the response body's `q` echo is added to the `required` set so code generators see the field consistently regardless of whether it was supplied. The `/api/v1` self-describing endpoint index updates the blog endpoint signature accordingly. The rev-22+ design-language thread continues into rev 122 — every public surface continues to read as authoritative with-or-without code generation, so the upcoming MCP server's wrapping work has nothing left to design on the procurement-evidence axis or the public marketing axis at either output shape (CSV / JSON).
JSON variant on every procurement-evidence v1 export endpoint (activity / outputs / decisions / stale-tasks / sources / cost) + one-tap 'Caught up' affordance on the rev-76 since-last-visit badge
- ?format=json on the v1 activity / outputs / decisions / stale-tasks export endpoints. Closes the explicitly-named rev-120 next-sprint candidate. Until rev 121 the rev-120 v1 export endpoints were CSV-only — MCP hosts wanting to pipe the procurement-evidence audit trail through their own analytics had to parse CSV. Rev 121 adds an optional `?format=json` query param to every rev-120 v1 export endpoint (activity, outputs, decisions, stale-tasks). CSV stays the default for backwards compatibility. The JSON variant returns typed `{ rows, total, capped }` payloads (with `since` / `until` for activity, `thresholdDays` for stale-tasks). Each pair of endpoints shares one row-fetcher helper (`getWorkspaceActivityRows`, `getWorkspaceArtifactsRows`, `getWorkspaceDecisionsRows`, plus the existing `getStaleTasks`) so the CSV takeaway artefact and the JSON-pipeline read can never drift. JSON projects `tags` as arrays (not the CSV `|`-joined string) and `publicShareUrl` as relative paths.
- ?format=json on /api/v1/workspace/sources-export and /api/v1/workspace/cost-export. Closes the v1 export JSON symmetry across the full six-axis procurement-evidence cluster (activity / outputs / decisions / stale-tasks / sources / cost). Sources JSON projects `includeKeywords` and `excludeKeywords` as arrays (not the CSV `|`-joined string) plus the rev-16 health diagnostic columns + rev-12 7-day signal-rate inline. Cost JSON returns a structured object grouped by axis (`{ daily, byTask, bySource, byAssignee, timezone, generatedAt }`) so MCP hosts and FinOps pipelines can render the cost summary without parsing the four-section CSV. New `getWorkspaceSourcesRows()` + `getWorkspaceCostSummary()` helpers query through the same per-axis getters the CSV uses so the two output formats can never drift. The procurement-evidence v1 cluster is now both CSV-takeaway-ready (procurement reviewers) AND JSON-pipeline-ready (MCP hosts) on every axis — closes the protocol-bound side of the takeaway story.
- ✓ Caught up affordance on the rev-76 WhatsNewBadge. Until rev 121 the only way to clear the rev-76 'N updates since you were last here' badge + the rev-76 per-row 'new' pills + the rev-120 per-bucket 'N new' pills was to wait for the rev-76 60-second `markWorkspaceVisited` throttle to tick on the next render — operators landing on the dashboard, scanning the deltas, then wanting to dismiss them now had no one-click escape. Rev 121 adds an explicit `✓ Caught up` button inside the rev-76 badge that POSTs to a new viewer+ `/api/workspace/mark-caught-up` endpoint, bypassing the throttle so every 'new' surface anchored to the operator's `lastVisitedAt` (badge / bucket pill / row pill / Only-new chip) collapses on the next render. Per-user-per-workspace — an admin marking themselves caught up doesn't affect another admin's pills. Pairs naturally with the rev-120 per-bucket 'N new' pills as the explicit close-the-loop affordance.
- /docs page documents the new JSON format + cumulative trust-signal polish on the v1 self-describing index. Every rev-120 v1 export entry on the self-describing index at `GET /api/v1` and the public docs page now names the `?format=csv|json` parameter inline so MCP-host code generators reading the index discover the JSON variant without having to read the changelog. Cumulative micro-polish — every rev 22+ has carried at least one. Rev 121's polish is on the typed-contract surface itself: the index reads as authoritative with-or-without code generation, so the upcoming MCP server's wrapping work has nothing left to design on the procurement-evidence axis at any of the six axes (activity / outputs / decisions / stale-tasks / sources / cost) at either output shape (CSV / JSON).
Procurement-evidence CSV v1 mirrors (activity / outputs / decisions / stale-tasks) + activity log expand-all/collapse-all + per-bucket 'N new since visit' pill
- /api/v1/workspace/activity-export — bearer-auth CSV mirror of rev-7 dashboard endpoint. The rev-7 activity CSV download has been dashboard-only for 113 revs. Until rev 120 MCP hosts and audit-evidence-takeaways had to scrape the JSON `/api/v1/activity` endpoint and re-format for procurement reviewers — the JSON is paginated, newest-first, and not the procurement-friendly shape SOC 2 / ISO 42001 reviewers ask for. New `GET /api/v1/workspace/activity-export?since=ISO&until=ISO` route reuses `getWorkspaceActivityCsv()` verbatim — one server-side implementation, two surfaces. Same `since` / `until` ISO query params as the rev-65 dashboard variant so the takeaway can be scoped to a specific quarter or month. Closes the procurement-evidence v1 cadence pattern at the activity axis.
- /api/v1/workspace/artifacts-export — bearer-auth CSV mirror of rev-22 dashboard endpoint. The rev-22 outputs catalog CSV (createdAt, updatedAt, kind, status, title, summary, tags joined by `|`, share URL, view count) was dashboard-only for 98 revs. New `GET /api/v1/workspace/artifacts-export` route mirrors the rev-22 dashboard endpoint exactly. Reuses `getWorkspaceArtifactsCsv()` verbatim. Closes the procurement-evidence v1 cadence pattern at the outputs axis.
- /api/v1/workspace/decisions-export — bearer-auth CSV mirror of rev-47 dashboard endpoint. The rev-47 decisions log CSV (scoped exactly to the rev-9 dashboard 'Decisions log' semantics — status ∈ {approved, archived}, kind ≠ brief) was dashboard-only for 73 revs. New `GET /api/v1/workspace/decisions-export` route mirrors the rev-47 dashboard endpoint exactly. Reuses `getWorkspaceDecisionsCsv()` verbatim. Distinct from the existing `GET /api/v1/decisions` endpoint (which returns JSON) — the CSV variant is the procurement-friendly takeaway shape. Closes the procurement-evidence v1 cadence pattern at the decisions axis.
- /api/v1/workspace/stale-tasks-export — bearer-auth CSV mirror of rev-50 dashboard endpoint + activity log expand-all/collapse-all + per-bucket 'N new since visit' pill. Three-part change. (a) The rev-50 stale-tasks CSV (with rev-51 cost columns inline) was dashboard-only for 70 revs. New `GET /api/v1/workspace/stale-tasks-export?thresholdDays=N` mirrors the rev-50 endpoint with the same 1-60 threshold override. Closes the procurement-evidence v1 cadence pattern at the stale-task axis — the procurement-evidence v1 mirrors are now complete across activity (rev 7), outputs (rev 22), decisions (rev 47), stale-tasks (rev 50), cost-summary (rev 60), and sources (rev 96), a five-axis cluster all on the protocol-bound surface. (b) The rev-78 time-bucketed activity log + rev-79 per-bucket collapse gained a one-tap `Expand all` / `Collapse all` chip pair above the bucket list when 2+ buckets exist — until rev 120 an operator with 4 collapsed buckets had to click 4 chevrons to expand them all. Persisted via the same localStorage + dashboardPrefs sync as the per-bucket toggle. (c) Each bucket head now also shows a brand-color `N new` pill when ≥1 entry in that bucket landed since the operator's previous visit — pairs with the rev-76 since-last-visit primitive at the rev-78 bucket axis so operators with collapsed buckets see at a glance which ones carry unseen content without expanding them.
Cmd/Ctrl+Enter submits any composer + ⌘B/⌘I/⌘⇧K markdown shortcuts + Esc-to-cancel symmetry across all nine typed-input surfaces
- Cmd/Ctrl+Enter submits any open composer (universal Slack/GitHub/Linear pattern). Rev 14/22/24 + the rev-115 markdown toolbar + the rev-117/118 visible draft-saving made every typed-input composer on the dashboard rich and recoverable, but submission still required reaching for the mouse. Rev 119 closes the keyboard surface gap with a shared `useComposerShortcuts` hook in `src/lib/composer-shortcuts.ts` that binds Cmd/Ctrl+Enter to a submit handler and Esc to a cancel handler on the supplied textarea ref. Wired into all nine typed-input composers — artifact body editor, operator note, renew, memory edit, top-level comment, signal-add, memory-add, task-create (summary AND goal), and bulk import — so an operator with hands on keyboard can compose, format, and submit without ever touching the mouse. The hook scopes to the textarea ref so multiple composers on the same page coexist cleanly; preventDefault on both events stops Enter from submitting surrounding forms and Esc from bubbling to the rev-23 dashboard shortcuts overlay.
- Cmd+B / Cmd+I / Cmd+Shift+K markdown shortcuts in the rev-115 toolbar. The rev-115 markdown toolbar shipped one-tap Bold / Italic / Code / Link / Quote / List / Heading buttons above every typed-input composer, but no keyboard shortcuts. Rev 119 adds the standard rich-text editor pattern via a new useEffect inside `MarkdownToolbar` that binds Cmd/Ctrl+B (bold), Cmd/Ctrl+I (italic), and Cmd/Ctrl+Shift+K (link insert) on the textarea ref. Plain Cmd+K is reserved by the rev-27 command palette — composing Shift keeps the link affordance discoverable without shadowing the palette. Tooltips updated to reflect the platform-aware shortcut glyph (⌘ on macOS, Ctrl+ elsewhere) so operators discover the shortcuts without opening the rev-23 overlay. Single change to `markdown-toolbar.tsx` automatically reaches all nine composers using the toolbar.
- Esc-to-cancel symmetry across composers. Most rev-119 composers now respect Esc to close. The shared `useComposerShortcuts` hook accepts an optional `onCancel` that fires on Esc when the textarea is focused. Wired into the artifact body editor, operator note, renew composer, memory editor, memory-add, task-create, and bulk import — pairs with the rev-117/118 'Discard draft' affordance so an operator can either persist or abandon in-progress work with a single key. Task comments deliberately skip the Esc binding because the existing inline keydown handler already handles Esc (dismissing the @-mention popover or cancelling edit mode); double-binding would clash. The hook calls preventDefault but NOT stopPropagation so existing inline Esc handlers can still observe the event.
- Keyboard shortcuts overlay split into Global + Composer sections + cumulative visual polish. The rev-23 keyboard shortcuts overlay (`?` to open) gains a 'Composer (typed-input fields)' section listing the rev-119 shortcuts (⌘ Enter submit, Esc cancel, ⌘ B bold, ⌘ I italic, ⌘ ⇧ K link) so they're discoverable without reading the toolbar tooltips. New `.ld-shortcut-section-head` CSS divides the overlay into Global vs Composer groups, anchored by an uppercase eyebrow + subtle bottom border that matches the rev-22+ design language. Submit/Cancel buttons across every composer also gained `title` attributes naming the shortcut (e.g. 'Save edit (⌘/Ctrl + Enter)', 'Cancel (Esc)') so even operators who don't open the overlay see the affordance on hover. Cumulative micro-polish (every rev 22+ has carried at least one); rev 119 closes the keyboard discoverability gap on the most-frequent operator action across the dashboard.
Auto-save drafts on memory bulk import + markdown toolbar + row-by-row staged preview + 'N staged' chip on closed trigger
- Auto-save drafts on memory bulk import (closes the named rev-117 next-sprint candidate). Rev 117's running state explicitly named the rev-22 memory bulk import composer as the only typed-input composer left without auto-save — the previous eight composers (artifact body, operator note, renew note, memory edit, comment, signal-add, memory-add, task-create) all carried the rev-115/116/117 auto-save primitive but the bulk paste-list, the most-painful loss-of-work surface on the dashboard (up to 30 lines × ≤600 chars), did not. Rev 118 closes that. New `draftKey.memoryBulkImport` namespace + workspace-scoped storage. The composer restores any in-flight paste on open with the rev-116 amber 'Restored draft from this browser. Import or discard.' callout, persists debounced 500ms, fires the rev-117 brand-color teal '✓ Draft saved' chip on every persist, and clears the draft on submit / Discard / empty-textarea. Closes the auto-save symmetry across every typed-input composer on the dashboard — nine composers, one consistent persistence vocabulary.
- Markdown toolbar on the bulk import textarea (extends rev-115/116 toolbar coverage to the ninth surface). Rev 115 shipped the markdown toolbar (B / I / `code` / link / quote / list / ordered-list / heading + rev-116 strikethrough / image / table) above every typed-input composer that drives the rev-113/114 live-preview surface. The bulk import was the only typed-input composer that didn't carry the toolbar even though operators pasting structured memory entries (bulleted brand-voice rules, decision rationales with sub-points) benefit from the same one-tap insert affordance. Rev 118 closes that by mounting the compact toolbar variant above the bulk paste-list textarea (skipHeading=true since heading-level structure rarely fits a memory entry's title-from-content split). Closes the help-typing affordance on the ninth composer surface.
- Row-by-row staged preview cards mirroring the server splitter. The server-side `bulkAddMemoryEntries` helper splits each line at the first `.:!?` separator to derive a title + content pair. Until rev 118 that split was server-side only — operators routinely mis-formatted a line ('Brand voice; warm, direct, never corporate' splits at the wrong character) and only discovered it post-creation when the memory list rendered with ugly title/content boundaries. Rev 118 closes the gap with a client-side mirror of the splitter + a row-by-row staged preview showing what each entry will become before the operator submits. Capped at 5 visible rows so a 30-line paste doesn't grow the panel unbounded; '…and N more' tail when truncated. Brand-color left-border + soft gradient anchors the preview as a sibling of the rev-112 template-goal preview surface.
- 'N staged' chip on the closed Bulk import button + extracted scoped CSS classes for the bulk-import surface. Until rev 118 the operator had no signal that an in-progress paste-list was waiting inside the closed bulk-import panel — the rev-117 auto-save was invisible on the closed trigger. Rev 118 surfaces a small brand-color teal 'N staged' chip on the closed button whenever a draft has at least one valid line (4+ chars). Same colour vocabulary as the rev-117 transient '✓ Draft saved' chip so the persistence narrative reads as one across the open and closed states of the composer. Plus cumulative architectural cleanup: the bulk-import composer's previous inline-styled treatment (style={{}} attributes on every element) is replaced with scoped `.ld-memory-bulk-*` classes in globals.css so the panel finally reads as part of the rev-22+ design language thread that the rest of the dashboard follows. New 220ms slide-in animation on the open panel matches the rev-114 live-preview entry animation so all the dashboard's expanding panels feel uniform.
Auto-save drafts on signal-add + memory teach + task creation + visible 'Draft saved' chip across all eight composers
- Auto-save drafts on signal-add + memory teach + task creation forms (closes the named rev-116 next-sprint candidate). Closes the explicit rev-116 next-sprint candidate ("auto-save drafts on the three remaining rev-115 toolbar surfaces"). Rev 116 closed auto-save on the four most-frequently-opened entity-edit composers (operator notes, renew note, memory edit, comment); the three remaining surfaces — rev-22 signal-add form, rev-6 memory teach-the-desk add form, and rev-24 task creation form — create new entities rather than editing existing ones, so they need workspace-scoped draft keys instead of entity-scoped. Rev 117 ships exactly that, extending the rev-116 draftKey namespace with three new helpers (signalAdd, memoryAdd, taskCreate). Each composer now restores any in-flight draft on open with a contextual 'Restored draft from this browser. Submit / Save / Queue or discard.' callout, persists changes debounced 500ms, and clears the draft on submit / cancel / empty-field. Templates on the task creation form deliberately clear the draft on apply so the operator sees exactly the template shape, not a half-merged state. Until rev 117 a stray Cmd-W on a partial signal-add or task creation form destroyed the work; rev 117 closes that on every typed-input creation composer not just every editing composer.
- Visible 'Draft saved' chip across all eight auto-save composers (closes the named rev-116 next-sprint candidate). Closes the explicit rev-116 next-sprint candidate ("draft-saved indicator on auto-save composers"). Rev 115/116 shipped silent auto-save — operators didn't know the draft was persisted until they reopened the composer. Rev 117 closes that affordance loop with a transient 'Draft saved' chip that fades on each persist via a 1.8s ld-draft-saved-fade keyframes animation. Re-mounts on every save (key={savedAt}) so the animation re-fires on every persist, not just the first. Brand-color teal palette distinguishes the rev-117 transient persistence signal from the rev-116 amber 'Restored draft' callout (which is sticky until dismissed). Wired into all eight typed-input composers — five existing rev-116 surfaces (artifact body editor, operator notes, renew note, memory edit, comment) + three new rev-117 surfaces (signal-add, memory-add, task-create) — so the workspace's auto-save vocabulary now includes both the recovered-draft callout (rev 116) and the just-persisted-draft chip (rev 117) with one consistent visual story across every typed-input creation and editing surface.
- Workspace-scoped draftKey namespace + the rev-117 DraftSavedChip primitive. Cumulative architectural cleanup. The rev-116 draftKey namespace covered five entity-scoped composers (artifactBody, taskOperatorNote, taskRenewNote, taskComment, memoryEdit) keyed by entity id. Rev 117 adds three workspace-scoped composers (signalAdd, memoryAdd, taskCreate) keyed by workspace id since they create new entities rather than editing existing ones — a partial signal-add in workspace A should NOT restore on switching to workspace B. New shared `DraftSavedChip` component (src/components/dashboard/draft-saved-chip.tsx) consumes a single `savedAt` timestamp prop so every composer surface uses the same chip with no per-component duplication. The chip uses the existing rev-116 storage layer verbatim — pure UI on top of the rev-116 primitive, no new persistence concept.
- Visual polish — `.ld-draft-saved-chip` palette + animation. Cumulative micro-polish (every rev 22+ has carried one). New .ld-draft-saved-chip CSS uses brand-color teal (rgba(31,143,137,*)) so it reads as a positive persistence signal — distinct from the rev-116 amber .ld-draft-restored callout palette ("your previous work is here, decide what to do with it"). The chip lifts 2px on appear, holds for ~1s at full opacity, then fades 2px upward — clean enough to read without competing with the rev-114 live preview or the rev-115 word-count chip. Pointer-events: none so it never blocks form interaction. The two affordances (restored callout sticky + saved chip transient) read with one coherent vocabulary at different scopes: amber sticky = your past work, teal transient = your current keystroke.
Auto-save drafts on every typed-input composer + table/strikethrough/image markdown toolbar buttons + shared restored-draft callout vocabulary
- Auto-save drafts extended to operator notes + renew note + memory edit + comment composer (closes named rev-115 candidate). Closes the explicit rev-115 next-sprint candidate ("auto-save drafts on the seven other rev-115 toolbar surfaces"). Rev 115 shipped 7-day-TTL localStorage drafts on the artifact body editor only; rev 116 extracts that primitive into a shared draft-storage helper (src/lib/draft-storage.ts with draftKey namespacing helpers) and wires it into four more typed-input composer surfaces: rev-14 operator notes (per-task key), rev-50 renew note (per-task key), rev-7 inline memory editor (per-memory key), and the rev-26 top-level new-comment composer (per-task key). Each composer now restores any in-flight draft when re-opened with a 'Restored draft from this browser. Save or discard.' callout, persists changes debounced 500ms, and clears the draft on submit / cancel / matching-saved-state. The most-painful operator-loss flows on the dashboard — a stray Cmd-W during a 600-char operator note, a 1000-char comment composed before the tab crashed, a memory edit interrupted by a notification — all now survive.
- Markdown table + strikethrough + image buttons on the rev-115 toolbar. Closes the named rev-115 next-sprint candidate ("markdown table support in the rev-115 toolbar") and adds two more standard markdown affordances in the same cycle. Strikethrough wraps `~~text~~` (GFM-rendered correctly by the rev-10 marked.js pipeline). Image inserts `` and selects the URL portion so the operator can paste their image URL inline (mirrors the rev-115 link insert at the image axis). Table inserts a 3-row × 2-col GFM skeleton on its own line and selects 'Header 1' so the operator can immediately overwrite. Heading + table only render in the default (non-compact) variant since they rarely fit inline composers. The toolbar surface now carries 10 buttons across all default-variant composers (B / I / strike / `code` / link / image / quote / list / ordered-list / heading / table) and 8 across compact composers, closing the rendering symmetry on every standard markdown affordance the rev-10 marked.js pipeline already supports.
- Shared draft-storage helper extracted to src/lib/draft-storage.ts. Cumulative architectural cleanup. The rev-115 artifact-body-editor implemented draft persistence as a one-off per-component helper (DRAFT_TTL_MS const + draftStorageKey() / readDraft() / writeDraft() / clearDraft() inline). Rev 116 extracts that into a typed shared module with namespaced key helpers (draftKey.artifactBody, draftKey.taskOperatorNote, draftKey.taskRenewNote, draftKey.taskComment, draftKey.memoryEdit) so every composer on the dashboard speaks the same draft vocabulary. TTL stays at 7 days (the rev-78 multi-device-sync of dashboard prefs is the wrong primitive for in-progress edits — drafts are local because they're in-progress and shouldn't fan out to other devices mid-edit). Future composers can claim a new namespace via one new draftKey helper.
- Visual polish — shared `.ld-draft-restored` callout class. Cumulative micro-polish (every rev 22+ has carried one). Until rev 116 the rev-115 artifact-body-editor restored-draft callout was inline-styled (background: rgba(207,108,58,0.08); border: 1px solid rgba(207,108,58,0.32); color: #7c4520). Rev 116 promotes that into a shared CSS class (.ld-draft-restored + .ld-draft-restored-action) so the four other composers that gained auto-save in rev 116 (operator notes, renew note, memory edit, comments) read the recovered-draft state with one consistent visual vocabulary. Amber palette signals 'your previous work is here, decide what to do with it' — distinct from the brand-color .ld-md-live-preview surface which signals 'this is what your typing will render as.' Each composer's callout copy is contextual ('Save or discard.' / 'Send or discard.' / 'Post or discard.' / 'Renew or discard.') so the action verb matches the composer's submit button.
Markdown toolbar across every typed-input composer + word-count/reading-time chip + auto-save drafts on the artifact body editor
- Shared markdown toolbar on every rev-113/114 live-preview composer (closes named rev-114 candidate). Closes the named rev-114 next-sprint candidate at the operator-help-typing axis. Rev 113/114 closed the verify-after-typing surface (live preview) on seven composer surfaces (signal detail, artifact body editor, memory teach + memory edit, task creation summary + goal, operator note, renew note, comment). Rev 115 closes the help-typing surface — a small toolbar above each textarea with one-tap buttons for B / I / `code` / link / quote / list / ordered-list / heading. Toolbar preserves cursor position via the textarea ref + selection range, replacing the highlighted span with the wrapped output and re-selecting the inserted text so the operator can keep typing. Compact variant for inline composers (operator notes, comments, renew notes); default variant for full-form composers (artifact body, memory, signal detail, task creation). Now every composer surface carries the standard write/verify/render trio that every modern markdown editor pairs.
- Word-count + reading-time chip on the rev-23 inline artifact body editor. Cumulative micro-polish on the operator's most load-bearing direct-edit surface. The character-count chip has shipped since rev 23; rev 115 adds words + estimated reading time at 220 wpm (the same heuristic the rev-104 blog reading-time chip uses) alongside the existing char count. Long structured briefs (the rev-10 governance-first artifact, decision artifacts with rationale + risk sections, customer-onboarding drafts) now show the structural metric — ~8 minute read at 220 wpm — not just raw char count. Procurement reviewers reading shared briefs care about reading time; operators editing them now see what their reviewer will see.
- Auto-save drafts to localStorage on the artifact body editor. New 7-day-TTL localStorage drafts on the rev-23 inline artifact body editor. While the editor is open, every change is debounced + persisted (per-artifact key); on Edit-click, the draft is restored if it differs from the saved state, with an amber 'Restored draft from this browser. Save or discard.' callout + Discard button. Drafts survive a tab close, refresh, accidental Cancel, or browser crash. Cleared on save / discard / matching-saved-state. Closes the most-painful operator-loss flow on the dashboard — a 12,000-char brief edit that gets discarded by a stray Cmd-W now survives.
- Cumulative visual polish on the new toolbar surface. Cumulative micro-polish (every rev 22+ has carried at least one). New .ld-md-toolbar + .ld-md-toolbar-btn CSS shares the rev-22+ design language: 1px transparent border that emphasises in brand-color on hover, 1px hover lift, focus-visible outline ring matching the rev-38 dashboard accessibility pattern. Compact variant uses 22px buttons (vs 26px default) so the toolbar reads as ambient affordance on inline composers without overwhelming the textarea below. Toolbar buttons use Lucide icons (Bold, Italic, Code, Link, Quote, List, ListOrdered, Heading) so the visual vocabulary matches every other dashboard chip surface.
Live markdown preview on signal detail + artifact body editor + memory teach-the-desk + memory edit — closes the rev-113 typed-input rendering quintet
- Live markdown preview on the rev-22 signal-add form detail field (closes named rev-113 candidate). Closes the named rev-113 next-sprint candidate at the signal-axis composer. Until rev 114 the rev-22 manual signal-add form rendered the 'What happened?' detail field as a plain textarea — operators pasting structured signal context (forwarded email bodies, Slack threads, customer feedback excerpts with bulleted asks) had no surface to verify how the field would render once the AI cycle and downstream renderers consumed it. Rev 114 adds an inline brand-color-accented preview directly below the textarea using the rev-113 unified .ld-md-live-preview surface + same shouldRenderAsMarkdown() gate so short one-line signals stay plain. Closes the rendering symmetry gap on the signal-axis composer alongside the rev-113 task-creation + operator-note + renew-note + comment composers.
- Live markdown preview on the rev-23 inline artifact body editor (closes named rev-113 candidate). Closes the named rev-113 next-sprint candidate at the artifact-body axis. The rev-23 inline body editor is the operator's primary direct-edit surface for outputs — title (160 chars), summary (260 chars), body (12,000 chars markdown). Until rev 114 the body field rendered as a plain monospace textarea; operators editing structured output (briefs with headings + bulleted action items, decision artifacts with rationale + risk sections) had to save and look at the rendered card to verify their changes parsed correctly. Rev 114 adds an inline preview that fires the same rev-10 renderArtifactBody helper used at save time + on the public share page — the preview is bit-for-bit what the operator sees post-save. New .is-long-form modifier on .ld-md-live-preview-body extends max-height 220px → 360px since the body field caps at 12,000 chars (vs 600 for operator notes / 1000 for comments). Closes the verify-before-save gap on the operator's most load-bearing direct-edit surface.
- Live markdown preview on the rev-6 memory teach-the-desk form + rev-7 inline memory editor. Extends the rev-114 typed-input primitive to the two memory composer surfaces. The rev-6 'Teach the desk' add form and the rev-7 inline memory editor both feed durable knowledge into the workspace's memory store; rev-110 made memory entries render as markdown but the *write* surface stayed plain. Operators pasting structured knowledge (bulleted brand-voice rules, decision rationales with sub-points, lessons-learned with numbered steps) now see the rendered shape before saving so the next AI cycle's memory retrieval reads what they intended. Closes the read/write symmetry on the memory-axis the same way rev 111 closed it on operator notes + comments and the same way rev 113 closed it on the rest of the typed-input cluster.
- Tactile hover affordance on every .ld-md-live-preview surface + cumulative visual polish. Cumulative micro-polish — every rev 22+ has carried at least one. Every .ld-md-live-preview surface (now five composers + two memory variants) gains a 220ms hover lift: border-color steps from rgba(31,143,137,0.32) → 0.55, gradient deepens, a soft brand-color box-shadow appears on the bottom edge. The hover affordance signals 'this preview is real, scroll it, hover it' rather than competing visually with the active textarea above it — operators verifying long structured paste benefit because they can scroll the preview without losing the textarea. Plus the new .is-long-form modifier extends max-height to 360px on the artifact body editor preview only, so 12,000-char bodies scroll cleanly without competing with the surrounding edit shell.
Live markdown preview across task creation form + operator notes + renew notes + comment composer + unified .ld-md-live-preview surface
- Markdown preview on task creation form summary + goal (closes named rev-112 candidate). Closes the named rev-112 next-sprint candidate. Until rev 113 the rev-24 manual task creation form rendered both the 'Why this matters' summary and the optional goal as plain textareas — operators pasting structured task descriptions had no surface to verify how the field would render once the AI cycle and the rev-10 artifact body / rev-110 memory / rev-111 operator-note renderers consumed it. Rev 113 adds an inline brand-color-accented preview directly below each textarea that fires when the rev-110/rev-111/rev-112 shouldRenderAsMarkdown() heuristic returns true (≥80 chars OR markdown-syntax detection). Short one-line summaries stay plain — only structured paste pays the marked.js round-trip. Closes the rendering symmetry on the typed-input axis (rev 112 closed the *saved-template* preview; rev 113 closes the *live-typed* preview).
- Markdown preview in operator note composer + renew note composer. Mirrors the rev-113 task-creation primitive at the rev-14 operator-notes axis and the rev-50/rev-112 renew-note axis. Past operator notes already render as markdown (rev 111); the rev-113 live preview closes the read/write symmetry so operators pasting structured steering see exactly what the AI cycle prompt will see *before* submitting. Same shouldRenderAsMarkdown() gate. Renew note composer follows in lockstep — renew notes route through addTaskOperatorNote() (rev 112) and the past notes render as markdown (rev 111), so a structured renew note will be readable as markdown after submission and the live preview signals that contract before the operator commits.
- Markdown preview in comment composer (preserves @-mention pills). Closes the rendering quartet across all four operator-loaded composers (task creation summary + goal, operator note, renew note, comment). Posted comments already render as markdown via rev 111 with @-mention pills preserved through renderCommentMarkdown(); the rev-113 live preview reuses the same helper so operators see their structured comment + mention pills before posting. Long discussion threads with code samples or numbered checklists no longer surprise the author with a different rendered shape post-submit.
- Unified .ld-md-live-preview CSS — one visual family across four surfaces. Cumulative micro-polish (every rev 22+ has carried one). New .ld-md-live-preview shared CSS class anchors all four typed-input preview surfaces (rev-24 summary, rev-24 goal, rev-14 operator note, rev-50/rev-112 renew note, rev-26 comment) in one consistent visual vocabulary alongside the rev-112 read-only saved-template preview. Brand-color dashed left border + soft gradient + 220ms ld-md-live-fade entrance animation. Max-height 220px with overflow-y so a long structured paste doesn't push the textarea off-screen. New .ld-md-live-preview-label uppercase eyebrow + per-surface contextual copy ('Markdown preview', 'this is what the next cycle will read', '@-mentions render as pills') so the operator knows what they're verifying without reading the surrounding form. Closes the visual-hierarchy gap that would have grown as the rev-by-rev rendering symmetry thread reaches every dashboard text surface.
Optional renew note feeds AI as direction + persistent template markdown preview + workspace-shared template usage counts + cumulative renew-button polish
- Optional renew note + AI direction (closes named rev-111 candidate). Closes the named rev-111 next-sprint candidate at the renew axis. Until rev 112 the rev-50 renew flow was a one-tap 'I am still working on this' affordance — bumps updatedAt + clears the rev-50 archive warning stamp + writes an activity-log line, no operator → AI direction channel. Operators routinely renewed a stale task because they were waiting on a teammate's input or an external dependency, and the desk had no way to know that. Rev 112 adds an optional 600-char inline note. When provided, the renew also routes the note through the rev-14 addTaskOperatorNote() helper so it lands on task.operatorNotes JSONB and the next runAiTaskSession reads it under the OPERATOR NOTES (treat as authoritative) prompt block. Activity log entry is differentiated to 'Operator renewed task with operator direction' so the audit trail captures both lineage and intent. The chevron next to the existing renew button reveals the textarea inline; the one-tap path stays untouched. Mirrors the rev-111 regenerate-as-direction pattern at the renew axis exactly. The four operator → desk channels (rev 11 approve/regenerate, rev 14 operator notes, rev 21/22 priority/due, rev 23 inline body edit, rev 24 manual creation) plus the rev-111 enriched regenerate plus the rev-112 enriched renew now all read as one consistent steering vocabulary — every operator action that touches a task reaches the AI cycle via the same authoritative direction surface.
- Markdown rendering on persistent task template goal preview (closes named rev-111 candidate). Closes the named rev-111 next-sprint candidate at the templates axis. Extends the rev-10 / rev-110 / rev-111 markdown rendering quartet to a fifth axis (the rev-66 persistent task template goal field). When an operator applies a saved template whose goal carries markdown structure (headings, lists, callouts), a brand-color-accented preview block renders above the form fields so the operator sees how the goal will render before queueing — closes the verification gap that until rev 112 only became visible after queueing the task and waiting for the AI cycle to consume it. Reuses the rev-111 shared shouldRenderAsMarkdown() heuristic so short one-line goals stay plain and only structured goals pay the marked.js round-trip. New .ld-task-create-template-preview typography pairs with the rev-110 memory-md + rev-111 operator-note + comment-md treatments so all five markdown surfaces (artifact bodies, memory entries, operator notes, task comments, persistent template goals) read as one visual family.
- Workspace-shared usage count chip on persistent template chips. Every persistent template chip (rev 66) now surfaces a quiet '· N×' workspace-shared usage count when usageCount > 0. Distinct from the rev-65 per-operator localStorage 'recent' badge — the rev-65 badge surfaces *this operator's* most-recent template; the rev-112 chip surfaces *the workspace's* historical popularity. The two reading horizons coexist on every chip without competing visually (recent = brand-color pill, usage = subtle teal counter). Multi-operator teams now see at a glance which saved templates are load-bearing across the workspace vs vestigial — foundation for pruning low-value templates. Pure read of the existing rev-66 usageCount column on the task_template table; no schema change.
- Cumulative polish — split renew button + visual treatment for the new note shell. Cumulative micro-polish (every rev 22+ has carried at least one). The rev-50 amber-tinted renew button is now a two-part cluster: the primary 'Renew' button keeps the one-tap path unchanged + a chevron toggle reveals the rev-112 inline note shell. New .ld-task-renew-shell + .ld-task-renew-textarea + .ld-task-renew-submit + .ld-task-renew-cancel CSS uses the same brand-color amber→brand-color gradient as the rev-111 regenerate primary button so the two operator → AI direction surfaces (regenerate-with-note + renew-with-note) read as siblings with one consistent visual vocabulary. The textarea picks up the rev-111 :focus-visible outline ring so keyboard-only operators land cleanly. Closes the visual-hierarchy gap that would have grown as the rev-50 renew button accumulated more behavior.
Markdown rendering on operator notes + task comments + regenerate notes feed AI as direction + dashboard accessibility polish
- Markdown rendering on operator notes. Closes the named rev-110 next-sprint candidate. Mirrors the rev-110 memory markdown render at the rev-14 operator-notes axis. Notes ≥80 chars or carrying markdown syntax now render with structured headings, lists, code blocks, and links instead of as a flat block. Same gating threshold as rev 110 so short one-line steering stays plain text. New .ld-operator-note-md typography overrides tighten the rev-10 .app-item-md spacing for the note-row context (notes live inside a constrained <ol> already so the default rhythm reads too airy). Closes the rendering symmetry trio across artifacts (rev 10), memory (rev 110), operator notes (rev 111).
- Markdown rendering on task comments — preserves @-mention pills. Extends the rev-10 / rev-110 / rev-111 markdown rendering to the rev-26 task discussion thread. New renderCommentMarkdown() helper in src/lib/markdown.ts runs the standard escape → marked → external-link pass, then post-processes the resulting HTML to wrap @-mention tokens in the same .ld-mention / .ld-mention-desk pill spans the rev-27 client-side renderCommentBody uses for short comments — long structured comments retain mention discoverability without losing markdown formatting. Both the dashboard /api/tasks/{id}/comments and the v1 /api/v1/tasks/{id}/comments endpoints now project a textHtml field per comment when the gating heuristic fires. New .ld-comment-md typography matches the rev-110 memory render rhythm so the four markdown surfaces (artifact bodies, memory entries, operator notes, comments) read as a coherent visual family. Closes the markdown rendering quartet across every operator-loaded surface where structured paste is common.
- Regenerate reviewer note now feeds AI as authoritative direction (not just memory). Closes the named rev-110 next-sprint candidate. Until rev 111 the rev-11 regenerate flow saved an optional reviewer note as a generic preference memory entry — the next AI cycle would pick it up via the rev-1 memory retrieval system, but only by token-overlap relevance scoring (alongside every other memory entry). Rev 111 routes the reviewer note through the rev-14 addTaskOperatorNote() helper so it lands on task.operatorNotes JSONB and the next runAiTaskSession reads it under the OPERATOR NOTES (treat as authoritative) prompt block. The same helper still writes the preference memory + the activity-log entry so audit trail is unchanged. Closes the per-output operator-direction loop alongside the rev-23 inline body editor — operators steering a regenerate now know the desk reads their note as direction, not retrieval.
- Dashboard accessibility polish — focus-visible rings on note + comment textareas. Cumulative micro-polish (every rev 22+ has carried at least one). New :focus-visible 2px brand-color outline on every operator-note + comment textarea so keyboard-only operators land cleanly across every input the operator might touch when steering the desk. Matches the rev-38 dashboard accessibility pattern across all three steering channels (operator notes, comments, regenerate). The rev-111 markdown surfaces (operator notes + comments) inherit the same .app-item-md base typography as the rev-10 artifact body + rev-110 memory entries so all four surfaces read with one consistent compact-markdown vocabulary.
One-tap task duplicate + memory entries render as markdown + outputs panel keyword search + dashboard panel polish
- One-tap task duplicate (dashboard + v1 in lockstep). New duplicateTask() helper + POST /api/tasks/{id}/duplicate route + matching POST /api/v1/tasks/{id}/duplicate v1 mirror in lockstep + TaskDuplicateButton client component. Inherits kind, deliverableType, priority, assignee, tags, goal, context. Clears workLog, operatorNotes, comments, blockedByTaskIds, sourceSignalIds, sourceMemoryIds, dueAt — the original's lifecycle stays with the original. Distinct from the rev-66 persistent task templates (generic recurring shape) — duplicate is the most-direct re-run primitive: the operator's exact title + goal carry over with a ' (copy)' suffix, and the new task lands at the front of the queue. Diversifies away from the 11-rev blog-marketing thread (rev 99-109) back into operator-loaded surface stickiness. Mirrors the rev-37 / rev-50 / rev-95 v1-in-lockstep cadence pattern — every dashboard mutation has a v1 equivalent in the same cycle.
- Memory entries render as markdown. Closes the rendering symmetry with rev-10 artifact bodies. Memory entries promoted from outputs via the rev-26 promote-to-memory flow pull 1200 chars from artifact bodies that often carry markdown — until rev 110 they rendered as plain text so headings, lists, code blocks, and tables read flat. The 80-char + markdown-syntax threshold gates the marked.js round-trip so short one-line preferences (the rev-22 bulk import shape) stay plain text and the marked.js cost is only paid on entries that actually benefit. New .ld-memory-md typography overrides tighten the rev-10 .app-item-md spacing for the shorter memory-entry rhythm so the rev-110 markdown render reads as a sibling of the rev-10 artifact body without inheriting full-artifact air.
- Per-panel inline keyword search on the outputs panel. Mirrors the rev-38 inline activity-log search on the outputs panel — when a workspace accumulates 10+ approvals waiting after a noisy weekend, narrowing by title/summary keyword is the missing third axis alongside the rev-12 kind filter + rev-15 tag filter. Composes with both filters via intersection. Activates at 6+ outputs so quiet workspaces don't see clutter. Closes the per-panel keyword-search symmetry across the dashboard's three load-bearing list surfaces — memory has had it since rev 8, activity log got it in rev 38, outputs closes the trio at rev 110.
- Dashboard panel polish — animated panel-alive dot + duplicate-button affordance. Cumulative micro-polish (every rev 22+ has carried at least one). New 5px brand-color dot in the top-left corner of every dashboard panel that fades in on panel hover (220ms ease) — quiet 'this surface is alive' affordance that draws the eye on interaction without competing with the panel content. Pinned-panel surfaces (rev-22 pinned signals, rev-23 pinned tasks) opt out via #panel-{id}::before {display:none} so the rev-22 / rev-23 pulsing-dot vocabulary stays distinct. New .ld-task-duplicate-btn neutral-palette chip distinguishes the rev-110 duplicate action from the rev-50 amber renew chip and the destructive-red delete chip — duplicate is creation, renew is recovery, delete is destructive; three orthogonal action vocabularies on one card. New .ld-artifact-search input picks up the rev-39 brand-color focus ring so the new outputs-panel keyword surface reads as part of the dashboard's existing input typography.
Per-tag RSS feed + '/' keyboard shortcut on /changelog + sitemap RSS entries + archive surface a11y polish
- Per-tag RSS feed at /blog/tag/[slug]/rss.xml. Closes the named rev-108 next-sprint candidate. Mirrors the rev-104 per-category RSS + rev-105 per-author RSS shapes at the third axis so feed aggregators (Feedly, Inoreader, founder / AI-tooling release-roundup newsletters) can subscribe to one tag vocabulary with the same client they use for the workspace-wide blog feed. Closes the four-axis public-cadence subscription cluster on the protocol-bound side: workspace blog (rev 97) / per-category (rev 105) / per-author (rev 105) / per-tag (rev 109) / changelog (rev 37). Readers tracking only one tag (e.g. #MCP) now have a passive distribution channel scoped exactly to their interest without the brand-voice noise from off-theme posts. Pure read of the existing getPostsByTagSlug() helper so the page and feed never drift. Surfaced on every per-tag archive hero as an inline 'RSS' chip plus alternates metadata so feed-reader auto-discovery just works.
- '/' keyboard shortcut on /changelog keyword search. Mirrors the rev-103 /blog '/' keyboard shortcut at the changelog axis. The rev-101 changelog keyword search was visible but mouse-only — keyboard-first power-users who already use '/' on the dashboard (rev 17) and on /blog (rev 103) had no parity on the third reading surface. Rev 109 closes that gap. Press '/' from anywhere on /changelog to focus the search input; Esc clears the query + blurs. The visible <kbd>/</kbd> hint inside the search input fades on focus for discoverability, matching the rev-103 visual treatment exactly so all three keyword surfaces speak one consistent power-user vocabulary.
- Sitemap entries for per-tag RSS feeds. Extends the rev-108 per-tag HTML archive sitemap entries with their RSS-feed companions, mirroring the rev-105 per-category + per-author sitemap shape exactly. Same lower priority (0.4) as the rev-105 per-category + per-author RSS feeds since the HTML archive is the human-readable destination and the feed is the subscription endpoint. Crawler-side discoverability for feed aggregators reading the sitemap. Closes the indexability gap on the rev-109 RSS feed in the same cycle the feed itself ships — the cadence pattern from rev 37 onwards (ship the primitive + the sitemap entry in lockstep) holds unbroken.
- Archive surface accessibility polish. Cumulative micro-polish (every rev 22+ has carried at least one). Adds :focus-visible outline rings to every archive cross-navigation chip across the per-author + per-category + per-tag archive pages so keyboard-only readers land cleanly on every chip with one consistent visual landing pad. The .blog-tag-other-chip already had hover lift via rev 108; rev 109 closes the accessibility gap on the matching per-author + per-category chips so all three archive surfaces speak one consistent accessibility vocabulary. Closes a small but consistent affordance gap that's been growing since the per-author + per-category archive pages shipped (rev 105 + rev 107) without the same accessibility treatment the rev-108 per-tag chips inherited from the start.
Per-tag HTML archive page + 'More in this category' callout on post detail + archive summary stats + v1 tag endpoints
- Per-tag HTML archive at /blog/tag/[slug]. Closes the third axis on the public discovery surface alongside rev-105 per-author and rev-107 per-category archive pages. The rev-103 tag filter chips on /blog let readers narrow the index by tag inline, but had no dedicated drillable page. Tags collapse by slug (so 'MCP' and 'mcp' resolve to one page); the most-recent post's display name wins. Each per-tag page surfaces a hash-glyph hero with archive summary stats (posts + total words + estimated reading time + latest date) and a 'browse other tags' chip cluster at the bottom for cross-navigation. SSG'd via generateStaticParams. Sitemap updated in lockstep so crawlers see every per-tag archive page alongside the existing per-author + per-category pages — the public discovery surface is now uniformly drillable on every dimension.
- v1 endpoints /api/v1/blog/tags + /api/v1/blog/by-tag/{slug}. Mirrors the rev-105 per-author endpoints (/blog/authors + /blog/by-author/{slug}) at the tag axis. Closes the v1 blog cluster's seventh axis after listing (rev 102) + detail (rev 103) + categories (rev 104) + authors (rev 105) + related (rev 106) + neighbours (rev 107). MCP hosts rendering 'posts tagged MCP' or AI tooling roundup newsletters writing per-topic weekly digests now get the answer in one bearer-less GET instead of fetching the full /api/v1/blog and filtering client-side. The by-tag endpoint includes a `summary` block with totalWords + estimatedMinutes so callers don't have to re-aggregate per-post wordCount. No auth — same model as the rev-102/103/104/105/106/107 endpoints. Cache-control public, max-age=300, s-maxage=1800.
- 'More in this category' callout on /blog/[slug]. Closes the named rev-107 next-sprint candidate at the post-detail axis. Until rev 108 the rev-107 per-category HTML archive closed the listing-level cross-category surface, but readers landing on a single post had no in-context cluster of category siblings — only the rev-106 related-posts grid (which ranks by tag-overlap content similarity, not category). Rev 108 adds the missing axis: every post page now surfaces a 2-card grid of up to 4 same-category siblings with date + read-time + title, plus a 'browse all N' chip linking to the rev-107 per-category archive. Distinct from rev-106 related (tag-similarity) — surfaces same-vocabulary work at the thematic-cluster axis. Pairs naturally with the rev-107 chronological prev/next nav (read newer/older in the timeline) for the full content-similarity (rev 106) + thematic-cluster (rev 108) + chronological (rev 107) trio of post-detail discovery affordances. Plus the post tags themselves are now clickable links to the rev-108 per-tag archive — until rev 108 they were inert spans.
- Archive summary stats on per-category, per-author, per-tag pages. New `summarizeArchive()` helper aggregates total post count + total words + estimated reading time across an archive's full post set. Surfaces inline in the hero of every archive page (per-category, per-author, per-tag) so readers see 'this is a 45-minute read across 8 posts' before scrolling the card grid. The estimated reading time is computed from the sum of word counts (not the sum of per-post readTime strings, which round per-post) so a small archive of short posts and a large archive of long posts show different totals. Mirrored on the v1 by-tag endpoint as a typed `summary` block. Cumulative micro-polish on the public discovery surface — every archive page now reads as a load-bearing surface rather than just a list.
Markdown-driven author profile overrides + chronological prev/next nav + per-category HTML archive page + v1 neighbours endpoint
- Markdown-driven author profile overrides. Closes the named rev-106 next-sprint candidate. Until rev 107 author profiles lived only in data/blog-authors.json. Rev 107 lets post markdown frontmatter optionally carry authorBio / authorTagline / authorAvatar / authorUrl — the most-recent post's frontmatter override wins, then the JSON registry, then null. A contributor's first PR can now include both their post and their profile in one place without touching a separate file. JSON registry stays the canonical home for fields not expressible in markdown (links[]) — the override surface is intentionally narrow but covers 95% of the contributor-onboarding case.
- Chronological prev/next navigation on /blog/[slug]. Closes the chronological reading axis on the rev-102+ blog cluster. The rev-106 related-posts (tag overlap) primitive answered 'you might also like' by content similarity; rev 107 adds the timeline axis ('newer post' / 'older post') for binge-readers walking the archive. Either side renders as a quiet 'first post' / 'latest post' card when the source is at the head or tail of the timeline. Pure derived state — no schema, no migration. Pairs naturally with the rev-106 related-posts grid that already renders above for the full content-similarity + timeline-axis pair.
- Per-category HTML archive page at /blog/category/[key]. Closes the gap the rev-106 sitemap explicitly noted ('Next doesn\'t currently have a /blog/category/[key] HTML route. List only the RSS endpoints so feed-reader auto-discovery works without shipping a 404 for the missing HTML axis'). Mirrors the rev-105 per-author archive page pattern at the category axis with a coloured hero that reflects the category's accent + a 'browse other categories' chip row at the bottom for cross-navigation. Sitemap updated in lockstep so crawlers see every per-category archive page (high priority) alongside the existing per-category RSS endpoints (lower priority — the HTML archive is the human-readable destination).
- GET /api/v1/blog/{slug}/neighbors. Closes the v1 parity gap on the rev-107 dashboard prev/next navigation primitive in the same cycle the dashboard primitive ships. Distinct from the rev-106 /api/v1/blog/related/{slug} endpoint (which ranks by tag-overlap content similarity) — neighbours walks the publication timeline. MCP hosts rendering 'binge-read the timeline' widgets, AI tooling newsletters generating per-post chronological sidebars, and search clients building 'next post' affordances now have a one-call answer for the chronological axis. Closes the sixth axis on the v1 blog cluster (listing + detail + categories + authors + related + neighbours). No auth — same model as the rev-102/103/104/105/106 endpoints.
Per-author profiles (bio + avatar) + table of contents on long posts + sitemap author/category pages + v1 related-posts endpoint
- Per-author profiles (bio + avatar URL via JSON registry). Closes the named rev-105 next-sprint candidate. New data/blog-authors.json profile registry keyed by slugifyAuthor() so adding a real human byline only needs (a) the post frontmatter and (b) a new entry in the registry — no schema migration. Surfaces on the rev-105 per-author archive hero (avatar image + tagline + bio + outbound links) and the post-detail byline (avatar image when registered, gracefully degrading to the rev-105 initials treatment otherwise). Mirrored on /api/v1/blog/authors and /api/v1/blog/by-author/{slug} via a new typed `profile` block (null when no profile registered) so MCP hosts rendering 'browse by byline' surface bio + avatar without a follow-up call. Pairs with rev-105 byline drill-down as the depth-axis on the byline surface.
- Table of contents sidebar on /blog/[slug]. Surfaces the rev-102 per-heading anchors as a navigable index. Until rev 106 long-form posts (the rev-10 governance-first essay + rev-42 MCP announcement post both 2,000+ words across 8+ sections) had no at-a-glance overview of the content — readers either scrolled the whole post or relied on the rev-104 reading-progress bar to gauge position. The TOC closes that gap: every <h2> and <h3> surfaces as a stickied chip-list on the right edge of the article, with active-section highlighting via IntersectionObserver so the reader sees where they are at a glance. Reads headings the SAME way the rev-102 BlogHeadingAnchors writes them — same slugify rules + same dup-suffix logic — so the TOC and the per-heading chip resolve to the same anchor id every time. Hidden below 1100px (the reading-progress bar covers narrower viewports) and below 4 headings (more visual noise than navigation value on short posts).
- Sitemap.xml extends to per-author + per-category pages. Until rev 106 only static pages + per-post detail pages were listed in /sitemap.xml — crawlers (and AI tooling discovery systems) had no signal that the rev-104 /blog/category/[key]/rss.xml and rev-105 /blog/author/[slug] surfaces existed at all. Rev 106 closes that gap by enumerating every author archive page + every per-category RSS endpoint. Per-author lastModified anchors to the author's latestDate so search engines see the freshness signal. SEO win that pairs with the rev-103 JSON-LD Article markup (machine-readable per-post) + rev-105 byline drill-down (machine-readable per-author) for the full crawler-discoverability story across all three axes.
- GET /api/v1/blog/related/{slug}?limit=5. Closes the fifth axis on the v1 blog cluster (listing + detail + categories + authors + related). Until rev 106 the rev-103 detail endpoint inlined related posts as a side payload but there was no standalone axis reachable by slug — MCP hosts rendering 'you might also like' widgets, AI tooling roundup newsletters generating per-post sidebars, and search clients building 'more like this' affordances had to fetch the full detail body or re-implement the rev-102 tag-overlap scoring client-side. Rev 106 makes it a one-call answer that also returns the source post identity + author profile avatar + a per-result `sharedTagCount` so callers rendering 'shared 3 tags' hints don't have to recompute. Returns 404 when the source slug is missing. Cache-control public, max-age=300, s-maxage=1800.
Per-author drill-down + per-category RSS feeds + v1 byline endpoints
- Per-author archive at /blog/author/[slug]. Closes the named rev-104 next-sprint candidate. Every blog post's byline is now a clickable chip that lands on a stable per-author archive page listing every post by that author, newest-first. Mirrors the rev-103 tag drill-down pattern at the byline axis. The primitive becomes load-bearing the moment a real human byline lands; even today it gives readers a way to slice the archive by voice without re-typing in the rev-102 search input. SSG'd via generateStaticParams so the page is static + cacheable like every other blog page.
- Per-category + per-author RSS feeds. Closes the per-axis subscription gap on the brand-voice surface. The rev-97 workspace-wide blog feed answers 'subscribe to all the brand voice'; rev 105 adds per-category (/blog/category/[key]/rss.xml) and per-author (/blog/author/[slug]/rss.xml) feeds so readers tracking only governance-first posts (or only one byline) can subscribe to a single slice. Pairs with the rev-37 /changelog/rss.xml + rev-97 /blog/rss.xml as the four-axis public-cadence subscription cluster (workspace blog / per-author / per-category / changelog). Per-category subscribe chips now sit inline in every CategorySection head and per-category sidebar row.
- GET /api/v1/blog/authors + GET /api/v1/blog/by-author/{slug}. Closes the fourth axis on the v1 blog cluster. Rev 102 added the listing endpoint; rev 103 added the per-post detail; rev 104 added the category taxonomy; rev 105 closes the byline axis. The cluster is now four axes deep on the protocol-bound side, matching the depth of the rev-100/101 changelog cluster (listing + sinceRev) and the rev-98/99 roadmap cluster (planned + most-requested + trending). MCP hosts rendering 'browse by byline' or AI tooling newsletters writing per-byline weekly roundups now get a typed contract instead of scraping the SSR'd author pages. OpenAPI 3.1 spec typed in lockstep (cadence pattern from rev 78 onward holds unbroken into rev 105).
- Sidebar Authors + Browse-by-category cards + clickable byline chip. New AuthorsCard sidebar lists every byline with post count + click-to-archive link. New CategoriesCard sidebar pairs every category with its rev-105 per-category RSS feed in one click. Featured card adds a 'by [Author]' meta line. Post detail page wraps the existing avatar + name + date + readTime + word-count meta block in a single Link to the per-author archive — clicking anywhere on the byline cluster lands on the archive. New rev-105 visual polish: brand-color hover glow on the byline link, amber-tint per-category RSS chip in section heads, focus-visible rings + tactile hover lift on every new chip, stacked-on-mobile responsive treatment of the per-author hero.
Reading progress bar + scroll-to-top FAB + print-friendly /blog + GET /api/v1/blog/categories
- Reading progress bar on /blog/[slug]. Thin top-of-viewport bar that fills brand-color → amber as the reader scrolls through `.blog-prose`. Anchored to the article body (not the entire page) so the bar reads as the reader's progress through the essay, not their progress through the rev-102 related-posts grid + rev-103 CTA card. Hidden until the reader has actually scrolled into the article so the header reads clean on first paint; clamps to 100% at the bottom so the bar reads as a completion signal. Quality polish that signals 'this is a real publication' without competing with the rev-103 per-heading permalinks or the rev-102 per-post share affordances. Distinct from the rev-103 read-time chip in the author block (which scrolls offscreen the moment the reader starts reading) — the progress bar stays load-bearing throughout the entire read.
- Scroll-to-top FAB on /blog/[slug]. Mirrors the rev-38 dashboard `ScrollToTop` primitive at the blog axis. The blog has accumulated long-form posts (the rev-10 governance-first essay + rev-42 MCP announcement post both run 2,000+ words across 8+ sections; the rev-92 procurement-evidence posts go even longer) and operators finishing a long read had no quick path back to the rev-102 related-posts grid or the rev-103 CTA card without manual scrolling. Same 800px threshold as the dashboard variant so the visual vocabulary reads as siblings across the two surfaces. Distinct visual treatment (brand-amber gradient `#cf6c3a → #b85723`, `ArrowUp` icon, soft shadow) so the blog FAB doesn't get confused with the dashboard FAB when a reader visits both surfaces in the same session.
- Print-friendly /blog/[slug] takeaway stylesheet. Closes the procurement-takeaway-friendly gap on long-form blog posts. Mirrors the rev-92 share-page print rules + rev-93 work-log print + rev-94 timeline print at the /blog axis so all four reading surfaces print with one consistent vocabulary. Hides every navigation + interaction surface (top nav, share chips, scroll-to-top, reading progress bar, related-posts grid, bottom CTA, back-to-blog link, per-heading anchor chips) so the print reads as the essay alone. Title + author block + prose get clean print typography (26pt / 11pt / 1.55 line-height); external links print their full URL inline via `a[href^='http']::after` so the offline reader can trace every reference. Operators sharing the rev-13 ISO 42001 governance docs essay or the rev-10 governance-first category post with a procurement reviewer now get a clean PDF takeaway. The rev-92 print stylesheet's reach extends to its fourth surface — load-bearing for SOC 2 / ISO 42001 procurement evidence packages.
- GET /api/v1/blog/categories — third axis on the v1 blog cluster. Public read of the blog's category taxonomy on the v1 surface. Rev 102 added the listing endpoint, rev 103 added the per-post detail; rev 104 closes the third axis with a category list including the canonical key, human-readable name, accent colour, description, and post count for every category that has at least one post. MCP hosts rendering 'Loop Desk's blog by category' in their own UI now match the public site exactly without scraping the SSR'd category sections. No auth — same model as the rev-102/103 endpoints + /badge.svg + /roadmap-* + /changelog. Cache-control public, max-age=300, s-maxage=1800. The v1 blog cluster (listing + detail + categories) is now functionally complete on the protocol-bound side, matching the depth of the rev-100/101 changelog cluster (listing + sinceRev) and the rev-98/99 roadmap cluster (planned + most-requested + trending). The MCP server (Q3 #1) gains one more pre-typed surface with nothing left to design on the blog axis.
Blog tag filter chips + JSON-LD Article markup + GET /api/v1/blog/{slug} + '/' keyboard shortcut
- Tag filter chip row on /blog (URL-shareable scope). Closes the rev-39 cross-entity tag drill-down at the public blog axis. Top 14 tags surface as clickable chips above the post list; tap a chip to scope the visible cards to that tag without re-typing in the rev-102 search input. Active tag reflects into the URL hash (`/blog#tag=memory`) so an owner sharing a scoped view lands the recipient on the same filter — mirrors the rev-100 roadmap status filter pattern at the /blog axis. Persists per-browser in localStorage so a returning visitor keeps their preferred scope; URL hash wins over localStorage on first load. One-tap '🔗 Copy link' chip appears alongside the active tag for one-tap share — mirrors the rev-101 roadmap filter share chip + rev-101 changelog permalink chip. Empty-state copy when the filter has no matches in any visible category. Tags went live across artifacts (rev 15), memory (rev 21), tasks (rev 24), task descriptive insight panel (rev 28), focus tags (rev 29), in-app drill-down (rev 39); rev 103 closes the same gap on the public blog surface.
- JSON-LD Article structured data + richer OpenGraph + Twitter card. Pure SEO win that pairs with the rev-102 v1 blog endpoint as the second machine-readable surface on the blog axis: v1 returns brand voice for code generators, JSON-LD returns brand voice for SEO crawlers + Google Knowledge Graph + AI tooling roundup newsletters that scrape schema.org markup. Every blog post now emits an inline <script type='application/ld+json'> block with @type=Article, headline, description, datePublished, dateModified, author (Organization), publisher (with logo), mainEntityOfPage, keywords (from tags), wordCount, inLanguage. Plus richer OpenGraph metadata (canonical URL, siteName, twitter card=summary_large_image) so share links to blog posts in Slack/X/LinkedIn render with proper preview cards. Closes the structured-data gap that's been open since the blog launched.
- GET /api/v1/blog/{slug} — single-post detail on the v1 surface. Closes the v1 parity gap on the rev-102 blog cluster. Rev 102 added the listing endpoint at /api/v1/blog; rev 103 adds the per-post detail. Returns the full HTML body + tags + read-time + word-count + canonical URL plus 3 related posts via tag overlap so MCP hosts + AI tooling roundup newsletters can render 'you might also like' without a follow-up call per slug. No auth — same model as /api/v1/blog (the listing endpoint), /badge.svg, /roadmap-*, /changelog. Cache-control public, max-age=300, s-maxage=1800 because content changes only when we publish. OpenAPI 3.1 spec typed in lockstep with the dashboard primitives (cadence pattern from rev 78 onward holds unbroken into rev 103). Pairs with /api/v1/blog (listing) as the two-axis blog read surface — same depth pattern as the rev-100/101 changelog axis (listing + sinceRev incremental polling).
- '/' keyboard shortcut on /blog search + reading-time chip on every card. Mirrors the rev-17 dashboard `WorkspaceSearch` keyboard pattern at the /blog index. Press / from anywhere on the page to focus the rev-102 search input; Esc clears + blurs. Ignored when the user is already typing in another input. Visible '/' kbd hint inside the search box (fades out on focus) for discoverability. Plus every blog card on the index page (FeaturedCard + CategorySection cards) now shows a small clock-icon read-time chip alongside the date so readers can triage by length without opening the post. Same Clock icon vocabulary used elsewhere in the brand. Cumulative micro-polish (every rev 22+ has carried at least one) — but rev 103's polish is load-bearing because the rev-102 search input was the most-used affordance on /blog and the keyboard-first power-users (the same ones that use rev-17 / on the dashboard, rev-23 ⌘K command palette) now have parity on the public surface.
Per-blog-post share affordance + per-heading anchor permalinks + blog index keyword search + GET /api/v1/blog
- Per-blog-post share chip cluster (Copy link / Email / RSS). Closes the named rev-101 next-sprint candidate ('/blog per-post permalink share affordance'). Rev 101 closed the share→deliver loop on /roadmap (chip) + /changelog (per-rev permalink); rev 102 ships the same primitive at the /blog/[slug] axis. Every blog post now gets three chips in the author block: '🔗 Copy link' (one-tap clipboard write of the canonical post URL with execCommand fallback), '✉ Email' (mailto: wrapper that pre-fills subject + body so an operator forwarding to a procurement reviewer doesn't have to context-switch to Gmail), and '⭷ RSS' (subscribe link to /blog/rss.xml). Mirrors the rev-95 share-page email wrapper + rev-101 changelog permalink chip + rev-101 roadmap filter share chip vocabulary. Closes the public marketing share-affordance trio across the three reading surfaces.
- Per-heading anchor permalinks within blog posts. Every <h2> and <h3> inside the rendered prose now gets a slug-derived `id` and a '🔗' chip on hover that copies a deep-link like `loopdesk.space/blog/<slug>#<heading-slug>` to the clipboard. Pure client-side overlay on top of the SSR'd HTML the marked parser produces — runs once on mount, walks the rendered prose, and injects id + chip + click handler into each heading. A reader landing with `#some-section` in the URL gets a smooth-scroll + 2.4s flash highlight on the matching heading via the post-level share component's hash listener. Mirrors the rev-101 changelog per-rev permalink pattern at the per-heading axis on the blog. Crawlers and feed readers see every heading unconditionally.
- Blog index keyword search. Mirrors the rev-101 changelog keyword search at the /blog index axis. New `<input type="search">` above the post list filters by title + excerpt + tags, all client-side. Counter shows 'N of M' when a query is active; empty state with one-click 'Clear search' affordance. The blog has accumulated 27+ posts since the pivot to founder/operator topics in 2026-04-28; until rev 102 the page was scroll-only across categorised sections. Procurement reviewers + journalists writing tooling roundups + AI-tooling release-roundup newsletters can now retrieve posts by keyword in one keystroke. The same retrieval instinct that the rev-38 dashboard activity log + rev-17 workspace search ship now reaches the public blog surface.
- GET /api/v1/blog + OpenAPI typed schema. Public read of blog posts on the v1 surface. Closes the four-axis public marketing surface on the protocol-bound side: items (rev 98 — planned), votes (rev 98 — most-requested), changelog (rev 100 — shipped), blog (rev 102 — brand voice). Optional filters: `limit` (1-50, default 20), `tag` (case-insensitive substring match against any tag on the post), `sinceDate` (ISO date — return only posts published after). No auth — same model as /badge.svg + /roadmap-* + /changelog. Aggressive cache headers because content changes only when we publish. OpenAPI 3.1 spec typed in lockstep with the dashboard primitives (cadence pattern from rev 78 onward holds unbroken into rev 102). MCP hosts + procurement teams + AI tooling roundup newsletters reading via JSON now have parity with the existing HTML + RSS surfaces.
Roadmap 'Copy link' chip + /changelog per-rev permalinks + /changelog keyword search + /api/v1/changelog sinceRev
- Roadmap 'Copy link' chip on the active filter. Closes the named rev-100 next-sprint candidate. Rev 100 ships the URL-hash share mechanism (#filter=next/soon/considering); rev 101 closes the share→deliver loop with a one-tap '🔗 Copy link' chip that appears at the end of the chip row whenever a non-'all' filter is active. One tap → clipboard → brief '✓ Copied' affordance for 1.6s. No more copying-from-the-address-bar tax for owners sharing scoped views to stakeholders. Mirrors the rev-95 mailto wrapper at the roadmap-filter axis — every public share surface now has a one-tap deliver affordance.
- /changelog per-rev permalinks + 'Copy link' chips. Every rev card now carries `id="rev-N"` so a shared URL (`loopdesk.space/changelog#rev-100`) lands the recipient on that rev with a smooth-scroll + 2.4s flash highlight. A '🔗 Copy link' chip in each rev's meta column gives a one-tap deliver path; hashchange listener also re-fires the highlight on back/forward navigation. Mirrors the rev-31 comment-permalink pattern at the changelog axis. Procurement teams citing a specific rev can now share the deep link instead of pasting the rev label and asking the recipient to scroll.
- /changelog keyword search across 100+ revs. The changelog has accumulated 100 revs since the rev-14 page launch and counting; readers looking for a specific topic ('cost spike', 'MCP', 'Slack push', 'source rename') had to scroll. New `<input type="search">` above the rev list filters by rev label + title + highlight labels + descriptions, all client-side. Counter shows 'N of M' when a query is active; empty state with one-click 'Clear search' affordance. Mirrors the rev-38 dashboard activity log keyword surface at the public marketing surface — the same retrieval instinct works on both surfaces.
- GET /api/v1/changelog?sinceRev=N + OpenAPI typed schema. Closes the rev-100 deep-link share thread on the protocol-bound axis. New optional `sinceRev` query param returns only releases whose rev number is strictly greater than the supplied value. MCP hosts polling the cadence can ask 'what's new since rev 95?' and get only rev 96+ back without parsing every prior rev or doing a follow-up filter. Composes with `limit`: sinceRev filters first, then limit bounds from the most-recent end. Response shape gains `filtered` + `sinceRev` so callers can verify the filter applied. OpenAPI 3.1 spec typed in lockstep with the dashboard primitives (cadence pattern from rev 78 onward holds unbroken into rev 101).
Roadmap URL-shareable filter + Recently shipped + v1 changelog endpoint + milestone polish
- Roadmap status filter now URL-shareable (#filter=next/soon/considering). Closes the named rev-99 next-sprint candidate. Rev 99 ships per-browser localStorage persistence on the rev-99 status filter chips; rev 100 also reflects the active filter into the URL hash. An owner sharing `loopdesk.space/roadmap#filter=next` now lands the recipient on the same scoped view regardless of their saved-per-browser preference. The hash wins over localStorage on first load (a shared link is the strongest possible signal of intent), and a hashchange listener keeps the chip row in sync with back/forward navigation. `history.replaceState` keeps the URL clean so toggling filters doesn't spam the back button. The lowest-friction expression of 'this scope is meant to be shared.'
- 'Recently shipped' panel on /roadmap. New brand-purple-accented panel reads the rev-37 changelog releases module and surfaces the 3 most-recently shipped revisions directly above the rev-99 status filter. Three reading horizons in one page: past (recently shipped), present (rev-97 most-requested + rev-99 trending now), future (the phase tree). Procurement teams + early adopters reading the public surface get the full 'where we've been ↔ what's most-asked-for ↔ what we're considering' picture without leaving the page. The brand-purple palette distinguishes it from the rev-97 brand-green most-requested strip and the rev-99 amber trending strip so visitors read three distinct colour vocabularies for three distinct reading horizons.
- GET /api/v1/changelog endpoint. Public read of the rev-37 changelog content on the v1 surface. Pairs with /api/v1/roadmap-items (rev 98) and /api/v1/roadmap-votes (rev 98) as the third axis of the public marketing surface on the protocol-bound side: items return what's planned, votes return what's most-requested, changelog returns what shipped. MCP hosts + procurement teams checking what shipped recently can do so without scraping. No auth (public marketing surface — same model as /api/v1/badge.svg). Optional `limit` query param (1-100, default 20). Cache-control public, max-age=300, s-maxage=1800. The /changelog page, /changelog/rss.xml feed, and the new /api/v1/changelog endpoint all read from the rev-37 src/lib/changelog-releases.ts module so the four surfaces never drift.
- OpenAPI typed schema + milestone visual accent on the changelog. The rev-100 /api/v1/changelog endpoint is typed in /api/v1/openapi.json in the same cycle the dashboard primitive ships (cadence pattern from rev 78 onward). Plus subtle visual polish: milestone revs (every 100) on the /changelog page now wear a brand-purple left-border accent stripe + 'milestone' pill so the cadence's own anniversaries read as load-bearing rather than blending into the rev-by-rev stream. Cumulative micro-polish — every rev 22+ has carried at least one. Closes the rev-100 thread of 'every public surface reaches every other public surface' across pages (changelog + roadmap), feeds (RSS), and protocol-bound JSON (v1).
Roadmap hot-strip sparkline + Trending now (7d momentum) section + status filter chips + v1 mostTrending
- 14-day vote-trend sparkline on the 'Most requested' hot-strip rows. Closes the named rev-98 next-sprint candidate. Rev 98 shipped the per-item 14-day sparkline on the main roadmap list; rev 97's most-requested hot strip showed cumulative count only. Rev 99 surfaces the same trajectory shape inline on every top-3 hot-strip row so visitors see at one glance which leaders are picking up momentum vs. which are static at their cumulative count. Compact 44×14 variant of the rev-98 sparkline keeps the strip scannable. Hidden on viewports below 540px where the row already wraps.
- 'Trending now · last 7 days' second hot strip — momentum-based ranking. The rev-97 'Most requested' strip ranks by cumulative count which means an item that's just started picking up steam can't surface above an established leader until weeks of votes accumulate. Rev 99 adds a second hot strip directly below it that ranks by trailing-7-day momentum (sum of votes in the last week). Tiebreaker is cumulative voteCount so a sustained week beats a one-day spike. Excludes items already in the top-3 cumulative ranking so the two strips never duplicate — the goal is to surface what's about to start showing in the cumulative ranking, not to repeat it. Amber accent on the strip distinguishes the momentum reading horizon from the brand-green cumulative one.
- Status filter chips (All / Next up / Coming soon / On the radar). The roadmap page has accumulated 13 items across 3 phases over rev 38/97/98. A visitor who only cares about 'what's shipping next' had no way to scope the view without scrolling past everything. Rev 99 adds a chip row above the phase list with per-status counts; tap a chip to scope, tap All to clear. Filter is pure CSS via a data-attribute on <body> — the SSR'd phase tree never re-renders, and items just hide via the matching `[data-roadmap-status]` selectors. Empty phase containers hide via :has() so a filtered phase doesn't show a head with no items. Persists per-browser in localStorage so a returning visitor keeps their preferred scope.
- v1 endpoint extended with `momentum7d` + `mostTrending` + OpenAPI typed schema. GET /api/v1/roadmap-votes now returns `momentum7d` per item (sum of trailing-7-day votes) plus a `mostTrending` ranking that mirrors the new /roadmap second hot strip. MCP hosts and code generators reading the public marketing surface can render the same two-axis view (cumulative + momentum) in their own UIs without scraping. OpenAPI 3.1 typed schema covers the new fields so client SDK generators see the discriminated `mostRequested` vs `mostTrending` shape exactly. Cache headers unchanged (60s public / 300s s-maxage).
Public roadmap on the v1 surface — votes + items + 14-day trend + OpenAPI typed schema
- GET /api/v1/roadmap-votes — closes the named rev-97 next-sprint candidate. Rev 97 shipped the public /roadmap upvote primitive. Until rev 98 the vote counts were only readable through the public dashboard endpoint or by scraping the page. Rev 98 mirrors the counts on the v1 surface so MCP hosts + code generators reading the public roadmap can see what the community is asking for next without scraping HTML. Each item carries the cumulative vote count + a trailing 14-day per-day trend bucket, plus a top-5 most-requested ranking. Public endpoint (no auth header) — same model as /api/v1/badge.svg. Cache-control public, max-age=60, s-maxage=300.
- GET /api/v1/roadmap-items — public roadmap content as JSON. Pairs with /api/v1/roadmap-votes as the machine-readable companion to the rev-38 /roadmap page: votes return counts, items return the full content (title, description, status, key, phase, cadence). MCP hosts can render 'what is Loop Desk shipping next?' in their own UIs with a single bearer-less GET. Cache-control public, max-age=300, s-maxage=1800. Reuses the rev-97 `src/lib/roadmap-items.ts` shared module so the page, vote endpoint, and v1 surface never drift.
- Per-item 14-day vote-trend sparkline on /roadmap. Cumulative dashboard polish — every rev 22+ has carried at least one. Each roadmap row now renders a small 14-day trend sparkline between the status pill and the upvote button. Today's bar wears a brand-color highlight; older days fade neutral so the trajectory shape reads cleanly. Hidden when every bucket is zero so quiet items don't see clutter. Pure derived state from the rev-97 roadmap_vote rows — no schema change. Pairs with the rev-98 v1 endpoint that exposes the same trend shape so the public dashboard surface and the protocol-bound surface read the same trajectory.
- OpenAPI 3.1 typed schemas for rev-97 + rev-98 roadmap endpoints. The two new endpoints are documented in /api/v1/openapi.json with full request/response schemas: /roadmap-votes returns counts + 14-day trend + most-requested ranking, /roadmap-items returns the full phases/items tree. Header changelog block summarises the rev-98 work. MCP-host code generators reading the spec can typecheck their tool calls and auto-generate client SDKs against the public marketing surface without bridge code.
Public roadmap upvoting + /blog/rss.xml feed + dashboard source health + latest-release pill
- Public roadmap upvoting + most-requested hot strip — closes the named rev-95/96 next-sprint candidate. Rev 38 shipped the public /roadmap page; rev 95 + rev 96 named upvoting as the natural next-sprint candidate citing the operator-feedback loop on the public surface. Rev 97 closes that. Anonymous voting via a per-browser fingerprint cookie minted by `POST /api/roadmap/vote`; one vote per item per fingerprint enforced by a composite primary key on the new `roadmap_vote` table; localStorage mirrors the server state so the UI shows 'voted' pressed-style without trusting the cookie alone. New 'Most requested' hot strip pinned to the top of /roadmap surfaces the top 3 most-voted items with brand-color treatment so visitors see what the community is asking for at a glance — the load-bearing prioritisation signal procurement teams + early adopters use to push the roadmap toward what they actually need. Roadmap content moved into `src/lib/roadmap-items.ts` as a stable per-item key so items can move between phases (next/soon/considering) or get reworded without breaking historical vote counts.
- /blog/rss.xml — public RSS 2.0 feed for the blog. Mirrors the rev-37 /changelog/rss.xml shape so feed aggregators (Feedly, Inoreader, founder / AI-tooling newsletters that read RSS) can subscribe to both surfaces with the same client. Pure read of the existing `getAllPosts()` helper so the page and feed never drift. Marketing surface — visible cadence on /changelog answers 'what shipped' and the blog answers 'what we're thinking about'; the two RSS feeds together turn the brand voice into a passive distribution channel without the operator's permission. Subscribe link added to the blog hero stats row + `<link rel='alternate' type='application/rss+xml'>` in page metadata so feed readers auto-discover it.
- Source health alert pill in dashboard status bar. The dense per-row pills inside the rev-1 sources panel already surface health state, but operators with 10+ sources had to scroll to spot any feed in error or paused state. The new pill collapses that to a glance — eye lands on it in the status bar (alongside rev-12 heartbeat, rev-39 density, rev-77 personal inbox, rev-85 reset chip), one tap scrolls to #panel-sources. Renders only when at least one source is in error or paused state — quiet days never see it. Mirrors the rev-77 personal inbox pattern at the source axis. The status-bar instrument cluster now reads as eight instruments deep — heartbeat (rev 12) + desk health (rev 13) + cycle performance (rev 14) + read-only pill (rev 16) + density toggle (rev 39) + what's-new badge (rev 76) + personal inbox (rev 77) + reset chip (rev 85) + source health (rev 97).
- Latest changelog release pill in dashboard status bar. Operators are heads-down on the desk and routinely miss the rev-by-rev cadence even though the changelog has shipped a new release every few days for 23+ revs. The new pill surfaces the most recent rev (e.g. 'rev 97 · new') in the dashboard status bar with a brand-color treatment when the operator hasn't seen this rev yet, fading to a quiet grey once dismissed. localStorage tracks 'last seen rev' per browser. Pairs with the rev-76 what's-new badge: the badge answers 'what happened in MY workspace since I was here last', the pill answers 'what shipped on the product since I was here last'. One workspace scope, one product scope. Sources from the existing rev-37 `changelog-releases` module so the page, feed, and pill never drift.
Bulk source rename + sources CSV export + workspace search reaches sources + dashboard polish
- Bulk source rename / find-and-replace — closes the named rev-95 next-sprint candidate. Rev 95 shipped single-source rename; a workspace with 20+ rss.app/fetchrss bridge feeds wanting a brand rebrand ('Acme → Globex') still required N inline-rename clicks. Rev 96 collapses that to one operation. New `bulkRenameSources()` helper + `POST /api/sources/bulk-rename` route + matching `POST /api/v1/sources/bulk-rename` v1 mirror in lockstep. Operates either workspace-wide (omit sourceIds) or scoped to an explicit subset (cap 50, mirroring rev-36 source bulk pause/resume/delete). Three-stage UI: type the find/replace text, tap Preview to see exactly which labels would change, tap Apply to commit. The preview is the safety net — operators verify before pressing the irreversible-feeling button. Activity log records each per-source rename via the same setSourceLabel-style messages as the rev-95 inline rename + a single bulk summary line so the audit trail captures both lineage and operation. **Strategic significance**: closes the explicit rev-95 next-sprint candidate at the named friction point. The source surface is now five operations deep (rev 6 pause/resume/delete + rev 16 health diagnostics + rev 26 keyword filters + rev 36 bulk pause/resume/delete + rev 95 single rename + rev 96 bulk rename) — every standard source-management action is available without delete-and-recreate.
- Sources CSV export — closes the procurement-evidence sextet at the source axis. The procurement evidence quintet (rev 6 JSON full + rev 7 activity CSV + rev 22 outputs CSV + rev 47 decisions CSV + rev 50 stale-tasks CSV + rev 60 cost summary CSV) covered every other surface; sources was the missing edge. A SOC 2 / ISO 42001 reviewer asking 'show me your input set with health diagnostics' had to take JSON + filter in Excel. Rev 96 closes the gap with a single procurement-friendly takeaway CSV that includes the rev-16 health diagnostic columns (lastSuccessAt / lastErrorAt / lastErrorMessage), the rev-26 keyword filter columns (includeKeywords + excludeKeywords joined by `|`), and the rev-12 7-day signal-rate column derived from the dashboard's existing per-source count query. New `getWorkspaceSourcesCsv()` helper + `GET /api/workspace/sources-export` route + `GET /api/v1/workspace/sources-export` v1 mirror in lockstep. Filename stamps the workspace slug so multi-workspace operators don't end up with a folder of indistinguishable sources.csv downloads. **Strategic significance**: closes the procurement-evidence sextet — the takeaway artefact set is now complete across every load-bearing surface. The same six-CSV trust signal that procurement teams already accept on the work / decision / cost / stale axes now applies to the input axis.
- Workspace search reaches sources — closes the seventh-axis search coverage gap. Workspace search has accumulated six entity types since rev 17 (signals + tasks + outputs + memory + comments + activity). Sources was the seventh and missing axis — a workspace with 20+ feeds had to scroll the sources panel to find a feed by keyword. Becomes load-bearing after rev 95 made source labels mutable: operators renaming feeds inevitably accumulate names they later want to find by partial match. Rev 96 closes the gap on both the dashboard search and the v1 surface. The dashboard `WorkspaceSearch` component gains a `sources?: SearchSource[]` prop that filters by label + type. The `/api/v1/search` endpoint runs a parallel SQL query against sources joined into the same response shape with a `sources: counts.sources + sources[]` block. Pressing Enter on a source hit scrolls the dashboard to the new `panel-sources` anchor on the sources panel. **Strategic significance**: closes the search-coverage cluster on the protocol-bound side — MCP hosts wrapping `/api/v1/search` for cross-workspace lookup tooling now reach every load-bearing entity in the workspace.
- Cumulative dashboard polish — sources panel hover lift + bulk-rename brand vocabulary. Cumulative micro-polish (every rev 22+ has carried at least one). New subtle `app-item-sm:hover` treatment on the sources panel (220ms ease, brand-color border + soft box-shadow lift) so the four cumulative source affordances (rev-95 rename, rev-6 pause/resume/delete, rev-26 keyword filter, rev-96 bulk rename) read as tactile rather than static — same pattern as rev-33's panel fade-in. New `.ld-source-bulk-rename*` styling shares the same brand-color gradient + border vocabulary as the rev-95 inline rename so single and bulk rename read as siblings. New `:focus-visible` ring on every chip + button inside the sources panel rows so keyboard-only operators land cleanly across every source affordance. **Strategic significance**: every rev 22+ has carried at least one piece of cumulative polish. The rev-by-rev discipline is what keeps the dashboard from drifting toward the design-debt smell that hand-rolled SaaS dashboards usually develop after 96 revs.
Per-task share-page input transparency + email-share wrapper + source rename + v1 + OpenAPI
- Per-task share-page input transparency — closes the named rev-94 next-sprint candidate. Rev 94 shipped the per-task work-log share + per-task timeline share pages, but neither surfaced the rev-41 artifact-share evidence primitive that procurement reviewers expect on every governance surface. Rev 95 closes it. New `getPublicTaskSourceSummary()` helper mirrors the rev-42 `getPublicArtifactSourceSummary()` shape exactly: counts + per-kind breakdown only, no signal detail bodies or source URLs that may carry internal context. Both `/share/work-log/<token>` and `/share/timeline/<token>` pages now render an 'Inputs that shaped this task' panel reading from the rev-1 task.sourceSignalIds + rev-44 task.sourceMemoryIds primitives. **Strategic significance**: closes the procurement-narrative-loop on the public surface that rev 94's running state explicitly named — external readers can now verify procurement evidence end-to-end on every share surface, not just artifacts. The rev-42 artifact share evidence primitive becomes load-bearing on two more public surfaces in one cycle.
- Email-share wrapper button — closes the named rev-94 next-sprint candidate. Rev 94's running state explicitly named 'work-log share email-link wrapper' as a rev-95 candidate, citing the share→deliver loop closure for operators forwarding share links via email. Rev 95 ships the wrapper across all three share button surfaces (rev-10 artifact, rev-93 work-log, rev-94 timeline) as one shared `ShareEmailButton` component. New 'Email' chip alongside the existing Show/Hide/Copy/Revoke chips opens the operator's default mail client via `mailto:` with the subject + body pre-filled (artifact title or task title + share URL + 'shared via Loop Desk' attribution). Pure client-side — no server round trip, no Resend, no deliverability concerns; the email lands in the operator's outbox where they can review + send. **Strategic significance**: closes the share→deliver loop the rev-94 next-sprint focus named. Procurement-conscious operators forwarding evidence to a reviewer no longer have to copy the share URL + open Gmail + paste + type a procurement-friendly subject + body by hand. One tap → mail client opens with the right subject + body. The lowest-friction expression of 'this share link is meant to be sent.'
- Source rename inline — closes a long-outstanding usage-friction gap. Source labels have been write-once at create-time since rev 1. Workspaces with 20+ feeds (especially rss.app / fetchrss bridges with generic auto-generated names like 'Daily updates from Acme') routinely accumulated labels that weren't workstream-meaningful, and changing them required a delete-and-recreate dance that lost the rev-5 GUID dedup state. Rev 95 ships inline rename. New `setSourceLabel()` helper + extended `PATCH /api/sources/{id}` route accepting `{ status }` OR `{ label }` (mutually exclusive) + matching `PATCH /api/v1/sources/{id}` v1 mirror in lockstep + new `SourceRename` client component (Pencil chip → inline editable input → Save / Cancel). Activity log records every rename so the audit trail captures what the source used to be called — load-bearing for SOC 2 / ISO 42001 reviewers tracing how the workspace's input set evolved. **Strategic significance**: cumulative diversification away from the cost / digest / share-page cluster that has dominated rev-by-rev work for many cycles. The source surface is the operator's primary input-tuning surface (rev 6 pause/resume + rev 16 health diagnostics + rev 26 keyword filters + rev 36 bulk operations + rev 95 rename) — closing the rename gap finally makes every standard source-management action available without a delete-and-recreate workaround.
- v1 mirror + OpenAPI typed schema for the rev-95 endpoint. New `PATCH /api/v1/sources/{id}` mirrors the dashboard endpoint (mutually exclusive `{ status }` | `{ label }` payload) so MCP hosts driving the desk programmatically can pause/resume + rename through one bearer-auth call. Typed in the rev-78 OpenAPI 3.1 spec via `oneOf` so code generators see the discriminated-union shape exactly. Indexed in `/api/v1`. The cadence pattern of 'ship the dashboard primitive + the v1 mirror + the OpenAPI typed schema in lockstep' that started rev 37 continues unbroken into rev 95 — the source-mutation surface on the protocol-bound side now matches the dashboard surface 1:1, leaving nothing for the upcoming MCP server to design.
Per-task work-log share daily-views sparkline + per-task timeline share permalink + v1 + OpenAPI parity
- Per-task work-log share 14-day daily-views sparkline — closes the named rev-93 next-sprint candidate. Rev 93 shipped the public per-task work-log share page + cumulative view counter, but no daily-views surface — the rev-13 artifact share already had a 14-day sparkline (rendered inline in the dashboard ArtifactShareButton), so engagement-tracking parity was the missing edge. Rev 94 closes it. New `task_work_log_share_view` per-day event table + extended `recordTaskWorkLogShareView()` writes both the cumulative counter and an event row + new `getTaskWorkLogShareDailyViews()` helper + new `GET /api/tasks/{id}/work-log-views` route + matching `GET /api/v1/tasks/{id}/work-log-views` v1 mirror. New ViewSparkline subcomponent inside the rev-93 TaskShareWorkLogButton lazily fetches the 14-day buckets when the share link is open and renders the same brand-color SVG sparkline as the rev-13 artifact share button. **Strategic significance**: closes the engagement-tracking parity gap explicitly named in the rev-93 next-sprint focus list. Operators iterating on procurement evidence now see whether a reviewer has been re-reading the work log over a week vs reading once and moving on — load-bearing context for following up on shared evidence.
- Per-task timeline share permalink — closes the named rev-93 next-sprint candidate. Rev 93 shipped the work-log share (`/share/work-log/<token>`) covering AI cycle output + operator notes only; the rev-40 in-app TaskTimeline aggregates more (adds rev-26 comments + the creation event) so a richer 'everything that happened on this task' public read was the natural follow-up. Rev 94 closes it. New `task.publicTimelineToken` + `publicTimelineViewCount` + `publicTimelineLastViewedAt` columns + `setTaskTimelineShareToken()` + `getTaskByTimelineShareToken()` + `recordTaskTimelineShareView()` helpers + `POST /api/tasks/{id}/share-timeline` route + matching `POST /api/v1/tasks/{id}/share-timeline` v1 mirror. New public read-only `/share/timeline/[token]` page renders the rev-40 unified timeline (creation + AI cycles + operator notes + comments) with per-kind colour-tinted left-border accents (brand-color = AI cycle, amber = operator note, brand-purple = comment, green = creation) so an external reader scans the narrative by event type. Robots-noindex, fire-and-forget view counter, print-friendly via the rev-92 print stylesheet (extended in lockstep). New TaskShareTimelineButton mounts alongside the rev-93 TaskShareWorkLogButton on every active-work card so operators choose which surface to expose per task. **Strategic significance**: closes the named rev-93 next-sprint candidate. The procurement story across the public surface is now six levels deep: input evidence (rev 42) + revision lineage (rev 44) + interactive narrowing (rev 45/46) + take-and-print (rev 92) + AI cycle reasoning trail (rev 93) + complete task narrative (rev 94). Distinct from rev-93 work-log share so operators control the granularity of what they expose — narrow procurement evidence (rev 93) vs full collaboration narrative (rev 94).
- v1 + OpenAPI typed-schema coverage on the new rev-94 endpoints. Both new dashboard primitives ship with v1 mirrors in the same cycle (the cadence pattern from rev 37 onward continues unbroken). New `POST /api/v1/tasks/{id}/share-timeline` + `GET /api/v1/tasks/{id}/work-log-views` endpoints + matching typed schemas in the OpenAPI 3.1 spec + entries in the `/api/v1` self-describing endpoint index. **Strategic significance**: closes the v1 parity gap on the rev-94 dashboard primitives in the same cycle the dashboard primitives ship. The MCP server's task-share tooling now has two parallel surfaces (work-log narrow + timeline broad) typed end-to-end with no protocol-translation work left to design.
- Visual polish — timeline share page anchored in the rev-93 worklog vocabulary + per-kind tints. New `.ld-share-timeline-*` CSS uses the same brand-green left-border + soft gradient as the rev-93 work-log share page so the two procurement-takeaway surfaces on `/share/*` read as siblings. Per-kind row tints distinguish AI cycles (brand-color), operator notes (amber `#cf6c3a`), comments (brand-purple `#6b4ed6`), creation events (green `#2c8a4a`), and activity-log entries (neutral) — same per-kind colour vocabulary as the rev-40 in-app TaskTimeline + rev-35 activity log glyph palette so the dashboard reads with one consistent kind-tinting language across all three surfaces. The rev-92 print stylesheet extended to cover the new timeline classes so the takeaway PDF reads cleanly. **Strategic significance**: keeps the rev-by-rev visual-hierarchy discipline alive — every share surface now carries the same chrome but distinguishes itself by accent palette, so the operator's eye reads the surface type without parsing copy.
Per-task work log shareable permalink + workspace-shared saved searches + outbound subscription bulk actions
- Per-task work log shareable permalink — closes the named rev-92 next-sprint candidate. Until rev 93 the rev-12 per-task work log + rev-14 operator notes were operator-visible on the dashboard but had no external-reader surface — a procurement reviewer auditing AI cycle work had to either be granted dashboard access or rely on the operator copy-pasting the work log into another doc. Rev 93 closes that. New `task.publicWorkLogToken` column + `setTaskWorkLogShareToken()` helper + `POST /api/tasks/{id}/share-work-log` route + matching `POST /api/v1/tasks/{id}/share-work-log` v1 mirror. New public read-only `/share/work-log/[token]` page renders the work log + operator notes as a procurement-friendly evidence trail with token + estimated-cost summary, robots-noindex, and print-friendly via the rev-92 print stylesheet. The rev-92 print stylesheet primitive turns out to be load-bearing on the rev-93 surface — operators take it as a clean single-document PDF for their procurement evidence package without any new tooling. New `TaskShareWorkLogButton` mounts on every active-work card alongside the rev-12 TaskWorkLog button. Mirrors the rev-10 ArtifactShareButton shape exactly so operators read the share affordance with one consistent vocabulary across the artifact + work-log share surfaces. **Strategic significance**: closes the procurement-evidence gap on AI cycle work explicitly named in the rev-92 next-sprint focus list. The procurement story across the public surface is now five levels deep: input evidence (rev 42) + revision lineage (rev 44) + interactive narrowing (rev 45/46) + take-and-print (rev 92) + AI cycle reasoning trail (rev 93).
- Workspace-shared saved searches — closes the named rev-92 next-sprint candidate. New `workspace.sharedSavedSearches` JSONB column carrying `{ id, name, query, createdByUserId, createdByName, createdAt }[]`. Capped at 30 entries per workspace (vs rev-18's 6 per-user) since it pools across the team. New `addSharedSavedSearch()` / `listSharedSavedSearches()` / `removeSharedSavedSearch()` helpers + `GET/POST/DELETE /api/workspace/shared-saved-searches` routes + matching v1 mirrors with `asUserId` attribution defaulting to workspace owner. The `WorkspaceSearch` client component surfaces shared chips in a teal-tinted row (visually distinct from rev-18 personal grey chips) below the personal saved row in the dropdown when query is empty. New 'Share with team' footer button (Users icon, gated to editor+) lets the operator promote their current query into the workspace-shared list with one tap + a name prompt. **Strategic significance**: rev-18 personal saved searches are per-user-per-workspace localStorage — drift across devices and don't cross between teammates. Workspace-shared lets multi-operator teams curate a power-user search vocabulary that every member inherits ('competitor X', 'concern', 'renewal Q3') without re-typing on every machine. Distinct from rev-18 — both surfaces coexist so an operator's personal queries stay private. Pairs with the rev-78 multi-device sync of personal panel collapse + rev-78/79/80/81 dashboardPrefs personal config as the cross-device dashboard collaboration story.
- Bulk pause/resume/delete on outbound subscriptions — closes six-entity bulk-action symmetry. New `bulkUpdateOutboundSubscriptions()` helper + `POST /api/workspace/outbound/bulk` route + matching `POST /api/v1/outbound/subscriptions/bulk` v1 mirror. New bulk-action bar surfaces inline in the rev-19 OutboundSubscriptions client component when 2+ subscriptions exist + 1+ is selected, with Pause / Resume / Delete buttons + Select all visible / Clear actions. New checkbox column on every subscription row when ≥2 subscriptions exist. Caps at 50 subscription IDs per call. Activity log records every bulk action with the affected count. **Strategic significance**: rev-26 task bulk + rev-33 signal bulk + rev-34 memory bulk + rev-36 source bulk + rev-6 artifact bulk shipped on the four core entities + sources. Rev 19 turned outbound delivery into a real per-event router but bulk operations on the subscription axis itself were missing — operators with 8+ subscriptions (e.g. a FinOps team mirroring different events to different downstream URLs across multiple staging environments) cycled them one click at a time. Rev 93 collapses that to one selection + one action. Closes the six-entity bulk-action symmetry across every CRUD surface on the dashboard.
- Visual polish — work-log share page + shared-search chip + bulk-action bar all anchored in the brand-color vocabulary. New `.ld-share-worklog-*` CSS treatment for the rev-93 public work-log page uses the same brand-green left-border + soft gradient as the rev-42 source-evidence panel + rev-44 revision-history panel so the three governance surfaces on `/share/*` read with one consistent visual vocabulary. Per-cycle work log rows wear a brand-color marker; per-operator note rows wear an amber `#cf6c3a` marker so the eye scans the cycle ↔ note alternation cleanly. New stats grid (cycles + operator notes + total tokens + estimated AI cost) anchors the procurement-evidence framing at the top of the takeaway. New `.ld-search-shared-chip` teal-tinted treatment distinguishes workspace-shared chips from rev-18 personal grey chips. New `.ld-outbound-bulk-bar` matches the rev-26/33/34/36 bulk-bar typography + brand-color accent + animation so the six bulk surfaces read as siblings. Print stylesheet extended to cover the rev-93 work-log surface: `.ld-share-worklog-stats` strips background / picks up borders, rows + foot read cleanly without screen chrome bleed-through. Cumulative micro-polish — every rev 22+ has carried at least one. **Strategic significance**: rev 93's polish is load-bearing because three new surfaces ship in one cycle and need to read with one consistent visual story so the dashboard doesn't acquire the design-debt smell that hand-rolled SaaS dashboards usually develop after 93 revs.
Digest configuration audit endpoint + 'n' keyboard shortcut + print-friendly /share/[token] + integrations panel polish
- Historical digest-gating audit log on dashboard + v1 — closes the named rev-91 next-sprint candidate. Rev 91's running state explicitly named 'unified digest configuration audit endpoint' as the rev-92 candidate, citing the rev-90 closure-receipt event + rev-90 activity-log entry + rev-91 multi-channel push as the three layers that together capture every gating change but had no historical-read surface. Rev 92 closes that. New `getDigestAuditLog()` helper reads the rev-90 `digest_gating_change` activity-log entries within a bounded window (default 30 days, max 365), parses each detail string into a structured `{ outcome, recipientLabel }` shape, and returns newest-first. New `GET /api/workspace/digest-audit?sinceDays=30&limit=100` (admin-only) + matching `GET /api/v1/workspace/digest-audit?sinceDays=30&limit=100` (bearer-auth) endpoints expose it on both the dashboard and protocol-bound surfaces in lockstep. New collapsible inline 'Gating history' section in the integrations panel renders directly under the rev-89/91 gating-pill cluster so admins see both current outcome AND historical drift in one glance. OpenAPI 3.1 spec entry typed in lockstep. **Strategic significance**: closes the four-axis digest-config instrument cluster on the protocol-bound surface — rev-86 digest-preview (render-only test) + rev-89 digest-recipients-gating (current-instant outcome) + rev-90 digest-config (current snapshot) + rev-92 digest-audit (historical drift). The MCP server's digest-config tooling now has nothing left to design across the *complete* descriptive→audit lifecycle. Pairs with the rev-91 multi-channel push as the historical-read surface that completes the rev-90 closure-receipt loop on the procurement-conscious side.
- Power-user keyboard shortcut 'n' opens the new task creation form. Extends the rev-23 keyboard shortcuts overlay + rev-27 ⌘K command palette with the third primary creation affordance — power-user keyboard parity with Linear/Raycast/GitHub for the most-frequent originating operator action (handing the desk a new task). Press `n` from anywhere on the dashboard (when not typing in an input/textarea) and the desk: (a) smooth-scrolls to the rev-24 `panel-task-create`, (b) dispatches a `loopdesk:open-task-form` custom event, (c) the form's new useEffect listener expands the `open` state and focuses the title input. Documented in the rev-23 keyboard shortcuts overlay so the shortcut is discoverable. **Strategic significance**: rev 23 shipped the keyboard surface (?, /, g+a/t/s/m/h, Esc); rev 27 shipped the command palette; rev 92 closes the third edge of the keyboard creation triangle. The dashboard now has three orthogonal entry points for new tasks — chip-tap on a quick-start template (rev 64), command palette ⌘K (rev 27), single-key shortcut `n` (rev 92) — covering chip-discovery, search-driven, and muscle-memory operators in one consistent vocabulary.
- Print-friendly /share/[token] page — procurement-friendly takeaway at zero schema cost. Procurement reviewers reading shared briefs frequently want to print or save-to-PDF for their evidence package; until rev 92 the page printed with the brand header, the rev-45 ShareTagDrillDown chip row, the rev-18 ShareFeedbackForm reaction surface, and the dashboard footer all bleeding ink and competing with the load-bearing content. Rev 92 adds an `@media print` block in `globals.css` that hides every operator-only or interactive surface (header / drill-down / feedback / footer / status pills) and strips the card chrome so the body reads as a clean single-document print. Title + summary + body get print typography (24pt / 12pt / 11pt). External links print their full URL inline via `a[href^='http']::after` so the reviewer can trace every reference without scanning the screen version. Revision lineage (rev 44) + source evidence (rev 42) print as boxed appendix blocks at the end with `page-break-inside: avoid` so they read as a clean appendix. **Strategic significance**: rev 42 made input transparency operator-visible on the share page; rev 44 made revision lineage external-reader-visible. Rev 92 makes both procurement-takeaway-friendly. The procurement story across the public surface is now four levels deep: input evidence (rev 42) + revision lineage (rev 44) + interactive narrowing (rev 45/46) + take-and-print (rev 92).
- Visual polish — audit history list typography + brand-green/amber row tints. New `.ld-digest-audit-*` CSS uses a brand-green left-border accent on `would_send` rows + amber on `muted` rows, mirroring the rev-89/91 gating-pill colour vocabulary so the three dashboard gating surfaces (rev-89 rollup pill / rev-91 per-member pill / rev-92 audit row) read as one consistent visual story. The collapse/expand chevron (▸/▾) reads as a quiet affordance under the digest control cluster — the section is expanded only when the operator wants it, so the integrations panel doesn't grow visually for admins who don't care about the audit history. Cumulative micro-polish — every rev 22+ has carried at least one. **Strategic significance**: the integrations panel now reads as a four-level digest control cluster (rev-2 on/off → rev-79 personal sections → rev-80/81 mute config → rev-89 gating preview → rev-92 audit history) wearing one consistent typography + colour vocabulary. The rev-by-rev discipline of one targeted polish per rev keeps the panel from drifting into instrument-stack chaos after 92 revs.
digest.gating_changed multi-channel push (Slack + email) + members-panel gating pill + visual polish
- Slack + email push companion to digest.gating_changed — closes the named rev-90 next-sprint candidate. Rev 90 fired the gating change as an outbound webhook event + activity-log entry; rev 91 closes the named rev-90 next-sprint candidate by adding the matching Slack push (workspace-wide audit-trail channel via the workspace's slackWebhookUrl — every admin sees the gating change in chat alongside the rev-90 audit log + outbound) and per-recipient email companion (the affected operator's own receipt of their gating change with subject 'Your digest delivery resumed' or 'Your digest is now muted'). Distinct surfaces: outbound webhook = downstream integrations (CRM, FinOps tool, audit), Slack = workspace-wide audit, email = per-recipient personal receipt. Best-effort everywhere; never blocks the prefs save (matches the rev-90 dispatch + activity-log discipline). New `buildDigestGatingChangedSlackPayload()` Slack block carries traffic-light or no-bell emoji depending on the new gating outcome plus mute reasons (weekend mute / quiet-hours window) inline. **Strategic significance**: closes the rev-90 named candidate. The full digest gating axis now reaches every operator-loaded channel — outbound (rev 90 — integrations), activity log (rev 90 — audit), Slack (rev 91 — workspace chat), email (rev 91 — per-recipient inbox).
- In-dashboard gating outcome pill on every owner/admin member row. Closes the in-app visibility gap on the rev-89 multi-recipient gating primitive. Until rev 91 the rev-89 dashboard pill on the integrations panel showed the workspace-wide rollup ('Would send N of M') but admins couldn't see *who specifically* was muted without opening the rev-89 v1 endpoint. Rev 91 surfaces the per-recipient gating outcome inline on every owner/admin row in the workspace members panel — `✓ digest` (brand-green) or `🔇 muted` (amber) with a tooltip explaining the mute reason (weekend mute / quiet-hours window / both). Pure derived state via the new `getDigestGatingForOwnerAdmins()` helper — no schema, one extra workspace + members query that runs alongside the existing dashboard fetch. **Strategic significance**: pairs with the rev-89 integrations-panel rollup pill as the per-recipient detail surface. Matches the rev-89 colour vocabulary (brand-green when clean, amber when muted) so the two gating surfaces read with one consistent visual story.
- OpenAPI 3.1 spec rev-91 changelog block. Documents the rev-91 multi-channel push surface in the OpenAPI 3.1 changelog header. The rev-89 multi-recipient gating endpoint + rev-90 digest-config endpoint were already typed in lockstep with their dashboard primitives; rev 91 adds no new v1 endpoints since the Slack + email channels are dispatched alongside (not downstream of) the existing `digest.gating_changed` outbound event. The protocol surface is unchanged; the multi-channel push completes the rev-90 closure-receipt loop on the operator-visible side. **Strategic significance**: keeps the cadence pattern alive — every rev's OpenAPI changelog entry documents what happened on the typed-contract surface even when the protocol surface itself is stable. MCP-host code generators reading the spec see exactly when each rev's primitive landed.
- Visual polish — gating pill colour vocabulary alignment. New `.ld-member-gating-pill` CSS uses the same brand-green-vs-amber palette as the rev-89 `.ld-digest-gating-pill` so the two gating surfaces (integrations panel rollup + members panel per-row) read as siblings with one consistent visual story. 1px hover lift + 160ms transition for tactile click affordance + `cursor: help` so admins know to hover for the tooltip. Cumulative micro-polish (every rev 22+ has carried at least one). **Strategic significance**: keeps the rev-by-rev visual-hierarchy discipline alive — the dashboard now has two related but distinct gating surfaces (workspace-wide rollup, per-member detail) wearing the same colour vocabulary so admins pattern-match instead of re-reading from scratch.
digest.gating_changed closure receipt + v1 digest-config aggregate + activity-log glyph + state-pulse polish
- digest.gating_changed outbound event — closes the named rev-89 next-sprint candidate. Rev 89's running state explicitly named 'per-recipient gating push notification' as the rev-90 candidate, citing the rev-37 closure-receipt pattern at the digest gating axis as the natural next step. Rev 90 closes that. New `digest.gating_changed` outbound event + `dispatchDigestGatingChangedWebhook()` dispatcher + private `notifyDigestGatingFlip()` helper in `src/lib/workspaces.ts` wired into `setDashboardPrefs()` via fire-and-forget `void …catch(() => null)` so a downstream Slack/email/webhook failure can't ripple back to block the prefs save itself. Fires only when (a) the rev-80 `digestQuietWeekends` or rev-81 `digestQuietHoursStart`/`digestQuietHoursEnd` prefs are in the patch AND (b) the today gating outcome actually flipped for the simulated-now instant in workspace TZ AND (c) the recipient is an owner/admin (so the change actually affects who would receive the digest). Activity-log entry + outbound payload include both previous and next gating outcomes (`weekendMuted`, `quietHoursMuted`, `wouldSend`) plus the evaluated instant + workspace timezone so a downstream audit trail can reconstruct exactly when the flip happened. **Strategic significance**: closes the rev-37 closure-receipt pattern at the digest gating axis. Until rev 90 downstream integrations watching the rev-80/81 quiet-window primitives (via the rev-89 v1 multi-recipient gating endpoint) had to *poll* for changes — there was no event-bus signal saying 'Maria's quiet-hours config just changed and she's now muted today.' Rev 90 closes the loop. The full digest gating axis now has read (rev 89 multi-recipient endpoint) + write (rev 79/80/81 dashboardPrefs) + closure-receipt event (rev 90) on every channel.
- GET /api/v1/workspace/digest-config — aggregate digest configuration snapshot. New bearer-auth aggregate read endpoint that returns workspace-level digest config (digestEmail bool + IANA timezone) + every owner/admin recipient's per-recipient dashboardPrefs (digestPersonalSections / digestQuietWeekends / digestQuietHoursStart-End) in one call. Until rev 90 an MCP host or audit tool wanting to render 'this workspace's full digest configuration' had to enumerate three round trips — workspace + members + per-recipient prefs — collapsing to one bearer-auth call now. Pure read-only, no side effects. Listed in the `/api/v1` self-describing index. **Strategic significance**: closes the procurement-friendly aggregate read surface for the rev-80/81/89 digest gating cluster. Pairs with the rev-86 send-test (real email) + rev-87 view-preview HTML (render only) + rev-88 simulated-date primitive (gating block in JSON) + rev-89 multi-recipient gating endpoint (per-recipient gating outcome at an instant) as the fifth instrument on the digest verification cluster — this one is the configuration *snapshot* surface. The MCP server's digest-config tooling now has nothing left to design.
- Activity-log glyph + per-kind tint for digest_gating_change. Extends the rev-35/86 glyph + tint vocabulary to cover the new rev-90 `digest_gating_change` activity-log kind. Traffic-light glyph (🚦) so operators reading the activity log see the gating-flip surface distinct from the cost-spike (⚡) and chronic-warning (⏳) glyphs — gating-change is a transition signal, not an alarm. New `.ld-activity-digest_gating_change` per-kind tint uses a brand-green palette (rgba(15,118,110,*)) so it reads as state-positive (the rev-37 task.unblocked closure visual vocabulary) rather than amber/red (alarm). **Strategic significance**: closes the per-kind activity-log scannability symmetry on the new rev-90 closure-receipt kind in the same cycle the kind ships. The activity log now reads at three levels — visual (glyph + colour), categorical (chip filter), keyword (search). The dashboard-side audit trail surface for the digest gating axis is now complete.
- Visual polish — gating pill state-pulse + simulated-date persistence. Two cumulative micro-polish pieces (every rev 22+ has carried at least one). (a) The rev-89 gating pill now rings out with a brief 0.6s 1.04× scale + soft brand-color glow (`@keyframes ld-digest-gating-pulse`) every render — the rev-89 `transition` rule already smoothed colour changes; the new keyframes draws the operator's eye to the pill on first render and on every re-render after a simulated-date change so the operator catches the gating-outcome flip without re-reading the surrounding copy. Matches the rev-37 task.unblocked + rev-85 reset-chip success-pulse vocabulary so all the dashboard's transition-signal affordances ring out the same way. (b) The rev-89 `<input type="datetime-local">` value now persists in localStorage under a per-workspace key so admins iterating on rev-78/79/80/81 configuration don't lose their simulated instant on every refresh. Lazy initializer keeps SSR clean. **Strategic significance**: keeps the rev-by-rev visual-hierarchy discipline alive — every chip + pill on the integrations panel now wears its own state vocabulary AND survives across refreshes. Operators iterating on configuration over a coffee break don't have to re-type the simulated-date when they come back.
Inline gating banner + multi-recipient gating endpoint + dashboard simulated-date input
- Inline gating banner on the dashboard view-preview HTML — closes the named rev-88 next-sprint candidate. Rev 88's running state explicitly named the inline gating banner as the rev-89 candidate. Rev 89 ships it. The dashboard `/api/digest/preview?format=html` GET response now leads with a small inline banner that surfaces the rev-88 gating outcome at a glance: brand-color check + 'Would send · would deliver to maria@example.com' or amber 🔇 + 'Muted · maria@example.com — blocked by weekend mute (Saturday) + quiet hours window 22:00→07:00'. Until rev 89 admins opening the View-preview tab had to inspect the JSON variant to verify the rev-80/81 gating decision. Rev 89 makes the answer the first thing the eye lands on. The banner uses the same colour vocabulary (brand-color = action, amber = muted) as the rev-32+ cost-spike + chronic banners so the visual story across alarm + gating surfaces stays consistent. **Strategic significance**: closes the named rev-88 next-sprint candidate. Pairs with the rev-86 send-test (real email send) + rev-87 view-preview (HTML render) + rev-88 simulated-date primitive (gating block in JSON) as the fourth instrument on the digest verification cluster — admins now see the gating outcome inline in the rendered tab without parsing JSON.
- GET /api/v1/workspace/digest-recipients-gating + dashboard mirror — multi-recipient gating preview. New bearer-auth v1 endpoint mirrors the rev-88 single-recipient gating decision across every owner/admin recipient on the workspace in one call. Returns per-recipient `{ digestQuietWeekends, digestQuietHoursStart/End, digestPersonalSections, weekendMuted, quietHoursMuted, wouldSend }` plus a workspace-level rollup of `{ recipientCount, wouldSendCount, mutedCount }`. Optional `simulatedDate=ISO` (±60d) overrides the gating instant identically to the rev-88 view-preview surface. Pure read-only — no email sends, no activity-log writes, no dashboardPrefs mutations. Reuses the same `workspaceOwnerEmails()` projection + rev-15 timezone math + rev-80 weekend-mute + rev-81 quiet-hours gating the production cron applies, so the answer is identical to what the 13:15 UTC cron would compute at the same instant. Dashboard mirror at `GET /api/digest/recipients-gating` (admin-only) drives the new live 'Would send N/M' pill in the integrations panel. Typed in the OpenAPI 3.1 spec in the same cycle. **Strategic significance**: until rev 89 the rev-88 gating block answered the *single-recipient* question. The multi-recipient surface is what admins actually need on multi-operator desks ('this digest fires for half the team but not Maria — let me check her quiet-hours config') and it's the natural shape for the upcoming MCP server's 'show me who would receive the next digest' tooling.
- Dashboard simulated-date input + live 'Would send N/M' pill in integrations panel. New `<input type="datetime-local">` next to the rev-88 View-preview button lets admins ask 'what will the digest gating decision look like next Saturday at 8pm?' directly from the dashboard without crafting URLs by hand. The input's value is plumbed into both the View-preview anchor's `href` (so opening the tab carries the simulated instant) AND the new live gating-summary fetch that re-renders a 'Would send N/M' pill on every change. Pill colour follows the rev-32+ vocabulary: brand-color when all recipients receive, amber when some are muted, deep-red when every recipient is muted at the simulated instant. AbortController prevents stale fetches from clobbering newer ones if the operator scrubs the input rapidly. **Strategic significance**: until rev 89 the rev-88 simulated-date primitive was protocol-bound only — admins on the dashboard had to construct the URL by hand. Rev 89 surfaces it as a first-class input. The live pill closes the loop: admins see the gating outcome continuously without opening a tab.
- Visual polish — digest controls cluster reads as a unified row. Cumulative micro-polish (every rev 22+ has carried at least one). New `.ld-digest-controls` flex-wrap CSS so the rev-36 Send-test, rev-88 View-preview, rev-89 simulated-date input, rev-89 gating pill, and the rev-2 on/off toggle all read as a unified controls row that wraps cleanly on narrow viewports. The `<input type="datetime-local">` picks up the Loop Desk surface typography via `.ld-digest-simulated` (Loop Desk border + radius + focus ring) so it reads as a sibling of the surrounding buttons rather than a stamped-on browser-default control. The new `.ld-digest-gating-pill` shares the rev-21+ pill colour vocabulary with `is-clean` / `is-mixed` / `is-blocked` modifiers so admins read the gating outcome without parsing copy.
Simulated-date digest preview + dashboard render-only button + OpenAPI gating block
- simulatedDate query param on digest preview GET — closes the named rev-87 next-sprint candidate. Rev 87's running state explicitly named 'simulated-date dry-run on the rev-87 digest preview GET' as the rev-88 candidate, citing pairing with the rev-80/81 weekend-mute + quiet-hours window prefs since their behaviour depends on the calendar day. Rev 88 closes that. The rev-87 GET endpoint now accepts an optional `simulatedDate=ISO` query param (bounded to ±60 days). When set, the rendered HTML stays workspace-current (signals/cycles/outputs reflect today's actual data — pretending those changed too would invalidate the preview), but the rev-80 weekend-mute and rev-81 quiet-hours-window gating decision runs against the simulated instant in the workspace timezone. The JSON response gains a new `gating` block surfacing `{ simulatedAt, timezone, weekday, hour, isWeekend, weekendMuted, quietHoursMuted, quietHoursStart, quietHoursEnd, digestQuietWeekends, digestPersonalSections, wouldSend }` so an MCP host can render the gating outcome alongside the HTML. **Strategic significance**: until rev 88 admins iterating on rev-80/81 dashboardPrefs configuration could verify the rendered content but couldn't verify the *gating decision* without waiting for that calendar day to actually arrive. Rev 88 closes the loop — admins can now ask 'would Maria receive this digest next Saturday at 8pm Tokyo?' and get a deterministic answer in one bearer-auth call. Pairs with the rev-86 POST (real send) + rev-87 GET (render-only) as the third leg of the 'verify every dimension of the digest pipeline' surface.
- Dashboard 'View preview' button — render-only sibling to the rev-36 send-test button. Rev 36 shipped `/api/digest/preview` POST as the dashboard send-test path. Until rev 88 admins iterating on configuration burned a real Resend email per iteration. Rev 88 adds a GET handler on the same dashboard route that delegates to the new `dryRun: true` shape and renders the HTML inline. New 'View preview' button mounted in the integrations panel directly beside the rev-36 'Send test digest' button so the two read as siblings: send-test verifies inbox-side rendering, view-preview iterates on configuration without mailbox cost. Owner/admin only (matches the POST guard). Default `?format=html` so the browser renders the body in the new tab. Optional `?simulatedDate=ISO` mirrors the rev-88 v1 surface so admins can preview future-day digests directly from the dashboard. **Strategic significance**: closes the dashboard-side parity gap on the rev-87 dry-run primitive in the same cycle the v1 enhancement ships. The cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror in lockstep) continues unbroken.
- OpenAPI 3.1 typing for the simulatedDate param + gating block. Following the cadence pattern from rev 78 onward — every v1 enhancement gets typed in the OpenAPI 3.1 spec in the same cycle it ships. Rev 88 types the new `simulatedDate` query param (string, format: date-time, bounded ±60d) on the rev-87 GET endpoint plus the new `gating` block on the JSON response (with full property typing for weekday / hour / isWeekend / weekendMuted / quietHoursMuted / digestQuietWeekends / digestPersonalSections / wouldSend). **Strategic significance**: the typed-contract surface for the digest-preview endpoint now covers both the HTML render and the gating decision. MCP hosts wrapping the digest preview for 'when will the next digest fire?' tooling can typecheck the response shape end-to-end. The cadence pattern of 'ship the primitive + the v1 surface + the index entry + the OpenAPI typed schema + the dashboard mirror in lockstep' that started rev 37 continues unbroken through rev 88.
- Visual polish — render-only button anchors the digest controls cluster. Cumulative micro-polish (every rev 22+ has carried at least one). The new 'View preview' button uses the same `ld-btn ld-btn-ghost` palette as the rev-36 send-test button so the two read as siblings in the digest controls cluster. Anchor element (vs button) so the new tab opens cleanly with `target="_blank"` + `rel="noreferrer noopener"`. Title attribute reads 'Open the rendered digest HTML in a new tab without sending an email' so the affordance is discoverable on hover. The send-test button's title reads 'Send a one-shot preview of today's digest to your email' — the two title strings together communicate the render-only ↔ real-send distinction at a glance.
Bulk chronic-ack endpoints + v1 digest-preview dry-run
- Bulk chronic-ack endpoints across all three chronic dimensions — closes the named rev-86 next-sprint candidate. Rev 86's running state explicitly named 'bulk chronic-ack endpoints (per-tag / per-source / per-assignee — the three axes where chronic makes sense)' as the natural rev-87 candidate. Rev 87 closes that. Six new endpoints — `POST /api/cost/by-tag/chronic-ack/bulk`, `POST /api/sources/chronic-ack/bulk`, `POST /api/cost/by-assignee/chronic-ack/bulk` plus their three v1 mirrors — accept a `{tags: string[≤50]}` / `{sourceIds: string[≤50]}` / `{assigneeUserIds: string[≤50]}` payload and stamp the rev-71/72 chronic-ack columns (`workspace.tagChronicAcks` / `source.chronicAckedAt` / `workspace_member.chronicAckedAt`) for every supplied item that belongs to the workspace. Each helper fires one closure-receipt outbound event per acked item so downstream integrations watching the rev-73 `tag.chronic_warning_acked` / `source.chronic_warning_acked` / `assignee.chronic_warning_acked` events see the same per-item shape they get from a single ack call (vs an inconsistent bulk-shape contract). Three new chronic bulk-ack bars surface in the cost-by-tag / cost-by-source / cost-by-assignee panels when 2+ items have crossed the chronic threshold AND the operator has editor+ access — distinct amber palette from the rev-60/63/68 daily bulk-ack bars (red palette) so operators tell at a glance which horizon they're acting on. **Strategic significance**: closes the chronic-axis bulk-action symmetry to match the daily-axis bulk-action symmetry that already shipped (rev 57/60/63/68). The cost-axis MCP cluster now has bulk-ack symmetry on every axis where chronic makes sense — protocol-bound surface has nothing left to design on the chronic-ack side. Until rev 87 a manager triaging 5 chronic per-assignee warnings after a quarter-end push had to tap each chip individually; rev 87 collapses that to one click on the dashboard and one bearer-auth call from MCP.
- GET /api/v1/workspace/digest-preview dry-run — closes the named rev-86 next-sprint candidate. Rev 86's running state explicitly named 'rev-86 v1 GET digest preview dry-run (returns the rendered HTML inline without sending)' as the natural rev-87 candidate. Rev 87 ships it. New GET handler on the existing rev-86 endpoint accepts optional `userId=` (defaults to workspace owner like the POST companion) + optional `format=html|json` (defaults to JSON). Reuses the existing `previewDigestForUser` helper through a new `dryRun: true` option that (a) skips the Resend precondition check so workspaces without `RESEND_API_KEY`/`EMAIL_FROM` can still verify the rendered shape, (b) skips the actual email send, (c) skips the activity-log write. Returns either the raw HTML body with `text/html` content type (when `format=html`) or `{ ok, dryRun: true, recipient, subject, html }` JSON (default). Honours every recipient's rev-78/79/80/81 dashboardPrefs exactly as the POST endpoint would. **Strategic significance**: closes the email-round-trip cost on digest-config testing. Until rev 87 every iteration on dashboardPrefs configuration burned a real Resend send + a real mailbox round-trip + an activity-log entry. Rev 87 lets MCP hosts pipe the digest body through their own preview tool (visual diff, screenshot, markdown conversion, cypress visual regression) without any per-iteration mailbox cost. Pairs with the rev-86 POST endpoint (which still sends a real email so admins can verify their inbox-side rendering) as complementary verification surfaces — render-only for fast iteration, real-send for end-to-end verification.
- OpenAPI 3.1 spec coverage for the four new endpoints in lockstep. Following the cadence pattern from rev 78 onward — every new v1 endpoint gets typed in the OpenAPI 3.1 spec in the same cycle it ships. Rev 87 types the three new chronic bulk-ack endpoints + the new GET dry-run on the digest-preview endpoint. **Strategic significance**: closes the typed-schema chronic bulk-action symmetry to match the daily bulk-action typing that rev 86 just closed. The MCP server's chronic-axis bulk-action tooling now has typed contracts on every dimension (per-tag / per-source / per-assignee) — the cadence pattern of 'ship the dashboard primitive + the v1 mirror + the index entry + the OpenAPI typed schema in lockstep' that started rev 37 continues unbroken through rev 87.
- Visual polish — amber chronic palette on the new bulk-ack bars. Cumulative micro-polish (every rev 22+ has carried at least one). The three new chronic bulk-ack bars wear an amber `rgba(184, 109, 36, *)` palette distinct from the rev-60/63/68 daily bulk-ack bars (red palette) so operators tell at a glance which horizon they're acting on. The amber palette matches the rev-71/72 single-item chronic-ack chips so the chronic-axis vocabulary across the dashboard reads with one consistent treatment — chip and bulk bar share the same colour story, just at different scopes. Pairs with the rev-71/72 single-item chronic-ack pulse animation so the operator's eye sees a coherent state machine: chronic chip pulses when fired → bulk bar surfaces in matching palette → ack flashes through both surfaces.
OpenAPI bulk-action coverage, v1 digest preview, activity log glyph expansion
- OpenAPI typed schema for the 8 remaining bulk action endpoints — closes the named rev-85 next-sprint candidate. Rev 85's running state explicitly named 'OpenAPI spec coverage on the few remaining v1 endpoints (the bulk action endpoints)' as the rev-86 candidate. Rev 86 closes the typed-schema bulk-action symmetry on the v1 surface: `/tasks/bulk` (rev-26/35), `/signals/bulk` (rev-33/35), `/memory/bulk-update` (rev-34/35), `/artifacts/bulk` (rev-6/35), `/tasks/cost-spike-ack/bulk` (rev-57), `/sources/cost-spike-ack/bulk` (rev-60), `/cost/by-assignee/cost-spike-ack/bulk` (rev-63), `/cost/by-tag/cost-spike-ack/bulk` (rev-68). All eight previously had self-describing index entries but no typed JSON schema in the OpenAPI 3.1 spec — MCP hosts had to read string descriptions and infer the request shape. Rev 86 makes them all typecheckable. **Strategic significance**: closes the typed-schema bulk-action symmetry across the protocol-bound surface. The cost-axis MCP cluster's daily-ack bulk surface (per-task / per-source / per-assignee / per-tag) is now four axes typed; the four core entity bulk surfaces (tasks / signals / memory / artifacts) are all four typed. The MCP server's bulk-action tooling has nothing left to design on the typed-contract surface.
- POST /api/v1/workspace/digest-preview — closes the dashboard-only digest-test gap from rev 36. New bearer-auth v1 endpoint mirrors the rev-36 dashboard `/api/digest/preview` endpoint. Body `{ userId? }` defaults to workspace owner if omitted (matches rev-77 v1 personal-inbox + rev-30 v1 reaction conventions). Recipient must be an owner/admin of the workspace; viewers can't preview because the digest exposes per-recipient personalisation that low-trust roles shouldn't be able to push to senior recipients' inboxes. Bypasses the per-workspace 22h cron interval gate (matching the dashboard preview path) so MCP hosts iterating on configuration can fire repeatedly without waiting for the next 13:15 UTC tick. The recipient's rev-78/79/80/81 dashboardPrefs (digest opt-out / weekend mute / quiet hours window) are honoured exactly as the production cron would. **Strategic significance**: the digest is the only operator-loaded channel that had no v1 surface to verify since rev 36. Rev 86 closes the gap — the digest email's full rendering path (workspace summary + assigned tasks + mentions + stale + personal cost + personal chronic + personal inbox sections, gated by each recipient's dashboardPrefs) is now exerciseable from the protocol-bound side, so the upcoming MCP server can ship 'test the digest config' tooling without dashboard-only bridge code. Also typed in the OpenAPI 3.1 spec in the same cycle the endpoint ships.
- Activity log glyph + per-kind tinting expansion — six accumulated kinds finally get visual affordance. The rev-35 activity log glyph map has been growing rev-by-rev (cost spike kinds rev-32/55/58/62/67, chronic kinds rev-61/64/70, source auto-pause rev-62) but six accumulated kinds were falling through to the '•' default since they shipped: `stale_tasks` (rev-48 + rev-50 sweep), `share` (rev-10 share-page render), `webhook` (rev-19 outbound delivery), `email` (rev-7 email forwarding + rev-25 digest send), `comment` (rev-26 task discussion), `operator_note` (rev-14 AI direction primitive). Rev 86 adds glyphs (⌛ / ⤴ / ⇝ / ✉ / 💬 / ✎) and per-kind tints (amber / brand-purple / brand-color / teal / brand-purple / amber) so operators reading the activity log now see every kind the codebase actually emits with a per-kind colour signal. **Strategic significance**: closes the rev-35 per-kind scannability symmetry on the activity log. Operators reading a busy log no longer have to read the leading word of every detail line to identify the kind — the glyph + tint is the load-bearing fast-scan affordance, especially valuable on the rev-78 time-bucketed log + rev-21 chip filter + rev-38 keyword search composed view that rev-79 introduced as the four-dimensional read shape.
- Visual polish — pulse animation on dashboard reset success state. Cumulative micro-polish (every rev 22+ has carried at least one). The rev-85 dashboard reset chip's 1.2s brand-green flash now also rings out via a `box-shadow` pulse animation that fades from `rgba(15,118,110,0.42)` to transparent over 1.2s with a subtle 1.02× scale step at 0%. Pairs with the existing color/border-color flash so the chip reads through three distinct visual states on a single tap (neutral / hover-lifted / success-pulsed). Distinct visual confirmation that doesn't require a toast — the pulse is large enough to draw the eye on first reset but small enough not to compete with the rev-12 heartbeat indicator or rev-76 what's-new badge in the same status bar. **Strategic significance**: keeps the rev-by-rev visual-hierarchy discipline alive — every chip + pill + button on the status bar now wears its own state vocabulary (heartbeat = ambient state, what's-new = temporal delta, personal inbox = actionable counter, density = layout, reset = reset-just-fired). Each reads at a different cadence so they never compete.
Dashboard reset chip, per-recipient Slack quiet hours, OpenAPI v1 surface expansion
- Dashboard reset-to-defaults chip — closes the named rev-84 next-sprint candidate. Rev 84's running state explicitly named 'dashboard "reset to defaults" UI affordance' as the natural rev-85 step. Rev 84 shipped the DELETE primitive on `/api/workspace/dashboard-prefs` (and its v1 mirror) but operators could only fire it via curl. Rev 85 closes the loop with an in-app chip beside the rev-39 density toggle in the status bar. One tap → confirm dialog → DELETE → router.refresh, plus aggressive localStorage cache clearing of the rev-77 collapsed panels, rev-82 panel order, and rev-79 collapsed activity buckets so the in-tab UI matches the freshly-defaulted server state without waiting for the rev-78 cross-device merge to flow back through. Visual treatment matches the density toggle's neutral palette so neither competes for the eye over the rev-77 personal inbox pill (amber) or rev-76 what's-new badge (brand-color); a 1.2s brand-green success flash confirms the action took. **Strategic significance**: every operator-tunable layer on the dashboard now has a clean reset path *and* a 1-click affordance — pairs with the rev-39 density toggle as the second compactness primitive (density tunes spacing globally; reset clears every per-user prefs field). Cumulative trust signal: an operator who accidentally hides too many panels can't get stuck.
- Per-recipient Slack quiet hours for assignment + mention-ack pushes — closes the named rev-82 final remaining forward-compat candidate. Rev 82's running state explicitly named 'per-recipient Slack quiet hours' as the final remaining rev-82 forward-compat candidate, noting it was non-trivial because Slack pushes go to a workspace-wide webhook so per-recipient mute would need to map onto per-event surfaces (assignment, mention-ack) rather than the brief push. Rev 85 ships exactly that. New `isUserInPersonalQuietHours()` helper reuses the rev-81 `dashboardPrefs.digestQuietHoursStart/End` window (originally only gated digest emails) and now also gates: (a) the rev-17 task assignment Slack + email push (gated by *assignee's* prefs), (b) the rev-35 mention-ack closure-receipt Slack + email push (gated by the *original mention author's* prefs). Workspace-wide events (rev-1 cycle brief, rev-31 ad-hoc push, rev-32+ cost-spike pings) deliberately keep the rev-15 workspace-level quiet hours since they fan out to a workspace-wide channel. The rev-35 outbound `task.mention_acked` event fires regardless of personal quiet hours since downstream integrations are an integration concern not a recipient concern. Best-effort never-block-the-send: every error path defaults to 'send the push' so a configuration glitch can't silently drop pings. **Strategic significance**: closes the four-axis operator-respect cluster from rev 80/81 (workspace on/off → personal sections → weekend mute → quiet hours window) by extending the *fourth axis* from email-only to per-recipient Slack pushes. An operator who set 22:00→07:00 muting now gets coverage on both digest email AND per-recipient task push channels.
- OpenAPI 3.1 spec expansion to load-bearing v1 endpoints — 11 more endpoints typed. Continued the rev-by-rev OpenAPI coverage expansion thread. Rev 85 types: `/sources` GET + POST (rev 13 — load-bearing core entity), `/tasks/{id}/blockers` PUT (rev 37 — task-dependency primitive), `/tasks/{id}/notes` GET + POST (rev 76 — operator-direction channel), `/tasks/stale` GET (rev 48 — diagnostic surface), `/tasks/auto-archived` GET (rev 50 — audit-trail closure), `/tasks/{id}/renew` POST (rev 50 — operator counter-action), `/tasks/{id}/sources` GET (rev 43 — input transparency), `/tasks/{id}/source-memory` GET (rev 44 — memory transparency), `/tasks/{id}/timeline` GET (rev 41 — unified per-task feed), `/artifacts/{id}/sources` GET (rev 41 — procurement evidence), `/artifacts/{id}/versions` GET (rev 44 — revision lineage), `/sources/cost-spikes` GET (rev 58), `/sources/{id}/cost-trajectory` GET (rev 60), `/sources/bulk` POST (rev 36 — closes the five-entity bulk symmetry on the protocol surface), `/badge.svg` GET (rev 19 — public marketing endpoint, no auth header). **Strategic significance**: the typed-schema surface now covers the input-transparency cluster (artifact + task sources + source-memory), the task-lifecycle audit cluster (stale + auto-archived + renew), task dependencies (blockers), the operator-direction channel (notes), and the public marketing badge — five new surface clusters that MCP-host code generators can typecheck against. The MCP server (Q3 #1) gains typed contracts for the most operator-loaded surfaces beyond the cost-axis cluster (which rev 79 typed) and the integration observability cluster (which rev 84 typed).
- Visual polish — focus-visible ring on reset chip + brand-green success flash. Cumulative micro-polish (every rev 22+ has carried one). The new rev-85 reset chip wears the same neutral-palette base as the rev-39 density toggle so the two layout-tuning affordances read as siblings, plus a `.is-success` modifier that flashes brand-green for 1.2s after a successful reset before reverting — distinct visual confirmation that doesn't require a toast. New `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. Hover state strengthens both the density toggle AND reset chip's border-color by stepping from rgba(11,26,42,0.08) to 0.16 — same step the rev-22+ design language uses for tactile click affordance. **Strategic significance**: the status bar instrument cluster now reads as seven instruments deep — heartbeat (rev 12), desk health (rev 13), cycle performance (rev 14), read-only pill (rev 16), density toggle (rev 39), what's-new badge (rev 76), personal inbox (rev 77), and now reset chip (rev 85). The rev-by-rev visual-hierarchy discipline is what keeps the status bar from drifting into instrument-stack chaos.
Per-row cost-panel column visibility, dashboard-prefs reset, OpenAPI integration observability
- Per-row column visibility on cost panels — closes one of the two remaining rev-82 forward-compat candidates. Rev 82 named four forward-compat candidates for dashboard-prefs: panel order, default sort, per-row column visibility on cost panels, per-recipient Slack quiet hours. Rev 83 closed the first two; rev 84 closes the third. New optional `costPanelColumns?: { source?: string[]; assignee?: string[]; tag?: string[] }` field on `DashboardPrefs`. Each list is *subtractive* (defaults visible) — operators with dense cost panels (50+ rows after a busy quarter) can hide trajectory sparklines, spike pills, token counts, or task counts they don't actively triage on. Recognised column ids: `trajectory` (rev-60 sparkline), `spike-pill` (rev-58 ⚡ pill), `tokens` (meta tokens count), `task-count` (meta task count). Per-axis (source / assignee / tag) so an operator who wants the trajectory visible on the per-task axis but hidden on the per-source axis can express that. Capped at 30 hidden columns per axis. Wired into all three cost panels (CostBySourcePanel, CostByAssigneePanel, CostByTagPanel) so the schema field is load-bearing on the dashboard surface immediately. Also wired into both `/api/workspace/dashboard-prefs` and `/api/v1/workspace/dashboard-prefs` Zod schemas in lockstep, plus typed in the OpenAPI 3.1 spec. **Strategic significance**: the dashboard now has all four axes of dashboard-prefs forward-compat closed across rev 82/83/84 — panel order, default sort, per-axis column visibility (rev 84). The remaining rev-82 candidate (per-recipient Slack quiet hours) sits naturally for rev 85+ since Slack pushes go to a workspace-wide webhook so per-recipient mute would need to map onto per-event surfaces like assignment/mention-ack rather than the brief push.
- DELETE /api/workspace/dashboard-prefs + v1 mirror — reset all preferences to defaults. New primitive that pairs with the rev-78 multi-device sync as the 'back to defaults' affordance. Until rev 84, an operator who accidentally hid too many panels via rev-77 collapse + rev-82 panelOrder + rev-84 costPanelColumns had to enumerate each field individually to restore. Rev 84 collapses that to one DELETE. Clears every rev-77/79/82/83/84 prefs field so all panels show, panels are in default order, active-work sort is smart, and all cost-panel columns are visible. New `DELETE /api/workspace/dashboard-prefs` (viewer+ — operator can only reset their own prefs) + matching `DELETE /api/v1/workspace/dashboard-prefs?userId=…` v1 mirror in lockstep. **Strategic significance**: cumulative trust signal — every operator-tunable layer on the dashboard now has a clean reset path. Foundation for a rev-85+ 'reset to defaults' UI affordance.
- OpenAPI spec coverage expansion to integration observability + procurement evidence cluster — closes the named rev-83 next-sprint candidate. Rev 83's running state explicitly named 'OpenAPI spec coverage expansion to the outbound deliveries + retry endpoints' as the rev-84 candidate. Rev 84 closes that — and pairs it with the rest of the integration + procurement-evidence cluster: `/outbound/deliveries` GET + `/outbound/deliveries/{id}/retry` POST (rev-46 v1 endpoints with no typed schema), `/workspace/export` GET (rev-45 — full procurement takeaway), `/workspace/import` POST (rev-41 — categorical append), `/workspace/cost-export` GET (rev-62 — cost CSV), `/workspace/summary` GET (rev-43 — procurement panel), `/workspace/tag-search` GET (rev-40 — cross-entity drill-down), `/sources/preview` POST (rev-40 — silent-failure prevention), `/workspace/dashboard-prefs` DELETE (rev-84 — new). The OpenAPI spec now covers the integration observability cluster, the procurement evidence cluster, the cost-axis cluster, the discussion + reactions + focus surface, the workspace-config cluster, the team-management surface, and the load-bearing core. **Strategic significance**: every dashboard primitive that operates on the protocol-bound side now has a typed schema for code generators + MCP hosts. The MCP server (Q3 #1) gains typed contracts for outbound observability + procurement evidence — the two surfaces procurement-conscious B2B buyers care about most.
- Cumulative cost-panel polish — uniform row hover + meta typography across the three cost panels. Until rev 84 each cost panel had slightly different row padding + meta spacing. The new rev-84 hidden-column primitive (`dashboardPrefs.costPanelColumns`) means row content is now genuinely variable per operator preference, so the underlying typography needs to compose cleanly across any subset of {trajectory, spike-pill, tokens, task-count}. New unified `.ld-cost-{axis}-meta` styling (flex + gap + uniform color + min-height) so a row with all four columns shown reads identically to a row with only one. Plus subtle row-hover treatment on all three cost panels so they read as scannable rather than static — same hover lift the rev-22+ design-language thread established. Cumulative micro-polish (every rev 22+ has carried at least one).
Panel order UI affordance, default Active Work sort, per-subscription outbound test, outbound Zod fix
- Panel order UI affordance — up/down chevrons close the named rev-82 next-sprint candidate. Rev 82 shipped the `panelOrder` schema + protocol foundation; rev 83 closes the loop with the in-app affordance. New ▲▼ buttons next to the rev-77 collapse chevron on every panel head let operators promote a panel up the order or demote it down. Visual ordering applied via CSS `order` (works on grid + flex children) so we don't mutate the DOM tree — panels in `panelOrder` get `order: idx - 60` (negative offset so they sort BEFORE unordered panels); panels not in the array stay at their default DOM position. Persisted to localStorage (sync render) + the rev-82 `panelOrder` JSONB on `workspace_member.dashboardPrefs` (cross-device sync). The control group fades to 0.55 opacity by default and emphasises on panel hover so the controls don't scream from every panel head when the user isn't interacting. Pairs with the rev-39 density toggle (panel padding) + rev-77/78 collapse (panel inventory) as the third axis of dashboard tuning. The dashboard now has all three axes: padding (rev 39), inventory (rev 77/78), and order (rev 83).
- Per-user default sort for the Active Work queue — closes the named rev-82 forward-compat candidate. Rev 82's running state named four forward-compat candidates for dashboard-prefs: panel order, default sort, per-row column visibility on cost panels, per-recipient Slack quiet hours. Rev 83 closes the second. New optional `activeWorkSort?: 'smart' | 'priority' | 'due' | 'recent'` field on `DashboardPrefs`. Default 'smart' preserves the rev-29/22/21 default behaviour (focus → due → priority → updated). The other three give power-users direct control: 'priority' sorts by manual priority desc, 'due' sorts by deadline asc with no-due last, 'recent' sorts by most-recently-touched first (kanban-style). Pinned (rev 23) + needs_input tasks always come first regardless of sort mode — they're operator-attention buckets, not a sort axis. New `ActiveWorkSort` client component renders a tiny dropdown right above the Active Work panel head (doesn't need a settings section). Wired into both `/api/workspace/dashboard-prefs` and `/api/v1/workspace/dashboard-prefs` Zod schemas in lockstep, plus typed in the OpenAPI 3.1 spec. **Strategic significance**: closes the second rev-82 forward-compat candidate. Power-users with strict deadline workflows now get due-first ordering with one click; teams running kanban-style backlogs get recently-updated first. The smart default stays correct for everyone else.
- Per-subscription outbound webhook test (closes rev-19 + rev-17 test-surface gap). Rev 17 shipped the single-URL outbound test against `workspace.outboundWebhookUrl`. Rev 19 made outbound a real per-event router (subscriptions, delivery log, retry). Until rev 83 there was no way to test individual rev-19 subscriptions — the rev-17 test only fired the legacy URL, leaving operators with rev-19 subscriptions unable to verify wiring without waiting for a real production event. Rev 83 closes the gap. New `testOutboundSubscription()` helper in `src/lib/outbound.ts` fires an `artifact.test` payload to the specific subscription's URL using the workspace's `webhookSigningSecret` (so the HMAC signature matches what the endpoint will see in production). Records the attempt in `outbound_webhook_delivery` so the rev-19 delivery log shows it alongside production deliveries. New `POST /api/workspace/outbound/{id}/test` route + matching `POST /api/v1/outbound/subscriptions/{id}/test` v1 mirror in lockstep. New 'Send test' button on every subscription row in the OutboundSubscriptions panel (gated to active subscriptions). **Strategic significance**: closes the per-subscription test parity gap on every channel — UI, dashboard API, v1/MCP, OpenAPI. The cadence pattern from rev 37 onward continues: ship the dashboard primitive + the v1 mirror + the index entry + the OpenAPI typed schema in lockstep.
- Bug fix: dashboard outbound subscription Zod schemas synced to ALL_OUTBOUND_EVENTS. Found while shipping the rev-83 per-subscription test endpoint: the dashboard's `POST /api/workspace/outbound` and `PATCH /api/workspace/outbound/{id}` Zod schemas had been hard-coded to four rev-18 events (`artifact.ready`, `artifact.approved`, `signal.created`, `task.assigned`) since rev 19. Meanwhile the rev-73 grouped picker UI surfaces 30+ events including the entire cost-spike alarm cluster, chronic warnings, and closure receipts. Operators picking newer events in the picker hit a silent 400 'Invalid subscription payload.' on save. The bug was masked because the rev-81 v1 mirror used `ALL_OUTBOUND_EVENTS` correctly, so MCP hosts could create subscriptions but dashboard users couldn't. Rev 83 fixes both dashboard endpoints to mirror `ALL_OUTBOUND_EVENTS` exactly. **Strategic significance**: silent bug fix. Until rev 83, every operator who tried to subscribe to a rev-26+ event via the dashboard was hitting the 400 silently — the picker showed all 30+ events as available, the save call failed, the operator's subscription never landed.
v1 desk-state control (loop + pause-until), workspace-config OpenAPI coverage, panel order forward-compat
- v1 desk-state control — PATCH /api/v1/workspace/loop + /api/v1/workspace/pause-until (closes long-outstanding parity gap). Until rev 82 the rev-1 desk-state primitive (loop on/paused/off) and the rev-21 scheduled-pause primitive (pauseUntilAt with auto-resume up to 14 days) were dashboard-only on the protocol-bound surface. An MCP host driving the desk could read the loop state via `GET /api/v1/workspace` but couldn't change it without a custom dashboard session — the rev-1 primitive was 81 revs old without v1 parity. Rev 82 closes both gaps: `PATCH /api/v1/workspace/loop` accepts `{ loopState: 'on' | 'paused' | 'off' }`; `PATCH /api/v1/workspace/pause-until` accepts `{ pauseUntilAt: ISO | null }` with the same 14-day max window the dashboard route enforces. Both reuse the existing `updateWorkspaceLoopState` and `setWorkspacePauseUntil` helpers so the two surfaces share one server-side implementation. **Strategic significance**: the most load-bearing missing v1 primitive shipped today. An MCP-host watcher agent that detects an anomaly and wants to pause the desk while a human investigates can now do so without operator intervention — pairs with the rev-21 in-app cost-cap warning + rev-32 cost-spike alarm to give the entire desk-state control surface programmatic parity.
- OpenAPI 3.1 spec expanded to workspace-config cluster (closes named rev-81 next-sprint candidate). Rev 81's running state explicitly named 'OpenAPI spec coverage expansion to the workspace-config endpoints' as the rev-82 candidate, citing the cluster of `/workspace` profile + loop + integrations + Slack quiet-hours + cost-cap + auto-archive endpoints that have shipped over many revs without typed schemas. Rev 82 closes that. The spec now types: `/workspace/loop` PATCH (rev-1 primitive), `/workspace/pause-until` PATCH (rev 21), `/workspace/auto-archive-config` GET/PUT (rev 49), `/workspace/cost-spike-config` GET/PUT (rev 56), `/workspace/source-cost-spike-config` GET/PUT (rev 59), `/workspace/source-chronic-spike-config` GET/PUT (rev 62), `/workspace/tag-cost-spike-config` GET/PUT (rev 69), plus the new rev-82 `panelOrder` field on dashboard-prefs. **Strategic significance**: the workspace-config cluster is the operator's primary defensive control surface — daily cost cap, spike auto-pause across all four axes (per-task, per-source, per-tag), and stale-task auto-archive. MCP hosts wrapping `/api/v1` for autonomous defensive control now have typed contracts for every primitive in the cluster. Coverage continues to grow rev-by-rev; the full self-describing index at `/api/v1` remains authoritative for the complete endpoint list while the OpenAPI spec is the *typed* subset for code generators.
- Per-user panel ordering on dashboard-prefs (closes rev-81 forward-compat candidate). Rev 81's running state named four forward-compat candidates for the rev-79/80/81 dashboardPrefs JSONB shape: panel order, default sort, per-row column visibility on cost panels, per-recipient Slack quiet hours. Rev 82 closes the first one (and the most load-bearing for power-user dashboard density). New optional `panelOrder?: string[]` field on the existing `DashboardPrefs` TS type — same multi-device sync mechanism as rev 78 (localStorage write-through cache for sync render, server JSONB as source of truth for cross-device drift). Capped at 60 entries with the same dedup + drop-empty normalisation as `collapsedPanels`. Panels not present in the array fall back to their default dashboard position so a partial reorder is supported (operators can pin Today + Approvals to the top without enumerating every panel id). Wired into both the dashboard `/api/workspace/dashboard-prefs` and v1 `/api/v1/workspace/dashboard-prefs` Zod schemas in lockstep, plus typed in the OpenAPI 3.1 spec. **Strategic significance**: the dashboard has accumulated 30+ panels over 80 revs. Rev 78 added per-panel collapse; rev 82 adds the layout primitive's TS type and v1 surface so the upcoming UI work has nothing left to wire on the protocol side. The third axis — default sort — and the fourth — per-recipient Slack quiet hours — remain natural rev-83+ candidates.
- v1 self-describing index documents the new endpoints. The rev-82 v1 endpoints (`PATCH /api/v1/workspace/loop`, `PATCH /api/v1/workspace/pause-until`) are listed in the `/api/v1` self-describing endpoint index alongside the existing 100+ entries. The cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror + the index entry + the OpenAPI typed schema in lockstep) holds. **Strategic significance**: the v1 surface remains *both* fully self-describing for human developers reading the index *and* fully typed for code generators reading the OpenAPI spec. Until rev 82 the rev-1 desk-state primitive was the longest-outstanding v1-vs-dashboard parity gap; rev 82 closes it.
v1 members + invites + outbound subscriptions, per-recipient digest quiet-hours, OpenAPI invites/members coverage
- v1 members + invites surface — closes the longest-outstanding v1-vs-dashboard parity gap (66 revs old). The rev-15 invite flow has been dashboard-only for 66 revs. Until rev 81 an MCP host driving the desk programmatically could read every workspace surface but couldn't list the team or invite a teammate — the longest-outstanding v1 parity gap on the open list. Rev 81 closes it: `GET /api/v1/workspace/members` returns members + pending invites; `POST /api/v1/workspace/invites` invites by email (existing users join immediately, unregistered emails queue and auto-attach on signup); `DELETE /api/v1/workspace/members/{userId}` removes; `DELETE /api/v1/workspace/invites/{inviteId}` revokes. Each mutation accepts optional `asUserId` to attribute the action; defaults to workspace owner. The helper validates that asUserId is an owner/admin so v1 bearer-auth callers can't escalate via asUserId. **Strategic significance**: the upcoming MCP server's team-management tooling now has nothing left to design on the rev-15 surface — protocol-translation work only. The pattern across rev 37 onwards (ship the dashboard primitive + the v1 mirror in lockstep) finally catches up the oldest dashboard-only mutation surface on the codebase.
- v1 outbound subscriptions surface — closes the rev-19 v1 parity gap on the subscription-management side. Rev 19 turned outbound delivery into a real router (per-event subscriptions + delivery log + retry); rev 46 mirrored the *delivery log + retry* on v1; the *subscription management* surface (list, create, update, delete) was still dashboard-only. Until rev 81 an MCP host could see *what* fired but couldn't say 'wire `signal.created` to my CRM.' Rev 81 closes that: `GET /api/v1/outbound/subscriptions` lists subscriptions + the full ALL_OUTBOUND_EVENTS vocabulary; `POST` creates; `PATCH/DELETE /api/v1/outbound/subscriptions/{id}` updates and removes. The GET response carries the `availableEvents` array so MCP hosts can render an event picker without hardcoding the vocabulary. **Strategic significance**: pairs with rev-46 deliveries + retry as the full outbound observability + control surface on the protocol-bound side. The cost-axis MCP cluster has been complete for revs; the *integration* MCP cluster now closes too.
- Per-recipient digest quiet-hours window — fourth axis of operator-respect on the daily digest. Rev 80 closed three axes of operator-respect: workspace on/off (rev 5), per-recipient personal-section opt-out (rev 79), per-recipient weekend mute (rev 80). The fourth — *hour-of-day* — was the missing edge. The digest cron fires once at 13:15 UTC; in NYC EST that's 8:15am, in Tokyo it's 22:15. Recipients in late-evening regions had no way to skip the cron without disabling the digest entirely. Rev 81 adds `digestQuietHoursStart` + `digestQuietHoursEnd` (both 0-23 in workspace timezone, both nullable) on `workspace_member.dashboardPrefs`. When BOTH ends are set AND the workspace's local hour at fire time falls inside the [start, end) window, the cron skips THIS recipient. Wrap-around windows supported (e.g. 22 → 7 means 22, 23, 0-6 inclusive). Activity log records exactly how many recipients were quiet-hour-muted so admins troubleshooting 'why didn't I get the digest?' have the answer in the audit trail. UI in the integrations panel directly under the rev-80 weekend-mute toggle so all four digest controls (workspace on/off, personal-sections opt-out, weekend mute, quiet-hours window) read as siblings. v1 mirror via the existing `/api/v1/workspace/dashboard-prefs` GET/PUT pair; OpenAPI 3.1 spec types both new fields. **Strategic significance**: especially load-bearing for cross-timezone teams. Rev-80 weekend mute + rev-81 quiet-hours together give recipients fine-grained control over WHEN the digest reaches their inbox without the workspace owner having to coordinate cron schedules across regions.
- OpenAPI 3.1 spec expanded to invites/members + outbound subscriptions (closes the named rev-80 next-sprint candidate). Rev 80's running state explicitly named 'OpenAPI spec coverage expansion to the rev-15/16 invites + members surface' as the rev-81 candidate. Rev 81 closes that — and adds the rev-19 outbound subscription endpoints in the same cycle since they're the natural complement to the rev-46 deliveries log + retry endpoints that already had v1 parity. The spec now types: `/workspace/members` GET, `/workspace/invites` POST, `/workspace/members/{userId}` DELETE, `/workspace/invites/{inviteId}` DELETE, `/outbound/subscriptions` GET/POST, `/outbound/subscriptions/{id}` PATCH/DELETE, plus the rev-81 `digestQuietHoursStart` + `digestQuietHoursEnd` fields on dashboard-prefs. **Strategic significance**: the OpenAPI spec now covers the *team management* + *integration management* surfaces in addition to the previously-typed cost-axis, discussion, reactions, focus, and core CRUD clusters. MCP hosts wrapping `/api/v1` for collaboration + integration tooling now have typed contracts for the load-bearing 'add a teammate, route an event to my CRM' workflows. Coverage continues to grow rev-by-rev; the full self-describing index at `/api/v1` remains authoritative for the complete endpoint list.
Per-recipient weekend mute, workspace reactions summary, OpenAPI discussion + reactions + focus surface coverage
- Per-recipient weekend-mute for the daily digest (closes the third axis of operator-respect). Multi-operator desks accumulate operators with different work rhythms. Some are heads-down on weekends; others want to disconnect. Until rev 80 the only digest controls were workspace-wide on/off (rev 5) and per-recipient personal-section opt-out (rev 79) — but neither covered the temporal axis. Rev 80 adds the third axis: `digestQuietWeekends` on `workspace_member.dashboardPrefs`. Default false (digest sends every day). When true, the cron skips THIS recipient on Saturday + Sunday in the workspace timezone — Mon-Fri delivery is unaffected. Persisted on the existing JSONB column (no schema migration). Activity log records exactly how many recipients were weekend-muted on a given day so admins can audit 'why didn't I get the digest yesterday?'. UI in the integrations panel directly under the rev-79 personal-sections toggle so all three digest controls (workspace on/off, personal-sections opt-out, weekend mute) read as siblings. **Strategic significance**: closes the operator-respect cluster on three orthogonal axes — workspace-level chat-channel quiet (rev 15 Slack quiet hours), per-recipient personal-section opt-out (rev 79), per-recipient weekend mute (rev 80). The per-recipient temporal axis is the most-requested ergonomic feature from operators with strong weekend boundaries.
- Workspace reactions summary — third descriptive lens after rev-28 tag insights + rev-31 focus history. Comments (rev 29), artifacts (rev 33), memory entries (rev 33), and signals (rev 34) have all carried the same `Record<emoji, userId[]>` reaction shape with the same five-emoji vocabulary (👍 / 👀 / 🎯 / ❤️ / 🚀) since rev 34. Until rev 80 every reaction was scoped to its single entity — operators could see which comment got 👍 but had no surface for 'what content is resonating across the workspace this week?'. Rev 80 closes that with one aggregate read: `getWorkspaceReactionsSummary()` sums every reaction (deduplicated per-user-per-emoji-per-entity) across the four reactable surfaces and returns per-emoji workspace-wide totals + top-N reacted entities with parent context. New `GET /api/v1/workspace/reactions-summary?topLimit=8` v1 endpoint + matching dashboard endpoint + new `ReactionsSummaryPanel` client component mounted as a sidebar panel below the rev-28 tag insights and rev-39 tag manager. Hidden when the workspace has no reactions yet. **Strategic significance**: pairs with rev-28 tag insights (descriptive: what tags is the team working on?) and rev-31 focus history (prescriptive: what themes does the team prioritise?) as the third descriptive lens — what content is the team reacting to? Pure derived state — no schema, no migration, no new persistence.
- OpenAPI 3.1 spec expanded to the discussion + reactions + focus surface (closes the named rev-79 next-sprint candidate). Rev 78 shipped the OpenAPI 3.1 spec covering the load-bearing core (workspace, signals, artifacts, tasks, memory, search, today, stats). Rev 79 expanded coverage to the cost-axis cluster + chronic-ack endpoints + dashboard-prefs. Rev 79's running state explicitly named expanding to 'the rev-26 comments + rev-29 reactions + rev-30 mentions inbox endpoints' as the rev-80 candidate. Rev 80 closes that — the OpenAPI spec now types: `/tasks/{taskId}/comments` GET/POST (rev 26-28), `/tasks/{taskId}/comments/{commentId}/reaction` POST (rev 29), `/artifacts|memory|signals/{id}/reaction` POST (rev 33-34), `/workspace/focus-tags` GET/PUT (rev 29-30), `/workspace/focus-history` GET (rev 31), `/workspace/insights` GET (rev 28), the new `/workspace/reactions-summary` GET (rev 80), and the rev-80 `digestQuietWeekends` field on dashboard-prefs. **Strategic significance**: the discussion + reactions + focus cluster is the most operator-loaded surface on the desk that wasn't typed until rev 80. MCP hosts wrapping `/api/v1` for collaboration tooling (post a comment, react to an artifact, set focus tags from chat) now have typed contracts for every primitive. Coverage continues to grow rev-by-rev; the full self-describing index at `/api/v1` remains authoritative for the complete endpoint list.
- Visual polish — reactions summary panel + cumulative micro-polish. New `.ld-reactions-summary-*` CSS uses the same brand-accent palette as the rev-28 tag-insights panel so the three descriptive panels (tag insights / tag manager / reactions summary) read as a coherent visual cluster. Per-emoji tally chips with hover lift; per-entity rows with kind-tinted left-border accents (purple for comments, brand-color for artifacts, amber for memory, teal for signals) so operators can scan reactions by entity-type at a glance. Click any row to jump to the matching panel. Cumulative micro-polish — every rev 22+ has carried one — and rev 80's polish is load-bearing because the new sidebar panel needed visual distinction without competing with the rev-28 + rev-31 panels above it.
v1 dashboard-prefs mirror + per-recipient digest opt-out + activity-log bucket collapse + OpenAPI cost-axis coverage
- `GET/PUT /api/v1/workspace/dashboard-prefs` — bearer-auth v1 mirror of the rev-78 dashboard endpoint. Closes the v1 parity gap on the rev-78 dashboard prefs primitive in the same cycle pattern that has been running since rev 37 — every dashboard primitive ships with the corresponding v1 path within one rev. Until rev 79 the rev-78 multi-device sync of panel collapse state was dashboard-only on the protocol-bound side; an MCP host driving the desk could read every other workspace member field but couldn't sync the per-user dashboard preferences. Caller specifies `userId` via `?userId=`; defaults to workspace owner so a quick MCP probe works without enumerating members first. Forward-compat: rev 79 also extends the prefs JSONB shape with `digestPersonalSections` (per-recipient digest opt-out) and `collapsedActivityBuckets` (multi-device sync of the rev-78 activity-log time-bucket UI). MCP hosts can sync all three through the same endpoint.
- Per-recipient digest opt-out for the rev-25 / 31 / 49 / 53 / 76 / 78 personal sections. Multi-operator desks accumulate operators with different digest preferences — some admins are already on top of their queue and want only the workspace-shared summary; others want the full personal-section experience. Until rev 79 the only digest control was the workspace-wide `digestEmail` boolean. Rev 79 adds the per-recipient `digestPersonalSections` toggle (default true). When false, the daily digest skips the rev-25 assignee + rev-31 mentions + rev-49 personal stale + rev-53 personal cost + rev-76 personal chronic + rev-78 personal inbox sections for THAT recipient — they still get the workspace-shared summary, fresh signal, and approval-waiting blocks. Persisted on `workspace_member.dashboardPrefs` so multi-operator teams have per-recipient control without a new schema column. UI in the integrations panel right under the existing digest on/off toggle. **Strategic significance**: closes the per-recipient personalisation symmetry on the OPT-OUT axis. The rev-25/31/49/53/76/78 cycle built six per-recipient personal sections; rev 79 makes them recipient-controllable.
- Per-bucket collapse on the rev-78 time-bucketed activity log (multi-device sync). Rev 78 introduced Today / Yesterday / Earlier this week / Older buckets on the activity log. Rev 79 makes each bucket header clickable to collapse independently. Persisted via `workspace_member.dashboardPrefs.collapsedActivityBuckets` so the operator's '/Older is always collapsed' preference follows them across machines. localStorage stays as the write-through cache for sync render. Pairs with the rev-77 panel collapse + rev-78 time-bucket UI as the operator's third axis of dashboard density control after rev-39 layout density. The chevron + hover state read as ambient affordance rather than a primary action — the bucket label remains the visual focus. **Strategic significance**: cumulative micro-polish (every rev 22+ has carried at least one) — but rev 79's polish is load-bearing because the rev-78 buckets accumulated visible-row volume that operators who only care about today's activity were forced to scroll past.
- OpenAPI 3.1 spec coverage expanded to the cost-axis cluster + chronic-ack endpoints + dashboard-prefs (closes named rev-78 follow-up). Rev 78 shipped the curated OpenAPI 3.1 subset covering the most load-bearing core (workspace, signals, artifacts, tasks, memory, search, today, stats, cost-today). Rev 79 closes the named rev-78 next-sprint candidate ('OpenAPI spec coverage expansion') by extending typed schemas to: the full cost-axis cluster (per-task cost + trajectory, top-cost-tasks, per-source / per-assignee / per-tag breakdowns with trajectory variants), every cost-spike alarm endpoint across the rev-32/55/58/62/67 five-axis cluster, the daily-ack endpoints across the per-task / per-source / per-assignee / per-tag axes, the chronic-ack endpoints across all three axes where chronic makes sense (per-tag rev 71, per-source rev 72, per-assignee rev 72), the rev-73 chronic-acks listing endpoint, and the rev-79 dashboard-prefs GET/PUT pair. **Strategic significance**: the upcoming MCP server wrapping `/api/v1` (Q3 #1) gains typed contracts for the cost-axis cluster — the most schema-rich subset of the v1 surface and the area MCP hosts most need typed parameter validation for. Coverage continues to grow rev-by-rev; the full self-describing index at `/api/v1` remains authoritative for the complete endpoint list.
Personal inbox in daily digest, multi-device sync for panel collapse, OpenAPI 3.1 spec, and time-bucketed activity log
- Personal action inbox section in the daily digest email. Closes the named rev-77 next-sprint candidate ('per-recipient digest entry for the personal inbox'). Rev 77 surfaced the operator's actionable inbox in the dashboard status bar via the `PersonalInboxPill` (unacked @-mentions + due-soon assigned + stale assigned). Rev 78 mirrors it on the email channel so operators who don't open the dashboard before noon still get the same one-line 'what waits for me right now?' answer. New `buildPersonalInboxSection()` helper in `src/lib/digest.ts` reuses the rev-77 `getPersonalInbox()` helper verbatim — pure derived state, no new query. Renders FIRST among the per-recipient sections (above rev-25 assignee, rev-31 mentions, rev-48 stale) since it's the actionable shortlist; the longer sections still render below for the complete view. Hidden when the operator's inbox total is zero so quiet days never read as nag-spam. Plumbed through both the cron production loop and the rev-36 admin preview path. **Strategic significance**: closes the per-recipient personalisation symmetry across rev-25 assignee + rev-31 mentions + rev-49 personal stale + rev-53 personal cost + rev-76 personal chronic + rev-78 personal inbox. The daily digest is now fully personalised across every load-bearing per-recipient surface — the actionable axis joins the temporal + cost + chronic axes that already had per-recipient digest sections.
- Multi-device sync for panel collapse via `workspace_member.dashboardPrefs` JSONB. Closes the named rev-77 next-sprint candidate ('dashboard panel collapse multi-device sync via `workspace_member.dashboardPrefs` JSONB'). Rev 77 ships per-panel collapse state in localStorage; rev 78 layers a workspace-shared canonical store on top so the same operator's tuned panel inventory follows them across devices. New JSONB column on `workspace_member` (currently carries `collapsedPanels: string[]` only; future revs can add panel order, default sort, hidden columns without a new column). New `getDashboardPrefs()` / `setDashboardPrefs()` helpers + `GET/PUT /api/workspace/dashboard-prefs` route. The rev-77 `PanelCollapsibility` component is extended to (a) read localStorage first for sync render, (b) fetch the server prefs and merge by union (a panel collapsed on either device stays collapsed — safer default than 'last-write-wins'), (c) fire-and-forget the PUT on every toggle so the server stays in sync. localStorage stays as the write-through cache; server is the source of truth for cross-device drift. **Strategic significance**: pairs with the rev-39 density toggle as the second compactness primitive. Until rev 78 a power-user with a tuned dashboard had to re-collapse the same panels on every machine they logged in on. Rev 78 closes that gap.
- `GET /api/v1/openapi.json` — machine-readable OpenAPI 3.1 spec for the v1 surface. Until rev 78 the v1 surface was self-describing only via `/api/v1` (string descriptions of every endpoint) — fine for human developers reading curl examples but not consumable by code generators. Rev 78 ships an OpenAPI 3.1 spec describing the most load-bearing endpoints (workspace, signals, artifacts, tasks, memory, search, today, stats, runs, cost-today, whats-new, personal-inbox) with typed request/response schemas. Public endpoint — no auth required to read the spec since the spec describes the auth model itself. 5-minute browser cache + 30-minute CDN cache so the upstream MCP host can poll without thrashing. **Strategic significance**: OpenAPI 3.1 is the published Anthropic / Linear / Stripe / GitHub pattern for surfacing APIs to MCP hosts and code generators. Hosts that read this spec can typecheck their tool calls, generate client SDKs in any language, and (in Anthropic's case) auto-generate MCP tool definitions from path + parameter metadata. Curated subset to start; rev-by-rev the spec grows toward full coverage. The full self-describing index at `/api/v1` remains authoritative for the complete endpoint list. Q3 #1 priority (the actual MCP server wrapping `/api/v1`) gains a typed contract surface.
- Time-bucketed activity log (Today / Yesterday / Earlier this week / Older). The activity log has accumulated 14+ kinds across 35+ revs (per-kind glyphs + tinting in rev 35, chip filter in rev 21, in-place keyword search in rev 38, 'only new since last visit' in rev 76). Until rev 78 the visible rows rendered as a flat scroll regardless of horizon — operators reading a busy log had to mentally compute 'is this from today or yesterday?' from the timestamp on every row. Rev 78 buckets the visible rows into four time horizons after the chip + keyword + only-new filters apply, so the buckets reflect what the operator chose to see. Bucket headers are quiet uppercase typography — ambient labels rather than competing with the row content. Activates only when there are 8+ visible rows so quiet workspaces don't see clutter. **Strategic significance**: cumulative diversification of the activity log surface. Pairs with rev-21 chip filter (categorical scoping), rev-38 keyword search (free-text scoping), rev-76 only-new toggle (temporal-since-visit scoping). Rev 78 adds the *temporal-bucketed* dimension. The log now reads at four orthogonal axes without the operator needing to flip between modes.
Personal action inbox status-bar pill, per-panel collapse persistence, and v1 mirrors for whats-new + personal-inbox
- Personal action inbox status-bar pill. New `getPersonalInbox()` helper aggregates the current operator's *actionable* inbox into one number — unacked self-mentions (rev 27 mention primitive + rev 34 workspace-shared ack) + per-recipient stale assigned tasks (rev 47 detector at 5d threshold + rev 49 per-recipient scoping) + due-soon-and-mine open tasks (rev 22 dueAt within 24h or already overdue, scoped to assignee). New `PersonalInboxPill` client component sits in the dashboard status bar alongside the rev-76 WhatsNewBadge and rev-39 DensityToggle; clicking opens a compact popover listing each axis with click-to-jump anchors to the matching panel (mentions / tasks / stale-tasks). Hidden when total is zero. **Strategic significance**: distinct from the rev-76 WhatsNewBadge (temporal: what changed since I was last here) by being *actionable* — things that wait for THIS operator's attention right now. The dashboard status-bar instrument cluster now reads six instruments deep: rev-12 heartbeat (loop state) + rev-13 desk health (trust score) + rev-14 cycle performance (latency) + rev-16 read-only pill (auth state) + rev-39 density toggle (layout) + rev-76 what's-new badge (temporal delta) + rev-77 personal inbox (actionable inbox). Pure derived state — no schema, no migration. Multi-operator teams catching up after a day off get the answer to 'what waits for ME?' without scrolling every panel.
- Per-panel collapse / expand persistence. The dashboard has accumulated 30+ panels over 76 revs. Rev 39 added the density toggle (compact mode tightens spacing). Rev 77 closes the complementary axis: per-panel *visibility* — operators can now collapse panels they don't need today via a chevron in every `.app-panel-head`. Persisted in localStorage under `loopdesk:collapsed-panels:<workspaceId>` so the choice survives across sessions and is per-workspace. New `PanelCollapsibility` client component scans every `.app-panel[id]` on mount, injects the chevron, binds click → toggle `.is-collapsed` class + write storage. CSS handles the visual collapse via `.app-panel.is-collapsed > *:not(.app-panel-head)` rules. Compact mode (rev 39) tightens the collapsed panel further. **Strategic significance**: pairs with the rev-39 density toggle as the second compactness primitive — together operators on power-user workspaces tune both panel padding (rev 39 — global) and panel inventory (rev 77 — per-panel) without screaming for a real dashboard editor. Pure client-side. No schema cost. Cumulative diversification of the operator-stickiness layer alongside the rev-77 personal action inbox.
- `GET /api/v1/workspace/whats-new?since=ISO` — bearer-auth mirror of rev-76 temporal-delta surface. Closes the v1 parity gap on the rev-76 dashboard 'what's new since you were last here' badge in lockstep with the rev-77 dashboard primitive. Bearer-auth endpoint accepts an optional `since` ISO timestamp (defaults to last 24h, clamped to 30-day max lookback) and returns per-kind counts (signals/artifacts/outputs/approvals/activity) + small samples in one call. Distinct from `/api/v1/workspace/today` (rev 45 — calendar-day snapshot) by being *anchored to a caller-provided timestamp* rather than today's day-start. Distinct from `/api/v1/activity` (rev 20 — full audit log) by aggregating per-kind counts so an MCP host can render '5 new signals · 2 outputs' without paginating every entity. **Strategic significance**: until rev 77 an MCP host driving the desk had no single endpoint to answer 'what landed since timestamp X?' — it had to enumerate `/api/v1/signals` + `/api/v1/runs` + `/api/v1/artifacts` and filter client-side. The cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror in lockstep) continues unbroken. Pairs with `GET /api/v1/workspace/personal-inbox` (rev 77 — actionable axis) so the personal-axis surface is now end-to-end on the v1 protocol-bound side: rev-76 lastVisitedAt stamp + rev-77 whats-new (temporal) + rev-77 personal-inbox (actionable).
- Visual polish — amber-tinted personal inbox pill + smooth collapse chevron + focus-visible ring. New `.ld-personal-inbox-pill` CSS uses an amber-tinted palette distinct from the rev-12 heartbeat (brand-color) and rev-76 what's-new badge (brand-color pulsing dot) so the three status-bar instruments read with three distinct visual vocabularies — heartbeat = ambient state, what's-new = temporal delta, inbox = actionable counter. Hover lift + focus-visible outline ring matches the rev-38 dashboard accessibility pattern. New `.ld-panel-collapse-chevron` uses a subtle muted-grey palette so the chevron reads as ambient affordance rather than a primary action — the panel head's content remains the visual focus. Mobile breakpoints shrink both surfaces so they fit cleanly on phones. Cumulative micro-polish — every rev 22+ has carried at least one — and rev 77's polish is load-bearing because the status bar's instrument density grew 5→6 with the new actionable pill and needed visual distinction without competing with the rev-76 what's-new badge that landed last cycle.
Personal chronic warnings in digest, v1 operator-notes mirror, and 'what's new since you were last here' badge
- Per-recipient personal chronic-warning section in the daily digest. Closes the named rev-75 next-sprint candidate at the per-recipient axis. Rev 75 shipped workspace-shared chronic-warning sections (every owner/admin sees the same list); rev 76 adds the personal scoping mirror at the chronic axis. New `buildPersonalChronicSection()` helper surfaces, for each recipient, (a) their own queue if they're a chronic-spiking assignee — the strongest personal signal: 'your queue is chronically over-loaded' — and (b) workstream tags that show up on tasks assigned to them AND are currently chronic. Solo desks (≤1 named assignee) deliberately skip the section to avoid redundancy with the rev-75 workspace-shared sections. Plumbed through both the cron production loop (`runDailyDigest`) and the `previewDigestForUser` admin testing path. **Strategic significance**: closes the per-recipient personalisation symmetry across rev-25 assignee section + rev-31 mentions section + rev-49 personal stale section + rev-53 personal cost section + rev-76 personal chronic section. The daily digest is now fully personalised across every load-bearing per-recipient surface that benefits from per-recipient context. Multi-operator teams see workspace-wide chronic state first, then drill into 'and these chronic tags / your queue specifically' personal scoping — same shape rev-49 introduced for stale tasks.
- `POST/GET /api/v1/tasks/{id}/notes` — close v1 parity gap on the rev-14 operator-notes primitive. The rev-14 operator-notes primitive (the canonical 'steer this task mid-flight' channel that gets fed to the next AI cycle as authoritative direction) has been dashboard-only for 62 revs. Rev 76 closes the v1 parity gap with a thin bearer-auth mirror: GET reads the existing notes on a task, POST appends a new note that the next pulse cycle picks up — same effect as the dashboard route, same memory write, same auto-promotion of `needs_input` → `queued`. **Strategic significance**: until rev 76 an MCP host driving the desk programmatically could read every task field and update status / tags / blockers / due dates / pin / assign / cost-spike-ack — but couldn't fire the most direct AI-direction primitive on the desk. The operator-notes endpoint is the most load-bearing channel between an MCP host and the AI cycle's behavior, and now it's reachable. Indexed in `/api/v1` self-describing endpoint list.
- 'What's new since you were last here' status-bar badge + activity log 'show only new' filter. New `workspace_member.lastVisitedAt` timestamp column + `markWorkspaceVisited()` helper that reads the previous timestamp and stamps the current one in a single call (60-second update throttle to handle render storms). The dashboard server component calls it idempotently on every render so per-user-per-workspace 'last visit' state is workspace-shared (consistent across devices) rather than localStorage-fragile. New `WhatsNewBadge` client component sits in the dashboard status bar below the company name + heartbeat row, surfacing 'N signals · M outputs since you were last here 4h ago' with a brand-color pulsing dot. The activity log filter (rev 21) gains a 'Only new (N)' chip + per-row 'new' pill marker that highlights every entry that landed after the previous visit. Diversifies away from the rev-51-onward cost-axis cluster — answers a different question (when did *I* last look?) than the rev-33 TodayPanel (workspace-shared what happened today?) or the rev-12 LoopHeartbeat (state right now?). **Strategic significance**: pairs with rev-12 heartbeat (state) + rev-13 desk health (trust score) + rev-14 cycle performance (latency) + rev-16 read-only pill + rev-39 density toggle as the *fifth* status-bar instrument — but uniquely temporal: the others answer 'what state is the desk in right now?' while the rev-76 badge answers 'what changed while I was away?'. Multi-operator teams catching up after a day off no longer scroll every panel to find what's new — the answer is in the status bar. Cumulative diversification of the dashboard status-bar instrument cluster.
- Visual polish — pulsing brand-color dot on what's-new badge + per-row activity accent + matching focus-visible ring. New `.ld-whats-new-badge` CSS uses the same brand-color palette as the rev-12 heartbeat indicator so the two status-bar instruments read as siblings. Pulsing dot animation (`@keyframes ld-whats-new-pulse`) draws the eye on workspace return without screaming. The activity log's 'new' rows wear a 2px brand-color left-border accent + an inline 'new' pill so operators can spot fresh entries even when the 'Only new' filter is off. Mobile breakpoint shrinks the badge typography 0.78rem → 0.74rem so it fits cleanly on phones. Cumulative micro-polish — every rev 22+ has carried one — and rev 76's polish is load-bearing because the status bar's instrument density grew 4→5 instruments and the new instrument needed visual distinction without screaming.
Chronic-warning digest sections close the email parity gap + at-a-glance chronic summary on TodayPanel
- Chronic-warning digest sections across all three axes (per-source / per-assignee / per-tag). Closes the named rev-74 next-sprint candidate ('source.chronic_warning digest section'). Until rev 75 the chronic warning was push-loud on Slack (rev 64/70/74) + outbound (rev 64/70/74) + in-app banner (rev 61/64/70) but absent from the daily digest email. Solo founders and email-first operators who didn't have the dashboard tab open and didn't sit in Slack saw daily spikes in the digest but never the chronic warnings — the structural-fix prompt that's most actionable for non-Slack workspaces. Rev 75 closes that gap on every axis where chronic makes sense: new `buildSourceChronicWarningSection`, `buildAssigneeChronicWarningSection`, and `buildTagChronicWarningSection` helpers in `src/lib/digest.ts` render up to 5 chronic alarms per axis with `⏳ Nd in a row` + `⚡ ratio×` + today $ pills + structural-fix recommendation copy. Amber palette (vs the daily section's red) signals 'different alarm, different operator response — daily acks won't help.' Pre-fetched once per workspace and reused across every owner/admin recipient (workspace-shared, since chronic state is workspace-level diagnostic context). Wired into both the cron path (`runDailyDigest`) and the preview path (`previewDigestForUser`) so admins iterating on configuration see the same surface they'll receive in production. **Strategic significance**: closes the email-channel parity gap on the chronic axis. The chronic alarm cluster now reaches every operator-loaded channel — in-app banner, Slack push, outbound webhook, AND digest email — across all three dimensions where chronic makes sense. The cost-axis story is now end-to-end visible across attribution + alarm + ack + chronic in every channel an operator might be on.
- Chronic-warning summary on TodayPanel for at-a-glance morning visibility. New `chronicWarnings` aggregate count (sources/assignees/tags) computed in `getDashboardState()` from the existing rev-58/62/67 spike detector returns + rev-72 chronic-ack stamp + workspace.tagChronicAcks JSONB. Rendered as a single brand-amber chip on the rev-33 TodayPanel right below the headline when ANY axis has an active chronic warning. Pure derived state — no schema, no extra round-trip (reuses existing `sourceCostSpikes`, `assigneeCostSpikes`, `tagCostSpikes` from the dashboard payload). Tap the chip to scroll to the matching cost-by-X panel where the rev-72/73 chronic-ack chips + buttons live. Tooltip explains chronic state and the structural-fix vocabulary. **Strategic significance**: until rev 75 the operator's morning glance at TodayPanel showed today's signals/cycles/outputs/spend with day-over-day deltas + 7d baseline + sparkline + projected end-of-day spend — but answering 'is anything *structurally* wrong?' required scrolling to the rev-57 / rev-52 / rev-66 cost panels and inspecting each row's chronic chip. Rev 75 collapses that to a glance. Pairs naturally with the rev-32 cost-spike banner (workspace baseline drift) + rev-21 cost cap warning (hard ceiling) — three layers of cost alarm visibility on the morning-check surface.
- `buildDigestHtml` accepts three new sections + ordered render after daily spikes. Extended the digest HTML builder type signature with `sourceChronicWarningSection`, `assigneeChronicWarningSection`, `tagChronicWarningSection`. Render order: chronic sections appear *after* the daily-spike sections so readers see today's spike context first, then the 'and these have been chronic for days' structural follow-up — closes the cognitive gap between 'something happened today' and 'something has been wrong for days.' **Strategic significance**: the digest email now reads as a complete morning report: workspace summary → assigned tasks (rev 25) → unread mentions (rev 31) → stale tasks (rev 48) → personal cost (rev 53) → daily spikes (rev 55/60/63/68) → chronic warnings (rev 75). Six layers of personalised + workspace-shared content covering every read horizon an operator might care about.
- Visual polish — chronic summary chip with focus-visible ring + tactile hover lift. New `.ld-today-chronic-summary` CSS uses the same amber palette as the rev-72 chronic-ack chips so all chronic-axis surfaces read with one consistent visual vocabulary across the dashboard. 1px hover lift + brand-color border emphasis on hover for tactile click affordance. `:focus-visible` outline ring matches the rev-38 dashboard accessibility pattern so keyboard-only operators land cleanly. Cumulative micro-polish — every rev 22+ has carried at least one — and the rev-75 chip closes a small but real visual-hierarchy gap: until rev 75 the morning chronic state had no surface at the top of the dashboard.
Per-source chronic warning + chronic-resumed closure receipt complete the per-source chronic loop
- `source.chronic_warning` Slack push + outbound event — closes the per-source chronic-warning push parity gap. Closes the named rev-73 next-sprint candidate. Until rev 74 the rev-61 source persistent-spike banner was *in-app only* — per-tag (rev 70) and per-assignee (rev 64) chronic warnings had Slack pushes + outbound events but per-source did not, so operators using the rev-61 nudge-only flow (vs the rev-62 auto-pause flow) had no push channel telling them a feed had crossed the chronic threshold. Rev 74 closes that gap. New `buildSourceChronicWarningSlackPayload()` Slack block + `dispatchSourceChronicWarningWebhook()` outbound dispatch fire from the daily sweep when one or more sources have crossed the chronic threshold (3+ consecutive days running) AND haven't been chronic-acked. Rate-limited to once per workspace per 24h via the new `source_chronic_warning` activity-log kind so the chronic axis doesn't drown out the rev-58 daily ⚡ alarm. Distinct from the rev-62 `source.chronic_auto_paused` event (which fires only when the operator opted into auto-pause); rev 74 fires for every chronic state regardless of action configuration. Mirrors `tag.chronic_warning` (rev 70) + `assignee.chronic_warning` (rev 64) at the per-source axis. **Strategic significance**: closes the chronic-warning push parity gap so all three axes where chronic makes sense (per-tag, per-source, per-assignee) now have a complete in-app banner + Slack push + outbound event trio.
- `source.chronic_resumed` outbound event — closes the rev-62 closure-receipt loop on auto-pause. Closes the named rev-73 next-sprint candidate. Until rev 74, downstream integrations watching the rev-62 `source.chronic_auto_paused` event saw 'alarm fired' but never 'alarm resolved' — when the operator resumed an auto-paused source after triage, the closure had to be inferred or polled. Rev 74 closes the loop. New `dispatchSourceChronicResumedWebhook()` fires from `setSourceStatus()` and `bulkUpdateSources()` whenever a source whose `lastErrorMessage` starts with 'Auto-paused after' (the rev-62 sweep marker) is resumed back to `connected`. Payload carries the source identity, the operator who resumed it, and the consecutive-spike-days count at the time of auto-pause for downstream context. Also clears the stale auto-pause `lastErrorMessage` + resets `consecutiveSpikeDays` to 0 on resume so the dashboard doesn't keep showing the auto-pause reason after triage. Mirrors the rev-37 `task.unblocked` closure pattern at the chronic auto-pause axis. **Strategic significance**: external integrations (CRM, FinOps tool, project tracker grouped by initiative) can now reconcile alarm-open with alarm-resolved on the per-source chronic axis without polling the dashboard — same primitive that landed in rev 37 for the dependency-blocked → unblocked transition, now applied to the rev-62 auto-pause → resume transition.
- Outbound subscription picker + activity log glyphs extended for the two new event kinds. The rev-73 grouped picker now exposes `source.chronic_warning` and `source.chronic_resumed` in the Chronic warnings group alongside the rev-62 `source.chronic_auto_paused` so operators can subscribe to the full per-source chronic lifecycle (warning → auto-pause → resume) from one URL. Activity log surfaces a `⏳` glyph + 'Source chronic warning' label on every `source_chronic_warning` row, matching the rev-35 per-kind tinting vocabulary used by `tag_chronic_warning` and `assignee_chronic_warning`. **Strategic significance**: closes the dashboard-surface parity gap on the per-source chronic axis — every channel an operator might be on (in-app banner, Slack push, outbound webhook, activity log, outbound picker) now reads with one consistent vocabulary.
- Chronic-auto-paused source row visual treatment + `(was chronic-auto-paused)` resume affordance. Every paused source whose `lastErrorMessage` starts with 'Auto-paused after' now wears a soft amber left-border + `⏳ auto-paused (chronic)` pill in the sources panel — distinct from operator-paused sources (default neutral) and feed-error sources (red `⚠ error`). The pill carries an info tooltip explaining why the desk paused the source. Resume from the source-actions row continues to fire the rev-74 closure receipt and clear the stale message. Cumulative micro-polish — every rev 22+ has carried at least one — but rev 74's polish is load-bearing because chronic-auto-paused sources need a different triage decision (apply a keyword filter / remove / accept the cost) than operator-paused or feed-error sources. **Strategic significance**: the operator scanning the sources panel can now distinguish the three pause states (operator, error, chronic-auto-paused) without reading every detail line.
Closure-receipt outbound events + chronic-ack listing endpoint close the chronic-ack loop
- Three new closure-receipt outbound events (`tag.chronic_warning_acked`, `source.chronic_warning_acked`, `assignee.chronic_warning_acked`). Closes the named rev-72 next-sprint candidate ("closure-receipt outbound events for chronic-ack"). Until rev 73, downstream integrations watching the rev-64 `assignee.chronic_warning` and rev-70 `tag.chronic_warning` outbound events had no closure signal — they'd see 'chronic alarm fired' but never 'chronic alarm acknowledged.' A FinOps tool tracking AI cost on a per-workstream dashboard, a manager dashboard listening to per-recipient chronic warnings, or a project tracker grouped by initiative had to either poll the dashboard for the current ack state or accept stale alarms. Rev 73 closes the loop. New `dispatchTagChronicWarningAckedWebhook`, `dispatchSourceChronicWarningAckedWebhook`, `dispatchAssigneeChronicWarningAckedWebhook` dispatchers wired into the matching `acknowledgeTagChronicWarning` / `acknowledgeSourceChronicWarning` / `acknowledgeAssigneeChronicWarning` helpers via `void dispatch...({...})` fire-and-forget so a downstream failure can't block the ack itself. Mirrors the rev-37 closure-receipt pattern (`task.unblocked`) + rev-61 task/source cost-spike-acked + rev-63 assignee.cost_spike_acked + rev-68 tag.cost_spike_acked at the *chronic horizon* (7d mute) rather than the *daily horizon* (per-day mute). **Strategic significance**: every rev-73 change makes an *existing* primitive load-bearing in a new place — the rev-64 + rev-70 chronic-warning outbound events now have closure receipts; the rev-71/72 chronic-ack write surface now reaches downstream integrations not just the dashboard.
- `GET /api/v1/cost/chronic-acks` listing endpoint. Closes the named rev-72 follow-up at the read surface. New `listChronicAcks()` helper aggregates the three chronic-ack stamp surfaces — `workspace.tagChronicAcks` JSONB (per-tag), `source.chronicAckedAt` timestamp (per-source), `workspace_member.chronicAckedAt` timestamp (per-assignee) — and returns only the acks within the rev-71 7-day TTL. Stale stamps are excluded since they no longer suppress the chronic surface. Each row carries the ack timestamp + the expiry timestamp + a humanised handle so MCP hosts can render 'currently muted: #q3-launch (5d left), Steve's queue (3d left), RSS bridge X (6d left)' without follow-up calls per axis. **Strategic significance**: closes the rev-72 named follow-up at the read surface. Pairs with the rev-71 v1 chronic-spike read endpoints + rev-71/72 v1 chronic-ack write endpoints + the new rev-73 closure-receipt outbound events for the complete read/write/closure picture on the chronic surface. The cost-axis MCP cluster's chronic surface is now functionally complete — the upcoming MCP server's chronic-ack tooling has nothing left to design across read, write, or closure.
- Outbound subscription picker grouped by category — exposes 26+ events across six groups. Until rev 73 the `OutboundSubscriptions` picker only exposed the rev-18 four event types (`artifact.ready`, `artifact.approved`, `signal.created`, `task.assigned`) — operators couldn't subscribe to the cost-spike alarm cluster, the chronic-warning push surface, or the rev-73 chronic-ack closure receipts without writing a manual API call against the v1 surface. Rev 73 expands the picker to 26 events grouped into six categories: outputs & memory, signals & tasks, daily cost alarms, cost-alarm closure receipts, chronic warnings, chronic-ack closure receipts. Each group renders with a category header + hint inside a soft-bordered fieldset (`.ld-outbound-event-group` CSS) so the surface stays scannable as the event count grows. **Strategic significance**: closes a year-long ergonomics gap that had been silently growing since rev 18. The rev-19 per-event subscription primitive was load-bearing but the picker UI capped what operators could discover; rev 73 brings the dashboard surface to parity with the v1 surface.
- Success-flash visual feedback on every chronic-ack chip. Until rev 73, clicking a chronic-ack chip kicked off a fetch + router.refresh and the chip simply disappeared on the next render — visually identical to a slow-network click that hadn't completed yet. Operators couldn't tell their counter-action took effect. Rev 73 adds a 480ms success state: on success, the chip switches to a brand-green `is-success` modifier (`@keyframes ld-chronic-ack-success`) showing `✓ Muted 7d` with a soft scale-up + box-shadow pulse before `router.refresh()` swaps the chip into its rev-72 muted/acked state. Applies to all three chronic-ack chips (per-tag rev-71, per-source rev-72, per-assignee rev-72). **Strategic significance**: cumulative micro-polish — every rev 22+ has carried at least one — but rev 73's polish is load-bearing because the chronic-ack surface is the operator's primary defensive lever on the rev-71/72/73 chronic-axis cluster, and the click → silent-disappear transition was the largest UX friction point left on the surface.
Per-source + per-assignee chronic-warning ack closes the chronic-ack symmetry on every cost axis
- Per-source chronic-warning ack (`source.chronicAckedAt`). New `source.chronicAckedAt` timestamp column + `acknowledgeSourceChronicWarning()` helper + `POST /api/sources/{sourceId}/chronic-ack` route + `SourceChronicAckButton` client component mounted directly beside the rev-61 `⏳ Nd` chronic chip on every spiking row of the rev-57 cost-by-source panel whose `consecutiveSpikeDays >= 3`. The rev-61 persistent-spike recommendation banner + rev-62 chronic auto-pause sweep both skip sources whose chronic-ack stamp is younger than the 7-day TTL. **Strategic significance**: closes the named rev-71 next-sprint candidate ('per-source chronic-warning ack') on the operator counter-action surface at the per-source axis. Distinct from the rev-59 daily ack (which mutes today's rev-58 alarm) — the rev-61/62 chronic surface fires on a *structural* problem (keyword filter, permanent pause, remove the source) so the right operator response is 'I see this, I'm intentionally letting it run, mute the chronic surface for a week' rather than 'stop alarming today.' Different surface, different TTL (7 days vs same-day), different intent. Mirrors rev-71 per-tag chronic ack at the per-source axis.
- Per-assignee chronic-warning ack (`workspace_member.chronicAckedAt`). New `workspace_member.chronicAckedAt` timestamp column + `acknowledgeAssigneeChronicWarning()` helper + `POST /api/cost/by-assignee/{assigneeId}/chronic-ack` route + `AssigneeChronicAckButton` client component mounted beside the rev-64 `⏳ Nd` chronic chip on every spiking row of the rev-52 cost-by-assignee panel whose `consecutiveSpikeDays >= 3`. The rev-64 chronic-warning sub-sweep in `pingAssigneeCostSpikes` now skips assignees whose chronic-ack stamp is younger than the 7-day TTL — the rev-64 Slack push + outbound `assignee.chronic_warning` event mute together. **Strategic significance**: mirrors the rev-71 per-tag + rev-72 per-source chronic ack at the per-recipient axis exactly. Closes the chronic-ack symmetry across all three dimensions where chronic makes sense (per-tag rev 71 + per-source rev 72 + per-assignee rev 72). The full per-recipient cost story is now end-to-end across detection (rev 62) → daily ack (rev 63) → chronic counter (rev 64) → chronic warning push (rev 64) → chronic ack (rev 72).
- v1 mirrors in lockstep (`POST /api/v1/sources/{id}/chronic-ack`, `POST /api/v1/cost/by-assignee/{id}/chronic-ack`). Two new bearer-auth endpoints mirror the rev-72 dashboard chronic-ack primitives in the same cycle the dashboard primitives ship (cadence pattern from rev 37 onward). MCP hosts can now ack chronic warnings programmatically on every axis — daily mute for one day on the per-task / per-source / per-assignee / per-tag daily alarms (rev 56 / 59 / 63 / 68), chronic mute for one week on the per-source / per-assignee / per-tag chronic surfaces (rev 72 / 72 / 71). **Strategic significance**: the cost-axis MCP cluster now closes the **detect → triage → daily-ack → chronic-ack** loop on every axis where chronic makes sense without a single dashboard-only path. Pairs with rev-71 v1 chronic-ack at the per-tag axis to form the complete chronic-ack v1 cluster.
- Muted chronic-pill state + visual polish. New `.ld-cost-source-chronic-ack` + `.ld-cost-assignee-chronic-ack` CSS using the same amber/warning palette as the rev-71 `.ld-cost-tag-chronic-ack` so all three chronic-ack buttons read with one consistent vocabulary across the per-tag / per-source / per-assignee surfaces. New `.is-acked` modifier on every chronic chip class — when the workspace has acked the chronic warning, the `⏳ Nd` chip switches to a muted neutral-grey palette with a strikethrough so the eye reads it as 'I've already triaged this' without losing visibility into the underlying chronic state. The full row-level reading order across all three chronic-axis panels is now: ⚡ pill (red, today's alarm) ↔ Ack button (red, mute today) :: ⏳ chip (red, structural alarm) ↔ Ack 7d button (amber, mute 7d) ↔ ⏳ chip muted-grey when acked. Different alarms, different acks, different states, one consistent reading order. Cumulative micro-polish — every rev 22+ has carried at least one.
Per-tag chronic-warning ack + per-source/per-assignee chronic v1 mirrors close the chronic axis on every cost dimension
- Per-tag chronic-warning ack (`workspace.tagChronicAcks`). New JSONB column `workspace.tagChronicAcks` (`Record<tag, ISO date>`) + `acknowledgeTagChronicWarning()` helper + `POST /api/cost/by-tag/{tag}/chronic-ack` route + `TagChronicAckButton` client component mounted directly beside the rev-70 `⏳ Nd` chronic chip on every tag row whose `consecutiveSpikeDays` has crossed the chronic threshold (3d+). The rev-70 chronic warning sub-sweep now skips tags whose chronic-ack stamp is younger than 7 days. **Strategic significance**: closes the named rev-70 next-sprint candidate ('per-tag chronic-spike ack'). Distinct from the rev-68 per-day daily ack (different surface, different TTL, different intent). The rev-67 daily alarm names today's anomaly — operator response is 'stop alarming today' (per-day mute). The rev-70 chronic warning names a structural problem — operator response is 'I see this, I'm intentionally letting it run' (7-day mute). Both can coexist on the same workstream: an operator who's seen both can ack each independently — daily mute for one day, chronic mute for one week.
- Per-source chronic-spikes v1 endpoint (`GET /api/v1/cost/by-source/chronic-spikes`). Closes the named rev-70 next-sprint candidate ('per-source/per-assignee chronic v1 mirrors') at the per-source axis. New bearer-auth endpoint returns sources whose `source.consecutiveSpikeDays` (rev-61 counter) has crossed the chronic threshold (3d+), sorted by counter value descending. Each row carries today/baseline/spikeRatio when the source is also currently in the rev-58 daily detector; otherwise those fields read 0 (chronic state can persist a day after today's spend cooled). **Strategic significance**: rev 70 shipped the chronic axis on the v1 surface for per-tag but per-source had no v1 equivalent — MCP hosts driving the desk could see the rev-58 daily alarm and the rev-62 chronic auto-pause action programmatically but couldn't read which sources had crossed the chronic threshold without polling the dashboard. Rev 71 closes that gap.
- Per-assignee chronic-spikes v1 endpoint (`GET /api/v1/cost/by-assignee/chronic-spikes`). Mirrors the rev-71 per-source endpoint at the per-recipient axis. Returns workspace members whose `workspace_member.consecutiveSpikeDays` (rev-64 counter) has crossed the chronic threshold (3d+), with name/email resolved server-side so the payload is self-contained. Today's spike state (rev-62 daily detector) projected when present. **Strategic significance**: closes the chronic axis on the v1 surface for per-assignee — until rev 71 the rev-64 chronic counter + rev-64 chronic warning push existed only on the dashboard panel and the Slack/outbound surfaces. The chronic axis on the v1 surface now reads consistently across all three dimensions where chronic makes sense: per-source (rev 71) + per-assignee (rev 71) + per-tag (rev 70). The cost-axis MCP cluster has nothing left to design on the chronic surface.
- v1 chronic-ack mirror + visual polish. New bearer-auth `POST /api/v1/cost/by-tag/{tag}/chronic-ack` mirrors the rev-71 dashboard chronic-ack endpoint in lockstep — the cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror in the same cycle) continues unbroken. New `.ld-cost-tag-chronic-ack` CSS uses an amber/warning palette distinct from the rev-68 daily ack button's red palette so the row-level visual story reads: ⚡ pill (red, today's alarm) ↔ Ack button (red, mute today) :: ⏳ chip (red, structural alarm) ↔ Ack 7d button (amber, mute 7d). Different alarms, different acks, different colours, one consistent reading order. Cumulative micro-polish — every rev 22+ has carried at least one — but rev 71's polish is load-bearing because it answers the open question from rev-70: 'I keep seeing the chronic warning daily, what should I actually do?' — the answer is now one chip-tap away.
Per-tag chronic-spike counter + warning closes the chronic axis on the per-tag cost story
- Per-tag chronic-spike counter (`workspace.tagConsecutiveSpikeDays`). New JSONB column `workspace.tagConsecutiveSpikeDays` (`Record<tag, number>`) — tags are workspace-scoped strings, not entities, so the counter lives on the workspace row instead of a per-entity column (mirrors the rev-68 `tagCostSpikeAcks` shape). The rev-70 `pingTagCostSpikes` daily sweep increments the counter for every tag the rev-67 detector flags, resets to 0 the first day a tag isn't spiking. Independent of the rev-68 `tagCostSpikeAcks` per-day mute — the counter keeps growing through ack-and-spike-again cycles so a workstream that's been 'ack me daily' for a week shows the strongest possible chronic signal. **Strategic significance**: the load-bearing primitive for the rev-70 chronic recommendation banner + chronic-warning push. Until rev 70 a chronically-noisy workstream (a focus-tag the team should drop, a scoped-too-broadly initiative, a tag attached to a runaway source) was visually identical to a one-off spiking workstream. Rev 70 makes the chronic case distinguishable from the one-off without requiring the operator to remember. Mirrors the rev-61 source counter + rev-64 assignee counter at the per-tag axis on the workspace-scoped JSONB.
- Chronic warning push (Slack + outbound `tag.chronic_warning`). New `tag.chronic_warning` outbound event + `dispatchTagChronicWarningWebhook()` + `buildTagChronicWarningSlackPayload()` Slack block fires when any tag's `consecutiveSpikeDays` crosses the chronic threshold (3 days). Sub-sweep added to `pingTagCostSpikes` mirrors the rev-64 assignee chronic warning shape exactly: rate-limited via its own `tag_chronic_warning` activity-log kind so the chronic ping doesn't drown out the rev-67 daily one. Nudge-not-act because the right answer (drop the focus tag, source filter, scope down, raise cap) depends on context the desk doesn't have. **Strategic significance**: closes the chronic axis on the per-tag cost story alongside the rev-67/68/69 daily-spike axis. The named rev-69 next-sprint candidate. The full per-tag cost story is now five layers deep across two axes: cumulative (rev 66) → trajectory (rev 67) → daily spike alarm (rev 67) + ack (rev 68) + auto-pause (rev 69) on the daily axis, and rev-70 counter + chronic warning + chronic banner on the chronic axis.
- ⏳ Nd chip + chronic recommendation banner on cost-by-tag panel. Every cost-by-tag row whose rev-70 consecutive count is ≥ 2 surfaces a quiet `⏳ Nd` pill alongside the rev-67 ⚡ pill on the rev-66 panel. Pill stays neutral until the count crosses the chronic threshold (3d) then turns brand-red so the eye reads chronic separately from one-off. New persistent-spike recommendation banner above the row list fires when any visible tag has been spiking for ≥ 3 consecutive days, recommending a structural change (drop focus tag / source filter / scope down / raise cap) rather than another daily mute. Distinct from the rev-67 daily alarm banner (which fires on a single day's anomaly). **Strategic significance**: closes the rev-69 named candidate at the *operator-nudge* layer. Pairs with the rev-61 source persistent-spike banner + rev-64 assignee chronic-spike chip so the three chronic-axis panels (per-source / per-assignee / per-tag) read with one consistent vocabulary. The cost-axis story is now uniform across every axis on every layer.
- `GET /api/v1/cost/by-tag/chronic-spikes` v1 mirror + projection extension. New bearer-auth endpoint returns workstream tags whose consecutive count has crossed the chronic threshold. Distinct from the rev-67 `/spikes` endpoint (which names today's anomaly) — chronic names a structural problem. The rev-67 daily-spike endpoint also gained a `consecutiveSpikeDays` field on every row so MCP hosts can render the same chronic affordance the dashboard shows. The cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror in lockstep) continues. Plus activity-log glyph + per-kind tint for the new `tag_chronic_warning` kind closes the rev-35 per-kind tint symmetry on the chronic-axis cluster (per-source `source_chronic_auto_paused` + per-assignee `assignee_chronic_warning` + per-tag `tag_chronic_warning` all now read consistently in the activity log). **Strategic significance**: the cost-axis MCP cluster is now seven endpoints deep across the per-tag axis alone (cumulative + trajectory + daily-spike + chronic-spike + ack + bulk-ack + config). The MCP server has nothing left to design on the per-tag cost surface.
Per-tag cost-spike auto-pause closes the descriptive→defensive loop on the per-tag cost story
- Per-tag cost-spike auto-pause toggle. New `workspace.tagCostSpikeAction` text column ('none' | 'pause' | null). When set to 'pause', the pulse engine's `selectNextTask` filter now also skips tasks whose tags overlap with any currently-spiking workstream until the operator acknowledges via the rev-68 ack surface. New `setTagCostSpikeAction()` + `getCostSpikePausedTagTaskIds()` helpers reuse the rev-67 `detectTagCostSpikes()` detector verbatim. Mirrors the rev-56 per-task auto-pause + rev-59 per-source auto-pause patterns at the per-tag axis. Default null (off, opt-in) so the rev-67/68 alarm + ack behaviour is unchanged unless the operator opts in. **Strategic significance**: closes the named rev-68 next-sprint candidate. The cost-spike alarm cluster (workspace rev 32 / per-task rev 55-56 / per-source rev 58-59 / per-assignee rev 62-63 / per-tag rev 67-69) now has descriptive → defensive coverage on every one of the five axes — the same auto-action pattern that started rev 20 with the workspace-level cost cap reaches the per-workstream axis.
- Workspace config endpoints + v1 mirror in lockstep. New admin-only `PATCH /api/workspace/tag-cost-spike-config` route + matching bearer-auth `GET/PUT /api/v1/workspace/tag-cost-spike-config` v1 mirror. The cadence pattern from rev 37 onward (ship the dashboard primitive + the v1 mirror in the same cycle) continues. **Strategic significance**: the cost-axis MCP cluster (workspace / per-task / per-source / per-assignee / per-tag) now has full descriptive → ack → reconfigure coverage on every axis through the protocol-bound surface. The MCP server has nothing left to design on the cost-spike axis across all five axes.
- Integrations panel UI section + adaptive cost-by-tag banner. New 'Per-tag cost-spike action' section in the integrations panel under the existing Cost guardrails cluster (alongside rev-49 stale auto-archive, rev-56 per-task action, rev-59 per-source action, rev-62 chronic auto-pause). Two buttons: 'Alarm only' (rev 67/68 default) vs 'Auto-pause spiking workstreams' (rev 69 opt-in). Plus the rev-67 ⚡ alarm banner on the cost-by-tag panel now adapts when auto-pause is enabled — solid border + stronger gradient + 'Auto-pause is on' copy distinguishes 'desk will act' from 'operator should consider'. New `.ld-cost-tag-spike-banner.is-auto-action` CSS mirrors the rev-62 source chronic banner treatment at the per-tag axis. **Strategic significance**: cumulative micro-polish (every rev 22+ has carried at least one). The integrations panel's Cost guardrails cluster is now five primitives deep — rev-20 cost cap + rev-49 stale auto-archive + rev-56 per-task action + rev-59 per-source action + rev-62 chronic action + rev-69 per-tag action. The pattern is now uniform across every axis: every defensive control follows the opt-in toggle shape so an operator can tune the desk's autonomy gradient at any axis without ever feeling the desk is 'running away'.
Per-tag cost-spike acknowledgment + bulk-ack + digest section + closure receipt
- Per-tag cost-spike acknowledgment + inline 'Ack' button (closes named rev-67 candidate). New `workspace.tagCostSpikeAcks` JSONB column (`Record<tag, ISO date>`) — tags are workspace-scoped strings, not entities, so the per-axis ack lives on the workspace row instead of a per-entity column. New `acknowledgeTagCostSpike()` helper + `POST /api/cost/by-tag/{tag}/cost-spike-ack` route + `TagCostSpikeAckButton` client component mounted directly beside the rev-67 ⚡ pill on every spiking row of the rev-66 cost-by-tag panel. The rev-67 detector now skips tags acked in the current workspace-TZ day, so the ⚡ pill, daily Slack push, outbound `tag.cost_spike` event, and rev-68 digest section all agree on what 'acked' means. **Strategic significance**: closes the named rev-67 next-sprint candidate at the operator counter-action surface. Mirrors the rev-56 task ack / rev-59 source ack / rev-63 assignee ack at the per-tag axis. The five-axis cost-spike alarm cluster (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62 / per-tag rev 67) now has a complete operator counter-action surface on every axis. Distinct from rev-29 focus tags (which BIAS attention TOWARD the tag) — focus is a sustained operator preference, ack is a one-day mute.
- Bulk per-tag cost-spike ack (dashboard + v1 in lockstep). New `bulkAcknowledgeTagCostSpikes()` helper + `POST /api/cost/by-tag/cost-spike-ack/bulk` route + matching `POST /api/v1/cost/by-tag/cost-spike-ack/bulk` v1 mirror. New bulk-ack bar surfaces inline in the rev-66 `CostByTagPanel` when `canAck && visibleSpikingTags.length >= 2`, mirroring the rev-57 task bulk + rev-60 source bulk + rev-63 assignee bulk pattern at the per-tag axis. Caps at 50 tags per call. **Strategic significance**: rev-67 daily Slack push lists up to 5 spiking tags; until rev 68 operators landing on the dashboard had to ack each spiking workstream individually. Bulk-ack collapses that to one tap. The cost-axis MCP cluster on the protocol-bound surface now closes the **detect → triage → ack** loop on every axis (workspace rev 32 / per-task rev 55-56 / per-source rev 58-59 / per-assignee rev 62-63 / per-tag rev 67-68) — the MCP server has nothing left to design on the cost-spike ack surface across the five-axis cluster.
- Per-tag cost-spike section in the daily digest email. New `buildTagCostSpikesSection()` helper in `src/lib/digest.ts` mirrors the rev-55 per-task / rev-60 per-source / rev-63 per-assignee sections at the per-tag axis. Every owner/admin recipient sees up to 5 spiking workstreams with ratio + today vs avg + contributing-task count + tag handle + deep-link to the rev-66 cost-by-tag panel. Pre-fetched once per workspace via `detectTagCostSpikes()` and reused across every recipient — same pattern the rev-55/60/63 sections use. The rev-36 `previewDigestForUser()` admin testing path also mirrors so admins iterating on configuration see the same surface they'll receive in production. **Strategic significance**: rev-67 closed the per-tag cost-spike push surface on the dashboard ⚡ pill + Slack + outbound channels but the daily digest email channel was missing. Solo founders and email-first owners who don't have the dashboard tab open and don't sit in Slack now get the same heads-up Slack-first teams already have. The full per-tag cost story is now end-to-end visible across attribution (rev 66) + trajectory (rev 67) + alarm (rev 67) + ack (rev 68) in every channel an operator might be on (dashboard / Slack / digest email / outbound webhook / v1 / MCP). Closes the alarm cluster's fifth axis on the email channel — workspace (rev 32) / per-task (rev 55) / per-source (rev 60) / per-assignee (rev 63) / per-tag (rev 68) all now reach every operator-loaded channel.
- tag.cost_spike_acked outbound event + closure receipt. New `tag.cost_spike_acked` outbound event + `dispatchTagCostSpikeAckedWebhook()` fires when an operator acks a tag spike (single or bulk). Mirrors the rev-61 `task.cost_spike_acked` + `source.cost_spike_acked` + rev-63 `assignee.cost_spike_acked` closure-receipt pattern at the per-tag axis. Wired into both `acknowledgeTagCostSpike` and `bulkAcknowledgeTagCostSpikes` via `void dispatch...({...})` so a downstream failure can't block the ack itself. Plus subtle visual polish: new `.ld-cost-tag-spike-ack` ack button + `.ld-cost-tag-bulk-bar` bulk-ack bar styles share the rev-63 assignee ack visual vocabulary so the five cost panels (workspace banner, top-cost-tasks, cost-by-source, cost-by-assignee, cost-by-tag) read with one consistent ack vocabulary across all five axes. **Strategic significance**: until rev 68, downstream integrations watching the rev-67 `tag.cost_spike` event had no closure signal — they'd see 'alarm fired' but never 'alarm acknowledged.' A FinOps tool tracking AI cost on a per-workstream dashboard, a project tracker grouped by initiative, or a board-status integration aggregating 'open cost issues this week' had to either poll the dashboard for the current ack state or accept stale alarms. Rev 68 closes the loop on the alarm cluster's fifth axis.
Per-tag cost trajectory + spike alarm, fifth alarm-cluster axis closes, v1 parity
- Per-tag cost trajectory primitive (closes named rev-66 next-sprint candidate). New `getTagCostTrajectory()` helper + extended `getCostByTag()` with optional `trajectoryDays` so each row carries a `trajectory7d` cents array. The rev-66 dashboard panel now renders an inline sparkline beside the cumulative bar on every tag row — same visual vocabulary as the rev-60 by-source + rev-61 by-assignee sparklines so the four cost panels (per-task / per-source / per-assignee / per-tag) read with one consistent vocabulary. New `GET /api/cost/by-tag/{tag}/trajectory` viewer-auth endpoint + matching `GET /api/v1/cost/by-tag/{tag}/trajectory` bearer-auth mirror in lockstep. **Strategic significance**: closes the trajectory dimension on every cost axis — workspace (rev 53) → per-task (rev 54) → per-source (rev 60) → per-assignee (rev 61) → per-tag (rev 67). Operators answering 'is this workstream steadily expensive or just spiking today?' no longer have to drill into per-task cost rows.
- Per-tag cost spike alarm + outbound `tag.cost_spike` event. New `detectTagCostSpikes()` helper aggregates `task.dailyCostHistory` per-tag using the rev-66 'task contributes its full cost to every tag it carries' methodology. Surfaces tags whose today spend in workspace TZ is >= 2× the trailing 7-day daily average AND >= $0.50 absolute AND >= 3 historical non-zero days (matches the rev-32/55/58/62 detector thresholds). Daily Slack push from `runDailyDigest()` via the new `pingTagCostSpikes()` sweep — rate-limited via `tag_cost_spike` activity-log entry to once per workspace per 24h with the same dead-Slack-webhook auto-clear path. New `OutboundEvent` value `tag.cost_spike` + `dispatchTagCostSpikeWebhook()` so external integrations can route the alarm by workstream (CRM tagged by project, FinOps board sliced by initiative, board-status digest grouped by goal). New bearer-auth `GET /api/v1/cost/by-tag/spikes` endpoint. The synthetic `untagged` bucket is excluded — flagging 'untagged work is spiking' isn't actionable. **Strategic significance**: closes the alarm cluster's fifth axis (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62 / per-tag rev 67) so the cost-spike push cluster is now functionally complete on every meaningful axis. The strongest possible operator-facing answer to 'this initiative is going off the rails on AI cost' because it names the workstream, not just the workspace.
- Inline ⚡ pill + spike banner + spiking-row accent on the rev-66 cost-by-tag panel. Every spiking row on the rev-66 panel now gets a brand-red ⚡ N× pill alongside the rev-66 cost amount + percent + bar (with tooltip carrying today vs avg detail). Spiking rows wear a left-border accent + tinted amount color matching the rev-58 source spike + rev-62 assignee spike treatment so the alarm vocabulary reads consistent across all five panels. Banner above the row list summarises the loudest spike inline so operators see the alarm without reading every row. **Strategic significance**: the rev-66 panel went from cumulative-only (one read) to three-level (cumulative + trajectory + alarm). Closes the visual-hierarchy gap on the cost-by-tag axis — operators now scan the panel and triage by alarm state without leaving the dashboard sidebar.
- Activity log glyph + tint for `tag_cost_spike` + cumulative micro-polish. New `tag_cost_spike` entry in the rev-35 activity log glyph + per-kind tint maps so the new daily push reads with the same ⚡ visual vocabulary as the rev-32/55/58/62 spike kinds. Filter chip now offers 'Tag cost spike' as a one-click scope. The five cost-spike kinds (cost_spike / task_cost_spike / source_cost_spike / assignee_cost_spike / tag_cost_spike) all wear the same brand-amber tint so the operator's eye reads them as one alarm cluster. Plus subtle visual polish on the rev-66 panel: cleaner row-head spacing accommodates the new sparkline + pill alongside the cumulative amount without crushing on narrow viewports. Cumulative micro-polish — every rev 22+ has carried one. **Strategic significance**: closes the per-kind-tint symmetry gap that's been growing since rev 35 introduced the glyph map but never received the per-recipient (rev 62) or per-task (rev 55) variants. The activity log now scans visually for every alarm kind in the cost-spike cluster.
Persistent task templates, per-tag cost attribution, save-detected-pattern, v1 parity
- Persistent workspace task templates (closes named rev-65 candidate). New `task_template` table + helpers + dashboard CRUD lets operators save a custom task shape (or the rev-65 detected pattern) as a workspace-scoped persistent template that surfaces alongside the rev-64 static set in the quick-start chip row. Each row carries label, emoji, hint, title, summary, goal, kind, deliverableType, priority, tags, plus `usageCount` + `lastUsedAt` so apply records usage server-side and the recent-first ordering benefits multi-operator workspaces (the rev-65 localStorage usage map only covers one operator). Capped at 20 templates per workspace; unique by label. **Strategic significance**: rev 64 introduced static templates; rev 65 introduced AI-suggested-from-history (one-shot, session-only); rev 66 fills the missing slot — workspace-specific recurring shapes the operator wants durable. Pairs with the rev-19 onboarding templates as the workspace lifecycle's three template surfaces: day 1 onboarding (rev 19) / day N static (rev 64) / day N personalised + persistent (rev 65 + rev 66).
- Save-as-template button + save-detected-pattern affordance. New 'Save as template' button on the rev-24 task creation form lets operators lift the current input into a persistent workspace template before queueing — handles the 'I just typed something I want to reuse' path. New 'Save' button alongside the rev-65 detected-pattern chip lets operators promote the AI-suggested pattern from one-shot into durable. Both paths route through the same `POST /api/workspace/task-templates` endpoint with editor+ role enforced server-side. The dashboard refreshes on success so the new template surfaces in the chip row immediately. **Strategic significance**: rev 65 named 'an upcoming rev that lets operators save a detected pattern' as the natural follow-up; rev 66 ships both the manual save path and the detected-pattern lift in one cycle.
- Per-tag cost attribution panel + v1 endpoint (closes 4th cost axis). New `getCostByTag()` helper aggregates `task.totalInputTokens + totalOutputTokens` by `task.tags` so operators can answer 'which workstream is eating my AI spend?'. Each task contributes its full cost to *every* tag it carries (distinct from rev-57 source attribution which splits equally — a task doesn't have multiple 'primary sources' the way it can have multiple thematic tags). New `CostByTagPanel` mounts in the dashboard sidebar alongside the rev-51/52/57 cost panels with a purple→amber gradient bar so the four cost panels stack as siblings without competing visually. New `GET /api/v1/cost/by-tag` v1 mirror in lockstep. **Strategic significance**: closes the missing fourth axis on the cost-attribution cluster (per-task rev 51 / per-recipient rev 52 / per-source rev 57 / per-tag rev 66). Pairs with rev-39 tag drill-down (cross-entity retrieval) and rev-29 focus tags (prescriptive attention) for the full tag-axis story across all four operator surfaces: write → descriptive → prescriptive → retrieval → cost.
- v1 mirrors for persistent task templates + visual polish. Five new bearer-auth v1 endpoints — `GET/POST /api/v1/workspace/task-templates`, `PATCH/DELETE/POST /api/v1/workspace/task-templates/{id}` — mirror the dashboard endpoints in lockstep. MCP hosts can list, create, update, delete, and record-usage on persistent templates via the protocol surface. The new persistent chip row uses a teal-tinted left-border accent to distinguish it from the rev-64 static row + rev-65 detected pattern, and the rev-65 detected-pattern row gains an inline 'Save' button that lifts the suggestion into a persistent template. Plus subtle polish: hover/transition treatments on the new persistent chips + remove buttons match the rev-22+ design-language thread. **Strategic significance**: closes the v1 parity gap on the rev-66 dashboard primitive in the same cycle the primitive ships.
Recent template tracking, AI-suggested template from history, scoped activity CSV, source-coverage health line
- Recent template usage tracking + auto-prioritized chip row. The rev-64 quick-start chip row now tracks operator usage in localStorage (per-workspace key `loopdesk:template-usage:<workspaceId>`) and re-orders chips so the operator's most-recent six-or-eight task shapes float to the front. The single most-recent template wears a 'recent' pill so the eye lands on it first. Pure client-side memory — no schema, no API round-trip, no SSR mismatch. **Strategic significance**: rev 64 introduced templates as a stickiness pivot from the cost-axis cluster; rev 65 makes the template list adapt to the *individual operator's* working pattern. Operators with a 'I always run a competitor analysis on Mondays' rhythm see that template waiting at the front of the row instead of having to scan the same eight chips every time.
- AI-suggested template from operator history (closes named rev-64 candidate). New `suggestTemplateFromHistory()` helper in `src/lib/task-templates.ts` analyses the last 30 days of tasks and proposes a synthetic template when 3+ tasks share a (kind, deliverableType, primary tag) triple. Surfaces as a distinct '✨ Detected from your recent tasks' chip ahead of the static templates with the recurring pattern's pre-fills (matching kind/deliverable/tags + a hint that names the source pattern, e.g. 'You've created 4 similar customer briefs in the last 30 days'). De-duped against the static TASK_TEMPLATES so a workspace already running competitor-analysis tasks doesn't see two competitor-analysis chips. Pure deterministic computation — no AI cost, runs server-side inside `getDashboardState()` against existing `task` rows. **Strategic significance**: closes the named rev-64 next-sprint candidate at the cheapest possible tier. Foundation for an upcoming rev that lets operators *save* a detected pattern as a workspace-scoped persistent template (which would require new schema). Until then, the suggestion is one-shot per session and the operator overwrites the title/summary inline like with the static templates.
- Scoped activity CSV export (procurement quarterly/monthly artefact). `getWorkspaceActivityCsv()` now accepts optional `since` / `until` Date bounds. The `/api/workspace/activity-export` route accepts ISO `since` and `until` query params (until is end-of-day for the chosen date so the filter is intuitive). New 'scope to date range' UI mounts directly under the existing JSON / activity-CSV / outputs-CSV / decisions-CSV / stale-tasks-CSV / cost-summary-CSV row in the integrations panel — two date inputs + a download button that flips its label between 'Download full activity CSV' and 'Download scoped CSV' depending on whether bounds are set. Filename also reflects the scoped range so a SOC 2 reviewer downloading 'loopdesk-acme-activity-2026-04-01-to-2026-04-30.csv' has procurement-evidence provenance baked into the artefact. **Strategic significance**: until rev 65 the activity export returned the trailing 5,000 rows unconditionally — sufficient for the procurement-evidence trio (rev 6/7/22) but a SOC 2 reviewer scoping a specific quarter or month had to filter the downloaded CSV by hand in Excel. Cap stays at 5,000 rows even with the filter so the takeaway stays Excel-sized; if a quarter has more than 5k entries the response shows the most-recent 5k inside the window.
- Source coverage info-line on the desk-health widget + v1 mirror. The rev-13 `DeskHealth` widget gains a 5th *informational* HealthLine showing distinct source-types as filled cells against the total source-type vocabulary (10: website, shopify, etsy, feedback_inbox, notion, drive, manual, rss, review_site, linkedin). Pure informational — does **not** change the 100-point score (which would be a v1 stats / badge SVG breaking change). Adaptive copy: 'one type — consider broadening' / 'two types — healthy starter mix' / 'three types — strong coverage' / 'N types — broad coverage' so operators see at a glance whether they're concentrated on RSS or have a mature signal mix. Mirrored on the v1 stats endpoint as `distinctSourceTypes` so MCP hosts can render the same coverage info in their own UIs. **Strategic significance**: pairs with the rev-13 single trust number + rev-14 cycle performance + rev-15 signal sparkline + rev-64 activity heatmap as the *fifth* instrument on the desk-health cluster — but along the source-coverage axis the others don't cover. Until rev 65 a workspace pulling everything from a single noisy LinkedIn bridge had no in-app diagnostic that nudged 'you'd benefit from a second source type'. Pairs naturally with the rev-19 onboarding templates (which seed initial sources) so the source-coverage line answers 'have we kept growing the source mix since onboarding?'.
Task quick-start templates, per-assignee chronic warning, activity heatmap, cumulative UI polish
- Task quick-start templates (operator-stickiness pivot). New `src/lib/task-templates.ts` with 8 vertically-scoped operator quick-start templates (Competitor analysis, Customer feedback synthesis, Quarterly board update, Renewal conversation prep, Pricing decision review, Content brief, Hiring scoping, Strategic question). Each template pre-fills title / summary / goal / kind / deliverableType / priority / tags so an operator can go from 'I want a board update' to 'task queued' in two taps. The chip row replaces the old 'Queue task directly' button when the form is collapsed; tapping a chip opens the form with all fields pre-filled and a clear 'X — adjust before queueing' eyebrow that names the active template. **Strategic significance**: pivots from the cost-axis cluster (13 revs deep) into the operator-stickiness layer. Mirrors the rev-19 onboarding-template shape (memory + signal presets) but at the workspace's *ongoing* starvation point (day N) rather than the day-1 starvation point. Every direct-task path until rev 64 started from a blank form, even though operators routinely run the same 6-8 task shapes — quick-start templates collapse repeated typing into chip-tap-and-go.
- Per-assignee chronic-spike counter + chronic warning push (closes rev-63 named candidate). New `workspace_member.consecutiveSpikeDays` integer column + chronic-warning sweep wired into the existing `pingAssigneeCostSpikes()` daily cron. Mirrors the rev-61 source consecutiveSpikeDays counter at the per-recipient axis: the counter increments every time the rev-62 detector flags an assignee as spiking, resets to 0 the first day they don't. When any assignee crosses the chronic threshold (3 days), a new `assignee.chronic_warning` outbound event + Slack push fires recommending rebalancing rather than auto-acting. Distinct from the rev-62 daily ⚡ alarm — the chronic warning names a *structural* problem (queue overload, scope creep, noisy task) and is rate-limited via its own activity-log kind so the chronic warning doesn't drown out the daily ⚡ alarm on the same workspace. New `⏳ Nd` chip on the cost-by-assignee panel (mirrors rev-61 source chronic chip): stays neutral until the count crosses 3d, then turns brand-red. **Strategic significance**: nudge-not-act because the right answer (reassign work / raise cap / hire) depends on context the desk doesn't have. The four-axis alarm cluster now reads consistently — daily push (rev 32/55/58/62) + chronic counter + warning (rev 61 source / rev 64 assignee).
- 7-day × 4-block activity heatmap (UI/visual improvement). New `ActivityHeatmap` client component renders inside the rev-13 desk-health panel. 28 cells (7 days × 4 hour-blocks: night 23-5 / morning 5-11 / midday 11-17 / evening 17-23 UTC). Reads top-to-bottom for diurnal rhythm, left-to-right for weekly cadence. The diagonal of dark cells is the desk's natural rhythm; the bright outliers are when something unusual happened. Mirrored on the v1 surface as `sevenDay.heatmap` on `/api/v1/stats` so MCP hosts can render the same temporal-shape view. Pure derived state from the existing 7-day desk_runs query — no extra round-trip, no schema change. Hidden when there are no cycles in the window (fresh workspaces don't see clutter). **Strategic significance**: pairs with rev-13 desk-health + rev-14 cycle performance + rev-15 signal sparkline as the fourth instrument on the 'is the desk healthy?' cluster — but along the temporal-shape axis the others don't cover. Useful for capacity planning ('the desk runs hottest mid-morning') + diagnosing dead-window patterns ('we never get overnight cycles, is the daemon up?').
- Cumulative UI polish + accessibility (cost cluster, focus chips, panel target flash). Cumulative micro-polish (every rev 22+ has carried at least one). Five small but cumulative pieces: (a) the rev-29 focus-tag chips gain a tactile 1px hover lift + soft shadow + brand-colour border-shift on hover so the focus surface feels active rather than static. (b) The rev-29 focus-suggest buttons gain matching transform + `:focus-visible` outline so keyboard-only operators see a clear landing pad. (c) New `.app-panel.is-target-flash` keyframes animation gives a soft 1.8s pulse when scroll-into-view lands on a panel via #panel-* navigation (mention permalinks, palette jumps, scroll-to-top jumps) — clearer 'you arrived here' affordance without a toast. (d) New `:focus-visible` ring on the rev-23 keyboard-shortcut FAB + rev-38 scroll-to-top FAB so the three power-user FABs (palette + shortcuts + scroll-top) land consistently. (e) New `.sr-only` utility class for the rev-64 heatmap's per-cell screen-reader labels so the heatmap is meaningful to assistive tech. **Strategic significance**: the rev-by-rev discipline of one targeted polish per rev is what keeps the dashboard from drifting toward the design-debt smell that hand-rolled SaaS dashboards usually develop after 64 revs.
Per-assignee cost-spike ack + bulk-ack + digest section + closure receipt
- Per-assignee cost-spike acknowledgment (closes rev-62 alarm-only gap). New `workspace_member.costSpikeAckedAt` timestamp column + `acknowledgeAssigneeCostSpike()` helper + `POST /api/cost/by-assignee/{assigneeId}/cost-spike-ack` route + matching v1 mirror + new `AssigneeCostSpikeAckButton` mounted directly beside the rev-62 ⚡ pill on every spiking row of the rev-52 cost-by-assignee panel. The rev-62 detector + daily Slack push + outbound `assignee.cost_spike` event + new rev-63 digest section all skip assignees acked in the current TZ-day so a triaged spike doesn't keep firing. Mirrors rev-56 task ack + rev-59 source ack at the per-recipient axis exactly. Ack = 'I see this teammate's queue spike, stop alarming today, but watch for tomorrow.' Strategic significance: the four-axis cost-spike alarm cluster (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62) now has a complete operator counter-action surface on every axis.
- Bulk per-assignee ack + v1 parity (closes 4-axis bulk-ack symmetry). New `bulkAcknowledgeAssigneeCostSpikes()` helper + `POST /api/cost/by-assignee/cost-spike-ack/bulk` + bearer-auth `POST /api/v1/cost/by-assignee/cost-spike-ack/bulk`. New bulk-ack bar surfaces inline in the cost-by-assignee panel when 2+ rows are spiking, mirroring the rev-57 top-cost-tasks + rev-60 cost-by-source bulk pattern at the per-recipient axis. Caps at 50 IDs per call, matching the rev-26 / rev-33 / rev-34 / rev-36 / rev-57 / rev-60 bulk surface vocabulary. The rev-62 daily Slack push lists up to 5 assignee spikes; until rev 63 operators landing on the dashboard had to ack each spiking teammate individually. Bulk-ack collapses that to one tap. The cost-axis MCP cluster on the protocol-bound surface now closes the **detect → triage → ack** loop on every axis (workspace rev 32 / per-task rev 55-56 / per-source rev 58-59 / per-assignee rev 62-63) — the MCP server has nothing left to design on the cost-spike ack surface.
- Per-assignee cost-spike digest section (closes alarm push-channel symmetry). New `buildAssigneeCostSpikesSection()` helper in `src/lib/digest.ts` mirrors the rev-55 per-task + rev-60 per-source sections at the per-recipient axis. Pre-fetched once per workspace inside `runDailyDigest()` and reused across every recipient — same pattern the rev-55 + rev-60 sections use. The rev-36 `previewDigestForUser()` admin testing path also mirrors so admins iterating on configuration see the same surface they'll receive in production. Workspace-shared (every owner/admin recipient sees the same list — 'this teammate's queue is anomalously expensive' is workspace-level diagnostic context). Strategic significance: closes the per-assignee push surface across all three operator-loaded channels — in-app ⚡ pill + ack button (rev 62 + rev 63), Slack push (rev 62), digest email (rev 63). Solo founders and email-first operators who don't have the dashboard tab open get the same heads-up Slack-first teams already have. The full per-assignee cost story is now end-to-end visible across attribution + trajectory + alarm + ack in every channel an operator might be on.
- assignee.cost_spike_acked outbound event (closure receipt) + visual polish. New `assignee.cost_spike_acked` outbound event + `dispatchAssigneeCostSpikeAckedWebhook()` fires when an operator acks an assignee spike (single or bulk). Mirrors the rev-61 `task.cost_spike_acked` + `source.cost_spike_acked` closure-receipt pattern at the per-recipient axis — lets downstream integrations (CRM, FinOps dashboard, board-status integration) close their own alarm tickets without polling. Plus subtle visual polish: new `.ld-cost-assignee-spike-ack` ack button + `.ld-cost-assignee-bulk-bar` bulk-ack bar share the rev-59 source ack visual vocabulary so the four cost panels (workspace banner, top-cost-tasks, cost-by-source, cost-by-assignee) read with one consistent ack vocabulary. New row-hover lift on every cost-by-assignee row matches the rev-60 cost-by-source hover pattern so all three cost-cluster panels feel scannable rather than static. Cumulative micro-polish — every rev 22+ has carried at least one. Strategic significance: closes the closure-receipt loop on the alarm cluster's fourth axis. The full alarm story is now consistent across all four axes (detect → push → ack → closure receipt).
Per-source chronic-spike auto-pause, per-assignee cost-spike alarm, cost CSV v1
- Per-source chronic-spike auto-pause (closes rev-61 named candidate). New `workspace.sourceChronicSpikeAction` ('none' | 'pause') + `sourceChronicSpikeThresholdDays` (2-14, default 3) columns. When set to 'pause', the rev-62 daily sweep auto-pauses sources whose rev-61 `consecutiveSpikeDays` crosses the threshold. The auto-action variant of the rev-61 chronic-spike recommendation banner — pairs with the rev-49 stale-task auto-archive opt-in pattern at the per-source axis on the *defensive* side rather than the *nudge* side. New Slack push + outbound `source.chronic_auto_paused` event fire on every sweep. New admin-only dashboard route + integrations panel UI + bearer-auth `/api/v1/workspace/source-chronic-spike-config`. Banner copy adapts when auto-pause is on ('these will be paused at threshold N') and warns when feeds are 1 day from auto-action so operators see what's coming tomorrow.
- Per-assignee cost-spike alarm (closes alarm-cluster fourth axis). New `detectAssigneeCostSpikes()` helper aggregates the rev-54 `task.dailyCostHistory` by `task.assignedToUserId` to surface assignees whose today queue-cost is >= 2× their trailing 7-day daily average AND >= $0.50 absolute. Closes the alarm cluster's fourth axis (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62). New daily Slack push via `buildAssigneeCostSpikeSlackPayload()` + new `assignee.cost_spike` outbound event + new bearer-auth `GET /api/v1/cost/by-assignee/spikes` endpoint. New inline ⚡ pill on every spiking row of the rev-52 CostByAssigneePanel — same visual vocabulary as the rev-58 source pill so the alarm cluster reads with one consistent shape across all four axes. Operators answering 'whose queue is anomalously expensive today?' no longer have to compute it mentally.
- Cost summary CSV v1 mirror endpoint. New bearer-auth `GET /api/v1/workspace/cost-export` mirrors the rev-60 dashboard cost CSV endpoint on the v1 surface. Closes the v1 parity gap on the procurement-friendly takeaway artefact. The cost-axis MCP cluster on the protocol-bound surface now reaches seven axes: per-cycle (rev 13) + per-task (rev 51) + per-task-trajectory (rev 54) + per-recipient (rev 52) + per-recipient-trajectory (rev 61) + today (rev 53) + per-source (rev 57) + per-source-trajectory (rev 60) — all with v1 mirrors plus the procurement-evidence CSV takeaway. The MCP server has nothing left to design on the cost surface.
- Visual polish: unified alarm-pill vocabulary across cost cluster + chronic-banner auto-action state. The rev-58 source ⚡ pill, rev-61 ⏳ chronic pill, and the new rev-62 per-assignee ⚡ pill now all share the same brand-red treatment so operators read alarm pills consistently across all four cost-cluster panels (workspace banner, top-cost-tasks ⚡ pill, cost-by-source ⚡+⏳ pills, cost-by-assignee ⚡ pill). Rev-61 chronic-spike recommendation banner gains a new `is-auto-action` state when the workspace has opted into rev-62 auto-pause — solid border + stronger gradient + brand-color tint distinguishes 'desk will act' from 'operator should consider'. Cumulative micro-polish — every rev 22+ has carried at least one.
Per-assignee cost trajectory, persistent-spike recommendation, cost-spike-acked closure receipts
- Per-assignee cost trajectory primitive (extends rev-54 to per-recipient axis). Closes the third trajectory axis on the cost cluster: workspace (rev 53), per-task (rev 54), per-source (rev 60), and now per-assignee (rev 61). New `getAssigneeCostTrajectory()` helper aggregates the rev-54 `task.dailyCostHistory` JSONB by assignee. New `GET /api/cost/by-assignee/{assigneeId}/trajectory` viewer-auth endpoint + matching `GET /api/v1/cost/by-assignee/{assigneeId}/trajectory` bearer-auth mirror. The rev-52 `getCostByAssignee()` also extended with optional `trajectoryDays` so each row carries a `trajectory7d` cents array — the dashboard panel renders an inline sparkline beside the rev-52 cumulative bar on every row. Same brand-amber today gradient as the rev-54 task + rev-60 source trajectory sparklines so all three cost panels read with one consistent visual vocabulary. Pass `__unassigned__` as the route segment to drill into the unassigned bucket. Operators answering 'is this teammate's spend steady or spiking?' no longer have to compute it mentally.
- Cost-spike acked outbound events (closure receipts for task + source spikes). Two new outbound events `task.cost_spike_acked` + `source.cost_spike_acked` fire when an operator clicks the inline ack button (rev-56 task / rev-59 source) or the bulk-ack action (rev-57 task bulk / rev-60 source bulk). Lets external integrations (CRM, FinOps dashboard, board-status integration) close their own alarm tickets / dashboards / paging surfaces without polling. Mirrors the rev-37 `task.unblocked` closure pattern at the cost-spike axis. Wired into all four ack paths — `acknowledgeTaskCostSpike`, `bulkAcknowledgeTaskCostSpikes`, `acknowledgeSourceCostSpike`, `bulkAcknowledgeSourceCostSpikes` — fire-and-forget so a downstream failure can't block the ack itself. Closes the rev-37 closure-receipt pattern at the cost-axis.
- Per-source consecutive spike days counter. New `source.consecutiveSpikeDays` integer column rolls a workspace-TZ-day counter every time the rev-58 daily detector flags the source as spiking; resets to 0 the first day the source isn't spiking. Independent of the rev-59 `costSpikeAckedAt` per-day mute — the counter keeps growing through ack-and-spike-again cycles so a source that's been 'ack me daily' for a week shows the strongest possible chronic-noise signal. Daily sweep updates the column on every workspace pass regardless of the once-per-day Slack/outbound rate limit so the counter reflects actual spike-day state.
- Persistent-spike recommendation banner + 'Nd in a row' pill. Every cost-by-source row whose rev-61 consecutive count is >= 2 surfaces a quiet '⏳ Nd' pill alongside the rev-58 ⚡ pill on the rev-57 panel. Pill stays neutral until the count crosses the chronic threshold (3d) then turns brand-red so the eye reads chronic separately from one-off. New persistent-spike recommendation banner above the row list fires when any visible source has been spiking for >= 3 consecutive days, recommending a structural change (keyword filter, permanent pause, remove the source) rather than a daily mute. Distinct from the rev-58 daily alarm banner (which fires on a single day's anomaly). Closes the named rev-60 next-sprint candidate at the operator nudge surface — addresses 'I keep acking the same feed, what should I actually do?'.
Per-source cost spike digest section, bulk ack, trajectory sparkline, cost CSV export
- Per-source cost spike section in the daily digest email. Closes the rev-59 next-sprint candidate on the email channel. Mirrors the rev-55 per-task spike section at the per-source axis: every owner/admin recipient sees up to 5 spiking feeds with ratio + today vs avg + contributing-task count. Solo founders and email-first operators who don't have the dashboard tab open get the same heads-up Slack-first teams already receive via the rev-58 daily Slack push. Workspace-shared (every recipient sees the same list) since 'this feed is anomalously expensive' is workspace-level diagnostic context — pairs with rev-55 task spikes + rev-48 stale tasks + rev-53 personal cost section to form the full cost-axis story across every channel an operator might be on.
- Bulk per-source cost-spike acknowledgment (dashboard + v1). New `bulkAcknowledgeSourceCostSpikes()` helper + `POST /api/sources/cost-spike-ack/bulk` route + matching `POST /api/v1/sources/cost-spike-ack/bulk` v1 mirror. Bulk-ack bar surfaces inline in the rev-57 cost-by-source panel when 2+ visible rows are spiking. Mirrors the rev-57 task bulk-ack at the per-source axis. Caps at 50 IDs per call. Operators landing on the dashboard after a busy morning no longer have to ack each spiking source individually — collapses the triage to one tap.
- Per-source cost trajectory primitive (helper + endpoints + dashboard sparkline). New `getSourceCostTrajectory()` helper aggregates the rev-54 `task.dailyCostHistory` JSONB by source via `task.sourceSignalIds → signal.sourceId` so today's per-source spend can be plotted against trailing N days (1-30, default 7). New `GET /api/sources/{id}/cost-trajectory` viewer-auth endpoint + matching `GET /api/v1/sources/{id}/cost-trajectory` bearer-auth mirror. The rev-57 `getCostBySource()` also extended with optional `trajectoryDays` so the dashboard panel renders an inline sparkline beside the rev-57 cumulative bar + rev-58 ⚡ pill on every row — same brand-amber today gradient as the rev-54 task trajectory sparkline so the two cost panels read with one consistent visual vocabulary. Pairs with rev-58 alarm + rev-59 ack to form the descriptive → defensive → action triad on every per-source row.
- Cost summary CSV export + cost-by-source row hover polish. New `getWorkspaceCostCsv()` helper + `GET /api/workspace/cost-export` route + new 'Cost summary (CSV)' button in the integrations panel data-export section. Single procurement-friendly CSV carrying trailing-30-day daily AI spend, top-cost tasks, cost-by-source, and cost-by-assignee — all in workspace timezone with section headers (DAILY / BY_TASK / BY_SOURCE / BY_ASSIGNEE) so a SOC 2 / ISO 42001 reviewer can answer 'how is AI cost shaped here?' in one read. Closes the procurement evidence cluster on the cost axis (rev 6 JSON full + rev 7 activity + rev 22 outputs + rev 47 decisions + rev 50 stale tasks + rev 60 cost = six takeaway artefacts). Plus subtle row hover treatment on the cost-by-source panel so the panel reads as scannable rather than static.
Per-source cost-spike auto-pause + ack: RSS poller filter, inline ack button, config UI, v1 endpoints
- Per-source cost-spike auto-pause toggle. Closes the named rev-58 next-sprint candidate. New `workspace.sourceCostSpikeAction` column controls what happens when the rev-58 detector flags a spiking feed: `none`/null (rev-58 alarm-only default — no behavioural change) or `pause` (rev-59 auto-pause — the RSS poller skips spiking feeds for the rest of today's workspace-TZ day until the operator acknowledges). Mirrors the rev-56 per-task auto-pause pattern at the per-source axis. Closes the descriptive → defensive loop on the per-source cost story: rev 57 attribution + rev 58 spike alarm + rev 59 defensive auto-action. Operators with a runaway feed (noisy LinkedIn bridge, RSS broker that flooded one Friday) no longer have to manually intervene — opt-in once and the queue self-heals.
- Per-source cost-spike acknowledgment + inline 'Ack' button. New `source.costSpikeAckedAt` timestamp + `acknowledgeSourceCostSpike()` helper + `POST /api/sources/{id}/cost-spike-ack` route + `SourceCostSpikeAckButton` client component mounted directly beside the rev-58 ⚡ pill on every spiking row of the rev-57 cost-by-source panel. The rev-58 detector now skips sources acked in the current workspace-TZ day, so the ⚡ pill, daily Slack push, outbound `source.cost_spike` event, and rev-59 auto-pause filter all agree on what 'acked' means. Distinct from rev-6 source pause (permanent until resumed) and rev-26 keyword filters (per-item gating). Ack = 'I see this source spike, stop alarming today, but watch for tomorrow.' Mirrors the rev-56 task ack primitive at the per-source axis exactly.
- POST /api/v1/sources/{id}/cost-spike-ack + GET/PUT /api/v1/workspace/source-cost-spike-config. Bearer-auth mirrors of both rev-59 dashboard endpoints in the same cycle the dashboard primitives ship. The cost-axis MCP cluster now closes the **detect → triage → ack → reconfigure** loop on every axis (workspace rev 32 / per-task rev 55-56 / per-source rev 58-59) — eight cost axes deep with three control surfaces (auto-archive config + task cost-spike config + source cost-spike config) and three ack surfaces (task ack + bulk task ack + source ack). The upcoming MCP server has nothing left to design on the per-source cost surface. An MCP host can now opt the workspace into auto-pause programmatically AND acknowledge spikes on individual sources programmatically — pairs with `/api/v1/sources/cost-spikes` (rev 58 detector read) so a watcher agent can detect → triage → ack the alarm without operator input when the feed's spike is expected (e.g. a planned campaign-tracking RSS feed that bursts on launch day).
- Integrations panel 'Per-source cost spike action' section + visual polish. New section in the integrations panel under the rev-49 'Cost guardrails' grouping right below the rev-56 per-task action — two sibling controls that read as the descriptive→defensive layer for the cost-axis story across both per-task and per-source axes. Two buttons: 'Alarm only' (rev-58 default) and 'Auto-pause spiking feeds' (rev-59 opt-in). Active button rendered in brand-color primary; inactive ghost. The rev-58 spike banner copy adapts when auto-pause is on ('Auto-pause is on — these feeds skip the next poll until acked'). Plus quiet ld-cost-source-spike-ack chip styling: transparent background, brand-red border, subtle hover lift — deliberately quieter than the alarm pill itself. Cumulative micro-polish (every rev 22+ has carried at least one). The integrations panel's queue-hygiene cluster is now three primitives deep — rev-20 cost cap + rev-49 stale auto-archive + rev-56/59 cost-spike actions — every defensive control follows the opt-in toggle shape so operators tune the desk's autonomy gradient without ever feeling the desk is 'running away.'
Per-source cost spike: detector, daily Slack push, /api/v1/sources/cost-spikes, inline ⚡ pill
- Per-source cost spike detector. Closes the alarm axis to match the rev-57 per-source attribution axis. New `detectSourceCostSpikes()` aggregates the rev-54 `task.dailyCostHistory` JSONB by source (joined via `task.sourceSignalIds → signal.sourceId`) so today's per-source spend can be compared against the trailing 7-day daily average per-source. Cost is split equally across distinct sources on a task — same defensible aggregation as rev-57 attribution. Surfaces sources spiking 2× their baseline AND >= $0.50 absolute today AND with >= 3 historical days of spend. Where rev-32 fires the alarm at the workspace level and rev-55 at the per-task level, rev-58 fires at the per-source level — answers 'which feed is driving the spike?' without making the operator scroll the rev-57 panel and squint at bar widths.
- Daily Slack push + outbound source.cost_spike event. New `pingSourceCostSpikes()` sweep added to `runDailyDigest()` mirroring `pingCostSpikes` (rev 32) and `pingTaskCostSpikes` (rev 55) shape exactly. Pings Slack via the new `buildSourceCostSpikeSlackPayload()` block (header `:zap: Per-source cost spike` + listing each spike with ratio + today vs avg + contributing-task count + source type), fires the new `source.cost_spike` outbound event via `dispatchSourceCostSpikeWebhook()`, writes a `source_cost_spike` activity-log entry. Rate-limited to once per workspace per 24h matching the rev-32 / rev-55 pattern. Dead-Slack-webhook auto-clear matches `notifyBriefToSlack`. Slack now carries the third axis on the cost alarm surface — workspace (rev 32), per-task (rev 55), per-source (rev 58) — so external automations (CRM, FinOps dashboard, board-status integration) can route the alarm by feed instead of just by workspace.
- Inline ⚡ pill + spike banner on rev-57 cost-by-source panel. Every row currently spiking gets a brand-red ⚡ N× pill in the row header alongside the rev-57 cost amount + percent + label. Tooltip surfaces 'Today $X.XX vs trailing-7d avg $Y.YY — mute or filter to control.' Spiking rows also gain a red left-border accent + tinted amount color matching the rev-57 polish on the rev-51 top-cost-tasks panel — three cost panels, one consistent visual vocabulary. Plus a soft amber→red banner above the source list when any source is spiking, summarising the loudest spike inline so operators see the alarm without reading the full row breakdown. The dashboard now reads cumulatively (cost + percent), trend-aware (sparkline still in TodayPanel), and alarm-aware (rev-58 pill + banner) on the same per-source axis — the same three-level read shipped on the per-task axis in revs 51/54/55.
- GET /api/v1/sources/cost-spikes (close v1 parity gap in lockstep). Bearer-auth endpoint mirrors the rev-58 dashboard primitive in the same cycle the dashboard surface ships (cadence pattern that started rev 37 and held through rev 57 continues). Pairs with `/api/v1/cost/by-source` (rev 57 attribution) + `/api/v1/tasks/cost-spikes` (rev 55 per-task) + the existing rev-32 workspace-level `workspace.cost_spike` outbound event as the three-axis alarm cluster on the protocol-bound surface. The cost-axis MCP cluster now matches across both axes: per-task (rev 51) → per-assignee (rev 52) → per-source (rev 57) on the attribution side; workspace (rev 32) → per-task (rev 55) → per-source (rev 58) on the alarm side. The MCP server has nothing left to design on the cost-spike surface — exclusively protocol-translation work.
Per-source cost attribution, end-of-day spend forecast, bulk cost-spike ack, cost panel polish
- Per-source cost attribution panel + GET /api/v1/cost/by-source. Closes the cost-axis cluster on the upstream side. New `getCostBySource()` helper joins per-task AI spend (the rev-51 cumulative columns) through `task.sourceSignalIds[] → signal.sourceId → source` so the dashboard and v1 surface answer 'which source is driving the most AI spend?'. Cost is split equally across the distinct sources contributing to a task — a defensible aggregation that avoids over-weighting a chatty source landing alongside a load-bearing one. New `CostBySourcePanel` mounts as a sibling of the rev-51 top-cost-tasks panel and rev-52 cost-by-assignee panel; the three together form the four-axis cost-observability cluster (per-task / per-recipient / per-source / per-cycle). Mirrored on `/api/v1/cost/by-source` so MCP hosts can answer the same question with one bearer-auth call. The MCP-cost surface is now five axes deep on the protocol-bound side and functionally complete on every axis.
- End-of-day spend forecast on TodayPanel. Linearly extrapolates today's current spend across the remaining hours of the workspace-TZ day so operators get a forward-looking answer to 'if today keeps going at this pace, will I hit my cost cap before the day rolls over?'. New `projectedEndOfDayCents` + `hoursElapsedToday` fields on the existing `TodaySnapshot` shape (zero schema cost — pure derived state). Inline chip beside the rev-53 7d-baseline + sparkline turns amber and shows percent-of-cap when the projection would breach. Pairs with the rev-21 80% cap warning + rev-32 spike alarm: the cap warning fires at a hard threshold, the spike alarm fires on baseline drift, the projection lets the operator see where today is going *before* either alarm fires. Suppressed in the first hour of the day to avoid noisy extrapolation; clamps to actual spend after 23h so a near-finished day doesn't display a slightly-larger forecast.
- Bulk cost-spike acknowledgment (dashboard + v1). Mirrors the rev-56 single-task ack across many task IDs in one call. New `bulkAcknowledgeTaskCostSpikes()` helper + `POST /api/tasks/cost-spike-ack/bulk` route + matching `POST /api/v1/tasks/cost-spike-ack/bulk` v1 mirror in lockstep. New bulk-ack bar surfaces inline in the rev-51 `TopCostTasksPanel` when 2+ visible tasks are spiking — operators landing on the dashboard from the rev-55 daily Slack push (which lists up to 5 spikes) can now clear all of them in one tap instead of scrolling each card individually. Caps at 50 IDs per call, matching the rev-26 / rev-33 / rev-34 / rev-36 bulk surface vocabulary. Spiking rows in the panel also gain a quiet ⚡ pill so the row reads as alarmed without screaming.
- Cost cluster polish + 'View breakdown ↗' jump from TodayPanel. Three small cumulative pieces of cost-cluster polish. (a) The TodayPanel spend stat now carries a 'View breakdown ↗' link that scrolls smoothly to the rev-51 top-cost-tasks panel — closes the rev-33-to-rev-57 cost navigation loop so operators no longer have to scroll to find the per-task / per-team / per-source breakdowns. (b) The rev-57 per-source panel uses a teal→amber gradient bar that visually anchors it as a sibling to the rev-51 brand→amber and rev-52 indigo→teal panels — three complementary takes on the same dollar figure, one consistent visual vocabulary. (c) The rev-51 top-cost-tasks panel now decorates spiking rows with a quiet red left-border accent + tinted amount color so operators can scan the panel and triage by alarm state without reading every meta line. Cumulative micro-polish — every rev 22+ has carried at least one.
Per-task cost spike auto-pause + acknowledgment, v1 cost-spike-config + ack endpoints, inline ack button, integrations panel section
- Per-task cost spike auto-pause toggle (opt-in). Closes the named rev-55 next-sprint candidate. New `workspace.taskCostSpikeAction` column controls what happens when a task is detected as spiking by the rev-55 detector: `none`/null (rev-55 alarm-only default — no behavioural change) or `pause` (rev-56 auto-pause — the pulse engine's `selectNextTask` filter skips spiking tasks until the operator explicitly acknowledges). Mirrors the rev-49 stale-task auto-archive opt-in pattern + the rev-20 workspace-level cost-cap auto-pause pattern at the per-task axis. Closes the descriptive → defensive loop on the per-task cost story: rev 51 cumulative + rev 54 trajectory + rev 55 spike alarm + rev 56 defensive auto-action. Operators with a runaway task that's eating the budget no longer have to manually intervene — opt-in once and the queue self-heals.
- Per-task cost spike acknowledgment (POST /api/tasks/{id}/cost-spike-ack + v1 mirror). New `task.costSpikeAckedAt` timestamp + `acknowledgeTaskCostSpike()` helper. The rev-55 detector now skips tasks acked in the current workspace-TZ day, so the ⚡ pill, daily Slack push, digest section, and rev-56 auto-pause filter all agree on what 'acked' means. Distinct from rev-23 pin (permanent — exempts forever) and rev-50 renew (resets staleness). Ack = 'I see this spike, stop alarming today, but the underlying spike is real and I want to keep watching for tomorrow.' Mirrored on `/api/v1/tasks/{id}/cost-spike-ack` so the cost-axis MCP cluster now closes the detect → triage → ack → reconfigure loop without a single dashboard-only path.
- Inline 'Ack' button beside the rev-55 ⚡ pill. New `CostSpikeAckButton` mounts directly beside the rev-55 ⚡ pill on every spiking active-work card. One click POSTs `/api/tasks/{id}/cost-spike-ack` and refreshes — the pill disappears, the auto-pause filter (when enabled) releases the task, the daily Slack push and digest section both skip it. Visual weight is deliberately quieter than the pill itself (no background, no pulsing) — the pill is the alarm; the button is the answer. The active-work card pill row now reads at four levels: cumulative spend (rev 51) → trajectory shape (rev 54) → spike alarm (rev 55) → operator response (rev 56) — all in the same line.
- GET/PUT /api/v1/workspace/cost-spike-config + integrations panel UI. Bearer-auth endpoint reads/writes `workspace.taskCostSpikeAction` so MCP hosts can flip the workspace's cost-spike behaviour programmatically. Indexed in the /api/v1 self-describing index. Plus a new 'Per-task cost spike action' section in the integrations panel (alongside the rev-49 stale-task auto-archive section under 'Cost guardrails'/'Queue hygiene') with two buttons: Alarm only (default) and Auto-pause spiking tasks. The cost-axis story now has TWO operator control surfaces on the protocol-bound side (cost-spike-config + auto-archive-config) plus ONE counter-action surface (cost-spike-ack). Closes the named rev-55 follow-up; the cost-axis MCP cluster is six axes deep + two control surfaces + one ack surface — the upcoming MCP server has nothing left to design on the cost surface.
Per-task cost spike: detector, daily Slack push, /api/v1/tasks/cost-spikes, inline pill, digest section
- Per-task cost spike detector + daily Slack push + outbound event. Closes the named rev-54 next-sprint candidate. New detectTaskCostSpikes() helper mirrors the rev-32 workspace-level detectCostSpike() at the per-task axis using the rev-54 dailyCostHistory primitive. Surfaces every open (non-done) task whose today spend in the workspace timezone is >= 2× the trailing 7-day daily average AND >= $0.50 absolute, sorted by spike ratio descending (loudest first). Skips tasks with fewer than 3 historical days of non-zero spend so a brand-new expensive task can't trigger on its first day. New pingTaskCostSpikes() sweep added to runDailyDigest() — pings Slack via the new buildTaskCostSpikeSlackPayload() block, fires the new task.cost_spike outbound event, writes a task_cost_spike activity-log entry. Rate-limited via the activity log to once per workspace per 24h, same shape as the rev-32 workspace-level alarm. The rev-32 alarm answers 'is this workspace anomalously expensive?'; rev-55 answers 'WHICH TASK is anomalously expensive?' — the missing diagnostic axis on the cost-spike story.
- GET /api/v1/tasks/cost-spikes — bearer-auth mirror. Bearer-auth endpoint returns the same per-task spike list the dashboard pill + daily Slack push + digest section read from. Each row carries taskId, title, status, priority, todayUsd / baselineUsd / spikeRatio, plus the assignee block when set. Indexed in the /api/v1 self-describing endpoint list. Closes the v1 parity gap on the rev-55 dashboard primitive in the same cycle the dashboard primitive ships (the cadence pattern that started rev 37 holds through rev 55). Pairs with /api/v1/cost/today (workspace axis), /api/v1/cost/by-assignee (per-recipient axis), /api/v1/tasks/top-cost (per-task absolute cost), and /api/v1/tasks/{id}/cost-trajectory (per-task trajectory) as the cost-axis MCP cluster. Until rev 55 an MCP host driving the desk had to read every task's cumulative cost (rev 51) and 7-day trajectory (rev 54) and compute the spike condition itself; rev 55 makes the answer load-bearing on the protocol surface.
- Inline ⚡ cost-spike pill on every spiking task card. Every active-work card whose task is currently spiking gets a pulsing red ⚡ N× pill in the header pill row alongside the rev-51 cost pill and the rev-54 trajectory sparkline. Pure derived state — the dashboard payload now includes the spike list and the renderTaskCard hot path looks up the spike via O(1) map lookup. The cost story now reads cumulative ($X.XX, rev 51) → trajectory (7-bar sparkline, rev 54) → spike (⚡ N×, rev 55) across three pills in one row. The active-work card pill row has accumulated 8+ pills + 5 affordance rows over rev 21–54; rev 55 is the first ALARM-level pill on the cost axis (vs ambient context). Pulsing animation matches the rev-37 'now ready' visual vocabulary so the alarm reads as load-bearing without overwhelming the queue.
- Per-task cost spike section in the daily digest email. New buildTaskCostSpikesSection() helper drops a workspace-shared '⚡ Per-task cost spike' block into every owner/admin recipient's digest email. Solo founders and email-first operators who don't have the dashboard tab open get the same heads-up that Slack-first teams already had via the rev-55 daily Slack push. Pre-fetched once per workspace inside runDailyDigest() and reused across every recipient so the call is cheap. The rev-36 previewDigestForUser() admin testing path also mirrors the change so admins iterating on configuration see the same surface. Closes the cost-spike push surface across all three operator-loaded channels: in-app pulsing pill (the operator's eye), Slack push (the operator's chat), digest email (the operator's inbox). The full per-task cost story is now end-to-end visible without the operator having to mentally aggregate cumulative + trajectory + cap data.
Per-task cost trajectory: pulse plumbing, dashboard sparkline, v1 endpoint, top-cost panel mini-chart
- Per-task daily-cost history + pulse engine plumbing. Closes the named rev-53 next-sprint candidate. New task.dailyCostHistory JSONB column the pulse engine appends to on every cycle: workNextTask snapshots the global tokenSink before/after runAiTaskSession, computes the per-task delta the same way the rev-51 cumulative columns are populated, then buckets the delta into today's slot keyed by the workspace-timezone ISO date (YYYY-MM-DD) via the new formatLocalDateKey helper. Trimmed to the trailing 30 days on every write so the JSONB never grows unbounded on long-running tasks. Pure history — the rev-51 cumulative columns remain authoritative for total spend; this map is the trajectory primitive that rev 54's sparkline + cost-trajectory endpoint read from.
- Per-task cost trajectory sparkline on every active-work card. New TaskCostSparkline component renders inline next to the rev-51 💸 cost pill on every active-work card whose dailyCostHistory has at least one non-zero day. 7-bar mini-chart, today rightmost, today highlighted with brand-amber so the eye lands on the most recent value first. Pairs with the rev-53 workspace-level cost sparkline on TodayPanel — same visual vocabulary, different scope (workspace ↔ per-task). Operators triaging a noisy task can now see 'is this task spiking cost or steady?' at a glance without leaving the card. The active-work card pill row has accumulated 8+ pills + 5 affordance rows over rev 21–51; rev 54 is the first piece of in-card visualisation rather than just text/pills, anchoring the per-task cost narrative across both reading levels.
- GET /api/v1/tasks/{id}/cost-trajectory + matching dashboard endpoint. Bearer-auth endpoint returns the trailing N daily cost buckets (default 7, max 30) for a single task in workspace timezone, oldest → newest, with zero-fill for days the task wasn't worked. Mirrored on the dashboard side at GET /api/tasks/{id}/cost-trajectory (viewer+ auth) so external integrations + the dashboard share one server-side implementation via getTaskCostTrajectory(). Closes the v1 parity gap on the rev-54 dashboard primitive in the same cycle the dashboard primitive ships (the cadence pattern that started rev 37 holds). The /api/v1/tasks/top-cost endpoint also gained a trajectory7d cents array on every row so MCP hosts rendering 'top spenders' can render the trajectory shape without a follow-up call per task. The cost-axis MCP cluster (per-cycle/per-task/per-teammate/today/per-task-trajectory) is now 5 axes deep — exclusively protocol-translation work for the upcoming MCP server.
- Top-cost-tasks panel: trajectory mini-chart on every row + visual polish. The rev-51 TopCostTasksPanel listed cumulative spend + status + bar widths but the trajectory shape was invisible. Rev 54 adds a 52×14 trajectory sparkline inline on every row alongside the existing cost label so an operator can answer 'are my expensive tasks expensive because they're long-running or because today spiked?' without drilling in. Plus subtle CSS polish on the rev-51 cost pill: it's now a flex container so the inline sparkline sits beside 💸 $X.XX without the layout breaking; today-bar hover state inverts to deep-amber for a tactile click affordance. Cumulative micro-polish — every rev 22+ has carried at least one.
Per-recipient cost in digest, /api/v1/cost/today, dashboard self-cost highlight, 7-day cost sparkline
- Per-recipient cost section in the daily digest email. Closes the named rev-52 next-sprint candidate. The rev-52 cost-by-assignee aggregation feeds a new buildPersonalCostSection() helper that drops a 'Your AI spend' block into each recipient's digest email when the workspace has 2+ named assignees. Each owner/admin sees 'Your queue is N% of tracked workspace spend ($X.XX across M tasks, of $Y.YY workspace total)' with a deep-link to the dashboard panel. Solo desks skip the section so the line never reads tautologically as '100% you'. Pre-fetched once per workspace inside runDailyDigest() and reused across every recipient so the call is cheap. The previewDigestForUser() admin path also includes the section so admins iterating on configuration can verify it works. Closes the per-recipient personalisation symmetry across rev-25 assignee section + rev-31 mentions section + rev-49 personal stale section + now rev-53 personal cost section — the daily digest is fully personalised on every load-bearing per-recipient surface.
- GET /api/v1/cost/today endpoint. Cost-only Today projection on the v1 surface. Returns today/yesterday/baseline7d cents, the new rev-53 7-day daily-cost sparkline, dailyCostCap + capPercent, and the byAssignee[] breakdown — self-contained one-call cost surface for MCP hosts. Pairs with rev-52 /api/v1/cost/by-assignee (per-recipient), rev-51 /api/v1/tasks/top-cost (per-task), rev-13 /api/v1/runs (per-cycle), rev-18 /api/v1/stats (workspace 7-day) as the five-axis cost cluster on the protocol-bound side. Where /api/v1/workspace/today returns the full snapshot, /api/v1/cost/today returns a cost-only projection so MCP hosts asking 'what is today's spend trajectory?' don't fetch and discard everything else. The MCP-cost surface is functionally complete after rev 53.
- Self-cost highlight on the rev-52 cost-by-assignee panel. The rev-52 dashboard panel listed every teammate's cost slice without highlighting the current operator's row. Multi-operator teams scanning the panel had to read every name to find their own queue. Rev 53 closes the gap: a brand-color callout banner above the list reads 'Your queue is N% of tracked spend ($X.XX across M tasks)' when the current user has cost accrued, and the matching row in the list itself gets a brand-color left-border + softer green bar fill + 'you' pill in the label. Solo desks skip the callout (the row would always be 100% self). Visual polish — every rev 22+ has carried at least one. The rev-53 callout is the first thing the operator's eye lands on in a multi-operator workspace's cost panel, anchoring 'where am I in this picture?' before they start scanning the rest.
- 7-day daily-cost sparkline on the Today panel + cost trajectory. TodaySnapshot extended with costSparkline7d: number[7] (cents per day for the trailing 7 days INCLUDING today, oldest → newest left-to-right). The bucketCostSparkline() helper aggregates desk_run rows from the existing rev-53 trailing-window query — no extra round-trip. New CostSparkline component renders inside the TodayPanel spend stat as a 7-bar mini-chart: today is the rightmost bar with brand-amber gradient highlight; older days fade to neutral so the eye lands on today first. Hidden when every day is zero (fresh workspace). Pairs with the rev-34 yesterday delta (volatility), rev-35 7d-avg baseline (trend number), and now rev-53 trajectory (visual shape) so the spend stat reads three complementary horizons without overwhelming the dashboard. The cost-axis instrument cluster is now 11 layers deep: rev 7 transparency, rev 8 per-cycle drill-down, rev 9 30-day projection, rev 20 hard cap, rev 21 80% warning, rev 32 spike alarm, rev 33 today snapshot, rev 34 yesterday delta, rev 35 7d-avg baseline, rev 51 per-task, rev 52 per-teammate — and now rev 53 daily trajectory.
Per-assignee cost breakdown: dashboard panel, cost-spike scoping, v1 endpoint, MCP-cost story closes
- Per-assignee cost breakdown helper + dashboard panel. New getCostByAssignee() helper aggregates the rev-51 per-task cumulative cost columns (totalInputTokens + totalOutputTokens) by task.assignedToUserId so multi-operator desks can answer 'where is our AI cost going by teammate?' at a glance. Joins to the users table for human-readable name/email rendering. Tasks with no assignee aggregate into a synthetic unassigned bucket so the breakdown reconciles with the workspace total. New CostByAssigneePanel client component renders alongside the rev-51 TopCostTasksPanel as the per-recipient axis: each row shows the teammate's total cumulative spend, what percent of the workspace total that represents, a proportional indigo→teal bar (visually distinct from the rev-51 brand→amber bar so the two panels read as siblings), and the underlying token + task count. Hidden on solo desks. Strategic significance: the cost story has been built up across 9 revs (rev 7 transparency, rev 8 per-cycle drill-down, rev 9 30-day projection, rev 20 hard cap, rev 21 80% warning, rev 32 spike alarm, rev 33 today snapshot, rev 34 yesterday delta, rev 35 7-day baseline, rev 51 per-task) and was almost entirely *vertical* (across time). Rev 52 opens the *horizontal* (across team) axis. ISO 42001 control families on AI cost transparency call out per-team cost attribution as a maturity step — rev 52 closes that gap.
- Cost-spike alarm gets per-assignee scoping (Slack + outbound). Closes the load-bearing rev-51 named follow-up. The rev-32 detectCostSpike() now also returns a byAssignee[] array — top 5 named buckets sorted desc by spend, with name/email/percent/USD. The Slack push from pingCostSpikes() now includes a 'open-task spend by teammate' line when 2+ named assignees show up (omitted on solo desks where the line reads tautologically as '100% you'). Operators reading the alarm see 'today's spend is 3.2× the trailing average — and Maria has $4.20 (40%), Leo has $2.10 (25%)' without leaving Slack. The workspace.cost_spike outbound webhook payload also carries the same byAssignee array so downstream FinOps tools, project trackers, or board-status dashboards can route the alarm by teammate. The rev-32 cost-spike trio (in-app banner + Slack + outbound) now reads as a four-axis: where (workspace/per-team/per-task/over-time).
- GET /api/v1/cost/by-assignee endpoint. Bearer-auth endpoint mirrors the dashboard panel exactly. Same shape, accessible programmatically. Pairs with the rev-51 /api/v1/tasks/top-cost (per-task axis), rev-13 /api/v1/runs (per-cycle cost), and rev-18 /api/v1/stats (workspace 7-day cost) as the four-axis cost-observability surface on the protocol-bound side. Strategic significance: the upcoming MCP server's cost tooling has nothing left to design — the four cost axes (per-cycle / per-task / per-teammate / workspace 7-day) all have v1 mirrors in lockstep with the dashboard, so MCP-cost tools are exclusively protocol-translation work.
- MCP cost story announcement post + visual polish. New blog post 'Where Is Your AI Cost Going? Per-Teammate Cost Visibility for SMB AI Workspaces' explains the per-recipient cost axis as the load-bearing primitive that closes the procurement story for governance-first AI tools. SEO surface area for 'AI cost attribution by team', 'per-team AI spend visibility', 'FinOps for AI workspaces' — buyer-side terms that sit one notch above 'governance-first' in the procurement vocabulary. Cumulative micro-polish on the new dashboard panel: indigo→teal gradient bar fill so the two cost panels stack cleanly, percent pill in brand-color so the proportional reading lands first, italic muted treatment on unassigned rows so they don't compete with named-assignee rows.
Per-task cost attribution: cumulative tokens on every task, dashboard pill, top-cost panel, v1 endpoints
- Per-task cost attribution columns + pulse-engine plumbing. Closes the named rev-49 cost-attribution follow-up. Until rev 51, every layer of the cost story (rev 7 transparency, rev 8 per-cycle drill-down, rev 9 30-day projection, rev 20 hard cap, rev 21 80% warning, rev 32 spike alarm, rev 34 yesterday delta, rev 35 7-day baseline) operated at workspace scope. Operators with a noisy task that had been retried 30 times had no surface to identify the runaway cost-eater. Rev 51 closes that. New task.totalInputTokens + totalOutputTokens columns. Inside workNextTask(), snapshot the global token sink before runAiTaskSession, compute the delta after, and increment the task columns by exactly that delta via a SQL `column + delta` clause in each of the three exit paths (blocked / complete / continue). Reflection + ideation token spend is workspace-level and intentionally not attributed to any task. The global desk_run total still receives every cycle's tokens — this delta is just the slice that belongs to the in-flight task.
- Top-cost-tasks dashboard panel. New TopCostTasksPanel sidebar component renders above the integrations panel, listing the open (non-done) tasks whose cumulative AI spend is highest, capped at 5 rows. Each row carries a brand-color proportional bar, the cost amount in tabular-num typography, the task status pill, the task title (click to scroll-into-view + flash-highlight the matching active-work card), and a meta line with session count + total tokens. Hidden when no task has accrued any cost yet — fresh workspaces don't see it. Pairs with the rev-13 desk health widget (workspace-level cost stat) and the rev-32 cost-spike alarm for the full descriptive (top spenders) → defensive (alarm) trio.
- Per-task cost pill on every active-work card + on stale-task panel rows. Cumulative dashboard polish — until rev 51, the staleness pill (rev 48), focus ribbon (rev 29), priority pill (rev 21), due pill (rev 22), assignee chip (rev 16), comment-count badge (rev 35), and blockers pill (rev 36) all hung off the active-work card pill row, but cost was workspace-level only. Rev 51 adds an unobtrusive 💸 $X.XX pill (with token-count tooltip) on every task card whose cost > 0, plus the same pill on every stale-task panel row so reviewers see 'this stale task burned $X.XX before being abandoned' inline — strongest possible 'auto-archive saves money' signal alongside the rev-49 lifecycle.
- GET /api/v1/tasks/{id}/cost + GET /api/v1/tasks/top-cost (v1 surface) + dashboard mirror. New bearer-auth endpoints expose per-task cost on the protocol-bound surface in lockstep with the dashboard primitive. /tasks/{id}/cost returns { totalInputTokens, totalOutputTokens, estimatedCostUsd, sessionCount, costPerSessionUsd }. /tasks/top-cost returns the same TopCostTaskRow[] shape the dashboard panel uses. New dashboard mirror at /api/tasks/{id}/cost (viewer+ auth) so external integrations + the dashboard share one server-side implementation. The /api/v1/tasks list endpoint now also projects totalInputTokens + totalOutputTokens on every row so MCP hosts can render 'task X cost $Y' without a follow-up call. Stale-task CSV export gained three new cost columns (totalInputTokens, totalOutputTokens, estimatedCostUsd) so SOC 2 / ISO 42001 reviewers reading the takeaway artefact see the cost story attached to every stale task. Pairs with the rev-13 /api/v1/runs (per-cycle cost) and rev-18 /api/v1/stats (workspace 7-day cost) as the three-axis cost-observability surface — the upcoming MCP server's cost tooling has nothing left to design.
Pre-archive warning to assignee, stale-tasks CSV, /api/v1/tasks/auto-archived audit, one-tap renew
- Pre-archive warning notification (per-task push to assignee). The rev 47/48/49 stale-task lifecycle worked at workspace scope — the rev-48 stale_warning Slack push said 'your workspace has N stale tasks', and the rev-49 task.auto_archived event was a closure receipt. Multi-operator teams needed a per-task push to the *assignee* saying 'this is yours and it's about to disappear'. New task.archiveWarnedAt timestamp column + sweepArchiveWarnings() helper + runArchiveWarnings() cron sweep at the end of runDailyDigest() (immediately before runStaleTaskAutoArchive so an assignee gets at least one cron-cycle of heads-up before their task disappears). Per-task Slack post (one message per task so the assignee can deep-link straight to the right card) + per-task Resend email to the assignee + new task.archive_warning outbound event. Rate-limited via the archiveWarnedAt stamp so a second cron tick within the warning window can't double-fire. Closes the missing edge of the rev 47/48/49 lifecycle: workspace-level surface (rev 48) → per-task per-recipient warning (rev 50) → workspace-level closure (rev 49).
- One-tap renew button on every stale task. Cheapest possible operator action for the rev-48/49 staleness lifecycle. Distinct from rev-23 pin (permanent — exempts from auto-archive forever) and rev-14 operator-note (which feeds the AI). Renew = 'the queue is right, just keep it visible'. New POST /api/tasks/:id/renew route + matching POST /api/v1/tasks/:id/renew bearer-auth mirror + new TaskRenewButton component that mounts inline on every task showing the rev-48 staleness pill. One click bumps updatedAt + clears the rev-50 archiveWarnedAt stamp so the staleness countdown resets. Distinct from delete-and-recreate (destructive) and pin-and-unpin (visible state change). Closes the friction gap between rev-23 pinning (forever) and rev-14 operator-note (AI direction) — the operator's 'I'm still working on this, don't auto-archive' affordance.
- Stale-tasks CSV export — close procurement evidence quintet. Procurement evidence pattern grows: rev 6 JSON full + rev 7 activity CSV + rev 22 outputs CSV + rev 47 decisions CSV + rev 50 stale-tasks CSV. SOC 2 / ISO 42001 reviewer asking 'show me everything that's gone quiet over the audit window' now has a one-click answer. New GET /api/workspace/stale-tasks-export route + getWorkspaceStaleTasksCsv() helper. Optional ?thresholdDays=N (1-60) overrides the default 5d staleness floor for different audit lenses ('anything quiet for 14+ days' vs 'anything quiet for 5+ days'). Surfaced as the fifth button in the integrations panel data-export section beside JSON / activity / outputs / decisions.
- GET /api/v1/tasks/auto-archived (audit-trail closure on the v1 surface). Until rev 50 an MCP host could see *current* stale tasks (rev 48 /tasks/stale), the task.auto_archived outbound event (rev 49 push), and the workspace activity log (rev 20 /activity) — but no single endpoint answered 'what has the desk auto-archived recently?'. Rev 50 closes that gap with a bearer-auth endpoint identifying auto-archived tasks by the synthetic blocker prefix the rev-49 sweep stamps on every row. Bounded query window (default 30 days, max 365). Pairs with the rev-48 /tasks/stale + rev-49 task.auto_archived outbound event as the full audit trail of the rev 47/48/49/50 stale-task lifecycle on the protocol-bound surface.
Stale-task auto-archive lifecycle close, per-recipient digest scoping, v1 + dashboard config, countdown pill
- Stale-task auto-archive (close lifecycle: rev 47 detect → rev 48 surface → rev 49 act). Rev 47 detected stale tasks (StaleTasksPanel + GET /api/v1/tasks/stale); rev 48 surfaced them across digest + Slack + outbound + in-line pill; rev 49 closes the lifecycle by letting the desk *act*. New workspace.staleTaskAutoArchiveDays integer column (nullable = off, opt-in by design, valid range 7-90 days). New runStaleTaskAutoArchive() sweep runs at the end of runDailyDigest() — for every onboarded workspace with a configured threshold, calls sweepStaleTaskAutoArchive() which marks non-pinned stale tasks past threshold as status=done with a synthetic blocker note ('Auto-archived after N days idle (threshold: Md). Re-queue if you still need it.') so the audit trail records what happened. Capped at 25 tasks per sweep so a workspace that just enabled the feature with hundreds of stale tasks doesn't see them all archived in one tick. Pinned tasks are deliberately excluded — pinning is the rev-23 affordance for 'keep around regardless of staleness.' Slack push via new buildTaskAutoArchivedSlackPayload() block. New OutboundEvent task.auto_archived + dispatchTaskAutoArchivedWebhook(). Strategic significance: closes the lifecycle that started in rev 47. The full stale-task story is now end-to-end across detect → surface → act, with operator-controlled timing (rev 49 is opt-in) so the desk respects the queue but can self-clean when the operator wants it to.
- Per-recipient stale-task scoping in the daily digest (close named rev-48 follow-up). Rev 48's running state explicitly named 'per-recipient stale-task scoping in digest' as the next-sprint candidate. Rev 49 ships it. getStaleTasks() now accepts an optional assignedToUserId. The daily digest cron (and the rev-36 previewDigestForUser() admin testing path) loads per-recipient stale-task data alongside the rev-48 workspace-shared data, deduplicates against the shared list, and renders a separate 'N of your tasks quiet for 5+ days' section. Solo-operator workspaces continue to see only the workspace-shared section (since their assignee section IS the workspace section). Multi-operator teams now see 'the desk has 8 stale tasks; 3 are yours' without enumerating the dashboard. Strategic significance: closes the per-recipient personalisation symmetry across rev-25 assignee section + rev-31 mentions section + rev-48 stale section + now rev-49 personal stale section. The daily digest is now fully personalised across every load-bearing surface that benefits from per-recipient context.
- Workspace auto-archive config — admin route + integrations panel + v1 mirror + per-user filter. New PATCH /api/workspace/auto-archive-config route (admin-only via requireWorkspaceRole). New GET/PUT /api/v1/workspace/auto-archive-config bearer-auth mirror so MCP hosts inherit the config primitive at parity with the dashboard. New 'Stale-task auto-archive' section in the integrations panel under the rev-20 daily cost cap section so the two queue-hygiene primitives (cost cap + stale archive) read as siblings. The v1 GET /api/v1/tasks/stale endpoint also gained an optional assignedToUserId query param mirroring the rev-49 digest change so MCP hosts on multi-operator desks can answer 'which of my tasks are rotting?' programmatically. Strategic significance: every dashboard primitive shipped this rev has a v1 equivalent shipped in the same rev — the cadence pattern that started in rev 37 (blockers + v1 blockers in lockstep) continues. The rev-49 auto-archive lifecycle has nothing left for the upcoming MCP server to design.
- Countdown-to-auto-archive on the in-line stale pill + dashed-card visual cue. The rev-48 in-line stale pill ('⏳ Nd quiet') now also surfaces 'archives in Md' or 'archives today' when the workspace has the rev-49 auto-archive threshold configured AND the task is past the rev-48 staleness threshold. Tasks within 3 days of auto-archival render the pill in red with a 2.4s amber→red pulsing-shadow animation, and the surrounding card border switches to dashed-red — lower-key than rev-23 pinned glow or rev-37 now-ready pulse since the action being signaled is 'consider intervening' not 'this is ready'. Cumulative micro-polish (every rev 22+ has carried one). Strategic significance: rev 48 made staleness in-queue visible; rev 49 makes auto-archive *anticipated*. Operators see the lifecycle countdown without leaving the active-work card — pin the task to keep it (the rev-23 affordance is now load-bearing on the rev-49 lifecycle), re-queue with a cycle to bump updatedAt, or operator-note it to surface intent. The pill row now reads across four orthogonal axes: triage (priority/due/assignee), discussion (comments + mentions), rot (staleness), and lifecycle (auto-archive countdown).
Stale-task push across digest + Slack + outbound, v1 mirror, in-line staleness pill
- Stale-task section in the daily digest email (close named rev-47 follow-up). Rev 47's running state explicitly named 'stale-task auto-flag email digest' as the rev-48 candidate, citing the new dashboard panel as a diagnostic surface that only fires when an operator is looking at the dashboard. Rev 48 closes the loop for solo founders and email-first operators who don't have the dashboard tab open. New buildStaleTasksSection() helper renders up to 5 stale tasks with day-counts, status pills, and inline blocker call-outs into the existing daily digest email body. Stale tasks are workspace-shared (every owner/admin recipient sees the same list, since 'this work is rotting' is workspace-level diagnostic context) — distinct from the rev-25 per-recipient assignee section and the rev-31 per-recipient mentions section which are personal-inbox state. Pure derived state — reuses the rev-47 getStaleTasks() helper verbatim, no schema change.
- Stale-task daily Slack push + new task.stale_warning outbound webhook event. Cron sweep added at the end of runDailyDigest() — pingStaleTasks(). For every onboarded workspace whose Slack webhook is set, fetches getStaleTasks() and (a) pings Slack via the new buildStaleTasksSlackPayload() block (header + section listing up to 5 stale tasks with N-d quiet pills + bulb context CTA), (b) dispatches the new task.stale_warning outbound webhook event via dispatchStaleTasksWebhook(). Rate-limited via a stale_tasks activity-log entry to once per workspace per 24h with the same dead-Slack-webhook auto-clear path as pingStuckLoops() and pingCostSpikes(). New OutboundEvent value task.stale_warning + matching dispatcher in src/lib/outbound.ts. Pairs with the rev-47 dashboard panel + rev-48 digest section as the third stale-task push channel — the same trio shape as the rev-32 cost-spike alarm (in-app banner + Slack push + outbound). External automations (CRM, project tool, board-status integration) can now mirror 'this work is rotting' as a workspace-level signal.
- GET /api/v1/tasks/stale endpoint (v1 parity for rev-47 dashboard primitive). New bearer-auth endpoint mirrors the rev-47 dashboard StaleTasksPanel exactly. Accepts optional thresholdDays (1-60, default 5) and limit (1-50, default 10). Delegates to the same getStaleTasks() helper the dashboard uses so the two surfaces share one server-side implementation. Until rev 48 an MCP host driving the desk could read every task field but couldn't answer the diagnostic question 'which tasks are silently rotting in the queue?' without enumerating /api/v1/tasks and computing staleness client-side. Rev 48 closes that gap. Pairs with the rev-46 /api/v1/decisions endpoint and the rev-37 task.unblocked outbound event as the full task-state observability surface on the protocol-bound side.
- In-line staleness pill on every active-work card. Cumulative dashboard polish — until rev 48, the staleness signal was only visible in the dedicated rev-47 StaleTasksPanel sidebar. Operators triaging a long active-work queue had to cross-reference the dedicated panel against each card by title to know which were stale. Rev 48 surfaces a small ⏳ N-d quiet pill in the active-work card pill row (alongside the rev-21 priority + rev-22 due + rev-23 pinned + rev-16 assignee + rev-26 comment-count pills) for any queued/in_progress task aged 5+ days that isn't pinned. Pure derived state — same staleness rules as the rev-47 server-side helper. The pill row now reads at-a-glance: triage state (priority/due/assignee), discussion state (comments + mentions), and rotting state (staleness) all in one row. Closes the visual-hierarchy gap that the rev-47 StaleTasksPanel didn't address.
Share-page status filter, decisions CSV export, stale-task detector, dashboard hover polish
- Share-page status filter on the related-briefs drill-down (close named rev-46 follow-up). Rev 46's running state explicitly named 'share-page status filtering' as the next-sprint candidate, citing the rev-46 kind filter as a half-completed narrowing surface that needed status to close. Rev 47 ships it. Every shared brief at /share/<token> now lets external readers narrow the rev-45 tag drill-down by status (Approved / Ready / Archived) in addition to kind. The /api/share/{token}/related endpoint accepts an optional status query param; getRelatedSharedArtifactsByTag validates against an explicit PUBLIC_ARTIFACT_STATUSES allowlist that excludes draft (operators don't share drafts in practice — the helper hard-excludes it as a safety net so a bug or operator slip can't accidentally leak draft material to a procurement reviewer). Each result row now renders a status pill alongside the existing kind pill so the reviewer can see status at a glance without clicking through. Strategic significance: a SOC 2 / ISO 42001 reviewer auditing decisions tagged #q3-launch is most often interested in the live (approved) set without seeing every superseded archived revision in the same drill-down. Rev 47 closes the rev-46 kind+status narrowing pair on the public surface.
- Decisions log CSV export — close procurement evidence quartet. The procurement evidence trio (rev 6 JSON full export + rev 7 activity CSV + rev 22 outputs CSV) has been the canonical takeaway artefact set for SOC 2 / ISO 42001 reviewers since rev 22. Rev 47 closes the quartet with a decisions CSV scoped exactly to the rev-9 dashboard 'Decisions log' semantics (status ∈ {approved, archived}, kind ≠ brief). Pairs with the rev-46 GET /api/v1/decisions endpoint as the protocol-bound + procurement-friendly read pair. New /api/workspace/decisions-export route + getWorkspaceDecisionsCsv helper. Mirrors the rev-22 outputs CSV column shape (createdAt, updatedAt, kind, status, title, summary, tags, share URL) so a reviewer reading both side-by-side sees one column vocabulary. The decisions CSV is the takeaway artefact a procurement reviewer asks for when their question is 'what has this team *decided* (not drafted, not noted) over the audit window?' — until rev 47 the only path was to filter the outputs CSV in Excel.
- Stale-task detector panel. Surfaces tasks that have been queued or in_progress for >5 days without progress (no updatedAt bump = no workLog entry from the pulse engine + no operator steering = quietly rotting in the queue). Pinned tasks are excluded server-side; pinning is the rev-23 affordance for 'intentionally kept around regardless of staleness.' Pure derived state — no schema, no migration. Hidden when there are no stale tasks (most workspaces will see nothing here, which is the correct default). Strategic significance: pairs with the rev-12 heartbeat (loop state) + rev-17 stuck-loop banner (cycle cadence) + rev-37 task-unblocked push (transition signal) for the full task-health surface. Those answer 'is the desk alive?', 'is the cycle alive?', and 'is this dependency unblocked?'. Stale-task answers the missing fourth: 'is this task quietly rotting in the queue?' Common causes: blocked on missing context, keeps getting passed over by higher-priority work, or has been superseded but never archived. The dashboard now diagnoses all four failure modes at a glance.
- Dashboard hover polish + panel-target flash. Cumulative micro-polish — every rev 22+ has carried one. Three small but cumulative pieces of visual polish: (a) subtle hover transition on every informational app-pill (priority, status, kind, due) so the pill row feels less inert (interactive chips already had hover treatment, informational pills were dead), (b) strengthened panel-target flash so a deep link from a mention permalink (rev 31) or stale-task jumpTo (rev 47) reads with a clearer 'you arrived here' affordance even on amber-tinted panels (stale-task, stuck-loop), (c) refined share-page status pill + kind pill side-by-side layout so the rev-47 status narrowing reads as a sibling to the rev-46 kind narrowing without crowding. The rev-by-rev discipline of one targeted polish per rev is what keeps the dashboard from drifting toward the design-debt smell that hand-rolled SaaS dashboards usually develop after 47 revs.
Share-page kind filter, /api/v1/decisions, outbound deliveries + retry on v1, activity keyword search on v1
- Share-page kind filter on the related-briefs drill-down (close named rev-45 follow-up). Rev 45's running state explicitly named 'share-page kind/status filtering' as the next-sprint candidate. Rev 45 shipped tag drill-down on /share/<token>; rev 46 lets external stakeholders narrow the drill-down to a single artifact kind (brief / draft / decision / watchlist / note). New kind chip row inside the open panel. The /api/share/{token}/related endpoint accepts an optional kind query param; getRelatedSharedArtifactsByTag validates against the artifactKindEnum exactly so an attacker can't smuggle SQL or unknown values. A procurement reviewer auditing 'decisions tagged #q3-launch' is no longer forced to read every draft tagged the same way to find them.
- GET /api/v1/decisions (close 37-rev v1 parity gap). The rev-9 dashboard 'Decisions log' sidebar panel — last 10 approved/archived non-brief artifacts in a 30-day window — has been dashboard-only for 37 revs, the longest-outstanding v1-vs-dashboard parity gap. Rev 46 mirrors it on the v1 surface as a single bearer-auth call: GET /api/v1/decisions?windowDays=30&limit=10. Both surfaces now route through the same getRecentDecisions helper, so the dashboard and v1 return identical shapes. As a small fix-along-the-way, the dashboard surface now also explicitly excludes briefs (matching the rev-9 documented intent — the spec said 'non-brief' but the implementation didn't enforce it). Pairs with rev-13 /api/v1/runs, rev-18 /api/v1/stats, rev-20 /api/v1/activity, rev-43 /api/v1/workspace/summary, and rev-45 /api/v1/workspace/today as the six-axis 'how is this desk doing' instrument cluster on the protocol-bound surface.
- GET /api/v1/outbound/deliveries + POST /api/v1/outbound/deliveries/{id}/retry (close 27-rev v1 parity gap). Rev 19 turned outbound delivery into a real router (per-event subscriptions + delivery log + one-click retry). Until rev 46 the delivery log + retry button were dashboard-only — an MCP host monitoring an integration couldn't answer 'did the artifact.ready event fire on this artifact?' or 'which downstream URL is 502'ing?' or 'retry that one that just failed' without opening a browser. Rev 46 mirrors both endpoints on the v1 surface. Reuses the existing listOutboundDeliveries / retryDelivery helpers so the two surfaces share one server-side implementation. Pairs with rev-19 outbound subscriptions to close the integration debugging loop on the protocol-bound surface — an MCP-host integration-monitor agent can now both detect failures and recover from transient downstream blips programmatically.
- Activity keyword search on /api/v1/activity (close v1 parity gap on rev-38 dashboard primitive). Rev 38 shipped the dashboard inline activity-log search; rev 46 mirrors it on v1 with an optional q query param. Runs a SQL-level case-insensitive ILIKE across detail AND kind, capped at 200-char input length so the LIKE pattern stays bounded. Composable with the existing since and kind filters — all three must match for a row to surface. Until rev 46 an MCP host driving the desk could read the activity log paginated by since/kind but had no way to answer 'when did we last get a Slack send error?' or 'did we have a cost spike last week?' without pulling everything and filtering client-side. The full audit trail story on the v1 surface (cycles + signals + outputs + activity + decisions + outbound deliveries) is now keyword-searchable end-to-end.
Share-page tag drill-down, v1 workspace export + today snapshot, source health on v1
- Share-page tag drill-down for stakeholders (close named rev-44 follow-up). Rev 44's running state explicitly named 'share-link tag-based search for stakeholders' as the next-sprint candidate. Rev 39 shipped the in-app cross-entity tag drill-down for operators; rev 45 brings the same retrieval primitive to external readers. Every shared brief at /share/<token> now renders its tags as clickable chips below the body. Tap a chip and an inline panel expands listing other share-enabled briefs in the same workspace tagged the same way (newest-first, up to 12). Each result is a link to the related brief's own /share/<token> page so external readers can navigate the related set without ever leaving the public surface. Critical scoping rule: only artifacts with a publicShareToken set are returned, so the drill-down never exposes briefs the operator hasn't explicitly chosen to share. New public no-session GET /api/share/{token}/related?tag=… endpoint backs it. Procurement reviewers + customers + investors can now navigate a workspace's shared material thematically — the same retrieval surface operators have had since rev 39.
- GET /api/v1/workspace/export (close 39-rev v1 parity gap). The rev-6 workspace JSON export shipped 39 revs ago and has been dashboard-only ever since. Rev 45 finally mirrors it on the v1 surface so MCP hosts (and any bearer-auth integration) can pull the full workspace takeaway artefact — workspace profile, sources, signals, tasks, artifacts, memory, recent runs/activities, plus the rev-43 sourceEvidence + rev-44 taskSourceEvidence resolved-input appendices — with one bearer-auth call. Same scrubbing rules as the dashboard endpoint (ingest token / Slack webhook / signing secret / share tokens / view counts all set to null/0). Pairs with the rev-41 workspace import on v1 (rev 41 mirror of the rev-40 dashboard import) to close the JSON round-trip on the protocol-bound surface: MCP hosts can now both read the full export and append memory/signals/sources from a prior export programmatically. Closes the longest-outstanding v1 parity gap.
- GET /api/v1/workspace/today (mirror rev-33 dashboard Today panel). The rev-33 dashboard Today panel aggregates today's signals captured, cycles run, outputs created, approvals processed, and OpenAI spend in the workspace's local timezone, with the rev-34 yesterday delta sub-object and rev-35 7-day rolling baseline layered on top. Rev 45 mirrors the same TodaySnapshot shape on v1 so MCP hosts driving the desk can answer 'what happened today?' with one bearer-auth call instead of stitching together /api/v1/signals + /api/v1/runs + /api/v1/artifacts + the cost math. Pairs with rev-18 /api/v1/stats (7-day health), rev-13 /api/v1/runs (per-cycle token spend), rev-20 /api/v1/activity (audit trail), and rev-43 /api/v1/workspace/summary (all-time totals) as the five-axis 'how is this desk doing' instrument cluster on the protocol-bound surface.
- Source health diagnostics in /api/v1/sources projection. The rev-13 /api/v1/sources endpoint has been dashboard-feature-incomplete since it shipped: the rev-16 source health columns (lastSuccessAt / lastErrorAt / lastErrorMessage) appear in the dashboard sources panel as the '⚠ error · last good 4h ago' diagnostic pill but were never projected on v1. Rev 45 closes the gap. MCP hosts can now render the same health pill in their own UIs without bridge code. Pairs with rev-8 RSS auto-pause-on-failure (the prevention layer), rev-16 source health diagnostics (the dashboard explanation layer), and rev-26 keyword filters (the noise-tuning layer) for the full source-observability story across both the dashboard and the protocol-bound surface.
Per-task memory transparency, full share-page revision lineage, v1 artifact versions, task source-evidence in JSON export
- Per-task memory transparency on every active-work card (close named rev-43 follow-up). Rev 43's running state explicitly named 'per-task source memory transparency' as the next-sprint candidate to close the load-bearing rev-41 transparency loop on the memory side of the task surface. Rev 41 made memory transparency visible on every artifact (post-cycle); rev 43 closed the signal-side loop on the task (pre-cycle / in-flight). Rev 44 closes the matching memory-side loop. New task.sourceMemoryIds JSONB column stamped each cycle by the pulse engine in workNextTask (latest retrieval wins). New 'N memory entries' chip mounts on every active-work card with at least one source memory entry; one tap expands a panel listing the memory entries the AI is pulling for this task (kind, importance, pinned state, content excerpt). Lazy-fetched via the new GET /api/tasks/{id}/source-memory route. Pairs with rev-43 TaskSources for the full per-task input picture: 'which signals AND which durable knowledge is shaping this task right now?' Operators debugging 'why is the AI heading this direction' get the complete answer before the artifact lands. Mirrored on v1 via GET /api/v1/tasks/{id}/source-memory in lockstep so MCP hosts inherit the same transparency vocabulary.
- Full artifact version chain on /share/{token} (close named rev-43 follow-up). Rev 43's running state named 'share-link revisions panel' as the next-sprint candidate. Rev 39 shipped the per-output revision diff in-app; rev 42 shipped the share-page transparency. Rev 44 closes the lineage gap on the public share surface: every shared brief with a parentArtifactId now renders a 'Revision history' panel listing every prior version (id, title, status, date) walked through the rev-20 parentArtifactId chain. New getPublicArtifactVersionChain helper deliberately projects only the lineage fields — body content stays scoped to the artifact actually shared, since prior drafts may carry abandoned positioning the operator chose to remove. SOC 2 / ISO 42001 reviewers reading a shared brief can now see the full revision lineage at a glance. Brand-accent stripe pairs with the rev-42 source-evidence panel so both governance surfaces read with the same visual vocabulary.
- GET /api/v1/artifacts/{id}/versions (close v1 parity gap on rev-20 primitive). The rev-20 artifact version-chain endpoint (`GET /api/artifacts/{id}/versions`) has been dashboard-only for 24 revs. Rev 44 mirrors it on the v1 surface so MCP hosts driving the desk read the full revision lineage of any output through the same protocol vocabulary. Bodies are projected (consistent with the dashboard endpoint, since v1 callers have viewer-equivalent access). Pairs with the rev-44 share-page version chain (external readers) + rev-39 inline diff view (operator readers) so revision lineage is reachable from every read surface. The v1 cadence pattern of 'ship the primitive, mirror the v1 endpoint within one rev' continues with the older rev-20 dashboard primitive that had been waiting for protocol parity.
- Per-task source evidence in workspace JSON export. Rev 43 shipped the artifact-side sourceEvidence appendix in the JSON export — `artifactId → { signals, memory }` — so procurement reviewers could cross-reference each output's inputs without writing a join. Rev 44 closes the task-side symmetry now that tasks carry both sourceSignalIds (rev 1) AND sourceMemoryIds (rev 44). New top-level taskSourceEvidence map: `taskId → { signals: [{ id, kind, priority, title }], memory: [{ id, kind, importance, title }] }` for every task with at least one source. Reviewers auditing in-flight work — not just shipped output — can read 'which inputs are shaping this task' with one lookup. Export bumps to exportVersion 3 (forward-compatible with the rev-40 importer which only reads typed array fields). Closes the procurement evidence symmetry: shipped artifacts AND in-flight tasks now both carry resolved evidence trails in the takeaway artefact.
Per-task input transparency, memory copy, JSON-export source evidence, v1 workspace summary
- Per-task input transparency on every active-work card (close named rev-42 follow-up). Rev 42's running state explicitly named 'per-task input transparency' as the next-sprint candidate to close the load-bearing rev-41 transparency loop on the task surface. Rev 41 made input transparency visible on every artifact (post-cycle); rev 43 closes the loop on the task (pre-cycle / in-flight). New 'N source signals' chip mounts on every active-work card with at least one source signal; one tap expands a panel listing the signals that shaped this work item (kind, priority, title, detail, source link). Lazy-fetched via the new GET /api/tasks/{id}/sources route. Reads from the existing rev-1 task.sourceSignalIds JSONB column (no migration). Operators debugging 'why is the AI heading this direction' no longer have to wait for an artifact to land — the queue itself answers the input question. Pairs with rev-12 work log (what the AI did) + rev-40 timeline (when it did it) + rev-41 artifact sources (what shaped the output) for the full per-task transparency story before, during, and after the cycle.
- Markdown copy chip on every memory entry. Rev 42 shipped one-tap markdown copy on every artifact for the 'paste a brief into Notion / Slack / Linear' workflow. Rev 43 mirrors the same primitive on memory entries — operators paste durable knowledge into wikis / one-pagers / customer-onboarding docs just as often as outputs, and the rev-26 promote-to-memory flow only handles the output → memory direction (not memory → external doc). New MemoryCopyButton mounts alongside the existing edit / pin / delete chips on every memory row, available to all members including viewers (copy is read-only). Assembles a small markdown package: title (H3), kind + importance meta line, content body. Fallback execCommand path for older browsers and non-secure contexts. Pairs with rev-21 memory tags + rev-34 memory bulk operations + rev-33 memory reactions for the complete read/teach/curate/lift-out memory surface.
- Resolved source-evidence appendix in workspace JSON export (close named rev-42 follow-up). Rev 42's running state named 'input transparency on the artifact JSON export' as a next-sprint candidate. The rev-41 columns sourceSignalIds + sourceMemoryIds rode every artifact row in the export from rev 41 onwards as ID arrays, and the export had always carried signals + memory as their own arrays — but a procurement reviewer cross-referencing the input set had to write a join. Rev 43 makes it one read: a new top-level sourceEvidence appendix maps artifactId → { signals: [{ id, kind, priority, title }], memory: [{ id, kind, importance, title }] } for every artifact with at least one source. The export bumps to exportVersion 2 (forward-compatible with the rev-40 import). Procurement teams reviewing AI tool exposure under ISO 42001 / SOC 2 frameworks can answer 'which inputs shaped this brief' without parsing the workspace's full signal + memory arrays.
- GET /api/v1/workspace/summary (close rev-42 v1 parity). Rev 42 shipped the procurement-friendly workspace summary panel (signals, artifacts by status, memory, sources by status, tasks by status, cycles, approved artifacts, anchor timestamps) as a dashboard-only surface. Rev 43 mirrors it on the v1 surface as a single bearer-auth call. MCP hosts can now answer 'how big is this workspace' for an MCP-driven procurement-evidence tool without enumerating every entity. Pairs with the rev-13 /api/v1/runs (cycle history) + rev-18 /api/v1/stats (7-day health) + rev-20 /api/v1/activity (audit trail) as the four-axis 'how is this desk doing' instrument cluster on the protocol-bound surface.
- GET /api/v1/tasks/{id}/sources (v1 mirror of rev-43 dashboard endpoint). Mirrors the new rev-43 dashboard per-task input transparency endpoint on the v1 surface. MCP hosts driving the desk can render the same 'N source signals' transparency surface in their own UIs — same vocabulary, same shape — without bridge code. Closes the v1-vs-dashboard parity loop in the same rev that the dashboard primitive lands, matching the rev-37 / rev-40 / rev-41 cadence pattern.
Share-page input transparency, markdown copy on artifacts, search entity scoping, workspace at-a-glance
- AI input transparency on /share/{token} (close named rev-41 follow-up). Rev 41 introduced the artifact source-evidence primitive for the operator-facing dashboard. The rev-41 running state explicitly named 'input transparency on /share/{token}' as the next-sprint candidate to close the procurement evidence loop for stakeholders reading shared briefs. Rev 42 ships it. Every shared output now carries a procurement-friendly 'Based on N signals + M memory entries' panel with per-kind chip breakdown (e.g. 'feedback · 3 · competitor · 2'), rendered server-side from the same sourceSignalIds + sourceMemoryIds columns. Tuned for external readers — count-and-kind-only, no signal detail bodies or source URLs that might leak internal context. Reviewers viewing a brief through a public link get the same evidence trail SOC 2 / ISO 42001 procurement teams ask the operator for, without the operator having to copy-paste evidence into a separate doc.
- One-tap markdown copy on every artifact (and on /share/{token}). New ArtifactCopyButton mounts alongside the rev-15 share / rev-23 inline edit / rev-26 promote / rev-31 push-to-Slack chips on every approval-queue artifact + the latest brief panel + the public share page. One tap copies a clean markdown package to the clipboard: title (#), summary (italics), body, and source URL list. Operators routinely paste briefs into Notion / email / Slack / Linear and were copying by hand. Power-user QoL — pairs with the rev-23 inline body editor: edit in-place, then copy out. The fallback execCommand path covers older browsers and non-secure contexts.
- Search entity-type scoping chips. WorkspaceSearch already covered six entities (signals, tasks, outputs, memory, comments, activity) with rev-17 cross-entity search + rev-18 saved searches + rev-20 keyboard navigation + rev-29 activity-log inclusion. Rev 42 adds the missing scoping primitive: a chip row above the result groups lets operators narrow the visible matches to specific entity types without re-typing the query. Each chip shows the unfiltered count for that kind so the operator knows what's being hidden. Pairs with rev-18 saved searches as the workspace-search QoL trio (cross-entity + bookmark + scope).
- Workspace at-a-glance procurement panel. New 'Procurement-ready summary' panel mounts above the integrations panel showing all-time totals across the five core entities (signals captured, outputs produced with by-status breakdown, memory entries, sources connected with by-status, tasks lifetime with by-status), total cycles, approval rate, plus first-cycle-date / last-cycle-date / workspace-created-date. Pure derived state via one parallel Promise.all of Postgres count queries. Companion to the rev-6 JSON export + rev-7 activity CSV + rev-22 outputs CSV (the procurement evidence trio): where those are takeaway artefacts an owner sends to a procurement reviewer, this is the single-screen instrument cluster the owner shows the reviewer in real time. The shape mirrors the SOC 2 / ISO 42001 reviewer's natural axes so the read top-to-bottom matches the questionnaire shape.
- MCP and the Protocol-Bound Future of Business AI (announcement post). Rev-37/38/39/40/41 running states all named 'MCP server announcement post' as the dedicated marketing piece pending the actual MCP server launch. Rev 42 publishes the precursor: a public blog post explaining what MCP is, why protocol-bound AI workspaces matter for buyers evaluating governance-first tools, and the v1 surface state of completion that makes the upcoming MCP server exclusively protocol-translation work. SEO surface area for 'MCP business workspace' / 'Loop Desk MCP' — the named buyer-facing terms on the protocol-bound side of the line.
AI cycle input transparency, v1 timeline + import + sources mirrors, per-source sparkline
- AI cycle input transparency on every output. Procurement teams reviewing AI tool exposure routinely ask 'which inputs shaped this output?'. Until rev 41 the answer was a triangulation across the rev-12 per-task work log + rev-20 artifact version chain + the operator's memory of what was in the workspace at cycle time. Rev 41 ships a primary primitive: every artifact now carries sourceSignalIds + sourceMemoryIds JSONB columns stamped by the pulse engine at insert time. The dashboard surfaces a 'Based on N signals + M memory entries' chip on every approval-queue artifact and the latest brief; one tap expands a panel listing the actual signals (kind, priority, title, detail, source link) and memory entries (kind, importance, content, pinned status) the AI cycle saw. Drives both the audit story and operator trust — when a brief reads off, the operator can see whether the input set was wrong, the cycle missed key context, or the output itself drifted.
- GET /api/v1/tasks/{id}/timeline (close named rev-40 follow-up). Rev 40 named 'mirroring per-task timeline on the v1 surface' as the natural rev-41 step. Rev 41 ships it. New bearer-auth endpoint mirrors the rev-40 dashboard route, delegating to the same getTaskTimeline() helper. MCP hosts can now ask 'what happened on this one task' with one HTTP call across all five timeline sources (creation + AI cycles + operator notes + comments + activity).
- POST /api/v1/workspace/import + GET /api/v1/artifacts/{id}/sources. Two more v1 surface gaps closed. The rev-40 workspace JSON import was dashboard-only; rev 41 mirrors it on bearer-auth so MCP hosts can append memory + signals + sources from a prior export programmatically. The new artifact sources endpoint exposes the rev-41 transparency primitive on the v1 surface so MCP hosts can render 'show me the inputs that shaped this output' without a follow-up call.
- Per-source 7-day signal volume sparkline. Rev 12 added the per-source 7-day signal count pill ('this feed has produced N signals'). Rev 41 adds the diagnostic complement: a 7-bar sparkline next to the count showing whether the feed lit up evenly or in one big burst. Single extra query in the existing dashboard Promise.all bucketed per-day in JS. Pairs with rev-16 source health diagnostics + rev-8 auto-pause-on-failure for the full source observability story: count answers 'is it producing?', sparkline answers 'is it producing healthily?', error pill answers 'why is it broken?'.
- Visual polish — entry animation on transparency panel + 7-day sparkline accent. The new 'Based on N signals + M memory entries' panel uses a 220ms ld-fade-in entry animation so the panel reads as a branch off the artifact rather than something stamped on. The per-source 7-day sparkline uses the rev-15 brand-color treatment for non-zero days and a soft neutral fill for empty days so the 'this source has been quiet' signal reads at a glance. Cumulative micro-polish — every rev 22+ has carried one.
v1 tag drill-down + rename mirror, per-task timeline, workspace JSON import, source preview v1
- v1 mirror for rev-39 tag drill-down + tag rename (close named v1 parity gap). Rev 39 shipped the cross-entity tag drill-down + admin-only tag rename/merge as dashboard-only routes. The rev-39 next sprint focus explicitly named 'mirroring them onto the v1 surface is the natural pre-MCP-server step.' Rev 40 closes that gap with two new bearer-auth endpoints — GET /api/v1/workspace/tag-search?tag=…&limit=25 and POST /api/v1/workspace/tags/rename — that delegate to the same getTagDrillDown() and renameWorkspaceTag() helpers the dashboard uses. The MCP server's tag-drill-down + tag-consolidation tooling now needs no custom bridge code. Indexed in the /api/v1 self-describing endpoint list.
- Per-task activity timeline (close named rev-38 candidate). Rev 12 surfaced the per-task work log; the rev-21 activity log filter chips + rev-38 keyword search could be scoped to a single task — the rev-38 running state explicitly named 'per-task activity log scope' as a rev-40+ candidate. Rev 40 ships it. New TaskTimeline client component mounts alongside the existing rev-12 TaskWorkLog button on every active-work card; tap 'Timeline' to see a unified, timestamp-sorted list of every event that touched this task: creation, AI cycles (workLog), operator notes (rev 14), comments (rev 26), and recent workspace activity log entries that mention the task. Per-kind filter chips with counts (All / AI cycle / Operator / Comment / Activity). Each row is colour-tinted by kind (matching the rev-35 activity log glyph palette). Pure derived state — no schema change. New endpoint GET /api/tasks/:id/timeline?limit=80 (viewer+ auth). Operators debugging a single task no longer have to triangulate across three surfaces; the answer to 'what did the desk and the team do on this one task' is one tap away.
- Workspace JSON import (companion to rev-6 export). Rev 6 shipped the workspace JSON export but the only way to act on the file was to parse it manually or restore from a database backup. Rev 40 closes the loop with an admin-only POST /api/workspace/import endpoint that appends memory entries, signals, and sources from a prior export onto the current workspace. Tasks, outputs, runs, and activity are deliberately not imported (they reference internal IDs — parentArtifactId, blockedByTaskIds, sourceSignalIds — that don't cross workspaces; if an operator needs that level of restoration, a database backup is the right tool). Per-category checkbox picker. Caps at 500 memory + 500 signals + 50 sources per call. Source state-bearing config keys (failureCount, seenGuids, lastError, etc.) are stripped on import — only the recipe carries over. UI mounted in the integrations panel beside the existing JSON / activity-CSV / outputs-CSV exports. Operators can now restore from accidental bulk-deletes (rev 34 added memory bulk delete; rev 22 added signal bulk delete; rev 33 added signal bulk delete; rev 36 added source bulk delete — all four delete surfaces now have a recovery path).
- Source URL preview v1 mirror (close rev-38 v1 surface gap). Rev 38 shipped POST /api/sources/preview as a dashboard-only synchronous feed validator. Rev 40 mirrors it as POST /api/v1/sources/preview (bearer-auth, same shape) so an MCP host or any bearer-auth integration can validate a feed URL before issuing the create call. Pairs with the rev-13 v1 source create endpoint to close the 'create-time validation' loop on the v1 surface. The MCP server's source-onboarding tool now has nothing left to design — every dashboard primitive in the source-add flow has a v1 equivalent.
- Visual polish — task timeline accent strip, slower pin pulse, tag chip hover. The new TaskTimeline panel uses per-kind left-border accents (brand-color for AI cycle, amber for operator notes, purple for comments, neutral for activity, green for created) so the eye scans timeline entries by event type without reading every meta line. The rev-23 pinned-task pulse animation slowed from 2.4s → 3.2s so the indicator reads as ambient state rather than urgency on workspaces with many pinned items. Rev-39 tag manager chips lift 1px on hover with a 140ms transition for a tactile click affordance. Cumulative micro-polish — every rev 22+ has carried one.
Per-output revision diff, cross-entity tag drill-down, tag rename/merge, dashboard density toggle
- Per-output revision diff view (close named rev-38 candidate). Rev 20 introduced the artifact version chain (parentArtifactId stamped on regenerate) but until rev 39 the only way to see what changed was to expand both versions and diff them by eye. New ArtifactDiffView component mounts alongside the rev-20 ArtifactVersionLink on every artifact with a parent. Click 'Compare to previous' to see a unified line-by-line diff between the current body and its immediate parent's body, with title changes called out separately. LCS-based line diff implemented inline (~30 lines, no new dependency). Pairs with the rev-11 reviewer note + rev-23 inline body edit so operators have full visibility into every revision's content delta — load-bearing for governance-first procurement teams that ask 'show me what was different about this output's earlier version'.
- Cross-entity tag drill-down modal. Tags went live for outputs (rev 15), memory (rev 21), and tasks (rev 24). Rev 28 introduced tag insights as a *descriptive* surface ('which tags is the team using?'). Rev 29 added focus tags as a *prescriptive* surface ('what should the team work on?'). Rev 39 closes the *retrieval* surface: click any tag in the new workspace-wide TagManager panel and a modal opens listing every task, output, and memory entry that carries it, sorted newest-first. Until rev 39 there was no way to ask 'show me everything tagged #q3-launch' without filtering each entity panel independently. The cross-entity surface knits the tag corpus together for the first time. New endpoint GET /api/workspace/tag-search?tag=… (viewer+ auth) backs it via Postgres JSONB @> array containment, which is more precise than text-cast ILIKE (a tag like 'q3' wouldn't over-match 'q3-launch').
- Workspace tag rename / merge (admin only). Pairs with the rev-39 drill-down to close the silent fragmentation problem. Three months of operator usage inevitably accumulates fragmented tags (q3-launch / q3launch / q3_launch all referring to the same workstream) — until rev 39 the only fix was to walk every entity by hand and re-tag. New POST /api/workspace/tags/rename body { from, to } operates on tasks.tags, artifacts.tags, and memory_entries.tags in one sweep; if a row already carries the target tag, the source is dropped (no duplicates). Admin-only at the route layer. Inline rename/merge form mounts inside the new TagManager panel, with autocomplete from the workspace's existing tag pool. Activity log records the merge and how many entities were updated. Closes the load-bearing problem named in rev 25 ('silent tag fragmentation') with a one-action consolidation primitive.
- Dashboard density toggle (compact mode). The dashboard has accumulated 30+ panels over 38 revs across active work, today, decisions, tag insights, focus, mentions, pinned tasks, dependency graph, signal mix, latency, health, members, sources, integrations, activity, and counting. Power-user workspaces with many panels open at once benefit from a compact mode that tightens panel padding + item spacing without hiding any data. New DensityToggle button mounts in the status bar next to the heartbeat indicator; one tap flips between 'Comfy' (default) and 'Compact'. Persisted in localStorage so it sticks across sessions. Adds an `is-compact` class to <body> consumed by CSS overrides — shrinks .app-panel padding 22→14px, .app-item padding 14→10px, .app-grid gap 16→12px, status-bar 18→14px. Cumulative micro-polish (every rev 22+ has carried one) — rev 39 finally addresses the panel-density problem that 38 revs of additive panel work created.
- Visual polish — diff slide-in, tag-chip hover affordances, compact-mode density. Subtle 220ms slide-in on the new ArtifactDiffView panel so the diff content reads as a branch off the artifact rather than something stamped on. Cursor pointer + on-hover brightness on the rev-28 tag-insights bar (the chips were always click-to-copy but the cursor never advertised it). Compact-mode tag chips use a tighter 3px/8px padding for power-user workspaces with 50+ tags. Diff line backgrounds use soft red/green tints that read clearly on both light and dark dashboard panel backgrounds without being aggressive. Cumulative — the dashboard now reads at a higher density without losing any of the affordance vocabulary the previous 38 revs accumulated.
Dependency graph view, public roadmap page, source URL preview, activity search
- Task dependency graph view (close named rev-37 candidate). Rev 36 made dependencies a real queue gate; rev 37 made unblocking push-loud; rev 38 surfaces the whole dependency network at a glance. New 'Who's blocked on what' panel renders above active work whenever ≥1 task has declared blockers (rev 36's blockedByTaskIds), showing each dependent → its blockers with status pills and click-to-jump navigation. Ready-to-start dependents float to the top with a brand-color highlight. Multi-step workflow operators (draft → review → publish; competitor analysis → pricing decision → board prep) no longer have to expand every task's blocker pill to see the project shape.
- Public /roadmap page. Forward-looking companion to the rev-14 /changelog. Lists Q2/Q3 2026 priorities (Stripe, MCP server, Slack interactive approvals, Mailgun/Postmark inbound DNS) plus considering items (Shopify, ISO 42001 cert, mobile app, JSON import). Sitemap-listed; nav link added to landing/docs/changelog. Procurement teams evaluating governance-first AI tools weight visible velocity heavily — a public roadmap next to the rev-by-rev changelog is the cheapest possible 'where are we going' trust signal alongside the 'where have we been' cadence.
- Source URL preview before saving. Operators previously had to add an RSS / review-site / LinkedIn source, wait for the next pulse cycle, and discover then whether the feed was actually parsing — a real friction tax for noisy bridge URLs (rss.app, fetchrss) where the syntax is finicky. New POST /api/sources/preview validates a feed URL synchronously and the source form calls it on URL blur, showing inline 'Feed looks good · 23 items parsed' with 3 sample titles when ok, or 'Feed check failed: HTTP 404' when broken. Editor+ role required (a viewer can't add a source so doesn't need to preview one). Closes the silent failure mode that has been the #1 source-onboarding friction since rev 5 introduced the RSS poller.
- Inline activity log search. Rev 21 shipped chip filters on the activity log; rev 29 added activity to the workspace search. The activity panel itself still had no in-place search — operators looking for 'when did we last get a Slack send error' had to scroll the activity stack manually. Rev 38 closes that gap: a small native search input above the chip filter row composes with the existing kind chips via intersection (kind AND keyword). Matched substrings highlight inline so the eye lands on the match without reading the whole detail line. Renders only when there are 6+ activities so quiet workspaces don't see clutter.
- Visual polish — scroll-to-top FAB + status bar accent + focus ring. The dashboard has accumulated 30+ panels over 37 revs (active work, today, decisions log, tag insights, focus history, mentions, pinned tasks, dependency graph, signal mix, latency, health, members, sources, integrations, activity). Scrolling back to the top after deep-diving the activity log was a pure friction tax — rev 38 adds a scroll-to-top FAB that appears after 800px of scroll and stacks below the rev-23 keyboard shortcut FAB and rev-27 command palette FAB. The status bar gains a subtle brand-tinted gradient + on-hover left-edge accent stripe, anchoring the dashboard's identity surface visually. Refined focus-visible ring on every dashboard button and pill so keyboard-only operators get a consistent landing pad regardless of which control they're on. Cumulative micro-polish — every rev 22+ has carried one.
Task unblocked push, v1 blockers endpoint, public RSS, 'now ready' task cue
- task.unblocked Slack + email + outbound notification. Closes the named rev-36 next-sprint candidate. Rev 36 made task dependencies a real queue gate (selectNextTask filters out tasks until every blocker is done) but no push channel told operators when a blocker just flipped to done. Rev 37 adds it: when a task transitions to done — via either updateTaskStatus or the AI completion path in pulse — every dependent task gets re-evaluated; if its remaining blockers are now all done, the assignee gets a Slack ping, an email if Resend is configured, and the new task.unblocked outbound webhook event fires for downstream automations. Best-effort across all three channels: a Slack/email/outbound failure cannot block the originating task's status update.
- PUT /api/v1/tasks/:taskId/blockers — v1 parity for the rev-36 blockers primitive. The rev-36 dashboard endpoint (PUT /api/tasks/:id/blockers) was dashboard-only. Rev 37 mirrors it onto the bearer-authenticated v1 surface, so the upcoming MCP server's task-dependency tooling needs no custom bridge code. Same 8-blocker cap, same plain-JSONB validation in setTaskBlockedBy() — a deleted blocker degrades gracefully to 'blocker missing' without orphaning the dependent. Closes the last v1 surface gap from the rev-36 list. Indexed in the /api/v1 self-describing endpoint list (and fixes the rev-36 typo that referenced /api/tasks/{id}/blockers).
- Public /changelog/rss.xml feed. RSS 2.0 feed at /changelog/rss.xml exposes the last 30 revisions to feed aggregators (Feedly, Inoreader, the dozens of 'what shipped this week' AI-tooling newsletters that read RSS). Marketing surface — visible velocity is the cheapest possible trust signal for procurement teams evaluating governance-first AI tools, and an RSS feed turns the rev-by-rev cadence into a passive distribution channel without the operator's permission. Cache-control public, max-age=300, s-maxage=1800. The /changelog page now exposes it via a Subscribe link and an HTML <link rel='alternate'> in the page head so feed readers auto-discover it. Refactored the releases data into a shared src/lib/changelog-releases.ts so the page and feed never drift.
- 'Now ready' visual cue on tasks where every blocker is done. Until rev 37, a task whose declared blockers (rev 36) had all transitioned to done was visually identical to a task that was still waiting — only the small TaskBlockers pill changed. Rev 37 adds a more prominent affordance: a brand→purple 'Ready' ribbon pulses on the corner of the card and the card itself gets a soft brand-color glow. Distinct from a task with zero blockers (no need for a 'ready' affordance), distinct from in_progress (already running). Pairs with the rev-37 task.unblocked Slack/email push — the push is the moment-of-transition alarm, the ribbon is the persistent 'still waiting to start' affordance for the operator scanning the queue. Tucks to bottom-right when the task also matches a rev-29 focus tag so the two ribbons don't overlap.
Task dependencies, source bulk ops, digest preview, v1 task badges, task-card colour accents
- Task dependencies (blocked-by) — load-bearing project-mgmt primitive. Until rev 36 a task could be steered four ways (priority, due, pin, assignee) but couldn't say 'I can't start until X is done.' The pulse engine's selectNextTask now skips any task whose declared blockers aren't all status=done; the dashboard surfaces a 🔒 Blocked by N · M done pill in the active-work card and a picker that lets operators wire up to 8 blocker tasks per dependent without leaving the queue. Plain JSONB (no FK) so a deleted blocker degrades gracefully to 'blocker missing' rather than orphaning the dependent. New PUT /api/tasks/:id/blockers endpoint.
- Source bulk operations (close five-entity bulk symmetry). Outputs got bulk approve/archive in rev 6; tasks in rev 26; signals in rev 33; memory in rev 34. Sources was the last surface without bulk. Operators with noisy LinkedIn / RSS bridges that flooded the workspace previously had to pause/remove sources one click at a time. Rev 36 closes the gap with a checkbox row + Pause / Resume / Remove bulk action bar (renders when 3+ sources exist). Capped at 50 IDs. New POST /api/sources/bulk + matching POST /api/v1/sources/bulk so the upcoming MCP server inherits the surface.
- Workspace digest preview button. The daily digest cron is fire-once-and-forget — operators previously had no way to verify their digest content (rev-25 personalised assignee section, rev-31 personal mentions section) without waiting for the next 13:15 UTC run. Rev 36 adds a 'Send test digest' button to the integrations panel that delivers the same payload the cron would, but bypasses the per-workspace 22h interval gate so admins can iterate on configuration. Owner/admin-only at the route layer to prevent viewer abuse.
- Comment count + unacked-mention badges in /api/v1/tasks. Rev 35 surfaced commentCount + unackedSelfMentions inline in the dashboard task card; rev 36 mirrors the same projection in the v1 task list endpoint. Each row now carries commentCount and unackedSelfMentionsByUser (a per-user-id map) so MCP-host task-list tools can localise the 'this task has an active conversation' / 'an @-mention is waiting for X' cues without reparsing the comments JSONB themselves. Foundation for the upcoming MCP server's task list tool.
- Task-card priority colour accent + visual hierarchy refresh. The active-work task card has accumulated 12+ pills + 5 affordance rows over 34 revs. Until rev 36 every card looked the same and operators had to parse every pill to triage. Rev 36 adds a priority-driven left-border accent — urgent → red, high → amber, normal → no accent, low → muted — so an operator scanning the queue can triage by colour at a glance. Pinned tasks still win via specificity. Closes the rev 35 named follow-up.
v1 bulk endpoints, mention-ack push, comment count badges, 7-day baseline, activity glyphs
- Bulk operations on the v1 surface (close MCP-protocol gap). Rev 6 / 26 / 33 / 34 shipped bulk operations on the dashboard for outputs / tasks / signals / memory respectively — but until rev 35 those mutations were dashboard-only. The MCP server work (Q3 #1) now has nothing left to design on this surface: four new bearer-auth endpoints — POST /api/v1/tasks/bulk, POST /api/v1/signals/bulk, POST /api/v1/memory/bulk-update, POST /api/v1/artifacts/bulk — mirror the dashboard semantics exactly. Operators driving the desk via the upcoming MCP server can now clear a noisy queue in one tool call without enumerating IDs through individual PATCHes.
- Mention ack push notification (close rev-34 named gap). Rev 34 introduced workspace-shared mention acks — when a teammate clicks 'I've got this', their name pins to the comment so other operators see who's already on it. Rev 35 closes the missing push channel: the original mention author now gets a Slack ping (and email when Resend is configured) the moment a teammate acks their @-mention. New task.mention_acked outbound event for downstream integrations. The full mention conversation now reads end-to-end across all three channels — opener (rev 27), receipt (rev 30 inbox + rev 34 ack), closure (rev 35 push to author).
- Comment count + unacked-mention badges in Active Work. Until rev 35, the rev-26 task discussion thread was buried at the bottom of every task card — operators had to expand the discussion to know if anything was happening. Now every task with comments shows a 💬 N pill in the header. When the current user has an unacked @-mention on the task, the pill turns brand-color and adds an @N indicator — that's the 'I should look at this' signal directly in the queue, distinct from the rev-30 mentions inbox panel which is the workspace-wide pull surface.
- Today vs 7-day baseline on the Today panel. Rev 33 added the Today snapshot panel; rev 34 added yesterday delta. Rev 35 layers the trend horizon — every stat now shows the rolling 7-day daily-average baseline as a quiet trailing fact (e.g. '7d avg 4.2'). The window excludes today so today's value can't dilute its own baseline. Operators get two complementary horizons: yesterday for short-term volatility, 7-day for trend. Pairs with the rev-21 cost-cap warning + rev-32 cost-spike detector for the full descriptive → defensive cost narrative.
- Activity log glyphs + per-kind colour tinting. The activity log accumulates 14 distinct kinds (cycle, capture, task, artifact, memory, status, slack, cost, outbound, source, member, stuck_loop, cost_spike, signal). Until rev 35 every row had the same brand-color dot — operators had to read every detail line to scan visually. Rev 35 replaces the dot with a tinted glyph per kind (↻ cycle, ⇣ capture, ✓ task, ✦ artifact, ★ memory, etc.) — pure unicode so it renders consistently across platforms with no extra dependency. Operators can now skim the log for cost spikes or stuck loops without reading every line.
Memory bulk ops, signal reactions, mention read-receipts, today/yesterday delta
- Memory bulk operations (close 4-entity bulk symmetry). Outputs got bulk approve/archive in rev 6; tasks in rev 26; signals in rev 33; rev 34 closes the symmetry on the fourth core entity. Memory entries now have the same checkbox column + bulk action bar — Pin / Unpin / +Tag / −Tag / Delete / Select all visible. The +Tag and −Tag actions take a tag-name input so an operator can normalise a fragmented tag set in one pass (e.g. fix every memory tagged 'q3launch' to 'q3-launch'). Capped at 50 entries per call. New POST /api/memory/bulk-update endpoint distinct from the rev-22 bulk-import path.
- Reactions on signals (close 4-entity reaction symmetry). Comment reactions shipped rev 29; output + memory reactions shipped rev 33. Signals were the last surface without the lightweight 👍 / 👀 / 🎯 / ❤️ / 🚀 ack vocabulary. Rev 34 closes the symmetry — operators can now ack a fresh competitor move ('👀 saw it, not urgent') or a customer-feedback signal ('❤️ thanks') without promoting it to a task or writing a comment. Mounts inline on every signal row. New endpoints POST /api/signals/:id/reaction (dashboard, editor+) and the v1 mirror POST /api/v1/signals/:id/reaction so the upcoming MCP server inherits the surface.
- Mention read-receipts (workspace-shared 'I've got this' state). Rev 30 added the per-user mention inbox; rev 32 added inline reply. Until rev 34 the inbox state was per-user-only (localStorage dismiss) — a multi-operator team had no way to see whether a mention had been actioned by anyone. Rev 34 adds workspace-shared mention acks: tap '🙋 I've got this' on any unread mention and your name + timestamp pin to the comment so every other teammate sees who's already on it. Distinct from per-user dismiss (which still lives in localStorage); distinct from comment reactions (which ack the comment generally, not the call-to-action specifically). New POST /api/tasks/:taskId/comments/:commentId/ack endpoint. The acked mention gets a soft brand-color left border + an ack-pill in the header showing the first acker's name + count.
- Today/yesterday delta on the Today panel. Rev 33 introduced the Today snapshot panel — single-glance morning answer to 'what happened on the desk while I was away?'. Rev 34 layers the missing context — every stat now shows a tiny day-over-day delta chip (▲ 3 vs yesterday, ▼ 2, or · same). For count stats (signals/cycles/outputs) up reads green; for spend, up reads amber (more $ = warning) and down reads green (less $ = good). One getTodaySnapshot query window extended; no new schema, no new round-trip. Pairs with the rev-21 cost-cap warning + rev-32 cost-spike detector for the full descriptive → defensive cost story.
Reactions on outputs + memory, Today snapshot, signal bulk ops, panel polish
- Reactions on outputs and memory entries. Rev 29 introduced the comment-reaction primitive (👍 / 👀 / 🎯 / ❤️ / 🚀) — the lightest possible 'I saw it' / 'I'm on it' acknowledgment that doesn't bloat a thread. Rev 33 extends the same primitive to the two other entities operators routinely want to ack without writing a comment: artifacts (outputs) and memory entries. Same emoji set so the workspace's reaction vocabulary stays consistent across all three surfaces. New shared EntityReactions component drives both. Mounted next to the existing tags on every approval-queue artifact and every memory row. New endpoints POST /api/artifacts/:id/reaction and POST /api/memory/:id/reaction, plus their v1 mirrors so the upcoming MCP server inherits both.
- Today snapshot panel. New 'At a glance' sidebar panel renders at the top of the dashboard sidebar. Aggregates today's signals captured, cycles run, outputs created, approvals processed, and OpenAI spend — all in the workspace's local timezone. Single-glance morning answer to 'what happened on the desk while I was away?' Pairs with the rev-13 desk health score (is the desk healthy over 7 days?) and the rev-14 cycle performance widget (is it fast?) to form a four-axis instrument cluster: today's snapshot answers what just happened, health answers is it working, performance answers is it fast, and cycle history answers is it consistent.
- Signal bulk operations. Mirrors the rev-26 task bulk action surface on the signal entity. New checkbox column on every recent-signal row (when canEdit); selecting one or more reveals a bulk action bar with Pin / Unpin / Delete / Select all visible / Clear. Caps at 50 IDs per request. The pattern operators told us about for tasks — clearing a noisy weekend feed in one go — applies just as cleanly to signals after a noisy news week or a competitor RSS bridge that flooded the feed. New POST /api/signals/bulk endpoint.
- Panel polish + entry animation. Subtle 240ms fade-in animation when a panel mounts (shows up most visibly on dashboard refresh). Smoother panel hover lift (slightly stronger box-shadow + brand-color border on hover). Thin horizontal accent line under each panel head that fades in on hover so the eyebrow + title relationship reads as a unit. Cumulative micro-polish keeps the dashboard feeling reactive to mouse movement without distracting from the work in the panels.
Memory promotion → Slack, pinned outputs, cost spike alert, inline mention reply
- Slack + outbound push when an output is promoted to memory. Rev 26 made promote-to-memory a one-click operator move — the explicit signal that 'this insight should outlive the artifact's lifecycle.' Until rev 32 the action was silent: only operators staring at the dashboard knew the team's collective memory had grown. Now every promotion fires a Slack ping (with the kind, importance, and content excerpt) and dispatches a new memory.promoted outbound webhook event so downstream automations (a CRM, a wiki, a knowledge base) can mirror durable knowledge as it accumulates.
- Pinned outputs (close 4-entity pinning symmetry). Memory pinning shipped rev 5; signals rev 22; tasks rev 23. Rev 32 closes the symmetry on the fourth core entity. A pinned output stays at the top of the approval queue regardless of how many fresh outputs land below it — the load-bearing brief, decision, or draft you want the team to actually see, not buried under noise. Pin button mounts inline next to the existing share/regenerate/promote chips. New PATCH /api/artifacts/:id/pin endpoint and the pinned visual treatment matches the rev-22/23 pinned panels for design consistency.
- Cost spike detection. Companion to the rev-21 cost cap warning. Where the cap warning fires only when an explicit dollar ceiling is configured, the spike detector fires whenever today's spend is at least 2× the trailing 7-day daily average and at least $0.50 absolute — even on workspaces with no cap. Catches a runaway noisy source or stuck loop before it eats the day's budget. Surfaces as a dismissible amber banner at the top of the dashboard (6h localStorage suppression so a real spike re-surfaces the next day) and pushes a Slack alert + workspace.cost_spike outbound event from the daily cron, rate-limited to once per workspace per day. New OutboundEvent: workspace.cost_spike.
- Inline reply from the mentions inbox. Rev 30 added the mentions inbox; rev 31 added permalinks so a click from email or Slack lands on the exact comment. Rev 32 closes the open-and-respond loop. Tap Reply on any unread mention in the inbox and an inline textarea appears — Esc to cancel, post to send. The reply lands as a threaded comment under the original mention (parentCommentId set) so it folds under the thread automatically. Three-second action vs the previous scroll-into-thread → type → scroll-back loop.
Mentions in digest, focus drift timeline, comment permalinks, ad-hoc Slack push
- Unread mentions in the daily digest email. Rev 27 made @-mentions push-loud via Slack/email/outbound; rev 30 surfaced them as a dashboard inbox panel. Rev 31 closes the steady-state surface — your daily digest email now ends with a per-recipient 'N mentions from your team' section listing the last 24 hours of unread mentions, each with a deep-link to the specific comment. Solo founders without Slack get the same heads-up that Slack-first teams already had. Reuses the rev-30 getUserMentionsInbox() helper.
- Focus drift timeline (90-day). Rev 30 added a 'previously: …' diff line beneath the active focus tags. Rev 31 turns that into a real timeline — every focus shift in the last 90 days renders as a vertical thread of dots with the tag set, the date, and how long that focus held before being replaced. Two summary stats anchor the view: total shifts and percentage of time spent focused (vs no-focus). Top tags by total focused-duration appear as proportional pills. New endpoint GET /api/v1/workspace/focus-history?sinceDays=90 mirrors the dashboard view onto the v1/MCP surface. Drives entirely from the activity log — no new schema.
- Comment permalinks + deep-link from mentions inbox. Every comment now carries id="comment-<id>". A new 'Link' chip in the comment header copies the permalink to the clipboard. The TaskComments component listens for #comment-<id> in the URL hash and auto-opens the discussion thread, scrolls the matching row into view, and briefly highlights it. The rev-30 mentions inbox and the rev-31 digest email both build links of this shape, so a click from anywhere lands the operator on the exact comment with the thread already expanded — not just on the parent task.
- Ad-hoc 'Push to Slack' button on every output. Until rev 31, Slack only carried the auto-generated cycle brief. Operators routinely wanted to send a single output (a draft, a decision, an updated watchlist) to the channel right now without waiting for the next cycle. The new chip on every approval-queue artifact does that in one click — Slack post includes the artifact's title, summary, body excerpt, the public share-link if one exists, and an attribution line. Endpoint POST /api/artifacts/:id/push-slack reuses the dead-webhook auto-clear path so a stale URL doesn't silently retry. Editor+ role required.
Focus-aware memory retrieval, mentions inbox, focus history diff, v1 reaction + focus endpoints
- Focus-aware memory retrieval. Rev 29 made the AI cycle's task selection and prompt focus-aware, but memory retrieval still ranked entries by recency + token overlap + importance only — a focus-themed task could pull in off-theme memory. Rev 30 closes that gap: a memory entry tagged with a current focus tag receives a +0.6 boost in the retrieval score so AI cycles working on focus-themed tasks recall focus-themed prior knowledge first. The third place focus is now load-bearing alongside queue selection (rev 29) and AI prompt (rev 29).
- Mentions inbox panel. Every comment that @-mentions you across every task in the workspace now surfaces in a dedicated 'You were @-mentioned' dashboard panel between Pinned tasks and Active work. Each row shows the author, the comment text, the parent task title, and a one-click jump to the comment. Read state persists in localStorage per-workspace so dismissed mentions stay dismissed across sessions; 'Mark all read' clears the queue. Closes the rev-27 mentions push channel with the matching pull surface — multi-operator teams catching up after a day off no longer have to scroll every task to find what was directed at them.
- Team focus history (week-over-week diff). The 'Team focus' sidebar panel now also shows the prior focus snapshot inline beneath the active tags. Tags that were dropped from focus render struck-through; tags that survived render in brand-color pills. Drives from the existing activity-log entries (rev 29 already records every focus change) so no new schema. Pairs with rev-29's focus tags by giving operators a sense of focus drift week-over-week — if you switched from #q3-launch to #renewals two weeks ago and the desk is still pulling Q3-relevant memory, the history line is the load-bearing visual signal.
- v1 surface for comment reactions + focus tags. Two new bearer-authenticated endpoints close the rev-29 MCP symmetry gap: POST /api/v1/tasks/:id/comments/:commentId/reaction toggles a 👍/👀/🎯/❤️/🚀 reaction (defaults to workspace owner if no asUserId provided), and GET/PUT /api/v1/workspace/focus-tags reads or sets the workspace's focus tags. The MCP server is now exclusively a protocol-translation job — every dashboard mutation including the rev-29 reactions and focus tags has a v1 equivalent.
Comment reactions, team focus goals, activity in search, focus-aware AI
- Comment reactions (👍 / 👀 / 🎯 / ❤️ / 🚀). Five-emoji reaction bar appears under every comment. One click toggles your reaction — counts surface inline with member names on hover. Reactions persist on the comment itself (no new event, no outbound storm) and are scoped per-user so an ack reads as 'someone saw this' without bloating the thread or bumping updatedAt timestamps. The natural rev-29 step from rev 28's threading: now teammates can ack 'on it' or 'looks right' without writing a full reply.
- Team focus tags — set the desk's weekly priority theme. New 'Team focus' sidebar panel lets owners/editors pin up to 3 tags as the team's focus for the week. The dashboard queue layers a focus-tag boost between needs_input and due-date weight so tasks tagged with a focus tag float above off-theme work. Tasks that match a focus tag wear a small ★ focus ribbon. The pulse engine reads focus tags too — task selection inside the AI cycle agrees with what the operator sees, and the AI prompt receives a 'TEAM FOCUS THIS WEEK' line that biases retrieval and recommendations toward the chosen themes. Endpoint: PUT /api/workspace/focus-tags. Pairs with rev-28 tag insights — insights are descriptive, focus is prescriptive.
- Workspace search now reaches activity log. The rev-17 search and the rev-28 comment coverage already let operators find any discussion or memory entry by keyword. Rev 29 adds the seventh result group: activity. Type a kind name (slack, capture, cost) or any phrase in an activity-log entry to surface matching events, with timestamps and matched substrings highlighted. Mirror coverage on /api/v1/search with the same activity result group. Closes the search coverage gap from rev 28 — every workspace-level surface is now keyword-searchable from one input.
- Focus-aware AI cycle + visual polish. Pulse engine task selection and AI prompt both now honour the workspace's focus tags so AI work agrees with the queue order operators see. Reaction buttons reuse a soft accent treatment that reads as ack-not-action, the focus-tag ribbon uses the brand→amber gradient first introduced for tag insights, and the team-focus panel shares vocabulary with the rev-28 'what your team is focused on' panel so the descriptive/prescriptive pair reads as a unit.
Threaded discussion + comment edit, search reaches comments, tag insights
- Comment threading + edit window. Every top-level comment now has a Reply button — a reply renders indented under its parent with a soft brand-color thread line, capped at one level deep so a reply-to-a-reply folds back onto the original parent. The comment author can edit their own comment for 10 minutes after posting (owners and admins can edit any comment at any time); edited comments show an inline 'edited' marker. The composer is a single reusable component for new comments, replies, and edits — same @-mention popover, same @desk bridge into authoritative AI direction. Closes the obvious rev-26 follow-up: hallway-style discussion was already in the product, but until rev 28 a typo in a posted comment had nowhere to go but Delete-and-rewrite, and a multi-question thread became one flat list.
- Workspace search now reaches task comments. Type a query into the rev-17 workspace search and you now also see a Comments group — the comment text, the author, and the parent task title (with matched substrings highlighted). Pressing Enter on a comment hit scrolls to the active-work panel where the conversation lives. The /api/v1/search endpoint returns the same comment hits (coarse-filtered via a JSONB text-cast, refined per-comment), so any MCP host or custom integration sees the comment surface too. Until rev 28 the rev-26 comments primitive was invisible to the workspace search — a multi-operator team could mention a competitor in a task discussion three weeks ago and have no way to find it again.
- Tag insights — what your team is focused on this fortnight. New sidebar panel that counts tag occurrences across tasks, artifacts, and memory in the last 14 days, weighted equally per appearance, sorted by frequency, top 8 surfaced as proportional bars. Click a tag to copy it to the clipboard for pasting into the workspace search. Companion endpoint GET /api/v1/insights returns the same data with per-entity breakdown (tasks/artifacts/memory) for MCP hosts and dashboards. Tags went live for artifacts (rev 15), memory (rev 21), and tasks (rev 24); rev 28 turns the accumulated tag corpus into a strategic summary view that answers 'what's our team actually working on lately' at a glance.
- Visual polish on threaded discussion + insights bars. New .ld-task-comment-reply visual treatment (left-border accent + lighter background) so threaded replies are visually distinct from new top-level comments without feeling buried. .ld-tag-insights-bar uses a brand→amber gradient for the proportional bars so the panel reads as a quick visual hierarchy rather than a number stack. The same Reply / Edit / Delete chip row keeps the comment header tight even with the new affordances.
Comment @-mentions, @desk → AI bridge, ⌘K command palette, richer empty states
- Comment @-mentions — Slack + email + outbound notifications. A comment that includes @<name-or-email> resolves the token against the workspace member roster (matches by first name, name slug, email local-part, or full email) and pings every matched teammate via Slack and email — fire-and-forget, never blocks the response. A new outbound webhook event task.mentioned fires alongside, so external CRMs/paging tools can mirror the alert. The TaskComments composer adds an inline mention popover that suggests the right token as the operator types after @. Closes the rev 26 silent-comment gap that made multi-operator teams' threads invisible until someone happened to look.
- @desk in a comment promotes it to authoritative AI direction. Type @desk in a comment and Loop Desk also calls addTaskOperatorNote() with the comment text (with the @desk token stripped). The next AI cycle reads it as authoritative direction, just like a rev-14 operator note. The comment itself stays in the teammate-only thread but gets an @desk → AI badge so the audit trail shows when chatter became direction. The bridge is one-way and explicit — the AI never sees regular comments, and a regular comment cannot accidentally become direction.
- Cmd/Ctrl + K command palette. Floating ⌘K button + global Cmd/Ctrl+K shortcut opens a typeahead palette covering navigate (Approvals, Active work, Recent signal, Memory, Desk health, Changelog, Docs), create (Hand the desk a task, Log a signal, Teach the desk, Add a source — all gated to non-viewer roles), and manage (Run desk now, Integrations + settings, Workspace members — gated to owner/admin). ↑↓ navigates, Enter runs. Layered on top of rev 17's / search shortcut and rev 23's g+ panel jumps for full power-user keyboard parity with Linear, Raycast, and GitHub command palettes.
- Richer empty states across Approvals, Active work, Memory, Recent signal, and Sources. Replaced the single-line .app-empty plain text with a new .app-empty-rich pattern: gradient panel + icon + title + helper copy + a ⌘K hint that points at the exact palette command needed to break the empty state. New workspaces saw five panels with thin one-line copy; rev 27 turns each into a small invitation that names the next step. Pairs with the rev-19 onboarding templates and the rev-11 activation checklist to make day-1 of a fresh desk feel guided rather than barren.
Task comments, output → memory promotion, source keyword filter, bulk task actions
- Task comments — teammate-to-teammate discussion. New task.comments column + endpoints + Discussion thread on every active-work card. Comments are explicitly distinct from operator notes (rev 14): notes feed the AI as authoritative direction; comments are for teammates to leave context, ask each other questions, or flag a question without polluting the AI's working set. The comment author or a workspace admin can delete. Outbound webhook event task.commented fires on every new comment so external automations can mirror the thread. Mirrored on the v1 surface (GET/POST /api/v1/tasks/{id}/comments).
- Output → memory promotion. New Save to memory chip on every approval-queue artifact. One click promotes the artifact's title + summary + a slice of body into a durable memory entry (kind=lesson by default, importance 8). Tags carry over. Closes the gap where a great output was approved and then archived without the underlying insight ever becoming durable workspace knowledge — future cycles will retrieve it via the existing memory scoring.
- Per-source keyword include/exclude filter. Every RSS, review-site, and LinkedIn source gets a Filter chip that opens an inline editor for include and exclude keyword lists (up to 10 each). Items whose title + body match an exclude keyword are dropped silently with an activity-log entry; if any include keywords are configured, items must match at least one. Lets operators tune noisy feeds without pausing them entirely. Stored on source.config (no schema change).
- Bulk task actions — select + delete / set priority / tag. Active-work panel now shows a checkbox on every task when the operator has edit role. Selecting one or more tasks reveals a bulk action bar above the queue: set priority (urgent/high/normal/low), add or remove a tag (with autocomplete from the workspace tag pool), or bulk delete (with confirm). New POST /api/tasks/bulk endpoint enforces a 50-task cap. Mirrors rev 6's bulk approve/archive on the artifact side.
Task filters + saved views, tag autocomplete, MCP-foundation completeness, personalised digest
- Task queue filters + saved views. Active-work panel mounts a new TaskFilterList with kind / priority / tag dropdowns plus a Mine-only chip and a Save filter button. Saved views persist to localStorage (per-workspace, six max), surface as one-click chips, and survive across sessions. Closes the filter symmetry — signals (rev 9), outputs (rev 12 + rev 15), memory (rev 8 + rev 21), and now tasks all support kind + tag scoping. The saved-view bookmark UX mirrors rev 18's saved searches.
- MCP-foundation completeness — POST /api/v1/tasks + PUT /api/v1/tasks/{id}/tags. The dashboard task-create endpoint (rev 24) and task-tags endpoint (rev 24) are now mirrored onto the bearer-authenticated v1 surface. An MCP host or external automation can now originate work directly without going through a webhook signal first, and tag any existing task. The /api/v1 self-describing index lists both new endpoints. The MCP server protocol-translation work no longer has to add bespoke task primitives — every dashboard mutation exists in v1.
- Tag autocomplete across tasks / artifacts / memory. Reusable TagInput subcomponent renders a suggestion popover whenever the operator opens the inline tag form on any of the three tag-enabled entities. Suggestions come from the workspace-wide tag pool for that entity, ranked starts-with first, then includes. ↑↓ navigates, Enter accepts, Esc cancels. Reduces the silent fragmentation problem (q3-launch vs q3launch vs q3_launch) that made cross-entity filtering unreliable.
- Personalised digest — Your assigned tasks. The daily digest email now appends a Your assigned tasks section per recipient, scoped strictly to the recipient's workspace user-id. Groups: Overdue, Due this week, Needs your input, In progress. Solo founders see only their own; multi-operator teams see each owner/admin's section in their own copy. No new schema; reuses task.assignedToUserId (rev 16) and dueAt (rev 22). Pairs with rev 24's email companion to make digest the canonical channel for assigned-and-due-soon work.
Manual task creation, task tags, due-task email reminders, focus polish
- Hand the desk a task directly. New POST /api/tasks route + dashboard form. Operators no longer have to log a fake signal first when they already know what the desk should do — type a title, summary, optional goal, pick output type and priority, optionally set a due date or assign to a teammate. Becomes the fifth direct operator → desk channel after approve/regenerate (rev 11), operator notes (rev 14), priority/due (rev 21/22), and inline body edit (rev 23).
- Task tags + tagging symmetry across signals/tasks/artifacts/memory. New task.tags JSONB column + PUT /api/tasks/:id/tags route. Every active-work card now mounts inline tag chips with the same lowercase-hyphen normalisation, 8-tag cap, and Plus / X UX as artifact tags (rev 15) and memory tags (rev 21). Closes the symmetry across all four core entities — a governance-first desk should let you label work as well as signal.
- Email companion to task due reminders. Rev 23 made due dates push-loud via Slack + outbound. Rev 24 adds the email channel for solo founders and operators who don't live in Slack. The 4h-before-due sweep now also emails the assignee (when assigned + Resend is configured); the activity log records exactly which channels fired (Slack + email + outbound).
- Visual polish — focus rings, hover details, pill row. Form fields gain a subtle 2px brand-color outline on focus for keyboard accessibility. Collapsed `<details>` elements (used in the task-create form's optional goal field) brighten on hover. Pill rows continue to wrap cleanly with the rev-23 row-gap.
Task due reminders, inline artifact edit, pinned tasks, keyboard shortcuts
- Task due-date Slack reminders. Closes the rev-22 deadline-aware queue loop. The cron pulse now sweeps every 5 minutes for tasks due within the next 4 hours; matches fire a Slack ping (respecting quiet hours) and the new task.due_soon outbound event. Each task is reminded once per due-window, and rescheduling clears the stamp so the new window re-fires.
- Inline artifact body edit before approval. AI output is sometimes 90% right but needs a name swapped or a sentence tightened. The new Edit chip on every approval-queue item opens an inline editor for title, summary, and body — save in place, no archive, no regenerate. Edits land in the activity log for the audit trail.
- Pinned tasks (symmetry with rev-22 pinned signals). Mirror of pinned signals: pin any task and it surfaces in a dedicated Pinned tasks panel above the active queue with a soft accent and a pulsing dot. Pinned tasks always sort first, regardless of due-date or priority, so a board-meeting deliverable can't slip behind a noisy queue.
- Power-user keyboard shortcuts + help overlay. Press ? to open the shortcut overlay; / focuses search (rev 17); g a / g t / g s / g m / g h jump to Approvals / Active work / Recent signal / Memory / Desk health. A floating keyboard-icon FAB opens the overlay for discovery. The cheapest power-user retention wedge available.
- Visual polish on pinned panels and pill rows. Pinned-tasks panel gets the same brand-colour border accent + gradient background introduced for pinned signals in rev 22; pinned cards get a pulsing dot on the left edge so they're scannable. The pill row in active-work cards now wraps with proper row-gap so long pill chains stay readable on narrow viewports.
Task due dates, pinned signals, memory bulk import, outputs CSV export, lint fix
- Task due dates with deadline-aware queue. Every open task gets a Set due button with five presets (1h, end of today, tomorrow 5pm, in 3 days, in 1 week). The queue sort now boosts overdue tasks first, then tasks due in the next 24h, before falling back to manual priority. Pill colour shifts from neutral to live (soon) to warn (overdue) so the queue scans at a glance.
- Pinned signals — keep what matters at the top. Mirror of the rev-5 memory pinning pattern. Pin any signal from the recent-signal panel and it surfaces in a dedicated Pinned section with a soft brand-colour accent. Pinned signals stay visible regardless of recency — perfect for tracking a competitor move or escalating customer thread across multiple cycles.
- Memory bulk import (paste-list shortcut). New "Bulk import" button on the memory panel opens a textarea where you paste up to 30 lines (one memory per line). Pick a kind, importance, and optional pin — every line becomes its own memory entry. Cuts the manual one-by-one tax that the rev-19 onboarding templates only solved for the very first cycle.
- Outputs CSV export (governance audit trail trio). GET /api/workspace/artifacts-export returns the workspace artifact catalog as CSV (createdAt, updatedAt, kind, status, title, summary, tags, share URL, view count). Pairs with the rev-6 JSON export and the rev-7 activity-log CSV to round out the procurement / SOC 2 evidence trio.
- Lint fix on the desk control's pause-for menu (rev-21 follow-up). Cleaned up an impure-during-render call that snuck into the rev-21 scheduled-pause UI — the menu now renders exactly the same but no longer triggers React's purity rule on lint.
Manual task priority, scheduled pause, activity log filter, cost cap warnings, memory tags
- Manual task priority editing. Click a task's priority pill to promote it from normal to urgent (or demote it to low). Every change re-orders the queue immediately and writes an activity-log entry. Third operator-steering primitive after approve/regenerate (rev 11) and operator notes (rev 14).
- Scheduled pause with auto-resume. Press "Pause for…" in desk control to pick a preset (1h / 4h / tomorrow 9am / next Monday 9am) — the desk auto-pauses now and auto-resumes when the window expires. Cron + daemon both honour it. Removes the most common reason operators leave the desk off entirely (vacations, weekends, deep work).
- Activity log filter chips. The Live activity panel now renders kind chips (cycle / capture / task / output / cost / outbound / status / source / member) when there are 2+ kinds; tap to filter, tap again to clear. The activity row limit also bumped 8→30 so the filter is meaningful.
- Cost cap 80% Slack warning. Companion to the rev-20 hard cap. When today's spend hits 80% of cap, the desk pings Slack (if configured), writes an activity-log entry, and fires a workspace.cost_cap_warning outbound event — once per day in workspace timezone. Auto-pause is still the hard ceiling; this is the heads-up before that.
- Memory tags + tag filter. Mirror of the rev-15 artifact tags pattern: every memory entry can carry up to 8 lowercase-hyphen tags. The memory panel surfaces a tag filter row when at least one tag exists; click any tag chip to scope the list. Search now also matches tags.
Daily cost cap, workspace timezone, version history, /api/v1/activity, search keyboard nav
- Daily cost cap with auto-pause. Set a hard ceiling on today's OpenAI spend (e.g. $5/day). When hit, the desk auto-pauses; resume from the dashboard. The strongest possible answer to per-cycle credits — you literally cannot overspend, and there's nothing to top up.
- Workspace timezone (no more UTC quiet hours). Pick an IANA timezone for your workspace. Slack quiet hours and the daily cost-cap rollover now respect it — set quiet hours in your local time and stop doing the UTC math at 11pm.
- Artifact version history. Every regenerate now links the new output to the archived prior version. The dashboard surfaces a Version history button that walks the chain; the public share page calls out when an output replaces an earlier draft. Closes the regenerate audit-trail gap.
- GET /api/v1/activity (governance audit trail over the API). Workspace activity log is now bearer-authenticated over the JSON API. Filter by since=ISO and kind=k1,k2. Foundation for the upcoming MCP server's audit-trail tool.
- Search keyboard navigation (↑↓ + Enter). Press / to open the workspace search, then arrow up/down to highlight a result and Enter to scroll the dashboard to that section. Power-user shortcut on top of the rev-17 search and rev-18 saved queries.
Per-event outbound subscriptions, delivery log, onboarding templates, public health badge
- Per-event outbound webhook subscriptions. Rev 17 shipped a single outbound URL; rev 18 expanded the events; rev 19 adds the routing model. Add multiple URLs, each scoped to a subset of events: pipe artifact.approved to Notion, signal.created to Salesforce, task.assigned to Linear — all from the same workspace, signed with one secret.
- Outbound delivery log + one-click retry. Every outbound POST is logged with status, HTTP code, and reason. The integrations panel surfaces the last 20 attempts; failed deliveries get a retry button that replays the original payload. No more guessing whether a downstream tool got the event.
- Industry onboarding templates. New workspaces pick from Ecommerce, SaaS, Consulting, Agency, or Creator templates during onboarding — each pre-seeds three high-importance preferences and a sample signal so the first cycle has substance. Cuts the empty-desk first-week cliff.
- Public desk health badge (SVG). GET /api/v1/badge.svg returns a shields.io-style live badge of your desk health score. Embed in a README, a Notion page, or a status page — it ticks every five minutes against your real desk.
- Visual polish on the desk health widget. Tightened the dial proportions, larger score numeral, cleaner spacing on the breakdown bars. Same data, more legible.
Outbound event expansion, share feedback, /api/v1/stats, saved searches
- Outbound webhooks now fire on artifact.approved, signal.created, and task.assigned. Rev 17 shipped artifact.ready as the first outbound event. Rev 18 expands to four real-world events teams asked for: when an output is approved, when a fresh signal lands, and when a task is routed to a teammate. Same HMAC signing, same single URL.
- Stakeholder feedback on share links. Every public share page now has a feedback form. Stakeholders can react with looks-good, raise-a-concern, or leave a comment — and the response flows back into the desk as a fresh feedback signal. Concerns land at high priority. Closes the bidirectional loop on shared briefs.
- /api/v1/stats — desk health over the API. Pulls the dashboard's health score, runtime phase, 7-day cycles/signals/approvals/cost, signal mix, and latency. Foundation for the MCP server's 'how's my desk doing?' tool.
- Saved searches in the workspace search bar. Type a query, click the bookmark icon, and it sticks around as a one-click chip the next time you focus the search bar. Up to 6 per workspace, persisted in localStorage.
- Stuck-loop Slack alert (server-side). Rev 17 added an in-app banner. Rev 18 adds a Slack-ping companion that fires from the daily digest cron when a desk has been on but quiet for 3x its loop interval. Rate-limited to once per 12 hours.
Outbound webhooks, workspace search, stuck-loop detection, assignee pings
- Outbound webhooks for artifact.ready. Save a URL in the integrations panel and Loop Desk POSTs every approval-ready artifact to it — signed with the same HMAC secret as inbound. Pipe into Zapier, n8n, your CRM, or anything that speaks JSON.
- Workspace search (with `/` shortcut). A keyword box at the top of the dashboard searches signals, tasks, outputs, and memory at once. Press / from anywhere on the page to focus it. /api/v1/search exposes the same primitive for MCP and external clients.
- Stuck-loop detection banner. If the desk is on but no cycle has fired for 3x the loop interval (or never, on a brand-new workspace), a red banner offers a one-click 'Run desk now'. Heartbeat tells you the state — this nudges action when something is wrong.
- Task-assignee notifications. When a task is assigned to a teammate, Loop Desk posts to Slack and emails the assignee directly. Closes the routing loop opened by rev 16's task assignment.
Operator steering, cycle latency, and a public changelog
- Operator notes on tasks. Tell the desk what to focus on without writing any new context up front. Notes are appended to the task and read by the next work cycle as authoritative direction.
- Cycle latency + success rate. A new sidebar panel shows median + p95 cycle wall-clock time and 7-day success rate. The desk's performance is now legible at a glance.
- Mobile-responsive dashboard. Status bar and sidebar grid stack cleanly on phones; metrics rebalance for narrow viewports. Same loop, more places to use it.
- Public changelog. You're reading it. Velocity is part of the product — every revision lands here within hours of the commit.
Governance hardening + a single trust number
- HMAC on the email forwarding webhook. The same per-workspace signing secret that protects the signal webhook now protects inbound email parsing.
- API v1 expanded: tasks + sources. GET/PATCH /api/v1/tasks, GET/POST /api/v1/sources, with plan-tier enforcement. The MCP server is now exclusively a protocol-translation job.
- Share link 14-day sparkline. Every public share page now records a per-render event; the share button surfaces a 14-day SVG sparkline of who's actually reading.
- ISO 42001 governance docs. Loop Desk's controls are mapped to ISO/IEC 42001 control families: human oversight, transparency, risk management, data governance.
- Desk health score. A 0-100 score from loop liveness, signal flow, cycle activity, and approvals + sources. One number, four explainable parts.
Programmatic API, output filter, heartbeat, work log
- API v1 (MCP foundation). Versioned, ingest-token-authed JSON surface at /api/v1/* — workspace, signals, artifacts, memory, runs. Self-describing index at /api/v1.
- Loop heartbeat. A live indicator in the dashboard status bar that pulses Live / Cycle due / Lagging / Paused / Off. The 'is the loop alive?' trust gap is closed.
- Per-task work log. Every cycle's progress on a task is now visible inline — a styled timestamped trail per task, governance parity with the workspace activity log.
Stakeholder share links + approval-loop closure
- Public share links for any output. One click mints a robots-noindexed /share/[token] URL. Read-only, scrubbed from JSON exports.
- Reviewer notes on approve. Add a 500-char note when approving an output. Stored as a high-importance preference memory + appended to the audit trail.
- Regenerate output. Try again on any approval-queue item. Archives the current artifact, optionally captures a reviewer note, and re-queues the parent task.
- Activation checklist. 5 steps to first value: connect a source, log a signal, run a cycle, approve an output, wire an integration. Auto-hides at 100%.
Governance-first positioning + markdown outputs
- Governance-first landing page. New positioning: the AI that remembers your business — and never acts without you. Pricing section explicitly contrasts with Notion Custom Agents' per-cycle credits.
- Markdown output rendering. Artifact bodies render with proper headings, lists, code blocks. External links open safely in a new tab.
- Review site + LinkedIn source types. G2 / Trustpilot / Google Reviews via RSS bridges; LinkedIn via rss.app or fetchrss. The top-3 unmet SMB signal sources, shipped.
Older revisions live in our git history. Start free — there are no metered credits to run out.