releases
AGENTS.md14 starsChanged 13 days agoApache-2.0View on GitHub
Changelog indexer and registry for AI agents and developers. The user-facing CLI lives in this monorepo at apps/cli/ (published as @buildinternet/releases); alongside it are the backend (API worker + D1), the remote MCP server, and the web frontend. Per-directory details: apps/README.md and packages/README.md. Running the four dev:* services behind portless, fresh-worktree bootstrap, worktree teardown, and local D1 schema parity (db:reset:local / db:pull) and auth rate-limit / signing-secret notes: local-development.md. Root package.json declares apps/api, apps/web, apps/webhooks, and packages/* as workspaces. apps/mcp/…
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository15 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- backfilling-sourcesSkill
- classify-media-relevanceSkill
- finding-changelogsSkill
- firecrawl-monitoringSkill
- generating-release-contentSkill
- grouping-releasesSkill
- local-ingestSkill
- maintaining-orgsSkill
- managing-sourcesSkill
- parsing-changelogsSkill
- regenerating-overviewsSkill
- security-auditSkill
- seeding-playbooksSkill
- creating-releases-jsonSkill
The file
We split this AGENTS.md into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 116
Search-query log: /v1/search and the MCP search tool write each query (≤200 chars) + mode/counts/duration to search_queries (web carries …
- Very specific: line 185
Do not edit .env directly.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Releases
Changelog indexer and registry for AI agents and developers. The user-facing CLI lives in this monorepo at apps/cli/ (published as @buildinternet/releases); alongside it are the backend (API worker + D1), the remote MCP server, and the web frontend.
Stack
- Runtime: Bun
- Language: TypeScript (strict mode)
- Database: Cloudflare D1 + Drizzle ORM. No local SQLite path in this repo —
bun:sqliteis only used for test fixtures (tests/db-helper.ts). - API: Cloudflare Worker with Hono (
apps/api/) - MCP: Remote MCP server (
apps/mcp/) - AI: Anthropic SDK (
@anthropic-ai/sdk); managed agents via@anthropic-ai/claude-agent-sdk
Layout
apps/
web/ Next.js frontend (Vercel)
cli/ the `releases` CLI (published as @buildinternet/releases)
api/ REST API (Cloudflare Worker, Hono + D1)
mcp/ remote MCP server (Cloudflare Worker)
webhooks/ webhook delivery queue consumer (Cloudflare Worker)
packages/ shared code: published @buildinternet/releases-* (incl. releases-lib) and private @releases/*
actions/ GitHub Actions (publish-changelog)
skills/ published agent skills
scripts/ operational + maintenance scripts
tests/ shared test helpers, fixtures, evals, cross-app tests (app tests live in apps/*/test)
docs/ architecture docs, runbooks, plansPer-directory details: apps/README.md and packages/README.md.
Commands
- Lint + format + type-check:
bun run check(oxlint withtypeCheckviaoxlint-tsgolint, thenoxfmt --check) - Lint only:
bun run lint(bun run typecheckis an alias) - Format:
bun run format:check - Tests:
bun run test(not part ofcheck; runs the root script below — a barebun testputs every suite in one process;bun test apps/cliis the last process in it) - Targeted
tsc:apps/mcpstill usesnpx tsc --noEmitin CI (carved-out workspace; excluded from root oxlint).apps/webandtests/have their own tsconfigs for local runs. - Evals (
tests/evals/) are manual and on-demand only. They call AI APIs, cost money, and take minutes.bun run eval:evaluationis the only in-repo suite (URL evaluation, ~30s). Parsing + discovery evals live with the CLI (apps/cli/).
Local development
Running the four dev:* services behind portless, fresh-worktree bootstrap, worktree teardown, and local D1 schema parity (db:reset:local / db:pull) and auth rate-limit / signing-secret notes: local-development.md.
Workspaces and carved-out packages
Root package.json declares apps/api, apps/web, apps/webhooks, and packages/* as workspaces. apps/mcp/ is intentionally excluded — it pins zod to the MCP SDK's nested copy, so it carries its own bun.lock and needs its own bun install.
The root test script is bun test packages/ && bun test tests/ apps/web/ apps/mcp apps/webhooks && bun test apps/api, so apps/api runs in its own bun test process, after the rest. This is deliberate isolation, not a style choice: bun's mock.module() is process-global, keyed by resolved module, and not restorable per-specifier (mock.restore() doesn't undo it, and re-mocking with the real impl is impossible — every import of the path resolves to the mock). The scrape-fetch tests that mock.module("@releases/adapters/cloudflare", …) at module scope (now in tests/adapters/) stub the same path apps/api's render-check tests import for real — an ordering-dependent flake if they ran in the same process. Splitting apps/api into a separate process makes the leak structurally impossible. Keep tests/adapters/ (and mcp/webhooks) in the root-cwd multi-dir invocation — that's where their module mocks resolve against the single root-workspace copy of @releases/adapters the source uses. Isolating apps/api (rather than tests/adapters/) is what keeps both sides resolving correctly.
Reach for interface injection before process isolation. Process isolation is the fix of last resort — it only works when the leak crosses a boundary the test script can split on. When a mock stubs a third-party module that sibling suites import, prefer passing a fake through the seam the code already has. packages/ai/src/aisdk-text-model.test.ts used to mock.module("ai", () => ({ generateText })), which dropped every other export and made overview-content.ts's import { Output, parsePartialJson } from "ai" fail with an unhandled SyntaxError between tests, ordering-dependent. Spreading the real module back into the factory does not rescue it: mocking ai at all forces a second evaluation of the module, so ai/test's MockLanguageModelV3 and the generateText it runs through resolve to different instances and the run hangs. The fix was to delete the module mock and drive the real generateText with a MockLanguageModelV3 — a plain argument, scoped to one test, invisible to every other file. Most SDKs ship such a seam (ai/test here); use it.
Shared code is split between published npm packages (@buildinternet/releases-*) and private in-tree packages (packages/):
packages/core/→ published as@buildinternet/releases-corefrom this monorepo. Pure, runtime-neutral helpers shared with the CLI: DB schema (source of truth),categories,dates,changelog-range,changelog-slice,overview,id,slug,tokens,cli-contracts,lookup-coordinate,fts(FTS5 query sanitizer — wraps tokens in phrase quotes soorg/repoand similar punctuation don't error). Consumed here and by the in-tree CLI viaworkspace:*, so schema changes and their CLI updates land in one PR.packages/core-internal/→ imported as@releases/core-internal. Private, workspace-only. DB-coupled / worker-only helpers the thin client doesn't need:release-upsert(drizzle upsert config),hash(node crypto), webhook surface (webhook-sign,webhook-resilience,webhook-url-safety,webhook-delivery,webhook-alert-format, plusrelease-eventwire types),schema-coverage(therelease_coveragetable, part of the drizzle composite schema).packages/api-types/→ published as@buildinternet/releases-api-typesfrom this monorepo. Wire protocol — request/response shapes served by the API worker, consumed by the MCP worker, web frontend, and the OSS CLI. Consumed here and by the in-tree CLI viaworkspace:*; wire changes and the CLI that adopts them ship together. Additive by default — renames/removals go through a one-minor-version deprecation alias before removal.- Publishing core / api-types / sdk: in-repo
exportspoint atsrc/*.ts;npm packrunsscripts/pack-dist.ts, which buildsdist/, adds.jsto relative imports, pinsworkspace:deps, and swapsexportsto it, so npm consumers get JS +.d.ts. Keep relative imports inpackages/coreextensionless (Turbopack won't map./x.jsto./x.tsthere); CI dry-run packs the packages. packages/adapters/— adapter primitives (types,source-meta,content-hash), thegithub,cloudflare,crawl, andfeedadapters, plus the shared scrape/agent fetch orchestration (scrape-fetch/extract-deps-worker/deterministic-update, persisting via an injected API fetcher — used by the API worker's update workflow, #1946). All pure / worker-safe.packages/ai/→ imported as@releases/ai-internal.evaluate(URL recommendation +buildMetadataFromEvaluation),playbook(deterministic markdown generation),providers(provider-detection table),release-content(Haiku 5.5 summarization fortitle_generated/title_short/summary— shared byscripts/generate-release-content.tsand the ingest-time hook),marketing-classifier(Haiku 5.5 binary verdict on whether a feed item is real product news vs. marketing — used byfetchOnewhenmetadata.marketingFilteris set). Worker-safe; caller passes the Anthropic client.packages/rendering/→ imported as@releases/rendering/*. Atom feed helpers, markdown/JSON formatters, and media URL helpers.packages/search/→ imported as@releases/search/*. Embedding providers/cache, Vectorize hybrid search, and release/entity/changelog embedding pipelines.packages/lib/— slim private utilities (config,errors,source-edit, Anthropic client/error helpers, managed-agent rate limits,anthropic-pricingfor list-price cost estimates on managed-agent sessions,spend-capdaily-spend KV gate,session-error-classify).loggeris published as@buildinternet/releases-lib/logger.packages/releases-lib/→ published as@buildinternet/releases-lib(CLI helpers:config,legacy-env,logger), part of the CLI's fixed release group. A workspace here; the CLI and@releases/*packages consume it viaworkspace:*.
Onboarding is agent-driven, not a remote harness: a local Claude Code session runs the local-ingest skill, self-serve listing, or on-demand /v1/lookups to add orgs/products/sources by hand. See agents.md.
The release_coverage schema lives with the rest of the DB-coupled internals in packages/core-internal/src/schema-coverage.ts (imported as @releases/core-internal/schema-coverage), not at the repo root.
Conventions
Keep entries to one line: the rule + a pointer to the doc that owns the detail. When a feature needs a paragraph, that paragraph belongs in
docs/architecture/, not here. This section has bloated twice from append-on-ship; resist it.
- This repo is public — keep PII out of committed content. No absolute home-dir paths (
/Users/<name>/…— write~/…or repo-relative), no personal email addresses (use@example.comin fixtures/docs), no customer data or real tokens, in any committed file — including generated plans and specs underdocs/plans/anddocs/superpowers/. - Logging splits by runtime: worker code (
apps/api,apps/mcp,apps/webhooks) MUST log vialogEvent()from@releases/lib/log-event(worker-safe structured JSON); CLI + runtime-neutral packages use@buildinternet/releases-lib/logger(stderr +~/.releases/logs/). Never import thefs-backed@buildinternet/releases-lib/loggerinto a worker. Payload conventions, severity, andErrorunwrapping: logging.md. - Source types (fetch adapters):
github,scrape,feed,agent,appstore. Adapter behavior +appstorematerialization: ingest.md. - Ingest pipeline (fetch → parse → insert) — dedup (
UNIQUE(source_id,url)+RELEASE_URL_UPSERT+ same-day body hash), smart-fetch backoff, URL exclusion (ignored_urlsorg-scoped /blocked_urlsglobal, viaisUrlExcluded()), release suppression (suppressed=1), and the ingest-time Haiku 5.5 passes (content summarization, the per-source marketing classifier viametadata.marketingFilter, feed-content enrichment viaFEED_ENRICH_ENABLED): ingest.md. Cron/Workflow orchestration: remote-mode.md. - GitHub Action ingest (#2291):
actions/publish-changelogdiffs changelog markdown on push and POSTsupsert-contentto the existing/releases/batchroute (no parallel write path). Planning is@buildinternet/releases-core/changelog-publishso the Action andreleases publishshare one parser (#2377). Product docs: Publish from GitHub Actions, Publish from any CI. See ingest.md → GitHub Action ingest. - Push-fed sources (#2374, #2381):
metadata.ingestMode = "push"marks a source fed directly by its publisher (e.g.actions/publish-changelog) — mirrorsfirecrawl.enabledas "externally driven, no local cadence": excluded from the poll cron, theSourceActoralarm, the scrape/agent OrgActor drain, and the first-party staleness scan;POST .../releases/batchstampslastFetchedAton a successful write since there's no poll to do it. A separate, quieter staleness scan (cron/push-staleness.ts) instead flags a push-fed source whose publisher appears to have stopped pushing. Set via the freeform source metadata PATCH routes — no schema change — or automatically on the first batch write made with an owner's publish token (#2390; only when the key is absent, so a curator's explicit"poll"opt-out sticks). See ingest.md → GitHub Action ingest. - Release importance (1–5, AI-scored,
releases.importance): scored inside the same ingest-timerelease-contentsummarize call, fail-open tonull; filterable via?minImportance=onGET /v1/releases/latest(forces a cache bypass); web shows it as a flame glyph at 4-5 only. See ingest.md → Content summarization. source.urlis for humans; fetch routing lives in metadata.source.urlis the canonical human-readable URL; machine fetch endpoints (RSS viametadata.feedUrl, GitHub-CHANGELOG override viametadata.githubUrl, …) live in metadata. Test: would a human ever want to land on this URL? If no, it's metadata. See remote-mode.md → Display URL vs. fetch routing.- Crawl mode uses Cloudflare's
/crawlendpoint for multi-page changelogs, stored insource.metadata.crawlEnabled. Seepackages/adapters/src/crawl.ts. - Firecrawl monitoring: external fetch backend for
scrapesources behind an anti-bot challenge our Browser Rendering can't clear; toggled per source viasource.metadata.firecrawl(not a newtype), excluded from the poll-fetch cron, prod-only secrets. Gotcha: the webhook carries a hunkless whole-document diff (no@@headers) — parse only viaaddedContentFromDiff. See firecrawl-monitoring.md. - Full-history backfill for windowed scrape sources:
POST /v1/workflows/backfill-source { sourceId, markdown?, maxWindows?, dryRun? }loops extraction over every window and upserts idempotently; deep Firecrawl path routes to durableBackfillSourceWorkflow(R2 snapshot + per-window steps, resumable) behindBACKFILL_WORKFLOW_ENABLED(default off); returns202 { instanceId, statusUrl }async. See firecrawl-monitoring.md. - Source staleness signal + render dry-run (#1528, #2381): daily scans on the
0 4 * * *tick flag overdue first-party (cron/source-staleness.ts,source-staleness), Firecrawl (cron/firecrawl-staleness.ts,firecrawl-staleness), and push-fed (cron/push-staleness.ts,push-staleness) sources; when any are actionable,sendStalenessDigestemails operators viaSEND_EMAIL(logs-only when the rollup is empty). Render dry-run:POST /v1/sources/:id/fetch?dryRun=trueon a client-rendered scrape source (crawlEnabled/renderRequired) renders the index once and returns{ renderCheck, candidateCount, sampleUrls }with no extraction/MA loop (CLI:releases source fetch <src> --dry-run). No feature flag. See firecrawl-monitoring.md. - Transactional email — every message renders through one shell (
renderEmailfrom@releases/rendering/email-shell): lane + severity rule, inline-styled table HTML plus a matched plain-text part, a copyable URL under every button, a footer that always states why it arrived, markdown-rendered release content, and Gmail Go-To/One-Click annotations (dormant until the domain is registered with Google). See emails.md. - Admin test emails:
/admin/emails(admin session) previews every outbound template via fabricated[test]sends (GET/POST /v1/admin/emails/{samples,test}); live follow digests stay onPOST /v1/admin/digest/test, real collection digests onPOST /v1/admin/digest/collection-test. See web.md → Admin hub. - AI model lanes:
/admin/models(admin session) overlays OpenRouter model ids per cheap-call lane (summarize/extract/feed-enrich/marketing) from OpenRouter's live catalog; stored insite_settings, wrangler vars are the fallback. See web.md → Admin hub. - LLM model pins live in
@releases/lib/models(modelId/resolveModel); a bump changes the pin plus anANTHROPIC_PRICINGrow. Request builders usesamplingParams/thinkingParams. Wrangler vars and/admin/modelsstill override the OpenRouter lanes. - Marketing classifier threshold:
/admin/classifieroverlays the marketing-classifier suppression threshold (GET/PUT /v1/admin/marketing-classifier,site_settingskeymarketing_classifier, default0.65) and lists/toggles per-sourcemetadata.marketingFiltervia the existing source PATCH route. Stored rows are re-classified withPOST /v1/admin/marketing-classifier/reclassify(dry-run by default, 25 per call). See ingest.md → Marketing classifier. - Raw capture + re-extract: the steady-state scrape path captures the scraped markdown to
released-rawbehindraw-snapshot-capture-enabled(default off; the update path POSTs toPOST /v1/orgs/:orgSlug/sources/:sourceSlug/raw-snapshot, scrape-only, #1283);POST /v1/workflows/reextract-source { sourceId, snapshotId?, maxWindows?, dryRun? }re-runs extraction from a stored snapshot with no live scrape, reusing the backfill machinery (#1284). See firecrawl-monitoring.md. - In-place release re-fetch:
POST /v1/workflows/refetch-release { releaseId, url?, dryRun?, force? }re-fetches ONE release's live page and updates the row in place — samerel_id, replaced title/content/publishedAt (media only on extractor hit), AI fields nulled for regen; a stored#fragmentURL requires an explicit same-hosturl, which rewrites the stored URL to the canonical permalink; placeholder extraction content is rejected and a >50% shrink needsforce: true(#2077). See firecrawl-monitoring.md. - Local ingest (local Claude Code, no remote dispatch): the
local-ingestskill (.claude/skills/local-ingest/) has the agent fetch + extract releases itself and write via the idempotent/batchupsert — skipping the update workflow's server-side extraction and its inference bill. Mandatoryrobots.txt/Content-Signalopt-out preflight (preflight.ts) refusesai-input=no(ai-train=noalone proceeds;conductor.buildis refused via itsai-input=no). See local-ingest.md. - Local backfill workflow (local Claude Code, no remote dispatch): the
backfill-source/backfill-sweepdynamic Workflows (.claude/workflows/) wrap thelocal-ingestprimitives in a deterministic harness — fail-closed preflight, explicit window cap, budget-gated extract waves, known-URL dedup,/batchupsert by typedsrc_id — to backfill a source's history locally without the remote extraction bill. Front-door: thebackfilling-sourcesskill; dry-run is the default. See local-ingest.md. - Opt-out / safety gates fail closed. On an ambiguous or unparseable response (e.g. an HTML anti-bot challenge), a gate returns the safe verdict (
unknown/refuse, neverproceed); when merging multi-value directives the strictest reading wins (anois never overwritten by a lateryes). Reference gate:local-ingest/preflight.ts(0proceed /1refuse /2unknown). See local-ingest.md → Preflight. - Prod D1 local backups:
bun run db:backupdumps every real table (auth included, FTS excluded — D1 export can't handle FTS5 virtual tables) to~/Code/.backups/releases/with retention rotation; dumps hold real user data — never commit/upload them. See local-development.md → Prod D1 backups. daysAgoIso()from@buildinternet/releases-core/datesfor date cutoffs.- D1's hard limit is 100 bound parameters per prepared statement. Dynamic
OR/INlists and batch INSERTs go throughmaxChunkLength/chunkForBindBudget(@buildinternet/releases-core/d1-limits);releasesinserts useRELEASES_BATCH_CHUNK_SIZE, sized for one bind per column.inArray(...)lookups chunk at 90 IDs. An unchunked statement surfaces asSQLITE_ERROR: too many SQL variableson poll/ingest. Details: storage-portability.md. - Local wrangler D1 one-shots are wall-clock bounded via
node scripts/run-timed.mjs <sec> -- …(kills the whole process group on deadline) so a timed-out agent can't orphan a multi-GB miniflare boot;db:migrate:local/db:reset:local/db:queryalready route through it, long-livedwrangler dev/preview:*deliberately do not, and manual local D1 calls should be wrapped the same way. See local-development.md → Local D1 schema parity. - Entity IDs are single typed nanoids (
rel_/src_/org_/…): the same string is PK and public ID. No dual UUID/public_idprep. New SQL goes throughapps/api/src/queries/*; no newreleases_fts MATCHsites. Seams + capability map: storage-portability.md. - Shared read queries: reads needed by both the API and MCP workers live in
packages/queries(@releases/queries/*, API defaults for deleted/hidden rows; MCP differences are explicit options). Migration map + open decisions: shared-queries.md. - Workflows-based ingest: every per-source fetch runs as a
POLL_AND_FETCH_WORKFLOWinstance,create()d from that source'sSourceActorDO alarm (#1776/#1819 — the actor is the sole fetch driver; per-source tier intervals normal=4h / low=24h + smart backoff pace the alarm). The hourly poll cron is a re-seed heartbeat only — no fan-out, no inline fetch fallback. See remote-mode.md. - OrgActor scrape/agent drain (#1777/#1946):
SourceActorself-flags stranded scrape/agent sources and arms a per-orgOrgActorDO that dispatches one deterministic update run per org via the sharedstartDeterministicUpdategate (kill switch + spend cap + #1815 scrape lock; no budget/lock logic in the actor). Update runs execute entirely via the API worker'sDeterministicUpdateWorkflow. See remote-mode.md. - Feature flags via Cloudflare Flagship (Tier 1). Boolean kill switches / rollout gates evaluate at runtime through the
FLAGSbinding; registry is@releases/lib/flags(flag(binding, varValue, def)); order is Flagship → wrangler var → default, failing open to the var. Adding a flag: add aFLAGSentry, convert the read toawait flag(...), and create the same kebab-case key in BOTH Flagship apps (releases-platform{,-staging}). Numeric tunables and secrets are intentionally NOT in Flagship. See feature-flags.md. - Be judicious with feature flags — do NOT add one by default. Every flag is permanent maintenance surface: a registry entry, a runtime branch on every read, a key that must be created and kept in sync across BOTH Flagship apps, and a dead code path to eventually retire. A flag per feature is how a flag registry rots into dozens nobody dares delete. Default to shipping a feature enabled, with no flag. Reach for one ONLY when there's a concrete reason runtime toggling earns its keep: a genuine kill switch for something risky/expensive/external-facing, a staged rollout you actually intend to ramp, or an operational lever you'd pull without a redeploy. If you can't name which of those applies, you don't need a flag. When unsure, ship it on and add the flag later if a real need appears — adding is cheap, a sprawl of stale flags is not. Prefer one well-scoped kill switch over several fine-grained per-feature toggles.
- Extract tier:
extractFromBody()branches on body token count — ≤50K one-shot/v1/messages, >50K a multi-round tool-use loop via AI SDK (extract-with-tools-aisdk.ts; legacy Anthropic SDK loop inextract-with-tools.tsonly when noaiSdkModel), gated behindEXTRACT_TOOLLOOP_ENABLED(per-sourcemetadata.extractStrategy = "toolloop"); falls back to one-shot on any error. See extract.md. - CLI (
apps/cli/, #2445):releasesis built and published from this monorepo as a 7-package changesets fixed group (@buildinternet/releases, 5 platform binaries,@buildinternet/releases-lib);publish-cli.ymlships npm + GitHub release + Homebrew tap. Public API calls go through@buildinternet/releases-sdk(apps/cli/src/api/sdk.ts); internal routes stay onapiFetch, and both sharecliFetch. Ship flow and trusted-publisher setup: cli-distribution.md. - Classification taxonomy — source
kindenum, products, release type (feature/rollup), tags, categories, collections, and how these axes differ (kindvstypevscategory): taxonomy.md. - REST route surface — route-naming buckets (#494), org-scoped routes + dual-registration +
bare_slug_rejected(#690/#698), the/v1/lookupsfamily (coordinate POST + on-demand materialization, by-domain, slug resolvers), org catalog, entity resolution (IDs over slugs), pagination shape, and the OpenAPI coverage gate (#894): routing.md. - Public API surface:
apps/api/src/api-surface.tslists the public operations (production/v1/openapi.json, SDKs); every other route is internal. New routes start internal; promoting one needs a tag, 2xx response schema and request body, enforced byscripts/check-openapi-coverage.ts. See routing.md → Public API surface. - Inbound provider webhooks live at
/v1/inbound/<provider>(firecrawl,github): a namespace with no shared auth middleware, where each handler checks its own secret. Never put a receiver under a gated namespace (/v1/integrations,/v1/webhooks). A router's.use("/prefix/*")gates other routers' routes too, whichapps/api/test/route-gate-isolation.test.tsguards (#2428). See routing.md. - Product and source URLs share one namespace: every product and source lives at
/{org}/{slug}, and a product slug may never equal a source slug in the same org; product create/adopt and source rename return 409, generated source slugs take-releases./sources/:idis a 308 alias. Any new product/source write path must checkisSlugHeldByProduct/isSlugHeldBySource(@releases/queries/entities). See web.md → Product and source URLs. - Friendly release URLs —
/release/rel_<id>-<slug>: therel_ID (positional,rel_+ 21 chars — nanoid may contain-/_) is the only routing key; the slug derives fromtitle_shortat request time (no stored column, no backfill) and stale/bare forms redirect to canonical with a 308 (permanentRedirect). Helpers in@buildinternet/releases-core/release-slug. See routing.md. - Error responses — every non-2xx response is the nested envelope
{ error: { code, type, message, details? } };throwaReleasesErrorsubclass and letrespondErrorserialize it (never hand-rollc.json({ error })). Taxonomy source of truth ispackages/core/src/errors.ts; the three-layer split, producers/consumers, and how to add a code: errors.md. - Search-query log:
/v1/searchand the MCPsearchtool write each query (≤200 chars) + mode/counts/duration tosearch_queries(web carriesX-Releases-Surface: web). Read viaGET /v1/admin/search-queries{,/top}. Kill switchSEARCH_QUERY_LOG_DISABLED. Distinct fromtelemetry_events, which carries only command names and stays PII-clean for the OSS CLI contract. - Release coverage: multiple releases can cover one launch (marketing post + changelog + app note); canonical + coverage items tracked in
release_coverage, read paths hide coverage-side rows by default. Ingest also links release candidates, backports, and sibling-package releases (system:version-line). See coverage.md. - Org overviews: AI-generated
knowledge_pages(scopeorg) summarize recent activity; display staleness warningOVERVIEW_STALE_DAYS = 30from@buildinternet/releases-core/overview. Automated regen runs on a daily cron with a per-org cadence (7d default, 2d velocity fast tier,overview_cadence_daysmanual override; #1895). See web.md → Org overviews. - Collection daily summaries: one
collection_daily_summariesrow per (collection, ET day) — title + one-line summary + bullet takeaways generated nightly over closed ET days via the shared summarization lane (reusesSUMMARIZE_MODEL+ its Haiku fail-open, distinct only bygenerationName; no per-feature model var), gated per-collection bycollections.daily_summary_enabled; read viaGET /v1/collections/:slug/daily-summaries, rendered as timeline date headers. See web.md → Daily summaries. - GitHub CHANGELOG files are fetched alongside tagged releases, stored in
source_changelog_files(refresh piggybacks on every GitHub fetch); web surfaces them viaGET /v1/sources/:slug/changelog. See web.md. - ISR revalidation is ingest-driven, not clock-driven. Ingest pings web's
POST /api/revalidate(notifyWebRevalidate); the pages'revalidate = 86400is only a backstop. A fetch revalidate anywhere in the ROOT LAYOUT's import graph caps EVERY route in the app — guarded byapps/web/src/lib/isr.test.ts. See web.md → ISR revalidation. - API-worker → web internal calls share ONE channel credential (
RELEASES_SERVICE_KEY/WEB_SERVICE_KEY, verified byverifyServiceKey), mirroringRELEASES_PROXY_KEYinbound. Reuse it for the next internal endpoint — do NOT mint a per-feature secret; it is deliberately not the root API key. See web.md → ISR revalidation. - Media handling: at ingest,
normalizeMediaUrl()(packages/rendering/src/media-url.ts) strips Next.js/Vercel image-optimizer wrappers so the underlying CDN URL is stored. Ingest-time R2 mirroring runs whenever theMEDIAbucket binding is bound (always in prod); an unbound bucket stores third-party URLs verbatim. See web.md → Media handling. - Inline hosted-video cards (#1549): the cron poll-fetch media pre-pass detects Wistia/Loom/Vimeo/YouTube links in a new release's body (
detectInlineVideos,packages/rendering/src/video-embed.ts), resolves a poster via oEmbed, and appends a{ type:"video", url:<poster>, alt, linkUrl:<watchUrl> }media[]item that rides the existingprocessMediaForR2mirror; web renders a read-only play-thumbnail card (InlineVideoCard) linking out. Special-cased to video (first inline-body asset promoted to mirrored media); fail-open, no flag; iframe embed deferred. See web.md → Inline hosted-video cards. - Manual release media edit:
PATCH /v1/releases/:id { media: [...] }REPLACES a release's storedmedia[]wholesale (curator fix without re-sync); not-yet-mirrored items (nor2Key, not onMEDIA_ORIGIN) run through the ingestprocessMediaForR2mirror, gated onenv.MEDIA != null. Cron re-fetch never clobbers this (onConflictDoNothing/RELEASE_URL_UPSERTonly backfills stored-empty media). See [web.md → Media handling]. - Inline-video retrofit (#1549 backfill):
POST /v1/workflows/backfill-video { releaseId?|sourceId?|all?, limit?, dryRun? }(admin, idempotent, no flag) re-runsdetectInlineVideosover a stored release body, mirrors the poster, and APPENDS thetype:"video"item to the existingmedia[](dedup bylinkUrl) — built onrunVideoBackfillinlib/media/media-backfill.ts, sibling ofbackfill-media. See web.md → Inline hosted-video cards. - Org avatars: stored at
orgs/{slug}.{ext}inreleased-media, served fromhttps://media.releases.sh/orgs/{slug}.{ext}; pointer onorganizations.avatar_url, writable viaPATCH /v1/orgs/:slug { avatarUrl }. New orgs landnullwith the OG/web fallback togithub.com/{handle}.png(#982 open). Reuse theorgs/{slug}.{ext}key for any new avatar-write path. See web.md. - Scoped API tokens: opaque
relk_<lookupId>_<secret>Bearer tokens (api_tokenstable), scope ladderread ⊂ write ⊂ admin; staticRELEASES_API_KEYis implicit root; kill switchAPI_TOKENS_DISABLED. MCP gates the AI tools + the on-demand/v1/lookupsfallback onwriteand forwards the caller's own token. See remote-mode.md → Auth model and mcp.md → scope enforcement. User-owned API keys use the Better Auth@better-auth/api-keyplugin (prefixrelu_,apikeytable, per-key rate-limit + metering), capped read-only (USER_API_KEY_MAX_SCOPE = "read"— write/admin refused at the mint route and clamped at auth time, so a user key can never satisfywrite), gated byuser-api-keys-enabled; therelk_lane stays for machine principals. Browserreleases loginmints one of these read-onlyrelu_keys with no copy-paste via device authorization (RFC 8628;deviceAuthorization()+bearer(), always registered) —verificationUrimust be an absolute web-origin URL, not a relative/device. The CLI never stores the device session: it signs out after each step, account commands run their own one-shot approval with a named purpose (the device-codescope), and logout self-revokes viaDELETE /v1/tokens/me(see remote-mode.md → Auth model). - OAuth role provisioning: a user's OAuth scope ceiling is the Better Auth
user.rolecolumn (user→read,curator→read+write,admin→read+write+admin; NULL → read-only, fail-closed); manage it via the root-key-gatedPATCH /v1/admin/users/roleroute /releases admin user set-role(auditedrole-changedlogEvent), NOT an env var (the oldOAUTH_ADMIN_USER_IDSbootstrap is gone, #1484). First admin seeded by a one-time D1 write. See remote-mode.md → Role provisioning. - OAuth client provisioning: register/manage "Sign in with Releases" OAuth clients via the root-key-gated
admin/oauthroute family (POST/GET/PATCH/DELETE /v1/admin/oauth/clients[...],rotate-secret);reloc_secrets shown once,trusted⇒skip_consent,tokenEndpointAuthMethod:"none"⇒ public/PKCE client. RFC 7591 dynamic client registration is ON (allowDynamicClientRegistration: true) so agent-run MCP clients self-register at the public/oauth2/registerendpoint (untrusted ⇒ consent-required, forced public/PKCE, stored scopes capped at identity+read/DCR_SCOPES— neverwrite/admin/skip_consent; rate-limited 5/min/IP in prod); the admin route stays for first-party/trustedclients, and the plugin's session self-service write endpoints remain admin-only (#1482). The consent page never trusts a client's self-reported name/client_uri: it shows the signedredirect_uridestination and a verified badge only for operator-setmetadata.official(admin routeofficial, default true on create). CIMD (#2409) is on too: an HTTPS-URLclient_id(Claude's "published identity") is resolved by@better-auth/cimdon first authorize into a row with the same DCR ceiling; fetch transport isauth/oauth-cimd.ts. See remote-mode.md → OAuth client provisioning. - OAuth resource-server JWT lane: the REST API + MCP workers verify the AS's JWT access tokens with
jose+ the JWKS endpoint (NEVER by importing better-auth intoapps/mcp— zod-pin) via the shared@releases/lib/oauth-jwt(isJwtShaped/verifyOAuthJwt); checks sig +iss+aud+exp, maps thescopeclaim (already role-clamped at issuance) onto theread ⊂ write ⊂ adminladder. Additive + fail-consistent: a verify failure is treated like an invalidrelk_(REST 401 on a write/admin route, public read stays open; MCP → anonymous read, never gates the unauthenticated path). MCP audience viaOAUTH_JWT_{ISSUER,AUDIENCE}wrangler vars (#1483). Generic MCP clients (MCPJam-class): DCR extragrant_typesare intersected, DCRapplication_typeis defaulted/coerced tonativefor loopback/private-use-scheme redirect clients (opencode/Cursor-class; live on Better Auth 1.7 — host-bearing private-use redirects are accepted upstream since 1.7.8, so the dist patch is gone; an explicitwebis still rewritten here because oauth-provider never overrides a sentapplication_type), kitchen-sinkscope=is downscoped, origin and/mcpare both valid RFC 8707 resources, and same-environment MCPaudis accepted on REST JWT gates so follows tools can forward the Bearer. See mcp-cimd-interop.md and remote-mode.md → Auth model. - Rate-limit tiers: three rungs on CF native limiters — anonymous-IP 120 / account-userId 300 / machine-token 600 — selected by the shared
@releases/lib/rate-limit-tiers; account-tierrelu_verify isCREDENTIAL_CACHE-backed; consumption is therate-limit/decisionAxiom event. These read-surface tier counters never move to KV/D1 (the separate/api/auth/*brute-force limiter is the one limiter that's KV-backed — #1728, see the auth note above). See remote-mode.md. - Entity notices: a small curator-set note on an org/product/source, stored under
metadata.notice({ message, linkText?, coordinate?, href? }); set via the entity PATCH route, surfaced as a typednoticefield + a web banner + an MCP pointer. See web.md → Entity notices. - Owner-declared manifest (
releases.jsonv2): declaration manifest at domain/.well-known/releases.json(org + optionalproducts[]+ top-levelreleases[]locators) and repo-rootreleases.json(product binding + reporeleases[]); fill-if-empty reconciliation + cost-tiered source materialization; fail-closed, never clobbers curator/editorial fields.github+path+publish: "push"materializes a push-fed source only for a verified owner. See well-known-config.md. - Self-serve listing lane (#1947 phase 2): anonymous
POST /v1/listing/{validate,activate}— live manifest validation + instant-stub activation with atracking_requested_atdemand signal; invalid-manifest activation is a 400ValidationError(no 422 in the taxonomy); rate limited per-IP (10/min) and per-domain on activate (3/min), both CF-native; routes live in a newpublicWriteRoutesnamespace bucket (integrity enforced by handler-level guards, not auth middleware); kill switchlisting-self-serve-enabled. Curator surface isGET /v1/orgs?trackingRequested=1(admin-gated inside the handler), not a separate/admin/*route. See well-known-config.md → Self-serve listing. - Ownership claims (#1947 phase 3a): signed-in-only
POST /v1/listing/claim{,/verify}+GET /v1/listing/claimsproves domain control via arelv_token, checked either as a.well-known/releases-verify.txtfile or a_releases-challenge.<domain>DNS TXT record (either passes, fail-closed on any ambiguous response); reuseslisting-self-serve-enabled; unlocks self-serve Tier-1 promotion (follow-up PR), not org edit rights. See well-known-config.md → Ownership claims. - Publish tokens (#2373): a verified claim holder mints a
relk_token bound to ONE source (api_tokens.source_id, scopepublish, off the ladder) via session-only/v1/me/publish-tokens(cookie + exact web origin, or a one-shot device-approval session as Bearer, #2388 — never a key or JWT; web UI on/account/webhooks, #2387); it passes auth only onPOSTto that source's tworeleases/batchroutes (middleware path allowlist + handler source check), and the claim/source is re-checked every request, fail closed. Ending a claim (DELETE /v1/listing/claims/:idfor the owner,DELETE /v1/orgs/:slug/claims/:idfor admins, kept asrevoked, #2389) stops its tokens on the next request. See remote-mode.md → Publish tokens. - Mobile-app discovery (#1907): separate from the manifest — discovers a domain's undeclared native apps from
/.well-known/apple-app-site-association+/.well-known/assetlinks.json. iOS bundle IDs resolve via iTunesbundleId=lookup and land as paused, hiddenappstorecandidates (curator unpauses); Android package names become an internalorg.metadata.discoveredAppshint (noplaystoretype). Own 06:30 UTC cron +POST /v1/orgs/:slug/discover-apps; gated bywell-known-materialization-enabled. Requires hidden-source containment (public read/search/embed paths filteris_hidden=0viasources_visible). See well-known-config.md → Mobile-app discovery. - User follows + feed + webhooks: signed-in users follow orgs/products (
user_follows);/v1/me/follows+/v1/me/feed(org follow = its products too). Self-serve/v1/me/webhooks(org-scoped max 10 + onescope: "follows"URL) fans out real-timerelease.createdusing the follow graph; account UI on/account/notifications, CLIreleases webhook …, API docs at/docs/api/webhooks. Gate: Better Auth session OR Bearer user principal (relu_/ OAuth JWT) — same as follows, NOT relk_/root/anonymous. MCP covers both surfaces vialist_webhooks/manage_webhook(#1678, #2326). Workspace-owned equivalent:/v1/workspaces/:workspaceId/webhooks(org-scoped only, owner/admin manage, #2324). See routing.md, web.md, mcp.md, docs/webhooks.md. - Semantic alerts (#2304): signed-in
/v1/me/semantic-alertsCRUD (max 5, threshold default 0.80, follows-only) behindsemantic-alerts-enabled(default off). When on,release.createdscores enabled alerts with one JEVnoulbatch per release and sends webhook and/or email. Admin preview inserts synthetic releases through batch publish (POST /v1/admin/semantic-alerts/preview). Match quality isGET /v1/admin/semantic-alerts/summary(AEblob4 = semantic-alert); JEV spend stays onai_usagelanesemantic-alert-match. Query text stays out of Analytics Engine. See semantic-alerts.md. - Stripe customer registration (
@better-auth/stripe, billing groundwork — customer management only, no subscriptions yet):createCustomerOnSignUpcreates a Stripe Customer per user, linked viauser.stripeCustomerId. GATED on BOTHSTRIPE_SECRET_KEY+STRIPE_WEBHOOK_SECRETresolving (buildStripePlugin, same inert-until-provisioned seam as dash/sentinel/social) — secrets bound via Secrets Store (prod bound; staging deliberately unbound until its sandbox Stripe secrets exist — the gate keeps it inert). Worker-compat: Stripe client usesStripe.createFetchHttpClient(). Webhook served at/api/auth/stripe/webhook(existing/api/auth/*catch-all). - Passkeys (
@better-auth/passkey, WebAuthn/FIDO2): always-on (no flag, like magic link) — adds thepasskeytable andsignIn.passkey/passkey.{add,list,update,delete}endpoints.rpID/originare pinned to the WEB origin viaderivePasskeyRp(env)(fromWEB_BASE_URL), NOT the plugin'sbaseURL-derived default — the ceremony runs in the browser onreleases.sh, not the API worker onapi.releases.sh, so the default would fail every origin/rpID check. The challenge cookie rides the.releases.shcross-subdomain cookie domain. Web:passkeyClient()inauth-client.ts, a manage panel on/account(passkeys-panel.tsx), and a passkey button + conditional-UI autofill on the sign-in form. The auth UI itself is no longer behindNEXT_PUBLIC_AUTH_UI_ENABLED(retired) — it followsAUTH_CONFIGURED(=NEXT_PUBLIC_BETTER_AUTH_URLset). See remote-mode.md → Auth model. - Account settings — email change + social connections:
/accountalso surfaces anemail-panel.tsx(Better AuthchangeEmail, server-enabled viauser.changeEmail— a confirmation link goes to the user's CURRENT address,updateEmailWithoutVerificationleft off) and asocial-connections-panel.tsx(listAccounts/linkSocial/unlinkAccount; lists/links/unlinks the providers inNEXT_PUBLIC_AUTH_SOCIAL_PROVIDERS, self-renders nothing when none are configured, and relies on Better Auth refusing to unlink the last sign-in method). Provider chrome (SOCIAL_PROVIDERS,PROVIDER_META) is shared with the sign-in form viaapps/web/src/lib/social-providers.tsx. - Workspaces (Better Auth org plugin): user-tenancy "Workspaces" — every user gets a personal one (lazy
session.create.beforeprovisioning that backfills existing users; deterministicws-<userId>slug for race-dedupe), DISTINCT from the registryorganizations(indexed vendors).auth*-prefixed tables (organization/member/invitation) inschema-auth.ts; always-on, no flag; minimal/account/workspacesUI. Inert org-keyed Stripesubscriptionseam (plans: [],referenceId= workspace id,authorizeReferenceowner/admin gate).member.role≠user.role. See workspaces.md. - uploads.sh OAuth client (workspace connect): Releases Index is a public PKCE client of uploads.sh (
releases-sh,files:read+offline_access). Owner/admin connect and disconnect from/account/integrations; tokens are encrypted at rest per workspace. Connect plumbing only — no screenshot proxy yet. Redirect URIs, env, and API: uploads-oauth.md.
Further reading
Deep dives live in docs/architecture/. A reader's guide with task-based entry points is at docs/README.md.
- local-development.md — portless dev URLs, worktree bootstrap/teardown, local D1 schema parity, auth rate limiting and the local signing secret.
- staging.md — the staging read-surface: hosts, per-environment managed agents, disabled crons, access gate, deploy and prod-sync commands.
- deploy-coupling.md — account-scoped wrangler bindings and open-core boundary for forks/self-hosters.
- remote-mode.md — D1, auth model (scoped API tokens), rate limiting, migrations, sessions, cron polling + retier, workflows-based ingest, discovery guardrails.
- storage-portability.md — SQLite/D1 seams,
createDb, entity-ID invariant (single typed nanoid), FTS ownership, backend capability map, and what an optional future Postgres backend would cost (aspirational, not in progress). - ingest.md — ingest pipeline: source-type adapters, dedup + D1 batching, smart-fetch backoff, URL exclusion / suppression, and the ingest-time AI passes (summarization, marketing classifier, feed enrichment).
- emails.md — transactional email: the shared shell, the three lanes, the email-client constraints it encodes, Gmail annotations, and the admin preview catalog.
- logging.md — per-runtime logging:
logEvent()for workers vs. thefs-backed logger for CLI/neutral packages, payload conventions, severity,Errorunwrapping. - routing.md — REST route surface: naming buckets, org-scoped routes + dual-registration, the
/v1/lookupsresolver family + on-demand GitHub materialization, org catalog, entity resolution, pagination shape, public API surface + coverage gate. - errors.md — standardized error envelope: taxonomy source of truth, the three-layer package split (core/api-types/lib), producers (
respondError) + consumers, how to add a code. - idempotency.md — optional replay-safe POST contract, encryption secret provisioning, supported routes, cleanup, and failure boundary.
- taxonomy.md — classification axes: source
kind, products, release type, tags, categories, collections, and how they differ. - semantic-search.md — Vectorize indexes, hybrid RRF, query cache, related-entity rails.
- mcp.md — remote MCP server, scope enforcement, WebMCP parity, MCP Registry listing.
- mcp-cimd-interop.md — generic MCP client OAuth (DCR grant_types, kitchen-sink scope, origin vs
/mcpresource, forwarded Bearer). - agents.md — how local Claude Code agents onboard/maintain orgs: skills, local workflows, evals, and the onboarding API surface.
- coverage.md — release coverage + ingest-time grouping, cron observability.
- web.md — changelog range/slicing API, GitHub CHANGELOG ingestion, Open Graph images, org overviews, category overlay, collections, media pipeline, org avatars.
- events.md — release event bus:
ReleaseHubDurable Object,GET /v1/releases/streamWebSocket, fire-and-forget publish from batch + cron ingest. - cli-distribution.md — OSS repo, npm, Homebrew tap.
- ai-gateway.md — optional Cloudflare AI Gateway passthrough for Anthropic SDK calls; covers direct worker calls, leaves Voyage embeddings + managed-agent internal loops on the direct path.
- classification-analytics.md — marketing-classifier decisions in Analytics Engine: positional schema, sampling, retention, admin summary/recent routes, and the Status Classifications tab.
- extract.md — two-tier extraction path: one-shot inline for small bodies, multi-round tool-use loop for large ones, hard fallback to one-shot on any failure. Feature-gated behind
EXTRACT_TOOLLOOP_ENABLED. - local-ingest.md — local Claude Code onboarding path: the
local-ingestskill has the agent extract releases itself and write via/batch(no remote MA, no extraction billing), gated by a mandatoryrobots.txt/Content-Signalopt-out preflight. - feature-flags.md — Cloudflare Flagship Tier-1 boolean flags: registry, evaluation order, per-flag reference, dashboard setup.
- content-pipelines.md — map of every routine/scheduled AI-content pipeline (overview regen, batch summarize, collection summaries, digests, ingest-time passes): schedule, gate, model lane, manual trigger.
- firecrawl-monitoring.md — external Firecrawl fetch backend for challenge-blocked
scrapesources: monitor lifecycle, the hunklessmonitor.pagediff wire format, the diff-delta vs. full re-scrape ingest paths, cost gate, poll-fetch exclusion, and staleness resilience. - maintenance-workspace.md — per-user
~/.releases/work/(tasks / runs / reports) convention for agent-driven admin maintenance; durable, cost-aware trail for the seeding/maintaining/managing/overview skills, reachable across the monorepo and CLI checkouts. - well-known-config.md — owner-declared
releases.jsonv2 manifest: host-scoped authority (domain = org + products + release locators, repo = product binding), locator-first matching, cost-tiered source materialization, reconciliation precedence, the sync route + daily sweep. - workspaces.md — Better Auth organization module: personal-workspace provisioning (lazy, on session create), the schema/naming split from the registry
organizations, role separation (member.rolevsuser.role), and the inert org-keyed Stripe billing seam. - uploads-oauth.md — Releases Index as an uploads.sh OAuth client: workspace connect/disconnect, redirect URIs, encrypted tokens, env.
Environment
Do not edit .env directly. Required vars are documented in .env.example. App env vars use the RELEASES_ prefix (RELEASES_API_URL, RELEASES_API_KEY, RELEASES_DATA_DIR, …).
Staging
Staging is a read-surface for UI/API iteration plus an agent-iteration sandbox, not a full replica — no crons, no webhooks, no Vectorize, access-key gated. Hosts, deploy commands, and the prod-sync script: staging.md.
Legacy naming
The project was originally called "Released"; the rename to "Releases" leaves the Cloudflare resource names deliberately unchanged:
- Cloudflare resources keep the
released-prefix: D1 databasereleased-db, and R2 bucketsreleased-media(permanent, public media) andreleased-raw(ephemeral raw-page snapshots for backfill — content-hash keyed, 90-day lifecycle, public domainraw.releases.sh; see firecrawl-monitoring.md). The first two predate the rename, so renaming them is a live migration, not a text change; new resources (released-raw) keep the prefix for consistency.
Everything else — env vars (RELEASES_*), copy, prompts, display names, webhook headers (X-Releases-*), package/workspace names, the ~/.releases data dir, localStorage keys — uses the new name.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

