Repository navigation
Description
Problem Statement
A new thirdshift user has to find the User config section of the README, learn that ~/.thirdshift/config.toml exists, and write TOML by hand to change any default: making every Run a Merge run, fast-forwarding the Launch directory's Base branch, or sending Run notifications. Nothing after installing, and nothing on the first Run, tells them the file exists or what it can hold. Without a file, every setting is silently at its default, so there is nowhere to look to see what can be changed.
Solution
A new command, thirdshift setup, walks the user through Setup: a few questions about the settings that matter most, then writes a complete User config. Every setting appears in it with its real value and a comment saying what it does and what its default is, so the file itself shows everything that can be changed. Running it again changes the answers and keeps the rest of the file as it was.
The first Run on a machine with no User config, started from a terminal, offers Setup before doing anything else, like Claude Code does on its first start. The user can accept, answer the questions, and the Run carries on, or decline, and get the all-defaults file. A Run with no terminal is never interrupted by the offer. The installer (unchanged, generated by dist) ends its output with a one-line hint to run thirdshift setup.
User Stories
- As a new user, I want the install output to end by telling me about
thirdshift setup, so that I know how to choose my defaults without reading the README. - As a new user, I want my first Run from a terminal to offer Setup when I have no User config, so that I set my defaults before the factory does anything.
- As a new user, I want accepting the offer to run the questions and then carry on with the Run I asked for, so that I don't have to type the command again.
- As a new user, I want the Run I started to use the answers I just gave, so that choosing "every Run is a Merge run" applies right away.
- As a new user, I want a flag in the command I started (
--no-merge,--email,--no-email) to still win over the answers I just gave, so that the command means what it says. - As a new user, I want declining the offer to write the all-defaults User config and tell me
thirdshift setupchanges it, so that I'm not asked again and can still see every setting. - As a user who declined, I want no further offers on later Runs, so that thirdshift doesn't nag.
- As a user who runs thirdshift from cron, CI,
nohupor an agent's shell, I want a Run with no terminal never to prompt, so that it can't hang waiting for an answer. - As a user who runs thirdshift unattended, I want a Run with no terminal and no User config to write no file and run on the defaults, as today, so that an unattended Run creates nothing in my home folder.
- As a user who later runs thirdshift from a terminal, I want the offer to appear then, if there is still no User config, so that an unattended first Run doesn't use up the offer.
- As a user who mistyped a command, I want a bad command to exit
2with the argument error before any offer, so that I fix the command first. - As a user, I want
help,version,updateandemail-testnever to offer Setup, so that they stay quick and non-interactive. - As a user, I want to run
thirdshift setupat any time, so that I can change my defaults later. - As a user, I want Setup to ask whether every Run should be a Merge run, so that I set
merge.always. - As a user, I want Setup to ask whether every Run should first fast-forward my checkout of the Base branch, so that I set
launch.pull. - As a user, I want Setup to ask whether I want Run notifications, so that I set
email.always. - As a user who wants Run notifications, I want Setup to ask for the address and the sender, so that I set
email.toandemail.from. - As a user, I want the address question to suggest my GitHub email, so that I can usually just press Enter.
- As a user whose GitHub profile email is private, I want the suggestion to fall back to my git
user.email, so that I still get a sensible default. - As a user whose git email is a
@users.noreply.github.comaddress, I want it never suggested, so that notifications never go to an address that can't receive mail. - As a user with neither email available, I want the file to hold
email.toas a commented-out line, so that I can see where the address goes without an empty address being taken for a real one. - As a user who types an address without
@, I want Setup to ask again, so that a typo doesn't end up in the file. - As a user who wants Run notifications and has no
RESEND_API_KEYset, I want Setup to print theexportline to add to my shell profile, so that I know what is still missing. - As a user, I want Setup never to write the Resend API key anywhere, so that the User config holds no secret.
- As a user who set up notifications with
RESEND_API_KEYset, I want Setup to offer to send a test email (default No), so that I can check the setup there and then. - As a user, I want Setup not to ask about
logs.dir, and to write it at its default, so that the questions stay few and the setting is still visible. - As a user reading my User config, I want every key written with its real value and a trailing comment giving what it does and its default, so that I can tweak it by reading the file.
- As an agent reading a User config, I want real TOML values rather than commented-out ones wherever there is a default, so that I can parse and edit it.
- As a user who already has a User config, I want Setup to offer my current values as the default answers, so that pressing Enter keeps what I have.
- As a user who has commented my User config by hand, I want Setup to edit only the keys it asked about and keep my comments and other values, so that re-running it doesn't destroy my edits.
- As a user whose User config lacks some keys, because I wrote it by hand or a newer thirdshift added settings, I want Setup to add them at their defaults, so that afterwards the file lists every key this version knows.
- As a user whose User config doesn't parse, I want Setup to refuse with the same error a Run gives and leave the file alone, so that nothing I wrote is overwritten.
- As a user scripting a machine, I want
thirdshift setupwith no terminal to write the defaults without asking, so thatthirdshift setup </dev/nullsets up a machine unattended. - As a user with a User config, I want
thirdshift setupwith no terminal to keep my current values and only add missing keys, so that an unattendedssh host thirdshift setupcan never reset a configured machine. - As a user who presses Ctrl-C during the offer or the questions, I want no file written and the Run ended before any work, so that a half-answered Setup leaves nothing behind.
- As a user whose User config can't be written during a Run's offer, for example because of permissions, I want a
warning:line and the Run to carry on with the defaults, so that Setup can't stop my Run. - As a user, I want the offer and the questions on stderr, so that stdout still holds only the pull request URL.
- As a user reading
thirdshift help, I wantthirdshift setuplisted with a description, so that I can find it. - As a user reading the README, I want the Install and User config sections to point to
thirdshift setup, so that the docs and the install output agree.
Implementation Decisions
- Command surface.
setupjoinshelp,version,updateandemail-testas a command only when it is the first argument. It takes no arguments; anything after it is an argument error (exit2). It is listed in the help text. - Terminal test. "Has a terminal" means both stdin and stderr are terminals. It decides whether the first Run offers Setup and whether
thirdshift setupasks questions or writes the defaults. - Questions, in order:
merge.always(y/N),launch.pull(y/N), Run notifications, meaningemail.always(y/N). If yes,email.to, re-asked until it contains@, andemail.from, where an empty answer keeps Resend's shared sender. Then theRESEND_API_KEYhint if the variable is unset or empty. Then, if it is set, an offer to send a test email (default No), which reusesemail-test's path unchanged. Each question's default answer is the current value from an existing User config, or else the setting's default. - Defaults written out.
merge.always = false,launch.pull = falseandemail.always = false.email.from = "onboarding@resend.dev".logs.dir = "~/.thirdshift/logs".email.tois the suggested GitHub email (below), or a commented-out example line when there is none. It is the only key ever written commented out.
- File format. Every key is written with its value and a trailing comment giving what it does and its default, in the style of the README's User config example. Sections are in the order
merge,launch,email,logs. - The GitHub email suggestion.
- Setup-time only: a Run never looks it up, and a Run with
--emailand noemail.tostill stops for want of an address. - It is the public profile email from
gh api user. If that isnull, or the call fails, it isgit config --global user.email, unless that ends in@users.noreply.github.com. - It never asks for extra
ghscopes.
- Setup-time only: a Run never looks it up, and a Run with
- Editing an existing User config is in place, with a format-preserving TOML editor (e.g.
toml_edit, pure Rust, so it builds under musl per ADR-0003). Comments, key order and values Setup didn't ask about survive. Keys this version knows but the file lacks are added at their defaults. The file is parsed with the same rules a Run uses first; if that fails, Setup exits1with the Run's error and does not touch the file. - The first-Run offer.
- Order in a Run: parse arguments (exit
2on error), then the offer, then load the User config, then everything that follows today (email pre-checks, pre-flight, and so on). - When it's made: only when no User config exists and the terminal test passes.
- The prompt, on stderr:
No User config at <path>. Set your defaults now? [Y/n]. - Yes runs the same questions as
thirdshift setup, writes the file, and the Run continues with it. - No writes the all-defaults file, prints that
thirdshift setupchanges it, and the Run continues. - A failed write is a
warning:line on stderr; the Run continues with the defaults. - Ctrl-C during the offer or the questions writes no file and ends the command before any work. No Run notification can be owed yet, since the User config hasn't been loaded.
- Order in a Run: parse arguments (exit
- No terminal.
thirdshift setupwrites the defaults, or over an existing file keeps its values and adds missing keys, without asking. A Run offers nothing, writes nothing, and behaves exactly as today. - Install.
dist-workspace.tomlsetsinstall-success-msgtoRun 'thirdshift setup' to choose your defaults, or just start a Run and thirdshift will offer it., and the release workflow is regenerated withdist generate, not edited. The installer takes no new flags and runs nothing after installing. - Docs.
- The README's Install section mentions
thirdshift setupafter the install command. - The User config section describes Setup, the first-Run offer and the no-terminal behavior.
- The note that
update,versionandhelpnever read the User config is extended to say they never offer Setup. CONTEXT.mdalready defines Setup.
- The README's Install section mentions
Testing Decisions
- Tests exercise external behavior only, through the binary: the prompts on stderr, stdout, the exit code, and the User config file left on disk (contents and comments). No test reaches into the wizard's internals.
- Seam: the binary, driven by the existing
Scenarioharness (temporary$HOME, fakegh, fakeclaude). No new seam goes into the production code. - No-terminal paths use
Scenario's existing piped runs:- A first Run with no User config writes no file and behaves as today.
thirdshift setupwrites the full defaults file, or keeps values and adds missing keys over an existing one.- A broken User config is refused and left unchanged.
setupwith an extra argument exits2.
- Terminal paths use a new
Scenariohelper that runs the binary on a pseudo-terminal opened withlibc::openpty(libcis already a dependency), feeds scripted answers, and returns stdout, stderr, the exit code and the file. Cases:- Accepting the offer, then the Run completes using the new answers, e.g.
merge.alwaysmakes it a Merge run. - A command flag overrides a fresh answer.
- Declining writes the defaults file and a second Run doesn't offer again.
- A bad command exits
2with no offer. - An address without
@is asked again. - The
RESEND_API_KEYhint appears only when the key is unset. - Re-running Setup over a hand-commented file keeps the comments.
- Ctrl-C mid-questions leaves no file.
help,version,updateandemail-testnever offer Setup.
- Accepting the offer, then the Run completes using the new answers, e.g.
- The GitHub email suggestion: the fake
ghis extended to answerapi userwith an email,null, or a failure, and the scenario's gitconfig setsuser.email, including a@users.noreply.github.comcase, which must never be suggested. - Prior art:
tests/user_config.rsfor User config behavior through Runs,tests/command_surface.rsfor help text and argument errors, andtests/email_test.rswithtests/support/resend.rsfor the test-email offer.
Out of Scope
- Any install script of thirdshift's own, post-install hook, or new installer flags (
--no-setup,THIRDSHIFT_NO_SETUP). Thedistinstaller can't run custom logic or accept unknown flags, and the success message is enough. There is no change to thirdshift.app's/install.shredirect. - A
--defaultsflag forsetup; without a terminal,thirdshift setupalready writes the defaults. - Asking about
logs.dirin the wizard (it is written at its default and edited by hand). - Reading the primary GitHub email via the
userscope. - Using the GitHub email as a run-time fallback for
email.to. - Storing
RESEND_API_KEYanywhere, or editing the user's shell profile. - A "don't ask again" marker other than the User config itself.
Further Notes
- Declining the offer still writes the file on purpose: the file's presence is what stops the offer, and a complete file is what shows the user every setting.
- The GitHub email suggestion comes from the public profile because a default
gh auth logintoken lacks theuserscope that reading private addresses needs.
Activity
thirdshift setupwrites the defaults User config with no terminal #127thirdshift setup#132JacobStephens2 commentedon Sep 30, 2026
works