Public: anyone can read this; the crew can change it.

Plans are a setting: free, small, medium and large, each with a size, a monthly price, a monthly crew credit and a forge count #11152

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Plans are now a setting: free, small, medium and large each carry a size, a monthly price, a monthly crew credit and a forge count, and an edit governs the next spawn with no restart.

shipped production is serving it deploy ea75315

opened this

Plans are now a setting: free, small, medium and large each carry a size, a monthly price, a monthly crew credit and a forge count, and an edit governs the next spawn with no restart.

Status. tsc and scripts/gate.ts are clean, and test/plans.test.ts and test/mcp-size.test.ts pass. The openapi, contract, mcp, both-doors, features, security-floor, spawn and admin-limits files I ran also pass. The change reaches ~1,900 tests, too broad for scripts/fast.ts to name, so I ran those groups by hand and left the full suite to the forge check. The sandbox blocked outbound calls from the spawn and mirror tests; they still passed.

What changed

  • Setting: src/settings/plans.ts holds the plans as one forge_setting value, with the four defaults from the issue (forge count 3 on each). Validation is in one place: one to eight plans, unique keys, free always present at $0, prices and credits to the cent, size from SIZES. An unparseable stored value reads as the defaults. Every edit is kept in forge_setting_change.
  • Admin edit: only a person admin can change plans.

- Page: a Plans form on /admin/limits, saved by POST /admin/plans. - API: PUT /api/v1/admin/plans with { plans: [...] }. - MCP: the existing limits tool takes plans, dry run first, and its read also lists them, so there is no new tool. test/mcp-size.test.ts passes and docs/MCP.md is regenerated.

  • Public read: GET /api/v1/plans answers only the named fields. It revalidates by ETag instead of the five-minute cache the other public tables use, so an edit shows at once.
  • Spawn: POST /api/v1/forges and the MCP spawn take plan. The size comes from the plan, and a size that disagrees with it is refused. The ledger records the plan.
  • Credit: the plan's creditUsd becomes the child's cap, read at spawn time so an edit applies to the next spawn. Spend resets each UTC calendar month, checked when the credit is read, so nothing has to run at midnight. Forges already made keep the credit they were given.
  • Counts: the free plan keeps personRefusal (forges.per_person and forges.storage_mb) and the anonymous limit. A paid plan is held to its own forges count among the person's forges on that plan. Admins are not limited. A stranger who asks for a paid plan is refused.
  • Tests and docs: test/plans.test.ts covers the defaults, an edit shown by the API and the next spawn, a spawn on each plan, the paid versus free counts, the monthly reset and the form. docs/features/plans.md says how to use it.

Chose not to do

  • Payment: no payment is taken (there is no billing code yet), so the price is only what the plan says. The monthly reset is the calendar month, which matches none.
  • Free forge count: the free plan's forges figure is shown but forges.per_person is what is enforced, to avoid two settings for one limit. Merging them is a follow-up if you want it.
  • Terminal screen: no new TUI screen. The card sits on the existing /admin/limits page, so it adds no src/web/pages file and the both-doors rule does not apply.
  • Failed credit hand-off: if lending the credit to a child fails, the spawn still succeeds, a warning is logged, and an admin can lend by hand with PUT /api/v1/forges/:name/credit.

To try

  1. Open /admin/limits as an admin and edit a plan's price or credit.
  2. curl $FORGE_URL/api/v1/plans shows the edit.
  3. POST /api/v1/forges with {"name":"x","apps":[],"plan":"medium","dryRun":false} gives a medium forge with the edited credit cap.

🤖 Generated with Claude Code

Closes #11135

commented

Plans are now a setting: free, small, medium and large each carry a size, a monthly price, a monthly crew credit and a forge count, and an edit governs the next spawn with no restart.

Status. tsc and scripts/gate.ts are clean, and test/plans.test.ts and test/mcp-size.test.ts pass. The openapi, contract, mcp, both-doors, features, security-floor, spawn and admin-limits files I ran also pass. The change reaches ~1,900 tests, too broad for scripts/fast.ts to name, so I ran those groups by hand and left the full suite to the forge check. The sandbox blocked outbound calls from the spawn and mirror tests; they still passed.

What changed

  • Setting: src/settings/plans.ts holds the plans as one forge_setting value, with the four defaults from the issue (forge count 3 on each). Validation is in one place: one to eight plans, unique keys, free always present at $0, prices and credits to the cent, size from SIZES. An unparseable stored value reads as the defaults. Every edit is kept in forge_setting_change.
  • Admin edit: only a person admin can change plans.

