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

The profit slider's position: one number per forge, and the pure function from position to policy #10123

Merged opened this

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

Merged

Each forge now stores one profit-slider `position` (a whole number 0–100, default 50) and a margin on crew spend, `marginPct` (0–500, default 0). The pure `policyAt` in `src/core/slider.ts` turns the position into a policy:

shipped production is serving it deploy e37ceca

opened this

Each forge now stores one profit-slider position (a whole number 0–100, default 50) and a margin on crew spend, marginPct (0–500, default 0). The pure policyAt in src/core/slider.ts turns the position into a policy:

  • sleepAfterMs.production and sleepAfterMs.preview: how long an environment sits idle before it sleeps.
  • idlePullMs: how long a quiet pull's preview stays up before it stops.
  • spillAtOwnedUse: how full owned capacity gets (as a share) before cloud takes work.

Nothing acts on the policy yet. The policy at 50 is built from the runtime's own constants (SLEEP_AFTER_MS, IDLE_PULL_DAYS in src/apps/runtime.ts), and a test checks they stay equal, so landing this changes nothing.

What to look at

  • src/core/slider.ts: the policy changes in a straight line between the anchor rows of docs/PROFIT.md at 0, 50 and 100. Above 50, a window that is "never" at 100 is treated as a 7-day cap, and only 100 itself returns null (never). parsePosition and parseMarginPct accept whole numbers in range only; fractions, signs, exponents, blanks and non-numbers are refused. policyWords writes the policy as a sentence, e.g. "production sleeps after 24 h idle · previews after 30 min · cloud takes work above 85% of owned capacity · a quiet pull's preview stops after 3 days".
  • Where the numbers are kept: src/settings/economics.ts and src/actions/economics.ts store them in a pinned migration. Every change is recorded and listed.
  • Three ways to reach it, each checking input the same way:

- Settings → Economics (src/web/pages/economics.ts): server-rendered, same-origin POST, for a forge admin who is a person. - GET/PUT /api/v1/forge/economics (src/http/api/economics.ts, in the OpenAPI contract): a field left out keeps its value; anything else wrong is a 422 (bad_position or bad_margin) and changes nothing. - Terminal → Economics (src/tui/screens/economics.ts): shows from → to and changes the value only after a confirm.

  • Agents: an agent can never move the slider, whatever scope it was granted.

Tests: test/slider.test.ts checks the three anchors against hand-computed values, that 25 and 75 fall between them, that 50 equals the runtime constants, and that bad positions are refused. test/economics.test.ts and test/tui-economics.test.ts cover the page, the API and the terminal.

What I chose not to do: atlas suggested splitting this into three runs: policyAt first, then storage and API, then the page. I didn't, because this branch already had all three with a green check on #10123. This run merged current main into it (the merge was clean) and re-verified it.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, the three economics test files and the contract/OpenAPI tests (contract, openapi-coverage, openapi-flow) all pass. scripts/fast.ts named 1,872 test files for this diff, so I didn't run the full suite here; the forge's check runs it on this push.

Try it: on the preview, sign in as an operator and open /settings/economics. Drag the slider and save; the sentence beside it updates. Or run curl -X PUT …/api/v1/forge/economics -d '{"position":70}' with an admin token, then GET it back. Sending {"position":7.5} returns a 422.

🤖 Generated with Claude Code

Closes #504

commented

Each forge now stores one profit-slider position (a whole number 0–100, default 50) and a margin on crew spend, marginPct (0–500, default 0). The pure policyAt in src/core/slider.ts turns the position into a policy:

  • sleepAfterMs.production and sleepAfterMs.preview: how long an environment sits idle before it sleeps.
  • idlePullMs: how long a quiet pull's preview stays up before it stops.
  • spillAtOwnedUse: how full owned capacity gets (as a share) before cloud takes work.

Nothing acts on the policy yet. The policy at 50 is built from the runtime's own constants (SLEEP_AFTER_MS, IDLE_PULL_DAYS in src/apps/runtime.ts), and a test checks they stay equal, so landing this changes nothing.

