0032: Modules declare interactive setup steps; hz setup runs them all¶
Context and Problem Statement¶
Configuring a module today means reading CONFIGURATION.md and typing
hz system config set clock.zones '[...]' or hz system credential set ..., with the TOML syntax right the first
time. The maintainer asked for more: "modules can contribute to an interactive setup, so one can e.g. do hz clock
setup, which then interactively asks for timezone and working hours per timezone [...] wizard style in the cli. when
the user does hz setup, this triggers the setup steps of all modules. setup steps can be required or optional, which
is something the module can declare. on optional, the user can skip the setup step on hz setup". How does a module
declare such a step, how does it ask questions without knowing about the terminal
(ADR 0003), and where do the commands live, given that
ADR 0018 keeps the top level free of administration?
Decision Drivers¶
- Modules declare, the kernel collects, a head consumes: the pattern of health checks (ADR 0015) and credentials (ADR 0030).
- A step writes through the same validated paths as the commands people already use, so the wizard can't write a
value
hz system config setwould refuse. - Testable without a terminal: a step is a function, its questions can be scripted.
- Only an interactive head can run a wizard; MCP and REST clients are not people at a keyboard.
Considered Options¶
- A
SetupLetdeclared with@module.setup(required=...), asking through an injectedPrompter, run by the CLI head (chosen) - A setup operation per module (
@module.operation(heads={"cli"})namedsetup): an operation is a single call with parameters; MCP and REST would have to be excluded by hand, andhz setupwould have to guess which operations are setup steps by their name. - A generic step derived from each settings model (one question per field): asks things nobody needs to answer (directories, log levels), can't search IANA names or ask "hours per zone", and the maintainer wants modules to declare their steps. Rejected; a module without a declared step has none.
hz system setupinstead ofhz setup: consistent with ADR 0018, but the maintainer decided onhz setup; it is meant to be the first command a new user types.
Decision Outcome¶
Chosen option: a SetupLet declared with @module.setup, asking through an injected Prompter, run by the CLI
head, because modules stay head-agnostic, steps write through the existing services, and tests script the answers.
- Declaration.
@module.setup(required=False)on a function with a docstring adds aSetupLetnamed after the module (one per module, likeInfoLet). Its first docstring line is shown before the step. Its parameters are injected like an operation's: settings, services,Credentials, andPrompter. - Prompter.
heinzel.Prompteraskstext,choice,choices(several, or none),confirm,numberandsecretquestions, each with a default and, where it makes sense, avalidatecallback returning a problem or None; an invalid answer is explained and asked again. Heads implement two primitives,readandsay. The kernel's default is not interactive and raisesHeinzelError; the CLI binds a console prompter; tests useheinzel.kernel.ScriptedPrompter(fixturehz_prompter). - Writing. A step writes settings through
heinzel.ConfigEditor(provided bysystem.config, the same validation ashz system config set, user scope by default) and secrets throughCredentials.set. A step that is interrupted keeps what it already wrote. - Commands (CLI only).
hz setup [MODULE]is the command that runs setup steps (hz joinruns the missing ones too, see One journey):hz setupruns every step in order (see Order);hz setup clockruns one module's step, without the skip question. A name without a step that can run is a usage error that says why and lists the modules with one. An optional step asks "Set up clock now? [Y/n]" first; a required one doesn't. A step whose module is already configured says so and asks whether to change it. Ctrl-C stops the run and keeps finished steps; configuration files are replaced whole, never half written. A step that fails is reported and the others still run.hz setupends with a summary andhz system doctoron the configuration just written, and exits 1 when doctor reports a check down, saying so on stderr. A setup step never changes a module's command path: the module keeps exactly the commands it has without one (hz clock,hz echo hi), as in MCP and REST. - Update notice (ADR 0031).
hz setup clockis followed by the notice like every other command.hz setupleaves it to the doctor it ends with, whosereleasecheck names a newer release; when doctor doesn't run (disabled, or the run was stopped with Ctrl-C) the notice follows instead. - Already configured. A module counts as configured when one of its settings is set in a file or the environment, or one of its declared credentials is stored. A module with neither always runs its step.
- No terminal. Setup asks its questions on standard output and reads the answers from standard input, so both
must be a terminal:
hz setup > out.txtwould otherwise wait for answers to questions nobody sees. Otherwise, unless an answers file is given (Answers), it refuses and prints, on stderr, the equivalenthz system config setandhz system credential setcommands for each step's module. - Order.
@module.setup(phase=...)places a step:identity(who you are),membership(whom you work with),connect(tools, credentials, bindings; the default),work(what hz starts doing for you) andrun(whether it runs on its own). Steps run phase by phase, built-in modules first inside a phase, then the others alphabetically. Phases only order the steps: no heading, prompt or document shows them to a person. - One journey, joined at the front door.
hz join <team repository>(the perfect-onboarding ADR) is how a person starts: it says what hz keeps about them, runs theidentitysteps, joins the team, then runs the steps still missing.hz setupis what they re-run any time for whatever is still missing. Both say what hz keeps, and where, before they ask anything. - Resumable. Every finished step is recorded, with when and its keyed answers, in
<state_dir>/setup.json(mode 0600, written after each step).hz setupruns only the steps no earlier run finished (never run, failed or interrupted), says which are done, and, when nothing is missing, says so and runs doctor;--restartforgets the record and goes through every step again. - Answers. A question may carry a
key, its name within the module (roleasked byorgisorg.role).--answers <file>is TOML with a table per module ([org] role = "engineer"): keyed questions take their answer from it, an invalid one fails instead of being asked again, and with no terminal a keyed question without an answer takes its default or, without one, fails naming the key. Without a terminal, a module with answers runs its step; one without keeps its configuration, an optional step is skipped and a required one runs.--print-answerswrites the recorded answers as such a file, so a team bootstraps its answers from one finished setup;--jsonprints every step's outcome and doctor's status, questions going to stderr. Secrets are never keyed, so they never come from or go to a file (ADR 0030). - Names.
setupandjoinare reserved as module names, at every level, becausehz setupandhz joinare top-level commands (and[join]is the answers file's table forhz join's own questions). A module's operations may take any name,setupincluded: the step is never one of the module's commands. - Top level.
hz setupandhz joinare exceptions to ADR 0018, decided by the maintainer: everything else about Heinzelmännchen itself stays underhz system. - Nothing without a yes. Without a person,
hz joinchanges nothing unless the yes was given in advance:--yes(or[join] yes = true) makes a team whose roster lists you yours, and--propose([join] propose = true) separately allows the pull request that asks its owners to add you. A missing yes fails naming the flag. The read-only clone that reads the roster happens before any yes. With a person, an answers file that would change a module already set up asks first; flags the person typed don't.
Consequences¶
- Good: a new user runs
hz setuponce; a plugin author adds a wizard in a few lines and tests it with scripted answers. - Good: the wizard can't write what
hz system config setwould refuse; MCP and REST never see setup. - Good: a coding agent or a team sets someone up without a terminal from an answers file, and a run that was stopped continues where it stopped.
- Bad: the order is coarse (phase, built-ins, then alphabetical). A step that needs another step of the same phase
first has to say so in its text until an
after=declaration exists. - Bad: the record in
setup.jsoncan disagree with the configuration when someone edits the files by hand; the configured heuristic still offers a change, and--restartresets the record. - Bad:
hz setupexits 1 when any doctor check is down, also one of a module whose step was skipped: the run did what was asked, but the installation isn't healthy yet, and doctor's table says which check to fix. - Bad: "configured" is a heuristic; a module whose defaults are fine is offered its step until something is set.
- Neutral: new public API, additive (ADR 0026):
SetupLet,PrompterandConfigEditorinheinzel; for testsScriptedPrompter,ScriptExhaustedandINTERRUPTinheinzel.kerneland thehz_prompterfixture inheinzel.testing;toml_valueinheinzel.kernel. All are in the snapshot oftests/unit/public_api_test.py.
Confirmation¶
tests/unit/kernel/module_test.py: the decorator, one step per module, the reserved namesetup;tests/unit/kernel/prompter_test.py: every question, its validation and re-asking, withScriptedPrompter.tests/unit/heads/cli_setup_test.py: the order, the configured heuristic, the equivalent commands, skipping, required steps, already configured, failing steps, Ctrl-C and the console prompter.tests/integration/setup_test.py:hz setupandhz setup <module>answered on stdin, the summary and doctor on the configuration just written, the exit code when doctor reports a check down, the refusal without a terminal, that MCP and REST expose no setup, and that a collapsed module whose operation takes an argument and declares a step still runs ashz echo hi;tests/unit/heads/cli_setup_test.py: a terminal is needed on stdin and stdout.tests/integration/cli_test.py: every operation's command, and every group that runs one, accepts--json; a collapsed module with a setup step keeps its exit codes.tests/integration/update_notice_test.py: exactly one notice after its text output and none with--json, the notice afterhz setup <module>, and doctor's instead afterhz setup.plugins/heinzel-clock/tests/clock_test.py:hz setup clockand the migration of the old settings shape.tests/unit/kernel/prompter_test.pyandtests/unit/heads/cli_journey_test.py: keyed questions and preset answers, defaults without a person, answers files and the record insetup.json;tests/integration/journey_test.py: the order with who you are first, the privacy line first, resuming and--restart,--answerswithout a terminal,--print-answers,--json, andhz joinfound in the roster, proposed, and run by a coding agent with flags only.tests/unit/public_api_test.pysnapshots the new names.
More Information¶
- Decided by the maintainer: setup steps declared by modules, required or optional, one module's step on its own
and a top-level
hz setup, 2026-10-24. The technical shape (let, prompter, configured heuristic, order) was delegated. - Amended on 2026-10-24, decided by the maintainer: adding a setup step must never change a module's command path.
The first version mounted the step as
hz <module> setup, which turned a module that collapses into one command into a group; when that command took an argument it no longer collapsed (hz echo hibecame "No such command"), so the CLI path differed from the MCP and REST ones, and a plugin release adding a step broke scripts that called the module.hz setup [MODULE]is now the single entry for setup, and modules keep exactly their commands. Command paths stay stable across heads, and scripts don't break when a plugin adds a step. - Amended on 2026-10-24 for onboarding (the perfect-onboarding ADR, proposed in pull request #32): steps declare a
phase that orders them, the run is recorded and resumable, keyed questions can be answered from a file, and
hz joinis the front door withhz setupas the re-runnable "what is still missing". Decided by the maintainer: phases stay internal ordering, never shown; the person's story is the invite line. - Possible extensions:
after=to order steps by dependency.