Foreman›Docs›Running›Slots and environments

Slots and environments

Running · page 4 of 7

Worktrees per slot, the none, Docker and Supabase environments, and repairing a slot that is down.

Foreman 0.11.05 sections

A slot is one copy of your repository that one story builds in. A worker is a slot that is allowed to build right now: slots says how many copies Foreman keeps, workers how many of them build at the same time. This page is for whoever sets a project up to build more than one story at once, or runs a project whose stories need a database or a container stack of their own. It covers the folder each slot gets, what the three built-in environments give a slot, and what to do when a slot is down. With the Salesforce pack, two more environments give each slot a sandbox or a scratch org instead; see Build orgs.

Worktrees and env files

Each slot w1 to wN gets a git worktree at .foreman/worktrees/wN (under stateDir when you change it). Foreman creates it the first time the slot starts a story: it fetches <remote>/<baseBranch> from your main checkout (three tries, since several slots fetching at once can collide), forgets any worktree whose folder was deleted, and adds the new one detached at that commit. The story’s own branch is checked out inside it when the session starts. The dashboard shows “preparing the slot” while this happens, as a new slot can take a few minutes.

Your project’s env file, envFile (.env.local by default), is linked into the worktree from the main checkout, when the main checkout has one. Foreman never opens the file; it only makes the link. Foreman’s own keys file, foremanEnvFile (.env.foreman), is never linked, copied or created in a slot, so the agents building a story cannot read it. If envFile names that same file, nothing is linked.

The install block says how a fresh copy gets its dependencies:

"install": { "run": "npm ci", "when": "package-lock.json", "creates": "node_modules" }

run and when are required; creates is optional. The default is npm ci when package-lock.json changes. The command runs with sh -c in the worktree when the hash of the when file differs from the one recorded at the last install (kept in .foreman/slots/wN.install), or when the creates path is missing from the worktree. A worktree without the when file runs no install, and "install": null turns it off. A failed install puts the slot down (see below).

Three more things live in the folder. Foreman’s learned skills are copied into .claude/skills/foreman-learned-* and excluded from git, so a story commit never carries them; see How Foreman learns. When a story ends, anything left uncommitted is saved on a local leftover/ branch and the folder is reset and detached at the base again; see Leftover branches. And the dirty check that decides whether a folder is clean ignores the generated paths, so a regenerated lockfile or schema never parks a slot.

Every story session gets FOREMAN_RUN=1, FOREMAN_SLOT (w2), FOREMAN_STORY (the id), FOREMAN_SUPERVISOR_PID and FOREMAN_TEST_CORES in its environment, plus whatever the slot’s environment adds. The session itself is described on Story sessions.

How many slots and workers

slots is 1 by default and at most 8; a change takes effect when the run restarts. workers is 1 by default, never more than slots, and takes effect from the next pick; foreman run --workers N sets it for one run, and the dashboard’s Workers control saves it for this computer. When you lower the worker count, a slot above it finishes the story it has and then drains: foreman status reads “4 running, limit 3: draining”. A story waiting in a slot over the count moves to a free slot under it. A pack may cap slots: the Salesforce pack allows one slot per build org, and one slot in all when the first stage’s org is also a build org.

What each slot action does to the folder

The dashboard’s slot cards and foreman slot <wN> <action> offer these actions, by the slot’s state. Their words are the dashboard’s.

Action What it does Shown when
Skip story (skip) The session stops and the branch is kept. The story is not picked again while this run lasts, standby included, unless you ask for it with Build next; the slot moves on. foreman status lists the skipped stories. building, parked after a crash
Take over (takeover) Any work in progress is committed and pushed to the story branch. The story is claimed for you and blocked, and the slot moves on to another story. building, held, parked after a crash
Park (park) An idle or down slot parks at once and picks up nothing until you unpark it. On a building slot, the story in progress carries on to the end, then the slot parks. building, idle, down
Unpark (unpark) The slot goes back to idle and takes the next story that can be built. parked by you
Resume (resume) The slot goes back to work on the same story, keeping what was done so far. held, down, parked after a crash
Restart fresh from main (restart) The folder is reset, the local branch deleted, and the story built again from the start. parked after a crash
Resume as is (resume-as-is) With a story, the changes in the folder are that story’s work and it carries on. With none, they are put aside in a git stash and the slot takes a new story. parked on a dirty folder
Discard changes and reset (discard) It asks you to confirm first. The folder is hard reset to main and the changes are gone for good. parked on a dirty folder

A slot held by a halt offers only Take over, since nothing can start until you press “I’ve fixed it”. Skip, Take over and Restart tidy the folder the same way a finished story does: what was uncommitted goes on a leftover branch first.

Test cores and check runs

Several slots running tests at once would each take every core. So each story session gets FOREMAN_TEST_CORES: the machine’s cores split evenly between the workers, at least 2 each. Foreman’s own test config reads it, and your project’s test runner can too. foreman doctor warns when the load is over twice the cores during a run, and names the busiest processes.

Full check runs share the machine as well. checkRuns.maxParallel says how many full test runs may go at once across all slots; unset, it is one per 8 cores, at least 1. A run over the limit waits for a seat. A check runs once per tree: when a second slot reaches a check another slot is already running on the same merged tree, it waits and takes that run’s result instead of running the suite again. foreman report counts those waits.

env.adapter: none

The default. Nothing to start, nothing to repair: a slot is always up. The adapter takes one option, ports, a map of variable names to base port numbers. Each slot gets FOREMAN_SLOT=wN and every named port moved up by the slot number, so slot 1 gets base + 1, slot 2 base + 2.

"env": { "adapter": "none", "ports": { "PORT": 3000, "API_PORT": 4000 } }

Slot w2 then runs with PORT=3002 and API_PORT=4002. A base that is not a whole number, or any other option, is a config error. Use this when each slot can run its own copy of the app from the worktree alone, or when the stories need no running app at all.

env.adapter: docker

One Docker Compose project per slot. Options: composeFile (compose.yaml by default), projectPrefix (required) and ports, a map of variable names to base ports.

"env": { "adapter": "docker", "projectPrefix": "acme", "composeFile": "compose.yaml", "ports": { "DB_PORT": 5400, "APP_PORT": 3000 } }

Each slot gets FOREMAN_SLOT=wN, COMPOSE_PROJECT_NAME=<prefix>-wN (acme-w2) and every named port moved up by 100 times the slot number: slot 2 gets DB_PORT=5600 and APP_PORT=3200. Your compose file reads those variables for its published ports, so the slots never collide.

Before a story starts, Foreman runs docker compose -f <composeFile> -p <prefix>-wN up -d --wait in the worktree with those variables set. When that fails it runs down and tries up once more; a stack that comes up on the second try is reported as “environment reset”. If it still fails, Foreman runs down again, so unhealthy containers do not keep holding the slot’s ports, and the slot is down. The adapter needs the docker command; foreman doctor checks it is there.

env.adapter: supabase

One local Supabase stack per slot, for projects whose stories need a database. Options: projectPrefix (required), portStep (100), extraPorts (a map of names to { base, step }), excludeStudio (true) and colima (false).

"env": {
  "adapter": "supabase",
  "projectPrefix": "acme",
  "portStep": 100,
  "excludeStudio": true,
  // Only when Docker runs through Colima on this Mac.
  "colima": false,
  "extraPorts": { "APP_PORT": { "base": 3000, "step": 1 } }
}

The Supabase CLI reads its ports from environment variables, so supabase/config.toml is never edited. Each slot n gets, with up = portStep × n:

Variable Value
SUPABASE_PROJECT_ID <prefix>-wN
SUPABASE_API_PORT 54321 + up
SUPABASE_DB_PORT 54322 + up
SUPABASE_DB_SHADOW_PORT 54320 + up
SUPABASE_DB_POOLER_PORT 54329 + up
SUPABASE_STUDIO_PORT 54323 + up
SUPABASE_INBUCKET_PORT and SUPABASE_LOCAL_SMTP_PORT 54324 + up
SUPABASE_ANALYTICS_PORT 54327 + up
SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT 8083 + n
each extraPorts name base + step × n
FOREMAN_SLOT wN

Before a story starts, Foreman climbs a ladder, cheapest rung first: supabase status passing means the stack is up; otherwise supabase start (with -x studio while excludeStudio is on) means it was restarted; otherwise supabase stop --no-backup and start again means it was reset; and if that fails too the slot is down. A slot that needed a restart or a reset gets an event saying so. With colima: true, the adapter also has a repair: when the stack will not come up at all, Foreman runs colima status and, if Colima is not running, colima start, then tries the ladder again. The repair runs at most once per run.

The adapter needs the supabase command, and colima too when that option is on; foreman doctor checks for both. The migrate-supabase ship step uses the same block to find the hosted project; see Deploy targets.

The Salesforce pack’s environments

With "pack": "salesforce", two more adapters join these: sandbox-pool, one sandbox per slot from the salesforce.build list, and scratch, a scratch org made from a Dev Hub for each story and deleted when it closes. Both take no options of their own; the pack’s block configures them. The Salesforce pack is a preview: its release paths pass their own tests, but no story has yet shipped through them on a live org. See Build orgs.

When a slot is down

A slot is down when its environment would not start, or its worktree could not be prepared. The slot’s card on Waiting on you reads “Slot w2 is down” with the reason, foreman status shows the slot as down with the same reason, one warn event is logged (“Slot w2 is down: Its environment would not start: …”) and you get one down email. The other slots carry on. A down slot keeps its story, so Resume can queue it again.

The reasons you will see:

  • “Its environment would not start: <the adapter’s error>” when the adapter answered down, or threw. The error text is redacted and cut to 200 characters, since it goes into the email.
  • “Its environment would not start.” when the adapter answered down without saying why.
  • “Its worktree could not be prepared: Could not fetch origin/main after 3 tries.” when the fetch failed; “…Could not create the worktree for slot w2 at <path>.” when git worktree add failed; “…The install command (npm ci) failed in slot w2 (<path>).” when the install failed.

The repair ladder runs once per run. When the adapter has a repair (the Supabase adapter with colima: true) and prepare answers down, Foreman runs the repair and prepares the slot again. The outcome is one of two events: “The slot environment was down; repaired it.” and the story starts, or “The slot environment was down and could not be repaired.” A slot that is still down after a repair pauses the whole run with “The slot environment would not start, even after a repair.”, because the environment itself has probably gone (a container runtime that stopped), not just one slot. A later slot that goes down in the same run gets no second repair; it is put down with its reason.

What to do:

  1. Read the reason on the slot’s card or in foreman status. For Docker and Supabase, check the runtime is up (Docker Desktop or Colima) and try the command Foreman ran, in the worktree folder with the slot’s variables set.
  2. Fix the cause. For a failed install, run the install.run command in .foreman/worktrees/wN and read its output.
  3. Press Resume on the slot’s card, or run foreman slot wN resume. The slot prepares again and takes its story back. If the run paused after a failed repair, press Resume on the pause as well.
  4. If you want the slot kept out of the run for now, press Park instead; Unpark brings it back.

A down slot stays down across a restart of the run, as a parked one stays parked; both wait for you. See When things go wrong for the other ways a run can stall.