atmn 2.0.40 → 2.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -985,7 +985,7 @@ var API_ROUTES = [
985
985
  name: "proration_behavior",
986
986
  type: "string",
987
987
  required: false,
988
- description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
988
+ description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
989
989
  },
990
990
  {
991
991
  name: "redirect_mode",
@@ -1192,7 +1192,7 @@ var API_ROUTES = [
1192
1192
  name: "billing_behavior",
1193
1193
  type: "string",
1194
1194
  required: false,
1195
- description: "Whether to prorate the immediate phase. 'none' skips proration charges and credits."
1195
+ description: "Whether to prorate the immediate phase. 'none' skips proration charges and credits, 'bill_difference' charges/credits the full-period price difference."
1196
1196
  },
1197
1197
  {
1198
1198
  name: "no_billing_changes",
@@ -1291,7 +1291,7 @@ var API_ROUTES = [
1291
1291
  name: "billing_behavior",
1292
1292
  type: "string",
1293
1293
  required: false,
1294
- description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything."
1294
+ description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
1295
1295
  },
1296
1296
  {
1297
1297
  name: "billing_cycle_anchor",
@@ -1400,7 +1400,7 @@ var API_ROUTES = [
1400
1400
  name: "proration_behavior",
1401
1401
  type: "string",
1402
1402
  required: false,
1403
- description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
1403
+ description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
1404
1404
  },
1405
1405
  {
1406
1406
  name: "redirect_mode",
@@ -1601,7 +1601,7 @@ var API_ROUTES = [
1601
1601
  name: "billing_behavior",
1602
1602
  type: "string",
1603
1603
  required: false,
1604
- description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything."
1604
+ description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
1605
1605
  },
1606
1606
  {
1607
1607
  name: "billing_cycle_anchor",
@@ -1710,7 +1710,7 @@ var API_ROUTES = [
1710
1710
  name: "proration_behavior",
1711
1711
  type: "string",
1712
1712
  required: false,
1713
- description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
1713
+ description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
1714
1714
  },
1715
1715
  {
1716
1716
  name: "redirect_mode",
@@ -1851,7 +1851,7 @@ var API_ROUTES = [
1851
1851
  name: "proration_behavior",
1852
1852
  type: "string",
1853
1853
  required: false,
1854
- description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
1854
+ description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
1855
1855
  },
1856
1856
  {
1857
1857
  name: "redirect_mode",
@@ -2091,7 +2091,7 @@ var API_ROUTES = [
2091
2091
  name: "proration_behavior",
2092
2092
  type: "string",
2093
2093
  required: false,
2094
- description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
2094
+ description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
2095
2095
  },
2096
2096
  {
2097
2097
  name: "subscription_id",
@@ -2209,6 +2209,33 @@ var API_ROUTES = [
2209
2209
  }
2210
2210
  ]
2211
2211
  },
2212
+ {
2213
+ group: "billing",
2214
+ method: "verify",
2215
+ path: "/v1/billing.verify",
2216
+ description: "Checks a customer's Stripe subscriptions against Autumn's record of their plans and reports any drift. Read-only: it never changes Autumn or Stripe.",
2217
+ body: "object",
2218
+ fields: [
2219
+ {
2220
+ name: "customer_id",
2221
+ type: "string",
2222
+ required: true,
2223
+ description: "Autumn customer whose Stripe subscriptions should be checked against Autumn's customer_products."
2224
+ },
2225
+ {
2226
+ name: "subscription_ids",
2227
+ type: "json",
2228
+ required: false,
2229
+ description: "Optional whitelist of Stripe subscription IDs to verify. Defaults to every subscription linked to the customer's plans, plus any other active Stripe subscription on the customer, which is reported as not linked to Autumn."
2230
+ },
2231
+ {
2232
+ name: "strict",
2233
+ type: "boolean",
2234
+ required: false,
2235
+ description: "When true, report missing usage-based items and unexpected metered Stripe items. Defaults to false."
2236
+ }
2237
+ ]
2238
+ },
2212
2239
  {
2213
2240
  group: "billing",
2214
2241
  method: "import",
@@ -2698,7 +2725,7 @@ var API_ROUTES = [
2698
2725
  group: "balances",
2699
2726
  method: "batch_track",
2700
2727
  path: "/v1/balances.batch_track",
2701
- description: "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 202 immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 202 and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
2728
+ description: "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 200 (or 202) immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 200 (or 202) and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
2702
2729
  body: "array",
2703
2730
  fields: []
2704
2731
  },
@@ -3876,8 +3903,8 @@ var API_ROUTES = [
3876
3903
  {
3877
3904
  name: "events",
3878
3905
  type: "json",
3879
- required: true,
3880
- description: "The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events."
3906
+ required: false,
3907
+ description: "The events sent to this webhook. Leave it out to send every event. `vercel.*` events can't be mixed with other events."
3881
3908
  },
3882
3909
  {
3883
3910
  name: "description",
@@ -3939,7 +3966,7 @@ var API_ROUTES = [
3939
3966
  name: "events",
3940
3967
  type: "json",
3941
3968
  required: false,
3942
- description: "The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events."
3969
+ description: "The events sent to this webhook. Pass `[]` to send every event; omit it to keep the current list. `vercel.*` events can't be mixed with other events."
3943
3970
  },
3944
3971
  {
3945
3972
  name: "description",
@@ -6218,15 +6245,12 @@ var LINT_RULES = {
6218
6245
  webhooks: {
6219
6246
  label: "webhook",
6220
6247
  idField: "id",
6221
- required: ["events", "id", "url"],
6248
+ required: ["id", "url"],
6222
6249
  fields: {
6223
6250
  id: {
6224
6251
  minLength: 1,
6225
6252
  maxLength: 256,
6226
6253
  pattern: "^[a-zA-Z0-9_-]+$"
6227
- },
6228
- events: {
6229
- minItems: 1
6230
6254
  }
6231
6255
  },
6232
6256
  rules: [
@@ -7297,7 +7321,7 @@ var createClient = (options) => ({
7297
7321
  });
7298
7322
 
7299
7323
  // src/version.ts
7300
- var version = "2.0.40";
7324
+ var version = "2.0.42";
7301
7325
 
7302
7326
  // src/http/autumnFetch.ts
7303
7327
  var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
@@ -9436,7 +9460,7 @@ var SYNCED_LISTS = {
9436
9460
  idField: "id",
9437
9461
  responseIdField: "id",
9438
9462
  keys: ["id", "url", "events", "description", "disabled"],
9439
- required: ["events", "id", "url"],
9463
+ required: ["id", "url"],
9440
9464
  paths: ["description", "disabled", "events", "id", "url"],
9441
9465
  pull: true,
9442
9466
  envKeyed: ["url"]
@@ -9554,16 +9578,16 @@ import { dirname as dirname4, join as join7 } from "node:path";
9554
9578
  import chalk3 from "chalk";
9555
9579
 
9556
9580
  // src/generated/skills.ts
9557
- var SKILLS_VERSION = "2.0.40";
9581
+ var SKILLS_VERSION = "2.0.42";
9558
9582
  var SKILLS = [
9559
9583
  {
9560
9584
  name: "autumn-setup",
9561
9585
  description: "First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.",
9562
- version: "2.0.40",
9586
+ version: "2.0.42",
9563
9587
  markdown: `---
9564
9588
  name: autumn-setup
9565
9589
  description: First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.
9566
- version: 2.0.40
9590
+ version: 2.0.42
9567
9591
  ---
9568
9592
 
9569
9593
  # Setup
@@ -9721,11 +9745,11 @@ Then stop. Don't keep building, don't tour the dashboard, don't deploy anything.
9721
9745
  {
9722
9746
  name: "autumn-catalog",
9723
9747
  description: "Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.",
9724
- version: "2.0.40",
9748
+ version: "2.0.42",
9725
9749
  markdown: `---
9726
9750
  name: autumn-catalog
9727
9751
  description: Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.
9728
- version: 2.0.40
9752
+ version: 2.0.42
9729
9753
  ---
9730
9754
 
9731
9755
  # Catalog
@@ -13592,7 +13616,7 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
13592
13616
  },
13593
13617
  {
13594
13618
  path: "references/atmn.md",
13595
- contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means \"don't register this webhook there\", which is not an error:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: \"billing\",\n events: [\"billing.updated\", \"balances.limit_reached\"],\n url: {\n live: \"https://myapp.com/api/autumn\", // atmn push -p\n sandbox: \"https://staging.myapp.com/api/autumn\", // the default sandbox\n \"qa-team\": \"https://qa.myapp.com/api/autumn\", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work. `events` needs at least one entry.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that's rejected or belongs to another org skips its env with a warning and leaves that env's keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env's key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn't let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
13619
+ contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means \"don't register this webhook there\", which is not an error:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: \"billing\",\n events: [\"billing.updated\", \"balances.limit_reached\"],\n url: {\n live: \"https://myapp.com/api/autumn\", // atmn push -p\n sandbox: \"https://staging.myapp.com/api/autumn\", // the default sandbox\n \"qa-team\": \"https://qa.myapp.com/api/autumn\", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work.\n- Leave `events` out to receive every event. Don't write `events: []`.\n- Vercel webhooks require their own distinct webhook and are a mutually exclusive variant to the rest. It can only listen to Vercel events and cannot later be changed to listen to non-Vercel events.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that's rejected or belongs to another org skips its env with a warning and leaves that env's keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env's key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn't let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
13596
13620
  },
13597
13621
  {
13598
13622
  path: "references/catalog-update.md",
@@ -13607,11 +13631,11 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
13607
13631
  {
13608
13632
  name: "autumn-integrate",
13609
13633
  description: "Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.",
13610
- version: "2.0.40",
13634
+ version: "2.0.42",
13611
13635
  markdown: `---
13612
13636
  name: autumn-integrate
13613
13637
  description: Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.
13614
- version: 2.0.40
13638
+ version: 2.0.42
13615
13639
  ---
13616
13640
 
13617
13641
  # Integrate
@@ -18213,11 +18237,11 @@ Monitor your logs for this message. If you see it, check [status.useautumn.com](
18213
18237
  {
18214
18238
  name: "autumn-concepts",
18215
18239
  description: "Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.",
18216
- version: "2.0.40",
18240
+ version: "2.0.42",
18217
18241
  markdown: `---
18218
18242
  name: autumn-concepts
18219
18243
  description: Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.
18220
- version: 2.0.40
18244
+ version: 2.0.42
18221
18245
  ---
18222
18246
 
18223
18247
  # Concepts
@@ -22537,7 +22561,7 @@ var fixtureRowOf = ({
22537
22561
  }) => ({
22538
22562
  id: webhook.id,
22539
22563
  url: { [envKey]: webhook.url },
22540
- events: webhook.events,
22564
+ ...webhook.events.length > 0 ? { events: webhook.events } : {},
22541
22565
  ...webhook.description ? { description: webhook.description } : {},
22542
22566
  ...webhook.disabled ? { disabled: true } : {}
22543
22567
  });
@@ -22606,7 +22630,10 @@ var fieldAssignments = ({
22606
22630
  stated: stated2
22607
22631
  }) => {
22608
22632
  const assignments = [];
22609
- if (webhook.events.length > 0 && !sameEvents(stated2.events, webhook.events))
22633
+ if (webhook.events.length === 0) {
22634
+ if (stated2.events !== undefined)
22635
+ assignments.push({ path: ["events"], text: null });
22636
+ } else if (!sameEvents(stated2.events, webhook.events))
22610
22637
  assignments.push({
22611
22638
  path: ["events"],
22612
22639
  text: `[${webhook.events.map((event) => JSON.stringify(event)).join(", ")}]`
@@ -22645,8 +22672,6 @@ var updateWebhook = ({
22645
22672
  envKey
22646
22673
  }) => {
22647
22674
  const result = { lines: [], warnings: [], unlocated: [] };
22648
- if (webhook.events.length === 0 && Array.isArray(stated2.events))
22649
- result.warnings.push(`⚠ ${webhook.id} the dashboard webhook at this url receives every event; your next push narrows it to ${stated2.events.join(", ")}`);
22650
22675
  const located = locateWebhook({ pull, id: webhook.id });
22651
22676
  if (located === null) {
22652
22677
  result.unlocated.push({
@@ -22802,7 +22827,7 @@ var statedFrom = ({
22802
22827
  ...row,
22803
22828
  id: webhook.id,
22804
22829
  url: { ...row?.url, [envKey]: webhook.url },
22805
- events: webhook.events.length > 0 ? webhook.events : row?.events,
22830
+ events: webhook.events.length > 0 ? webhook.events : undefined,
22806
22831
  description: webhook.description,
22807
22832
  disabled: webhook.disabled
22808
22833
  });
@@ -22842,10 +22867,6 @@ var applyWebhooksPull = ({
22842
22867
  }));
22843
22868
  continue;
22844
22869
  }
22845
- if (webhook.events.length === 0) {
22846
- result.lines.push(`· webhook ${webhook.id} receives every event; give it an event list in the dashboard, or add it to your config, to manage it here`);
22847
- continue;
22848
- }
22849
22870
  const id = newId();
22850
22871
  const failure2 = appendWebhook({
22851
22872
  pull,
@@ -23021,7 +23042,7 @@ var webhooksHaveWork = ({
23021
23042
  webhooks
23022
23043
  }) => (webhooks?.changes ?? []).some((change) => change.action !== "unmanaged");
23023
23044
  var DETAIL_INDENT = " ";
23024
- var list2 = (values) => values.length === 0 ? "(none)" : values.join(", ");
23045
+ var list2 = (values) => values.length === 0 ? "every event" : values.join(", ");
23025
23046
  var updateDetails = ({
23026
23047
  id,
23027
23048
  before,
package/dist/index.js CHANGED
@@ -2234,15 +2234,12 @@ var LINT_RULES = {
2234
2234
  webhooks: {
2235
2235
  label: "webhook",
2236
2236
  idField: "id",
2237
- required: ["events", "id", "url"],
2237
+ required: ["id", "url"],
2238
2238
  fields: {
2239
2239
  id: {
2240
2240
  minLength: 1,
2241
2241
  maxLength: 256,
2242
2242
  pattern: "^[a-zA-Z0-9_-]+$"
2243
- },
2244
- events: {
2245
- minItems: 1
2246
2243
  }
2247
2244
  },
2248
2245
  rules: [
@@ -1 +1 @@
1
- {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/readWebhookEnvs.ts","../src/actions/pull/webhooks/removeWebhookEnv.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/pull/webhooks/webhookPullEnvs.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/isDashboardWebhook.ts","../src/actions/webhooks/newWebhookId.ts","../src/actions/webhooks/previewWebhooks.ts","../src/actions/webhooks/resolveWebhookEnv.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPullEnvs.test.ts","../test/webhooks/webhooksPush.test.ts"]}
1
+ {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/readWebhookEnvs.ts","../src/actions/pull/webhooks/removeWebhookEnv.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/pull/webhooks/webhookPullEnvs.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/isDashboardWebhook.ts","../src/actions/webhooks/newWebhookId.ts","../src/actions/webhooks/previewWebhooks.ts","../src/actions/webhooks/resolveWebhookEnv.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPullEnvs.test.ts","../test/webhooks/webhooksPush.test.ts","../test/webhooks/webhooksRender.test.ts"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmn",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
4
4
  "license": "MIT",
5
5
  "description": "The CLI for Autumn — define your pricing in code, sync it to Autumn, and keep everything in version control.",
6
6
  "bin": {
@@ -49,5 +49,5 @@
49
49
  "open": "10.2.0",
50
50
  "yaml": "2.9.0"
51
51
  },
52
- "gitHead": "24f8e6c3eabbd41819e7056bb6a4e570455a2a70"
52
+ "gitHead": "5dd16854ac3848701a74f14bddf6fe15a1c19ef6"
53
53
  }
@@ -23,7 +23,8 @@ const fixtureRowOf = ({
23
23
  }): Record<string, unknown> => ({
24
24
  id: webhook.id,
25
25
  url: { [envKey]: webhook.url },
26
- events: webhook.events,
26
+ // No list is every event; `events` is left out rather than written as `[]`.
27
+ ...(webhook.events.length > 0 ? { events: webhook.events } : {}),
27
28
  ...(webhook.description ? { description: webhook.description } : {}),
28
29
  ...(webhook.disabled ? { disabled: true } : {}),
29
30
  });
@@ -30,7 +30,7 @@ const statedFrom = ({
30
30
  ...row,
31
31
  id: webhook.id,
32
32
  url: { ...row?.url, [envKey]: webhook.url },
33
- events: webhook.events.length > 0 ? webhook.events : row?.events,
33
+ events: webhook.events.length > 0 ? webhook.events : undefined,
34
34
  description: webhook.description,
35
35
  disabled: webhook.disabled,
36
36
  });
@@ -110,12 +110,6 @@ export const applyWebhooksPull = ({
110
110
  );
111
111
  continue;
112
112
  }
113
- if (webhook.events.length === 0) {
114
- result.lines.push(
115
- `· webhook ${webhook.id} receives every event; give it an event list in the dashboard, or add it to your config, to manage it here`,
116
- );
117
- continue;
118
- }
119
113
  const id = newId();
120
114
  const failure = appendWebhook({
121
115
  pull,
@@ -25,8 +25,11 @@ const fieldAssignments = ({
25
25
  stated: StatedWebhook;
26
26
  }): Assignment[] => {
27
27
  const assignments: Assignment[] = [];
28
- // `[]` is a dashboard webhook receiving every event: not a list a config can state.
29
- if (webhook.events.length > 0 && !sameEvents(stated.events, webhook.events))
28
+ // No list is every event, which a config states by leaving `events` out.
29
+ if (webhook.events.length === 0) {
30
+ if (stated.events !== undefined)
31
+ assignments.push({ path: ["events"], text: null });
32
+ } else if (!sameEvents(stated.events, webhook.events))
30
33
  assignments.push({
31
34
  path: ["events"],
32
35
  text: `[${webhook.events.map((event) => JSON.stringify(event)).join(", ")}]`,
@@ -82,10 +85,6 @@ export const updateWebhook = ({
82
85
  envKey: string;
83
86
  }): WebhookEditResult => {
84
87
  const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
85
- if (webhook.events.length === 0 && Array.isArray(stated.events))
86
- result.warnings.push(
87
- `⚠ ${webhook.id} the dashboard webhook at this url receives every event; your next push narrows it to ${stated.events.join(", ")}`,
88
- );
89
88
  const located = locateWebhook({ pull, id: webhook.id });
90
89
  if (located === null) {
91
90
  result.unlocated.push({
@@ -955,7 +955,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
955
955
  type: "string",
956
956
  required: false,
957
957
  description:
958
- "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges.",
958
+ "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
959
959
  },
960
960
  {
961
961
  name: "redirect_mode",
@@ -1190,7 +1190,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1190
1190
  type: "string",
1191
1191
  required: false,
1192
1192
  description:
1193
- "Whether to prorate the immediate phase. 'none' skips proration charges and credits.",
1193
+ "Whether to prorate the immediate phase. 'none' skips proration charges and credits, 'bill_difference' charges/credits the full-period price difference.",
1194
1194
  },
1195
1195
  {
1196
1196
  name: "no_billing_changes",
@@ -1300,7 +1300,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1300
1300
  type: "string",
1301
1301
  required: false,
1302
1302
  description:
1303
- "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything.",
1303
+ "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
1304
1304
  },
1305
1305
  {
1306
1306
  name: "billing_cycle_anchor",
@@ -1420,7 +1420,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1420
1420
  type: "string",
1421
1421
  required: false,
1422
1422
  description:
1423
- "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges.",
1423
+ "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
1424
1424
  },
1425
1425
  {
1426
1426
  name: "redirect_mode",
@@ -1648,7 +1648,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1648
1648
  type: "string",
1649
1649
  required: false,
1650
1650
  description:
1651
- "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything.",
1651
+ "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
1652
1652
  },
1653
1653
  {
1654
1654
  name: "billing_cycle_anchor",
@@ -1769,7 +1769,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1769
1769
  type: "string",
1770
1770
  required: false,
1771
1771
  description:
1772
- "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges.",
1772
+ "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
1773
1773
  },
1774
1774
  {
1775
1775
  name: "redirect_mode",
@@ -1929,7 +1929,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
1929
1929
  type: "string",
1930
1930
  required: false,
1931
1931
  description:
1932
- "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges.",
1932
+ "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
1933
1933
  },
1934
1934
  {
1935
1935
  name: "redirect_mode",
@@ -2198,7 +2198,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
2198
2198
  type: "string",
2199
2199
  required: false,
2200
2200
  description:
2201
- "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges.",
2201
+ "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle.",
2202
2202
  },
2203
2203
  {
2204
2204
  name: "subscription_id",
@@ -2332,6 +2332,37 @@ export const API_ROUTES: readonly ApiRoute[] = [
2332
2332
  },
2333
2333
  ],
2334
2334
  },
2335
+ {
2336
+ group: "billing",
2337
+ method: "verify",
2338
+ path: "/v1/billing.verify",
2339
+ description:
2340
+ "Checks a customer's Stripe subscriptions against Autumn's record of their plans and reports any drift. Read-only: it never changes Autumn or Stripe.",
2341
+ body: "object",
2342
+ fields: [
2343
+ {
2344
+ name: "customer_id",
2345
+ type: "string",
2346
+ required: true,
2347
+ description:
2348
+ "Autumn customer whose Stripe subscriptions should be checked against Autumn's customer_products.",
2349
+ },
2350
+ {
2351
+ name: "subscription_ids",
2352
+ type: "json",
2353
+ required: false,
2354
+ description:
2355
+ "Optional whitelist of Stripe subscription IDs to verify. Defaults to every subscription linked to the customer's plans, plus any other active Stripe subscription on the customer, which is reported as not linked to Autumn.",
2356
+ },
2357
+ {
2358
+ name: "strict",
2359
+ type: "boolean",
2360
+ required: false,
2361
+ description:
2362
+ "When true, report missing usage-based items and unexpected metered Stripe items. Defaults to false.",
2363
+ },
2364
+ ],
2365
+ },
2335
2366
  {
2336
2367
  group: "billing",
2337
2368
  method: "import",
@@ -2873,7 +2904,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
2873
2904
  method: "batch_track",
2874
2905
  path: "/v1/balances.batch_track",
2875
2906
  description:
2876
- "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 202 immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 202 and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
2907
+ "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 200 (or 202) immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 200 (or 202) and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
2877
2908
  body: "array",
2878
2909
  fields: [],
2879
2910
  },
@@ -4140,9 +4171,9 @@ export const API_ROUTES: readonly ApiRoute[] = [
4140
4171
  {
4141
4172
  name: "events",
4142
4173
  type: "json",
4143
- required: true,
4174
+ required: false,
4144
4175
  description:
4145
- "The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events.",
4176
+ "The events sent to this webhook. Leave it out to send every event. `vercel.*` events can't be mixed with other events.",
4146
4177
  },
4147
4178
  {
4148
4179
  name: "description",
@@ -4211,7 +4242,7 @@ export const API_ROUTES: readonly ApiRoute[] = [
4211
4242
  type: "json",
4212
4243
  required: false,
4213
4244
  description:
4214
- "The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events.",
4245
+ "The events sent to this webhook. Pass `[]` to send every event; omit it to keep the current list. `vercel.*` events can't be mixed with other events.",
4215
4246
  },
4216
4247
  {
4217
4248
  name: "description",
@@ -97,8 +97,8 @@ webhooks: Array<{
97
97
  id: string;
98
98
  /** The https URL Autumn sends events to. Localhost and private-network addresses are rejected; tunnels such as ngrok work. */
99
99
  url: string;
100
- /** The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events. */
101
- events: Array<"customer.products.updated" | "customer.threshold_reached" | "balances.usage_alert_triggered" | "balances.limit_reached" | "billing.auto_topup_failed" | "billing.auto_topup_succeeded" | "billing.updated" | "invoice.finalized" | "vercel.resources.deleted" | "vercel.resources.provisioned" | "vercel.resources.rotate_secrets" | "vercel.webhooks.event">;
100
+ /** The events sent to this webhook. Leave it out to send every event. `vercel.*` events can't be mixed with other events. Defaults to []. */
101
+ events?: Array<"customer.products.updated" | "customer.threshold_reached" | "balances.usage_alert_triggered" | "balances.limit_reached" | "billing.auto_topup_failed" | "billing.auto_topup_succeeded" | "billing.updated" | "invoice.finalized" | "vercel.resources.deleted" | "vercel.resources.provisioned" | "vercel.resources.rotate_secrets" | "vercel.webhooks.event">;
102
102
  /** A note for your own reference. */
103
103
  description?: string;
104
104
  /** When true, no events are sent to the webhook. */
@@ -52515,7 +52515,7 @@ id: string;
52515
52515
  url: string;
52516
52516
  /** A note for your own reference. */
52517
52517
  description: string | null;
52518
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52518
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52519
52519
  events: Array<string>;
52520
52520
  /** When true, no events are sent to the webhook. */
52521
52521
  disabled: boolean;
@@ -52537,7 +52537,7 @@ id: string;
52537
52537
  url: string;
52538
52538
  /** A note for your own reference. */
52539
52539
  description: string | null;
52540
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52540
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52541
52541
  events: Array<string>;
52542
52542
  /** When true, no events are sent to the webhook. */
52543
52543
  disabled: boolean;
@@ -52556,7 +52556,7 @@ id: string;
52556
52556
  url: string;
52557
52557
  /** A note for your own reference. */
52558
52558
  description: string | null;
52559
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52559
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52560
52560
  events: Array<string>;
52561
52561
  /** When true, no events are sent to the webhook. */
52562
52562
  disabled: boolean;
@@ -52572,7 +52572,7 @@ id: string;
52572
52572
  url: string;
52573
52573
  /** A note for your own reference. */
52574
52574
  description: string | null;
52575
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52575
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52576
52576
  events: Array<string>;
52577
52577
  /** When true, no events are sent to the webhook. */
52578
52578
  disabled: boolean;
@@ -52591,7 +52591,7 @@ id: string;
52591
52591
  url: string;
52592
52592
  /** A note for your own reference. */
52593
52593
  description: string | null;
52594
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52594
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52595
52595
  events: Array<string>;
52596
52596
  /** When true, no events are sent to the webhook. */
52597
52597
  disabled: boolean;
@@ -52607,7 +52607,7 @@ id: string;
52607
52607
  url: string;
52608
52608
  /** A note for your own reference. */
52609
52609
  description: string | null;
52610
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52610
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52611
52611
  events: Array<string>;
52612
52612
  /** When true, no events are sent to the webhook. */
52613
52613
  disabled: boolean;
@@ -52626,7 +52626,7 @@ id: string;
52626
52626
  url: string;
52627
52627
  /** A note for your own reference. */
52628
52628
  description: string | null;
52629
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52629
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52630
52630
  events: Array<string>;
52631
52631
  /** When true, no events are sent to the webhook. */
52632
52632
  disabled: boolean;
@@ -52651,7 +52651,7 @@ id: string;
52651
52651
  url: string;
52652
52652
  /** A note for your own reference. */
52653
52653
  description: string | null;
52654
- /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty only for a webhook made in the dashboard that receives every event. */
52654
+ /** The events sent to this webhook, as `WebhookEventType` names; a type newer than your client is returned as-is. Empty means the webhook receives every event. */
52655
52655
  events: Array<string>;
52656
52656
  /** When true, no events are sent to the webhook. */
52657
52657
  disabled: boolean;
@@ -1217,7 +1217,7 @@ export const SYNCED_LISTS: Readonly<Record<string, SyncedListSpec>> = {
1217
1217
  idField: "id",
1218
1218
  responseIdField: "id",
1219
1219
  keys: ["id", "url", "events", "description", "disabled"],
1220
- required: ["events", "id", "url"],
1220
+ required: ["id", "url"],
1221
1221
  paths: ["description", "disabled", "events", "id", "url"],
1222
1222
  pull: true,
1223
1223
  envKeyed: ["url"],
@@ -2206,16 +2206,13 @@ export const LINT_RULES: LintRules = {
2206
2206
  webhooks: {
2207
2207
  label: "webhook",
2208
2208
  idField: "id",
2209
- required: ["events", "id", "url"],
2209
+ required: ["id", "url"],
2210
2210
  fields: {
2211
2211
  id: {
2212
2212
  minLength: 1,
2213
2213
  maxLength: 256,
2214
2214
  pattern: "^[a-zA-Z0-9_-]+$",
2215
2215
  },
2216
- events: {
2217
- minItems: 1,
2218
- },
2219
2216
  },
2220
2217
  rules: [
2221
2218
  {
@@ -14,16 +14,16 @@ export type BundledSkill = {
14
14
  }[];
15
15
  };
16
16
 
17
- export const SKILLS_VERSION = "2.0.40";
17
+ export const SKILLS_VERSION = "2.0.42";
18
18
 
19
19
  export const SKILLS: readonly BundledSkill[] = [
20
20
  {
21
21
  name: "autumn-setup",
22
22
  description:
23
23
  "First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.",
24
- version: "2.0.40",
24
+ version: "2.0.42",
25
25
  markdown:
26
- "---\nname: autumn-setup\ndescription: First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.\nversion: 2.0.40\n---\n\n# Setup\n\nTake the user from \"I want billing\" to pricing that is live in a sandbox org and working in their app — with as little between those two points as possible. You run the flow; two other skills do the heavy parts. `autumn-catalog` turns their pricing into `autumn.config.ts`. `autumn-integrate` puts the first calls in their code. Never do either of those jobs yourself.\n\n## Ground rules\n\nThese apply the whole time, not just in one phase.\n\n- CLI-first: everything happens in `autumn.config.ts` and `atmn`. The only browser moments are signing in, or claiming a keyless org later. Never send the user to the dashboard to do the work.\n- Never invent a price, limit, or plan name. A missing number is a question, never a guess.\n- Push only after the user approves the pricing (Phase 4), or when they already told you to go ahead without a review.\n- Sandbox by default: `AUTUMN_SECRET_KEY` is the sandbox key. Don't touch production during setup.\n- Keys: check that a key exists by its name only. Never read, print, or ask the user to paste a key into the chat. Same for a keyless org's claim token. A one-time email code is not a key — that one does come through the chat.\n- Two tries max to fix any failing command, then stop and show the error.\n- Never run `atmn reset`, and never remove existing plans unless the user clearly asked.\n- If a step is already done, say so in one line (\"Already signed in — skipping login\") and move on.\n\n## How to talk\n\nYou are a competent engineer pairing with the user, not an installer wizard and not a marketer.\n\n- Simple, everyday words: \"plans\", \"what's included\", \"extra usage\". No jargon in chat — schema words like `consumable`, `prepaid`, `usage_based` stay in the config. If a simpler word says the same thing, use it.\n- 1–3 sentences per message. The pricing summary is the only large thing you send.\n- One message, one purpose: a status, a question, or the pricing summary.\n- Don't ask permission for harmless work — reading the repo, drafting the config, building the summary. Ask only for decisions and approvals.\n- Say what's about to happen before it does: one line before the browser opens, one before the push.\n- When you need input, ask at most three short numbered questions — the ones that unblock you, nothing more. If your platform has a built-in way to ask questions with options, use it. Never re-ask something they answered.\n- Always end a message with something: the question, what you're doing next, or that you're done.\n- Don't paste the config or command output into chat; name the file and summarize. Errors are the exception — quote those exactly.\n- If you're stuck, send three lines: what failed, the exact error, what you need to continue.\n- No emoji, no hype, no \"Great question\". Plain and concrete: \"Connected to Acme (sandbox).\"\n\n## Progress\n\nCopy this checklist into your first message and keep it up to date. If you skip an item, say why in one line.\n\n- [ ] 1 Skills installed; checked for an existing config, key, and pricing\n- [ ] 2 Connected to Autumn — `atmn` installed, signed in or keyless\n- [ ] 3 Got the user's pricing — rough plans and prices to build from\n- [ ] 4 Pricing modeled and approved (the `autumn-catalog` skill runs this part)\n- [ ] 5 Pushed to Autumn and verified\n- [ ] 6 Working in the app — one plan bought, one feature gated (the `autumn-integrate` skill runs this part)\n- [ ] 7 Account linked (keyless only — drop this line if they signed in)\n- [ ] 8 Done\n\nFor items 4 and 6, another skill owns the conversation and its checklist replaces this one for the duration — show theirs, not this one. Come back here when they're finished.\n\n## Phase 1 — Check the project (silent)\n\nDon't message the user yet — just find out where things stand.\n\nFour skills share this job, and they ship inside the `atmn` CLI: `atmn init` writes them next to the config and prints the `npx skills add <that folder> -y` line that registers them with your agent, and `atmn skills update` refreshes them when `push` or `pull` say they are older than the CLI. If any is missing here, that is Phase 2's job — never fetch them from anywhere else.\n\n`autumn-setup` (this file) is the flow. `autumn-catalog` is how to build the pricing, plus the exact `atmn` commands — load it in Phase 4. `autumn-integrate` is how the app calls Autumn — load it in Phase 6. `autumn-concepts` explains Autumn's objects — the other two load it themselves.\n\nThen check three things:\n\n- `autumn.config.ts` exists → Autumn is already set up here. Say so, treat the file as the truth, and use `autumn-catalog` for the changes; come back at Phase 5 (push) when there's something to push.\n- An `AUTUMN_SECRET_KEY` exists (shell env, `.env`, `.env.local`) → already connected; skip the connect step in Phase 2.\n- They already told you the pricing (their message, a pricing page, the README) → Phase 3 is a quick confirm, not a list of questions.\n\n## Phase 2 — Introduce and connect\n\nStart with two or three sentences: what's going to happen (connect this project to an Autumn org → write the pricing into `autumn.config.ts` → approve the pricing → push). Something like this, in your own words:\n\n> Setting up Autumn. I'll connect this project to an Autumn org, write your pricing into `autumn.config.ts`, and show it to you to approve before anything is pushed.\n\nThen:\n\n1. Run `atmn init` from the project root with the user's package manager (`bunx atmn init`, `pnpm exec atmn init`, `yarn atmn init`, `npx atmn init` — read it off the lockfile). Every `atmn …` command below means that run command. One command does the whole connect step: it adds `atmn` as a dependency, places the config in `autumn/` (its own package in a monorepo — it asks where, or takes `--path` and `--name`), pulls whatever the org already holds, and installs these skills beside the config. Each run prints what it did and, when it needs an answer, the flag to pass; run it again with the flag.\n2. Key already there → `init` says who it's connected to and moves on. Say so in one line.\n3. No key → `init` stops and asks how to connect. Ask the user the same thing, one question, two options, plain words:\n\n > Two ways to start: sign in to an Autumn account (I'll open a browser), or go keyless — I set up a sandbox for you right now and you link an account later. Which do you want?\n\n Always ask this one, even if the user told you to handle everything yourself — a keyless org has no owner until they link it, and that is their call, not yours. Pick keyless without asking only when there is genuinely nobody in the chat to answer (an unattended run), and say in one line that you did.\n4. Connect the way they chose, by running `init` again with the flag:\n\n - **Sign in** → say a browser window is coming, then `atmn init --login`. It opens the browser to sign in and create or pick an org, prints the sign-in URL, and waits — that's normal, it's not stuck. If the browser doesn't open (SSH, sandbox), that's not a failure: send the user the printed URL as-is and wait. Keys get saved to `.env`. Fails → retry once, then stop and tell the user what failed. Offer the ways forward — go keyless, or they copy their own sandbox key from app.useautumn.com into `.env` as `AUTUMN_SECRET_KEY` — and wait for their answer. Never switch to keyless on your own.\n - **Keyless** → `atmn init --keyless`. It provisions a sandbox org and saves its key to `.env` as `AUTUMN_SECRET_KEY`. No account, no browser, nothing for the user to do. The org is a real one: pushing, customers, and billing all work the same. It has no owner until Phase 7 links one, and the key doesn't change when that happens. `init` prints the deadline for linking; note it for Phase 7. For what provisioning and linking do underneath, and their limits, read `references/keyless.md`.\n\n5. `init` pulled the org's catalog into the config. If plans showed up (\"Pulled N entries\"), say so and go through them with the user before changing anything. A brand-new or keyless org is empty; the config is a scaffold for Phase 4 to fill.\n\nDone when `init` finished and you know whether the org already has plans. Say so in one line — including whether it's keyless, since that decides how you finish.\n\n## Phase 3 — Get a starting point\n\nYou need the pricing in the user's own words — not the details, just what to build.\n\n- They already told you (their message, a pricing page, the README) → repeat it back in one or two sentences and move on. If they gave you everything, ask nothing.\n- They haven't → ask what they're building and what they want to charge. A pricing page, or a competitor's page they like, is a full answer too.\n\nDon't dig into details here — what to ask, what to assume, and how to handle unclear pricing is `autumn-catalog`'s job, next phase.\n\nDone when you have rough plans and prices to build from.\n\n## Phase 4 — Model, write, approve\n\nLoad `autumn-catalog` now — from here it owns the conversation: its Shape/Fill flow, its checklist (shown instead of this skill's), its questions, its catalog display, and the approval. Your speaking rules above still apply to its questions.\n\nThis whole phase is the catalog skill's: structure agreed → config written and valid → pricing shown in its format → user approved. Only come back here when that's done; don't push yet.\n\n## Phase 5 — Push\n\nOnly after the yes. Push with `atmn` using `autumn-catalog`'s atmn reference — it has the exact commands, flags, and versioning choices. If the push asks for decisions (new version of a live plan, deleting things), bring them to the user; never decide alone. Check the push output shows every plan and feature made it — a push that errored is not done, and after two failed fixes you stop and show the error.\n\n## Phase 6 — Get it working in the app\n\nPricing in a sandbox is invisible. The user believes Autumn works when their own app creates a customer, blocks something, and takes money — so go there next, in one short pass, before anything else gets built.\n\nSay what's now live (plans and features, which org, sandbox) and where the config file is, then offer it:\n\n> Plans are live. Want me to wire up the basics now — customers created on signup, buying a plan, one feature gated / tracked — so you can see it running in your app?\n\nThey say no → skip to Phase 7. They say yes, or already asked for the integration up front → this phase runs. Either way it's their call; never start editing app code unasked.\n\nScope it before handing over. The first pass is the thinnest thing that proves the loop, and nothing else:\n\n- a customer created where the app already knows who the user is\n- buying a plan: the main free → paid move, so real money moves once\n- `check` and `track` on the **one** feature that matters most — ask which if it isn't obvious, don't gate everything\n\nAnything else — every remaining feature, entities, seats, billing controls, the billing page — waits for a second pass. Say that out loud when you scope, so the user doesn't read a small first pass as a small integration.\n\nLoad `autumn-integrate` now with that scope — from here it owns the conversation and the code: its order of operations, its checklist, its verification. Your speaking rules above still apply.\n\nCome back when its verification passes: a customer that exists in sandbox, a check that denied, usage that landed, a plan that attached.\n\n## Phase 7 — Link the account (keyless only)\n\nSkip this entirely if the user signed in — they already own their org.\n\nOffer once, right after they've seen the integration work, because that's when there's something worth logging in to look at:\n\n> Want to link this to your account? I’ll create a secure sign-in link so you can claim the org and open everything that just ran in the dashboard.\n\nNo → fine, drop it and say the offer stands whenever. Yes → ask which email to send the link to, run `atmn login --claim <email>`, and give them the returned claim URL. Autumn also emails that same URL — say so, naming the address, so they know where to find it later (\"Also sent to you@example.com\"). Whoever opens the link and signs in becomes the owner — the email is only where the link is delivered, so treat the link like the key and give it to the user alone. Linking makes them the owner: same key, same plans, same customers, plus the dashboard. An account that already exists works the same way; the new org is added beside their existing ones and becomes active after confirmation.\n\nUnclaimed orgs don't wait forever, so mention the window `init` printed when you offer — as a fact, not a threat. If linking fails, nothing is lost: the key keeps working and they can try again, or sign up normally and push the same config. What the commands do underneath: `references/keyless.md`.\n\n## Phase 8 — Done\n\nThree lines: what's live and working, where it lives (`autumn.config.ts`, and the handlers if Phase 6 ran), and the obvious next thing — the rest of the features, or going to production when they're ready. A linked or signed-in user can see it all at app.useautumn.com; a keyless user who declined can't, so don't send them there.\n\nThen stop. Don't keep building, don't tour the dashboard, don't deploy anything.",
26
+ "---\nname: autumn-setup\ndescription: First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.\nversion: 2.0.42\n---\n\n# Setup\n\nTake the user from \"I want billing\" to pricing that is live in a sandbox org and working in their app — with as little between those two points as possible. You run the flow; two other skills do the heavy parts. `autumn-catalog` turns their pricing into `autumn.config.ts`. `autumn-integrate` puts the first calls in their code. Never do either of those jobs yourself.\n\n## Ground rules\n\nThese apply the whole time, not just in one phase.\n\n- CLI-first: everything happens in `autumn.config.ts` and `atmn`. The only browser moments are signing in, or claiming a keyless org later. Never send the user to the dashboard to do the work.\n- Never invent a price, limit, or plan name. A missing number is a question, never a guess.\n- Push only after the user approves the pricing (Phase 4), or when they already told you to go ahead without a review.\n- Sandbox by default: `AUTUMN_SECRET_KEY` is the sandbox key. Don't touch production during setup.\n- Keys: check that a key exists by its name only. Never read, print, or ask the user to paste a key into the chat. Same for a keyless org's claim token. A one-time email code is not a key — that one does come through the chat.\n- Two tries max to fix any failing command, then stop and show the error.\n- Never run `atmn reset`, and never remove existing plans unless the user clearly asked.\n- If a step is already done, say so in one line (\"Already signed in — skipping login\") and move on.\n\n## How to talk\n\nYou are a competent engineer pairing with the user, not an installer wizard and not a marketer.\n\n- Simple, everyday words: \"plans\", \"what's included\", \"extra usage\". No jargon in chat — schema words like `consumable`, `prepaid`, `usage_based` stay in the config. If a simpler word says the same thing, use it.\n- 1–3 sentences per message. The pricing summary is the only large thing you send.\n- One message, one purpose: a status, a question, or the pricing summary.\n- Don't ask permission for harmless work — reading the repo, drafting the config, building the summary. Ask only for decisions and approvals.\n- Say what's about to happen before it does: one line before the browser opens, one before the push.\n- When you need input, ask at most three short numbered questions — the ones that unblock you, nothing more. If your platform has a built-in way to ask questions with options, use it. Never re-ask something they answered.\n- Always end a message with something: the question, what you're doing next, or that you're done.\n- Don't paste the config or command output into chat; name the file and summarize. Errors are the exception — quote those exactly.\n- If you're stuck, send three lines: what failed, the exact error, what you need to continue.\n- No emoji, no hype, no \"Great question\". Plain and concrete: \"Connected to Acme (sandbox).\"\n\n## Progress\n\nCopy this checklist into your first message and keep it up to date. If you skip an item, say why in one line.\n\n- [ ] 1 Skills installed; checked for an existing config, key, and pricing\n- [ ] 2 Connected to Autumn — `atmn` installed, signed in or keyless\n- [ ] 3 Got the user's pricing — rough plans and prices to build from\n- [ ] 4 Pricing modeled and approved (the `autumn-catalog` skill runs this part)\n- [ ] 5 Pushed to Autumn and verified\n- [ ] 6 Working in the app — one plan bought, one feature gated (the `autumn-integrate` skill runs this part)\n- [ ] 7 Account linked (keyless only — drop this line if they signed in)\n- [ ] 8 Done\n\nFor items 4 and 6, another skill owns the conversation and its checklist replaces this one for the duration — show theirs, not this one. Come back here when they're finished.\n\n## Phase 1 — Check the project (silent)\n\nDon't message the user yet — just find out where things stand.\n\nFour skills share this job, and they ship inside the `atmn` CLI: `atmn init` writes them next to the config and prints the `npx skills add <that folder> -y` line that registers them with your agent, and `atmn skills update` refreshes them when `push` or `pull` say they are older than the CLI. If any is missing here, that is Phase 2's job — never fetch them from anywhere else.\n\n`autumn-setup` (this file) is the flow. `autumn-catalog` is how to build the pricing, plus the exact `atmn` commands — load it in Phase 4. `autumn-integrate` is how the app calls Autumn — load it in Phase 6. `autumn-concepts` explains Autumn's objects — the other two load it themselves.\n\nThen check three things:\n\n- `autumn.config.ts` exists → Autumn is already set up here. Say so, treat the file as the truth, and use `autumn-catalog` for the changes; come back at Phase 5 (push) when there's something to push.\n- An `AUTUMN_SECRET_KEY` exists (shell env, `.env`, `.env.local`) → already connected; skip the connect step in Phase 2.\n- They already told you the pricing (their message, a pricing page, the README) → Phase 3 is a quick confirm, not a list of questions.\n\n## Phase 2 — Introduce and connect\n\nStart with two or three sentences: what's going to happen (connect this project to an Autumn org → write the pricing into `autumn.config.ts` → approve the pricing → push). Something like this, in your own words:\n\n> Setting up Autumn. I'll connect this project to an Autumn org, write your pricing into `autumn.config.ts`, and show it to you to approve before anything is pushed.\n\nThen:\n\n1. Run `atmn init` from the project root with the user's package manager (`bunx atmn init`, `pnpm exec atmn init`, `yarn atmn init`, `npx atmn init` — read it off the lockfile). Every `atmn …` command below means that run command. One command does the whole connect step: it adds `atmn` as a dependency, places the config in `autumn/` (its own package in a monorepo — it asks where, or takes `--path` and `--name`), pulls whatever the org already holds, and installs these skills beside the config. Each run prints what it did and, when it needs an answer, the flag to pass; run it again with the flag.\n2. Key already there → `init` says who it's connected to and moves on. Say so in one line.\n3. No key → `init` stops and asks how to connect. Ask the user the same thing, one question, two options, plain words:\n\n > Two ways to start: sign in to an Autumn account (I'll open a browser), or go keyless — I set up a sandbox for you right now and you link an account later. Which do you want?\n\n Always ask this one, even if the user told you to handle everything yourself — a keyless org has no owner until they link it, and that is their call, not yours. Pick keyless without asking only when there is genuinely nobody in the chat to answer (an unattended run), and say in one line that you did.\n4. Connect the way they chose, by running `init` again with the flag:\n\n - **Sign in** → say a browser window is coming, then `atmn init --login`. It opens the browser to sign in and create or pick an org, prints the sign-in URL, and waits — that's normal, it's not stuck. If the browser doesn't open (SSH, sandbox), that's not a failure: send the user the printed URL as-is and wait. Keys get saved to `.env`. Fails → retry once, then stop and tell the user what failed. Offer the ways forward — go keyless, or they copy their own sandbox key from app.useautumn.com into `.env` as `AUTUMN_SECRET_KEY` — and wait for their answer. Never switch to keyless on your own.\n - **Keyless** → `atmn init --keyless`. It provisions a sandbox org and saves its key to `.env` as `AUTUMN_SECRET_KEY`. No account, no browser, nothing for the user to do. The org is a real one: pushing, customers, and billing all work the same. It has no owner until Phase 7 links one, and the key doesn't change when that happens. `init` prints the deadline for linking; note it for Phase 7. For what provisioning and linking do underneath, and their limits, read `references/keyless.md`.\n\n5. `init` pulled the org's catalog into the config. If plans showed up (\"Pulled N entries\"), say so and go through them with the user before changing anything. A brand-new or keyless org is empty; the config is a scaffold for Phase 4 to fill.\n\nDone when `init` finished and you know whether the org already has plans. Say so in one line — including whether it's keyless, since that decides how you finish.\n\n## Phase 3 — Get a starting point\n\nYou need the pricing in the user's own words — not the details, just what to build.\n\n- They already told you (their message, a pricing page, the README) → repeat it back in one or two sentences and move on. If they gave you everything, ask nothing.\n- They haven't → ask what they're building and what they want to charge. A pricing page, or a competitor's page they like, is a full answer too.\n\nDon't dig into details here — what to ask, what to assume, and how to handle unclear pricing is `autumn-catalog`'s job, next phase.\n\nDone when you have rough plans and prices to build from.\n\n## Phase 4 — Model, write, approve\n\nLoad `autumn-catalog` now — from here it owns the conversation: its Shape/Fill flow, its checklist (shown instead of this skill's), its questions, its catalog display, and the approval. Your speaking rules above still apply to its questions.\n\nThis whole phase is the catalog skill's: structure agreed → config written and valid → pricing shown in its format → user approved. Only come back here when that's done; don't push yet.\n\n## Phase 5 — Push\n\nOnly after the yes. Push with `atmn` using `autumn-catalog`'s atmn reference — it has the exact commands, flags, and versioning choices. If the push asks for decisions (new version of a live plan, deleting things), bring them to the user; never decide alone. Check the push output shows every plan and feature made it — a push that errored is not done, and after two failed fixes you stop and show the error.\n\n## Phase 6 — Get it working in the app\n\nPricing in a sandbox is invisible. The user believes Autumn works when their own app creates a customer, blocks something, and takes money — so go there next, in one short pass, before anything else gets built.\n\nSay what's now live (plans and features, which org, sandbox) and where the config file is, then offer it:\n\n> Plans are live. Want me to wire up the basics now — customers created on signup, buying a plan, one feature gated / tracked — so you can see it running in your app?\n\nThey say no → skip to Phase 7. They say yes, or already asked for the integration up front → this phase runs. Either way it's their call; never start editing app code unasked.\n\nScope it before handing over. The first pass is the thinnest thing that proves the loop, and nothing else:\n\n- a customer created where the app already knows who the user is\n- buying a plan: the main free → paid move, so real money moves once\n- `check` and `track` on the **one** feature that matters most — ask which if it isn't obvious, don't gate everything\n\nAnything else — every remaining feature, entities, seats, billing controls, the billing page — waits for a second pass. Say that out loud when you scope, so the user doesn't read a small first pass as a small integration.\n\nLoad `autumn-integrate` now with that scope — from here it owns the conversation and the code: its order of operations, its checklist, its verification. Your speaking rules above still apply.\n\nCome back when its verification passes: a customer that exists in sandbox, a check that denied, usage that landed, a plan that attached.\n\n## Phase 7 — Link the account (keyless only)\n\nSkip this entirely if the user signed in — they already own their org.\n\nOffer once, right after they've seen the integration work, because that's when there's something worth logging in to look at:\n\n> Want to link this to your account? I’ll create a secure sign-in link so you can claim the org and open everything that just ran in the dashboard.\n\nNo → fine, drop it and say the offer stands whenever. Yes → ask which email to send the link to, run `atmn login --claim <email>`, and give them the returned claim URL. Autumn also emails that same URL — say so, naming the address, so they know where to find it later (\"Also sent to you@example.com\"). Whoever opens the link and signs in becomes the owner — the email is only where the link is delivered, so treat the link like the key and give it to the user alone. Linking makes them the owner: same key, same plans, same customers, plus the dashboard. An account that already exists works the same way; the new org is added beside their existing ones and becomes active after confirmation.\n\nUnclaimed orgs don't wait forever, so mention the window `init` printed when you offer — as a fact, not a threat. If linking fails, nothing is lost: the key keeps working and they can try again, or sign up normally and push the same config. What the commands do underneath: `references/keyless.md`.\n\n## Phase 8 — Done\n\nThree lines: what's live and working, where it lives (`autumn.config.ts`, and the handlers if Phase 6 ran), and the obvious next thing — the rest of the features, or going to production when they're ready. A linked or signed-in user can see it all at app.useautumn.com; a keyless user who declined can't, so don't send them there.\n\nThen stop. Don't keep building, don't tour the dashboard, don't deploy anything.",
27
27
  references: [
28
28
  {
29
29
  path: "references/keyless.md",
@@ -36,9 +36,9 @@ export const SKILLS: readonly BundledSkill[] = [
36
36
  name: "autumn-catalog",
37
37
  description:
38
38
  "Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.",
39
- version: "2.0.40",
39
+ version: "2.0.42",
40
40
  markdown:
41
- '---\nname: autumn-catalog\ndescription: Modeling a user\'s pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.\nversion: 2.0.40\n---\n\n# Catalog\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines Autumn\'s data model — features, plans, plan items, balances — which every modeling decision builds on.\n\n## STRICT RULES — re-read before every config write\n\n1. **Amounts are in major units (e.g. dollars), never minor units (cents).** $180/month is `amount: 180`. $0.01 per credit is `amount: 0.01`. If any amount you wrote is 100× the user\'s number, it is wrong.\n2. **Never invent a price, limit, or plan name** — a missing number is a question.\n3. **One definition per real thing.** One feature per resource, one child plan per license pattern (parents customize their license, never get their own copy), one add-on per offer (sizes are tiers, not plans).\n\n**Building, or iterating on a live catalog?** What matters is whether customers are on these plans — not whether a config file exists. Still setting up (even across sessions, with a half-built `autumn.config.ts`) → the workflow below; edit the draft freely. Already running Autumn with customers, now changing prices/plans → that\'s an update with real stakes (versioning, migrations, grandfathering) — read `references/catalog-update.md` first. Unsure → check for customers (`atmn pull` / the org) or ask.\n\nTurning pricing into a catalog is two jobs:\n\n- **Shape** — decide the structure: which plans exist, what\'s a variant, what\'s an add-on, where balances live. Decided by relationships in their pricing, not by amounts.\n- **Fill** — put in the numbers and per-item details, then write and validate the config.\n\nDo Shape fully before Fill. Take numbers whenever the user mentions them, but never chase numbers during Shape — the one exception is "is this the same on every plan?", which is a structure question.\n\n## Progress (what the user sees)\n\nCopy this checklist into your first message and keep it up to date. It is the **only** structure the user ever sees — never say "step", "pass", "decide", or "fork" to them. If you skip an item, say why in one line.\n\n- [ ] 1 Your plans and prices\n- [ ] 2 What\'s included in each plan\n- [ ] 3 How billing behaves (signup, trials, limits)\n- [ ] 4 Licenses — paid seats, workspaces, projects (if any)\n- [ ] 5 Structure agreed\n- [ ] 6 Config written and checked\n\n## How to ask\n\n- One topic per message. Two questions at most, and only if both belong to that topic. Never mix topics in one message.\n- If one ambiguity changes which other questions apply, resolve it first before asking those.\n- Attach your guess to each question — "does the trial need a card? I\'d guess no" — a wrong guess gets corrected faster than a blank gets answered.\n- Assert the obvious instead of asking. "500 messages a month" resets monthly — state it as an assumption, don\'t ask. Questions are only for facts that change the structure and can\'t be guessed.\n- Skip anything they already told you. A good message to answer is a nod, not homework.\n- Never announce what you\'ll do next ("once I get those three, I\'ll restate…") — just work the current topic.\n- The restate (step 5) is the safety net: wrong assumptions get caught there cheaply, which is what makes fewer questions safe.\n\n## Shape: collect, then decide\n\nWork the four steps below in order, one at a time — each says when it\'s done. While collecting, make only the small calls each step allows; leave the big ones (marked ↦ Decide) for after.\n\n### Step 1 — Plans\n\n- What are the plans? ("Free, Pro $20/mo, Growth $50/mo")\n- Free tier? → a plan with no price that every new customer starts on automatically.\n- Anything bought *alongside* a plan rather than instead of it (packs, extra storage)? → note it ↦ Decide (add-on).\n- Enterprise tier? Ask. If it exists, model the base enterprise plan in the catalog, and tell the user: custom terms per customer (special prices, custom limits) are applied later when attaching, not modeled here.\n\nDone when you can list every plan they sell, including free, add-ons, and enterprise.\n\n### Step 2 — What each plan includes\n\nHave them describe what they charge for or limit, in their own words. Pick a type per feature:\n\n| They say | Feature type |\n|---|---|\n| "Pro has SSO" | boolean (on/off) |\n| "500 messages a month" | metered, resets |\n| "10 team members" | metered, no reset (held, not used up) |\n| "credits" / "tokens" / "wallet" | credit system — always, even if only one action uses it today. They will add more actions; a single mapped action is fine. Their app tracks the actions (chat, image), never the credit balance itself. |\n\nNote top-ups ("buy more when you run out", "auto-recharge") ↦ Decide (top-up placement).\n\n### Step 3 — How billing behaves\n\nThis step usually means explaining Autumn to the user in plain words. Do.\n\n- **Signup**: every new customer automatically gets the default plan — usually the free one. One default per group. A default can carry no prices at all — a "$0" plan with paid items (per-seat charges, prepaid packs) is a paid plan, not a default. If every plan bills something, there is no default; customers subscribe.\n- **Trials**: ask "does the trial need a card?" (guess from their motion — PLG usually no). Card → `free_trial` on the paid plan. No card → a **separate free trial plan** (`pro_trial`: no price, the paid plan\'s items, auto-enabled when everyone starts on it); the paid plan stays untouched, since a default plan can never be paid. For trial modeling details — trial-behavior on plans, read `references/plan.md` in the `autumn-concepts` skill.\n- **What is a plan attached to?** The customer, or each thing they own (workspace, project, site)? "Pro is $200 per workspace" → attached per entity. Pin this down — it changes everything downstream.\n- **Who uses each metered feature?** The customer as a whole, or each entity? If entities: one shared balance or separate ones? "Shared across…" → shared ↦ Decide (balances).\n\nDone when you know what attaches where, who consumes what, and how trials and signup work.\n\n### Step 4 — Licenses\n\nTriggered whenever the customer pays per unit of some entity — seats, workspaces, projects, sites, members: "each X is $10/month", "comes with 3 X". When you see one, always ask: **what does one X come with?** Never skip this because the user didn\'t say "seat". For modeling a license — the concept: child plan, license link, customize, read `references/licenses.md` in the `autumn-concepts` skill.\n\n- Nothing of its own ("$10 per seat", just a count) → a per-unit priced item on the plan. No entities. The common case.\n- The unit gets something of its own ("each seat gets 100 credits", "every workspace has its own allowance") → a **license**: a small plan of its own that the parent plan hands out per unit.\n- Units must be assigned, reassigned, or sit empty → also licenses. Rare — confirm they need it.\n\nFor deciding between a per-unit item, licenses, and entity-attached plans for a countable paid unit, read `references/fork-licenses.md`.\n\n### Before deciding: restate\n\nOnly after your questions are answered — never announce it in advance, and never restate facts nobody has confirmed yet. One short message: the plans, what\'s metered, what attaches where, who shares what. Let the user correct it. A wrong fact here is much cheaper than a wrong structure later.\n\nIf the user gave you everything up front and you asked nothing, skip the separate restate — the structure message (checklist item 5) does that job.\n\n## Decide\n\nResolve these with all facts in hand. Each has a default — when the facts genuinely don\'t settle it, show both options in one line each and ask.\n\n**Variant or separate plan?**\nA variant can change the price, swap items in or out, and change the trial — nothing else.\n\n- "Pro monthly / Pro annual, same features" → variant. (Annual usually still resets allowances monthly — billing and reset intervals are independent. Confirm.)\n- Volume buckets/tiers — one question decides: **is each bucket the subscription itself (→ a variant per bucket) or a purchase on top of one (→ a prepaid item)?**\n- Different features per tier → separate plans. One plan per tier is normal, not a smell.\n\nFor deciding whether volume buckets/tiers are variants of the plan or one prepaid volume-tiered item — the tells, the trap, when variants are forced, read `references/fork-variants.md`.\n\n**Add-on, or part of the plan?** Two independent questions: is the purchase priced/bundled per plan (→ item on each plan) or one offer across plans (→ one add-on plan, sizes as tiers on its prepaid item)? And does a plan exist at the level its balance is shared at (→ item there) or are all plans entity-attached (→ a customer-level add-on is forced)? Auto-recharge needs the prepaid item to exist — add it in Shape, it\'s structural.\n\nFor modeling packs, top-ups, or any purchase bought on top of a plan — add-on vs plan item, and what level it sits at, read `references/fork-addon.md`.\n\n**Where do balances and purchases live?**\nThe rule: **purchases and balance at the customer; usage tracking and caps at the entity.** Grants "shared across…" entities → pooled (`pooled: true` on the item); separate per-entity balances → no pooling; overage stays on the entity\'s plan either way.\n\nFor deciding where balances and purchases live — pooled grants, per-entity balances, customer-level packs, overage placement, read `references/fork-pooled.md`.\n\n**Groups?**\nCan one customer hold two plans at once from different lines (a support plan AND a sales plan)? → one group per line. Within a group, attaching a plan replaces the current one; that\'s what makes upgrades work.\n\n**One-off?**\n"Lifetime deal", "one-time pack" → a plan with a one-off price: single invoice, no subscription, balances never reset.\n\n## Check\n\nCompare the structure against the shapes in `references/cases.md`. If their pricing matches a known shape but your structure differs, either say why or fix it.\n\nShortcuts that are usually wrong — catch yourself before Show:\n\n| You\'re thinking | Check first |\n|---|---|\n| "seats + credits → licenses" | Are the credits per seat, or one shared pot? Shared → per-unit seats + pooled balance, no licenses. |\n| "annual pricing → separate plan" | Same features? → variant. |\n| "tiers → one plan with tiers" | Does overage or anything else differ per tier? → plan per tier. |\n| "they said credits but it\'s one action → plain meter" | Credits are always a credit system. |\n| "packs belong on the plan" | Are they shared across entities or plans? → add-on. |\n| "everyone starts on a Pro trial → trial + auto-enable on Pro" | A default plan can never be paid. → separate free `pro_trial` plan (Pro\'s items, no price, auto-enabled); Pro untouched. |\n| "several top-up sizes → one add-on plan per size" | Do the sizes differ only in quantity and price? → volume tiers on one prepaid item, one add-on plan. |\n| "the pack is priced per plan → an add-on per plan" | Per-plan pricing IS plan differentiation → a prepaid item on each base plan, no add-ons. Add-ons are for one offer shared across plans, or when no plan exists at the shared level. |\n| "the seat differs per plan → one seat plan per parent" | ONE child plan carrying the mainline take; each differing parent\'s license carries its own diff via `customize`. Never a `<parent>_seat` plan per parent. |\n\nFor checking the derived structure against known-good shapes, read `references/cases.md`.\n\n## Show\n\nPresent the structure in the "Showing the catalog" format below — the same one used for every catalog display, catalog inside a fenced code block. Numbers you don\'t have yet stay open, never invented: write the line without them ("AI messages per month — amount TBD"). Structure notes go in parentheses on the line they describe. The whole message:\n\n````\nHere\'s the structure I\'d build:\n\n```\nFeatures: AI credits (credit system — chat and image messages draw from it) · SSO (on/off)\n\nFree — no price, everyone starts here\n - 100 AI credits per month\nPro — $20/month, or annual (same features)\n - AI credits per month — amount TBD\n - SSO\nCredit pack (add-on) — price TBD, shared across all workspaces\n```\n\nI assumed: credits reset monthly, no rollover. Anything wrong?\n````\n\nList every assumption. Get a clear yes — "sounds good" without reading is not a yes. But if the user already told you to go ahead without review ("no need to ask", "just build it"), show the structure and keep going — don\'t stop to wait. If a late fact changes the structure, redo the affected decision, update the structure, and show what changed in one line.\n\n## Fill\n\nStructure agreed — now finish it. Four moves, in order.\n\n**1 — Fill in what\'s known.** Everything the user already said goes straight into the draft. Never re-ask a confirmed fact.\n\n**2 — Ask for missing essentials.** Values with no sane default: base prices, included amounts, per-unit prices, tier boundaries, trial length. Never invent one — a missing number is a question. Batch per the how-to-ask rules.\n\n**3 — Sweep the options.** Ask each of these **once for the whole catalog**, in plain words — never item by item. The answer distributes to every item it touches ("carry over on both plans, or just Growth?" only if they hint at a difference). Raise a bucket only when the structure makes it relevant; skip anything already answered.\n\n- **Carry-over** — any allowance that resets: "Should unused messages carry over month to month, or reset clean?"\n- **Running out** — any metered feature: "When they run out — hard stop, or keep going and bill the extra?" (often settled in Shape; skip if so)\n- **Top-ups** — credits present or buying-more mentioned: "Can they buy more before the reset?" → a one-off prepaid item\n- **Guardrails** — any plan with overage or heavy usage: "Any caps on how fast or how far usage can run — like a daily limit, or a ceiling on overage? Want customers warned as they approach limits?" → plan-level billing controls\n- **Anything else on/off** — always, it\'s cheap: "Any other on/off differences between plans — SSO, priority support, API access?"\n\nThe sweep exists so the user hears what\'s configurable without being marched through every item. Behind it, check every knob yourself — this list is internal, never show it:\n\n- per plan: base price · trial (length, unit, card — all explicit) · default plan · group · billing controls (guardrails answered → `billingControls` on the plan)\n- per item: billing method · included · price or tiers (tier behavior explicit) · reset · rollover · pooled (if Shape chose shared balances) · purchase caps · the one-off item auto-recharge needs\n\nLanding a guardrail answer means picking the right control — windowed cap vs overage ceiling vs alert vs allow-past-balance vs auto-recharge are different knobs with different fields. What each one is, the three overage knobs, and the plan→customer inheritance are the `autumn-concepts` skill\'s billing-controls reference — read it before writing `billingControls`. The flow rules here:\n\n- Plan-level controls are defaults every subscriber inherits — the right home for anything true of the whole plan ("free users max 20 emails/day"). Per-customer exceptions are a billing/customer operation, not catalog work.\n- A cap stated alongside an allowance ("1,000 a month but never more than 20 a day") is a usage limit on the plan, not a second item or a smaller allowance.\n- "Track it but don\'t bill it" / "let them run over, we\'ll invoice manually" → overage knobs on the plan, not a $0 price.\n- Auto-recharge needs its one-off prepaid item (already on the per-item list) AND the `autoTopups` control.\n\n**4 — Propose, then finalize.** One message: the full catalog in the format below, then "I assumed:" listing every knob you defaulted. Fold corrections in. Then write the config — and before saving, re-read every amount in it: dollars, never cents ($600 is `600`, not `60000`). Validate with `atmn push` (a preview; nothing is applied until `--yes`), fix what it flags, and show the final catalog — same format, no assumptions list. **Done means the config is written and valid — a summary is not done.**\n\n### Showing the catalog\n\nUse **exactly this format** every time you show the catalog — the Show structure message, the Fill proposal, and the done message alike. Never a markdown table, never a different layout per message. Send it as a fenced code block (```) so the indentation survives markdown rendering — plan names on bare lines otherwise get folded into the previous plan\'s list. It\'s the same grammar the dashboard renders. Features first with their kind, then plans, one line per item:\n\n```\nFeatures: AI messages (usage, resets) · Seats (held, not used up) · Credits (credit system — 1 message = 1 credit) · SSO (on/off)\n\nPro — $20/month\n - 500 AI messages per month\n · unused carry over, up to 500, for 1 month\n - 500 AI messages per month, then $0.01 per message\n - $10 per 1,000 credits\n - 5,000 credits for $50 per month\n - 3 seats included, then $10 per seat\n - $18 - $15 per seat\n - Unlimited projects\n - SSO\n - 100 credits per seat per month\nTeam — $500/month\n - 5 seats included, then $40 per seat per month\n each seat gets:\n · 100 summaries per month\n · SSO\nCredit pack (add-on) — $10 for 1,000 credits, buy anytime\n```\n\nThe Features line names every feature and its kind in plain words: `(usage, resets)` for consumable meters, `(held, not used up)` for non-consumable ones like seats, `(credit system — …)` with its action mappings, `(on/off)` for boolean. Item lines, top to bottom: plain allowance · allowance with overage · pure usage price (per billing unit) · prepaid bucket · included + prepaid per-unit · tiered rate shown first-to-last · unlimited · boolean (name only, never "enabled") · per-entity grant · one-off add-on. Numbers get commas; "then …" says what happens after the included runs out; annual variants go inline ("or $200/year — messages still reset monthly").\n\nConfigured item properties — carry-over, purchase caps, top-up behavior — are never their own `-` lines: indent them under the item they belong to as `·` lines, one behavior each, so the hierarchy is visible. Only show what\'s configured; defaults (like usage simply stopping when no overage price is set) get no line.\n\nBilling controls follow the same rule: `·` lines under the item they guard, in plain words — `· max 200 per day` · `· overage capped at 20% over` · `· overage tracked, not billed` · `· warned at 80%` · `· auto-buys 1,000 credits when below 100`. Never the schema names (usage_limits, spend_limits) in user-facing text.\n\n### Config gotchas\n\n- Amounts are plain dollars everywhere — base prices, tier `flatAmount`s, unit prices: $20 is `20`, never `2000`, and a $100 tier is `flatAmount: 100`, never `10000`. Re-check every number before writing; cents is the most common wrong config.\n- A default/auto-enabled plan can\'t be paid: no base price, no paid items.\n- A prepaid quantity **includes** the included amount, and so does each tier\'s `to` — the first tier\'s `to` must exceed `included`.\n- `billingUnits` rounds usage **up** when billing.\n- Set explicitly, never lean on defaults: `billingMethod` and `interval` on every item price, `tierBehavior` on tiered prices, and every trial field (`durationLength`, `durationType`, `cardRequired`, `onEnd`). Explicit defaults cause no spurious diffs.\n- Volume tiers charge the flat amount of the reached tier and are prepaid-only; graduated (the default) sums across brackets.\n- Rollover needs a resetting allowance; `max` and `maxPercentage` are mutually exclusive; `expiryDurationType` is required.\n- `billingControls` is a plain object on the plan with camelCase fields like the rest of the config (`featureId`, `overageLimit`); each control list replaces wholesale on update.\n- Pooled balances are config: `pooled: true` on the entity plan\'s item. Concluding "shared across workspaces" in Shape and then omitting the flag is the classic miss.\n- Pooled grant + overage = two items on the plan: the pooled grant carries no price; a separate usage-priced item (`included: 0`) carries the overage. A pooled item can\'t itself be usage-priced.\n- Don\'t write `proration` — leave it out and take server defaults.\n- Trial end behavior is `freeTrial.onEnd`: `"bill"` (default) charges when the trial ends, `"revert"` expires it and restores the previous plan.\n- Every plan row carries `versionSlug` and `active`, and every variant row and license link `versionSlug`. A plan\'s rows are its versions — exactly one `active: true` — and a version row left out of `plans` is deleted. How rows express versions, renames and drafts: `references/atmn.md`.\n\nThe config uses the builders `feature`, `plan`, `variant`, `license` as plain function calls with object arguments; items are plain objects inside a plan, and the file\'s default export is `atmn({...})` naming every collection. Never guess other functions or fields; the full shapes are in `references/atmn.md`.\n\n```ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const credits = feature({\n featureId: "credits",\n name: "Credits",\n type: "credit_system",\n creditSchema: [{ meteredFeatureId: "messages", creditCost: 1 }],\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [\n { featureId: credits.featureId, included: 500, reset: { interval: "month" } },\n ],\n});\n\nexport default atmn({ features: [credits], plans: [pro] });\n```\n\nPattern deep-dives, split one file per pattern under `references/` — read the matching one when filling that pattern\'s details:\n\nFor usage overage pricing, read `references/usage-based-pricing.md`.\nFor prepaid quantities (seats, packs, buckets), read `references/prepaid-pricing.md`.\nFor volume-tiered prices, read `references/volume-based-tiers.md`.\nFor $X per unit, read `references/per-unit-pricing.md`.\nFor billing vs reset intervals, read `references/recurring.md`.\nFor one-off purchases / top-ups, read `references/one-off-purchases.md`.\nFor auto-recharge, read `references/auto-top-ups.md`.\nFor carry-over, read `references/rollovers.md`.\nFor trial details, read `references/trials.md`.\nFor entity plans and license attach flows, read `references/entity-plans.md`.\nFor credit schemas and mappings, read `references/credit-systems.md`.\nFor default plans / auto-enable, read `references/free-plans.md`.\nFor add-on balance stacking, read `references/add-ons.md`.\nFor what variants can change, read `references/plan-variants.md`.\n\n## Conduct\n\n- In an existing config, match its patterns: if sibling plans carry their prepaid purchases as items, the new plan does too — don\'t introduce a different structure for the same kind of thing.\n- Stable lowercase IDs with underscores: `pro_plan`, `chat_messages`.\n- `entityFeatureId` is deprecated. Never mention or use it unless the user\'s existing config already has it.\n- Per-unit pricing pairs a base fee with the per-unit item ("$X/seat" plans still have a base price, even $0).\n- Speak plainly: "plans", "what\'s included", "extra usage". Schema words stay in the config — say "carry over" not "rollover", "shared across workspaces" not "pooled", "paid upfront" / "billed at month end" not "prepaid" / "usage_based".\n- Never volunteer what Autumn can or can\'t do. Don\'t offer options Autumn can\'t model, and don\'t explain limitations unprompted — only address one when the user directly asks to model that specific thing, and even then lead with the closest thing that works.\n- Start simple: the most important features first, confirm before adding more.\n\nBefore finishing: re-check the STRICT RULES at the top against the config you wrote.\n\n## Catalog operations\n\nFor using atmn, autumn.config.ts, or headless push flows, read `references/atmn.md`.\n\nFor changing an existing catalog: previewing, versioning, migrations, variant propagation, read `references/catalog-update.md`.\n\nFor creating or changing coupons, promo codes, feature grants, or referral programs, read `references/rewards.md`.',
41
+ '---\nname: autumn-catalog\ndescription: Modeling a user\'s pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.\nversion: 2.0.42\n---\n\n# Catalog\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines Autumn\'s data model — features, plans, plan items, balances — which every modeling decision builds on.\n\n## STRICT RULES — re-read before every config write\n\n1. **Amounts are in major units (e.g. dollars), never minor units (cents).** $180/month is `amount: 180`. $0.01 per credit is `amount: 0.01`. If any amount you wrote is 100× the user\'s number, it is wrong.\n2. **Never invent a price, limit, or plan name** — a missing number is a question.\n3. **One definition per real thing.** One feature per resource, one child plan per license pattern (parents customize their license, never get their own copy), one add-on per offer (sizes are tiers, not plans).\n\n**Building, or iterating on a live catalog?** What matters is whether customers are on these plans — not whether a config file exists. Still setting up (even across sessions, with a half-built `autumn.config.ts`) → the workflow below; edit the draft freely. Already running Autumn with customers, now changing prices/plans → that\'s an update with real stakes (versioning, migrations, grandfathering) — read `references/catalog-update.md` first. Unsure → check for customers (`atmn pull` / the org) or ask.\n\nTurning pricing into a catalog is two jobs:\n\n- **Shape** — decide the structure: which plans exist, what\'s a variant, what\'s an add-on, where balances live. Decided by relationships in their pricing, not by amounts.\n- **Fill** — put in the numbers and per-item details, then write and validate the config.\n\nDo Shape fully before Fill. Take numbers whenever the user mentions them, but never chase numbers during Shape — the one exception is "is this the same on every plan?", which is a structure question.\n\n## Progress (what the user sees)\n\nCopy this checklist into your first message and keep it up to date. It is the **only** structure the user ever sees — never say "step", "pass", "decide", or "fork" to them. If you skip an item, say why in one line.\n\n- [ ] 1 Your plans and prices\n- [ ] 2 What\'s included in each plan\n- [ ] 3 How billing behaves (signup, trials, limits)\n- [ ] 4 Licenses — paid seats, workspaces, projects (if any)\n- [ ] 5 Structure agreed\n- [ ] 6 Config written and checked\n\n## How to ask\n\n- One topic per message. Two questions at most, and only if both belong to that topic. Never mix topics in one message.\n- If one ambiguity changes which other questions apply, resolve it first before asking those.\n- Attach your guess to each question — "does the trial need a card? I\'d guess no" — a wrong guess gets corrected faster than a blank gets answered.\n- Assert the obvious instead of asking. "500 messages a month" resets monthly — state it as an assumption, don\'t ask. Questions are only for facts that change the structure and can\'t be guessed.\n- Skip anything they already told you. A good message to answer is a nod, not homework.\n- Never announce what you\'ll do next ("once I get those three, I\'ll restate…") — just work the current topic.\n- The restate (step 5) is the safety net: wrong assumptions get caught there cheaply, which is what makes fewer questions safe.\n\n## Shape: collect, then decide\n\nWork the four steps below in order, one at a time — each says when it\'s done. While collecting, make only the small calls each step allows; leave the big ones (marked ↦ Decide) for after.\n\n### Step 1 — Plans\n\n- What are the plans? ("Free, Pro $20/mo, Growth $50/mo")\n- Free tier? → a plan with no price that every new customer starts on automatically.\n- Anything bought *alongside* a plan rather than instead of it (packs, extra storage)? → note it ↦ Decide (add-on).\n- Enterprise tier? Ask. If it exists, model the base enterprise plan in the catalog, and tell the user: custom terms per customer (special prices, custom limits) are applied later when attaching, not modeled here.\n\nDone when you can list every plan they sell, including free, add-ons, and enterprise.\n\n### Step 2 — What each plan includes\n\nHave them describe what they charge for or limit, in their own words. Pick a type per feature:\n\n| They say | Feature type |\n|---|---|\n| "Pro has SSO" | boolean (on/off) |\n| "500 messages a month" | metered, resets |\n| "10 team members" | metered, no reset (held, not used up) |\n| "credits" / "tokens" / "wallet" | credit system — always, even if only one action uses it today. They will add more actions; a single mapped action is fine. Their app tracks the actions (chat, image), never the credit balance itself. |\n\nNote top-ups ("buy more when you run out", "auto-recharge") ↦ Decide (top-up placement).\n\n### Step 3 — How billing behaves\n\nThis step usually means explaining Autumn to the user in plain words. Do.\n\n- **Signup**: every new customer automatically gets the default plan — usually the free one. One default per group. A default can carry no prices at all — a "$0" plan with paid items (per-seat charges, prepaid packs) is a paid plan, not a default. If every plan bills something, there is no default; customers subscribe.\n- **Trials**: ask "does the trial need a card?" (guess from their motion — PLG usually no). Card → `free_trial` on the paid plan. No card → a **separate free trial plan** (`pro_trial`: no price, the paid plan\'s items, auto-enabled when everyone starts on it); the paid plan stays untouched, since a default plan can never be paid. For trial modeling details — trial-behavior on plans, read `references/plan.md` in the `autumn-concepts` skill.\n- **What is a plan attached to?** The customer, or each thing they own (workspace, project, site)? "Pro is $200 per workspace" → attached per entity. Pin this down — it changes everything downstream.\n- **Who uses each metered feature?** The customer as a whole, or each entity? If entities: one shared balance or separate ones? "Shared across…" → shared ↦ Decide (balances).\n\nDone when you know what attaches where, who consumes what, and how trials and signup work.\n\n### Step 4 — Licenses\n\nTriggered whenever the customer pays per unit of some entity — seats, workspaces, projects, sites, members: "each X is $10/month", "comes with 3 X". When you see one, always ask: **what does one X come with?** Never skip this because the user didn\'t say "seat". For modeling a license — the concept: child plan, license link, customize, read `references/licenses.md` in the `autumn-concepts` skill.\n\n- Nothing of its own ("$10 per seat", just a count) → a per-unit priced item on the plan. No entities. The common case.\n- The unit gets something of its own ("each seat gets 100 credits", "every workspace has its own allowance") → a **license**: a small plan of its own that the parent plan hands out per unit.\n- Units must be assigned, reassigned, or sit empty → also licenses. Rare — confirm they need it.\n\nFor deciding between a per-unit item, licenses, and entity-attached plans for a countable paid unit, read `references/fork-licenses.md`.\n\n### Before deciding: restate\n\nOnly after your questions are answered — never announce it in advance, and never restate facts nobody has confirmed yet. One short message: the plans, what\'s metered, what attaches where, who shares what. Let the user correct it. A wrong fact here is much cheaper than a wrong structure later.\n\nIf the user gave you everything up front and you asked nothing, skip the separate restate — the structure message (checklist item 5) does that job.\n\n## Decide\n\nResolve these with all facts in hand. Each has a default — when the facts genuinely don\'t settle it, show both options in one line each and ask.\n\n**Variant or separate plan?**\nA variant can change the price, swap items in or out, and change the trial — nothing else.\n\n- "Pro monthly / Pro annual, same features" → variant. (Annual usually still resets allowances monthly — billing and reset intervals are independent. Confirm.)\n- Volume buckets/tiers — one question decides: **is each bucket the subscription itself (→ a variant per bucket) or a purchase on top of one (→ a prepaid item)?**\n- Different features per tier → separate plans. One plan per tier is normal, not a smell.\n\nFor deciding whether volume buckets/tiers are variants of the plan or one prepaid volume-tiered item — the tells, the trap, when variants are forced, read `references/fork-variants.md`.\n\n**Add-on, or part of the plan?** Two independent questions: is the purchase priced/bundled per plan (→ item on each plan) or one offer across plans (→ one add-on plan, sizes as tiers on its prepaid item)? And does a plan exist at the level its balance is shared at (→ item there) or are all plans entity-attached (→ a customer-level add-on is forced)? Auto-recharge needs the prepaid item to exist — add it in Shape, it\'s structural.\n\nFor modeling packs, top-ups, or any purchase bought on top of a plan — add-on vs plan item, and what level it sits at, read `references/fork-addon.md`.\n\n**Where do balances and purchases live?**\nThe rule: **purchases and balance at the customer; usage tracking and caps at the entity.** Grants "shared across…" entities → pooled (`pooled: true` on the item); separate per-entity balances → no pooling; overage stays on the entity\'s plan either way.\n\nFor deciding where balances and purchases live — pooled grants, per-entity balances, customer-level packs, overage placement, read `references/fork-pooled.md`.\n\n**Groups?**\nCan one customer hold two plans at once from different lines (a support plan AND a sales plan)? → one group per line. Within a group, attaching a plan replaces the current one; that\'s what makes upgrades work.\n\n**One-off?**\n"Lifetime deal", "one-time pack" → a plan with a one-off price: single invoice, no subscription, balances never reset.\n\n## Check\n\nCompare the structure against the shapes in `references/cases.md`. If their pricing matches a known shape but your structure differs, either say why or fix it.\n\nShortcuts that are usually wrong — catch yourself before Show:\n\n| You\'re thinking | Check first |\n|---|---|\n| "seats + credits → licenses" | Are the credits per seat, or one shared pot? Shared → per-unit seats + pooled balance, no licenses. |\n| "annual pricing → separate plan" | Same features? → variant. |\n| "tiers → one plan with tiers" | Does overage or anything else differ per tier? → plan per tier. |\n| "they said credits but it\'s one action → plain meter" | Credits are always a credit system. |\n| "packs belong on the plan" | Are they shared across entities or plans? → add-on. |\n| "everyone starts on a Pro trial → trial + auto-enable on Pro" | A default plan can never be paid. → separate free `pro_trial` plan (Pro\'s items, no price, auto-enabled); Pro untouched. |\n| "several top-up sizes → one add-on plan per size" | Do the sizes differ only in quantity and price? → volume tiers on one prepaid item, one add-on plan. |\n| "the pack is priced per plan → an add-on per plan" | Per-plan pricing IS plan differentiation → a prepaid item on each base plan, no add-ons. Add-ons are for one offer shared across plans, or when no plan exists at the shared level. |\n| "the seat differs per plan → one seat plan per parent" | ONE child plan carrying the mainline take; each differing parent\'s license carries its own diff via `customize`. Never a `<parent>_seat` plan per parent. |\n\nFor checking the derived structure against known-good shapes, read `references/cases.md`.\n\n## Show\n\nPresent the structure in the "Showing the catalog" format below — the same one used for every catalog display, catalog inside a fenced code block. Numbers you don\'t have yet stay open, never invented: write the line without them ("AI messages per month — amount TBD"). Structure notes go in parentheses on the line they describe. The whole message:\n\n````\nHere\'s the structure I\'d build:\n\n```\nFeatures: AI credits (credit system — chat and image messages draw from it) · SSO (on/off)\n\nFree — no price, everyone starts here\n - 100 AI credits per month\nPro — $20/month, or annual (same features)\n - AI credits per month — amount TBD\n - SSO\nCredit pack (add-on) — price TBD, shared across all workspaces\n```\n\nI assumed: credits reset monthly, no rollover. Anything wrong?\n````\n\nList every assumption. Get a clear yes — "sounds good" without reading is not a yes. But if the user already told you to go ahead without review ("no need to ask", "just build it"), show the structure and keep going — don\'t stop to wait. If a late fact changes the structure, redo the affected decision, update the structure, and show what changed in one line.\n\n## Fill\n\nStructure agreed — now finish it. Four moves, in order.\n\n**1 — Fill in what\'s known.** Everything the user already said goes straight into the draft. Never re-ask a confirmed fact.\n\n**2 — Ask for missing essentials.** Values with no sane default: base prices, included amounts, per-unit prices, tier boundaries, trial length. Never invent one — a missing number is a question. Batch per the how-to-ask rules.\n\n**3 — Sweep the options.** Ask each of these **once for the whole catalog**, in plain words — never item by item. The answer distributes to every item it touches ("carry over on both plans, or just Growth?" only if they hint at a difference). Raise a bucket only when the structure makes it relevant; skip anything already answered.\n\n- **Carry-over** — any allowance that resets: "Should unused messages carry over month to month, or reset clean?"\n- **Running out** — any metered feature: "When they run out — hard stop, or keep going and bill the extra?" (often settled in Shape; skip if so)\n- **Top-ups** — credits present or buying-more mentioned: "Can they buy more before the reset?" → a one-off prepaid item\n- **Guardrails** — any plan with overage or heavy usage: "Any caps on how fast or how far usage can run — like a daily limit, or a ceiling on overage? Want customers warned as they approach limits?" → plan-level billing controls\n- **Anything else on/off** — always, it\'s cheap: "Any other on/off differences between plans — SSO, priority support, API access?"\n\nThe sweep exists so the user hears what\'s configurable without being marched through every item. Behind it, check every knob yourself — this list is internal, never show it:\n\n- per plan: base price · trial (length, unit, card — all explicit) · default plan · group · billing controls (guardrails answered → `billingControls` on the plan)\n- per item: billing method · included · price or tiers (tier behavior explicit) · reset · rollover · pooled (if Shape chose shared balances) · purchase caps · the one-off item auto-recharge needs\n\nLanding a guardrail answer means picking the right control — windowed cap vs overage ceiling vs alert vs allow-past-balance vs auto-recharge are different knobs with different fields. What each one is, the three overage knobs, and the plan→customer inheritance are the `autumn-concepts` skill\'s billing-controls reference — read it before writing `billingControls`. The flow rules here:\n\n- Plan-level controls are defaults every subscriber inherits — the right home for anything true of the whole plan ("free users max 20 emails/day"). Per-customer exceptions are a billing/customer operation, not catalog work.\n- A cap stated alongside an allowance ("1,000 a month but never more than 20 a day") is a usage limit on the plan, not a second item or a smaller allowance.\n- "Track it but don\'t bill it" / "let them run over, we\'ll invoice manually" → overage knobs on the plan, not a $0 price.\n- Auto-recharge needs its one-off prepaid item (already on the per-item list) AND the `autoTopups` control.\n\n**4 — Propose, then finalize.** One message: the full catalog in the format below, then "I assumed:" listing every knob you defaulted. Fold corrections in. Then write the config — and before saving, re-read every amount in it: dollars, never cents ($600 is `600`, not `60000`). Validate with `atmn push` (a preview; nothing is applied until `--yes`), fix what it flags, and show the final catalog — same format, no assumptions list. **Done means the config is written and valid — a summary is not done.**\n\n### Showing the catalog\n\nUse **exactly this format** every time you show the catalog — the Show structure message, the Fill proposal, and the done message alike. Never a markdown table, never a different layout per message. Send it as a fenced code block (```) so the indentation survives markdown rendering — plan names on bare lines otherwise get folded into the previous plan\'s list. It\'s the same grammar the dashboard renders. Features first with their kind, then plans, one line per item:\n\n```\nFeatures: AI messages (usage, resets) · Seats (held, not used up) · Credits (credit system — 1 message = 1 credit) · SSO (on/off)\n\nPro — $20/month\n - 500 AI messages per month\n · unused carry over, up to 500, for 1 month\n - 500 AI messages per month, then $0.01 per message\n - $10 per 1,000 credits\n - 5,000 credits for $50 per month\n - 3 seats included, then $10 per seat\n - $18 - $15 per seat\n - Unlimited projects\n - SSO\n - 100 credits per seat per month\nTeam — $500/month\n - 5 seats included, then $40 per seat per month\n each seat gets:\n · 100 summaries per month\n · SSO\nCredit pack (add-on) — $10 for 1,000 credits, buy anytime\n```\n\nThe Features line names every feature and its kind in plain words: `(usage, resets)` for consumable meters, `(held, not used up)` for non-consumable ones like seats, `(credit system — …)` with its action mappings, `(on/off)` for boolean. Item lines, top to bottom: plain allowance · allowance with overage · pure usage price (per billing unit) · prepaid bucket · included + prepaid per-unit · tiered rate shown first-to-last · unlimited · boolean (name only, never "enabled") · per-entity grant · one-off add-on. Numbers get commas; "then …" says what happens after the included runs out; annual variants go inline ("or $200/year — messages still reset monthly").\n\nConfigured item properties — carry-over, purchase caps, top-up behavior — are never their own `-` lines: indent them under the item they belong to as `·` lines, one behavior each, so the hierarchy is visible. Only show what\'s configured; defaults (like usage simply stopping when no overage price is set) get no line.\n\nBilling controls follow the same rule: `·` lines under the item they guard, in plain words — `· max 200 per day` · `· overage capped at 20% over` · `· overage tracked, not billed` · `· warned at 80%` · `· auto-buys 1,000 credits when below 100`. Never the schema names (usage_limits, spend_limits) in user-facing text.\n\n### Config gotchas\n\n- Amounts are plain dollars everywhere — base prices, tier `flatAmount`s, unit prices: $20 is `20`, never `2000`, and a $100 tier is `flatAmount: 100`, never `10000`. Re-check every number before writing; cents is the most common wrong config.\n- A default/auto-enabled plan can\'t be paid: no base price, no paid items.\n- A prepaid quantity **includes** the included amount, and so does each tier\'s `to` — the first tier\'s `to` must exceed `included`.\n- `billingUnits` rounds usage **up** when billing.\n- Set explicitly, never lean on defaults: `billingMethod` and `interval` on every item price, `tierBehavior` on tiered prices, and every trial field (`durationLength`, `durationType`, `cardRequired`, `onEnd`). Explicit defaults cause no spurious diffs.\n- Volume tiers charge the flat amount of the reached tier and are prepaid-only; graduated (the default) sums across brackets.\n- Rollover needs a resetting allowance; `max` and `maxPercentage` are mutually exclusive; `expiryDurationType` is required.\n- `billingControls` is a plain object on the plan with camelCase fields like the rest of the config (`featureId`, `overageLimit`); each control list replaces wholesale on update.\n- Pooled balances are config: `pooled: true` on the entity plan\'s item. Concluding "shared across workspaces" in Shape and then omitting the flag is the classic miss.\n- Pooled grant + overage = two items on the plan: the pooled grant carries no price; a separate usage-priced item (`included: 0`) carries the overage. A pooled item can\'t itself be usage-priced.\n- Don\'t write `proration` — leave it out and take server defaults.\n- Trial end behavior is `freeTrial.onEnd`: `"bill"` (default) charges when the trial ends, `"revert"` expires it and restores the previous plan.\n- Every plan row carries `versionSlug` and `active`, and every variant row and license link `versionSlug`. A plan\'s rows are its versions — exactly one `active: true` — and a version row left out of `plans` is deleted. How rows express versions, renames and drafts: `references/atmn.md`.\n\nThe config uses the builders `feature`, `plan`, `variant`, `license` as plain function calls with object arguments; items are plain objects inside a plan, and the file\'s default export is `atmn({...})` naming every collection. Never guess other functions or fields; the full shapes are in `references/atmn.md`.\n\n```ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const credits = feature({\n featureId: "credits",\n name: "Credits",\n type: "credit_system",\n creditSchema: [{ meteredFeatureId: "messages", creditCost: 1 }],\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [\n { featureId: credits.featureId, included: 500, reset: { interval: "month" } },\n ],\n});\n\nexport default atmn({ features: [credits], plans: [pro] });\n```\n\nPattern deep-dives, split one file per pattern under `references/` — read the matching one when filling that pattern\'s details:\n\nFor usage overage pricing, read `references/usage-based-pricing.md`.\nFor prepaid quantities (seats, packs, buckets), read `references/prepaid-pricing.md`.\nFor volume-tiered prices, read `references/volume-based-tiers.md`.\nFor $X per unit, read `references/per-unit-pricing.md`.\nFor billing vs reset intervals, read `references/recurring.md`.\nFor one-off purchases / top-ups, read `references/one-off-purchases.md`.\nFor auto-recharge, read `references/auto-top-ups.md`.\nFor carry-over, read `references/rollovers.md`.\nFor trial details, read `references/trials.md`.\nFor entity plans and license attach flows, read `references/entity-plans.md`.\nFor credit schemas and mappings, read `references/credit-systems.md`.\nFor default plans / auto-enable, read `references/free-plans.md`.\nFor add-on balance stacking, read `references/add-ons.md`.\nFor what variants can change, read `references/plan-variants.md`.\n\n## Conduct\n\n- In an existing config, match its patterns: if sibling plans carry their prepaid purchases as items, the new plan does too — don\'t introduce a different structure for the same kind of thing.\n- Stable lowercase IDs with underscores: `pro_plan`, `chat_messages`.\n- `entityFeatureId` is deprecated. Never mention or use it unless the user\'s existing config already has it.\n- Per-unit pricing pairs a base fee with the per-unit item ("$X/seat" plans still have a base price, even $0).\n- Speak plainly: "plans", "what\'s included", "extra usage". Schema words stay in the config — say "carry over" not "rollover", "shared across workspaces" not "pooled", "paid upfront" / "billed at month end" not "prepaid" / "usage_based".\n- Never volunteer what Autumn can or can\'t do. Don\'t offer options Autumn can\'t model, and don\'t explain limitations unprompted — only address one when the user directly asks to model that specific thing, and even then lead with the closest thing that works.\n- Start simple: the most important features first, confirm before adding more.\n\nBefore finishing: re-check the STRICT RULES at the top against the config you wrote.\n\n## Catalog operations\n\nFor using atmn, autumn.config.ts, or headless push flows, read `references/atmn.md`.\n\nFor changing an existing catalog: previewing, versioning, migrations, variant propagation, read `references/catalog-update.md`.\n\nFor creating or changing coupons, promo codes, feature grants, or referral programs, read `references/rewards.md`.',
42
42
  references: [
43
43
  {
44
44
  path: "references/usage-based-pricing.md",
@@ -138,7 +138,7 @@ export const SKILLS: readonly BundledSkill[] = [
138
138
  {
139
139
  path: "references/atmn.md",
140
140
  contents:
141
- '# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog\'s state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog\'s state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill\'s: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan\'s only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server\'s stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant\'s customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn\'s:\n\n- A variant is an entry in its base row\'s `variants` array and a license link an entry in the parent row\'s `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from "atmn";\n\nexport const messages = feature({\n featureId: "messages",\n name: "Messages",\n type: "metered",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: "pro_annual",\n versionSlug: "v2",\n name: "Pro Annual",\n customize: { price: { amount: 250, interval: "year" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: "pro_annual",\n versionSlug: "v1",\n name: "Pro Annual",\n customize: { price: { amount: 200, interval: "year" } },\n});\n\nexport const proV2 = plan({\n planId: "pro",\n versionSlug: "v2",\n active: true,\n name: "Pro",\n price: { amount: 25, interval: "month" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: "month" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: "prod_…", // written back by push --yes / pull\n planId: "pro",\n versionSlug: "v1",\n active: false,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: "month" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: "month" },\n price: {\n amount: 0.9,\n billingMethod: "usage_based",\n billingUnits: 1000,\n interval: "month",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server\'s catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org\'s catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means "don\'t register this webhook there", which is not an error:\n\n```ts\nimport { atmn, webhook } from "atmn";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: "billing",\n events: ["billing.updated", "balances.limit_reached"],\n url: {\n live: "https://myapp.com/api/autumn", // atmn push -p\n sandbox: "https://staging.myapp.com/api/autumn", // the default sandbox\n "qa-team": "https://qa.myapp.com/api/autumn", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox\'s slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work. `events` needs at least one entry.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook\'s signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that\'s rejected or belongs to another org skips its env with a warning and leaves that env\'s keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env\'s key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn\'t let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don\'t have an account.` — hand that to the setup flow, don\'t ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything.',
141
+ '# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog\'s state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog\'s state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill\'s: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan\'s only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server\'s stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant\'s customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn\'s:\n\n- A variant is an entry in its base row\'s `variants` array and a license link an entry in the parent row\'s `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from "atmn";\n\nexport const messages = feature({\n featureId: "messages",\n name: "Messages",\n type: "metered",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: "pro_annual",\n versionSlug: "v2",\n name: "Pro Annual",\n customize: { price: { amount: 250, interval: "year" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: "pro_annual",\n versionSlug: "v1",\n name: "Pro Annual",\n customize: { price: { amount: 200, interval: "year" } },\n});\n\nexport const proV2 = plan({\n planId: "pro",\n versionSlug: "v2",\n active: true,\n name: "Pro",\n price: { amount: 25, interval: "month" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: "month" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: "prod_…", // written back by push --yes / pull\n planId: "pro",\n versionSlug: "v1",\n active: false,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: "month" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: "month" },\n price: {\n amount: 0.9,\n billingMethod: "usage_based",\n billingUnits: 1000,\n interval: "month",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server\'s catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org\'s catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means "don\'t register this webhook there", which is not an error:\n\n```ts\nimport { atmn, webhook } from "atmn";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: "billing",\n events: ["billing.updated", "balances.limit_reached"],\n url: {\n live: "https://myapp.com/api/autumn", // atmn push -p\n sandbox: "https://staging.myapp.com/api/autumn", // the default sandbox\n "qa-team": "https://qa.myapp.com/api/autumn", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox\'s slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work.\n- Leave `events` out to receive every event. Don\'t write `events: []`.\n- Vercel webhooks require their own distinct webhook and are a mutually exclusive variant to the rest. It can only listen to Vercel events and cannot later be changed to listen to non-Vercel events.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook\'s signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that\'s rejected or belongs to another org skips its env with a warning and leaves that env\'s keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env\'s key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn\'t let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don\'t have an account.` — hand that to the setup flow, don\'t ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything.',
142
142
  },
143
143
  {
144
144
  path: "references/catalog-update.md",
@@ -156,9 +156,9 @@ export const SKILLS: readonly BundledSkill[] = [
156
156
  name: "autumn-integrate",
157
157
  description:
158
158
  "Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.",
159
- version: "2.0.40",
159
+ version: "2.0.42",
160
160
  markdown:
161
- "---\nname: autumn-integrate\ndescription: Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.\nversion: 2.0.40\n---\n\n# Integrate\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines the objects these calls operate on — customers, plans, balances, entities.\n\n## STRICT RULES\n\n1. **Everything is backend.** All Autumn calls run in the app's server code with `AUTUMN_SECRET_KEY` — never in the browser. The frontend gets billing data only through the app's own endpoints.\n2. **The old frontend layer is deprecated — never use or suggest it**: `autumnHandler` mounts, React hooks (`useCustomer`, `useEntity`, `AutumnProvider`), shadcn components. If an app already uses them, don't rip them out unasked — but build new work backend-only.\n3. **Resolve the customer before anything else.** Every handler that checks, tracks, or attaches must serve a brand-new user: `customers.getOrCreate` with the app's own stable id (user or org id from auth — whoever pays — never an email). Never assume the customer exists.\n4. **An error response is a failure.** A 4xx/5xx from any Autumn call is never \"working\" — read the error, fix the call.\n\n## Order of operations\n\nWork these in order; each step names what to settle and where the details live. Read the referenced doc before implementing the step — the snippets and parameters are there, not here.\n\n**1 — Discover the app.** Framework, server entry points, where auth lives, which existing routes handle the actions being billed. Settle the customer id: the app's stable user or org id — whoever pays. If the product bills per workspace/project/seat, those are entities under one customer, not separate customers.\n\n**2 — Settle the level.** Before writing any flow, ask the user how their plans and features actually work: what does a plan get bought for — the whole account, or each workspace/deployment/seat under it? And where does each feature's balance live — shared across the account, or per unit? The catalog hints (entity-scoped items, license plans) but doesn't decide — a plan can attach per deployment while credit packs stay account-wide. The answers flow through everything: `billing.attach` takes `entityId` for entity-level plans, `check`/`track` take `entityId` for entity-scoped balances, billing controls set at the matching level. Mixing levels silently misbills — an account-wide check against a per-seat balance always passes. For entities, entity-scoped balances, when to use which level, read `references/feature-entities.md`. For plans that attach per entity, read `references/entity-plans.md`.\n\n**3 — Install and connect.** `autumn-js` (or the platform's SDK) + `AUTUMN_SECRET_KEY` in server env. One shared client in server code. For install, client setup, the end-to-end happy path, read `references/setup.md`.\n\n**4 — Customer creation.** `customers.getOrCreate` where the app knows who the user is — signup/login hook, or lazily in billing handlers. Pass name/email when available. A default (auto-enable) plan attaches on creation — no attach call for the free tier. If step 2 settled anything at entity level, mirror this for entities: when the app creates its unit (a workspace, a deployment, a seat), create the entity then — usually in the same handler, right before the entity-level attach or check that needs it. For B2B invoicing, tax details (address, VAT IDs, reverse charge, a PO number on invoices) go in `billing_details` on create or update — written to Stripe, read back with `expand: [\"billing_details\"]`. For getOrCreate parameters, Stripe linking, pre-creating, read `references/creating-customers.md`. For billing_details fields, tax ID add/remove, replace semantics, read `references/managing-customers-billing-details.md`. For creating entities, entity parameters, read `references/feature-entities.md`.\n\n**5 — Purchase and plan changes.** All through two calls, split by intent (pass `entityId` when the plan attaches at entity level — step 2):\n - `billing.attach` — getting on a plan, AND upgrades/downgrades within a plan group: attach the new plan; a higher (monthly-normalized) price applies immediately with proration, a lower one schedules for period end. `planSchedule` overrides. Default attach charges a saved card in place and returns `checkout_url` only when payment is actually needed — handle both outcomes. Force `redirectMode: \"always\"` only when the product wants hosted checkout unconditionally; custom flows preview first. For hosted vs custom checkout, redirectMode, previewAttach, read `references/payment-flow.md`. For upgrade/downgrade behavior, scheduled downgrades, carry-over on upgrade, read `references/subscription-lifecycle.md`.\n - `billing.update` — changing the subscription you're on: prepaid quantities (seats), `cancelAction` (`cancel_end_of_cycle` / `cancel_immediately` / `uncancel`), customization. It never switches plans — that's attach's job. Preview twins exist for both calls — use them wherever the app shows a confirmation screen. For quantities, cancelAction, previewUpdate, prorationBehavior, read `references/updating-subscriptions.md`.\n - When several plans move together — a plan plus its add-on in one purchase, or tied cancellation — use the multi twins: `billing.multiAttach` (one checkout / one payment covering every plan) and `billing.multiUpdate` (one atomic call applying a `cancelAction` per plan). Two sequential attach/update calls mean two checkouts or a half-canceled customer, and concurrent ones trip the per-customer lock.\n - Payment snags are response **state, not exceptions**: `required_action` (3DS, payment method, failure) plus `payment_url` — handle the states, surface the URL. For 3DS, payment failures, required_action codes, read `references/edge-cases.md`.\n - For deep parameter detail on attach, update, and the other billing actions (createSchedule, discounts, custom terms), the `autumn-billing` skill is the reference — it's written for operating billing directly, but its parameter and edge-case knowledge applies when implementing these calls in code.\n\n**6 — Check and track.** In the billed action's own handler: `check` before the work, `track` after it succeeds. Gate on `allowed` — don't re-derive access from balances. Pass `requiredBalance` when an action costs more than 1, and `entityId` when the balance lives on an entity (step 2) — check and track must agree on the level. For the check → act → track pattern, read `references/gating.md`. For check parameters, allowed semantics, credit systems, entity checks, read `references/check.md`. For track and setUsage, read `references/tracking-usage.md`.\n\n**7 — Billing controls.** Per-customer policy on top of the plan: set via `customers.update` with `billing_controls` (`spend_limits`, `usage_limits`, `usage_alerts`, `auto_topups`, `overage_allowed`), or at purchase time by passing `billing_controls` on `billing.attach`. The flagship shape is a user-facing \"overage billing on/off\" toggle: off maps to a `spend_limits` entry with `skip_overage_billing: true` — overage still accrues but is never invoiced — plus an `overage_limit` (use `limit_type: \"usage_percentage\"`, e.g. `20` = overage up to 20% of the plan allowance) so the user is blocked at the cap instead of running up unbilled usage forever; on means clearing or replacing the entry (`skip_overage_billing: false`). Whether overage needs enabling at all depends on the plan: items with a usage price already allow it (controls only modulate the billing); pure-grant items hard-stop at zero and need `overage_allowed` per customer — which permits the overage but doesn't bill it. Alerts (\"email at 80%\") are `usage_alerts` with `threshold_type: \"usage_percentage\"`; prepaid balances self-replenish via `auto_topups`. For all five controls, field shapes, customer vs entity level, plan defaults, read `references/billing-controls.md`. For spend limit semantics, skip_overage_billing, usage alerts, read `references/spend-limits.md`. For auto top-up setup and requirements, read `references/auto-top-ups.md`.\n\n**8 — Billing data for the frontend.** The app exposes its own endpoints calling `customers.get` / `plans.list` server-side; the UI reads those. For customer payload, plans list with eligibility, portal, read `references/display-billing.md`.\n\n**9 — Verify.** Call the app's routes as a fresh user; confirm in sandbox that the customer exists, checks gated, usage recorded, purchase reached checkout. `customers.get` is the oracle. **Done means verified behavior — compiling is not done.** Then undo what verifying created — release the license assignments, delete the entities, and reverse the usage your test recorded (a negative `track` or `setUsage` back), and nothing more. Never delete the customer or any state that existed before you started; pre-existing subscriptions and assignments are real. Verification also re-runs flows, so billing endpoints must tolerate repeats: joining a plan you're already on returns success, not a crash.\n\n## Gotchas the docs don't cover\n\n- **Concurrent billing mutations on one customer return 429** — attach/update share a per-customer lock; serialize instead of retrying blindly.\n- **A denied check should say what ran out** — include the feature and its balance state in the response, not a bare unexplained 403. What the caller does with it is the app's call: an API returns a clear error code, a dashboard might prompt an upgrade.\n\n## Conduct\n\n- Follow the app's existing patterns — router style, error handling, response shapes. Autumn code should look like the app wrote it.\n- Use catalog ids exactly; never invent feature or plan ids. Missing from the catalog → say so; that's an `autumn-catalog` change, not a workaround.\n- Smallest working integration first: one gated action end to end, verified, before spreading wider.\n\nGoing to production: For live keys, production checklist, read `references/deploy.md`. For fail-open behavior during Autumn outages, read `references/fail-open.md`.",
161
+ "---\nname: autumn-integrate\ndescription: Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.\nversion: 2.0.42\n---\n\n# Integrate\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines the objects these calls operate on — customers, plans, balances, entities.\n\n## STRICT RULES\n\n1. **Everything is backend.** All Autumn calls run in the app's server code with `AUTUMN_SECRET_KEY` — never in the browser. The frontend gets billing data only through the app's own endpoints.\n2. **The old frontend layer is deprecated — never use or suggest it**: `autumnHandler` mounts, React hooks (`useCustomer`, `useEntity`, `AutumnProvider`), shadcn components. If an app already uses them, don't rip them out unasked — but build new work backend-only.\n3. **Resolve the customer before anything else.** Every handler that checks, tracks, or attaches must serve a brand-new user: `customers.getOrCreate` with the app's own stable id (user or org id from auth — whoever pays — never an email). Never assume the customer exists.\n4. **An error response is a failure.** A 4xx/5xx from any Autumn call is never \"working\" — read the error, fix the call.\n\n## Order of operations\n\nWork these in order; each step names what to settle and where the details live. Read the referenced doc before implementing the step — the snippets and parameters are there, not here.\n\n**1 — Discover the app.** Framework, server entry points, where auth lives, which existing routes handle the actions being billed. Settle the customer id: the app's stable user or org id — whoever pays. If the product bills per workspace/project/seat, those are entities under one customer, not separate customers.\n\n**2 — Settle the level.** Before writing any flow, ask the user how their plans and features actually work: what does a plan get bought for — the whole account, or each workspace/deployment/seat under it? And where does each feature's balance live — shared across the account, or per unit? The catalog hints (entity-scoped items, license plans) but doesn't decide — a plan can attach per deployment while credit packs stay account-wide. The answers flow through everything: `billing.attach` takes `entityId` for entity-level plans, `check`/`track` take `entityId` for entity-scoped balances, billing controls set at the matching level. Mixing levels silently misbills — an account-wide check against a per-seat balance always passes. For entities, entity-scoped balances, when to use which level, read `references/feature-entities.md`. For plans that attach per entity, read `references/entity-plans.md`.\n\n**3 — Install and connect.** `autumn-js` (or the platform's SDK) + `AUTUMN_SECRET_KEY` in server env. One shared client in server code. For install, client setup, the end-to-end happy path, read `references/setup.md`.\n\n**4 — Customer creation.** `customers.getOrCreate` where the app knows who the user is — signup/login hook, or lazily in billing handlers. Pass name/email when available. A default (auto-enable) plan attaches on creation — no attach call for the free tier. If step 2 settled anything at entity level, mirror this for entities: when the app creates its unit (a workspace, a deployment, a seat), create the entity then — usually in the same handler, right before the entity-level attach or check that needs it. For B2B invoicing, tax details (address, VAT IDs, reverse charge, a PO number on invoices) go in `billing_details` on create or update — written to Stripe, read back with `expand: [\"billing_details\"]`. For getOrCreate parameters, Stripe linking, pre-creating, read `references/creating-customers.md`. For billing_details fields, tax ID add/remove, replace semantics, read `references/managing-customers-billing-details.md`. For creating entities, entity parameters, read `references/feature-entities.md`.\n\n**5 — Purchase and plan changes.** All through two calls, split by intent (pass `entityId` when the plan attaches at entity level — step 2):\n - `billing.attach` — getting on a plan, AND upgrades/downgrades within a plan group: attach the new plan; a higher (monthly-normalized) price applies immediately with proration, a lower one schedules for period end. `planSchedule` overrides. Default attach charges a saved card in place and returns `checkout_url` only when payment is actually needed — handle both outcomes. Force `redirectMode: \"always\"` only when the product wants hosted checkout unconditionally; custom flows preview first. For hosted vs custom checkout, redirectMode, previewAttach, read `references/payment-flow.md`. For upgrade/downgrade behavior, scheduled downgrades, carry-over on upgrade, read `references/subscription-lifecycle.md`.\n - `billing.update` — changing the subscription you're on: prepaid quantities (seats), `cancelAction` (`cancel_end_of_cycle` / `cancel_immediately` / `uncancel`), customization. It never switches plans — that's attach's job. Preview twins exist for both calls — use them wherever the app shows a confirmation screen. For quantities, cancelAction, previewUpdate, prorationBehavior, read `references/updating-subscriptions.md`.\n - When several plans move together — a plan plus its add-on in one purchase, or tied cancellation — use the multi twins: `billing.multiAttach` (one checkout / one payment covering every plan) and `billing.multiUpdate` (one atomic call applying a `cancelAction` per plan). Two sequential attach/update calls mean two checkouts or a half-canceled customer, and concurrent ones trip the per-customer lock.\n - Payment snags are response **state, not exceptions**: `required_action` (3DS, payment method, failure) plus `payment_url` — handle the states, surface the URL. For 3DS, payment failures, required_action codes, read `references/edge-cases.md`.\n - For deep parameter detail on attach, update, and the other billing actions (createSchedule, discounts, custom terms), the `autumn-billing` skill is the reference — it's written for operating billing directly, but its parameter and edge-case knowledge applies when implementing these calls in code.\n\n**6 — Check and track.** In the billed action's own handler: `check` before the work, `track` after it succeeds. Gate on `allowed` — don't re-derive access from balances. Pass `requiredBalance` when an action costs more than 1, and `entityId` when the balance lives on an entity (step 2) — check and track must agree on the level. For the check → act → track pattern, read `references/gating.md`. For check parameters, allowed semantics, credit systems, entity checks, read `references/check.md`. For track and setUsage, read `references/tracking-usage.md`.\n\n**7 — Billing controls.** Per-customer policy on top of the plan: set via `customers.update` with `billing_controls` (`spend_limits`, `usage_limits`, `usage_alerts`, `auto_topups`, `overage_allowed`), or at purchase time by passing `billing_controls` on `billing.attach`. The flagship shape is a user-facing \"overage billing on/off\" toggle: off maps to a `spend_limits` entry with `skip_overage_billing: true` — overage still accrues but is never invoiced — plus an `overage_limit` (use `limit_type: \"usage_percentage\"`, e.g. `20` = overage up to 20% of the plan allowance) so the user is blocked at the cap instead of running up unbilled usage forever; on means clearing or replacing the entry (`skip_overage_billing: false`). Whether overage needs enabling at all depends on the plan: items with a usage price already allow it (controls only modulate the billing); pure-grant items hard-stop at zero and need `overage_allowed` per customer — which permits the overage but doesn't bill it. Alerts (\"email at 80%\") are `usage_alerts` with `threshold_type: \"usage_percentage\"`; prepaid balances self-replenish via `auto_topups`. For all five controls, field shapes, customer vs entity level, plan defaults, read `references/billing-controls.md`. For spend limit semantics, skip_overage_billing, usage alerts, read `references/spend-limits.md`. For auto top-up setup and requirements, read `references/auto-top-ups.md`.\n\n**8 — Billing data for the frontend.** The app exposes its own endpoints calling `customers.get` / `plans.list` server-side; the UI reads those. For customer payload, plans list with eligibility, portal, read `references/display-billing.md`.\n\n**9 — Verify.** Call the app's routes as a fresh user; confirm in sandbox that the customer exists, checks gated, usage recorded, purchase reached checkout. `customers.get` is the oracle. **Done means verified behavior — compiling is not done.** Then undo what verifying created — release the license assignments, delete the entities, and reverse the usage your test recorded (a negative `track` or `setUsage` back), and nothing more. Never delete the customer or any state that existed before you started; pre-existing subscriptions and assignments are real. Verification also re-runs flows, so billing endpoints must tolerate repeats: joining a plan you're already on returns success, not a crash.\n\n## Gotchas the docs don't cover\n\n- **Concurrent billing mutations on one customer return 429** — attach/update share a per-customer lock; serialize instead of retrying blindly.\n- **A denied check should say what ran out** — include the feature and its balance state in the response, not a bare unexplained 403. What the caller does with it is the app's call: an API returns a clear error code, a dashboard might prompt an upgrade.\n\n## Conduct\n\n- Follow the app's existing patterns — router style, error handling, response shapes. Autumn code should look like the app wrote it.\n- Use catalog ids exactly; never invent feature or plan ids. Missing from the catalog → say so; that's an `autumn-catalog` change, not a workaround.\n- Smallest working integration first: one gated action end to end, verified, before spreading wider.\n\nGoing to production: For live keys, production checklist, read `references/deploy.md`. For fail-open behavior during Autumn outages, read `references/fail-open.md`.",
162
162
  references: [
163
163
  {
164
164
  path: "references/feature-entities.md",
@@ -261,9 +261,9 @@ export const SKILLS: readonly BundledSkill[] = [
261
261
  name: "autumn-concepts",
262
262
  description:
263
263
  "Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.",
264
- version: "2.0.40",
264
+ version: "2.0.42",
265
265
  markdown:
266
- "---\nname: autumn-concepts\ndescription: Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.\nversion: 2.0.40\n---\n\n# Concepts\n\nAutumn is a database for your application billing state: features, plans, customers, subscriptions, purchases, balances, flags, and billing controls. It helps you iterate on pricing, manage credit access and usage, control overage, and keep billing behavior connected to product access.\n\nAutumn is a layer above Stripe; Stripe still handles subscription management, invoicing, and payment processing. Autumn provisions and updates Stripe subscriptions, schedules, invoices, and related billing objects for you.\n\n## Object graph\n\n```txt\nOrganization\n- features[] -> Feature\n- plans[] -> Plan\n- customers[] -> Customer\n\nConfiguration model:\nFeature\n- referenced by -> Plan Item\n- credit_system type: maps actions to credit costs (flat or tiered)\n\nPlan\n- items[] -> Plan Item\n - feature_id -> Feature\n - optional price -> usage_based or prepaid feature price\n- price -> base recurring or one-off price\n- versions[] -> parallel definitions of this plan; ONE is active\n- variants[] -> Plan (a derived plan storing only its differences)\n- licenses[] -> Plan (a seat plan this plan hands out per seat)\n- aliases -> old plan ids that still resolve after a rename\n\nPlan Item\n- feature_id -> Feature\n- optional price -> usage_based or prepaid feature price\n- pooled? -> entity grants combine into one shared customer balance\n\nRuntime model:\nCustomer\n- subscriptions[] -> Subscription -> Plan (a specific version of it)\n- purchases[] -> Purchase -> Plan\n- balances[feature_id] -> Balance -> Feature\n - pooled balance: fed by entity grants, spent by any entity\n - rollover, expiry, usage windows live here\n- licenses[] -> CustomerLicense -> seats granted / in use / remaining\n- flags[feature_id] -> Flag -> Feature\n- billing_controls -> customer-level usage controls\n- entities[] -> Entity -> same runtime shape scoped under Customer\n\nEntity\n- belongs to -> Customer\n- subscriptions[] -> Subscription -> Plan\n- purchases[] -> Purchase -> Plan\n- balances[feature_id] -> Balance -> Feature\n- flags[feature_id] -> Flag -> Feature\n- license assignment -> holds one seat from the customer's pool\n\nFrom config to customer state:\nPlan + Customer --billing.attach--> Subscription or Purchase\nPlan + Customer + entity_id --billing.attach--> Entity-scoped Subscription or Purchase\nParent plan's licenses --licenses.attach--> seat assigned to an Entity\nSubscription/Purchase -> Balance or Flag provisioning\n```\n\nTwo relationships changed recently — worth stating plainly because older docs describe the old way:\n\n**Versions are groups of customers, not history.** A plan's versions used to be numbered steps in time, and the newest was always live. Now each version is a definition that some group of customers lives on, and exactly one is **active** — the one attach uses when no version is named. Which changes are edits and which are new versions, drafts, and how customers move: the plan definition below.\n\n**Plans connect to other plans.** A plan can have variants (an annual twin storing only its differences), and it can offer licenses (a small seat plan it hands out per seat). So plans form a graph, not a flat list.\n\nUse these definitions as the mental model when designing or changing Autumn\npricing. Reason in terms of features, plans, plan items, customers/entities, and\nbilling controls before writing any config or calling the API — most modeling\nmistakes come from conflating a feature with a plan item, or a plan-level price\nwith a per-feature price.\n\n## Definitions\n\nLoad the matching definition when reasoning about that object.\n\nFor defining a feature — the atomic unit Autumn gates, tracks, or bills, and its types, read `references/feature.md`.\n\nFor defining a plan — the attachable package of items and pricing, read `references/plan.md`.\n\nFor modeling plan items, or when you need concrete API request-body examples (included usage, prepaid, usage-based, tiers), read `references/plan-items.md`.\n\nFor using customize, reading plan update previews, or representing a small diff from a base plan, read `references/customize.md`.\n\nFor reasoning about free trials and when billing begins, read `references/trials.md`.\n\nFor distinguishing a customer from an entity (seats, sub-accounts) and their runtime billing state, read `references/customer-entity.md`.\n\nFor reasoning about billing controls — runtime caps, alerts, overage, and top-ups, read `references/billing-controls.md`.\n\nFor setting or reading a customer's billing address, tax IDs, tax exemption, or invoice custom fields, read `references/billing-details.md`.\n\nFor reasoning about licenses — seat plans a parent plan hands out, seat pools, assigning and releasing seats, read `references/licenses.md`.",
266
+ "---\nname: autumn-concepts\ndescription: Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.\nversion: 2.0.42\n---\n\n# Concepts\n\nAutumn is a database for your application billing state: features, plans, customers, subscriptions, purchases, balances, flags, and billing controls. It helps you iterate on pricing, manage credit access and usage, control overage, and keep billing behavior connected to product access.\n\nAutumn is a layer above Stripe; Stripe still handles subscription management, invoicing, and payment processing. Autumn provisions and updates Stripe subscriptions, schedules, invoices, and related billing objects for you.\n\n## Object graph\n\n```txt\nOrganization\n- features[] -> Feature\n- plans[] -> Plan\n- customers[] -> Customer\n\nConfiguration model:\nFeature\n- referenced by -> Plan Item\n- credit_system type: maps actions to credit costs (flat or tiered)\n\nPlan\n- items[] -> Plan Item\n - feature_id -> Feature\n - optional price -> usage_based or prepaid feature price\n- price -> base recurring or one-off price\n- versions[] -> parallel definitions of this plan; ONE is active\n- variants[] -> Plan (a derived plan storing only its differences)\n- licenses[] -> Plan (a seat plan this plan hands out per seat)\n- aliases -> old plan ids that still resolve after a rename\n\nPlan Item\n- feature_id -> Feature\n- optional price -> usage_based or prepaid feature price\n- pooled? -> entity grants combine into one shared customer balance\n\nRuntime model:\nCustomer\n- subscriptions[] -> Subscription -> Plan (a specific version of it)\n- purchases[] -> Purchase -> Plan\n- balances[feature_id] -> Balance -> Feature\n - pooled balance: fed by entity grants, spent by any entity\n - rollover, expiry, usage windows live here\n- licenses[] -> CustomerLicense -> seats granted / in use / remaining\n- flags[feature_id] -> Flag -> Feature\n- billing_controls -> customer-level usage controls\n- entities[] -> Entity -> same runtime shape scoped under Customer\n\nEntity\n- belongs to -> Customer\n- subscriptions[] -> Subscription -> Plan\n- purchases[] -> Purchase -> Plan\n- balances[feature_id] -> Balance -> Feature\n- flags[feature_id] -> Flag -> Feature\n- license assignment -> holds one seat from the customer's pool\n\nFrom config to customer state:\nPlan + Customer --billing.attach--> Subscription or Purchase\nPlan + Customer + entity_id --billing.attach--> Entity-scoped Subscription or Purchase\nParent plan's licenses --licenses.attach--> seat assigned to an Entity\nSubscription/Purchase -> Balance or Flag provisioning\n```\n\nTwo relationships changed recently — worth stating plainly because older docs describe the old way:\n\n**Versions are groups of customers, not history.** A plan's versions used to be numbered steps in time, and the newest was always live. Now each version is a definition that some group of customers lives on, and exactly one is **active** — the one attach uses when no version is named. Which changes are edits and which are new versions, drafts, and how customers move: the plan definition below.\n\n**Plans connect to other plans.** A plan can have variants (an annual twin storing only its differences), and it can offer licenses (a small seat plan it hands out per seat). So plans form a graph, not a flat list.\n\nUse these definitions as the mental model when designing or changing Autumn\npricing. Reason in terms of features, plans, plan items, customers/entities, and\nbilling controls before writing any config or calling the API — most modeling\nmistakes come from conflating a feature with a plan item, or a plan-level price\nwith a per-feature price.\n\n## Definitions\n\nLoad the matching definition when reasoning about that object.\n\nFor defining a feature — the atomic unit Autumn gates, tracks, or bills, and its types, read `references/feature.md`.\n\nFor defining a plan — the attachable package of items and pricing, read `references/plan.md`.\n\nFor modeling plan items, or when you need concrete API request-body examples (included usage, prepaid, usage-based, tiers), read `references/plan-items.md`.\n\nFor using customize, reading plan update previews, or representing a small diff from a base plan, read `references/customize.md`.\n\nFor reasoning about free trials and when billing begins, read `references/trials.md`.\n\nFor distinguishing a customer from an entity (seats, sub-accounts) and their runtime billing state, read `references/customer-entity.md`.\n\nFor reasoning about billing controls — runtime caps, alerts, overage, and top-ups, read `references/billing-controls.md`.\n\nFor setting or reading a customer's billing address, tax IDs, tax exemption, or invoice custom fields, read `references/billing-details.md`.\n\nFor reasoning about licenses — seat plans a parent plan hands out, seat pools, assigning and releasing seats, read `references/licenses.md`.",
267
267
  references: [
268
268
  {
269
269
  path: "references/feature.md",
@@ -10,8 +10,8 @@ export type Webhook = {
10
10
  sandbox?: string;
11
11
  [sandboxSlug: string]: string | undefined;
12
12
  };
13
- /** The events sent to this webhook. At least one. `vercel.*` events can't be mixed with other events. */
14
- events: Array<
13
+ /** The events sent to this webhook. Leave it out to send every event. `vercel.*` events can't be mixed with other events. Defaults to []. */
14
+ events?: Array<
15
15
  | "customer.products.updated"
16
16
  | "customer.threshold_reached"
17
17
  | "balances.usage_alert_triggered"
@@ -19,7 +19,7 @@ export const webhooksHaveWork = ({
19
19
  const DETAIL_INDENT = " ";
20
20
 
21
21
  const list = (values: readonly string[]): string =>
22
- values.length === 0 ? "(none)" : values.join(", ");
22
+ values.length === 0 ? "every event" : values.join(", ");
23
23
 
24
24
  /** Every field that moved, one line each; the URL first, since it is the one that bites. */
25
25
  const updateDetails = ({