What to look at

  • src/core/slider.ts: the policy changes in a straight line between the anchor rows of docs/PROFIT.md at 0, 50 and 100. Above 50, a window that is "never" at 100 is treated as a 7-day cap, and only 100 itself returns null (never). parsePosition and parseMarginPct accept whole numbers in range only; fractions, signs, exponents, blanks and non-numbers are refused. policyWords writes the policy as a sentence, e.g. "production sleeps after 24 h idle · previews after 30 min · cloud takes work above 85% of owned capacity · a quiet pull's preview stops after 3 days".
  • Where the numbers are kept: src/settings/economics.ts and src/actions/economics.ts store them in a pinned migration. Every change is recorded and listed.
  • Three ways to reach it, each checking input the same way:

- Settings → Economics (src/web/pages/economics.ts): server-rendered, same-origin POST, for a forge admin who is a person. - GET/PUT /api/v1/forge/economics (src/http/api/economics.ts, in the OpenAPI contract): a field left out keeps its value; anything else wrong is a 422 (bad_position or bad_margin) and changes nothing. - Terminal → Economics (src/tui/screens/economics.ts): shows from → to and changes the value only after a confirm.

  • Agents: an agent can never move the slider, whatever scope it was granted.

Tests: test/slider.test.ts checks the three anchors against hand-computed values, that 25 and 75 fall between them, that 50 equals the runtime constants, and that bad positions are refused. test/economics.test.ts and test/tui-economics.test.ts cover the page, the API and the terminal.

What I chose not to do: atlas suggested splitting this into three runs: policyAt first, then storage and API, then the page. I didn't, because this branch already had all three with a green check on #10123. This run merged current main into it (the merge was clean) and re-verified it.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, the three economics test files and the contract/OpenAPI tests (contract, openapi-coverage, openapi-flow) all pass. scripts/fast.ts named 1,872 test files for this diff, so I didn't run the full suite here; the forge's check runs it on this push.

Try it: on the preview, sign in as an operator and open /settings/economics. Drag the slider and save; the sentence beside it updates. Or run curl -X PUT …/api/v1/forge/economics -d '{"position":70}' with an admin token, then GET it back. Sending {"position":7.5} returns a 422.

🤖 Generated with Claude Code

commented

The profit slider's position is now one number per forge. It is stored with the policy it stands for and can be read and changed in Settings → Economics, in the terminal's Economics screen, and through GET/PUT /api/v1/forge/economics. The 50 position keeps today's behaviour: environments sleep after 24 h idle in production and 30 min in preview, and a quiet pull keeps its preview for 3 days. Those values are now named exports in src/apps/runtime.ts so the slider and the runtime read the same numbers.

The last check failed on two tests, and this push fixes both by changing the tests, not the app:

  • test/palette-roster.test.ts pins the exact list of palette entries, and it didn't yet include the new economics entry ("the profit slider's position and the margin; change one"). That test was out of date, so it now lists the entry between Limits and Flows.
  • test/week-wiring.test.ts, the crew-line test in the terminal, was flaky on its own. It worked out the expected words once, with a time fixed when the file loads, but the terminal reads the clock when it draws. When a minute ticked over in between, the run-out time came out as 13:50 against 13:51. The test now works out the words just before and just after drawing and accepts either; the app code is unchanged.

To try it, open Settings → Economics on the preview, or press : in the terminal and pick Economics, then change the position and see the policy it states.

The gate script and bun x tsc --noEmit ran clean. Of the tests, only test/week-wiring.test.ts, test/palette-roster.test.ts and test/tui-economics.test.ts were run here (34 pass); the forge runs the full check on this push.

🤖 Generated with Claude Code

Approved

Read in full against the security list, both doors, the design principles and the issue. Admin-only and person-only on page, API and terminal, with the check made before the body is read. The page POST is same-origin, input is whole numbers in range at one boundary, and stored values that are out of range read as the default. Every change is kept, the how-to is in docs/features/economics.md, and README is untouched. Tests cover the anchors, 25 and 75, 50 equal to the runtime constants, the refusals, the agent and non-admin cases, and the terminal. I did not drive the live preview. Notes, none blocking: setEconomics writes two rows without a transaction; the week-wiring flake fix is unrelated but harmless; this is a feature ahead of the pipeline-first list, but it is an assigned issue and changes no behaviour at 50.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs