Foreman›Docs›Reference›foreman.config.json

foreman.config.json

Reference · page 2 of 6

Every key with its default, and how packs and your file layer together.

Foreman 0.11.012 sections

foreman.config.json sits at the root of your project and is committed, so the whole team runs the same factory. This page lists every key it may hold, what each one does, its default and the rule foreman config check applies to it. Most people set a dozen keys and leave the rest. You need this page when foreman config check complains, when a pack’s defaults surprise you, or when you want to know exactly what a step will do before the first release.

foreman config show prints your file with every default filled in, which is the quickest way to see the result of the layering below. foreman doctor runs the same check first. While a run is going and you break the file, the run keeps the last config that passed and foreman status says so; every other command refuses the invalid file until you fix it. See Every command for both commands.

How the layers combine

Foreman resolves the file in three layers, each merged over the one before:

  1. Foreman’s defaults, listed on this page.
  2. The defaults of the pack your file names with pack, if any. Today the only pack is salesforce, and its defaults are on Salesforce config. A file without pack gets layers 1 and 3 only.
  3. Your file.

Plain objects merge key by key and the later layer wins. Arrays replace rather than join: your checks list is the whole list, and so is your fileLocks.ignore.

Seven blocks replace as a whole at every layer, because half a default would be wrong: roles.builders, resources, stories, env, ship, notify and console. A project that names its own builders does not also get developer; an adapter’s options belong to that adapter alone; a project’s ship replaces the pack’s as the pack’s replaces Foreman’s. So a ship block of your own must carry its own steps, and a stories block of your own must carry its adapter. The team layer of a setting knows this: foreman settings set ship.ci.timeoutMinutes 45 --team starts the ship block from its defaults when your file has none, so the default steps are not lost.

The pack’s own block (salesforce) is not one of the seven. Your values win key by key over the pack’s defaults for it.

On top of the resolved file sits the local settings layer, .foreman/settings.json, which holds the settings you change on one machine. It is not part of the file and never committed. Settings and switches says which keys are also settings.

A key the validator does not know is an error, so a typo such as slot shows up instead of being ignored. Every error names its JSON path, such as checks[1].when, and foreman config check prints them all at once.

Top level

Key What it does Default Rule
version The config format version, so a later Foreman can migrate old files. none Required; must be 1.
project.name The project’s name, used in notification subjects and the dashboard. none Required; a non-empty string. project holds only name.
pack Turns a pack on and adds its defaults as layer 2. The pack’s own block, the top-level key of the same name, is then allowed and checked by the pack. none Must name an installed pack; today salesforce.
baseBranch The branch stories are cut from and, unless ship.pr.into says otherwise, merged into. main Non-empty string.
remote The git remote slots are cut from and story branches are pushed to. origin Non-empty string.
branchPrefix The start of every story branch name, followed by the story id in lower case, such as story/acme-12. story/ Non-empty string.
stateDir The state folder, where Foreman keeps everything it writes for a run. FOREMAN_STATE_DIR in the environment overrides it. See Files Foreman writes. .foreman Non-empty string.
envFile The project’s env file, holding the project’s own keys. It is linked into every slot worktree so the code under test finds its variables. .env.local Non-empty string.
foremanEnvFile Foreman’s own keys (the Jira sign-in, the Resend key, the dashboard token). Never linked or copied into a slot, so no agent can read it. .env.foreman Non-empty string; must not name the same file as envFile.
slots How many repo copies (git worktrees) Foreman keeps, one per story building. Also a setting; a change needs a restart. 1 A whole number from 1 to 8. The Salesforce pack lowers the top to one slot per build org.
workers How many of those slots build at the same time. Also a setting. 1 A whole number from 1 to 8 and not more than slots.
rounds How many times the builder may fix what the tester or reviewers found before the story fails. Also a setting. 3 A whole number from 1 to 10.
features The feature switches, each on or off. They are settings too, so most people change them with foreman features rather than in the file. Every switch and its default is on Settings and switches. all as listed there Only the known switches, each true or false; jiraCommentDetail is summary or full.
keepAwake The old place of features.keepAwake. Still read, as the team value of that switch unless the file sets the switch too. none Deprecated. foreman config check prints a warning telling you how to move it, and the next team write of any setting moves it for you.

install

install is what a slot runs to make a fresh worktree ready, such as installing dependencies. It runs when the hash of the when file changes, or when the creates path is missing from the worktree. Set install to null for no install step. The merge-main ship step runs it too, before the checks, when a file matching when changed in the merge.

Key What it does Default Rule
install.run The shell command. npm ci Required when install is an object; non-empty string.
install.when The file whose change means the install must run again, such as a lock file. package-lock.json Required; non-empty string.
install.creates A path the install produces, such as node_modules. When it is missing the install runs whatever the lock file says. foreman init sets it. none Non-empty string.

checks

checks is the list of commands that prove the project works. Each entry is { run, when, quick? }. The tester runs every check whose when matches a file the story changed; the merge-main ship step does the same on the merged tree when no later step tests merged code. foreman checks --changed runs them in a session, and a check that already ran on an unchanged tree is reused rather than run again. See Story sessions.

Key What it does Default Rule
checks[].run The shell command. none Required; non-empty string.
checks[].when always, or a glob a changed file must match for the check to run, such as src/**. none Required; always or a glob string.
checks[].quick A faster command for a change in progress: what the builder runs while building (foreman checks --quick). The full run still decides at test time. A quick run is never reused. none Non-empty string.

Your list replaces the default (empty) list and any pack list as a whole.

generated

generated names files a command produces, such as a lockfile or a generated client. A conflict in one of them during merge-main is settled by running its regenerate command and committing, instead of stopping the release. These paths are also never claimed by file locks, so two stories may both change them.

Key What it does Default Rule
generated[].path The file, as a path or glob. none Required; non-empty string.
generated[].regenerate The command that rebuilds it. none Required; non-empty string.

fileLocks

Two stories building at once never change the same file: after the architect’s design, the slot claims every file the design names, and a story whose design names a held file waits. fileLocks.ignore lists the files every story may touch without a claim. See Slots.

Key What it does Default Rule
fileLocks.ignore Globs for files no story claims. Every generated path is left out of claims too. ["CHANGELOG.md"] A list of non-empty strings; fileLocks holds only ignore. Your list replaces the default.

checkRuns

Key What it does Default Rule
checkRuns.maxParallel How many full test runs may go at once across all slots. Unset, Foreman allows one per 8 cores, and at least one. Also a setting. unset A whole number from 1 to 64; checkRuns holds only maxParallel.

resources

An exclusive resource is something only one story at a time may change, such as the database schema. A slot takes the resource when it picks a story whose design touches one of its paths, and a story that needs a held resource waits. The resources ship step stops a release (exit code 7) when a branch grew changes to a resource another live slot holds. See How Foreman picks and How a story ships.

Key What it does Default Rule
resources.<name> One resource. The name is how the architect and the lock files refer to it. {} The name must be a slug: lower-case letters, digits and hyphens, starting with a letter, and not a reserved word.
resources.<name>.paths The files or globs that belong to it. none Required; a non-empty list of non-empty strings.
resources.<name>.describe What it is, in words the architect and the release messages use. none Required; non-empty string.

Your resources block replaces the default as a whole.

roles

roles names the agent that plays each part of the pipeline and lets you add traits and pick a model per role. Replacing an agent name swaps in your own agent; everything else about the role stays. The builders object replaces as a whole, so a project that names one builder has only that builder. Agents and traits explains the roles; this table is the rule book.

Key What it does Default Rule
roles.architect.agent The agent that designs the story and writes the test plan. foreman:architect Required when the block is given; non-empty string. The block holds only agent, traits, model.
roles.architect.traits Extra rules for this role, as inline text or paths to .md or .txt files. none A list of non-empty strings. A listed .md or .txt file that does not exist is an error.
roles.architect.model The Claude model this role runs on. Unset, it runs on the story session’s model. Also a setting. unset One of haiku, sonnet, opus, fable.
roles.builders.<name> One builder. The architect picks a builder by its use. developer The name must be a slug and not a reserved word. The block holds only agent, use, traits, model, fullChecks.
roles.builders.<name>.agent The agent that builds. foreman:builder Required; non-empty string.
roles.builders.<name>.use When the architect should pick this builder, in words. everything Required; non-empty string.
roles.builders.<name>.traits As for the architect. none A list of non-empty strings.
roles.builders.<name>.model As for the architect. Also a setting. unset One of haiku, sonnet, opus, fable.
roles.builders.<name>.fullChecks true makes this builder run the full checks while building instead of each check’s quick command. none true or false.
roles.tester.agent The agent that re-runs the checks and verifies “Done when”. foreman:tester As for the architect; the block holds only agent, traits, model.
roles.tester.traits, roles.tester.model As for the architect. none, unset As for the architect.
roles.reviewers[] The reviewers that run once the tester approves. Your list replaces the default list. compliance (foreman:compliance-reviewer) and security (foreman:security-reviewer) Each entry holds only name, agent, prompt, traits, model, engine.
roles.reviewers[].name The reviewer’s name, used in findings, settings and the dashboard. none Required; a slug, not a reserved word, and not used twice.
roles.reviewers[].agent The agent that reviews. none Required; non-empty string.
roles.reviewers[].prompt Extra instructions for this reviewer. none A string.
roles.reviewers[].traits, roles.reviewers[].model As for the architect. The model setting is named roles.reviewers.<name>.model. none, unset As for the architect.
roles.reviewers[].engine Runs this review through another agent CLI instead of Claude, with the Claude reviewer as the fallback. See Agents and traits. none An object holding only name, model, minutes.
roles.reviewers[].engine.name Which CLI. none Required; today only antigravity.
roles.reviewers[].engine.model That CLI’s model name. none Required; non-empty string.
roles.reviewers[].engine.minutes How long the outside review may take before Foreman falls back. none A whole number from 1 to 180.

Trait files for every role, without touching the config, go under .claude/foreman/traits/ as all.md or <role>.md. Order: the pack’s traits, then those files, then the traits list, then what Foreman learned, so your own text wins.

stories

stories says where stories come from. The block replaces as a whole, so give the adapter and all its options together. The validator checks only that adapter is a non-empty string; each adapter checks its own options when it opens, and the options that are also settings (stories.jql, stories.pollSeconds, stories.ownerLabel, stories.transitions) are type-checked by the settings registry on both layers.

markdown

One markdown file per story in a folder. The default, and the shape Story files describes.

Key What it does Default Rule
stories.adapter Selects the source. markdown Required; markdown.
stories.dir The folder, relative to the project or absolute. stories Required.
stories.dirEnv An environment variable that, when set and not empty, replaces dir, for a notes vault that lives elsewhere on another machine. none A variable name.
stories.dirEnvSuffix A path joined to that variable’s value, such as Stories when the variable names the vault root. none A string.
stories.idPattern What a story id looks like, as a regular expression source. the markdown source’s own pattern A regular expression source, such as [A-Z]+-\\d+.
stories.fields Which frontmatter key holds each of group, groupName, order, dependsOn, blockedBy. group, group_name, order, depends_on, blocked_by An object of those keys.
stories.ownerTask { id?, title? }, regular expression sources; a story matching one is yours to do, never picked. {} turns owner-task detection off. the source’s own patterns An object.
stories.resourcesByGroup { resourceName: [groups] }: every story in those groups needs that exclusive resource. none An object of lists.
stories.groupLabel What a group is called in the story prompts. Group A string.
stories.groupSummary Where a finished group’s summary is written: a path relative to dir with {group}, {groupName} and {groupLabel} filled in. Unset, no summaries. none A string.
stories.progressHeading The heading the dated progress lines go under. ## Progress notes A string.
stories.track Where status changes go: file edits the notes in place; state keeps them in an overlay under the state folder and touches a note only in the story’s own commit that marks it done. state when dir is inside the project, else file file or state.
stories.newId The id template for follow-up stories Foreman creates: {n} is one more than the highest number of that shape, {group} the origin’s group. F-{n} Must match idPattern.

jira

The issues a Jira Cloud JQL filter finds. Stories from Jira covers sign-in, mapping and write-back.

Key What it does Default Rule
stories.adapter Selects the source. none jira.
stories.site The site host, such as acme.atlassian.net. none Required.
stories.jql The filter; its ORDER BY is the pick order. Also a setting. none Required; non-empty text.
stories.pollSeconds How long Foreman reuses the story list before asking Jira again; 0 asks every time. Also a setting. 60 A whole number of at least 0.
stories.doneWhenField A custom field id holding “Done when”, such as customfield_10050. none A string.
stories.ownerLabel Issues with this label are your tasks; Foreman never picks them. Also a setting. foreman-owner Non-empty text.
stories.newIssueType The issue type of the follow-ups Foreman creates. Task A string.
stories.transitions { event: transition name } for started, blocked, review, failed, shipped. An event left out moves nothing. Also a setting. {} Only those five keys, each non-empty text.
stories.briefsDir Where briefs are written, relative to the project. <stateDir>/stories A string.

salesforce

Cases in one sandbox become stories. It needs pack: "salesforce" and stories.org must be an org in salesforce.orgs that is a sandbox; the validator refuses stories.adapter: "salesforce" without the pack. The block’s keys (org, where or listView, orderBy, pollSeconds, idPattern, doneWhenField, dependsOnField, ownerField, ownerValue, statuses, briefsDir) are on Salesforce config, with the salesforce block itself. 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, and the Gearset path has not run against a live Gearset pipeline.

env

env is the slot environment: what each slot starts so its copy of the project can run alone. The block replaces as a whole. Every adapter gives each slot FOREMAN_SLOT=wN. Slots explains each adapter in use.

Key What it does Default Rule
env.adapter none, docker or supabase; the Salesforce pack adds sandbox-pool and scratch and sets one of them itself. none Required; non-empty string.
env.ports (none, docker) { NAME: base }: each variable is set to base + n for slot wN with none, and base + 100·n with docker. none An object of numbers.
env.composeFile (docker) The compose file. compose.yaml A string.
env.projectPrefix (docker, supabase) The compose or Supabase project name prefix; each slot is <prefix>-wN. none Required for both adapters.
env.portStep (supabase) How far every port moves per slot. 100 A number.
env.extraPorts (supabase) { NAME: { base, step } }: more variables to move per slot. none An object.
env.excludeStudio (supabase) Start without Supabase Studio. true true or false.
env.colima (supabase) Use Colima’s Docker socket. false true or false.

The Salesforce pack’s sandbox-pool and scratch adapters take no options; what they do is on Build orgs.

ship

ship is the release: the ordered steps a story branch goes through after review, and the options of each step. The block replaces as a whole, so a ship of your own must list steps. How a story ships walks through the steps and their exit codes; Deploy targets covers the deploy and Supabase steps. Two feature switches drop steps from the list at run time: mergeableRecheck off leaves out mergeable-recheck, and watchRollback off leaves out watch and runs no rollback.

Key What it does Default Rule
ship.steps The steps, in order. Core steps: merge-main, resources, prepare-migrations, supabase-credentials, push, pr, ci, migrate-supabase, mergeable-recheck, merge, deploy-github, smoke, watch. A pack adds its own. ["merge-main", "resources", "push", "pr", "ci", "mergeable-recheck", "merge"] Required; a non-empty list of non-empty strings. rollback may not appear: it runs only on failure. watch in the list needs ship.watch.command.
ship.productionHosts The hosts the tester and reviewers must never reach while they check a story (the production guard). Unset, Foreman uses the smoke URLs’ hosts and the Supabase project’s API and database hosts. none A non-empty list of host names with no scheme or path.

ci

Key What it does Default Rule
ship.ci.required Only these check names count; each must appear, finish and pass. Empty, every check counts and CI is detected by settling. [] A list of non-empty strings.
ship.ci.timeoutMinutes How long to wait in all for the checks. Also a setting. 30 A positive number.
ship.ci.settleSeconds How long the checks must stay finished with no new one appearing, so a fast check cannot pass before the real CI registers. Also a setting. 90 A positive number.

pr and merge

Key What it does Default Rule
ship.pr.into The branch pull requests merge into. Stories are still cut from baseBranch. Also a setting. unset (baseBranch) A plain branch name; with the Salesforce pack, never its productionBranch.
ship.pr.mergeMethod How the merge step merges. merge One of merge, squash, rebase.
ship.merge.keepBranch Keep the story branch after its pull request merges, remote and local. Also a setting. false true or false; merge holds only keepBranch.

deploy-github

Waits for the host’s deployment of the merge commit, as reported to GitHub. Anything but success exits 5 and writes the HALT file.

Key What it does Default Rule
ship.deploy-github.environment The GitHub deployment environment to watch. Production A string.
ship.deploy-github.timeoutMinutes How long to wait for the deployment. Also a setting. 15 A number.
ship.deploy-github.statusContext The commit status the host posts, such as Vercel. With it set, a host that refuses the deployment outright stops the release at once with the host’s own words instead of waiting. none A string.
ship.deploy-github.rollbackHint How to roll back by hand, added to the halt email of a deploy or smoke failure. none A string.

smoke

Key What it does Default Rule
ship.smoke.urls Live pages to fetch after the deploy, without following redirects. No answer, or a status of 500 or more, exits 5 and halts. Also a setting. [] A list of non-empty strings.
ship.smoke.expect { url: status }: a URL named here must answer exactly that status. none Each status a whole number from 100 to 599.

watch and rollback

Key What it does Default Rule
ship.watch.command A shell command that checks production, run with sh -c in the project root with the env file loaded. Any non-zero exit stops the release with code 5, halts and, with ship.rollback set, rolls back. none Required when watch is in steps; non-empty string.
ship.watch.minutes How long to watch, running the command once at the start and once at the end. Also a setting. 10 A positive number.
ship.watch.everySeconds How often to run it. Also a setting. 60 A positive number.
ship.watch.timeoutSeconds The longest one run may take. 120 A positive number.
ship.rollback.command The command that puts production back, such as vercel rollback --yes, run once when a step fails after a successful deploy and watchRollback is on. FOREMAN_STORY is set to the story. none Required when rollback is given; non-empty string. Never listed in steps.
ship.rollback.timeoutSeconds The longest the rollback may take; longer is a failed rollback. 300 A positive number.

prepare-migrations, supabase-credentials and migrate-supabase

Key What it does Default Rule
ship.prepare-migrations.dir Where the branch’s migrations live. Migrations that now sort before the base’s newest are renamed to follow it. supabase/migrations A string.
ship.migrate-supabase.projectRef The hosted Supabase project the migrations are pushed to, after CI and before the merge. none Required for the step.
ship.migrate-supabase.dir The migrations folder, read by supabase-credentials too. supabase/migrations A string.

supabase-credentials has no options: it stops the release with code 3 when the branch has migrations and the hosted credentials are missing from the env file.

notify and console

notify is how Foreman reaches you when the run stops, a story is blocked or a release parks; console is where the run’s state is published and your answers are read from. Both blocks replace as a whole. Notifications lists the events; Dashboard covers the default console.

Key What it does Default Rule
notify.adapter none writes each notification to <stateDir>/outbox/ and sends nothing; email-resend emails you through Resend with RESEND_API_KEY from the env file. none Required; non-empty string.
notify.to Where emails go. Unset, FOREMAN_NOTIFY_EMAIL, then your git email. Also a setting. unset Non-empty text.
notify.from The sender. Unset, RESEND_FROM_EMAIL. unset A string.
notify.subjectPrefix The start of every subject line. [<project name> factory] A string.
console.adapter dashboard serves the local dashboard; files writes status.json, reads control.json and keeps decisions under decisions/ in the state folder, for foreman status and foreman decide; artifacts publishes to two claude.ai artifact pages. dashboard Required; non-empty string.
console.host (dashboard) The address the dashboard listens on. The default is the loopback address, which keeps it on this computer only; anything else needs the dashboard token. Also a setting; a change needs a restart. the loopback address Non-empty text.
console.port (dashboard) The dashboard’s port. Also a setting; a change needs a restart. 4317 A whole number from 1 to 65535.
console.commandCenterUrl, console.decisionsUrl (artifacts) The two artifact pages. none Required for the adapter.
console.model (artifacts) The Claude model of the small headless call that pushes each update. haiku A model alias.
console.everySeconds (artifacts) How often to push. 60 A number.

watchdog

The watchdogs stop or report a story that has stopped moving. All eight keys are minutes or hours, as a positive number; fractions are allowed. Only the first two are settings. When things go wrong says what each one does to the run.

Key What it does Default Rule
watchdog.quietMinutes Stops an agent session that has printed nothing for this long. Also a setting. 30 A positive number.
watchdog.buildMinutes Stops a story with no stage change or agent finishing for this long, and any story at four times it. Also a setting. 120 A positive number.
watchdog.idleMinutes Reports the run stuck when a story can be built and a slot is free, yet nothing has started, for this long. 10 A positive number.
watchdog.errorMinutes Reports the run stuck when the supervisor’s loop has failed with the same error for this long. 5 A positive number.
watchdog.activityMinutes Reports a story stuck when its session has shown no activity (no stage change, log output or CPU) for this long. 20 A positive number.
watchdog.progressMinutes Lists a story as slow in foreman status when its session is still active but has had no stage change for this long. 45 A positive number.
watchdog.outliveMinutes Ends a session that printed its final result, or recorded its ending, and is still running this long after. 1 A positive number.
watchdog.ownerTaskHours How long an owner task may hold a story before you are told once more, by email. 24 A positive number.

state and cloud

state says where the run state lives; cloud tunes an unattended run. Run state on a branch and Run in the cloud explain both.

Key What it does Default Rule
state.store files keeps the state folder as plain files; branch makes it a git worktree of an orphan branch, so a laptop and a cloud runner share one copy through the remote. files files or branch; state holds only store and branch.
state.branch That branch. foreman-state A plain branch name, and not baseBranch.
cloud.envVars The project’s key variables the runner writes into envFile at start, besides those the adapters already need. [] A list of environment variable names.
cloud.idleMinutes In a container, how long to wait with the dashboard up when the backlog is empty before picking again. 10 A positive number.
cloud.actions.cron The schedule foreman cloud actions init writes into the GitHub Actions workflow. */30 * * * * Five cron fields of digits, *, /, , and -; actions holds only cron.

session and claude

session shapes what a story session may load; claude says what happens when the Console API key runs out of credit. session has no Foreman default, so a missing mcpServers means none and a missing userSettings means false; a pack may set extraTools or mcpServers, and your list replaces the pack’s. Story sessions explains the lean session.

Key What it does Default Rule
session.extraTools Tool names added to every story session’s allowed tools, such as mcp__acme__query. none A list of non-empty strings, each with no spaces and not starting with -; session holds only extraTools, mcpServers, userSettings and model.
session.mcpServers The servers from the project’s .mcp.json that story sessions load. none A list of server names: a letter or digit first, then letters, digits, _, . and -.
session.userSettings Whether sessions also load your user-level Claude Code settings, CLAUDE.md and memory. false true or false.
session.model The model of the story session itself, which only moves the story between stages and ships it. It does not set the agents’ models; those are roles.<role>.model. Unset, the Claude CLI’s own default applies. Also a setting. unset One of haiku, sonnet, opus, fable.
claude.fallback subscription switches sessions to your subscription when the key runs out; none pauses the run. Also a setting. subscription One of subscription, none; claude holds only fallback.

learning

learning controls what Foreman learns from repeated findings and how much it may spend doing so. Every key here is also a setting. How Foreman learns explains lessons, trials and undo.

Key What it does Default Rule
learning.auto Foreman learns traits and skills from repeated mistakes by itself and says so in Waiting on you. true true or false; learning holds only these eleven keys.
learning.apply The agents get learned traits and skills from the next story. Off still learns but uses none of it. true true or false.
learning.model The Claude model that proposes learned traits. haiku One of haiku, sonnet, opus.
learning.skillModel The Claude model that writes learned skills. sonnet One of haiku, sonnet, opus.
learning.window How many stories using a learned item Foreman watches before keeping or rolling it back. 5 A whole number of at least 3.
learning.maxPerRole The most learned traits one role may hold. 8 A whole number of at least 0; 0 adds none.
learning.maxSkills The most learned skills Foreman keeps. 10 A whole number of at least 0.
learning.maxEvaluating How many learned items may be on trial at once. 2 A whole number of at least 0.
learning.maxPerDay The most learned changes Foreman makes in one day. 3 A whole number of at least 0.
learning.maxUsdPerDay A daily spend cap for the learner sessions, in US dollars. set by Foreman A number of at least 0.
learning.redact Words or patterns removed from findings before the learner sees them. An entry written /pattern/flags is a regular expression; anything else is literal text. [] A list of non-empty strings.

A full example for Acme

Acme is a Node project deployed on Vercel, with stories in Jira and email through Resend. Everything not written here keeps its default.

{
  "version": 1,
  "project": { "name": "acme" },
  "baseBranch": "main",
  "slots": 2,
  "workers": 2,
  "rounds": 3,
  "install": { "run": "npm ci", "when": "package-lock.json", "creates": "node_modules" },
  "checks": [
    { "run": "npm test", "when": "always", "quick": "npm run test:unit" },
    { "run": "npm run lint", "when": "src/**" }
  ],
  // The lockfile settles its own merge conflicts.
  "generated": [{ "path": "package-lock.json", "regenerate": "npm install --package-lock-only" }],
  "resources": {
    "schema": { "paths": ["supabase/migrations/**"], "describe": "the database schema" }
  },
  "roles": {
    "reviewers": [
      { "name": "compliance", "agent": "foreman:compliance-reviewer" },
      { "name": "security", "agent": "foreman:security-reviewer", "model": "opus" }
    ]
  },
  // The whole block: adapter and options together.
  "stories": {
    "adapter": "jira",
    "site": "acme.atlassian.net",
    "jql": "project = ACME AND status = \"To Do\" ORDER BY Rank",
    "doneWhenField": "customfield_10050",
    "transitions": { "started": "In Progress", "review": "In Review", "shipped": "Done" }
  },
  // Each slot gets PORT = 3000 + n.
  "env": { "adapter": "none", "ports": { "PORT": 3000 } },
  // A ship block of its own must list every step.
  "ship": {
    "steps": ["merge-main", "resources", "push", "pr", "ci", "mergeable-recheck", "merge", "deploy-github", "smoke", "watch"],
    "ci": { "required": ["test"], "timeoutMinutes": 30 },
    "pr": { "mergeMethod": "squash" },
    "deploy-github": { "environment": "Production", "statusContext": "Vercel", "rollbackHint": "vercel rollback --yes" },
    "smoke": { "urls": ["https://acme.example/health"], "expect": { "https://acme.example/health": 200 } },
    "watch": { "command": "node scripts/check-errors.mjs", "minutes": 10 },
    "rollback": { "command": "vercel rollback --yes" }
  },
  // RESEND_API_KEY lives in .env.foreman, never here.
  "notify": { "adapter": "email-resend", "subjectPrefix": "[acme factory]" },
  "watchdog": { "quietMinutes": 30, "buildMinutes": 120 },
  "features": { "followUpsOfFollowUps": false },
  "learning": { "window": 5 }
}