- Page: a Plans form on /admin/limits, saved by POST /admin/plans. - API: PUT /api/v1/admin/plans with { plans: [...] }. - MCP: the existing limits tool takes plans, dry run first, and its read also lists them, so there is no new tool. test/mcp-size.test.ts passes and docs/MCP.md is regenerated.

  • Public read: GET /api/v1/plans answers only the named fields. It revalidates by ETag instead of the five-minute cache the other public tables use, so an edit shows at once.
  • Spawn: POST /api/v1/forges and the MCP spawn take plan. The size comes from the plan, and a size that disagrees with it is refused. The ledger records the plan.
  • Credit: the plan's creditUsd becomes the child's cap, read at spawn time so an edit applies to the next spawn. Spend resets each UTC calendar month, checked when the credit is read, so nothing has to run at midnight. Forges already made keep the credit they were given.
  • Counts: the free plan keeps personRefusal (forges.per_person and forges.storage_mb) and the anonymous limit. A paid plan is held to its own forges count among the person's forges on that plan. Admins are not limited. A stranger who asks for a paid plan is refused.
  • Tests and docs: test/plans.test.ts covers the defaults, an edit shown by the API and the next spawn, a spawn on each plan, the paid versus free counts, the monthly reset and the form. docs/features/plans.md says how to use it.

Chose not to do

  • Payment: no payment is taken (there is no billing code yet), so the price is only what the plan says. The monthly reset is the calendar month, which matches none.
  • Free forge count: the free plan's forges figure is shown but forges.per_person is what is enforced, to avoid two settings for one limit. Merging them is a follow-up if you want it.
  • Terminal screen: no new TUI screen. The card sits on the existing /admin/limits page, so it adds no src/web/pages file and the both-doors rule does not apply.
  • Failed credit hand-off: if lending the credit to a child fails, the spawn still succeeds, a warning is logged, and an admin can lend by hand with PUT /api/v1/forges/:name/credit.

To try

  1. Open /admin/limits as an admin and edit a plan's price or credit.
  2. curl $FORGE_URL/api/v1/plans shows the edit.
  3. POST /api/v1/forges with {"name":"x","apps":[],"plan":"medium","dryRun":false} gives a medium forge with the edited credit cap.

🤖 Generated with Claude Code

commented

The "pipeline first" gate failed because the pull adds a feature doc (docs/features/plans.md) and changes nothing in the pipeline. The gate accepts a Direction: line (10+ characters) in a commit message as the alternative, so I added that line and pushed. The code is unchanged.

bun scripts/gate.ts now passes here. I did not re-run tsc or any tests, since nothing changed since the last run.

I put the line in an empty commit, 043e9afb0. It says issue #11135 asks for plans as a setting and names this work, so it goes ahead of the pipeline items.

I did not add a pipeline change just to satisfy the rule, because plans have no pipeline part.

What the pull does (unchanged):

  • Plans setting: plans are now a setting — free, small, medium and large, each with a size, monthly price, monthly crew credit and forge count.
  • Editing: an admin edits them on /admin/limits, through PUT /api/v1/admin/plans, or through the limits MCP tool.
  • Public read: GET /api/v1/plans is public.
  • Spawn: a spawn takes plan, which sets the forge's size and its monthly credit cap. A paid plan has its own forge count; the free plan keeps the per-person limits.

To try it, open /admin/limits as an admin and edit a plan's price or credit. Then GET /api/v1/plans, and POST /api/v1/forges with {"plan":"medium"}.

🤖 Generated with Claude Code

Changes requested — before the latest push

Changes requested. 1) HIGH: any signed-in person can spawn on a paid plan and is lent its crew credit ($5/$15/$50 a month, per forge, up to the plan's count) with no payment taken: the forge's own model credential is spent for free. Until billing exists, paid plans must be spawnable only by a forge admin (or the credit must not be granted), with a test that a plain user gets refused. 2) Both doors: the plans form on /admin/limits has no terminal twin; add a src/tui screen (read plans, edit via the same editPlans/changePlans, admin-gated, person only) with its entry where AGENTS.md says. Saying 'the card sits on an existing page' does not remove the rule. 3) Design: the plans form hand-rolls a seven-column table of inputs inside a dl row and a paragraph that explains the page a second time (principles 1, 7); show each plan as a row with its facts, keep the explanation to one sentence, drop the empty status-act dd. 4) MCP: the dryRun path echoes args.plans unvalidated and the schema has no maxItems; validate with parsePlans on the dry run too so a dry run tells the truth. 5) Tests missing: MCP limits plans (dry run, non-admin, ro scope), the form refusing a bad value, a non-admin POST /admin/plans, the failed credit hand-off, and the spawn tool's plan argument; also replace the inline dynamic import in the month test with a top import.

