{"name":"Loop Desk API","version":"v1","auth":"Bearer <workspace ingest token>","endpoints":{"GET /api/v1/workspace":"Workspace profile + effective loop interval","PATCH /api/v1/workspace/loop":"Set desk state — JSON: { loopState: 'on' | 'paused' | 'off' } (rev 82 — closes the v1 parity gap on the rev-1 desk-state primitive; pairs with /workspace/pause-until for full desk-state control on the protocol-bound surface)","PATCH /api/v1/workspace/pause-until":"Schedule auto-resume pause — JSON: { pauseUntilAt: ISO | null } (max 14d). rev 82 — mirrors the rev-21 dashboard scheduled-pause primitive so MCP hosts can pause + auto-resume without dashboard sessions","PATCH /api/v1/workspace/loop-interval":"Set the per-workspace loop interval — JSON: { loopIntervalMinutes: number(1-1440) }. rev 140 — closes a long-outstanding configuration gap; the column has been settable in the schema since rev 1 but had no UI/API surface. Plan-tier floor (free=15, pro=5, team=1) is layered server-side. Returns both the requested + effective values.","GET /api/v1/workspace/focus-tags":"Current team focus tags (up to 3 lowercase-hyphen)","PUT /api/v1/workspace/focus-tags":"Set team focus tags — JSON: { tags: string[] } (≤3, lowercase-hyphen)","GET /api/v1/workspace/focus-history?sinceDays=90&limit=30":"Focus drift timeline — every focus shift in the window with duration","GET /api/v1/signals?limit=50&since=ISO":"Recent signals (max 200)","POST /api/v1/signals":"Create a signal — JSON: { kind, priority, title, detail, sourceUrl? }","GET /api/v1/artifacts?status=ready&kind=draft&limit=50":"Recent outputs (filterable)","GET /api/v1/tasks?status=queued,in_progress&limit=50":"Active queue (filterable by status)","POST /api/v1/tasks":"Create a manual task — JSON: { title, summary, kind?, deliverableType?, priority?, dueAt?, tags? }","PATCH /api/v1/tasks/{id}":"Update task status — JSON: { status }","PUT /api/v1/tasks/{id}/tags":"Replace task tags — JSON: { tags: string[] }","PUT /api/v1/tasks/{id}/blockers":"Set task dependencies — JSON: { blockedByTaskIds: string[≤8] } (rev 37 — pulse engine skips tasks until all blockers are status=done; fires task.unblocked outbound event when the last blocker flips to done)","GET /api/v1/tasks/{id}/comments?q=&authorId=&since=ISO":"List teammate-to-teammate comments on a task. Optional q (≤200 chars) keyword filter matches body + author name; optional authorId scopes to one author; optional since=ISO scopes to comments posted at-or-after that instant. Composes via intersection. Replies always surface with their parent so the thread frame never breaks. Returns { comments, total, filtered, matchedIds, query, authorId, since, authors }. (rev 134 — closes the recency axis on the per-task comment surface: until rev 134 the cluster 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. The since param + echo field let MCP hosts polling for 'what's new on this task in the last hour' walk 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 reads one consistent contract. Bounded to a 1-year max lookback. Rev 133 — closes the named rev-132 next-sprint candidate at the share-permalink axis: a `#comment-<id>?q=…&author=…&reactions=…&match=…` URL hash carries the rev-128 keyword/author + rev-130 reactions-only filter + rev-131 active-match index so MCP hosts driving deep-links from external systems (Slack interactive shortcuts, CRM linkbacks, procurement audit notes) build the same URL shape and benefit from the same filter+match restoration on the recipient's side. Bare `#comment-<id>` keeps the rev-31 / rev-128 behaviour exactly so existing permalinks (mentions inbox, digest emails) are unaffected. Rev 132 — when q is supplied, the response also carries `matchedIds` (ordered list of comment ids whose body OR author name contains the query) so MCP hosts rendering the rev-131 N/M match counter chip + ↑↓ arrow-key navigation can rotate by index without re-running the matcher; closes the named rev-131 next-sprint candidate at the v1 axis. 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). Rev 131 — every comment row + every distinct-author entry also carries `authorInitials` + `authorHue` so MCP hosts render the rev-130 avatar primitive without re-implementing the hash + initials helpers. Pairs with `/tasks/{id}/engagement` for the full per-thread visibility cluster.)","POST /api/v1/tasks/{id}/comments":"Post a comment — JSON: { text, asUserId?, parentCommentId? } (parentCommentId replies into a thread; defaults to workspace owner)","GET /api/v1/tasks/{id}/engagement":"Aggregate reaction-summary across every comment on the task. Returns { commentCount, totalReactions, topEmoji, reactionTotals: Record<emoji,count>, topReactedComments }. Mirrors the rev-128 dashboard `.ld-task-reaction-summary` chip on the v1/MCP surface — closes the per-task engagement-visibility axis at parity in the same cycle as rev-129 search. (rev 129)","GET /api/v1/tasks/{id}/notes":"List operator notes on a task — the rev-14 AI-direction primitive (read-side mirror, rev 76)","POST /api/v1/tasks/{id}/notes":"Post an operator note — JSON: { text } (≤600 chars). Fed to the next AI cycle as authoritative direction; auto-promotes needs_input → queued. Mirrors the rev-14 dashboard endpoint (rev 76 — closes the v1 parity gap on the operator-direction primitive)","POST /api/v1/tasks/{id}/comments/{commentId}/reaction":"Toggle a comment reaction — JSON: { emoji, asUserId? } (one of 👍 / 👀 / 🎯 / ❤️ / 🚀; defaults to workspace owner)","POST /api/v1/artifacts/{id}/reaction":"Toggle an output reaction — JSON: { emoji, asUserId? } (lightweight ack on an artifact without writing a comment)","POST /api/v1/memory/{id}/reaction":"Toggle a memory-entry reaction — JSON: { emoji, asUserId? } (validate a teammate's add without editing the memory itself)","POST /api/v1/signals/{id}/reaction":"Toggle a signal reaction — JSON: { emoji, asUserId? } (rev 34 — closes the four-entity reaction symmetry)","POST /api/v1/tasks/bulk":"Bulk task action — JSON: { action: \"delete\"|\"priority\"|\"tag\"|\"untag\", taskIds: string[≤50], priority?, tag? } (rev 35)","POST /api/v1/signals/bulk":"Bulk signal action — JSON: { action: \"delete\"|\"pin\", signalIds: string[≤50], pinned? } (rev 35)","POST /api/v1/memory/bulk-update":"Bulk memory action — JSON: { action: \"delete\"|\"pin\"|\"tag\"|\"untag\", memoryIds: string[≤50], pinned?, tag? } (rev 35)","POST /api/v1/artifacts/bulk":"Bulk artifact action — JSON: { action: \"approve_all\"|\"archive_all\" } (rev 35)","POST /api/v1/sources/bulk":"Bulk source action — JSON: { action: \"pause\"|\"resume\"|\"delete\", sourceIds: string[≤50] } (rev 36 — closes the five-entity bulk symmetry)","GET /api/v1/sources?limit=50":"Connected signal sources. Each row carries pollIntervalMinutes (rev 141 per-source override; null = workspace cadence) AND pollGating: { intervalMinutes, isOverride, lastSyncedAt, nextPollAt, isDueNow, minutesUntilDue } | null (rev 143 — null for non-pollable sources; mirrors the rev-142 /sources/cadence-overview perSource shape exactly so MCP hosts read one consistent vocabulary across both surfaces and don't need a follow-up call to render 'feed X polls in 3m')","POST /api/v1/sources":"Add a source — JSON: { type, label, url?, notes? }","PATCH /api/v1/sources/{id}":"Mutate a single source — JSON: { status: \"pending\"|\"connected\"|\"error\" } (rev 6 pause/resume) OR { label: string≤80 } (rev 95 rename) OR { pollIntervalMinutes: int 1-1440 | null } (rev 141 per-source poll-cadence override; null reverts to workspace cadence). Mutually exclusive payloads — exactly one field per request. The RSS poller honours lastSyncedAt + pollIntervalMinutes per source so a daily-published feed can run on a slower cadence than the workspace loop interval (rev 140) without dragging the rest of the workspace down.","POST /api/v1/sources/preview":"Validate a feed URL synchronously — JSON: { url, type? } (rev 40 — mirrors the rev-38 dashboard endpoint so bearer-auth integrations can pre-validate before creating)","POST /api/v1/sources/bulk-cadence":"Bulk per-source poll-cadence override — JSON: { pollIntervalMinutes: int 1-1440 | null, sourceIds?: string[≤50] }. Operates workspace-wide (omit sourceIds) or scoped to a subset. pollIntervalMinutes:null clears the override (revert to workspace cadence — rev 140). Manual / file-based sources are skipped silently (only feed-style sources rss / review_site / linkedin honour the rev-141 cadence axis). Closes the named rev-142 next-sprint candidate at the cadence axis by collapsing the N-tap throttle of 20+ feeds to one operation. Pairs with PATCH /sources/{id} (rev 141 single-source) + GET /sources/cadence-overview (rev 142 read-aggregate) as the third primitive on the per-source cadence cluster (rev 143)","POST /api/v1/sources/bulk-rename":"Bulk source rename / find-and-replace — JSON: { findText, replaceText, sourceIds?: string[≤50], caseSensitive?: boolean, preview?: boolean }. Operates workspace-wide (omit sourceIds) or scoped to a subset. Pass preview:true to return matches without applying. Closes the named rev-95 next-sprint candidate by collapsing the N-click rebrand of 20+ feeds to one operation (rev 96)","GET /api/v1/workspace/sources-export?format=csv|json":"Sources catalog with rev-16 health diagnostics + rev-26 keyword filter columns + rev-12 7-day signal-rate column. Default CSV download. Pass format=json for the same rows as a typed JSON payload with includeKeywords/excludeKeywords as arrays — closes the v1 export JSON symmetry across the six-axis procurement-evidence cluster (activity / outputs / decisions / stale-tasks / sources / cost). (rev 96 — mirrors the rev-96 dashboard endpoint; rev 121 added JSON variant)","GET /api/v1/workspace/tag-search?tag=…&limit=25":"Cross-entity drill-down for a tag (rev 40 — returns tasks/outputs/memory entries that carry the tag)","POST /api/v1/workspace/tags/rename":"Rename / merge a tag across the workspace — JSON: { from, to } (rev 40 — closes the rev-39 v1 parity gap)","GET /api/v1/tasks/{id}/timeline?limit=60":"Per-task unified timeline aggregating creation + AI cycles + operator notes + comments + activity (rev 41 — mirrors the rev-40 dashboard endpoint)","GET /api/v1/tasks/stale?thresholdDays=5&limit=10&assignedToUserId=…":"Stale tasks — queued/in_progress for >thresholdDays without an updatedAt bump, pinned excluded. rev 49 — optional per-assignee scoping mirrors the rev-49 digest change so MCP hosts on multi-operator desks can answer 'which of my tasks are rotting?'","GET /api/v1/memory/stale?thresholdDays=30&limit=12&includeNeverRetrieved=true&tag=…":"Stale memory entries — durable knowledge the AI cycle hasn't pulled in `thresholdDays` (default 30, range 7-365). Pinned + importance>=9 entries excluded server-side. Each row carries `lastRetrievedAt`, `retrievalCount`, `daysSinceRetrieved`, `daysSinceCreated` so MCP hosts can render 'used Nd ago' / 'never used' affordances without extra round-trips. Mirrors the rev-48 `/tasks/stale` pattern at the memory axis. Until rev 153 the memory entity was *write-only* on the usage axis — this endpoint closes the diagnostic gap on the protocol-bound surface (rev 153). rev 156 added optional `?tag=…` filter for per-workstream scoping.","GET /api/v1/memory/{id}/retrieval-trajectory?days=7":"Per-memory retrieval trajectory — trailing N daily retrieval counts (default 7, max 30) for a single memory entry in workspace timezone, oldest → newest, with zero-fill for days the entry wasn't retrieved. Reads `memory_entries.retrievalHistory` the pulse engine bumps via jsonb_set on every retrieval. Mirrors the rev-54 `/tasks/{id}/cost-trajectory` at the per-memory axis on the trajectory dimension. Closes the *trajectory* axis on the per-memory observability cluster — rev 153 surfaced *cumulative* count + recency, rev 157 surfaces *shape over time*.","GET /api/v1/memory/{id}/cost-trajectory?days=7":"Per-memory cost trajectory — trailing N daily cost buckets (default 7, max 30) for a single memory entry in workspace timezone, oldest → newest, with zero-fill for days the entry wasn't retrieved. Reads `memory_entries.dailyCostHistory` the pulse engine bumps via jsonb_set alongside the rev-159 cumulative column writes. Mirrors `GET /tasks/{id}/cost-trajectory` (rev 54) and `GET /memory/{id}/retrieval-trajectory` (rev 157) at the per-memory cost axis. Each row: `{ date, inputTokens, outputTokens, estimatedCostUsd }`. Closes the *trajectory* axis on the cost dimension at the per-memory level the same way rev-157 closed it on the retrieval dimension — rev 159 surfaced *cumulative* attributed cost, rev 160 surfaces *shape over time* (rev 160 — closes the rev-159 next-sprint candidate at the cost-trajectory axis).","GET /api/v1/memory/top-retrieved?limit=5&days=7":"Top-retrieved memory entries — workspace axis on the rev-157 trajectory primitive. Returns the N memory entries with the highest trailing-7-day retrieval count (sum of `retrievalHistory` JSONB across the workspace-TZ window). Each row carries `retrievals7d`, `retrievalCount`, `trajectory7d` array (oldest → newest), `pinned`, `tags`. Mirrors the rev-51 `/tasks/top-cost` shape (top-N by trailing-window activity) at the per-memory axis on the retrieval dimension. Pairs with `/memory/stale` (workspace axis on the staleness dimension) and `/memory/{id}/retrieval-trajectory` (per-memory axis on the trajectory dimension) for the full per-memory observability story across all three axis × dimension cells (rev 158).","GET /api/v1/memory/top-cost?limit=5":"Top-cost memory entries — workspace axis on the cost dimension. Returns the N memory entries with the highest cumulative attributed AI cost. The pulse engine distributes each cycle's per-task token delta across the memories the AI pulled by equal split — same defensible methodology as the rev-57 per-source cost attribution. Each row carries `totalAttributedInputTokens`, `totalAttributedOutputTokens`, `estimatedCostUsd`, `retrievalCount`, `lastRetrievedAt`, `pinned`, `tags`. Mirrors the rev-51 `/tasks/top-cost` shape at the per-memory axis on the cost dimension. Pairs with `/memory/stale` (staleness), `/memory/top-retrieved` (retrieval), and `/memory/{id}/retrieval-trajectory` (trajectory) — the per-memory observability cluster on v1 is now thirteen axes deep (rev 159 — closes the named rev-158 next-sprint candidate at the cost axis).","GET /api/v1/memory/cost-spikes?limit=10":"Per-memory cost spike alarm — closes the cost-spike alarm cluster's seventh axis on the protocol-bound surface alongside workspace (rev 32 outbound), per-task (rev 55 `/tasks/cost-spikes`), per-source (rev 58 `/sources/cost-spikes`), per-assignee (rev 62 outbound), per-tag (rev 67 `/cost/by-tag/spikes`). Live memory entries today >= 2× their trailing 7-day daily average AND >= $0.50 absolute. Pinned + importance>=9 entries excluded server-side as load-bearing-by-design (alarming on them is tautological). Pure read of the rev-160 dailyCostHistory primitive — no schema cost. Pairs with `/memory/top-cost` (rev 159 cumulative attribution) + `/memory/{id}/cost-trajectory` (rev 160 trajectory) as the three-axis cost-observability surface on the per-memory entity. As of rev 162, each row also carries `costSpikeAckedAt` so MCP hosts can render the muted-state chip without a follow-up call (rev 161 — closes the cost-spike alarm cluster on the per-memory axis; rev 162 — adds operator counter-action surface).","POST /api/v1/memory/{memoryId}/cost-spike-ack":"Acknowledge a per-memory cost spike — stamps `memory_entry.costSpikeAckedAt` so the rev-161 detector + daily Slack push + outbound `memory.cost_spike` event all skip this entry for the rest of today (workspace TZ). Mirrors the rev-56 task / rev-59 source / rev-63 assignee / rev-68 tag ack at the per-memory axis. Closes the alarm cluster's seventh axis on the closure-receipt loop — fires the new `memory.cost_spike_acked` outbound event (rev 162 — closes the named rev-161 next-sprint candidate at the per-memory axis on the operator counter-action surface).","POST /api/v1/memory/cost-spike-ack/bulk":"Bulk acknowledge per-memory cost spikes — body { memoryIds: string[≤50] }. Mirrors `/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 at the per-memory axis. Caps at 50 IDs per call. Fires one `memory.cost_spike_acked` outbound event per bulk call (rev 162 — closes the cost-spike-ack v1 cluster across all five axes).","GET /api/v1/memory/chronic-warnings?limit=10":"Per-memory chronic warning read — entries whose `consecutiveSpikeDays` counter has crossed the chronic threshold (3 days) AND haven't been chronic-acked within the 7-day TTL. Mirrors `/cost/by-source/chronic-spikes` (rev 71) + `/cost/by-assignee/chronic-spikes` (rev 71) + `/cost/by-tag/chronic-spikes` (rev 70) at the per-memory axis on the chronic horizon. Closes the chronic axis on the cost-spike alarm cluster's seventh axis at parity with the daily axis (rev 161) on the protocol-bound surface (rev 163 — closes the named rev-162 next-sprint candidate at the chronic-detection layer).","POST /api/v1/memory/{memoryId}/chronic-ack":"Acknowledge a per-memory chronic warning — stamps `memory_entry.chronicAckedAt` so the rev-163 chronic-warning sub-sweep + Slack push + outbound `memory.chronic_warning` event all skip this entry for the next 7 days (workspace TZ). Mirrors the rev-71 tag chronic / rev-72 source chronic / rev-72 assignee chronic at the per-memory axis. Closes the chronic axis on the cost-spike alarm cluster's seventh axis on the closure-receipt loop — fires the new `memory.chronic_warning_acked` outbound event (rev 163).","POST /api/v1/memory/chronic-ack/bulk":"Bulk acknowledge per-memory chronic warnings — body { memoryIds: string[≤50] }. Mirrors `/sources/chronic-ack/bulk` (rev 87) + `/cost/by-tag/chronic-ack/bulk` (rev 87) at the per-memory axis on the chronic horizon. Caps at 50 IDs per call. Fires one `memory.chronic_warning_acked` outbound event per bulk call (rev 163 — closes the chronic-ack v1 cluster across all chronic axes).","GET /api/v1/tasks/auto-archived?sinceDays=30&limit=50":"Tasks recently auto-archived by the rev-49 sweep (rev 50 — audit-trail closure for the stale-task lifecycle on the protocol-bound surface)","POST /api/v1/tasks/{id}/renew":"Re-affirm a task: bump updatedAt + clear archive warning. Optional `{ note: string ≤ 600 chars }` body routes the note through the rev-14 operator-notes primitive so the next AI cycle reads it as authoritative direction (closes rev-111 named candidate at the renew axis — rev 112). Without note, behaviour is the rev-50 default unchanged.","POST /api/v1/tasks/{id}/duplicate":"One-tap clone of an existing task as a fresh queued task with ' (copy)' suffix. Inherits kind, deliverableType, priority, assignee, tags, goal, context. Clears workLog, operatorNotes, comments, blockedByTaskIds, sourceSignalIds, sourceMemoryIds, dueAt — the original's lifecycle stays with the original. JSON: { asUserId? } — defaults to workspace owner. Mirrors the rev-110 dashboard endpoint in lockstep with the cadence pattern from rev 37 onward (rev 110)","GET /api/v1/workspace/auto-archive-config":"Read the workspace's stale-task auto-archive threshold in days (null = off) (rev 49)","PUT /api/v1/workspace/auto-archive-config":"Set the threshold — JSON: { staleTaskAutoArchiveDays: number(7-90) | null } (rev 49 — closes the lifecycle: rev 47 detect, rev 48 surface, rev 49 act)","GET /api/v1/workspace/memory-archive-config":"Read the workspace's stale-memory auto-archive threshold in days (null = off) (rev 154 — closes the descriptive→defensive loop on the memory usage story alongside rev-153's read primitive)","PUT /api/v1/workspace/memory-archive-config":"Set the threshold — JSON: { staleMemoryAutoArchiveDays: number(30-365) | null } (rev 154 — closes the named rev-153 next-sprint candidate ('memory auto-archive primitive'))","GET /api/v1/tasks/{id}/sources":"Per-task input transparency — returns the signals that originated this in-flight task (rev 43 — mirrors the rev-43 dashboard endpoint)","GET /api/v1/tasks/{id}/source-memory":"Per-task memory transparency — returns the memory entries the AI cycle pulled for this task (rev 44 — closes the rev-43 named follow-up)","GET /api/v1/artifacts/{id}/sources":"AI-cycle inputs that shaped this artifact — returns the signals + memory entries the cycle saw (rev 41)","GET /api/v1/artifacts/{id}/versions":"Full version chain for an artifact — walks the rev-20 parentArtifactId chain (max 12 hops) (rev 44 — mirrors the rev-20 dashboard endpoint)","POST /api/v1/workspace/import":"Append memory + signals + sources from an export JSON — JSON: { data, categories?: { memory?, signals?, sources? } } (rev 41 — mirrors the rev-40 dashboard endpoint)","GET /api/v1/workspace/summary":"All-time procurement-friendly counts (signals, artifacts by status, memory, sources by status, tasks by status, cycles, approved artifacts, anchor timestamps) (rev 43 — mirrors the rev-42 dashboard panel)","GET /api/v1/workspace/today":"Today snapshot — signals/cycles/outputs/approvals/spend in workspace timezone, plus yesterday delta + 7-day baseline (rev 45 — mirrors the rev-33 dashboard Today panel)","GET /api/v1/workspace/export":"Full workspace JSON export — workspace profile, sources, signals, tasks, artifacts, memory, recent runs/activities, plus rev-43/44 source-evidence appendices. Tokens + share metadata scrubbed (rev 45 — mirrors the rev-6 dashboard endpoint)","GET /api/v1/decisions?windowDays=30&limit=10":"Decisions log — last N approved/archived non-brief artifacts in the trailing window (rev 46 — closes the rev-9 dashboard sidebar v1 parity gap, 37 revs old)","GET /api/v1/outbound/deliveries?limit=20":"Recent outbound webhook delivery attempts (rev 46 — closes the rev-19 dashboard outbound delivery log v1 parity gap)","POST /api/v1/outbound/deliveries/{id}/retry":"Re-POST a stored payload from the delivery log (rev 46 — closes the rev-19 retry button v1 parity gap)","GET /api/v1/memory?limit=50":"Durable workspace memory. rev 158: every row also carries `retrievals7d` (sum of trailing 7 days of the rev-157 retrievalHistory in workspace TZ) alongside the rev-153 `retrievalCount` cumulative total + `lastRetrievedAt` recency stamp. MCP hosts ranking memory by recent-activity vs all-time see both signals in one call. rev 159: rows now also carry `totalAttributedInputTokens`, `totalAttributedOutputTokens`, and `estimatedCostUsd` so MCP hosts can rank memory by AI cost without a follow-up call per entry.","POST /api/v1/memory":"Teach the desk — JSON: { kind, title, content, importance?, pinned? }","GET /api/v1/runs?limit=25":"Recent loop cycles + token spend","GET /api/v1/activity?limit=50&since=ISO&kind=k1,k2&q=keyword":"Workspace activity log (governance audit trail). rev 46 — q does an in-place keyword filter across detail + kind, mirroring the rev-38 dashboard inline activity-log search.","GET /api/v1/search?q=…&limit=20":"Search signals, tasks, artifacts, memory, comments, activity log, and sources by keyword (rev 96 — closes the seventh-axis search coverage gap)","GET /api/v1/insights":"Top tags this 14-day window across tasks, artifacts, and memory","GET /api/v1/stats":"Desk health score, runtime phase, 7-day cycles/signals/approvals/cost","GET /api/v1/badge.svg?token=…":"Public shields.io-style SVG showing desk health score (no auth header — token in query)","GET /api/v1/roadmap-votes":"Public roadmap upvote counts + 14-day per-day trend + 7-day momentum per item, plus a top-5 most-requested ranking and a top-5 most-trending ranking (by trailing-7-day momentum, excludes items already in mostRequested so the two rankings never duplicate). No auth — public marketing surface (same model as /badge.svg). Closes the named rev-97 next-sprint candidate (rev 98). rev 99 — adds momentum7d + mostTrending alongside the existing cumulative ranking.","GET /api/v1/roadmap-items":"Public roadmap content (phases + items with title, description, status, key) as JSON. No auth — public marketing surface. Pairs with /roadmap-votes as the machine-readable companion to the public /roadmap page (rev 98).","GET /api/v1/onboarding-templates":"Public onboarding templates list — twenty-one industry-tuned templates (ecommerce / SaaS / consulting / agency / creator / healthcare / real-estate / legal / field-services / restaurant / education / property-management / nonprofit / accounting / fitness / B2B-services / home-services / creator-infrastructure / small-manufacturing / franchise / financial-advisor) that pre-load a workspace with brand voice, decision rules, and red-flag lessons specific to the vertical. Each row carries key + name + description + memoryCount + signalCount + url + the full memories[] + signals[] arrays so MCP hosts can render an industry-fit picker without scraping the rev-166 /templates page or the rev-169 per-template detail pages. No auth — public marketing surface (same model as /api/v1/roadmap-items + /api/v1/changelog + /api/v1/blog). Pairs with the existing public-marketing trio as the fourth axis: planned (roadmap), shipped (changelog), brand voice (blog), industry-fit (templates). Cache-control public, max-age=300, s-maxage=1800. Closes the named rev-171 next-sprint candidate (MCP server protocol-translation work) at the templates axis (rev 172).","GET /api/v1/changelog?limit=20&sinceRev=N":"Public changelog releases (newest-first). No auth — public marketing surface. Returns rev label + date + title + highlights so MCP hosts and procurement teams can read 'what shipped recently' without scraping /changelog HTML. Pairs with /roadmap-items (what's planned) and /roadmap-votes (what's most-requested) as the three axes of the public marketing surface on the protocol-bound side. rev 101 adds optional sinceRev query param so MCP hosts polling the cadence can fetch only new revs since their last read ('what's new since rev 95?'). Composes with limit. Cache-control public, max-age=300, s-maxage=1800 (rev 100/101).","GET /api/v1/changelog/{rev}":"Single changelog rev detail. Returns 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. Mirrors the rev-169 dashboard per-rev detail page on the v1 surface. Pairs with /api/v1/changelog (listing) as the two-axis changelog read surface (full history + per-rev detail) — same depth pattern the rev-102/103 blog cluster reached. No auth — public marketing surface. Cache-control public, max-age=300, s-maxage=1800. Closes the load-bearing v1 parity gap on the rev-169 dashboard primitive (rev 174).","GET /api/v1/blog?limit=20&tag=…&sinceDate=ISO&q=…":"Public blog posts (newest-first). No auth — public marketing surface. Returns slug + title + date + excerpt + tags + author + readTime + url. Optional tag filter (case-insensitive substring match) and sinceDate filter (ISO date) for cadence polling. As of rev 122 also accepts an optional `q` keyword query (≤200 chars) that searches across title, excerpt, and tags — mirrors the rev-102 dashboard `BlogSearch` matcher on the protocol-bound surface so MCP hosts and AI tooling roundup newsletters can retrieve posts by keyword without re-implementing the filter client-side. Pairs with /roadmap-items + /roadmap-votes + /changelog as the four-axis public marketing surface — items return what's planned, votes return what's most-requested, changelog returns what shipped, blog returns brand voice. Cache-control public, max-age=300, s-maxage=1800 (rev 102; rev 122 added q keyword search).","GET /api/v1/blog/{slug}":"Single blog post detail. Returns the full HTML body + tags + read-time + word-count + canonical URL plus 3 related posts via tag overlap so MCP hosts can render 'you might also like' without a follow-up call. No auth — public marketing surface. Pairs with /api/v1/blog (listing) as the two-axis blog read surface on the protocol-bound side. Cache-control public, max-age=300, s-maxage=1800 (rev 103).","GET /api/v1/blog/categories":"Public blog categories with descriptions, accent colours, and post counts. No auth — public marketing surface. Closes the v1 blog cluster's category axis after rev-102 listing + rev-103 detail so the cluster is now three axes deep (listing + detail + categories). MCP hosts rendering 'Loop Desk's blog by category' get a typed contract instead of scraping the SSR'd category sections. Cache-control public, max-age=300, s-maxage=1800 (rev 104).","GET /api/v1/blog/authors":"Public blog authors with post counts + latest-post date. No auth — public marketing surface. As of rev 106 each row also carries a `profile` block (tagline, bio, avatarUrl, homepageUrl, links) when the author has a registered profile in data/blog-authors.json — null otherwise so callers get a typed shape. Closes the v1 blog cluster's fourth axis after rev-102 listing + rev-103 detail + rev-104 categories. Pairs with the rev-105 per-author archive page (/blog/author/[slug]) and per-author RSS (/blog/author/[slug]/rss.xml). Cache-control public, max-age=300, s-maxage=1800 (rev 105/106).","GET /api/v1/blog/by-author/{slug}?limit=20":"Posts written by a specific author, newest-first. Returns 404 when the slug doesn't match any current author. Mirrors the rev-105 per-author archive page on the v1 surface so MCP hosts rendering 'posts by Steve' or AI tooling newsletters writing per-byline weekly roundups don't have to fetch /api/v1/blog and filter client-side. As of rev 106 the response also carries a `profile` block (tagline, bio, avatarUrl, homepageUrl, links) when the author has a registered profile in data/blog-authors.json — null otherwise so callers get a typed shape regardless. Cache-control public, max-age=300, s-maxage=1800 (rev 105/106).","GET /api/v1/blog/related/{slug}?limit=5":"Related posts for a given slug via tag overlap — closes the fifth axis on the v1 blog cluster (listing + detail + categories + authors + related). Returns the source post identity + author profile avatar + the related set with per-result `sharedTagCount` so callers rendering 'you might also like' widgets can show 'shared 3 tags' hints without re-implementing the rev-102 scoring. Returns 404 when the source slug doesn't match any post. Cache-control public, max-age=300, s-maxage=1800 (rev 106).","GET /api/v1/blog/{slug}/neighbors":"Chronological prev/next neighbours for a post — returns { newer, older } walking the publication timeline. Distinct from /related/{slug} (which ranks by tag-overlap content similarity); neighbours walks the timeline. Either side may be null when the source post is at the head/tail. Mirrors the rev-107 dashboard prev/next navigation primitive on the v1 surface in lockstep. 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. Cache-control public, max-age=300, s-maxage=1800 (rev 107).","GET /api/v1/blog/tags":"Public blog tag taxonomy with post counts + latest-post date. Tags collapsed by slug (so 'MCP' and 'mcp' resolve to one entry). No auth — public marketing surface. 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). Pairs with the rev-108 per-tag HTML archive page (/blog/tag/[slug]). Cache-control public, max-age=300, s-maxage=1800 (rev 108).","GET /api/v1/blog/by-tag/{slug}?limit=20":"Posts tagged with a specific tag, newest-first. Returns 404 when the slug doesn't match any current tag. Includes a `summary` block with totalWords + estimatedMinutes so callers rendering 'this is a 45-minute read across 8 posts' don't have to re-aggregate per-post wordCount. Mirrors the rev-105 per-author archive endpoint shape at the tag axis so MCP hosts rendering 'posts tagged MCP' or AI tooling roundup newsletters writing per-topic weekly digests don't have to fetch /api/v1/blog and filter client-side. Cache-control public, max-age=300, s-maxage=1800 (rev 108).","GET /api/v1/tasks/{id}/cost":"Per-task cumulative AI cost — { totalInputTokens, totalOutputTokens, estimatedCostUsd, sessionCount, costPerSessionUsd } (rev 51 — closes the rev-49 named cost-attribution follow-up)","GET /api/v1/tasks/{id}/cost-trajectory?days=7":"Per-task cost trajectory — trailing N daily cost buckets (1–30, default 7) in workspace timezone, oldest → newest, with zero-fill (rev 54 — closes the named rev-53 follow-up; the load-bearing primitive for 'is this task spiking cost or steady?')","GET /api/v1/tasks/top-cost?limit=5&includeDone=false":"Top open tasks by cumulative AI spend — each row now carries a `trajectory7d` cents array as of rev 54 (rev 51 + rev 54)","GET /api/v1/cost/by-assignee?limit=10&includeDone=false&trajectoryDays=7":"Per-assignee cost breakdown — aggregates per-task AI spend by assignedToUserId so MCP hosts can answer 'where is our cost going by teammate?'. As of rev 61, accepts optional `trajectoryDays` (1–30) so each row carries a `trajectory7d` cents array — mirrors the rev-60 by-source trajectory shape at the per-recipient axis. (rev 52 + rev 61)","GET /api/v1/cost/by-assignee/{assigneeId}/trajectory?days=7":"Per-assignee daily cost trajectory — trailing N daily cost buckets (1–30, default 7) in workspace timezone, oldest → newest. Use `__unassigned__` as the route segment to drill into the unassigned bucket. Closes the third trajectory axis on the cost cluster: workspace (rev 53) + per-task (rev 54) + per-source (rev 60) + per-assignee (rev 61).","GET /api/v1/cost/today":"Cost-only Today projection — today/yesterday/baseline7d cents, 7-day daily-spend sparkline, dailyCostCap + capPercent, plus byAssignee[] (rev 53 — pairs with rev-52 by-assignee + rev-51 top-cost as the MCP cost-axis cluster)","GET /api/v1/cost/by-source?limit=10&includeDone=false":"Per-source cost breakdown — aggregates per-task AI spend by the originating signal source so MCP hosts can answer 'which source is driving the most AI spend?' (rev 57 — closes the upstream axis on the cost-observability cluster; pairs with rev-51 per-task + rev-52 per-assignee + rev-53 today + rev-13 per-cycle for the five-axis MCP cost surface)","GET /api/v1/tasks/cost-spikes?limit=10":"Per-task cost spike list — open tasks today >= 2× their trailing 7-day daily average AND >= $0.50 absolute. Pairs with the rev-32 workspace-level cost spike at the per-task axis using the rev-54 dailyCostHistory primitive (rev 55 — closes the named rev-54 next-sprint candidate)","GET /api/v1/sources/cost-spikes?limit=10":"Per-source cost spike list — live sources today >= 2× their trailing 7-day daily average AND >= $0.50 absolute. Closes the alarm axis to match the rev-57 per-source attribution axis. Pairs with rev-32 (workspace) + rev-55 (per-task) as the three-axis alarm cluster on the protocol-bound surface (rev 58)","GET /api/v1/sources/cadence-overview":"Per-source poll cadence + next-poll estimate aggregate. Returns workspace cadence (rev 140), plan floor, and per-source breakdown (intervalMinutes, isOverride, lastSyncedAt, nextPollAt, isDueNow, minutesUntilDue) sorted soonest-due first. Mirrors the rev-141 per-source cadence override + rev-5 RSS poller gating logic so MCP hosts can answer 'when will source X poll next?' without computing it client-side (rev 142 — closes the named rev-141 next-sprint candidate at the protocol surface)","GET /api/v1/sources/quietness":"Per-source quietness / staleness aggregate. Returns workspace cadence floor + per-source breakdown of feeds that polled successfully but produced no signals beyond the 14-day staleness floor (or 2× the per-source cadence interval, whichever is greater, capped at 90d). Distinct from rev-16 loud failure (lastErrorMessage), rev-58 cost-spike, and rev-12 7-day signal counter. Quietness is the *prescriptive* answer to 'this feed has been silent long enough that the operator should consider whether it's still load-bearing.' Each row carries `quietnessAckedAt` (rev 145) so callers can render muted-state. Pairs with /sources (rev 13 + rev 142 pollGating) + /sources/cadence-overview as the third axis on the per-source health observability cluster (rev 144 — closes the named rev-143 next-sprint candidate at the protocol surface)","POST /api/v1/sources/{sourceId}/quietness-ack":"Acknowledge a per-source quietness warning for 7 days. Distinct from rev-6 pause (permanent until resumed), rev-26 keyword filters (per-item gating), and rev-72 chronic-ack (cost-spike axis). Quietness ack = 'I see this feed is silent, intentionally, mute the alarm for 7 days.' Suppresses the rev-144 🌙 chip badge state, the rev-145 daily Slack push, and the rev-145 outbound `source.quietness_warning` event for 7 days unless the source produces fresh signal. Mirrors the rev-72 chronic-ack TTL exactly so the two structural-axis acks share one vocabulary (rev 145 — closes the named rev-144 next-sprint candidate at the operator counter-action surface)","POST /api/v1/sources/quietness-ack/bulk":"Bulk acknowledge per-source quietness warnings for 7 days — JSON: { sourceIds: string[≤50] }. Mirrors the rev-145 single-source quietness ack at the bulk shape so an operator triaging multiple dormant feeds in the rev-146 daily digest email can mute the entire cluster in one call. Fires one source.quietness_acked outbound event per acked source so downstream integrations see the same per-item shape they get from a single ack call. (rev 146 — closes the named rev-145 next-sprint candidate at the bulk-action axis on the structural-quietness surface)","POST /api/v1/sources/{sourceId}/quietness-unack":"Revoke a per-source quietness ack before its rev-145 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) and want to *un-mute* the alarm rather than wait the 7d window out. Fires the new rev-148 source.quietness_unacked outbound event so downstream integrations that closed their alarm-open ticket on the rev-145 ack closure can re-open it on un-ack. Mirrors the rev-145 ack endpoint at the un-mute axis (rev 148 — closes the un-mute gap on the rev-145 ack closure; full per-source quietness lifecycle is now reachable end-to-end through MCP/v1 across visibility + ack + bulk-ack + un-ack)","POST /api/v1/sources/quietness-unack/bulk":"Bulk revoke per-source quietness acks — JSON: { sourceIds: string[≤50] }. Closes the un-mute symmetry on the rev-146 bulk-ack pattern. When an operator who bulk-muted N feeds via rev-146 realises one or more are actually critical, they can reverse the whole batch in one call. Already-clean sources (no ack stamp) are silent no-ops at the SQL layer; one source.quietness_unacked outbound event fires per actually-revoked source so downstream integrations see the same per-item shape they get from the single un-ack call. (rev 149 — closes the inline-vs-batch un-mute symmetry on the structural-quietness surface)","PATCH /api/v1/sources/{sourceId}/pause-until":"Schedule a per-source temporary pause with auto-resume — JSON: { pausedUntilAt: ISO | null }. ISO must be in the future and within the 14-day max window. Null clears the schedule and resumes a currently scheduled-paused source. Mirrors the rev-21 PATCH /api/v1/workspace/pause-until at the per-source axis. Operators muting a noisy LinkedIn bridge during a launch week or pausing a partner source during a holiday window now have a deterministic 'auto-resume on Friday 9am' primitive distinct from rev-6 manual pause (no schedule), rev-141 cadence override (still polls, just slower), and rev-145 quietness ack (separate axis). The auto-resume sweep flips the source back to `connected` on schedule expiry and writes an activity-log entry. (rev 150 — closes the long-outstanding gap on the source-management primitive cluster, diversifying away from the 6-rev quietness cluster (rev 144-149))","GET /api/v1/sources/scheduled-paused":"Aggregate read of all currently scheduled-paused sources — returns sources whose `pausedUntilAt` is set AND still in the future, sorted by resume-soonest first. Each row carries `sourceId`, `label`, `type`, `pausedUntilAt` ISO, and `hoursUntilResume` for at-a-glance rendering. Distinct from /sources/quietness (structural-alarm axis — feeds polled but produced no signal) and /sources/cost-spikes (cost-spike axis). Scheduled-pause is the operator-intent axis. Pairs with the rev-150 PATCH /sources/{id}/pause-until for the full read + write lifecycle on the protocol-bound surface; powers the rev-151 TodayPanel ⏰ chip + the rev-151 daily-digest pause-until section in lockstep. (rev 151 — closes the named rev-150 next-sprint candidate at the today-glance + protocol-bound + email-channel axes; 73rd unbroken cadence rev)","POST /api/v1/tasks/{id}/cost-spike-ack":"Acknowledge a per-task cost spike for the rest of today (workspace TZ). Suppresses the rev-55 ⚡ pill, daily Slack push, digest section, and rev-56 auto-pause filter for the acked task today. Distinct from rev-23 pin (permanent exempt) and rev-50 renew (resets staleness). Ack = 'I see this spike, stop alarming today.' (rev 56 — closes the named rev-55 follow-up)","POST /api/v1/tasks/cost-spike-ack/bulk":"Bulk acknowledge cost spikes — JSON: { taskIds: string[≤50] }. Mirrors the rev-57 dashboard bulk-ack surface so MCP hosts can clear several spikes in one call. (rev 57)","GET /api/v1/workspace/cost-spike-config":"Read the workspace's per-task cost-spike action — 'none' (rev-55 alarm-only) or 'pause' (rev-56 auto-pause: spiking tasks de-prioritise until acknowledged)","PUT /api/v1/workspace/cost-spike-config":"Set the action — JSON: { taskCostSpikeAction: 'none' | 'pause' | null } (rev 56 — closes the descriptive→defensive loop on the per-task cost story; mirrors rev-49 stale auto-archive opt-in pattern at the cost axis)","POST /api/v1/sources/{id}/cost-spike-ack":"Acknowledge a per-source cost spike for the rest of today (workspace TZ). Suppresses the rev-58 ⚡ pill, daily Slack push, outbound source.cost_spike event, and rev-59 auto-pause filter for the acked source today. Ack = 'I see this source spike, stop alarming today, but watch for tomorrow.' (rev 59 — closes the named rev-58 follow-up at the operator counter-action surface)","POST /api/v1/sources/cost-spike-ack/bulk":"Bulk acknowledge per-source cost spikes — JSON: { sourceIds: string[≤50] }. Mirrors the rev-57 task bulk-ack surface at the per-source axis so MCP hosts can clear several feeds in one call. (rev 60)","GET /api/v1/sources/{id}/cost-trajectory?days=7":"Per-source daily cost trajectory — trailing N daily cost buckets (1–30, default 7) in workspace timezone, oldest → newest. Mirrors rev-54 task trajectory at the per-source axis. (rev 60 — answers 'is this source steadily expensive or just spiking today?')","GET /api/v1/workspace/source-cost-spike-config":"Read the workspace's per-source cost-spike action — 'none' (rev-58 alarm-only) or 'pause' (rev-59 auto-pause: spiking feeds skip the RSS poll until acknowledged)","PUT /api/v1/workspace/source-cost-spike-config":"Set the action — JSON: { sourceCostSpikeAction: 'none' | 'pause' | null } (rev 59 — closes the descriptive→defensive loop on the per-source cost story; mirrors rev-56 per-task auto-pause at the per-source axis)","GET /api/v1/cost/by-assignee/spikes?limit=10":"Per-assignee cost spike list — open 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). Aggregates the rev-54 task.dailyCostHistory by `task.assignedToUserId`. (rev 62)","POST /api/v1/cost/by-assignee/{assigneeId}/cost-spike-ack":"Acknowledge a per-assignee cost spike for the rest of today (workspace TZ). Suppresses the rev-62 ⚡ pill, daily Slack push, outbound assignee.cost_spike event, and new rev-63 digest section for the acked assignee today. Mirrors rev-56 task ack + rev-59 source ack at the per-recipient axis. Ack = 'I see this teammate's queue spike, stop alarming today.' (rev 63 — closes the rev-62 alarm-only gap on the operator counter-action surface)","POST /api/v1/cost/by-assignee/cost-spike-ack/bulk":"Bulk acknowledge per-assignee cost spikes — JSON: { assigneeUserIds: string[≤50] }. Mirrors the rev-57 task bulk-ack + rev-60 source bulk-ack surface at the per-recipient axis. (rev 63)","GET /api/v1/workspace/source-chronic-spike-config":"Read the workspace's per-source chronic-spike action — 'none' (rev-61 nudge-only — pill + recommendation banner) or 'pause' (rev-62 auto-pause: feeds spiking N+ days in a row are auto-paused). Includes thresholdDays (2-14, default 3).","PUT /api/v1/workspace/source-chronic-spike-config":"Set the action — JSON: { sourceChronicSpikeAction: 'none' | 'pause' | null, thresholdDays?: 2-14 }. Closes the named rev-61 next-sprint candidate at the workspace-config surface. Mirrors rev-49 stale-task auto-archive opt-in pattern at the per-source axis on the defensive side. (rev 62)","GET /api/v1/workspace/cost-export?format=csv|json":"Cost summary takeaway — same procurement-friendly artefact as the rev-60 dashboard endpoint with daily trailing-30-day spend + top-cost tasks + cost-by-source + cost-by-assignee, all in one payload in workspace timezone. Default CSV download (four-section). Pass format=json for a structured JSON object grouped by axis (`{ daily, byTask, bySource, byAssignee }`) so MCP hosts and FinOps pipelines can render the cost summary without parsing the multi-section CSV. (rev 62 — closes the v1 parity gap on the cost CSV; rev 121 added JSON variant)","GET /api/v1/cost/by-tag?limit=10&includeDone=false&trajectoryDays=7":"Per-tag cost breakdown — aggregates per-task AI spend by tags so MCP hosts 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). As of rev 67, accepts optional `trajectoryDays` (1–30) so each row carries a `trajectory7d` cents array — mirrors the rev-60 by-source + rev-61 by-assignee shape at the per-tag axis. (rev 66 + rev 67)","GET /api/v1/cost/by-tag/{tag}/trajectory?days=7":"Per-tag daily cost trajectory — trailing N daily cost buckets (1–30, default 7) in workspace timezone, oldest → newest, with zero-fill. Pass `untagged` as the route segment to drill into the synthetic untagged bucket. 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).","GET /api/v1/cost/by-tag/spikes?limit=10":"Per-tag cost spike list — live tags whose today spend is >= 2× their trailing 7-day daily average AND >= $0.50 absolute. Aggregates `task.dailyCostHistory` per-tag (each task contributes its full spend to every tag it carries). The synthetic `untagged` bucket is excluded — flagging 'untagged work is spiking' isn't actionable. As of rev 70, every row carries `consecutiveSpikeDays` so MCP hosts can render the same 'Nd in a row' affordance the dashboard surfaces on the rev-66 panel. 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).","GET /api/v1/cost/by-tag/chronic-spikes?limit=10":"Per-tag chronic-spike list — workstream tags that have been spiking for 3+ consecutive days running. Distinct from the rev-67 daily-spike endpoint (which names today's anomaly) — chronic names a structural problem (workstream over-budget, noisy source attached to the tag, stale focus tag). Pairs with the companion `tag.chronic_warning` outbound webhook so external integrations can route the chronic alarm to a project tracker grouped by initiative or a FinOps dashboard sliced by workstream without polling. (rev 70 — closes the chronic axis on the per-tag cost story alongside rev-67/68/69's daily-spike axis; mirrors rev-61 source counter + rev-64 assignee counter at the per-workstream axis)","POST /api/v1/cost/by-tag/{tag}/cost-spike-ack":"Acknowledge a per-tag cost spike for the rest of today (workspace TZ). Suppresses the rev-67 ⚡ pill, daily Slack push, outbound tag.cost_spike event, and new rev-68 digest section for the acked tag today. Mirrors rev-56 task ack / rev-59 source ack / rev-63 assignee ack at the per-tag axis. Ack = 'I see this workstream's spike, stop alarming today, but watch for tomorrow.' Distinct from rev-29 focus tags (which BIAS attention toward the tag). (rev 68 — closes the named rev-67 next-sprint candidate at the operator counter-action surface)","POST /api/v1/cost/by-tag/cost-spike-ack/bulk":"Bulk acknowledge per-tag cost spikes — JSON: { tags: string[≤50] }. Mirrors the rev-57 task bulk-ack + rev-60 source bulk-ack + rev-63 assignee bulk-ack surface at the per-tag axis. The cost-axis MCP cluster now closes the detect → triage → ack loop on every axis (rev 68).","POST /api/v1/cost/by-tag/{tag}/chronic-ack":"Acknowledge a per-tag chronic-warning for 7 days. Distinct from the rev-68 daily ack (which mutes today's rev-67 alarm) — the rev-70 chronic warning fires when a workstream's `tagConsecutiveSpikeDays` crosses 3d (a structural problem the operator may be intentionally letting run). Different surface, different TTL (7d vs same-day), different intent. (rev 71 — closes the named rev-70 next-sprint candidate at the chronic-axis operator counter-action surface)","GET /api/v1/cost/by-source/chronic-spikes?limit=10":"Per-source chronic-spike list — sources whose `consecutiveSpikeDays` has crossed 3d. Each row carries today/baseline/spikeRatio when the source is also currently in the rev-58 daily detector (chronic state can persist a day after today's spend cooled). Mirrors the rev-70 per-tag chronic endpoint at the per-source axis. (rev 71 — closes the named rev-70 next-sprint candidate)","GET /api/v1/cost/by-assignee/chronic-spikes?limit=10":"Per-assignee chronic-spike list — workspace members whose `consecutiveSpikeDays` has crossed 3d. Same shape as the per-source endpoint at the per-recipient axis. (rev 71 — closes the chronic-axis v1 symmetry across all three dimensions where chronic makes sense: per-source + per-assignee + per-tag)","POST /api/v1/sources/{sourceId}/chronic-ack":"Acknowledge a per-source chronic-warning for 7 days. Distinct from the rev-59 daily ack (which mutes today's rev-58 alarm) — the rev-61 chronic banner + rev-62 chronic auto-pause fire when a source has been spiking 3+ days running (a structural problem the operator may be intentionally letting run). Different surface, different TTL (7d vs same-day), different intent. (rev 72 — closes the named rev-71 next-sprint candidate at the per-source chronic-axis operator counter-action surface)","POST /api/v1/cost/by-assignee/{assigneeId}/chronic-ack":"Acknowledge a per-assignee chronic-warning for 7 days. Distinct from the rev-63 daily ack — the rev-64 chronic warning Slack push + outbound assignee.chronic_warning event fire when a teammate's queue has been spiking 3+ days running (a structural problem the operator may be intentionally letting run). Mirrors the rev-71 per-tag + rev-72 per-source chronic-ack pattern at the per-recipient axis — closes the chronic-ack symmetry across all three dimensions where chronic makes sense. (rev 72)","GET /api/v1/cost/chronic-acks":"Currently-active chronic-warning acks across all three axes — per-tag, per-source, per-assignee — filtered to the rev-71 7-day TTL. Stale stamps are excluded since they no longer suppress the chronic surface. Lets MCP hosts answer 'what defensive operator actions are in effect right now?' with one bearer-auth call. (rev 73 — closes the named rev-72 next-sprint follow-up at the read surface)","GET /api/v1/workspace/tag-cost-spike-config":"Read the workspace's per-tag cost-spike action — 'none' (rev-67/68 alarm + ack only) or 'pause' (rev-69 auto-pause: tasks tagged with a currently spiking workstream skip the queue until acknowledged) (rev 69)","PUT /api/v1/workspace/tag-cost-spike-config":"Set the action — JSON: { tagCostSpikeAction: 'none' | 'pause' | null } (rev 69 — closes the descriptive→defensive loop on the per-tag cost story; mirrors rev-56 per-task + rev-59 per-source auto-pause at the per-tag axis)","GET /api/v1/workspace/task-templates":"List persistent workspace task templates — workspace-scoped recurring shapes the operator has saved alongside the rev-64 static set + rev-65 detected pattern (rev 66 — closes the named rev-65 next-sprint candidate)","POST /api/v1/workspace/task-templates":"Save a persistent task template — JSON: { label, emoji?, hint?, title, summary, goal?, kind, deliverableType, priority?, tags?, asUserId? } (capped at 20 templates per workspace; unique by label). Defaults asUserId to workspace owner.","PATCH /api/v1/workspace/task-templates/{id}":"Update a persistent template — partial JSON of the same shape as POST","DELETE /api/v1/workspace/task-templates/{id}":"Remove a persistent template","POST /api/v1/workspace/task-templates/{id}":"Record-usage — bumps usageCount + lastUsedAt so the recent-first ordering reflects programmatic apply (idempotent counter)","GET /api/v1/workspace/whats-new?since=ISO":"What landed since timestamp X — per-kind counts (signals/artifacts/outputs/approvals/activity) + small samples. Defaults to last 24h; clamped to 30-day max lookback. (rev 77 — bearer-auth mirror of the rev-76 dashboard temporal-delta surface)","GET /api/v1/workspace/personal-inbox?userId=…":"Per-user actionable inbox — unacked self-mentions + per-recipient stale assigned tasks + due-soon-and-mine. Defaults userId to workspace owner. (rev 77 — mirrors the rev-77 dashboard PersonalInboxPill)","GET /api/v1/openapi.json":"Machine-readable OpenAPI 3.1 spec describing the v1 surface — types every parameter and response shape so MCP hosts and code generators don't have to parse string descriptions. Public (no auth required to read the spec; the spec itself describes the auth model). Curated subset of the most load-bearing endpoints; full coverage tracks rev-by-rev. (rev 78 — opens the v1 surface to typed code generation; rev 79 expands coverage to the cost-axis cluster + chronic-ack endpoints + dashboard-prefs)","GET /api/v1/workspace/dashboard-prefs?userId=…":"Per-user-per-workspace dashboard preferences — collapsedPanels (rev 78), digestPersonalSections (rev 79 per-recipient digest opt-out), collapsedActivityBuckets (rev 79 multi-device sync of the rev-78 activity-log time-bucket UI). Defaults userId to workspace owner. (rev 79 — closes the v1 parity gap on the rev-78 dashboard prefs primitive)","PUT /api/v1/workspace/dashboard-prefs?userId=…":"Set per-user dashboard preferences — JSON: { collapsedPanels?, digestPersonalSections?, collapsedActivityBuckets?, digestQuietWeekends?, digestQuietHoursStart?, digestQuietHoursEnd?, panelOrder?, activeWorkSort?, costPanelColumns?, costPanelOrder?, costPanelOrderUpdatedAt?, taskCommentFilters?, density?, densityUpdatedAt? }. Partial update — only fields present in the body are written. Response carries `{ prefs, rejected }` where `rejected` enumerates every entry the OCC step silently dropped: rejected.taskCommentFilters[] (rev 137), rejected.costPanelOrder[] (rev 138 per-axis), rejected.density (rev 139 single-field, nullable). (rev 79; rev 80 added digestQuietWeekends; rev 81 added digestQuietHours; rev 82 added panelOrder; rev 83 added activeWorkSort; rev 84 added costPanelColumns; rev 127 added costPanelOrder; rev 135 added taskCommentFilters; rev 136 added density; rev 137 added taskCommentFilters OCC + rejection trail; rev 138 added costPanelOrderUpdatedAt OCC; rev 139 added densityUpdatedAt OCC + closes the OCC symmetry across all three multi-device-synced primitives)","DELETE /api/v1/workspace/dashboard-prefs?userId=…":"Reset per-user dashboard preferences to defaults — clears every rev-77/79/82/83/84 field so all panels show, panels are in default order, active-work sort is smart, and all cost-panel columns are visible. Useful when an operator accidentally hides too many panels. (rev 84 — pairs with the rev-78 multi-device sync as the 'back to defaults' affordance)","GET /api/v1/workspace/reactions-summary?topLimit=8":"Aggregated reactions summary across all four reactable surfaces (comments rev 29 + artifacts rev 33 + memory rev 33 + signals rev 34). Returns per-emoji workspace-wide totals + top-N reacted entities with parent context. Pure derived state; no schema. (rev 80 — closes the workspace-wide reaction-aggregation gap; until rev 80 reactions were entity-scoped only)","GET /api/v1/workspace/members":"List workspace members + pending invites (rev 81 — closes the longest-outstanding v1 parity gap on the rev-15 invite flow, 66 revs old)","POST /api/v1/workspace/invites":"Invite a teammate by email — JSON: { email, role?: 'admin'|'editor'|'viewer', asUserId? }. asUserId defaults to workspace owner. (rev 81)","DELETE /api/v1/workspace/members/{userId}":"Remove a member — body: { asUserId? }. Workspace owners cannot be removed. (rev 81)","DELETE /api/v1/workspace/invites/{inviteId}":"Revoke a pending invite. (rev 81)","GET /api/v1/outbound/subscriptions":"List per-event outbound webhook subscriptions + the full ALL_OUTBOUND_EVENTS vocabulary (rev 81 — closes the rev-19 v1 parity gap on the subscription-management surface; pairs with rev-46 deliveries + retry as the full outbound observability + control surface on the protocol-bound side)","POST /api/v1/outbound/subscriptions":"Create a subscription — JSON: { url, events: OutboundEvent[], label? } (rev 81)","PATCH /api/v1/outbound/subscriptions/{id}":"Update a subscription — JSON: { active?, events? } for pause/resume + event-set editing (rev 81)","DELETE /api/v1/outbound/subscriptions/{id}":"Remove a subscription (rev 81)","POST /api/v1/outbound/subscriptions/{id}/test":"Send an artifact.test payload to a specific subscription URL with HMAC signing — verifies wiring per-subscription (rev 83 — closes the per-subscription test gap left by the rev-17 single-URL test)","POST /api/v1/workspace/digest-preview":"Send a one-shot digest preview to a workspace member — JSON: { userId? }. Defaults to workspace owner. Recipient must be an owner/admin. Bypasses the per-workspace 22h cron interval gate so MCP hosts can iterate on configuration repeatedly without waiting for the next 13:15 UTC tick. Honours the recipient's rev-78/79/80/81 dashboardPrefs (digest opt-out / weekend mute / quiet hours window) exactly as the production cron would. (rev 86 — closes the dashboard-only digest-test gap from rev 36)","GET /api/v1/workspace/digest-preview?userId=&format=html|json&simulatedDate=ISO":"Dry-run companion to the rev-86 POST endpoint. Renders the same HTML the POST endpoint would send, but doesn't actually send the email and doesn't write the activity-log entry. The Resend precondition is also skipped so workspaces without RESEND_API_KEY/EMAIL_FROM can still verify the rendered shape. format=html returns the raw HTML body; format=json (default) wraps the HTML inline with recipient + subject + rev-88 gating block. simulatedDate (rev 88, ±60d) overrides 'now' for the rev-80 weekend-mute / rev-81 quiet-hours-window gating decisions so admins can answer 'would this recipient receive the digest next Saturday?' without waiting for that day to arrive. (rev 87 + rev 88 — closes the named rev-87 next-sprint candidate)","POST /api/v1/cost/by-tag/chronic-ack/bulk":"Bulk acknowledge per-tag chronic warnings for 7 days — JSON: { tags: string[≤50] }. Distinct from /cost/by-tag/cost-spike-ack/bulk (rev 68 — daily horizon, same-day mute). Mirrors the rev-71 single-tag chronic ack at the bulk shape. (rev 87 — closes the rev-86 chronic-axis bulk-action symmetry at the per-tag dimension)","POST /api/v1/sources/chronic-ack/bulk":"Bulk acknowledge per-source chronic warnings for 7 days — JSON: { sourceIds: string[≤50] }. Distinct from /sources/cost-spike-ack/bulk (rev 60 — daily horizon, same-day mute). Mirrors the rev-72 single-source chronic ack at the bulk shape. (rev 87 — closes the rev-86 chronic-axis bulk-action symmetry at the per-source dimension)","POST /api/v1/cost/by-assignee/chronic-ack/bulk":"Bulk acknowledge per-assignee chronic warnings for 7 days — JSON: { assigneeUserIds: string[≤50] }. Distinct from /cost/by-assignee/cost-spike-ack/bulk (rev 63 — daily horizon, same-day mute). Mirrors the rev-72 single-assignee chronic ack at the bulk shape. (rev 87 — closes the rev-86 chronic-axis bulk-action symmetry at the per-recipient dimension; closes the chronic-axis bulk-action loop on every dimension where chronic makes sense)","GET /api/v1/workspace/digest-recipients-gating?simulatedDate=ISO":"Multi-recipient gating preview. Returns the rev-88 single-recipient gating decision across every owner/admin recipient in one call so admins can answer 'who would actually receive the digest right now (or at this simulated instant)?' without enumerating the rev-86 single-recipient preview endpoint per recipient. Pure read-only — no email sends, no activity-log writes, no dashboardPrefs mutations. Reuses the same rev-15 timezone math + rev-80 weekend-mute + rev-81 quiet-hours gating the production cron applies. Optional simulatedDate (±60d) overrides the gating instant. (rev 89 — closes the named rev-88 next-sprint candidate)","GET /api/v1/workspace/digest-config":"Aggregate digest configuration snapshot — workspace-level digestEmail + IANA timezone + per-recipient dashboardPrefs (digestPersonalSections / digestQuietWeekends / digestQuietHoursStart-End) for every owner/admin recipient. Closes the procurement-friendly read surface for the rev-80/81/89 digest gating cluster — three round trips (workspace + members + per-recipient prefs) collapse to one bearer-auth call. Pure read-only. (rev 90)","GET /api/v1/workspace/digest-audit?sinceDays=30&limit=100":"Historical audit log of every digest gating change for the workspace — reads the rev-90 `digest_gating_change` activity-log entries that fire whenever a recipient's would-send outcome flips. Each entry is structured: id, createdAt, raw detail, parsed outcome (would_send | muted), and recipient label. Bounded query window (default 30 days, max 365). Pairs with rev-90 digest-config (current snapshot) + rev-89 digest-recipients-gating (current-instant outcome) + rev-86 digest-preview (render-only test) as the four-axis digest-config instrument cluster on the protocol-bound surface. (rev 92 — closes the named rev-91 next-sprint candidate)","POST /api/v1/tasks/{id}/share-work-log":"Mint or revoke a per-task work-log share token — JSON: { enable: boolean }. Returns { ok, publicWorkLogToken | null }. Mirrors the rev-10 artifact share endpoint shape on the task entity. The token resolves through the public `/share/work-log/<token>` page which renders the rev-12 work log + rev-14 operator notes as a procurement-friendly evidence trail. Pairs with the rev-92 print stylesheet for the public takeaway PDF. (rev 93 — closes the named rev-92 next-sprint candidate; closes the procurement-evidence loop on AI cycle work)","GET /api/v1/workspace/shared-saved-searches":"List workspace-shared saved searches. Distinct from rev-18 personal localStorage saved searches — these are workspace-shared so multi-operator teams can curate a power-user search vocabulary that surfaces in every member's WorkspaceSearch dropdown. (rev 93)","POST /api/v1/workspace/shared-saved-searches":"Add a workspace-shared saved search — JSON: { name: string, query: string, asUserId? }. Defaults attribution to workspace owner if asUserId omitted (mirrors rev-77 personal-inbox + rev-30 reaction conventions). Capped at 30 entries per workspace. (rev 93 — closes the named rev-92 next-sprint candidate)","DELETE /api/v1/workspace/shared-saved-searches":"Remove a workspace-shared saved search — JSON: { searchId: string }. (rev 93)","POST /api/v1/outbound/subscriptions/bulk":"Bulk pause/resume/delete on outbound subscriptions — JSON: { action: 'pause' | 'resume' | 'delete', subscriptionIds: string[≤50] }. Closes the five-entity bulk-action symmetry across outputs (rev 6) + tasks (rev 26) + signals (rev 33) + memory (rev 34) + sources (rev 36) on the outbound subscription axis. (rev 93)","POST /api/v1/tasks/{id}/share-timeline":"Mint or revoke a per-task timeline share token — JSON: { enable: boolean }. Returns { ok, publicTimelineToken | null }. The rev-93 work-log share (`/share/work-log/<token>`) covers AI cycle output + operator notes only; this richer surface adds the rev-26 comments + the creation event so an external reader sees the complete narrative of how the team and the desk worked the task end-to-end. Distinct token + page from the rev-93 work-log share so operators can choose which surface to expose per task. (rev 94 — closes the named rev-93 next-sprint candidate)","GET /api/v1/tasks/{id}/work-log-views":"Trailing 14-day daily view counts for a task's public work-log share page (rev 93). Returns { days: { date, count }[] } oldest-first with zero-fill so the array length is stable for sparkline rendering. Mirrors the rev-13 `/api/artifacts/{id}/views` shape so engagement-tracking on the work-log share surface reads identically to artifact shares. (rev 94 — closes the named rev-93 next-sprint candidate)","GET /api/v1/workspace/activity-export?since=ISO&until=ISO&format=csv|json":"Workspace activity log as CSV (default) or JSON (`format=json`). Mirrors the rev-7 dashboard endpoint on the protocol-bound surface. Optional since/until ISO bounds scope the takeaway. Capped at 5000 rows (most-recent within the window). The JSON variant returns `{ rows, total, since, until, capped }` so MCP hosts can pipe audit history through analytics pipelines without parsing CSV. Closes the procurement-evidence v1 cadence pattern at the activity axis (rev 120; rev 121 added JSON variant — closes the named rev-120 next-sprint candidate).","GET /api/v1/workspace/artifacts-export?format=csv|json":"Workspace artifact catalog as CSV (default) or JSON (`format=json`). Mirrors the rev-22 dashboard endpoint on the protocol-bound surface. Capped at 5000 rows. JSON projects `tags` as an array (not `|`-joined) and `publicShareUrl` as a relative path. Closes the procurement-evidence v1 cadence pattern at the outputs axis (rev 120; rev 121 added JSON variant).","GET /api/v1/workspace/decisions-export?format=csv|json":"Decisions log as CSV (default) or JSON (`format=json`) — approved/archived non-brief artifacts scoped exactly to the rev-9 dashboard 'Decisions log' semantics. Mirrors the rev-47 dashboard endpoint on the protocol-bound surface. Distinct from the rev-9 `/api/v1/decisions` (last 10 in 30 days for the dashboard sidebar) — the export shape is the procurement takeaway across the full 5,000-row cap. (rev 120; rev 121 added JSON variant.)","GET /api/v1/workspace/stale-tasks-export?thresholdDays=N&format=csv|json":"Stale-task CSV (default) or JSON (`format=json`) — queued/in_progress tasks aged past `thresholdDays` (1-60, default 5) with rev-51 cost columns inline. Pinned tasks excluded. Mirrors the rev-50 dashboard endpoint on the protocol-bound surface. Capped at 5000 rows. 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), sources (rev 96) AND every axis can be requested in JSON or CSV — closes the v1 export JSON symmetry on the protocol-bound surface (rev 120; rev 121 added JSON variant).","GET /api/v1/workspace/memory-export?format=csv|json":"Workspace memory entries as CSV (default) or JSON (`format=json`) — pinned + importance-ranked durable knowledge (facts / decisions / preferences / lessons) with tags inline. Capped at 5000 rows. Closes the procurement-evidence cluster's seventh axis on the protocol-bound surface — the workspace's accumulated brand voice, lessons, and pinned facts now reach procurement reviewers as a takeaway artefact alongside activity / outputs / decisions / stale-tasks / cost / sources (rev 125 — mirrors the rev-125 dashboard endpoint)."},"notes":"Bearer token = workspace ingest token (same one used by /api/webhooks/signals/[token]). Find it in the Integrations panel of your dashboard."}