Skip to content
open tomato/ rafa
GitHub
rafa pages
  1. rafa
  2. Configuration

Configuration

Start from no config at all, then add one block per step, from one person's project to a team with its own release rules.

Every key is optional. A project with no config runs on rafa's defaults, and each section below adds one block for the situation that calls for it. Stop at your own step: what you have is a working config.

Where settings live

rafa reads two scopes. The project scope is the repository's root; the user scope is ~/.rafa/, read under every project on the machine. Each scope reads up to two files:

  1. A module file, rafa.config.ts or rafa.config.mjs: a JavaScript or TypeScript module that exports a list of entries. This file is yours, and rafa never writes it.
  2. A YAML file, .rafa/config.yaml in the project and ~/.rafa/config.yaml for the user. This is the file rafa init writes and rafa config set edits, keeping every comment.

The layers stack, each key decided on its own: rafa's defaults, then the user scope, then the project scope. Within a scope, a key in the YAML file wins over the same key in the module file.

To see what applies and where each value came from:

rafa config show --sources
naming.branch: "feat/{stub}"  from local <project>/.rafa/config.yaml entry 0
effort.sync: "local"  from default
…

local is the project's YAML file, project its module file, user the user scope, and default a key no file names.

Step 0: no config

Nothing to write. rafa init puts every setting in .rafa/config.yaml, commented out at its default, so you can read the defaults there and uncomment a line, with its section line, to change it.

Step 1: one person, one project

The first thing most people change is the branch a loop creates. {stub} stands for the plan's stub and must appear exactly once:

# .rafa/config.yaml
naming:
  branch: feat/{stub}

A template without {stub}, or with it twice, is refused when the config loads:

naming.branch is "feat/main", expected a branch name template holding {stub} exactly once

To keep loop worktrees outside the repository folder (see The cycle):

loop:
  worktreeDir: ../rafa-loops

Which tests a loop runs

In 0.47.0 a loop runs the tests the checks section names, at each check point, and by default it names none. With no checks set, a loop's task, stage and pre-wrap-up steps run no tests, record that no check is configured, and block nothing; lint and type checks still run.

To get back the checks rafa ran before 0.47.0, adopt the escalation template:

templates: [escalation]

It sets checks.task: [affected], checks.stage: [related, integration] and checks.loop: [full], with the test-file patterns those checks use. rafa config show --sources names each value it sets as template:escalation.

Or name the checks yourself, one list per check point:

checks:
  task: [affected]
  stage: [related]
  loop: [full]

Each list takes affected, related, unit, contract, integration or full; anything else is refused when the config loads. Four more check points, pullRequest, stretch, releasePullRequest and publish, take the same values. [TBD: what each of the seven check points covers, and what each value selects]

Step 1, in a module file instead

The same setting can live in a module file. Pick the lightest form your project allows:

  • A repository with no package.json, rafa installed globally (a Python project, say): a rafa.config.mjs holding a plain list, or a rafa.config.ts with only an import type. The type import is removed before the file runs, so nothing is needed at run time.
  • A JavaScript or TypeScript project with rafa as a dev dependency: the same file, and your editor checks the entries through satisfies Config.
// rafa.config.mjs
export default [{ naming: { branch: 'feat/{stub}' } }];
// rafa.config.ts
import type { Config } from '@open-tomato/rafa/config';

export default [{ naming: { branch: 'feat/{stub}' } }] satisfies Config;

A module that imports @open-tomato/rafa at run time needs the package installed where the file runs; a global install is not enough.

WARN

A module file is code rafa imports, so it runs with your shell, credentials and remotes. rafa pr triage --resolve refuses a pull request whose branch carries a config module the base branch does not hold byte for byte, unless you pass --allow-local-config. A YAML file is data and is not checked this way.

Step 2: two machines

One laptop needs nothing: the effort store, rafa's record of sessions and commits, stays local. Two personal devices that meet now and then exchange a file:

effort:
  sync: file

Several devices covers the second device's first day, and the heavier strategies.

Step 3: a team with its own release rules

Releases start with fragments that settle on the base branch and tags made by hand. A team that wants settle to tag the commit it pushed:

release:
  tag: settle

A team whose base branch is protected, so nothing may push to it directly, has settle open one pending release pull request instead, and refuses a merge that would collide on a version:

release:
  settle: pr
  tag: settle
pr:
  versionCollision: refuse

The release flow explains the ladder these keys climb.

Step 4: several teams, and a company

Boards that should show their blockers in GitHub's own interface use its native relationships instead of labels:

board:
  relationships: native

A company syncing every device through a hub (see rafa-hub):

effort:
  sync: service
hub:
  url: https://hub.example.org
  tokenSecret: rafa-hub-token   # the name in the secret store, never the token

service with no hub.url in any file is refused when the config loads. The block also needs the lines that load the service module, which Connect devices gives; without them rafa doctor fails, saying no module registers a sync adapter of that kind.

Changing a key from the command line

rafa config set 'naming.branch=feat/{stub}'
✅ Set naming.branch in <project>/.rafa/config.yaml.
   was: (not set)
   now: feat/{stub}

config set edits the project's .rafa/config.yaml in place and keeps its comments, also beside a module file. It never writes the module file.

NOTE

Coming, in open specs: rafa init asking the policy questions and writing a default rafa.config.ts (#894), and rafa config setting a key for one session or pushing it to a managed project (#893).

Decisions not taken

Three routes for loading a module file that imports rafa were set aside. A Bun plugin redirecting the import was measured, and does not resolve. rafa setting NODE_PATH and starting itself again would start every command twice. Requiring a local install, as ESLint and Vite do, would refuse the global Python case above.