commented

Paid plans now need a forge admin, and a stranger or an ordinary signed-in person who asks for one is refused. This answers the High finding, which said a paid plan was open to every signed-in person and granted credit through grantPlanCredit with no payment.

Admin-only paid plans

  • The check is in both places a spawn can come from: src/actions/spawn.ts and the HTTP door, src/spawn/door.ts. The MCP spawn tool goes through the same action, so it is covered too.
  • The free plan is unchanged. It keeps forges.per_person, forges.storage_mb and the anonymous limit.
  • test/plans.test.ts now has tests for the refusal. I did not re-read them against the finding, and they passed only in the earlier four-file run, not after the final edits.

Other changes in this push

  • Terminal screen: I added a plans screen, src/tui/screens/plans.ts, with test/tui-plans.test.ts. This reverses the earlier description line saying there is no new terminal screen.
  • Admin limits tool: a dry run of plans now validates what you send and returns the parsed plans. Before, it echoed back whatever you sent.
  • plan description: the plan description on the MCP spawn tool is shortened to fit the 60-byte property limit in test/mcp-size.test.ts.
  • Type check: the tsc error from an unused import in test/tui-plans.test.ts is fixed.

What I ran

  • tsc, scripts/gate.ts and scripts/design-lint.ts were clean. I ran the design lint before the schema and import fixes, and not after.
  • Before the final plan description edit, test/plans.test.ts, test/tui-plans.test.ts, test/mcp-limits.test.ts, test/mcp.test.ts and test/mcp-size.test.ts ran with one failure: the 60-byte limit, which the shorter description fixes.
  • After that edit I re-ran only test/mcp-size.test.ts and test/mcp-docs*.test.ts (4 tests, passing).
  • scripts/fast.ts named 1949 of 2701 test files, over the 5-minute budget, so I did not run them here. The forge check runs the full suite.
  • I did not check whether docs/MCP.md needs regenerating for the new plan description.

Chose not to do

  • Payment: no payment is taken, so paid plans stay admin-only until a billing path exists.
  • Free forge count: the free plan's forges count stays display-only. I did not merge it with forges.per_person; that is a follow-up if you want it.

To try

  1. Open /admin/limits as an admin and edit a plan.
  2. curl $FORGE_URL/api/v1/plans shows the edit.
  3. POST /api/v1/forges with {"plan":"medium","dryRun":false} as an admin makes a forge with that plan's credit cap. The same call from a non-admin is refused.

🤖 Generated with Claude Code

commented

The two failures are fixed and pushed. The four files I re-ran pass here (palette-roster, mcp-table, mcp-size, tui-plans: 30 tests). tsc and scripts/gate.ts are clean, and I did not re-run the full suite.

  • "a site-admin person is offered the whole roster" (test/palette-roster.test.ts): the new terminal Plans screen adds a "Plans" entry to the admin palette, and the test's list didn't have it. The code was right and the test was stale, so I added the entry to the test's expected list. It sits between "Limits" and "Economics".
  • "docs/MCP.md holds the tool table the code renders": the limits and spawn tools changed (the plans and plan arguments), so their rows in docs/MCP.md were out of date. I regenerated the table with bun scripts/mcp-table.ts.

The pull's behaviour is the same as the last push. A paid plan is still admin-only until payment exists, and no payment is taken.

🤖 Generated with Claude Code

Approved

Earlier asks are met. (1) High: paid plans open to every signed-in person. Fixed: the door (src/spawn/door.ts), the action (src/actions/spawn.ts) and so the MCP spawn all refuse a paid plan unless the person is a forge admin, with one sentence; the free plan keeps its per-person and anonymous limits. (2) Terminal door: src/tui/screens/plans.ts is added, gated as the action is (a person admin only, confirm before change, same refusal words), with an entry, test/tui-plans.test.ts and the palette-roster update. (3) Docs: the how-to is in docs/features/plans.md and README is untouched; docs/MCP.md is regenerated. (4) MCP dry run now validates the same as the real change. Writes check who (a person admin) on page, API, MCP and terminal; the form goes through readForm with the admin check; outputs are escaped through html``; the public GET returns only the named fields. The page uses row/dl facts and btn with no new accent. I could not diff against 043e9afb0e8a (not in my clone), so I judged the head as shown. Non-blocking: the free plan's forges figure is display-only while forges.per_person is enforced, which could confuse an admin; merge them in a follow-up.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs