# Changelog

All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow SemVer.

## [0.9.17] — 2026-11-09

### Changed
- `docs/API.md` restructured as a reference: an introduction (authentication, conventions, an error table,
  metering), then one heading per endpoint with its role, request body and response shape as code, and the
  notes as prose. The same content as before, readable on the site at `/docs/api`.

### Added
- `scripts/export_site_docs.py`: the documentation in this repository (API reference, action contracts, MCP,
  the OpenTelemetry convention, the sandbox, the nine connector guides, the four SDK readmes, the two recorder
  readmes and the workflow-runtime readmes) exported to the site as one markdown file per document, with links
  rewritten to site routes and each document's last commit date in a manifest, plus `CHANGELOG.md` and an
  `llms.txt`. The site renders a page per document at `/docs/<slug>`, a `/changelog` page, and serves the raw
  markdown beside each. The export refuses anything that looks like a credential. `tests/test_site_docs_export.py`.

## [0.9.16] — 2026-11-05

### Fixed
- The public verification page said "chain not intact" for every verdict hash. `GET /v1/public/verify/{hash}`
  computed `chain_valid` only for statements; a verdict match carried no chain field and the page rendered the
  absence as a broken chain, under a footer saying the chain was intact. The endpoint now verifies the verdict's
  own chain (every verdict of its run up to that one re-hashes and links, from the run's genesis) and the page
  shows intact, not intact, or not checked, never a default. Found by an outside reader on a fresh sandbox PASS.
- The Agent sandbox answered FAIL, not UNKNOWN, for "a required system is down at verification time" in any
  workspace that had run the sandbox before: facts pushed by earlier runs made the down system count as a
  connected source, so its missing effects were judged. The harness now passes the systems the extractor could
  not reach to the verifier, which treats them as not connected for that verification, as a connector in error
  is. The benchmark and the test suite never saw this because they use a fresh workspace per scenario; the
  endpoint test now runs a baseline and then the unreachable scenario in one workspace.
- Trials are card-free, as the pricing page and the terms have always said: Checkout no longer collects a card
  when nothing is due today, and a trial that ends without a card cancels the subscription instead of charging.
  `GET /v1/settings/billing` carries the Founding Builder offer so the in-app upgrade cards show the price
  Checkout charges (they showed the list price while Checkout applied the offer).

## [0.9.15] — 2026-11-03

### Added
- Library contract `coinbase_agent_trade`: an agent places an order on a Coinbase account (Coinbase for Agents,
  the Coinbase MCP server's `orders_create`, or the Advanced Trade API). The subject is the product the agent
  was told to trade; the contract requires one order on it, created within the window and filled, for the
  notional asked, within the per-order limit and the agent's 24-hour total; it forbids a second order, a
  cancelled, expired or rejected one, and treats a fill Coinbase busts or corrects within 7 days as a reversal.
  Statuses and fields are transcribed from Coinbase's reference (cited); the limits are template values.
- Coinbase connector (`coinbase`, facts, read-only): orders and fills from the Advanced Trade API with a CDP
  API key holding the View permission only, a fresh two-minute ES256 token per request, an optional portfolio
  filter so an agent's portfolio is read on its own, seven days of re-read on every sync. Facts
  `order_created`, `order_filled`, `order_failed`, `order_reversed`, keyed on the product id. An Ed25519 key is
  refused with the reason. Available on every paid plan. `docs/connectors/coinbase.md`.
- Recorder mapping examples for the Coinbase MCP server (`recorder/*/examples/coinbase.json`) and the README
  notes on the remote-server bridge and `--warn-unmapped`.
- Sandbox: a seventh system of record, a broker with tradable products, orders and fills, and an eighth
  workflow, "Place a trade" (`sbx_place_order`), with the generic failure matrix plus seven specific cases:
  over limit, halted product, daily total exceeded, amount mismatch, wrong product (ETH-USDC for ETH-USD),
  order accepted but never filled, fill busted after settlement. 18/18 scenarios agree with the oracle; the
  Failure Lab fixtures are regenerated with the trade workflow (48 fixtures). `docs/benchmark.md` carries the
  scripted-agent rows.

## [0.9.14] — 2026-11-02

### Added
- The API serves the brand mark at `/favicon.ico` (16, 32 and 48 px) and `/favicon.png` (128 px). The Claude
  connectors directory and Claude's own UI read a server's icon from its URL, and the API host answered 404.

## [0.9.13] — 2026-11-02

### Fixed
- Invitations can be accepted. The invitation email always said "sign in to accept", but a member could only act
  inside a workspace they were already active in, so an invited membership stayed pending for ever and the
  invitee's first sign-in created a workspace of their own. Signing in with the invited address (hosted sign-in,
  the verification link, the development login) now activates every pending membership of that address, audited
  as `member.accepted`, and the sign-in lands in the workspace that invited them when they have no other.
- The web app has a workspace switcher (the account menu lists the member's other workspaces), so a person who
  belongs to more than one can move between them; `POST /v1/auth/switch-tenant` existed, nothing called it.

## [0.9.12] — 2026-11-02

### Added
- Every MCP tool carries a title and the standard annotations (`readOnlyHint`, `destructiveHint`,
  `idempotentHint`, `openWorldHint`). Fourteen tools are reads and say so, which lets Claude run them without a
  confirmation per call; `record_action` is the one write, marked non-destructive. The annotations must agree
  with the tool's scope, and `tests/test_mcp.py` checks that they do. Required for a listing in the Claude
  connectors directory, where a tool without them is flagged.
- `product-catalog/mcp-tools.yaml` now carries each tool's title and annotation-derived `read_only` and
  `destructive` flags.
- `docs/mcp.md`: the connector-directory listing (texts, use cases, authentication and data-handling answers,
  reviewer instructions).

### Fixed
- `tests/test_metering.py` pinned its actions to October and failed from the first of November; the fixture
  now records in the current month.

## [0.9.11] — 2026-10-29

### Added
- `product-catalog/`: the machine-readable product facts the CommitLayer knowledge MCP server
  (`kobikwelu/cl-repository-svcs`) answers from. Plans, features, connectors, verdicts, reason codes, MCP tools
  and scopes, API capabilities and release notes are exported from the code by `scripts/export_product_catalog.py`
  (with `openapi.json` and `mcp-schema.json` under `--artifacts`); products, security claims and limitations are
  written by hand and reviewed in the pull request that changes them.
- CI keeps the two in step: `tests/test_product_catalog.py` and the `knowledge-impact` workflow fail when the
  committed catalog differs from what the code exports, and `scripts/check_docs_impact.py` fails a pull request
  that changes routes, MCP tools, entitlements, connectors, reason codes, verdicts, pricing or security code
  without updating the matching manifest or doc (waivable with `docs-impact: none - <reason>` in the description).
- `knowledge-dispatch`: after a push to the production branch that touches product facts, a
  `commitlayer_knowledge_update` repository dispatch with the exact commit asks the knowledge repository to open
  a sync pull request. Needs a GitHub App installed on the knowledge repository (Contents: read and write).

### Fixed
- The approved security claims do not say records are deleted after 13 months: the retention setting is stored
  but no job enforces it yet. Uploaded files and transcript excerpts are purged on schedule and say so. Listed as
  a limitation until a purge job exists.

## [0.9.10] — 2026-10-29

### Fixed
- A webhook delivery held SQLite's write lock for as long as the receiver took to answer, up to the 15-second
  timeout: the delivery row was added before the network call and the next read flushed it. Every API write in
  that window waited, and a recorded action's verdict INSERT took two to three seconds whenever the previous
  event was being delivered to a slow receiver. Nothing is written now until the receiver has answered; a test
  takes the write lock from a second connection during the call to prove it.

## [0.9.9] — 2026-10-28

### Fixed
- The last slow statement on the admin metrics: the count of dead, unarchived jobs of the week walked every
  dead row through the status index, archived ones included, at one to two seconds. Migration 0019 adds
  `jobs(status, archived_at, created_at)`, which answers it from the index alone.

## [0.9.8] — 2026-10-28

### Fixed
- The admin metrics' last unindexed counts: jobs created in the last seven days (two seconds over the whole
  jobs table, once a minute) and failed webhook deliveries of the last day. Migration 0018 adds
  `jobs(created_at)` and `webhook_deliveries(status, created_at)`.

## [0.9.7] — 2026-10-28

### Changed
- The trial-ended error (`402 trial_expired`) names the plan the trial was on and says to add a payment method
  to keep it or choose another plan. It read "Choose a plan to continue" with `upgrade_to` set to the
  workspace's own plan, which the site rendered as "This needs the Scale plan" to a workspace already on
  Scale.

### Fixed
- After 0.9.6 the admin metrics were still one to four seconds: the slowest statement had become the queue
  depth, jobs by status with archived ones left out, over a table that kept every succeeded job forever.
  Migration 0017 adds `jobs(archived_at, status)`, which covers the query, and the nightly purge archives
  succeeded jobs after thirty days (they stay, as the record of what ran, and the admin list shows them on
  request), so the unarchived set is a month of jobs rather than all of them. The purge reports
  `succeeded_jobs_archived`.

## [0.9.6] — 2026-10-28

Recording an action took about a second in production (62 ms on a small database) and the admin metrics took
one to four seconds every minute.

### Fixed
- Three counts over the workspace's claims ran on every recorded action without an index for their filter (the
  monthly allowance by billing period, the daily cap by creation time, and "is this the first action", which
  counted every claim the workspace ever recorded). Migration 0016 adds `claims(tenant_id, billing_period,
  channel)` and `claims(tenant_id, created_at)`; the first-action check reads two rows instead of counting.
- `GET /v1/admin/metrics` scanned every run of the week, with a JSON extract per row, on each call. Migration
  0016 adds `runs(created_at)`.
- The nightly maintenance job checkpoints the write-ahead log with TRUNCATE instead of PASSIVE: a passive
  checkpoint stopped at the oldest open reader and never reset the file, so a WAL that grew under the metrics
  poll stayed large for every later commit to work through. The job's result says whether a reader held it back.

### Added
- Commit time in the slow-request log line (`N commits, M ms in commit`): the lock and checkpoint wait that the
  per-statement timing never saw. `database: { bytes, wal_bytes }` on the admin metrics, with an alert above
  256 MB of WAL.

## [0.9.5] — 2026-10-28

A transfer an agent sends from a Stripe financial account is verified against Stripe's record of it.

### Added
- `stripe_b2b_transfer`, a library action contract for `stripe pay` and the Treasury `send_money` tool: the
  transfer must exist for the intended recipient, once, for the amount requested, reach `posted`, and not be
  returned within 30 days; a failed or canceled one, a second one in the window, one over the per-transfer limit,
  in a currency not allowed, or over the agent's 24-hour total, fails. Status meanings are quoted from Stripe's
  OutboundPayment reference (retrieved 2026-10-28); limits are template values.
- The Stripe connector reads outbound payments from Treasury (v1, per financial account) and Money Management
  (v2, the API behind Global Payouts and Stripe Pay) into facts on the recipient: `outbound_payment_created`,
  `outbound_payment_posted`, `outbound_payment_returned`, `outbound_payment_failed`. The last 35 days are re-read
  on every sync so a later `posted` or `returned` lands. A key without the permissions still syncs refunds; the
  connection's health message names what to grant.

## [0.9.4] — 2026-10-27

A password sign-up proves its address before it gets a session.

### Added
- `users.email_verified_at` and the `email_verifications` table (migration 0015; accounts that exist already
  are marked verified, they were admitted before the gate). `app/core/emailverify.py`: a random token, stored
  hashed, good once and for 24 hours; a new link retires the previous one; at most one link a minute per address.
- The hosted sign-in callback, when WorkOS reports `authentication_method: Password` for an account that has
  never verified: the workspace is created as before, a link to `${WEB_URL}/verify-email?token=` is emailed, and
  the browser goes to `/login?notice=verify_email` with no session. Audit `auth.verification_sent` (or
  `auth.verification_throttled`).
- `POST /v1/auth/verify-email { token }` → the `Session` the sign-in withheld (`400 invalid_verification` when
  unknown, used or expired; `403 account_held` for a held workspace, whose address is still marked verified).
  Audit `auth.email_verified`, then `auth.login`. `POST /v1/auth/verify-email/resend { email }` → `{ ok: true }`
  whatever the address.
- Google, GitHub, Microsoft, Apple, SSO, magic-link and passkey sign-ins mark the address verified on arrival
  and never see the step; the development login and the dev-only `/signup` do the same.

### Changed
- The founder's welcome waits for the click on a password sign-up (`on_signup(welcome=False)`, then
  `on_email_verified`), with a new `email_verified` person event; every other sign-up gets it at once as before.

## [0.9.3] — 2026-10-27

### Changed
- The founder's welcome email comes from `WELCOME_FROM` (default "Kenneth at CommitLayer
  <hello@commitlayer.ai>") rather than the no-reply address; `send_email` takes a per-message sender.

## Operations

- 2026-10-27 · MASTER_KEY rotated (RUNBOOKS §7, the no-downtime path): nine tenant data keys re-wrapped,
  verified under the new key alone, fallback removed. Reason: the hosting provider's variable exposure.

## [0.9.2] — 2026-10-26

### Fixed
- The first-and-second-verdict milestones (0.9.0) counted every stored verification on each settled verdict:
  1.7 s per verification on a busy workspace. The query now stops after three rows.

## [0.9.1] — 2026-10-26

Sync lag that names its connection (Sentinel incident, 2026-10-26).

### Changed
- `sync_lag_seconds` no longer counts a connection with no stored credential or one waiting on
  re-authentication: the daily sync cannot move its watermark until someone acts, and `connections_in_error`
  already says so. Counting them turned a customer's expired token into a permanent platform alert.
  `sync_lag_seconds.excluded` reports how many were left out and why.

### Added
- `stalest_connections` on `GET /v1/admin/metrics`: the five oldest counted connections with tenant, source,
  name and age. `GET /v1/admin/connections`: every live connection of a real workspace, stalest first, with
  the exclusion reason, credential presence and health message. Connections tab on the admin page.

## [0.9.0] — 2026-10-26

Sign-up, activation and billing told to the systems that act on them (Growth OS attribution and lifecycle email).

### Added
- `app/ops/lifecycle.py`. A new workspace fires `user_signed_up` on the person (so the anonymous visitor the
  site identified becomes this user), queues the founder's welcome email (with `WELCOME_REPLY_TO`), and queues
  a `lifecycle_contact` job that posts the person id, workspace id, email, plan and consent source to Growth OS
  at `/v1/lifecycle/contacts` (`GROWTH_OS_URL`, `GROWTH_OS_TOKEN`; a 4xx other than 429 is final). A held
  sign-up fires the event but no welcome until it is approved.
- Milestones on the workspace owner's person: `first_action_recorded`, `first_verdict_received {verdict}` and
  `second_verification_completed` (settled verdicts of the workspace's own actions, sandbox excluded),
  `first_connector_connected {source}`, `checkout_started {tier, mode, founding}`, and from the Stripe webhook
  `subscription_started {tier, from}` and `plan_upgraded {from, to}` (a downgrade is not reported).
- `send_email` takes a Reply-To.

## [0.8.10] — 2026-10-25

### Fixed
- Webhook deliveries failed on every attempt since 0.8.7: the pinned-address request passed the TLS name as
  an option the module-level httpx helper does not take. The request now goes through a client with the
  option on the request, and the test drives a real transport instead of a patched helper. Deliveries that
  died in the meantime are retried by the queue as usual; the dead ones are the webhook's failure count.

## [0.8.9] — 2026-10-25

### Fixed
- The Stripe webhook handler reads a subscription's period end from its items when the endpoint's API version
  (2025-03 and later) no longer puts it on the subscription; the live endpoint is on such a version, the sandbox
  one was not. Without it the billing page had no renewal date.

## [0.8.8] — 2026-10-23

Master-key rotation as a command, with no downtime (Kenneth, after the hosting provider's key exposure).

### Added
- `MASTER_KEY_PREVIOUS`: during a rotation, a tenant data key still wrapped under the retired master key
  unwraps through this fallback, so the new key can be deployed before anything is re-wrapped.
- `ledger rotate-master-key [--no-backup]`: backup, then re-wrap every tenant data key under `MASTER_KEY`;
  idempotent; refuses to write anything when a key unwraps under neither value. `ledger verify-master-key`:
  every key unwraps under `MASTER_KEY` alone and one connection's credentials per tenant decrypt (exit 1
  while any key still needs the fallback). RUNBOOKS §7 rewritten around them; the hand-run script is gone.

## [0.8.7] — 2026-10-23

Security review follow-through (static audit of the deployed branch, rev. 2). Every finding verified against
the code before it was fixed; two were partly wrong and are noted below.

### Fixed
- **C1, cross-tenant SQL injection.** The `opportunity_reached_stage_within` primitive rendered the stage name
  into compiled SQL unescaped, and compiled contract SQL runs outside the ORM's tenant hook. The value now
  goes through the dialect escaper (`Dialect.str_`) and must be a plain name to compile at all.
- **H2, admin to owner.** Nobody assigns a role above their own (`403 role_above_own`) or changes or removes
  their own membership (`403 own_membership`); only an owner mints or demotes an owner. Accepting one's own
  invitation at the invited role still works.
- **H1, connector SSRF.** A connector's base URL from stored credentials passes the egress check (https, public
  name, public addresses) before any request; a hostile `Retry-After` is capped at sixty seconds.
- **NV-1, DNS rebinding on webhook delivery.** The delivery connects to the address the check resolved, with
  the original host for TLS and routing (`egress.pinned_post`).
- **M1 and M3, hosted sign-in.** The `state` is a signed ten-minute token, also set as a cookie on the API host;
  the callback requires the two to match, so a sign-in link made elsewhere goes nowhere (`auth.state_mismatch`
  audited). The in-process state dict, which grew without bound and broke across worker processes, is gone.
- **M4, token in the URL.** The callback redirect carries a one-time sixty-second code; `POST /v1/auth/handoff`
  trades it for the token server-side (`auth_handoffs`, hashed). The redirect also honours the `next=` the
  sign-in started with, which was lost before.
- **M8, token-type confusion.** `verify_session` accepts only a `session` token with a subject; another token
  the same key signs answers `401` instead of a `500`.
- **M2, open routes unthrottled.** `/v1/auth/*`, `/v1/public/*` and `/v1/oauth/*` are limited per client
  address (`ANON_RATE_PER_MINUTE`, default 60; `429` with `Retry-After`).
- **L4, sessions could not be revoked.** Sign-out stamps `users.sessions_revoked_at`; tokens issued before it
  are refused on REST and MCP.
- **L12.** Only an active membership grants access (REST and MCP resolution).
- **M5 and NV-2b (site).** One `safeInternalPath` guard for every `next` and back-link: `//host`, `/\host` and
  control characters fall back to the app. The site's callback route exchanges the code; the development login
  and impersonation hand their token to that route by POST instead of the URL.
- Migration 0014 (`users.sessions_revoked_at`, `auth_handoffs`).
- **H3, upload expansion.** Uploads are parsed off the event loop (a worker thread), with a row cap
  (200,000) and, for XLSX, a check of the declared uncompressed size (256 MiB) before the archive is opened.
- **H4, path traversal in object keys.** The stored name is the sanitised basename of the upload, and the local
  object store refuses any key with a `..` segment or an absolute path.
- **M10 and L9, parser by extension; 500s on bad input.** The format is checked by magic bytes; an unreadable
  file answers `400 unreadable_file`; an empty file is an empty table; a score, amount or unit price that is
  not a finite number is skipped rather than stored as `inf`/`nan` or crashing the ingest. The review said the
  crash came after the file was stored; it came before, so nothing was left behind, but the orphan on a later
  failure is real and the parse now precedes every write.
- **NV-2a.** The Parquet temp file is deleted and the DuckDB connection closed after every parse.
- **M7, dev-default secrets.** Outside `development` and `test`, the API refuses to start with a
  `SESSION_SECRET` or `HMAC_PEPPER` still at the dev default or under 32 characters, or an empty `MASTER_KEY`.
- **M9, dry-run without a bound.** A dry run is interrupted after `DRY_RUN_SECONDS` (default 10; `408
  dry_run_timeout`) and reads at most 50,000 rows.
- **L10.** The reference-number regex in transcript redaction no longer backtracks cubically on whitespace
  (3,000 spaces redact in milliseconds).
- **L11.** A client cannot set a connection's `role` or `kind`, so a free-form config no longer exempts a
  connection from the plan's count.
- **L1 and L7.** `/openapi.json` is off in production; `/health` is `status`, `version`, `engine_version` and
  `db` (`ok` or `error`) only; the integrations map and the database error text moved to `GET
  /v1/admin/metrics`.
- **L2.** The container runs as an unprivileged user: the entrypoint, still root, hands the data volume to
  `app` and drops privileges with `setpriv` before starting the server.
- **M6.** `python-multipart>=0.0.18` and `starlette>=0.47.2` as floors (the image already carried newer).
- **L3 (site).** The published image is built with `NEXT_PUBLIC_AUTH_MODE=workos`.
- Not done, on purpose: L8 (the benchmark SDKs stay in the image because the admin benchmark runs in
  production), L5 (a nonce-based script-src for the site needs middleware work and is a follow-up), and a
  lockfile for Python dependencies (follow-up).

## [0.8.6] — 2026-10-23

The sign-up screen: a mail server, a reputation, a country (Kenneth: stop automated sign-ups, hold Pakistan for
review, show every refusal on the admin page).

### Added
- `app/core/signupguard.py`: four checks on an address about to create a NEW account, cheapest first.
  Throwaway domain (as before); a domain with no mail server (`no_mail_server`); an address or network reported
  for sign-up spam at StopForumSpam, asked with the MD5 of the address and the public client address so the
  address never leaves the process (`reputation`, at `SIGNUP_REPUTATION_MIN_CONFIDENCE`); and the client's
  country from a local GeoLite2 database against `SIGNUP_COUNTRY_POLICY` (`IN=block,PK=hold`). A check that
  cannot complete lets the person through and logs it. An existing account is never screened.
- `hold`: the workspace is created with status `held`; sign-in and every product route answer `403
  account_held` ("waiting for a review") until an operator approves it. `signup_refusals` records every refusal
  and hold.
- Admin: `GET /v1/admin/signup-refusals` with the screen's state (policy, allow list, country database
  presence and age), `POST /{id}/approve` (active, owner emailed, `signup.approved`), `POST /{id}/reject`
  (suspended, `signup.rejected`). Sign-ups tab on the admin page.
- `refresh_geoip` job: fetches the GeoLite2 country database with `MAXMIND_LICENSE_KEY` on boot when missing
  and weekly after; unpacked to a temporary name and renamed over, so a reader never sees a half file.
- Migration 0013 (`signup_refusals`). Refused rows are purged after 90 days, decided ones after a year.

### Changed
- Sign-in page error codes: `no_mail_server`, `reputation`, `region_closed`, `account_held` join
  `disposable_email`.

## [0.8.5] — 2026-10-21

Transcript assist waits for the first invoice (infrastructure cost review: a Scale trial could spend its whole
model cap before any card was charged).

### Changed
- Transcript assist, the one model call in the product, cannot be switched on until the workspace's first
  invoice with money on it has cleared (`PATCH /v1/settings/transcript-assist` with `enabled: true` answers
  `402 payment_required`, details `{waiting_for: "first_invoice"}`); the pipeline refuses to label before
  that even if the switch was on. A workspace provisioned by hand (status `manual`) and the synthetic demo are
  exempt. `GET` carries `available` and `waiting_for`.
- The default monthly cost cap drops from $25 to $5. Migration 0012 moves a cap still at the old default; a cap
  someone set by hand is left alone.
- `invoice.paid` with `amount_paid > 0` records `tenants.first_paid_at` once, audited as
  `billing.first_invoice_paid`. The trial's $0 invoice does not count.

## [0.8.4] — 2026-10-20

Webhooks that say what is wrong and mend themselves (Kenneth: "fix this once and for all").

### Added
- A webhook now carries its own health: `last_error` in words that name the fix ("the receiver rejected the
  signature; rotate the secret here and set the new value there", "nothing is listening at this URL"),
  `last_response_code`, `last_attempt_at`, `last_delivered_at`, `disabled_at`. Shown on the settings page.
- `POST /v1/settings/webhooks/{id}/test` delivers now and reports the receiver's answer in the same call, so a
  wrong secret shows in one click; a success re-enables a disabled webhook.
- `POST /v1/settings/webhooks/{id}/rotate-secret`: a new signing secret shown once, the old one retired, the
  failure count reset. No more deleting and recreating a webhook (and losing its id) to get a secret.
- `POST /v1/settings/webhooks/{id}/enable`: re-enable by hand.

### Changed
- Disabling is no longer a dead end. Twenty consecutive failures still disable a webhook (audited,
  `webhook.disabled`, with the reason on the webhook), but one delivery an hour is still tried, and any success
  re-enables it (`webhook.recovered`). A receiver fixed next week comes back on its own.

## [0.8.3] — 2026-10-20

### Changed
- Dead engine jobs are archived, not deleted (Kenneth, on 0.8.2: keep them for the record). After thirty days a
  dead verification, run or statement job gets `archived_at`: it leaves the queue's counts, the status page and
  the admin metrics, and the admin job list shows it only with `include_archived=true`; a retry un-archives it.
  Dead outbound deliveries stay on the seven-day hard delete, since each delivery is on record in
  `webhook_deliveries` with its response and error. A deleted workspace takes its jobs, archived included;
  before this its jobs were the one table tenant deletion did not clear.

## [0.8.2] — 2026-10-20

### Changed
- Dead jobs no longer accumulate forever. A nightly `purge_dead_jobs` keeps a dead outbound delivery (webhook,
  integration, alert, email) for seven days and any other dead job for thirty; succeeded jobs are untouched.
  Migration `0009` removes the dead webhook-delivery jobs the Sentinel storm left, once, on deploy. Applied
  migrations are now logged with the rows they changed.

## [0.8.1] — 2026-10-20

What the Sentinel webhook storm exposed, fixed. (The settlement delay itself was on the Sentinel side: its
synthetic journey dated facts before the action it then created; see the GTM log.)

### Fixed
- MCP: every call wrote the key's `last_used_at`, so every model-facing read was a writer on the one SQLite lock,
  and a lock it lost answered the model a 500. Touches are now throttled to `API_KEY_TOUCH_SECONDS` and a busy
  database never fails the call, as on REST since 0.5.2. Same for OAuth tokens.
- Webhooks: a receiver answering 401 or 403 has refused the signature, which retrying cannot change. That
  delivery is now dead at once with a message naming the fix (the secret on the receiving side), instead of five
  attempts; the next event tries again and twenty refusals still disable the webhook. Delivery failures are one
  log line, not a traceback per attempt (a receiver that was down pushed the log past the platform's rate limit).
- Job queue: per-workspace concurrency is now per lane. Outbound deliveries (webhooks, integrations, alerts,
  email) wait on someone else's server for up to fifteen seconds each, and a flood of them held a workspace's
  settlement re-checks behind them; the engine lane has its own cap and is claimed first.

## [0.8.0] — 2026-10-19

Bot and input hardening, asked for by Kenneth: honeypots that ban, throwaway emails refused, markup kept out of
names, ceilings on input. Surveying for it found two holes worth more than the features.

### Security
- `POST /v1/auth/signup` created a user and a workspace for any address in production, with no proof the caller
  held it (only the session token was withheld), so a script could mint workspaces in bulk. It is now
  development-only, like the development login; production accounts come from the hosted sign-in.
- The client address was the leftmost `X-Forwarded-For`, which the caller writes. An IP allowlist could be passed
  by naming an allowed address, the audit log recorded a chosen address, and a ban keyed on it could have been
  aimed at an innocent customer. The address is now the one Railway's edge sets in `X-Real-IP`
  (`TRUSTED_IP_HEADER`); allowlist entries are validated and normalised.
- Validation errors no longer echo the offending input (a 4 MB body came back as a 4 MB error; a markup payload
  was reflected verbatim) and no longer crash into a 500 when a custom rule rejects a value.

### Added
- Bot traps that ban: scanner paths (`/.env`, `/wp-login.php`, `/.git/config` and their kind) answer 404 and ban
  the address; `/robots.txt` disallows a path nothing links to, and a crawler that goes there is banned; the lead
  and walkthrough forms carry a hidden honeypot field, and a submission that fills it is answered as a success,
  stored nowhere, and banned. A ban lasts `BAN_HOURS` (24), lives in the database so every API process honours it,
  never applies to a non-public address (the proxy, health checks) or to `BAN_EXEMPT_IPS`, and is refused with
  `403 blocked`. Platform admins list and lift bans at `GET` / `DELETE /v1/admin/bans`.
- `GET /v1/public/whoami`: the address the API sees, the one an allowlist is checked against.
- Throwaway email domains refused (`400 disposable_email`) where a new account, an invite or a lead would be
  created: the vendored CC0 list of 9,913 domains, subdomains included, adjustable with
  `DISPOSABLE_EMAIL_ALLOW` / `DISPOSABLE_EMAIL_EXTRA`. An existing account keeps signing in whatever its address.
  The hosted sign-in sends a refused address back to the sign-in page with the reason.
- Request body ceilings, enforced as the bytes arrive: 64 KiB on unauthenticated routes, 50 MiB for uploads
  (which used to read a whole file before checking it), 4 MiB elsewhere; `413 payload_too_large`.
- Every request string capped (20,000 characters unless the field names its own limit) through one shared base
  model, so a model written tomorrow is capped too. Names of workspaces, members, keys, connections and companies
  refuse `<`, `>` and control characters; free text drops control characters, including bidirectional overrides.
- Security headers: `Content-Security-Policy: default-src 'none'` on JSON, a no-script policy on the HTML
  exports, `X-Frame-Options: DENY`, and HSTS in production (security assessment item 7). On the site: HSTS,
  Permissions-Policy and the parts of a content security policy that cannot break it.

### Fixed
- The sign-in page printed any `?error=` text verbatim, so a link could put someone else's sentence on it. Only
  known error codes are shown now.

## [0.7.4] — 2026-10-19

### Added
- `METRICS_READ_TOKEN`: a bearer token that `GET /v1/admin/metrics` accepts in addition to a platform-admin
  session, for an external monitor (Sentinel) that needs the aggregate platform metrics around the clock.
  Read-only, one route, constant-time compare; a token sent anywhere else fails as an invalid session. Unset,
  nothing changes. Authored on the Sentinel branch (b1472cb), reviewed and merged here.

## [0.7.3] — 2026-10-19

### Fixed
- Public status page, again: a webhook receiver answering 404 produced a dead delivery job every minute and the
  engine read **degraded** within the hour of 0.7.2. Outbound deliveries (webhooks, integrations, alerts,
  email) die on someone else's endpoint and no longer count toward the engine's health; connector syncs
  already did not. What counts: verification, runs, statements, reconciliation, exports, housekeeping.
- Webhook URLs are normalised on save: repeated slashes in the path are collapsed, so a base pasted with a
  trailing slash plus a path with a leading one no longer calls `host//v1/...` on every delivery.

## [0.7.2] — 2026-10-19

### Fixed
- Public status page: "Verification engine" read **degraded** since 2026-10-16 because the check counted
  dead-lettered jobs for all time, and twelve nights of one customer's rejected Zendesk token had produced
  twelve of them. Degraded now means engine job types (not connector syncs) dead within the last 24 hours.
  Found by the Sentinel monitor's first live observation.

## [0.7.1] — 2026-10-19

### Fixed
- Shopify connector: a token with `read_orders` but not `read_customers` made Shopify answer `ACCESS_DENIED` on
  the `customer { email }` field, which failed the whole sync and read as a credential problem. The connector
  now sends the orders query again without the field, finishes the sync with `customer_hid` null (what the
  connector doc always promised), and notes the missing scope in the connection's health. Sync health messages
  carry a connector's notes in general. Found while preparing the live Shopify test.

## [0.7.0] — 2026-10-19

Pricing follow-through from an external review Kenneth brought (docs/GTM.md, 2026-10-19). Headline prices stay;
allowances rise; free usage gets a boundary; a founding offer; support only from Team up.

### Changed
- Allowances: Builder 5,000 (was 2,500), Team 25,000 (was 10,000), Growth 100,000 (was 50,000); Scale stays at
  250,000 until the database move. Overage 2.5¢ / 1.5¢ / 1¢ / 1¢ (was 4 / 3 / 2 / 1): a rate that never rises
  when you upgrade. Migration `0007` moves a stored entitlement still on the old figure to the new one and leaves
  a hand-set figure alone.
- Developer's grace past the allowance is bounded: 72 hours or 250 actions, whichever first. After it a new
  action answers `402 allowance_exceeded` and is not recorded (API, inline verify and OTLP alike); re-recording
  an existing action, verifying by id, dry runs and the settlement re-checks of recorded actions continue. Paid
  plans are unchanged: never refused, overage billed.
- Support is a line on every plan: none below Team ("Docs and self-serve; no support included"), email support
  on Team, priority on Growth and Scale, dedicated on Enterprise.

### Added
- Per-workspace rate limits by plan: `requests` a minute on every authenticated call and `writes` a minute on
  recording, verifying and pushing facts, as in-process token buckets; `429 rate_limited` with `Retry-After`,
  nothing applied. Developer fair-use caps a day: 250 recorded actions (counted from the database) and 5,000
  pushed facts; `429 fair_use_exceeded`. Figures in `app/billing/pricing.py` and on the public pricing
  endpoint; `RATE_LIMITS=false` only in tests.
- Founding Builder: Builder at $59 a month for 12 months for the first 50 workspaces, a Stripe coupon
  (`founding-builder`) created on first use with `max_redemptions` as the cap. Every Builder checkout or switch
  carries it while seats remain; the public pricing endpoint reports the seats left from Stripe's
  `times_redeemed` and shows nothing when Stripe is not configured or cannot be read. A coupon Stripe refuses
  between the read and the checkout falls back to the list price in the same call.
- `GET /v1/public/pricing` carries `support`, `rate_limits` and `fair_use` per tier, `grace_actions`, the
  `founding` offer, `included_everywhere` and the `compare` table the pricing page renders, so the page and the
  limits have one source.

## [0.6.3] — 2026-10-16

Connections that fail now say what happened and stop retrying what cannot change. From the live Zendesk and
Salesforce tests.

### Changed
- Provider errors are one line a person can act on: the host, the status, the endpoint without its query
  string, and the provider's own message (Salesforce `errorCode: message`, Zendesk `title: message`). The
  full URL and the link to a status-code encyclopedia are gone from connection health and sync history.
- A credential the provider rejects (401/403) is a terminal failure: the sync job is dead after one attempt
  with its error, instead of failing five times over half an hour; the connection reads "needs
  re-authentication"; the nightly sweep skips it until someone re-authenticates.
- Re-authenticating an API-key connection queues a sync immediately, so the fix shows within a minute.
- Connection page (web): the health banner distinguishes a rejected credential from a failed sync and carries
  the fix; Re-authorize opens a form with the connector's own credential fields instead of a dead button; the
  sync history groups consecutive identical outcomes ("failed × 5, 03:00 to 03:08"), clamps errors to one
  line with a details toggle, and shows ten groups with "show all".

## [0.6.2] — 2026-10-16

### Fixed
- Salesforce connector: the Opportunity query asked for `Contact.Email`, a field Opportunity does not have, so
  the first live sync against a real org failed with `400 INVALID_FIELD` and stored nothing (the Case part had
  already run). The customer on an opportunity is its primary `OpportunityContactRole`; the query now reads that
  role's `Contact.Email` through a subquery and hashes it as before. Found on the first connection to a
  Developer Edition org; a recorded-shape sync test now covers both queries, reopen detection and the watermark.

## [0.6.1] — 2026-10-16

### Changed
- The benchmark harness runs with one credential per side of the line: `--recorder-key` records the agent's
  actions, `--evidence-key` (bound to the sandbox's six sources) pushes the facts, and the full `--api-key` only
  creates the sandbox contracts and asks for verdicts. With a full key alone everything still runs and the
  result says `credentials: single`. The SDK READMEs (Python, JavaScript, Go, Java), the action-contract
  walkthrough and the site's code samples show a recorder client and an evidence client.

## [0.6.0] — 2026-10-16

Evidence provenance (item 1 of `docs/security-assessment.md`): the credential that records an action never
also supplies the facts that verify it.

### Changed
- New scope `facts:write`. `POST /v1/facts`, and inline facts on `POST /v1/actions/verify` outside a dry run,
  require it; `actions:record` no longer implies it. Recording (`POST /v1/actions`, an inline action on
  verify) requires `actions:record`. A key without the scope answers `403 insufficient_scope`.
- **Existing `recorder` keys can no longer push facts.** Keys that hold every scope (`full`, and keys created
  before scopes existed) are unchanged.
- Presets renamed to say which side of the line a key is on: `recorder` (record and read), `evidence`
  (push facts and watermarks, read verdicts and watermarks), `reader` (every read scope plus validate and
  draft; the old `evidence`), `full`. `GET /v1/settings/api-keys` returns `preset_descriptions`.
- A key with `facts:write` is bound to the sources it may push for (`fact_sources` on creation, required
  unless the key holds every scope). A fact or watermark for another source answers `403 source_not_allowed`.
- A source a live connector serves (its own name and everything it provides) cannot be pushed through any
  key: `409 source_served_by_connector`. A pushed fact can never stand in for what the connector reports.
- Model-facing principals (OAuth tokens and bearers on the MCP server) never hold `facts:write`.
- Contracts: `min_trust: self_reported | shared | independent` on an expected effect. A fact from a source
  below that level does not satisfy the effect; the check reports it as observed but not ok with the trust
  seen, the reason code is `effect_below_trust:<source>.<type>`, and the headline says why. Trust is read
  live (the connection's level, or the operator's per-source override on the facts-API connection), so a
  reclassification applies to the next verification. Default: no gate; existing contracts are unchanged.


Hardening from the security assessment in `docs/security-assessment.md` (items 2 to 6 of its priority list).

### Changed
- A recorded action is immutable. `POST /v1/actions` with an existing `action_id` is a no-op when it describes
  the same action and answers `409 conflict` when any of contract, subject, parameters, workflow, agent, type,
  channel, requested time or `compensates` differs. Until now the second call silently rewrote the stored
  action. A change of mind is a new action, or a compensating one.
- Future-dated inputs are refused: a fact's `occurred_at`, a declared watermark or an action's `requested_at`
  further ahead than `MAX_CLOCK_SKEW_SECONDS` (900) answers 400. Server ingestion time was already stored apart
  (`created_at`) and is unchanged by the caller.
- Every CSV export (statements, dispute packages, delta reports, the audit log) neutralises cells that a
  spreadsheet would read as a formula (`=`, `+`, `-`, `@`, tab, carriage return) by prefixing an apostrophe.
- Webhook URLs are checked when saved and again at every delivery: https only, no credentials in the URL, no
  `localhost` or `.internal`-style names, and the host must resolve only to publicly routable addresses
  (`EGRESS_GUARD`, on by default). A delivery whose URL now points inside a network fails without a request.
- Contract YAML is refused above `MAX_CONTRACT_BYTES` (128 KiB), with more than 16 anchors or 32 aliases, deeper
  than 32 levels or above 50,000 values, before anything expands. The loader was already the safe one.


### Changed
- The evidence freeze. When an action settles, the check table, headline, sources, facts, settlement and
  reversal are stored with the verdict (`verification_snapshots`, one row per settled run) and a read of the
  action returns them as they were, with `evidence_frozen_at`. Until now a read re-ran both lenses against the
  facts currently stored, so a FAIL that settled because an effect never arrived in time showed that effect as
  seen once it arrived late, next to a verdict that said the opposite. Facts recorded after settlement are
  listed apart in `facts_after_settlement`. A source's trust label and the action's resolution stay live.
  Pending actions and verdicts stored before this release are still evaluated fresh on read. Reading a settled
  action no longer runs the engine (it was a third of read CPU in the load profile).
- The boot-time demo seed refreshes the sample's watermarks before it re-verifies, so a settled sample never
  freezes a picture of its sources weeks behind.

## [0.5.2] — 2026-10-15

Phase C of the load plan: the re-test after 0.5.1 showed the list and summary reads as the slow path and
fifty writers polling the busy timeout against each other.

### Fixed
- `GET /v1/actions` and `GET /v1/actions/summary` loaded up to 2000 actions and ran three queries per action
  (1.7 s at ten readers, 18 s with writers present). The list now cuts the page in SQL and evaluates only that
  page; with a `verdict` or `workflow_id` filter the newest 2000 are evaluated in bulk. Latest verdicts load in
  one query per page or summary instead of one per action.
- A recorded action whose verification failed and whose recovery job could not be queued (both lost the write
  lock) stayed unverified: the recovery enqueue retries briefly, and the worker sweeps for recorded actions
  with no verdict older than `UNVERIFIED_ACTION_SWEEP_SECONDS` (60) once a minute and queues their verification.
- The worker no longer logs a traceback when the queue claim or a job loses the write lock under load; a lock
  loss is a warning and the job's own retry schedule handles it.

### Changed
- Write gate: at most `MAX_CONCURRENT_WRITES` (8) requests are on the write path (`POST /v1/actions`,
  `POST /v1/facts`) at once; a writer that finds no slot within `WRITE_SLOT_TIMEOUT_SECONDS` (2) answers 503
  `busy` with `Retry-After: 1` without touching the database. The verdict commit happens while the slot is held.
- `UVICORN_WORKERS` (2): the API runs as two processes. The load test showed reads CPU-bound in one Python
  process (12 ms of SQL in a 1.1 s list at ten readers); two processes served 85 requests per second where one
  served 19, and 33 writes per second where one served 18. The job worker still runs once, in the parent.
- Load test after this release, same script, local 4 vCPU box, two processes: ten readers p50 list 113 ms,
  summary 107 ms, single action 163 ms; ten writers 33 req/s, p50 197 ms, p95 0.9 s, no errors; fifty writers
  28 req/s, 870 of 887 recorded, 17 fast 503s, zero lock errors, zero tracebacks, every recorded action verified.
  Remaining CPU: the summary is linear in the month's action count (about 0.1 ms per action, 16 s at fifty
  concurrent dashboard refreshes over 2400 actions) and `GET /v1/actions/{ref}` re-runs the engine to rebuild
  the check table (a third of read CPU). Both are listed as the next items.

## [0.5.1] — 2026-10-15

Overload behaviour, after a load test and a fact-check of its review (docs/GTM.md log).

### Fixed
- A write that lost the SQLite lock could answer 201 and store nothing: the request session committed after the
  response was sent (FastAPI's request-scoped dependency). Every route now commits before the response goes out.
- `POST /v1/actions` records the claim in one short transaction, verifies outside any write transaction, and
  commits the verdicts in a second; if verification fails after the claim is in, a `verify_action` job picks it
  up. No action is recorded-but-never-verified.
- Two in-flight retries of one `action_id` could create two claims: `(tenant_id, raw_ref)` is now unique and the
  second insert takes the update path.
- API-key requests no longer write `last_used_at` on every call (that one line was most of the lock failures);
  it is refreshed at most every `API_KEY_TOUCH_SECONDS` (300), in its own short transaction. API-key reads of an
  action no longer write an audit row; a person viewing evidence is still audited.
- Slack alert jobs are queued only when a Slack channel is configured for the event. A job whose final status
  update lost the lock stayed "running" and the per-tenant cap held the tenant's queue; the status update is now
  its own retried transaction after the handler's work commits.

### Changed
- Overload is a 503 with `Retry-After: 1` and error code `busy`, never a 500: the write lock waits at most
  `SQLITE_BUSY_TIMEOUT_MS` (3000, was 10000), the connection pool matches the threadpool (40 + 24, 5 s timeout,
  was 5 + 10 with 30 s), and uvicorn admits at most `MAX_IN_FLIGHT_REQUESTS` (64) at once. Requests slower than
  `SLOW_REQUEST_MS` (1000) log one line with statement count, SQL time and the slowest statement.
- Parsed contracts are cached by YAML digest (C loader) and compiled contracts by digest, dialect and sources;
  a request resolves its contract once instead of three times.
- Nightly `optimize_database` job: ANALYZE, PRAGMA optimize and a passive WAL checkpoint.

## [0.5.0] — 2026-10-15

### Added
- Shopify connector (`shopify`, facts only): orders changed since the watermark through the Admin GraphQL API
  become `order_status` state facts and `order_cancelled`, `refund_created` and `fulfillment_created` event facts,
  keyed by the numeric order id; customer email hashed; throttling honoured. The flagship refund contract names
  Shopify as the order system and until now it could only be fed through the facts API. `docs/connectors/shopify.md`.
- The completion gate: `GET /v1/actions/{ref}/wait?timeout=` holds the request until the verdict settles or the
  timeout passes (60 seconds per call), evaluating the evidence that arrived and storing the settled verification
  once; `wait_for_settlement` / `waitForSettlement` in the Python and JS SDKs loop over it. Audit
  `action.awaited`. Documented as the pattern for a workflow that must not report success before the outcome is
  proven.
- Competitive landscape verified against each product's own site or docs, in `docs/GTM.md` section 6.
- Temporal and Inngest integrations for Python (`integrations/python`, distribution `commitlayer-integrations`):
  a Temporal activity interceptor that records a mapped activity as an action before it runs (idempotent on
  `temporal:{workflow_id}:{activity_id}`, fail-open) and a `commitlayer_verify` activity that waits on the
  completion gate with heartbeats; an Inngest `recorded_step` helper that records inside the memoised step and a
  `wait_for_verdict` step loop. The activity or step result is never sent. 61 tests against fakes; CI runs them.
- The same for TypeScript (`integrations/js`, `@commitlayer/integrations` with `temporal` and `inngest` subpath
  exports): an activity interceptor factory, a `commitlayerVerify` activity, `recordedStep` and `waitForVerdict`.
  34 tests. Both integrations wait through the completion gate rather than polling plain reads.
- SDKs: `contract_version` / `contractVersion` on record, and a public `request(method, path, body)` for any
  endpoint.

### Fixed
- The synthetic demo's connections never sync, so their fixed watermarks aged and every public sample reported
  its sources as weeks behind at settlement. Each boot now moves the demo watermarks to the current time; the
  showcase verdicts are unchanged, only the lag they reported.

## [0.4.0] — 2026-10-15

### Added
- CommitLayer over MCP (`docs/mcp.md`). The API serves an MCP server at `/mcp` (streamable HTTP, stateless, JSON
  responses) with fifteen tools that read actions, verdicts, reasons and evidence pointers, contracts, coverage,
  reconciliations and usage, validate, dry-run and draft contracts, and, with the `actions:record` scope, record
  an action; two resources (`commitlayer://reason-codes`, `commitlayer://actions/{ref}`) and two prompts. Nothing
  on it writes facts, activates contracts, overrides verdicts or deletes. Every call is audited as
  `mcp.tool_called`; plan gates answer with the plan that unlocks the tool.
- Scopes on API keys (`actions:read`, `actions:record`, `contracts:read`, `contracts:validate`, `contracts:draft`,
  `reconcile:read`, `connectors:read`, `usage:read`) with three presets: full, evidence (read-only) and runtime
  recorder. Over REST a key without `actions:record` acts as a viewer. Keys created earlier keep every scope.
- OAuth 2.1 authorization server in the API for remote MCP clients (ChatGPT, Claude): metadata documents,
  dynamic client registration, `/authorize` with PKCE handing off to the web app's consent page, `/token` with
  code exchange and refresh rotation, `/revoke`. Tokens are hashed, bound to the member and the workspace they
  approved, capped to the member's current role, and end when the member leaves. Consent endpoints
  `GET/POST /v1/oauth/consent`; audit `oauth.approved`, `oauth.denied`.
- The MCP recorder, `recorder/python` (`commitlayer-mcp-recorder`) and `recorder/js` (`@commitlayer/mcp-recorder`):
  a stdio JSON-RPC proxy that re-exports any downstream MCP server and records a mapped `tools/call` as an action
  (`POST /v1/actions`, `channel: mcp`) before forwarding it. Mapping file per tool (contract, subject path,
  params, action_id template, compensates); unmapped tools pass through; fail-open by default; the tool's own
  result is never sent. Standard library only, tested against a fake MCP server and a fake API.

### Changed
- `mcp` SDK dependency (pinned to the 1.x line; 2.x renamed FastMCP). New tables `oauth_clients`, `oauth_codes`, `oauth_tokens`; `api_keys.scopes` (migration
  `0004_api_key_scopes`).

## [0.3.0] — 2026-10-14

### Added
- Reconciliations as a first-class object (`/v1/reconciliations`): a vendor's claims for one period, keyed by
  contract and period like runs and statements. Creating one from a contract fixes the vendor, outcome type and
  subject kind; the view names which active connection covers each fact source the contract needs and the
  signals no source covers; the claims upload pins rows to the period and remembers the column mapping per
  vendor, so the next month needs no mapping; a one-off facts file can stand in for a system without a connector;
  the run, statement, exports, invoice and disputes are reached from the same view. New audit actions
  `reconciliation.*`.

### Changed
- Connections are live, read-only fact sources. File uploads happen inside a reconciliation; the connections
  upload endpoint stays for API clients but is no longer a connector in the app.
- The Zendesk connector is facts-only. Its claims path read an "automated resolutions" endpoint Zendesk does not
  provide; the automated-resolutions report is a download and is uploaded as the reconciliation's claims. The
  connector is now displayed as "Zendesk" with kind `facts`.
- Contract summaries carry `subject_kind`.

### Fixed
- A required fact source is satisfied by a broader family that contains it, in the wizard's coverage and in the
  engine's `missing_fact_source` rule alike: a HubSpot connection (tickets, crm) covers a contract that needs
  conversations. Before, the wizard said "covers none of the required sources" and the engine would have marked
  every claim unverifiable.
- Intercom: conversations a teammate starts from the Inbox carry `source: null` and no contacts; the connector
  assumed a dict and every sync of such a workspace failed.
- A failed sync now leaves a failed sync row and the connection in error. Before, the job handler raised and the
  worker's rollback erased that state, so the connection sat on "syncing" with rows stuck "running". A new sync
  marks any row still running as superseded.
- A connection's watermark now advances to the sync time (less a five-minute safety margin, `SYNC_SAFETY_LAG_SECONDS`)
  on every successful sync. Before, it stayed at the newest record seen, so a quiet source such as a Stripe account
  with no refunds this week looked days behind: the admin lag alert fired and pending action verdicts would have
  waited on it for the full settlement bound.
- The admin sync-lag metric ignores synthetic (demo) workspaces.
- The source that holds a vendor's uploaded claims no longer counts against the plan's connection limit; imported
  facts files still do. A Builder workspace with Zendesk, Stripe and a vendor export was refused a third live
  connection.
- Re-uploading a vendor's export moves its rows to the contract, period and connection of the new upload. Before,
  a row first uploaded under another contract kept that contract on every later upload, so a reconciliation could
  report "2 claims ingested" and count zero. Claim ids from CSV uploads are now scoped to the vendor as well as the
  vendor's row id.

## [0.2.8] — 2026-10-13

### Fixed
- Overage is now actually billed. A monthly job (`bill_overages`, the 2nd of each month) invoices every paid
  workspace for the verified actions it recorded past its allowance in the previous month, at the plan's
  per-action rate, as a Stripe invoice item on the next subscription invoice; once per period, with an audit
  entry `billing.overage_billed`. Before this the allowance reported overage but nothing charged it.
- Draft contracts are never counted: the contract limit applies at selection, so a Developer workspace can hold
  any number of drafts and two active contracts, as the pricing page says.
- Viewers are never seats: the seat limit counts owners, admins and analysts, and a viewer invite is never
  refused for it.
- PagerDuty, Jira and GitHub Checks are gated on their own `incident_channels` feature (Growth and above) rather
  than on Slack alerts, which start at Builder.

## [0.2.7] — 2026-10-13

### Changed
- Plans: a Builder plan at $99 (2,500 verified actions, four connections, five contracts, Slack and the orphan
  report) sits between Developer and Team; Starter is renamed Team and rebalanced to 10,000 actions, eight
  connections and ten contracts (its `starter` id is kept for stored entitlements, Stripe metadata and trial
  links; `STRIPE_PRICE_BUILDER` is the new price setting). Developer gets two connections. Growth and Scale
  keep their prices with 15 and 30 connections and unlimited contracts. Overage is a straight ladder: 4, 3, 2 and
  1 cents per action. Every paid plan may connect any core system; the count is the only limit. Custom action and
  outcome types start at Growth, the audit log export at Team.

## [0.2.6] — 2026-10-13

### Changed
- Plans: OpenTelemetry ingestion is on every plan (the actions it records count toward the allowance like any
  other); Starter's two connections may be any core connector (Stripe, Salesforce, HubSpot, Zendesk, Intercom);
  the facts API and OpenTelemetry push endpoints no longer count as connections. A connector not in the plan
  answers 402 with the lowest plan that includes it. The docs now say plainly that recording and verifying
  actions never answer 402; only feature, connection, seat and contract limits do.

### Added
- COMPENSATED: an action recorded with `compensates: "<action_id>"` corrects an earlier one. The earlier action's
  verdict and hash never change; it carries `resolution` (`compensating`, then `compensated` once the correction
  settles PASS, or `compensation_failed`), its headline says so, `GET /v1/actions?verdict=COMPENSATED` lists it,
  the summary counts it and `action.compensated` is emitted. A compensation is itself verified.
- Incident and review channels under Settings → Integrations (Growth+): PagerDuty (incident on FAIL, warning on
  UNKNOWN, resolved on PASS or compensation), Jira Cloud (one issue per failing action, comments on later
  verdicts) and GitHub Checks (a check run on the commit an action names, in progress while pending, updated on
  re-verification). Credentials are envelope-encrypted; deliveries run in the `deliver_integration` job with
  retries, and twenty consecutive failures disable the channel until it is saved again.
- SDKs in TypeScript/JavaScript (`sdk/js`), Go (`sdk/go`) and Java (`sdk/java`) with the same surface as the
  Python SDK and its action wrapper, standard library only; all SDKs accept `compensates`.

## [0.2.5] — 2026-10-13

### Changed
- Settlement waits for evidence, not for the clock. A verdict that is not yet a PASS stays `pending` after its
  window while a source the contract reads has a watermark behind the window, bounded by `SETTLEMENT_MAX_LAG`
  (48h). The re-check comes forward when that source syncs or its watermark is pushed, and otherwise runs every
  `SETTLEMENT_RECHECK_INTERVAL` (15m). The view, the list and the summary carry `settlement.waiting_on`; past the
  bound the verdict settles and names its `stale_sources`. `POST /v1/facts` accepts `watermarks` per source.
- Every outbound notification about a verdict says whether it is pending or settled, why (`reasons`), what it
  waits on, the record that reversed it (`reversal`) and what it was before (`previous`, `changed`). A new
  `action.pending` event announces the first provisional verdict once; `action.verified` is unchanged in name.

### Added
- Connection trust: `independent`, `shared` or `self_reported` on every connection (migration 0003), reported
  per source and per fact in each verification and counted in `summary.evidence_trust`; `PATCH
  /v1/connections/{id}` sets it, per pushed source for the facts API. SECURITY.md §16 states the credential rule.
- Weakening warnings: `POST /v1/contracts/validate` and `POST /v1/contracts` compare the version with the one it
  replaces and return every removed check, raised bound, extended window or shortened negative lookback; a
  saved weaker version emits `contract.weakened` and is counted in the audit entry.
- `POST /v1/contracts/draft`: an action contract drafted from recorded actions and the facts observed around
  them, with the sample's shares and arrival times in the YAML comments. Nothing is saved.
- Python SDK (`sdk/python`, standard library only): `Client` with `record_action`, `push_facts` (watermarks),
  `verify`, `get_action`, `draft_contract`, `validate_contract`, `save_contract`, and an `@client.action`
  decorator that records the call as an action before it runs, fail-open by default.

## [0.2.4] — 2026-10-11

### Changed
- The monthly allowance no longer stops anything. `POST /v1/actions`, inline `POST /v1/actions/verify` and the
  OpenTelemetry endpoint used to answer `402` once the month's verified actions were used up, which made the
  verification layer itself the reason an action went unverified. Now every action is recorded and verified;
  the response and `GET /v1/auth/me` carry an `allowance` block (`within`, `overage` on paid plans at the plan's
  per-action rate, `grace` then `exceeded` on Developer), and the first action past the allowance is audited
  once as `allowance.exceeded`. `public_pricing` exposes `overage_per_action` per tier and the policy text.
- Changing plan is one flow with a preview: `GET /v1/settings/billing/preview?tier=` says whether the choice
  starts a trial (and when the first charge falls), is charged today because the trial is used, or switches an
  existing subscription in place; `POST /v1/settings/billing/checkout` carries it out, with the trial days Stripe
  is given derived from the workspace's own trial rather than a flat 14. `STRIPE_PRICE_<TIER>` accepts a product
  id as well as a price id. Stripe's refusals and configuration gaps come back as messages (400/502), not 500s.
- Enterprise no longer lists an optional recovery share on credited disputes: a fee tied to what the verifier
  finds is at odds with the independence policy. Fees are predictable and unrelated to any verdict.

### Added
- Off-site database backups: `BACKUP_BUCKET` names an S3-compatible bucket (reached with the `S3_*` credentials)
  that receives every nightly dump in addition to the copy in the object store; the job fails when the off-site
  copy cannot be written, both copies are kept 35 days, and the worker queues one backup at boot (once a day) so a
  newly configured bucket holds a copy at once. `/health` reports `integrations.offsite_backups`.
- Second factor for privileged accounts: at every hosted sign-in the API mirrors the user's WorkOS authenticator
  enrolment into `users.mfa_enabled` and records the sign-in method on the session. Members enrol an
  authenticator app from Settings (`POST /v1/auth/mfa/enroll` and `verify`, through the WorkOS factor APIs; the
  secret is shown once and never stored). With `REQUIRE_ADMIN_MFA=platform` (platform admins) or `admins`
  (workspace owners/admins too), sessions without a second factor get `403 mfa_required` on every product and
  admin route, while the session and enrolment endpoints still answer so the gate page can fix it in place;
  `MFA_DELEGATED_METHODS` (default `SSO`) lists methods whose identity provider enforces the factor.
  `GET /v1/auth/me` carries `mfa: {required, satisfied, method}`. `/health` reports `integrations.admin_mfa`.
- Sign-out ends the hosted session as well: the WorkOS session id travels in the session token (through workspace
  switches and impersonation) and `POST /v1/auth/logout` returns the provider's `logout_url`, which brings the
  browser back to the site. Before, sign-out cleared the cookie and landed on the sign-in page, which with a live
  provider session signed the person straight back in.

## [0.2.3] — 2026-10-09

### Added
- Three action-contract primitives that look beyond one action, each domain-free (source + type + attribute +
  window + number):
  - `aggregate_exceeds` in a new `action.aggregate_limits` block: the sum of an effect attribute, or the count of
    effects, across the actions in scope (per agent, per subject, or across the contract) over a lookback.
  - `effect_attr_differs` in `forbidden_effects`: an observed value must equal the action's parameter, or the latest
    state before the action plus or minus a parameter, within a tolerance. No reference, no violation.
  - `reversal_observed` in a new `action.reversed_by` block (default lookahead 30d): a later record that undoes the
    action fails both lenses, and pushing such a fact after settlement re-verifies the action (`verify_action` job,
    one per fact) and emits `action.verified` again.
- Verification responses carry two new check groups, `aggregate_limits` and `reversals`; measured values (totals,
  expected values) are part of the evidence and therefore of the verdict hash.
- Library contract `payout_workflow` showing all three, and three library contracts outside money that use the same
  checks: `deploy_workflow` (CI, monitoring, change records; a rollback is the reversal), `access_grant_workflow`
  (identity provider, HR, tickets; a removed membership or a departure is the reversal) and
  `appointment_reschedule_workflow` (scheduling, record system, messaging; a cancellation or no-show is the reversal).
- Agent sandbox: a sixth workflow, **send a payout**, against a fifth fake system (ledger). Besides the eleven
  injected failures it has five cases of its own: over limit, frozen account, per-agent daily total exceeded (three
  earlier payouts run first), paid amount differing from the request, and a payout returned after the verdict
  settled (re-verified to FAIL at the re-check). Sandbox agent ids are per run.
- Agent sandbox: a seventh workflow, **deploy a service**, against a sixth fake system (platform: services, deploys,
  health checks), with five cases of its own: degraded service, environment not allowed, more than three deploys
  an hour, unhealthy after the deploy, and a rollback after the verdict settled.

### Fixed
- Verified actions are metered on the Verify path: a new `action_id` on `POST /v1/actions` or a non-dry-run
  `POST /v1/actions/verify` counts against the plan's monthly allowance and answers `402` once it is used up;
  an OTel batch is refused once the month is used up. Re-recording an action, dry runs, verifying by id and sandbox
  runs are never metered.
- `GET /v1/actions/orphans` is gated to Growth and above (`orphan_report` feature), as the pricing page says. Flags
  added to pricing after a tenant was seeded now resolve from the plan's defaults.
- `GET /v1/public/sample-statement` returns the latest demo statement that has billed claims; the monthly schedule
  also writes an empty statement for a period with no claims, which has no verdict chain to show.

## [0.2.2] — 2026-09-29

### Added
- Agent sandbox for every workspace: `GET /v1/sandbox/scenarios`, `POST /v1/sandbox/runs` run a scripted scenario
  inside the workspace (namespaced records, `channel: sandbox`, never metered). `sandbox.harness.run_scenario` is
  the shared core used by the benchmark and the endpoint.
- `GET /v1/overview` gains `verify_onboarding`: progress through the guided first-verdict flow (sample action,
  API key, first own action, action contract chosen).
- Hosted sign-in is locked to production (`DEV_LOGIN_KEY`), `GET /v1/auth/providers`, and sign-up creates the
  workspace on the plan picked on the pricing page (Developer by default).
- `links` on `POST /v1/facts` and `POST /v1/actions/verify`: two record ids that are the same case (a support ticket
  and its order). Effects on a linked subject count for the action's subject; linked subjects are never orphans.
- `docs/benchmark.md`: three real agents (Claude, GPT, Gemini) against the sandbox failure matrix, with a
  trace-only judge as the baseline; founder-only runner `POST /v1/admin/benchmark`.

### Changed
- Pricing (`app/billing/pricing.py`): plans are sized by verified actions per month (a billed outcome in Reconcile
  counts as one). New free **Developer** tier (1,000 actions, 2 contracts, facts API, webhook, no trial, no card)
  for engineers adopting action contracts; Starter gains a second connection and three contracts; Growth ten
  contracts and the orphan-effect report; Enterprise adds committed-use pricing and a shadow-mode enforcement
  preview. Tier ids and limit keys are unchanged, so stored entitlements keep working. The public pricing payload
  gains `unit`, and per-tier `for` and `free`.
- Benchmark replay (`--replay`, `replay_of`) re-verifies a recorded run on the current engine without model calls.

### Fixed
- A notification or CRM note keyed to the ticket instead of the order was reported as a missing effect and as an
  orphan (found by the gpt-5-mini benchmark run).

## [0.2.1] — 2026-09-28

### Added
- `sandbox/`: an artificial enterprise (payments, orders, CRM, tickets), failure injection, a scripted agent, five
  action contracts and a 62-scenario failure matrix scored against a database oracle (`docs/sandbox.md`).
- `GET /v1/actions/orphans?contract_id=&period=`: effects observed on subjects with no recorded action.

### Changed
- Action contracts no longer fall back to `no_structured_signal` (UNKNOWN); an asserted action with no observed
  effects is a FAIL naming every missing effect.

## [0.2.0] — 2026-09-28

### Added
- **Action contracts** (`kind: action`): an `action:` block (`window`, `preconditions`, `limits`, `expected_effects`, `forbidden_effects`, `unverifiable_if`) compiled into the two engine lenses, presented as *outcome* and *policy* with PASS / FAIL / UNKNOWN labels. New primitives `effect_observed`, `effect_count_exceeds`, `effect_attr_exceeds`, `state_before_action` / `precondition_unmet`, `param_exceeds` (`param_max`), `param_not_in` (`param_in`); reason codes that name the effect or parameter (`expected_effect_missing:<source>.<type>`, `duplicate_effect:…`, `forbidden_effect_observed:…`, `precondition_unmet:…`, `limit_exceeded:<param>`, `param_not_allowed:<param>`). Reference: `docs/action-contracts.md`.
- Synchronous action verification (`app/engine/actions.py`): actions stored as claims, single-claim SQL scope in the compiler, both lenses evaluated with evidence and hash chain under a Run with `triggered_by=action`; settlement window with `pending` status and a `verify_action` job that re-checks at `settles_at`; webhook/Slack event `action.verified`.
- API: `POST /v1/actions`, `POST /v1/facts`, `POST /v1/actions/verify` (inline action and facts, `dry_run`), `GET /v1/actions` (filters `contract_id`, `workflow_id`, `verdict`, `period`), `GET /v1/actions/summary`, `GET /v1/actions/{action_ref}`, public `GET /v1/public/sample-action`. Public definitions expose `kind`, `lens_labels`, `action_type`, `window` and per-section rule lists for action contracts.
- Library contract `refund_workflow` (internal refund policy template), synthetic demo actions including the showcase FAIL case `act_demo_refund_001`, and `tests/test_actions.py`.
- CLI: `ledger actions verify --tenant <slug|id> --file body.json [--dry-run]`.
- Migration `0002_contract_kind`: `contracts.kind` column and `raw_ref` indexes on claims and facts.

### Changed
- Product renamed **CommitLayer** (`commitlayer.ai`), positioned as the verification layer for enterprise AI actions; outcome-billing verification (statements, reconciliation, disputes) is presented as the **CommitLayer Reconcile** module. Placeholders replaced across code, contracts and docs; `{{LEGAL_ENTITY}}` remains.
- `available_sources` now includes connection source names and the systems that pushed facts through the API, so `missing_fact_source` can name systems directly (`stripe`, `shopify`).
- Docs: `docs/API.md` (Actions section), `METHODOLOGY.md` (section 13, action contracts), `README.md` (positioning, quickstart, CLI), `DECISIONS.md` (D18, D19), `docs/TRUST_CENTER.md`.

## [0.1.0] — 2026-09-11

### Added
- Outcome Contract DSL (YAML → AST → SQL) with 20 primitives, SQLite and DuckDB dialects, golden fixtures and property tests.
- Verification engine: two lenses, fixed evaluation order, evidence pointers, per-statement hash chain, deterministic statements, coverage disclosure.
- Starter contract library with cited vendor definitions: Intercom Fin (resolution, procedure handoff), Zendesk AI agents, Sierra, Decagon, Salesforce Agentforce, HubSpot Breeze customer agent, generic CSV, internal OTel.
- Statements (HTML/PDF/CSV/JSON), public hash verification, definition delta reports.
- Invoice import (CSV/XLSX/PDF with mandatory review), reconciliation, dispute packages with status tracking.
- Transcript assist: redaction, env-configured provider, cost caps, calibration sampling, 30-day excerpt purge, statement disclosure.
- Connectors: Intercom, Zendesk, Salesforce, HubSpot, Stripe, generic uploads, OTLP/HTTP ingestion; envelope-encrypted credentials; HMAC-hashed identifiers.
- Multi-tenant API with roles, entitlements driven by a single pricing config, API keys, webhooks, Slack, audit log, counterparty surface, admin surface.
- SQLite job queue + worker + schedules (daily syncs, monthly statements, purges, nightly backups).
- Synthetic demo tenant, `ledger` CLI, Docker image, Compose, Railway config, CI.
