Maison
Maison is a reusable macOS and Linux configuration framework:
- Nix/Lix owns operating-system state. nix-darwin configures Apple Silicon macOS; system-manager configures supported non-NixOS Linux.
- mise owns user state. The consumer repository supplies tools, packages, applications, preferences, and dotfiles.
Home Manager is intentionally absent. nh provides the local nix-darwin workflow, while deploy-rs handles remote
system-manager profiles and connection-aware system rollback.
Maison is provider-neutral. It contains framework code, neutral examples, tests, and bootstrap tooling—not a maintainer’s personal application list, deployment target, or dotfiles.
Public flake
Consumers pin Maison as a flake input and keep their own flake, lock file, inventory, host topology, configuration, and deployment state:
nix run github:RobertDeRose/maison#maison -- --help
nix flake check --no-update-lock-file
The public flake exports the CLI package and app, reusable nix-darwin and system-manager modules, the orchestration library, the inventory schema, and neutral validation fixtures. See the public flake reference and consumer reference.
Quickstart
For an existing consumer repository, clone it first. For example, Terroir can own the personal inventory and configuration while Maison remains the reusable framework:
git clone git@github.com:RobertDeRose/terroir.git "$HOME/src/terroir"
MAISON_BOOTSTRAP_VERSION="v0.1.1"
bash bootstrap.sh --consumer "$HOME/src/terroir" --ref "$MAISON_BOOTSTRAP_VERSION"
For a fresh consumer, Maison retains its Copier starter. Bootstrap renders the starter, registers the current host, pins the consumer lock, and stops for review before activation:
bash bootstrap.sh --setup "$HOME/src/terroir"
Review the generated flake.nix, inventory.toml, and user policy before creating the consumer’s first commit. The
starter is a setup-time scaffold only; runtime configuration and Git history remain consumer-owned.
The bootstrap script installs Maison under ~/.maison when needed, links the maison command, verifies pinned
platform artifacts, and hands control to the consumer repository. It never uses the Maison checkout as the personal
execution or deployment root.
For a local Maison checkout:
MAISON_HOME="$PWD" MAISON_CONSUMER_ROOT="$HOME/src/terroir" ./bin/maison plan
Consumer repository
A consumer repository must contain regular, committed files named flake.nix, flake.lock, and inventory.toml.
It normally also owns hosts/, config/mise/, dotfiles/, and any host-specific Nix modules. Maison commands resolve
the consumer in this order:
MAISON_CONSUMER_ROOT;MAISON_REPOSITORYorMAISON_REPOas compatibility aliases;- the Git checkout containing the current working directory, when it is not the Maison installation.
Maison’s own checkout is rejected as a consumer for packaged or explicit CLI invocations. Set the canonical environment variable in scripts and CI:
export MAISON_HOME="$HOME/.maison"
export MAISON_CONSUMER_ROOT="$HOME/src/terroir"
maison plan
The consumer’s flake should import Maison’s public modules and compose its own host outputs. Maison’s flake.lock is
only for Maison development, validation, and release; consumer commands never update it.
Declare logical confidential values in the consumer’s fnox.toml and let the consumer choose the fnox provider.
Passwords, tokens, SSH private keys, signing private keys, and other secret material resolve at runtime; they never
belong in Maison, the consumer Git history, Nix expressions, process arguments, or the Nix store. A private Git
repository is not a substitute for secret storage. See the fnox reference.
Bootstrap behavior
bootstrap.sh accepts --consumer PATH for an existing consumer or --setup PATH for a fresh Copier-generated
consumer. MAISON_CONSUMER_ROOT selects an existing repository. A fresh setup destination must be separate from Maison;
bootstrap renders overlay_template/, registers the current host through maison host add, creates the consumer
flake.lock, validates the required consumer files, and stops for review. Create the consumer’s first commit, then rerun
with --consumer to activate it.
Interactive bootstrap without either path offers the same fresh setup and otherwise installs only the CLI. Non-interactive
runs require --consumer or --setup and fail clearly instead of activating neutral or Maison-owned state. The
Copier starter does not create saved alternate roots or runtime overlay state.
Bootstrap first installs verified pinned mise, trusts only the Maison project configuration, installs verified Nix/Lix
artifacts when needed, and runs the bootstrap task with the consumer root. Use --user-only on the mise task when system
activation should be skipped. Do not use pipe-to-shell bootstrap examples; verify downloaded bootstrap artifacts against
bootstrap/artifacts.toml before execution.
Common commands
Run commands from the consumer checkout or set MAISON_CONSUMER_ROOT:
maison doctor
maison consumer validate
maison plan
maison apply
maison update
maison self update
maison system plan
maison system apply
maison system history
maison system rollback
maison system clean
maison user plan
maison user apply
maison user status
maison user update
maison deploy example-linux
maison consumer validate runs the read-only consumer contract: flake composition, inventory, mise package and dotfile
declarations, fnox references, documentation links, supported systems, and raw-credential/private-key boundaries. It
never activates a system, mutates a lock file, or invokes fnox providers, so CI does not need personal credentials.
maison self update upgrades only the Maison input in the selected consumer’s flake.lock. It builds the locked
candidate CLI, validates the consumer through that candidate, and records the candidate CLI in owner-only local state;
failed updates restore both the lock and the prior CLI state. Maison’s own checkout and lock are never an update target.
The same operations are available as Maison mise tasks, for example mise -C ~/.maison run system:plan. Authoring
commands write only consumer files. They require a Git checkout, reject pre-existing changes in mutation targets, and
create focused commits only after successful transactions. Use Git for consumer history, with fast-forward-only pulls
when you choose to synchronize it.
maison apply is deliberately system-first: it activates the consumer’s Nix system layer and then converges its user
layer. A user-layer failure does not roll back the active Nix generation.
Deployment and recovery
maison deploy <host> requires a clean consumer working tree and transfers committed consumer content only. Nix
planning and deployment target the consumer flake; the Maison checkout and lock remain untouched.
Repository replacement uses a root-owned transaction boundary and revision-bound rollback. Restricted recovery repairs only reversible user state; package and application side effects are not rolled back. See the architecture, deployment guide, recovery guide, and package policy.
Ownership boundary
| Nix/Lix | mise |
|---|---|
| Nix daemon, caches, GC, and store policy | User tools and runtimes |
| PAM, sudoers, and privileged policy | Homebrew formulae and casks |
| Hostname, timezone, locale, users | Mac App Store applications |
| Root-owned files and SSH daemon policy | User dotfiles and preferences |
| System launchd/systemd services | User services |
| System-wide fonts and OS integrations | Non-privileged configuration |
A file, package, service, or preference must have exactly one owner. Consumer-owned state is never silently copied into or written to Maison.
Repository layout
flake.nix Maison development/release inputs
inventory.toml neutral Maison validation inventory
nix/ reusable OS modules and orchestration helpers
.mise/tasks/ Maison framework workflows
scripts/ bootstrap, validation, and deployment transactions
overlay_template/ Copier starter for fresh consumer repositories
mise.toml Maison development tools and task discovery
mise.lock Maison development tool lock
schemas/ public inventory schema
Consumer repositories own their flake.nix, flake.lock, inventory, host overrides, mise policy, dotfiles, deployment
state, and personal configuration.
Development
Maison uses repository-owned mise tools and dstack/Beads workflow controls:
mise install --locked
mise run check
uv run scripts/check-docs.py
mise exec -- hk check
Use /plan-features, /start-feature <slug>, /implement-feature <slug>, and /close-feature <slug> for planned work.
Maison overview
- Project kind:
infrastructure - Public framework:
RobertDeRose/maison - Consumer repository: an installation-specific Git repository
Purpose
Maison is a reusable macOS and Linux configuration framework. Nix/Lix owns privileged system state; mise owns consumer user tools, packages, applications, preferences, and dotfiles.
Intended users
The primary users are maintainers and operators who bootstrap, validate, apply, recover, and remotely deploy supported Maison-managed hosts.
Current scope
Maison manages Apple Silicon macOS, aarch64 Linux, and x86_64 Linux host configuration; local system activation; remote Linux deployment; consumer user-environment convergence; inventory validation; package/tool/app authoring; recovery; and documentation.
Repository boundaries
Maison owns reusable framework code, neutral examples, tests, documentation, dstack controls, and validation tooling. The
consumer owns flake.nix, flake.lock, inventory.toml, host topology, mise policy, dotfiles, deployment state, and
personal configuration. The consumer’s fnox-selected provider owns passwords, tokens, secret values, SSH private keys,
signing private keys, and other private key material.
Boundaries
Maison does not support Intel macOS, Home Manager, arbitrary unmanaged package ownership, strict offline byte-for-byte reproduction, or storing private infrastructure identity and trusted access material in the public framework.
Documentation conventions
Source responsibilities
- Beads stores workflow state, dependencies, priorities, claims, findings, and execution evidence.
- Feature
design.mdfiles store intended feature behavior and design decisions. - Reader-facing pages under
docs/src/describe current supported behavior. - Implemented-feature
index.mdfiles preserve feature-specific delivery and audit history. planned-features.mdprovides a human roadmap; Beads is authoritative for live execution state.- Code and tests provide implementation evidence.
Page placement
Choose a page by the question the reader is asking:
| Reader question | Documentation concern |
|---|---|
| What is this project, who is it for, and what does it currently promise? | Introduction |
| Why is it structured this way, and what boundaries or invariants apply? | Architecture |
| How do I use, deploy, configure, observe, operate, or recover it? | Operator’s Manual |
| How do I build, test, change, migrate, or extend it? | Development Guide |
| What is the exact command, configuration key, interface, field, schema, default, or contract? | Reference |
| What did one delivered feature change, and how was it verified? | Implemented Features |
Create a focused page only after the project has concrete facts that answer a durable reader question. Do not create an empty page merely to fill a category. Keep navigation, feature designs, and validation rules aligned when pages change.
Concerns for this infrastructure project
Add focused pages when the infrastructure project has concrete environment, deployment, operations, recovery, inventory, configuration, security, development, or architecture facts to document.
Feature documentation
A feature design must name exact existing pages to update and exact new pages to create. Section names alone are not sufficient. User-facing documentation must stand on its own; readers should not need the internal feature design to understand supported behavior.
Status language
- Planned: intended but not delivered.
- Implemented: present in code but not necessarily validated or fully documented.
- Supported: implemented, validated, documented, and part of the current contract.
- Deprecated: still present but scheduled for replacement or removal.
- Removed: no longer available.
Drift handling
When design, implementation, tests, Beads, or published docs disagree:
- record the mismatch in Beads;
- determine whether the divergence was intentional;
- correct the implementation or update the appropriate authoritative artifact;
- preserve the rationale and validation evidence;
- close the drift issue only after the sources agree.
Writing style
State current behavior directly. Separate planned behavior from delivered behavior. Prefer exact commands, paths, defaults, failure cases, and validation evidence where precision matters.
Architecture
Principle
Maison separates configuration by privilege and repository boundary:
Maison framework repository
├── reusable Nix modules, validators, task machinery, and CLI
├── neutral inventory/schema fixtures and documentation
└── framework development and release lock
Consumer repository
├── flake.nix and flake.lock
├── inventory.toml and hosts/
├── config/mise/ and dotfiles/
└── deployment and personal configuration state
Nix/Lix system layer
├── nix-darwin on Apple Silicon macOS
├── system-manager on Linux
├── nh for local Darwin builds and switches
└── deploy-rs for remote Linux system profiles
mise user layer
├── consumer tools and runtimes
├── ordinary Homebrew formulae, casks, and MAS apps
├── consumer dotfiles and Tera templates
├── current-user macOS defaults
└── user services
The consumer is the execution and lock root. Maison’s checkout is never a personal deployment root and consumer commands do not update or write its files.
Supported systems are aarch64-darwin, aarch64-linux, and x86_64-linux. Intel macOS is rejected by bootstrap,
inventory validation, mise lock platforms, Nix outputs, and CI.
Framework and consumer repositories
Maison is provider-neutral. It owns reusable code, validators, task machinery, Nix modules, public schemas, neutral fixtures, and the CLI runtime. It does not own personal users, real hosts, deployment endpoints, package selections, dotfiles, or consumer locks.
A consumer repository owns the files for one installation:
flake.nixcomposes Maison’s public inputs and exports consumer host outputs;flake.lockpins that consumer’s inputs;inventory.tomldescribes users, hosts, profiles, and deployment targets;hosts/contains host-specific system overrides;config/mise/anddotfiles/contain user policy; and- Git history is the source of deployment and authoring state.
Commands select the consumer in this order:
MAISON_CONSUMER_ROOT;- compatibility aliases
MAISON_REPOSITORYorMAISON_REPO; and - the Git checkout containing the current working directory when it is not Maison.
Packaged or explicit CLI invocations reject Maison itself as the consumer. Direct mise use from a Maison checkout is
reserved for framework development and validation; authoring helpers still refuse to mutate that checkout.
Consumers declare logical confidential values in fnox.toml and choose their provider. Secrets, passwords, tokens,
SSH private keys, and signing private keys resolve only at runtime and belong in the selected provider, not in either
repository or the Nix store.
Fresh consumer setup
Maison retains overlay_template/ as a Copier starter for new consumer repositories. bootstrap.sh --setup PATH
renders it into a destination separate from Maison, registers the current host through the consumer host:add task,
pins the generated consumer flake, and stops for review before activation. After the operator creates the consumer’s first
commit, normal bootstrap uses that consumer. This template is a setup-time scaffold, not a second runtime configuration
root: Maison does not save, hide, synchronize, publish, or otherwise manage an alternate consumer path.
The canonical consumer is Terroir. The archived private terroir.original checkout is migration input only, and the
former nix-config source is a private archived framework reference; neither is an active Maison input. Maison’s public
history is fresh and contains only reusable, neutral content. The migration manifest determines what belongs in Terroir.
Nix system layer
The framework exposes reusable modules and public helpers. A consumer composes them into its own
darwinConfigurations, systemConfigs, and deploy outputs.
macOS
nix-darwin owns Nix/Lix daemon policy, primary-user registration, system shells, Apple Watch and Touch ID sudo, PAM,
login-window policy, Caps Lock remapping, host identity, time zone, services, system-wide fonts, and OS-integrated
installers such as FUSE-T. Ordinary applications and interactive CLI packages do not belong in
environment.systemPackages.
Linux
system-manager owns hostname, locale, time zone, managed users, sudoers, SSH daemon includes, root-owned Nix
configuration, authorized-key refresh, and system services. Linux activation verifies systemd, hostname, timezone and
/etc/localtime, SSH configuration/reload, and Maison-managed service units. A dedicated maison-deploy account owns
privileged deployment and repository-transaction entrypoints with command-scoped sudo rather than NOPASSWD: ALL.
/nix/var/nix/profiles/system-manager-profiles/system-manager is the canonical Linux system profile.
Upstream system-manager Rust tests remain enabled. Maison does not override package checks to disable them.
nh and deploy-rs
Darwin planning and activation use the consumer flake:
nh darwin build /path/to/consumer -H <host>
nh darwin switch /path/to/consumer -H <host>
Tasks fall back to Maison’s public nh app before the system profile installs nh. Linux local activation uses the
pinned system-manager CLI because nh does not expose a system-manager command.
Each deploy-enabled Linux consumer host becomes deploy.nodes.<host>. Its profile uses the canonical system-manager
closure, command-scoped deployment privilege, exact $PROFILE/bin/activate activation, and inventory-controlled
autoRollback and magicRollback. The local deploy-rs adapter uses one pkgs.deploy-rs package for its CLI, remote
activation binary, and matching schema.
mise user layer
Maison’s mise.toml owns framework development tools and task discovery. The selected consumer’s
config/mise/config.toml is the global user layer. Maison’s project configuration remains the project layer, so running
consumer commands does not dirty the Maison checkout.
During user convergence, Maison temporarily hides installed symlinks that point into the consumer configuration so mise can resolve relative dotfile sources from the consumer. Successful convergence retains the new consumer targets; dry-runs and failures restore the installed links.
Consumer package policy may own:
- cross-platform tools and Homebrew formulae in
config/mise/config.toml; - macOS tools and preferences in
config/mise/config.macos.toml; - Apple Silicon casks and MAS applications in
config/mise/config.macos-arm64.toml; - Linux-specific policy in
config/mise/config.linux.toml; and - native application configuration under
dotfiles/.
macOS defaults boundary
Mise may write current-user preference domains such as Dock, Finder, trackpad, screenshots, and application preferences.
It must not own sudo defaults domains, /Library/Preferences, login-window policy, authentication policy, PAM, or
host-scoped defaults -currentHost values.
Inventory interface
Shell tasks do not parse TOML with awk. schemas/inventory.toml is the public schema contract for supported systems,
profiles, feature keys, deploy keys, and defaults. .mise/lib/inventory.py uses Python tomllib for typed lookups, and
Nix imports the same schema contract before constructing outputs. Shared fixtures keep both validators aligned.
The consumer inventory replaces Maison’s neutral starter inventory; it is not merged with it. Host override directories are resolved relative to the consumer root and are allowed only when they match an inventory host.
Transaction boundaries
Local authoring commands require a Git checkout of the selected consumer. A deployed snapshot has .maison-revision but
no .git and is runtime source only. host:add, tool/package/app mutations, and update fail rather than writing
Maison or a deployed snapshot.
Local mutations serialize checked-in consumer writes through one target-repository lock. Journals live under
${XDG_STATE_HOME:-$HOME/.local/state}/maison/repository-mutations/ or the test override
MAISON_REPOSITORY_MUTATION_STATE_DIR, keyed by the canonical consumer path with owner-only permissions. A journal
copies original and candidate files so startup recovery can restore an incomplete mutation before reading mutable state.
Covered add/remove operations preserve unrelated tracked and untracked work, ignore ignored files, and reject dirty declaration or lock targets. Once validation and the journal complete, only the operation’s declaration and generated lock paths enter a focused commit. Maison does not fetch, pull, or push the consumer:
added(scope): `identifier`
removed(scope): `identifier`
A commit failure is post-transaction: validated consumer files remain in place for manual recovery and external package or application effects are not rolled back.
Remote deployment has an additional repository transaction around the consumer user phase:
maison apply
1. system:apply (consumer Nix generation)
2. user:apply (consumer mise convergence)
A failed system activation stops before user convergence. A failed user convergence leaves the active system generation in place. Deployment archives only committed consumer content, stages it through a root-owned same-filesystem transaction, and restores the prior consumer revision before restricted recovery. Package/application side effects remain follow-up work and are recorded in the recovery report.
The local mutation journal and the privileged remote deployment namespace are separate safety boundaries.
Repository migration contract
Maison and Terroir are separate repositories with separate ownership:
| Repository | Role | Owns |
|---|---|---|
RobertDeRose/maison | Public framework | Reusable Nix modules, mise orchestration, CLI, schemas, validation, tests, and neutral documentation |
RobertDeRose/terroir | Private consumer | flake.nix, flake.lock, inventory, hosts, mise policy, dotfiles, deployment state, and personal/site configuration |
archived private terroir.original | Historical consumer source | The pre-split personal/site configuration used to seed the fresh Terroir history |
archived nix-config | Historical framework source | The complete pre-split history and recovery reference |
Terroir is a consumer repository, not a second Maison checkout or command surface. Bitwarden and the consumer-selected fnox provider remain authoritative for passwords, tokens, private keys, and other confidential values. No secret value or private key is copied into either repository.
Migration order
- Freeze the archived private Terroir source and the source framework checkout; preserve both complete Git histories.
- Produce a reviewable manifest classifying every candidate path as public Maison content, private Terroir content, neutral replacement, or excluded material. Record the destination and rationale.
- Obtain explicit approval of the manifest before copying, replacing, or deleting any candidate path.
- Create fresh Git repositories for Maison and Terroir. Do not reuse
.git, refs, source commits, or source author metadata. Maison retains its dstack/Copier project controls; Terroir remains a plain configuration repository. - Populate each repository only from approved manifest entries. Replace public personal data with neutral examples and exclude secrets, private keys, real endpoints, and unrelated machine state.
- Validate both repositories without system activation: public-privacy scans, raw-secret scans, inventory validation, consumer validation, flake checks, documentation checks, and fresh-history checks.
- Confirm the new checkouts and their remotes before changing the source repository’s visibility or archive state.
- Make the historical
nix-configrepository private and archived only after all validation and remote checks pass. - Retain the approved manifest, source backup, staging trees, and migration evidence until the operator confirms the transition.
Fresh-history requirements
The public Maison history must contain only fresh logical commits and no imported source objects or private paths. The private Terroir history may contain only approved configuration and documentation, with no raw secrets or private-key material. A failed migration leaves both new repositories and the historical source intact for explicit repair; it never silently deletes or publishes a repository.
Recovery
If a new repository fails validation, stop the transition and repair it from the approved manifest. If the source must be
consulted, use the archived private terroir.original or nix-config checkout, or the preserved local source backup. Do not restore personal
configuration into public Maison. Re-run privacy, fresh-history, consumer, inventory, and documentation checks before
resuming the archive transition. The new Terroir checkout must retain a fresh history; do not import commits from
terroir.original.
After migration, normal operations select Terroir with MAISON_CONSUMER_ROOT or by running from its checkout. Maison
never stores a saved alternate configuration root and never writes the consumer’s files into its own checkout.
Operations
Select a consumer
Maison is the framework; the consumer repository is the execution and lock root. Select it explicitly or run from its Git checkout:
export MAISON_HOME="$HOME/.maison"
export MAISON_CONSUMER_ROOT="$HOME/src/terroir"
maison doctor
The consumer must contain flake.nix, flake.lock, and inventory.toml. Maison’s checkout is never used as the
personal deployment root.
Bootstrap
Use an existing consumer directly:
./bootstrap.sh --consumer "$HOME/src/terroir" --host "$(hostname -s)"
For a fresh consumer, use the retained Copier starter:
./bootstrap.sh --setup "$HOME/src/terroir" --host "$(hostname -s)"
Bootstrap renders overlay_template/ into the separate destination, registers the current host through Maison’s
validated host:add task, creates the consumer flake.lock, and stops for review without activation. Inspect the output,
create the consumer’s first Git commit, then rerun with --consumer to activate it. The starter is setup-time scaffolding;
the consumer owns all generated files and Git history. Interactive bootstrap without a consumer offers this path;
non-interactive runs require --consumer or --setup. No neutral Maison inventory is activated as a substitute.
Preview
maison system plan
maison user plan
maison plan
Planning labels system and user phases and never mutates the consumer or Maison. System planning may realize a Nix store/cache derivation, but never activates it. User planning renders its dry-run command sequence without invoking mise, package, dotfile, trust, or migration commands.
maison user plan --force-dotfiles
maison user apply --force-dotfiles
Forced apply snapshots refused dotfile targets under
~/.local/state/maison/backups/dotfiles/<timestamp>/. Restore a reviewed snapshot explicitly:
maison user restore-dotfiles ~/.local/state/maison/backups/dotfiles/<timestamp> --force
Apply
maison system apply
maison user apply
maison apply
maison apply activates consumer system state first, then converges consumer user state. A user-layer failure does not
roll back the active Nix generation. Linux activation requires a running systemd runtime and performs the configured
runtime verification.
Inspect drift and repository state
maison doctor
maison user status
maison system history
git -C "$MAISON_CONSUMER_ROOT" status --short
Maison reports user drift and system generations. Consumer Git history is operator-owned; inspect, pull, commit, and push it with Git rather than through a Maison repository command.
Authoring and updates
Authoring commands write only consumer files and require a Git checkout. They reject pre-existing changes in mutation targets, serialize mutations per repository, and create focused commits after successful transactions:
maison host add laptop --system aarch64-darwin --user operator
maison tool add github:owner/tool latest
maison package add brew:tool
maison app add ghostty
Update the consumer flake independently from Maison:
maison update # update the consumer flake.lock
maison update nixpkgs # update one consumer input
maison update --check # update, then validate
maison self update # upgrade Maison from the consumer's pinned input
maison user update # upgrade consumer mise-managed tools
maison package update # upgrade consumer Homebrew formulae
maison app update # upgrade consumer applications
maison update restores the consumer flake.lock if the update or optional validation fails. Maison’s lock is never an
implicit fallback. Use maison self update when upgrading the framework itself: it updates only the consumer’s Maison
input, builds and validates the candidate CLI, and rolls back the lock plus owner-only CLI state on failure. Scheduled
Maison dependency automation updates only Maison’s own lock and remains review-gated.
Deployment
maison deploy example-linux
Deployment requires a clean consumer tree and transfers committed consumer content only. Nix evaluation and deployment target the consumer flake; Maison’s checkout remains untouched. Use a fast-forward-only Git pull or an explicit push for the consumer before or after deployment when needed.
Recovery
Repository replacement uses the root-owned transaction boundary and revision-bound rollback. Restricted recovery repairs
only reversible user state, skips package/application side effects, and writes diagnostics under
~/.local/state/maison/recovery/. Deployed snapshots are runtime artifacts, not authoring checkouts.
Validation
maison check
mise install
mise run check:tests
mise exec -- hk check
Run focused subsystem tests while editing, then run the repository suite before committing.
Remote deployment
The consumer repository is the deployment source. Maison supplies the reusable modules, CLI, transaction helpers, and runtime artifacts; it is never the personal deployment root.
System only
maison system deploy example-linux
maison system deploy example-linux --dry-activate
This invokes deploy-rs against the consumer’s deploy.nodes.example-linux.profiles.system. The closure is activated as
root under /nix/var/nix/profiles/system-manager-profiles/system-manager. Linux deployment verifies systemd, hostname,
timezone, SSH configuration, and Maison-managed service units.
System and user
maison deploy example-linux
Deployment requires a clean consumer working tree and creates an archive from committed consumer content only. The archive
contains .maison-revision, but never .git, dirty changes, untracked files, ignored files, or local credentials. The
remote tree is a runtime snapshot, not an authoring checkout.
The privileged extractor bounds the uploaded archive at 256 MiB compressed, 4,096 members, 64 MiB per member, and 256 MiB total expanded regular-file content. It validates and extracts members incrementally and rejects traversal, symlink, and special-file entries.
After system activation, Maison:
- uploads the committed consumer archive;
- validates the managed-user repository path;
- allocates a root-owned transaction root on the same filesystem;
- recovers incomplete transaction state;
- stages the new consumer tree and rollback source;
- records expected revisions and runs user convergence as the managed user;
- restores and verifies the prior consumer revision after user failure;
- runs restricted recovery from the restored consumer; and
- finalizes or rolls back from the root-owned journal.
Restricted recovery repairs reversible dotfiles, mise lock links, non-package mise state, and finalization. It does not
rerun package/application convergence, application-backup migration, system activation, or Nix rollback. Recovery writes
a mode-0600 diagnostic under ~/.local/state/maison/recovery/; the original convergence failure remains the deployment
exit status.
Remote system and repository actions use the separate deployment SSH account configured by the consumer inventory (usually
maison-deploy). Sudo access is command-scoped and argument-bounded for deploy activation and Maison transaction helpers.
When the remote managed user lacks mise, the fallback reads bootstrap/artifacts.toml from the Maison runtime, verifies
the pinned artifact digest, and installs only that binary. Use --system-only to skip repository transfer and user
convergence.
Inventory fields
Real endpoints belong in the consumer inventory.toml:
[hosts.example-linux.deploy]
enable = true
hostname = "example-linux.example.invalid"
ssh_user = "maison-deploy"
user_ssh_user = "operator"
repo_path = "/home/operator/.maison"
remote_build = false
auto_rollback = true
magic_rollback = true
ssh_user activates the system with deployment privileges. user_ssh_user must equal the managed inventory username.
repo_path must be a normalized descendant of /home/<user>; both Python and Nix validation reject traversal and unsafe
paths. Deployment keys and field types are defined by schemas/inventory.toml.
Safety boundary
Deploy-rs rollback covers the system profile. Repository rollback covers the consumer tree used for subsequent user convergence and recovery. Privileged transaction state is inaccessible to the managed user. Mise changes completed before a later user-step failure are convergent state, not a Nix generation; package/application side effects remain recorded follow-up work.
Recovery
Bootstrap verification recovery
Bootstrap verification metadata lives in bootstrap/artifacts.toml. If a mise or Lix artifact fails verification, do
not execute it manually. Retry once; if it still fails, inspect the URL and checksum, then rerun with the reviewed pinned
artifact.
Consumer selection recovery
The consumer repository is the execution and lock root:
export MAISON_HOME="$HOME/.maison"
export MAISON_CONSUMER_ROOT="$HOME/src/terroir"
maison doctor
The consumer must contain flake.nix, flake.lock, and inventory.toml. If it is unavailable, restore that Git checkout
or set MAISON_CONSUMER_ROOT to another reviewed checkout. Never copy private inventory or deploy endpoints into Maison
as a recovery shortcut; secrets and private keys remain in the consumer’s fnox-selected provider.
System rollback
maison system history
maison system rollback
On Darwin, the task prefers darwin-rebuild switch --rollback and does not move the profile first. On Linux, it rolls
back /nix/var/nix/profiles/system-manager-profiles/system-manager and activates the selected profile’s bin/activate.
Failed aggregate apply
maison apply runs consumer system state first. A system failure prevents user convergence. A later user failure leaves
the successfully activated system generation in place:
maison user status
maison user plan
maison user apply
Rejected authoring command in a deployed snapshot
A deployed snapshot contains .maison-revision without .git. It can run runtime commands such as plan, apply, status,
deployment finalization, and recovery, but Maison rejects repository-writing commands there because the next deployment
will overwrite local source edits. Retry from the consumer Git checkout.
Failed local repository mutation
Local authoring mutations store untracked lock and journal state under:
${XDG_STATE_HOME:-$HOME/.local/state}/maison/repository-mutations/
Each state directory is keyed by the canonical consumer path and uses owner-only permissions because journals can copy consumer data. If a mutation is interrupted, the next mutation for that consumer acquires the lock, recovers incomplete journals, and only then reads repository state. If recovery fails, Maison preserves the journal, original copies, candidate copies, and diagnostics. Do not delete the journal directory until the consumer files and preserved copies are understood.
Failed user apply
Default maison user plan and maison user apply do not force replacement of whole-file conflicts. To preview and then
perform the same forced handoff:
maison user plan --force-dotfiles
maison user apply --force-dotfiles
The forced plan is read-only. The forced apply backs up exact targets mise refused before replacement. Backups live under
~/.local/state/maison/backups/dotfiles/<timestamp> and contain a manifest; inspect it before restoring:
maison user restore-dotfiles ~/.local/state/maison/backups/dotfiles/<timestamp> --force
Restore refuses paths outside Maison’s backup root, malformed or escaping manifest entries, and missing payloads. It stops
at the first failure and can be retried for entries still marked pending.
Before package convergence on macOS, Maison archives live application bundles in the backup tree with ditto, verifies the
ZIP, unregisters the bundle, and removes the original only after verification. Docker Desktop compatibility handling is
limited to the documented exact completion-link and cask cases; unrelated installer failures remain errors.
Failed remote user deployment
Deployment archives only committed consumer content and keeps repository transaction state in a root-owned same-filesystem namespace outside the managed user’s writable home. The deployment account uses command-scoped sudo elevation.
When consumer user convergence fails, deployment verifies and restores the prior consumer revision before running
restricted recovery as the managed user. Recovery repairs reversible dotfiles, mise lock links, non-package mise state,
and finalization. It does not rerun package/application convergence, application-backup migration, system activation, or
Nix rollback. An explicit --force-dotfiles choice is preserved; recovery never enables it implicitly.
Recovery writes remote-convergence-<failed-revision>.json under ~/.local/state/maison/recovery/ with failed/restored
revisions, exit statuses, completed steps, and package/application side effects. The original convergence failure remains
the deployment exit status. If rollback or transaction cleanup fails, inspect the journal, staging tree, rollback tree, and
lock before making changes.
On startup, Maison runs incomplete-transaction recovery before user convergence.
Failed system deployment
The user phase does not start until the system phase succeeds. Linux activation requires systemd and verifies hostname,
timezone/localtime, SSH configuration/reload, and Maison-managed service units. With auto_rollback and
magic_rollback enabled, deploy-rs attempts to restore the prior system profile after activation failure or lost
confirmation connectivity.
nix-darwin adoption files
The one-time Darwin preflight may preserve unmanaged files as:
/etc/nix/nix.conf.before-nix-darwin
/etc/nix/nix.custom.conf.before-nix-darwin
Retain them until several successful switches have completed.
Failed repository migration
If the Maison/Terroir migration fails, keep both staging checkouts, the approved manifest, and the original historical
source intact. Do not make the source public, delete it, or remove local backups as a recovery step. Re-run privacy,
consumer-boundary, fresh-history, and remote-state checks before archiving the source repositories. Consult only the
private terroir.original and nix-config archives. Secrets and private keys are never recovered from Git.
Broken shell
Use /bin/zsh or /bin/bash, ensure ~/.local/bin is on PATH, and inspect ~/.zshrc plus ~/.config/maison/zsh.
The shell is user-owned, so repairing it does not require rebuilding the Nix system closure.
Package policy
Decision order
- mise tool backend for versioned, user-scoped developer tools with prebuilt releases.
- mise built-in Homebrew formula for ordinary native CLI packages and shared-library applications.
- Homebrew cask or MAS through mise for ordinary user-facing macOS applications.
- Nix/nix-darwin for system-wide fonts, filesystem integrations, system services, privileged activation, and Nix administration workflows.
A package has one owner and may not be duplicated between [tools], [bootstrap.packages], and the Nix system closure.
Consumer package files
Maison keeps neutral, schema-valid package stubs. The consumer repository owns package policy:
config/mise/config.toml: cross-platform policy;config/mise/config.macos.toml: macOS policy;config/mise/config.macos-arm64.toml: Apple Silicon policy; andconfig/mise/config.linux.toml: Linux policy.
Common declarations remain in config/mise/config.toml; package --macos selects the Apple Silicon file. Inventory
profiles (base, dev, mac, and linux) select Nix modules, not a mise profile selector.
Intel macOS is unsupported; there is no placeholder configuration pretending to provide package parity.
Contributor tools
Repository validation, formatting, hooks, and documentation tools live in Maison’s mise.toml. Install them explicitly:
mise install
Normal maison user apply converges the consumer’s user config and does not install Maison’s contributor toolchain on
every managed host.
Mutations
maison tool add <backend:tool> [version]
maison tool remove <backend:tool[@version]>
maison package add <manager:package> [--macos]
maison package remove <manager:package> [--macos]
maison app add <cask>
maison app remove <cask>
Covered operations require a clean consumer Git checkout. They preserve unrelated work, leave ignored files untouched,
refuse dirty declaration or lock targets, and create focused commits after the transaction journal completes. Commit
failures leave validated files in place for manual recovery. Removing one version of a multi-version tool runs a targeted
mise lock --global <tool> against candidate files; full tool removal deletes that tool’s generated lock block directly.
Task reference
All commands operate on the selected consumer repository. Set MAISON_CONSUMER_ROOT or run from the consumer
checkout. The consumer must own flake.nix, flake.lock, and inventory.toml; Maison’s checkout is never an implicit
configuration root.
Aggregate commands
| Task | Behavior |
|---|---|
doctor | Diagnose the consumer’s two ownership layers without mutation |
consumer validate [--consumer PATH] | Validate consumer contracts without activation or provider access |
plan [--host HOST] [--force-dotfiles] | Preview consumer system state, then render the read-only user plan |
apply [--host HOST] [--force-dotfiles] | Apply consumer Nix state, then converge consumer user state |
update [INPUT] [--check] | Update only the consumer flake.lock; optionally validate |
self:update | Transactionally upgrade Maison from the consumer lock |
deploy HOST | Deploy the consumer’s Linux system profile and user state |
rollback | Roll back only the active Nix system generation |
bootstrap [--consumer PATH] | Verify Maison runtimes, select a consumer, and converge both layers |
check | Validate Maison framework data, scripts, tests, and Nix outputs |
System commands
| Task | Behavior |
|---|---|
system:plan | Build the consumer system closure without activation |
system:apply | Activate the consumer’s Darwin or system-manager output |
system:deploy | Deploy the consumer’s Linux system profile through deploy-rs |
system:history | List canonical system-profile generations |
system:rollback | Select and activate the previous generation |
system:clean [AGE] | Remove old system generations and collect the store |
User commands
| Task | Behavior |
|---|---|
user:plan [--force-dotfiles] | Render user convergence without invoking mutating commands |
user:apply [--force-dotfiles] | Apply consumer dotfiles, packages, tools, apps, and preferences |
user:restore-dotfiles BACKUP --force | Restore a manifest-backed Maison dotfile backup |
user:status | Report consumer user-environment drift |
user:update | Upgrade consumer mise-managed tools |
package:update | Upgrade consumer Homebrew formulae |
app:update | Upgrade consumer casks and Mac App Store apps |
Consumer configuration commands
| Task | Behavior |
|---|---|
host:add | Add a validated host to the consumer inventory and optional override |
host:list / host:validate | Read or validate the consumer inventory |
tool:add / tool:remove | Transactionally edit and focused-commit consumer tool config/lock |
package:add / package:remove | Transactionally edit and focused-commit consumer package config |
package:search | Search package sources without repository mutation |
app:add / app:remove | Transactionally edit and focused-commit consumer cask config |
docs:build / docs:serve | Build or serve the Maison contributor book |
Authoring commands require a Git checkout of the consumer and reject dirty mutation targets. plan, user:status,
list, validate, and search remain read-only. Deployed snapshots without .git reject source mutations and point
back to the consumer authoring checkout.
consumer validate is read-only and runs the Maison-owned consumer contract. It checks the consumer flake and lock,
inventory, supported systems, package and dotfile declarations, fnox references, documentation links, and raw
credential/private-key boundaries. It performs check-only Nix evaluation and never invokes fnox or activates a system.
user:plan and user:apply share one command plan. Plan renders dry-run variants; apply performs trust, package, dotfile,
lock-link, and finalization steps. Forced dotfile replacement creates an exact manifest-backed backup before mutation.
Hidden nix:* tasks are compatibility aliases for system:* tasks.
Verified bootstrap artifacts
Bootstrap metadata is checked in at bootstrap/artifacts.toml. Maison verifies each selected mise or Lix artifact before
installation. Runtime code comes from Maison; configuration and lock mutations are always rooted in the consumer.
When remote user convergence fails, deployment restores the prior consumer revision before restricted recovery. Recovery
repairs reversible user state without package/application convergence and writes a mode-0600 diagnostic under
~/.local/state/maison/recovery/.
Add a host
Real hosts belong in the consumer repository’s inventory.toml. Select the consumer explicitly or run from its Git
checkout:
export MAISON_HOME="$HOME/.maison"
export MAISON_CONSUMER_ROOT="$HOME/src/terroir"
maison host add "$(hostname -s)" --user "$(id -un)"
host:add writes the consumer inventory and, when requested, an optional hosts/<hostname>/system.nix override. It
requires a Git authoring checkout, validates the candidate inventory before replacement, and refuses to use Maison’s
neutral inventory as a mutation target.
The inventory must satisfy the shared schema in schemas/inventory.toml: supported systems are aarch64-darwin,
aarch64-linux, and x86_64-linux; profiles are base, dev, mac, and linux. Duplicate profiles, unknown
feature keys, unknown deploy fields, and wrong value types are rejected.
For a deployable Linux host, add an explicit deployment table to the consumer inventory.toml:
[hosts.example.deploy]
enable = true
hostname = "example.example.com"
ssh_user = "maison-deploy"
user_ssh_user = "operator"
repo_path = "/home/operator/.maison"
remote_build = false
auto_rollback = true
magic_rollback = true
Then validate and preview without activation:
maison host validate
maison system plan --host example
maison system deploy example --dry-activate
Host override directories must match an inventory host and may contain only system.nix. Host override files contain
OS-level exceptions; user differences belong in consumer mise configuration or dotfiles.
Add a tool
Search first:
maison package search <name>
Prefer a mise backend for versioned standalone tools. Add tools to the consumer repository:
maison tool add github:owner/repository latest
maison tool add npm:@scope/package latest
Use a bootstrap package for a Homebrew bottle or host-integrated package:
maison package add brew:tool --macos
Only add a package to Nix when system activation or a privileged service depends on it. Tool and package authoring writes
only consumer files, requires a clean Git authoring checkout, refreshes the consumer fast-forward-only, and rejects dirty
declaration or lock targets. A successful mutation creates a focused commit containing only its declaration and generated
lock paths, such as added(tool): \github:owner/tool@version``. If commit creation fails, validated files remain in place
for manual recovery.
Tool and package commands use parser-backed TOML edits. Supported comments, quoted keys, table boundaries, arrays of tables, and CRLF line endings are preserved; malformed configuration fails before Maison replaces the file.
Add a macOS application
Apple Silicon applications belong in the consumer repository’s config/mise/config.macos-arm64.toml:
maison app add example-app
maison user plan
maison user apply
app add requires a clean Git authoring checkout of the consumer, refreshes it fast-forward-only, and refuses a dirty
application declaration before installing. It records the declaration only after successful installation and creates a
focused added(app): \ghostty`commit.app remove` leaves installed data in place and creates the corresponding
focused removal commit. Maison is never a fallback mutation target.
Mac App Store entries use numeric mas: identifiers in the same file. System-wide fonts remain in
nix/modules/darwin/fonts.nix. Casks that install OS integrations, such as FUSE-T, require an explicit nix-darwin
module review instead of being added to the mise user package list.
Intel macOS is not a supported Maison target.
Developer tooling
This page applies to the public Maison framework repository. Maison uses dstack/Copier controls and Beads for project lifecycle work. Consumer repositories are plain Git configuration roots: they own their flake, lock, inventory, host configuration, mise policy, and dotfiles, but do not need to install Maison’s contributor workflow.
The repository uses mise to provide project tools and named tasks. Install mise, then run:
mise install --locked
The project toolchain includes Python 3.13.14. Bootstrap installs and activates that locked runtime with
mise exec --locked python, so its Python helpers do not depend on the host Python installation. Use the same
explicit runtime for direct Python commands:
mise exec --locked python -- python -m unittest -v tests.test_inventory_behavior
Use the same commands locally and in automation:
mise run check
mise run check:tests
mise exec --locked python -- python -m unittest -v tests.test_inventory_behavior
mise exec --locked python -- python -m unittest -v tests.test_deployment_contracts
mise exec --locked python -- python -m unittest -v tests.test_migration_behavior
mise exec --locked python -- python -m unittest -v tests.test_ownership_boundary
mise exec --locked python -- python -m unittest -v tests.test_repository_contracts tests.test_repository_mutation
mise exec --locked python -- python -m unittest -v tests.test_transaction_behavior
mise run fix
mise run docs:check
mise run docs:build
mise run docs:deployment:enable
mise run docs:serve
Use /update-project for the recorded template channel, or /update-project --stable / --unstable to change it. The
update always records the exact resolved template commit.
check is read-only. fix changes the working tree. Contextlint checks links, anchors, and image targets across README
and docs/**/*.md. The pre-commit hook may fix files while safely stashing unrelated unstaged work. The commit-message
hook enforces Conventional Commits, required scopes for changelog-visible changes, grammar, 72/100-character line
limits, and canonical optional Beads: footers. Harper applies its full native rule set to human-authored text after
filtering Git comments/diffs, canonical release subjects, and the canonical machine-readable footer. Run cog changelog
to preview the concise user-facing changelog. The hk policy uses native built-in steps whenever their behavior matches;
hk’s file locking coordinates independent steps. No dependency chain serializes unrelated checks. Go projects retain
two output-sensitive edges: gofumpt follows goimports, then fix-only module tidy observes the final imports.
No recognized language profile is active; only the universal tooling baseline runs.
Pi extensions are personal consumer content and are maintained in the private Terroir repository. Maison does not install or validate those runtime extensions.
The Python regression suite is split by subsystem. check:tests runs unittest discovery across every
tests/test_*.py file and is expected to complete within five minutes on a warm supported development checkout. Use the
focused commands above while editing a subsystem, then run mise -E dev run check once before committing broad or shared
test changes.
Tests that execute external commands use the shared helper in tests/support/processes.py. It defaults to a 30 second
timeout, starts commands in an isolated process group where supported, terminates the group on timeout, captures stdout
and stderr, and truncates failure diagnostics. Longer timeouts must be explicit, documented at the call site, and no
longer than 300 seconds.
Maison’s TOML mutation helper uses a pinned tomlkit wheel checked in under .mise/vendor/. The helper verifies the
wheel digest before importing it, so configuration repair commands still work when normal mise project tool resolution
is skipped or broken and do not depend on ambient Python site-packages.
GitHub validation
.github/workflows/ci.yml runs on every push to main and pull request. Its repository-checks job installs Nix,
isolates user-global mise configuration, installs the committed tools, and runs mise run check. The same workflow
also builds supported platform outputs and exercises the pre-mise bootstrap boundary.
.github/workflows/cache-refresh.yml is dependency-update automation, not an approval path. It may refresh flake.lock,
build cache targets, push or update the automation/refresh-flake-lock branch, open or edit the matching pull request,
and run the reusable hk and CI workflows against that branch. It must leave the pull request open for ordinary review
and branch protection; it must not run gh pr merge, use --admin, or enable auto-merge.
The consumer is selected locally with --consumer during bootstrap, MAISON_CONSUMER_ROOT, or the current consumer
Git checkout. Consumer files and locks are ordinary Git state and are never copied into Maison. Do not commit consumer
contents or Bitwarden material to Maison while running development checks.
Consumer validation
Consumers can run the Maison-owned contract without adding a second test suite:
maison consumer validate --consumer /path/to/consumer
The read-only check validates the consumer flake and lock, inventory, supported systems, mise package and dotfile
configuration, fnox references, documentation links, and raw credential/private-key boundaries. It evaluates expected
Nix outputs without activation or lock updates, and the validator itself never invokes fnox providers; provider
credentials are therefore not needed by the validation contract in CI. maison self update uses this same command
through the candidate Maison package after changing only the consumer’s Maison input, so failed framework upgrades can
restore the prior lock and local CLI state without touching Maison’s own checkout lock.
Hooks and recovery
Setup installs repository-local hk hooks when the destination is a Git repository. To restore tooling after an offline or degraded setup, run:
python3 scripts/setup-tooling.py --json
The command gives lock, install, and hook stages one temporary MISE_CONFIG_DIR, removes inherited global config
overrides, and deletes the temporary directory on exit. It preserves the scaffold on failure, reports the failed stage,
and uses the same command above for recovery. A repository created without Git can install hooks after Git initialization
with:
python3 scripts/setup-tooling.py --json
Feature lifecycle
Responsibilities
Beads executable state and dependencies
docs/src/features/<slug>/design.md intended feature behavior and design
reader-facing docs under docs/src/ current supported behavior
code and tests implementation evidence
docs/src/features/<slug>/index.md delivered reconciliation and audit record
Workflow commands are installed from RobertDeRose/dstack with the skills CLI. The CLI manages the agent-specific installation paths and updates; Copier manages this repository scaffold.
Start a session
bd prime
bd ready --type epic --label workflow:feature --json --limit 0
bd ready --json
Plan
/plan-features asks design-changing questions, defines the documentation architecture, creates slug-named feature
designs, pours one Beads epic/molecule per feature, and decomposes lifecycle and implementation into bounded child
tasks. Native planning resolves every decision needed by those tasks before implementation; unresolved decision gaps may remain only on imported migration work with explicit reconciliation blockers. It recommends the next feature by canonical slug and human name rather than by an opaque Beads hash.
A new feature uses:
docs/src/features/feature-slug/design.md
feat/feature-slug
Review and start
/start-feature <slug> resolves the human feature reference through Beads, activates the worktree, and runs
four isolated reviews. An exact feature name or a unique name fragment also resolves; the Beads ID remains
internal mutation/audit evidence.
The feature root is an epic. Lifecycle tasks are direct children, and bounded implementation tasks sit beneath the implementation coordinator task. A milestone is not used as the feature container.
One fresh, read-only context builder gathers a factual evidence packet once. Four fresh reviewers share that packet and independently cover:
- architecture consistency;
- simplicity and maintainability;
- documentation readiness;
- execution-graph readiness.
The packet contains factual source locations but no findings, recommendations, or verdict. Reviewers read extra source when it is insufficient. Do not add confidence reviewers without a distinct uncovered risk or user request. Fix verification resumes only affected reviewers; fresh replacements are used only when an original is unavailable or the scope materially changes, and receive the original evidence and post-review diff. Refresh a shared packet only after broad design, architecture, task-graph, or documentation-structure changes.
It reconciles clear findings, asks only blocking design questions, commits the reviewed design, and closes spec-reconcile only when implementation can proceed without inventing intent. A successful start records the canonical feature in repository-local Git configuration so /implement-feature can resume it from the base worktree when no selector is supplied.
Implement
Claim the next ready task beneath the implementation coordinator:
bd ready --parent <implementation-id> --claim --json
bd show <task-id> --json
Use parent-child for hierarchy and blocks only for real prerequisites. Keep code, tests, and affected documentation aligned in the same work unit. Record validation and review evidence, include the Beads ID in the commit message, and close the task only after its acceptance criteria pass. Each task gets exactly one fresh reviewer; fixes resume that reviewer. A fresh replacement is allowed only when the original is unavailable or scope materially changes. /implement-feature then claims the next ready child and continues until the implementation coordinator closes. It pauses only when every remaining child is blocked on explicit user decisions; native planned work should never reach that state.
Discovered work should retain provenance:
bd create "Describe discovered work" \
--type task \
--deps discovered-from:<current-task-id> \
--json
Add a blocking edge only when the discovery is required for safe completion.
Close
/close-feature compares delivered code with the design and reader-facing docs, creates a standalone implemented-feature record, and runs validation. One fresh context builder supplies a factual packet to two fresh holistic reviewers for delivery and drift. They follow the neutrality, extra-source, refresh, confidence-review, and replacement rules above; fixes resume only the affected reviewer. The workflow then performs an explicit pr, merge, or ready action. With no mode, it asks which action to take. Merge mode uses git merge --ff-only unless the target repository’s AGENTS.md explicitly permits merge commits; it never falls back to a merge commit after a failed fast-forward. Native Beads can append selected-feature rows to the tracked .beads/interactions.jsonl in the base worktree during close-out. Merge mode verifies that this is the only dirty path and that every change is append-only and belongs to the selected feature molecule or separately identified work with a discovered-from or parent-child path back to it, commits those rows on the feature branch, and restores the base copy only after committed preservation. Delivery and root closures happen after the merge; their interaction rows receive a separate interaction-only commit on the base branch. Malformed, rewritten, foreign, or mixed dirty state remains blocking.
Audit
/audit-project periodically compares Beads, designs, current docs, implemented-feature records, code, tests, and recent commits. Drift becomes linked Beads work rather than an untracked note.
Skill maintenance
After editing a canonical skill:
npx skills update
GitHub Pages deployment
Documentation deployment is generated but disabled by default. Local documentation commands and pull-request validation do not require GitHub Pages.
Enable deployment
Install GitHub CLI separately, authenticate an identity with repository administration access, and run from a clone whose GitHub repository can be resolved:
gh auth login
mise run docs:deployment:enable
The task checks gh, authentication, and repository resolution. It creates or updates Pages with the GitHub Actions
build type, then sets DOCS_DEPLOYMENT_ENABLED=true as the last mutation and prints the Pages URL. Repeating the task
updates the same configuration safely.
Deployment requires both Pages build_type=workflow and the repository variable to be exactly true. The workflow runs
only for pushes to the configured default branch and explicit manual dispatches; pull requests never deploy.
Recovery
A failed task names the failed GitHub operation and does not report success. Install gh from https://cli.github.com/
if necessary. Complete the same state manually:
gh api --method PUT repos/OWNER/REPO/pages -f build_type=workflow
gh variable set DOCS_DEPLOYMENT_ENABLED --body true --repo OWNER/REPO
gh api repos/OWNER/REPO/pages --jq .html_url
Use POST instead of PUT when the repository does not have an existing Pages site.
If URL lookup fails after the variable is set, the repository may already be enabled; verify both settings before rerunning or dispatching the Documentation workflow.
Tooling reference
Files
| File | Contract |
|---|---|
mise.toml | Maison development tools, environment, and task discovery. |
mise.lock | Maison development tool downloads; commit it. |
hk.pkl | Shared check/fix/pre-commit step map. |
.config/rumdl.toml | Markdown lint configuration. |
contextlint.config.json | Documentation links, anchors, and image targets. |
scripts/setup-tooling.py | Resolves the locked toolchain, installs tools and hooks, and reports status. |
schemas/inventory.toml | Public inventory schema consumed by Python and Nix validators. |
docs/src/reference/flake.md | Public flake outputs, modules, helpers, and checks. |
docs/src/reference/consumer.md | Consumer files, root selection, composition, and mutation boundaries. |
.github/workflows/ci.yml | Repository checks, platform builds, and bootstrap checks. |
Tools
The universal tool set includes hk 1.49.0, Cocogitto, Harper CLI, Contextlint, mdBook, uv, rumdl, typos, and the
Markdown table formatter. The committed lock targets linux-x64, linux-arm64, and macos-arm64; Intel macOS and
Windows are outside this POSIX-shell task contract.
Maison uses native hk behavior whenever it matches the required formatter or linter. Tests, compilers, module checks, and Nix commands remain gated by their project manifests. No recognized language profile is active; only the universal baseline is configured.
Consumer boundary
Maison owns its contributor toolchain. A consumer owns flake.nix, flake.lock, inventory.toml, host overrides,
config/mise/, dotfiles, and deployment state. Run consumer commands from that checkout or set an explicit root:
MAISON_HOME="$HOME/.maison" \
MAISON_CONSUMER_ROOT="$HOME/src/terroir" \
maison plan
Consumer operations use the consumer flake and lock directly. They do not use Maison’s lock as a fallback, do not pass a
a synthetic alternate configuration root, and do not write Maison files. For fresh setup, bootstrap.sh --setup PATH
renders the retained overlay_template/ Copier starter once; Copier is not part of normal runtime operations.
Tasks
| Task | Behavior |
|---|---|
check | Install Maison tools, then run data, shell, Python, and Nix checks. |
consumer:validate | Validate a consumer without activation or provider credentials. |
self:update | Upgrade Maison from the selected consumer’s pinned input. |
check:tests | Run bounded Python unittest discovery across tests/test_*.py. |
fix | Apply deterministic hk fixes to the working tree. |
docs:check | Build and validate documentation structure. |
docs:build | Build the mdBook site. |
docs:serve | Serve mdBook on port 3000 or a supplied port. |
consumer:validate is the mise task behind maison consumer validate; it owns the framework-side contract so a
consumer does not need a duplicate validation suite. self:update is the transactional framework upgrade path: it
updates the consumer’s Maison input, validates with the candidate CLI, and rolls back the consumer lock and local CLI
state together when any step fails. Inventory fixtures live under tests/fixtures/inventory/ and are
consumed by Python tests and Nix evaluation checks. Add fixtures when changing schema fields, defaults, profile
compatibility, deployment path rules, or host override layout.
Python tests are partitioned by inventory, consumer repository, deployment/bootstrap, migration, ownership, repository
mutation, transaction, configuration editing, deploy transaction, and user convergence behavior. External processes use
tests/support/processes.py, which supplies process-group cleanup, bounded diagnostics, and a 30-second default timeout.
The mise environment routes hooks through mise with HK_MISE=1 and sets GIT_CONFIG_PARAMETERS="'merge.ff=only'", so
Git rejects merges that require a merge commit.
Documentation and releases
contextlint validates documentation links and anchors. The cache-refresh workflow may update Maison’s own flake.lock,
warm cache targets, and open a review PR; it must not merge or bypass branch protection. The project .copier-answers.yml
records the dstack scaffold provenance for Maison itself; it is not a consumer configuration mechanism.
Pages requires build_type=workflow plus DOCS_DEPLOYMENT_ENABLED=true. The build job has contents: read; only the
deploy job has pages: write and id-token: write.
Public flake reference
Maison is a provider-neutral flake. A consumer pins Maison as an input and keeps its own inventory, host topology,
configuration, deployment state, and lock file. Maison’s flake.lock contains only inputs needed to develop, validate,
and release Maison itself.
Run the CLI
The packaged CLI does not require a Maison checkout:
nix run github:RobertDeRose/maison#maison -- --help
The same package is available as inputs.maison.packages.${system}.maison, and the app is available as
inputs.maison.apps.${system}.maison.
Public outputs
| Output | Contract |
|---|---|
packages.<system>.maison | Store-backed Maison CLI package |
apps.<system>.maison | Runnable app wrapper for the CLI package |
darwinModules | Reusable nix-darwin modules, including default, base, config, fonts, and system |
systemManagerModules | Reusable system-manager modules, including default and system |
lib | Maison orchestration functions and inventory validation helpers |
orchestration | Alias for the Maison orchestration library |
schemas.inventory | Public inventory schema path |
schemas.fnox | Provider-neutral fnox contract schema |
fixtures.inventory | Neutral valid and invalid inventory fixture corpus |
fixtures.fnox | Credential-free fnox contract fixture corpus |
The lib output also appears under lib.maison for consumers that prefer a named namespace. Its stable helpers are:
validateInventory inventory— normalize and validate a consumer inventory;mkDarwinSystem— compose a consumer-owned nix-darwin host;mkSystemManagerSystem— compose a consumer-owned system-manager host;mkDeployments— compose deployment definitions from consumer-owned data;profilesand the validation predicates — shared framework contracts;validateFnox config— validate logical fnox metadata without resolving secret values.
The host composition helpers receive consumer inputs, a consumer host record, and the consumer host-root path. Maison never supplies a personal user, host, deployment target, provider credential, or topology through these outputs.
Modules
A consumer selects modules in its own flake and supplies its own special arguments:
{
inputs,
...
}:
{
darwinConfigurations.example = inputs.darwin.lib.darwinSystem {
system = "aarch64-darwin";
specialArgs = {
user = {
username = "operator";
fullName = "Example Operator";
};
host = {
features.personalCache = false;
};
};
modules = [ inputs.maison.darwinModules.default ];
};
}
System-manager consumers use inputs.maison.systemManagerModules.default in the same way. The module paths are
framework building blocks; consumer repositories own the composition and all identity-bearing values.
Checks and fixtures
Run the public contract and platform checks without changing Maison’s lock file:
nix flake check github:RobertDeRose/maison --no-update-lock-file
Each supported system exposes checks.<system>.public for the packaged CLI, module paths, schemas, and fixture corpora;
the remaining checks cover inventory and platform-specific framework behavior.
The neutral inventory and fnox fixture corpora are intentionally credential-free. Consumers and CI can reuse them to verify framework handling without requiring an owner account or a particular provider.
Consumer repository reference
A consumer repository is the execution, configuration, and lock root for Maison. It owns the files that describe one installation; Maison supplies reusable orchestration and framework code.
Required files
| Path | Owner | Purpose |
|---|---|---|
flake.nix | Consumer | Host outputs and the Maison flake input |
flake.lock | Consumer | Pinned inputs for that consumer |
inventory.toml | Consumer | Users, hosts, profiles, and deployment targets |
README.md | Consumer | Maison relationship and consumer usage |
A consumer may also own hosts/, config/mise/, dotfiles/, and any host-specific Nix modules. The inventory and configuration paths are interpreted relative to the consumer root. If the consumer declares
runtime secrets, its root fnox.toml is validated under the fnox contract.
Fresh setup
The maintained overlay_template/ directory is a Copier starter for fresh consumers; its historical overlay name does
not change the ownership model. Use the verified bootstrap script to render and initialize one:
./bootstrap.sh --setup "$HOME/src/terroir"
Bootstrap registers the current host through Maison’s validated host:add task, creates the consumer flake.lock, and
stops before activation. Review the generated files, create the consumer’s first Git commit, and rerun bootstrap with
--consumer. Copier is not required for normal runtime commands, and Maison does not save or manage the generated
consumer path.
Root selection
Commands use the first available value:
MAISON_CONSUMER_ROOT;MAISON_REPOSITORYorMAISON_REPO;- the Git checkout containing the current working directory, when it is not the Maison installation.
The selected path must contain regular files named flake.nix, flake.lock, and inventory.toml, and must be a separate
checkout rather than Maison or a path nested inside it. Packaged or explicitly configured invocations reject Maison’s own
checkout as the consumer. Use an absolute path in automation:
MAISON_HOME="$HOME/.maison" \
MAISON_CONSUMER_ROOT="$HOME/src/terroir" \
maison plan --host laptop
Flake composition
Consumers pin Maison and compose their own outputs. Maison’s public lib, modules, schema, and fixtures are reusable
inputs; Maison does not supply personal identity, topology, deployment endpoints, or a consumer lock.
The framework CLI targets consumer installables such as darwinConfigurations.<host> and
systemConfigs.<host>. A consumer may expose additional aliases, but it must keep those outputs in its own flake.
Maison validation
Run the read-only contract before activation or after changing the consumer lock:
maison consumer validate --consumer /path/to/consumer
The validator checks the Maison lock input, inventory and supported systems, Nix host outputs, mise package and dotfile declarations, documentation links, fnox references, and raw credential/private-key boundaries. It runs check-only Nix evaluation, never activates a system or updates a lock file, and does not invoke fnox providers or require their credentials. Provider selection remains consumer-owned.
Mutation boundaries
plan is read-only with respect to the consumer repository. apply, update, host authoring, software authoring,
deployment, and recovery operate on the selected consumer. maison update replaces only the consumer’s flake.lock;
Maison’s lock is never an implicit fallback. Consumer Git operations remain outside Maison.
maison self update is the focused framework upgrade. It updates only the Maison input, builds the candidate CLI from
the resulting consumer lock, runs maison consumer validate through that candidate, and records its executable in
owner-only local CLI state. A failed update restores both the prior consumer lock and prior CLI state. The state file is
under ${XDG_STATE_HOME:-$HOME/.local/state}/maison/cli unless MAISON_CLI_STATE_FILE is set; it is local runtime
state and is not committed to either repository.
fnox reference
Maison provides a provider-neutral boundary for consumer-owned confidential values. A consumer may choose any fnox provider; Maison validates only logical names and the runtime safety policy. Maison does not select or authenticate an age, Bitwarden, cloud, keychain, or other provider.
Consumer contract
Place a consumer-owned fnox.toml at the repository root when runtime secrets are needed:
root = true
if_missing = "error"
env = "exec"
[providers.consumer_store]
type = "consumer-selected-provider"
endpoint = "https://secrets.example.invalid"
[secrets.GITHUB_TOKEN]
provider = "consumer_store"
key = "source-controlled-logical-reference"
env = "exec"
if_missing = "error"
The contract requires:
root = true, so parent and global project configuration cannot silently change the consumer contract;if_missing = "error", so required values fail before a Maison mutation starts;env = "exec", so values are not injected into an ambient interactive shell;- provider names and provider
typevalues remain opaque and consumer-selected; - secret names are shell-compatible logical names, while
providerandkeyare references, not credential values; and - inline defaults, provider credentials, and private-key fields are rejected by Maison’s validator.
fnox.local.toml may hold owner-local overrides, but it must be owner-only (0600 or stricter). Keep it out of Git.
The tracked fnox.toml should contain only provider metadata, encrypted material supported by fnox, and logical
references.
Maison publishes the contract as inputs.maison.schemas.fnox, neutral fixtures as inputs.maison.fixtures.fnox, and
pure evaluation-time validation as inputs.maison.lib.validateFnox. The validator returns logical metadata only; it
never returns a secret value for use in a derivation.
Runtime preflight
When fnox.toml exists, Maison validates it and runs the equivalent of:
FNOX_IF_MISSING=error FNOX_SHELL_OUTPUT=none fnox -c "$CONSUMER_ROOT/fnox.toml" check
This read-only preflight runs before plan, apply, update, deployment, and recovery. A failed check stops the operation
before repository, system, or user mutation and reports the logical names that need resolution without printing their
values. Install fnox through the consumer’s toolchain; Maison does not install or configure a provider on the
consumer’s behalf.
Before invoking fnox, Maison copies the declarative configuration into a temporary owner-only directory (0700) and
uses owner-only files (0600); the directory is removed after the command. Provider credentials and private keys are
fetched only by fnox during this runtime preflight or an activation command. They are passed through
memory/environment boundaries, never command arguments or logs, and never embedded in Nix expressions, derivations, or
the Nix store.
Inventory reference
Maison inventory is TOML with schema = 1. The public schema contract lives at schemas/inventory.toml; Python and
Nix validators load that same file for supported systems, profile names, feature keys, deploy keys, and defaults.
Maison ships only a neutral starter inventory. Real users, host names, deployment endpoints, and host overrides belong
in the consumer repository’s inventory.toml and hosts/ tree. Inventory may contain non-secret topology, but passwords,
tokens, SSH private keys, signing private keys, and other secrets remain in the consumer’s fnox-selected provider.
Top-level tables
| Field | Required | Contract |
|---|---|---|
schema | Yes | Must equal 1. |
[defaults] user | Optional | Default inventory user key for hosts that omit user. |
[users.<key>] | Yes | Declares managed user identities. |
[hosts.<name>] | Yes | Declares host system, user, profiles, optional features, and deployment fields. |
Users
| Field | Default | Contract |
|---|---|---|
username | User key | Portable lowercase account name; root is not allowed as a managed user. |
full_name | None | Required non-empty display name. |
email | None | Required non-empty email string. |
github | None | Required GitHub username. |
allow_nonportable | false | Allows an existing non-portable username only on Darwin hosts. |
Hosts
| Field | Contract |
|---|---|
| Host table name | One DNS label. |
system | aarch64-darwin, aarch64-linux, or x86_64-linux. |
user | Existing user key, or [defaults].user. |
profiles | Non-empty list from base, dev, mac, and linux; duplicates are rejected. |
The mac profile is Darwin-only. The linux profile is Linux-only.
Deployment
[hosts.<name>.deploy] accepts only keys declared by the schema.
| Field | Default | Contract |
|---|---|---|
enable | false | Boolean; deploy-rs is Linux-only when enabled. |
hostname | Host table name | Remote host name. |
ssh_user | maison-deploy | Deployment account; may be root for bootstrap but not the managed user. |
user_ssh_user | Managed username | Must equal the managed inventory username. |
repo_path | /home/<managed-user>/.maison | Normalized descendant of /home/<managed-user>, not the home directory itself. |
remote_build | false | Boolean deploy-rs option. |
auto_rollback | true | Boolean deploy-rs rollback option. |
magic_rollback | true | Boolean deploy-rs rollback option. |
Consumer host overrides
Host override directories must correspond to consumer inventory hosts and may contain only system.nix. User-level
differences belong in consumer mise configuration or dotfiles, not host override modules. The consumer inventory replaces
Maison’s neutral starter inventory; it is not merged with it.
Fixture contract
Inventory fixtures live under tests/fixtures/inventory/ and are consumed by both Python tests and Nix evaluation checks.
The parity tests compare normalized deployment defaults, accepted fields, types, profiles, feature keys, deploy keys, and
safe repository path rules.
Repository-boundary refactor report
Maison now implements one public framework and consumer-root model. The public checkout supplies runtime code and validation; the selected consumer supplies all installation-specific state.
Delivered boundaries
- Maison’s public flake exports the CLI, reusable system modules, orchestration helpers, schemas, and neutral fixtures.
- A consumer owns its flake, lock, inventory, host overrides, mise configuration, dotfiles, deployment state, and Git history.
- Nix/Lix owns privileged system state. mise owns consumer user state. fnox providers materialize confidential values only at the owner-controlled runtime boundary.
maison planand validation are read-only. Activation and authoring operate on the selected consumer only.maison self updatetransactionally updates only the consumer’s Maison input and local owner-only CLI state.- Remote deployment stages committed consumer content and keeps rollback state root-owned and separate from user state.
Retired architecture
The old private-overlay runtime, saved alternate configuration root, and repository publish/status/sync commands are
not supported. Maison does retain overlay_template/ as a Copier setup-time starter: bootstrap.sh --setup PATH
renders a fresh consumer, registers its first host, and pins its consumer lock. The template does not create saved
runtime state or a second framework authoring path. Consumer Git history remains explicitly managed by the consumer
operator.
The historical split sources remain private and archived as terroir.original and nix-config. Terroir is the fresh
consumer destination and Maison is the fresh public framework history. Migration evidence and recovery requirements are
recorded in the migration contract.
Validation
The repository checks cover data, shell, Python, Nix, documentation, consumer contracts, provider-neutral fnox fixtures, transaction rollback, and the public flake. Validation does not require personal fnox credentials and does not activate a system.
Planned features
This page is the human-readable roadmap. Beads is authoritative for live status, dependencies, claims, and ready-work selection.
Project direction
- Purpose: Maison is a two-layer macOS and Linux configuration system that uses Nix/Lix for privileged system state and mise for user tools, packages, applications, preferences, and dotfiles.
- Current scope: Maison manages Apple Silicon macOS, aarch64 Linux, and x86_64 Linux host configuration; local system activation; remote Linux deployment; user-environment convergence; inventory validation; package/tool/app authoring commands; recovery; and project documentation.
- Boundaries: Maison does not support Intel macOS, Home Manager, arbitrary unmanaged package ownership, strict offline byte-for-byte reproduction, or storing private infrastructure identity and trusted access material in the public control plane.
Roadmap conventions
- Directory names use
<slug>. - Detailed intent belongs in each feature’s
design.md. - Each feature is one Beads epic/molecule; lifecycle and implementation work are tasks beneath it.
- Human workflow references use
<slug>or the feature name. Root hashes are retained only for audit. - Live execution state is queried through Beads.
- Completed features move into Implemented features.
Feature map
| Slug | Feature | Beads root | Roadmap state | Dependencies | Design |
|---|---|---|---|---|---|
maison-001-root-owned-deployment-transactions | MAISON-001: Root-owned deployment transaction state | maison-mol-6y0 | Delivered | None | record |
maison-002-revision-bound-commit-rollback | MAISON-002: Revision-bound commit and rollback | maison-mol-quq | Delivered | maison-001-root-owned-deployment-transactions | record |
maison-003-restricted-deployment-privilege | MAISON-003: Restricted deployment privilege model | maison-mol-4v3 | Delivered | maison-002-revision-bound-commit-rollback | record |
maison-004-private-overlay-configuration | MAISON-004: Private configuration split (superseded) | maison-mol-e9t | Retired | maison-003-restricted-deployment-privilege | record |
maison-005-verified-bootstrap-artifacts | MAISON-005: Verified bootstrap artifacts | maison-mol-pa6 | Delivered | maison-004-private-overlay-configuration | record |
maison-006-review-gated-dependency-updates | MAISON-006: Review-gated dependency updates | maison-mol-aqu | Delivered | maison-005-verified-bootstrap-artifacts | record |
maison-007-parser-backed-toml-mutations | MAISON-007: Parser-backed TOML mutations | maison-mol-ywg | Delivered | maison-006-review-gated-dependency-updates | record |
maison-008-repository-mutation-locking | MAISON-008: Repository mutation locking and journals | maison-mol-4ev | Delivered | maison-007-parser-backed-toml-mutations | record |
maison-009-authoring-checkout-guard | MAISON-009: Authoring checkout guard | maison-mol-74l | Delivered | maison-008-repository-mutation-locking | record |
maison-010-shared-inventory-schema | MAISON-010: Shared inventory schema validation | maison-mol-bvr | Delivered | maison-009-authoring-checkout-guard | record |
maison-011-deterministic-test-suite | MAISON-011: Bounded deterministic test suite | maison-mol-8mr | Delivered | maison-010-shared-inventory-schema | record |
maison-012-plan-apply-parity | MAISON-012: Plan/apply semantic parity | maison-mol-6w9 | Delivered | maison-011-deterministic-test-suite | record |
maison-013-dotfile-backup-manifests | MAISON-013: Exact dotfile backup manifests | maison-mol-h8e | Delivered | maison-012-plan-apply-parity | record |
maison-014-remote-convergence-restoration | MAISON-014: Remote convergence restoration | maison-mol-vhhy | Delivered | maison-013-dotfile-backup-manifests | record |
maison-015-linux-runtime-verification | MAISON-015: Linux runtime activation verification | maison-mol-cg3x | Delivered | maison-014-remote-convergence-restoration | record |
maison-016-pi-typescript-validation | MAISON-016: Pi TypeScript validation boundary | maison-mol-fmud | Delivered | maison-015-linux-runtime-verification | record |
maison-017-maison-terroir-repository-split | MAISON-017: Maison/Terroir repository split | maison-mol-jfhs | Delivered | maison-016-pi-typescript-validation | record |
maison-overlay-copier-bootstrap | Copier fresh consumer bootstrap | maison-mol-5s9 | Delivered | maison-017-maison-terroir-repository-split | record |
maison-overlay-authoring-lifecycle | Retired alternate repository command surface | maison-mol-3jb | Retired | maison-overlay-copier-bootstrap | record |
The alternate repository command surface remains retired. The supported architecture is the public Maison framework plus one selected consumer repository; the retained Copier starter is setup-time only, and consumer Git history is managed explicitly with Git.
Later P2 maintainability follow-ups
The review also identified lower-priority maintainability and contract corrections. They remain source material for
future planning after MAISON-001 through MAISON-016 are reconciled: update command help and rollback guarantees,
user-finalization failures, centralized SSH policy, application-backup concurrency, non-TTY logging races, safer
system clean, contradictory macOS defaults, shared repository helpers, expanded doctor/user status, and
shebang-specific shell validation.
Design — MAISON-001: Root-owned deployment transaction state
Metadata
- Beads feature root:
maison-mol-6y0 - Feature slug:
maison-001-root-owned-deployment-transactions - Design path:
docs/src/features/maison-001-root-owned-deployment-transactions/design.md - Implemented record:
docs/src/features/maison-001-root-owned-deployment-transactions/index.md - Base branch:
dev - Status: delivered
- Review priority:
P0
Feature Summary
Move remote repository transaction journals, staging repositories, and rollback repositories out of the managed user’s control and into a root-owned same-filesystem transaction area.
User Intent
The review found that privileged deployment state and rollback data are stored beneath a directory controlled by the managed user. A managed-user compromise can unlink, replace, or interfere with transaction state while privileged finalization is occurring.
Goals
- Root owns deployment transaction state and journals.
- Staging, rollback, and journal paths live on the same filesystem as the destination.
- Filesystem operations reject symlinks, unexpected ownership, traversal, and cross-device replacement.
- Transactions use unpredictable IDs and a lock that serializes remote repository finalization.
Non-Goals
- Changing deploy-rs system profile rollback behavior.
- Changing the private overlay split beyond documentation of path ownership.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Create a root-owned transaction directory outside
/home/<managed-user>or any managed-user-writable ancestor while remaining on the destination filesystem. - Use unpredictable transaction IDs, explicit journals, and a single remote transaction lock.
- Validate ownership and permissions of every transaction, staging, rollback, and destination path before mutation.
- Use no-follow filesystem operations for all privileged boundary checks.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Replace <repo>.next.<pid>, <repo>.previous, and <repo>.deploy-state with a root-owned transaction manager implemented in tested Python stdlib code. Mise task files may invoke that Python task implementation directly.
The transaction manager uses a root-owned transaction root on the same filesystem as the destination repository. For the default /home/<managed-user>/.maison repository path, the default transaction root is a root-owned sibling namespace under /home, not under /home/<managed-user>, for example /home/.maison-deploy/transactions/<managed-user>/<repo-hash>/. If no same-filesystem transaction root exists outside a managed-user-writable ancestor, deployment fails with an actionable error rather than falling back to managed-user-controlled state. A later private overlay may configure an equivalent root-owned transaction root, but the configured path must pass the same same-filesystem, ownership, permission, and no-follow validation.
Each transaction allocates an unpredictable ID beneath that transaction root, creates staging and rollback directories there, writes an explicit journal before each irreversible step, and validates path ownership with directory-file-descriptor based no-follow checks where available. The command surface is limited to stage, finalize, abort, and inspect-incomplete. Revision-bound startup recovery and non-destructive rollback semantics are owned by MAISON-002; this feature only establishes the root-owned state boundary and journal foundation.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| README.md | Update | Replace managed-user-controlled transaction path claims with the root-owned transaction-root contract | transaction-contract, transaction-engine |
| docs/deployment.md | Update | Document transaction-root placement, validation, journals, locks, staging, and finalization command behavior | transaction-contract, transaction-engine |
| docs/recovery.md | Update | Document how to inspect incomplete root-owned transaction journals and where rollback state lives | transaction-contract, transaction-engine |
| docs/architecture.md | Update | Record the privileged repository transaction boundary and ownership invariant | transaction-contract |
| docs/task-reference.md | Update | Summarize the deploy task’s root-owned repository transaction behavior | transaction-engine |
docs/src/features/maison-001-root-owned-deployment-transactions/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Focused fault-injection tests for managed-user tampering with staging, journal, lock, and rollback paths.
- Behavioral deployment transaction tests covering ownership, no-follow rejection, same-filesystem enforcement, and journal writes.
mise -E dev run checkafter implementation stabilizes.uv run scripts/check-docs.py.
Implementation Decomposition
maison-001-root-owned-deployment-transactions transaction-contract— Add root-owned transaction contract, documentation contract, and tamper tests. This task owns the failing tests and reader-facing contract for transaction-root placement, same-filesystem enforcement, no-follow rejection, ownership validation, journals, and locks.maison-001-root-owned-deployment-transactions transaction-engine— Implement root-owned transaction staging and journals after the contract task. This task owns the production implementation and updates task-reference/deployment details that depend on the implemented command surface.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The
transaction-engine task depends on transaction-contract so tests and documentation contract land before production
implementation. No implementation task may run until spec-reconcile closes.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-002: Revision-bound commit and rollback
Metadata
- Beads feature root:
maison-mol-quq - Feature slug:
maison-002-revision-bound-commit-rollback - Design path:
docs/src/features/maison-002-revision-bound-commit-rollback/design.md - Implemented record:
docs/src/features/maison-002-revision-bound-commit-rollback/index.md - Base branch:
dev - Status: draft
- Review priority:
P0
Feature Summary
Make remote repository commit and rollback revision-bound, recoverable, non-destructive, and startup-recoverable.
User Intent
The rollback implementation can remove the active repository before proving that restoration will succeed. Finalization also lacks expected old/new revision checks.
Goals
- Record and verify expected old and new Maison revisions.
- Never delete the active repository before a recoverable replacement exists.
- Provide rescue renames, filesystem synchronization, and startup recovery.
- Fault-injection tests cover interruption at every finalization step.
Non-Goals
- Changing package-manager rollback semantics.
- Replacing deploy-rs rollback.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Journal expected old revision, candidate new revision, destination path, and recovery action before finalization.
- Verify active old revision before replacement and active new revision after replacement.
- Preserve at least one recoverable prior repository until success is proven and synchronized.
- Recover or stop safely when a previous transaction journal is found at startup.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Extend the transaction manager from MAISON-001 with revision-bound state transitions. Finalization validates the active old revision, validates the staged new revision, performs atomic rescue renames without deleting the active repository first, fsyncs parent directories and journal updates where supported, then records completion. Startup recovery reads incomplete journals and either completes a proven-safe transition or restores the previous repository before user convergence runs.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/deployment.md | Update | Align reader-facing contract with MAISON-002: Revision-bound commit and rollback | Implementation tasks |
| docs/recovery.md | Update | Align reader-facing contract with MAISON-002: Revision-bound commit and rollback | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-002: Revision-bound commit and rollback | Implementation tasks |
| docs/task-reference.md | Update | Align reader-facing contract with MAISON-002: Revision-bound commit and rollback | Implementation tasks |
docs/src/features/maison-002-revision-bound-commit-rollback/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Fault-injection tests for interruption before, during, and after active repository replacement.
- Tests for old-revision mismatch, new-revision mismatch, missing staged repository, missing previous repository, and startup recovery.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-002-revision-bound-commit-rollback revision-contract— Add revision-bound finalization and recovery tests.maison-002-revision-bound-commit-rollback revision-engine— Implement non-destructive revision-bound finalization.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. Sibling implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-003: Restricted deployment privilege model
Metadata
- Beads feature root:
maison-mol-4v3 - Feature slug:
maison-003-restricted-deployment-privilege - Design path:
docs/src/features/maison-003-restricted-deployment-privilege/design.md - Implemented record:
docs/src/features/maison-003-restricted-deployment-privilege/index.md - Base branch:
dev - Status: draft
- Review priority:
P0 - Review status: reconciled in /start-feature (documentation and design reviews complete)
Feature Summary
Replace unrestricted passwordless sudo and direct root deployment defaults with a dedicated maison-deploy account and narrowly scoped privileged commands.
User Intent
The current Linux model combines remotely sourced user keys with NOPASSWD: ALL, making a user-key compromise a root compromise.
Goals
- Use
maison-deployas the default privileged deployment account. - Separate deployment identity from the managed user.
- Restrict privileged commands to reviewed Maison deployment helpers and deploy-rs activation requirements.
- Remove
NOPASSWD: ALLfrom supported defaults.
Non-Goals
- Supporting arbitrary third-party deployment privilege brokers.
- Weakening deploy-rs profile activation checks.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Inventory and Nix validation model
maison-deployseparately from the managed user. - Sudoers grants are command-scoped and argument-bounded for Maison privileged helpers.
- Direct root deployment is not the default path.
- Docs explain account roles, trust boundaries, and recovery.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Introduce a Linux deployment account named maison-deploy. System-manager config creates or validates that account, installs reviewed authorized keys from the private overlay, and grants only the commands needed for deploy-rs activation plus Maison transaction finalization and recovery. User convergence still runs as the managed user. The deployment adapter connects through the deployment account by default and refuses inventory that grants unmanaged NOPASSWD: ALL.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Align reader-facing contract with MAISON-003: Restricted deployment privilege model | Implementation tasks |
| docs/deployment.md | Update | Align reader-facing contract with MAISON-003: Restricted deployment privilege model | Implementation tasks |
| docs/recovery.md | Update | Align reader-facing contract with MAISON-003: Restricted deployment privilege model | Implementation tasks |
| docs/task-reference.md | Update | Align reader-facing contract with MAISON-003: Restricted deployment privilege model | Implementation tasks |
| docs/add-a-host.md | Update | Align reader-facing contract with MAISON-003: Restricted deployment privilege model | Implementation tasks |
docs/src/features/maison-003-restricted-deployment-privilege/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Nix and Python inventory tests for separate deployment and managed users.
- Text and behavioral tests rejecting
NOPASSWD: ALLdefaults and direct root deployment defaults. - Deployment adapter tests proving privileged command allowlist construction.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-003-restricted-deployment-privilege privilege-contract— Add deployment-account and sudoers contract tests.maison-003-restricted-deployment-privilege privilege-implementation— Implementmaison-deployrestricted privilege model.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. Sibling implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-004: Private overlay configuration split
Metadata
- Beads feature root:
maison-mol-e9t - Feature slug:
maison-004-private-overlay-configuration - Design path:
docs/src/features/maison-004-private-overlay-configuration/design.md - Implemented record:
docs/src/features/maison-004-private-overlay-configuration/index.md - Base branch:
dev - Status: delivered
- Review priority:
P0
Feature Summary
Split reusable Maison functionality from personal/site-specific configuration by making a tracked private overlay repository the canonical source for sensitive infrastructure identity and local configuration.
User Intent
The public control plane must not contain mutable keys or real infrastructure metadata, but Maison must remain durable and easy to replicate. The user described Maison as the vehicle and the private repo as the driver.
Goals
- Maison remains generic and public-safe.
- A tracked private overlay repo owns real applications, hosts, usernames, emails, deploy targets, dotfiles, preferences, and trusted key material.
- Fresh setup accepts or prompts for the overlay source after Maison bootstraps itself.
- Examples in Maison remain real workable starter configurations without personal data.
Non-Goals
- Using an untracked-only local inventory as the durable source of truth.
- Using mise secrets as the primary schema for host topology.
- Storing credentials in public Maison.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Support
--overlay <git-url-or-path>for bootstrap/setup and prompt only in an interactive terminal. - Non-interactive setup fails clearly when required overlay input is missing.
- Store selected overlay source in local untracked Maison state and clone or update it into a standard path.
- Validate public Maison examples and private overlays with the same typed schema.
- Mise secrets may provide credentials, tokens, secret values, or the overlay location; typed TOML remains canonical for structured topology and trusted keys.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Introduce overlay discovery and loading across Python and Nix. Public Maison keeps schemas, validators, task machinery, templates, examples, and non-personal defaults. A private overlay repo supplies site configuration through typed TOML and optional dotfile trees. Bootstrap accepts --overlay, stores the selected source in local state, clones or updates the overlay, and passes overlay paths into validation, Nix evaluation, and mise convergence. Examples remain executable starter configurations with placeholder identities and safe sample hosts.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| README.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/architecture.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/deployment.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/add-a-host.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/add-a-tool.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/add-an-app.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/package-policy.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/task-reference.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
| docs/recovery.md | Update | Align reader-facing contract with MAISON-004: Private overlay configuration split | Implementation tasks |
docs/src/features/maison-004-private-overlay-configuration/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Schema tests for public examples and private overlay fixtures.
- Bootstrap tests for
--overlay, interactive prompt gating, non-interactive failure, local state writes, clone/update behavior, and missing overlay recovery. - Privacy tests rejecting personal hostnames, usernames, emails, deploy targets, mutable GitHub key URLs, and trusted keys in public Maison defaults.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-004-private-overlay-configuration overlay-contract— Add overlay schema, privacy, and bootstrap contract tests.maison-004-private-overlay-configuration overlay-loader— Implement private overlay discovery and loading.maison-004-private-overlay-configuration overlay-docs-examples— Rework docs and examples around public Maison plus private overlay.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. Sibling implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-005: Verified bootstrap artifacts
Metadata
- Beads feature root:
maison-mol-pa6 - Feature slug:
maison-005-verified-bootstrap-artifacts - Design path:
docs/src/features/maison-005-verified-bootstrap-artifacts/design.md - Implemented record:
docs/src/features/maison-005-verified-bootstrap-artifacts/index.md - Base branch:
dev - Status: delivered
- Review priority:
P0
Feature Summary
Eliminate unverified remote code execution during bootstrap by using pinned release artifacts with checksum or signature verification and immutable plugin revisions.
User Intent
The review identified curl ... | sh bootstrap paths for mise and Lix and mutable plugin repositories as unverified remote code execution.
Goals
- No bootstrap path executes a downloaded script before verification.
- mise and Lix installers are pinned to reviewed versions and checksums or signatures.
- Mutable plugin repositories are pinned to immutable revisions.
- Docs provide a safe fresh-install path.
Non-Goals
- Strict offline installation for every package manager.
- Replacing Nix binary cache trust policy.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Bootstrap downloads artifacts to disk, verifies checksums or signatures, then executes verified local files only.
- Pinned version and verification data are checked into Maison or provided by the private overlay where site-specific.
- Tests fail on pipe-to-shell bootstrap patterns and mutable plugin refs.
- Operator docs include verification failure recovery.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Refactor every Maison-owned bootstrap executor into explicit fetch, verify, and execute stages. The covered surfaces are the top-level bootstrap.sh, the shared .mise/lib/bootstrap.sh Nix/Lix installer path, and the remote deployment fallback that installs mise before user convergence. Each path downloads artifacts to a temporary file, verifies checked-in public metadata, and executes only the verified local file.
Store Maison-owned artifact metadata in a checked-in manifest that names the reviewed version, source URL, expected checksum or upstream signature material, supported systems, and recovery hint. Use one verification mechanism per artifact unless upstream release practices require otherwise; prefer checksums for static release binaries and signature verification only when the upstream artifact is designed for it. Private overlay metadata is allowed only for site-specific artifact mirrors or overrides, not as the default public trust root.
Pin runtime/plugin inputs that are needed during bootstrap or Maison command execution to immutable versions or revisions. Source-text guard tests cover bootstrap scripts, deployment bootstrap fallback scripts, public bootstrap documentation examples, and Maison runtime plugin/tool declarations. This does not change the broader workstation package policy that permits latest for ordinary user tools when lockfiles capture resolved artifacts.
The public command examples stop recommending unverified pipe-to-shell execution. Fresh-install documentation either uses a reviewed local checkout path or a download-then-verify bootstrap path.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| README.md | Update | Align reader-facing contract with MAISON-005: Verified bootstrap artifacts | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-005: Verified bootstrap artifacts | Implementation tasks |
| docs/deployment.md | Update | Document remote deployment bootstrap fallback verification | Implementation tasks |
| docs/recovery.md | Update | Align reader-facing contract with MAISON-005: Verified bootstrap artifacts | Implementation tasks |
| docs/task-reference.md | Update | Align reader-facing contract with MAISON-005: Verified bootstrap artifacts | Implementation tasks |
docs/src/features/maison-005-verified-bootstrap-artifacts/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Source-text guard tests banning
curl ... | shand mutable runtime/plugin refs in local bootstrap, remote deployment bootstrap fallback, and public bootstrap documentation examples. - Manifest schema tests for required version, URL, verification material, supported-system metadata, and recovery hints.
- Behavioral tests for successful verification, checksum/signature mismatch, missing metadata, and mismatched supported-system selection.
- Tests proving ordinary user-tool
latestpolicy remains scoped to non-bootstrap workstation convergence when lockfiles capture resolved artifacts. mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-005-verified-bootstrap-artifacts bootstrap-contract— Add verified-bootstrap manifest, local/remote bootstrap, documentation-example, and immutable-runtime-plugin tests.maison-005-verified-bootstrap-artifacts bootstrap-implementation— Implement verified artifact bootstrap across local bootstrap, shared Lix setup, remote deployment fallback, runtime/plugin pinning, and reader docs.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The contract task blocks implementation so tests and guardrails land first. Implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page; the planned implementation task touches the same bootstrap surfaces as the contract and therefore must run after it.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Design Changes During Specification Review
- Expanded scope from local bootstrap only to every Maison-owned bootstrap executor, including remote deployment’s mise fallback.
- Clarified that public checked-in metadata is the default trust root for Maison-owned bootstrap artifacts; private overlay metadata is limited to site-specific mirrors or overrides.
- Clarified that immutable plugin/runtime checks target bootstrap and Maison runtime inputs, not ordinary non-bootstrap
workstation tools that intentionally use
latestwith generated lockfiles.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-006: Review-gated dependency updates
Metadata
- Beads feature root:
maison-mol-aqu - Feature slug:
maison-006-review-gated-dependency-updates - Design path:
docs/src/features/maison-006-review-gated-dependency-updates/design.md - Implemented record:
docs/src/features/maison-006-review-gated-dependency-updates/index.md - Base branch:
dev - Status: delivered
- Review priority:
P0
Feature Summary
Stop administratively auto-merging dependency updates by separating cache warming from dependency approval and requiring normal branch protection and review.
User Intent
The cache-refresh workflow uses an administrative bypass to merge flake updates.
Goals
- Automated cache refresh never admin-merges dependency updates.
- Flake update PRs require ordinary review and branch protection.
- Cache warming remains available without approving dependency changes.
Non-Goals
- Removing automated PR creation.
- Changing Nix input semantics.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Remove
gh pr merge --adminor equivalent administrative bypass from workflows. - Separate cache-warming jobs from dependency-update approval jobs.
- Docs state review requirements and cache behavior.
- CI tests or workflow linting catch reintroduction of admin merge.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Update .github/workflows/cache-refresh.yml so cache realization and dependency approval are separate. Automation may update flake.lock, build the proposed closure matrix, warm Cachix, and open or update an automation/refresh-flake-lock pull request after cache warming succeeds. It must not merge that PR, use gh pr merge --admin, enable auto-merge, or otherwise bypass ordinary branch protection.
GitHub pull request review and protected-branch checks remain the only dependency approval mechanism. This feature does not introduce a second approval service or a new operator command. Cache warming can run against proposed dependency changes without granting approval; accepting the dependency update remains a normal reviewed PR merge.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/operations.md | Update | Explain that automated cache warming may prepare a flake update PR, but dependency approval remains review-gated | Implementation tasks |
| docs/src/development/tooling.md | Update | Document cache-refresh workflow behavior for contributors and CI maintainers | Implementation tasks |
| docs/src/reference/tooling.md | Update | List .github/workflows/cache-refresh.yml and its no-admin-merge contract | Implementation tasks |
docs/src/features/maison-006-review-gated-dependency-updates/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Workflow tests or source guards reject
--adminmerge, auto-merge enablement, and any dependency-automation job that invokesgh pr merge. - Workflow tests or source guards prove cache warming can run without granting dependency approval and that update PR creation/update remains separate from merge approval.
- Actionlint on updated workflows.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-006-review-gated-dependency-updates ci-contract— Add dependency-update workflow safety checks.maison-006-review-gated-dependency-updates ci-implementation— Separate cache warming from dependency approval.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The contract task blocks the implementation task so workflow safety guards land before the workflow change. The two planned implementation tasks are not parallel-safe because both touch workflow safety expectations and related tests/docs.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-007: Parser-backed TOML mutations
Metadata
- Beads feature root:
maison-mol-ywg - Feature slug:
maison-007-parser-backed-toml-mutations - Design path:
docs/src/features/maison-007-parser-backed-toml-mutations/design.md - Implemented record:
docs/src/features/maison-007-parser-backed-toml-mutations/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Feature Summary
Replace the fragile TOML editor with a formatting-preserving parser-backed mutation layer using pinned tomlkit.
User Intent
A concrete regression proved that config_edit.py can delete an unrelated following section when a table header includes a trailing comment.
Goals
- TOML mutation preserves comments, table boundaries, arrays of tables, quoted keys, and CRLF input.
- Malformed input fails safely without partial writes.
- Authoring commands share one parser-backed mutation layer.
Non-Goals
- A broad rewrite of all authoring commands before the mutation layer is stable.
- Supporting arbitrary TOML formatting normalization as a side effect.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Pin
tomlkitfor mutation scripts through a deterministic repository-controlled runtime path that does not rely on ambient Python site-packages. - Preserve
tool:addandtool:removerepairability when mise project tool resolution is broken; parser-backed editing must still work on the--skip-toolspath. - Replace section-splicing logic in
config_edit.pywith parser-backed edits for tools, bootstrap packages/apps, lock entries, and inventory host additions. - Add regression tests for trailing comments, quoted keys, arrays of tables, CRLF input, unrelated following sections, malformed structures, lock blocks, and host inventory additions.
- Mutation commands write only after successful parse, edit, validation, and lock reconciliation.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Introduce a small Python TOML mutation module built on pinned tomlkit. Existing TOML authoring surfaces call typed operations for adding and removing tools, bootstrap packages/apps, lock entries, and inventory hosts rather than editing text sections directly. Shell task transaction wrappers and command names remain unchanged.
The tomlkit runtime must be deterministic and repository-controlled because tool:add and tool:remove intentionally bypass mise project tool resolution when repairing invalid tool configuration. The implementation may vendor the exact reviewed tomlkit runtime or use an equivalently pinned checked-in dependency mechanism, but it must not depend on unpinned ambient site-packages.
The module preserves formatting where tomlkit can round-trip, including comments, quoted keys, table boundaries, arrays of tables, and CRLF newlines covered by tests. Unsupported malformed input is rejected before replacing files. Non-TOML authoring artifacts are outside this feature.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/src/development/tooling.md | Update | Document the pinned parser-backed TOML mutation runtime for contributors | Implementation tasks |
| docs/task-reference.md | Update | Document parser-backed transactional TOML mutation guarantees for configuration commands | Implementation tasks |
| docs/add-a-tool.md | Update | Explain tool/package edits preserve supported TOML formatting and fail without partial writes | Implementation tasks |
| docs/add-an-app.md | Update | Explain app edits preserve supported TOML formatting and fail without partial writes | Implementation tasks |
docs/src/features/maison-007-parser-backed-toml-mutations/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Focused
tests/test_config_edit.pyregression corpus for comments, quoted keys, arrays of tables, CRLF input, malformed TOML, lock blocks, host inventory additions, and unrelated sections. - Tests proving the pinned
tomlkitruntime is used without ambient site-packages and remains available on thetool:add/tool:remove--skip-toolspath. - Behavioral authoring command tests.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-007-parser-backed-toml-mutations toml-contract— Add parser-backed TOML regression corpus.maison-007-parser-backed-toml-mutations toml-implementation— Replace text-splice TOML editing with pinned tomlkit layer.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The contract task blocks the implementation task so regression coverage lands first. The two planned implementation tasks are not parallel-safe because both touch the mutation helper, authoring behavior, and related tests/docs.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-008: Repository mutation locking and journals
Metadata
- Beads feature root:
maison-mol-4ev - Feature slug:
maison-008-repository-mutation-locking - Design path:
docs/src/features/maison-008-repository-mutation-locking/design.md - Implemented record:
docs/src/features/maison-008-repository-mutation-locking/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Feature Summary
Serialize repository mutations and add crash recovery for multi-file operations.
User Intent
Package, application, tool, host, and update commands can concurrently overwrite one another, and rollback failures are suppressed.
Goals
- One repository-wide
fcntllock serializes mutating commands. - Multi-file operations journal intended changes and recover on startup or next mutation.
- Rollback failures are surfaced with diagnostics and preserved state.
Non-Goals
- Serializing read-only plan/status commands.
- Replacing Git as source history.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Repository mutation entrypoints for
tool:add,tool:remove,package:add,package:remove,app:add,app:remove,host:add, andupdateacquire the target repository lock before reading mutable repository state. - The lock and journals are local untracked state keyed by the canonical target repository path. They must not dirty the public repository or private overlay and must not use the root-owned remote deployment transaction namespace.
- Journals record original file copies, candidate file copies, validation state, commit state, and recovery action for each multi-file repository mutation.
- Mutation startup acquires the lock, recovers any incomplete local mutation journal for that target repository, then reads mutable state.
- Concurrent mutation attempts fail fast with clear diagnostics naming the busy repository and recovery state.
- Rollback failures return non-zero, leave the journal and copied state for recovery, and preserve diagnostics.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Add a small Python repository mutation context that uses stdlib fcntl locking, writes a durable local journal for repository file mutations, stages candidates in a temporary directory, validates, then atomically replaces files. Existing shell task command surfaces remain; mutating tasks call the shared context instead of open-coded temporary backup and cleanup paths.
The target repository is the repository whose checked-in or overlay-owned file is being mutated. The context stores its lock and journals in untracked local Maison state keyed by the target repository’s canonical path, for example under ${XDG_STATE_HOME:-$HOME/.local/state}/maison/repository-mutations/. Tests may override this root with a dedicated environment variable. The state directory uses owner-only permissions because journals may copy private overlay data.
Mutation commands attempt a non-blocking lock. If another mutation is active, the command exits non-zero with a message that identifies the busy repository and points to recovery guidance. After acquiring the lock and before reading mutable repository state, the context recovers any incomplete journal for that target repository. Read-only commands such as plan, status, list, validate, and search do not take this lock.
The local repository mutation journal is separate from the privileged remote deployment transaction namespace introduced by earlier features. MAISON-008 must not weaken the root-owned deployment transaction root, same-filesystem constraints, or revision-bound deployment rollback behavior.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Document the local repository mutation lock/journal boundary and its separation from remote deployment transactions | Implementation tasks |
| docs/operations.md | Update | Describe serialized mutation behavior, busy-lock failures, and affected commands | Implementation tasks |
| docs/recovery.md | Update | Describe local mutation journal recovery state, diagnostics to preserve, and safe operator response | Implementation tasks |
| docs/task-reference.md | Update | Document which mutation commands take the repository lock and which read-only commands do not | Implementation tasks |
docs/src/features/maison-008-repository-mutation-locking/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Concurrent mutation tests proving fail-fast locking for
tool:add,tool:remove,package:add,package:remove,app:add,app:remove,host:add, andupdaterepository mutations. - Crash-recovery tests for journal states before validation, after validation, before replacement, and after partial replacement.
- Tests proving rollback failures are not suppressed and preserve journal diagnostics.
- Tests proving read-only plan/status/list/validate/search commands do not require the repository mutation lock.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-008-repository-mutation-locking mutation-contract— Add concurrent mutation and journal recovery tests.maison-008-repository-mutation-locking mutation-implementation— Implement shared repository lock and mutation journal.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The contract task blocks the implementation task so concurrency, journal, and rollback expectations land first. The two planned implementation tasks are not parallel-safe because both touch transaction helpers, mutation task behavior, related tests, and documentation.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-009: Authoring checkout guard
Metadata
- Beads feature root:
maison-mol-74l - Feature slug:
maison-009-authoring-checkout-guard - Design path:
docs/src/features/maison-009-authoring-checkout-guard/design.md - Implemented record:
docs/src/features/maison-009-authoring-checkout-guard/index.md - Base branch:
dev - Status: reviewed
- Review priority:
P1
Feature Summary
Prevent authoring commands from modifying deployed snapshots that lack .git and will be overwritten by the next deployment.
User Intent
Remote archives intentionally contain no .git, but mutation commands remain exposed. These edits are ephemeral and misleading.
Goals
- Mutation commands require an authoring checkout for the target repository they write.
- Deployed snapshots reject authoring commands with clear guidance.
- Read-only operational commands remain available in deployed snapshots where safe.
Non-Goals
- Adding Git to remote deployment archives.
- Blocking user convergence in deployed snapshots.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- All mutating authoring commands verify an authoring checkout before writing the target repository.
- Deployed snapshots are detected by absence of
.gitplus Maison revision metadata. - A
.gitdirectory or worktree.gitfile counts as authoring checkout evidence. - Error messages point to the private overlay or authoring repo workflow.
- Task reference distinguishes authoring-only commands from deployed runtime commands.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Add a shared stdlib Python checkout-mode detector beside the existing repository mutation helper. Mutation tasks call it for the target repository before acquiring the mutation lock and before writing. The detector inspects only checkout marker state (.git or .maison-revision) so it does not read mutable TOML, lockfiles, or journaled content before MAISON-008 startup recovery.
In deployed snapshots, mutators fail with a non-zero status and an explanation that source edits must happen in an authoring checkout or private overlay repository. host:add guards the active inventory repository after resolving inventory_root, so a deployed public Maison snapshot may still add hosts to a private overlay clone when that overlay clone is a Git checkout. Read-only status, plan, apply, and recovery operations keep their current deployed behavior.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Document the authoring checkout versus deployed snapshot boundary | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-009: Authoring checkout guard | Implementation tasks |
| docs/task-reference.md | Update | Distinguish authoring-only commands from deployed runtime commands | Implementation tasks |
| docs/deployment.md | Update | Explain deployed archives are runtime snapshots, not authoring checkouts | Implementation tasks |
| docs/recovery.md | Update | Explain rejected deployed-snapshot authoring commands and overlay/source recovery path | Implementation tasks |
docs/src/features/maison-009-authoring-checkout-guard/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Gitless deployment fixture tests for every authoring mutator.
- Tests proving runtime apply/status commands still work without
.git. mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-009-authoring-checkout-guard checkout-contract— Add deployed-snapshot authoring guard tests.maison-009-authoring-checkout-guard checkout-implementation— Implement authoring checkout guard after the contract tests are committed.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The checkout implementation depends on the checkout contract task so tests land before behavior changes; these two tasks are sequential because they cover the same command surface, tests, and documentation pages.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Design Changes During Specification Review
- Narrowed deployed-snapshot behavior to fail-fast rejection instead of dynamic task hiding.
- Clarified that checkout guards apply to the target repository being written, preserving private overlay host authoring.
- Clarified that checkout detection uses only marker state and does not weaken MAISON-008 lock and journal recovery ordering.
- Added architecture documentation to the reader-facing documentation impact.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-010: Shared inventory schema validation
Metadata
- Beads feature root:
maison-mol-bvr - Feature slug:
maison-010-shared-inventory-schema - Design path:
docs/src/features/maison-010-shared-inventory-schema/design.md - Implemented record:
docs/src/features/maison-010-shared-inventory-schema/index.md - Base branch:
dev - Status: reviewed
- Review priority:
P1
Feature Summary
Make Python and Nix inventory validation identical through one schema source and shared valid/invalid fixture corpus.
User Intent
The Python and Nix implementations disagree about duplicate profiles, unknown features, types, and accepted fields.
Goals
- One checked-in public schema contract drives both Python and Nix validation.
- Shared valid and invalid fixtures cover public examples and private overlay shapes.
- Drift between Python and Nix validators fails CI.
Non-Goals
- Replacing TOML as the inventory format.
- Encoding actual private inventory in the public repo.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Define
schemas/inventory.tomlas the public inventory schema contract owned by Maison. - Python and Nix validation load schema data from that contract rather than duplicating allowed systems, profiles, feature keys, deploy fields, and defaults in separate source code.
- Python and Nix validation consume the same fixture corpus under
tests/fixtures/inventory/. - Fixtures cover duplicate profiles, unknown features, unknown deploy fields, type errors, accepted fields, overlay inventory and host-override layout, and deploy path constraints.
- Docs describe inventory fields, defaults, constraints, overlay inventory validation, and host override layout.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Create schemas/inventory.toml as the canonical public schema contract. The file contains schema version, supported
systems, profile names, feature keys and defaults, deploy keys and defaults, and the portable validation policy that both
validators must enforce. Python remains the ergonomic typed reader, while Nix imports the same contract with
builtins.fromTOML and uses it before constructing outputs.
Add a canonical fixture corpus under tests/fixtures/inventory/ with valid and invalid TOML inventories plus minimal
expected-result metadata. CI runs Python and Nix validators against every fixture so divergence fails before deployment
or authoring changes land. Overlay fixtures model an active overlay inventory and sibling hosts/ override layout; they
do not merge public and private inventories.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
schemas/inventory.tomlbecomes the single public schema contract for inventory validation data.- The shared fixture corpus is an executable compatibility contract for both Python and Nix validators.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Document schemas/inventory.toml as the shared inventory schema contract | Implementation tasks |
| docs/add-a-host.md | Update | Document inventory authoring constraints and overlay host override validation | Implementation tasks |
| docs/deployment.md | Update | Document deploy inventory fields and shared validation constraints | Implementation tasks |
| docs/src/reference/inventory.md | Create | Define inventory fields, defaults, constraints, and fixture contract | Implementation tasks |
| docs/src/reference/tooling.md | Update | Mention shared inventory schema and fixture validation tooling | Implementation tasks |
docs/src/features/maison-010-shared-inventory-schema/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Shared fixture corpus executed by Python tests and Nix checks.
- Coverage for active overlay inventory and host override layout, public example inventory, and deploy path constraints.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-010-shared-inventory-schema schema-fixtures— Add shared inventory schema contract and validation fixture corpus.maison-010-shared-inventory-schema schema-implementation— Load the shared schema contract from Python and Nix validators.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. The schema implementation depends on the schema-fixtures task because the fixture corpus is the executable contract; these tasks are sequential because they edit the same validation tests and documentation pages.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Design Changes During Specification Review
- Named
schemas/inventory.tomlas the canonical public schema contract for shared validation data. - Replaced ambiguous overlay merge wording with active overlay inventory and host override layout fixtures.
- Added an inventory reference page to document exact fields, defaults, constraints, and fixture contracts.
- Made schema implementation depend on fixture-contract work so tests define the compatibility boundary first.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-011: Bounded deterministic test suite
Metadata
- Beads feature root:
maison-mol-8mr - Feature slug:
maison-011-deterministic-test-suite - Design path:
docs/src/features/maison-011-deterministic-test-suite/design.md - Implemented record:
docs/src/features/maison-011-deterministic-test-suite/index.md - Base branch:
dev - Status: reviewed
- Review priority:
P1
Feature Summary
Make the test suite bounded and deterministic with subprocess timeouts, process-group cleanup, captured diagnostics, and subsystem splits.
User Intent
The main test file is 1,727 lines with 48 external-process calls and no timeout. Repeated bounded full-suite runs did not complete reliably.
Goals
- Every external process in tests has a timeout and process-group cleanup.
- Tests are split by subsystem.
- Diagnostics are captured without unbounded output.
- Source-text assertions become behavioral tests where practical.
Non-Goals
- Removing coverage for deployment or Nix behavior.
- Broadly rewriting production code solely for test organization.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Introduce a stdlib-only test subprocess helper with timeout, process-group termination, stdout/stderr capture, concise failure diagnostics, and temporary-directory isolation.
- Refactor tests into subsystem files without changing covered behavior.
- Bound every external test process through the helper. The default subprocess timeout is 30 seconds; longer per-call timeouts must be explicit, documented at the call site, and no longer than 300 seconds.
- Keep
check:testsdeterministic and expected to complete within five minutes on a warm supported development checkout; document focused validation partitions for subsystem work. - Replace fragile source-text assertions with behavioral checks when an executable contract exists.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Add a shared stdlib test subprocess helper and migrate external process calls to it. The helper starts commands in an isolated process group where the platform supports it, terminates the whole group on timeout, captures stdout and stderr, truncates diagnostics to a documented size, and accepts an explicit timeout override for known long-running Nix or Git fixtures.
Split the monolithic topology suite into inventory, deployment, ownership, transaction, repository, and migration test files. Keep fixtures isolated per test and capture concise diagnostics for failures. Update development docs with focused subsystem commands, the helper timeout policy, and the expected full-suite bound.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- Tests that execute external commands use a shared stdlib subprocess helper rather than ad hoc
subprocesscalls. - Test modules are organized by Maison subsystem so focused validation commands map to the area being changed.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/src/development/tooling.md | Update | Document bounded test helper behavior, focused subsystem test commands, and full-suite expectations | Implementation tasks |
| docs/src/reference/tooling.md | Update | Define validation partitions, subprocess timeout policy, and the shared test helper contract | Implementation tasks |
| docs/operations.md | Update | Summarize operator/contributor validation commands and bounded check expectations | Implementation tasks |
docs/src/features/maison-011-deterministic-test-suite/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Focused tests for the subprocess helper itself.
- Full
mise -E dev run checkcompletes within the documented bound. uv run scripts/check-docs.py.
Implementation Decomposition
maison-011-deterministic-test-suite test-helper— Add the bounded subprocess helper and focused helper tests.maison-011-deterministic-test-suite test-suite-split— Split subsystem tests, migrate existing external process calls to the helper, and update validation documentation.
Dependencies and Parallelism
This feature follows the Maison review order. Both implementation tasks depend on specification reconciliation. The suite-split task depends on the helper task because existing external process calls must migrate to that shared helper and both tasks touch tests and validation documentation. The implementation tasks are sequential.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Design Changes During Specification Review
- Corrected documentation impact paths to the mdBook source pages under
docs/src/development/anddocs/src/reference/. - Made the test subprocess contract explicit: default 30 second timeout, documented explicit overrides up to 300 seconds,
process-group cleanup, bounded diagnostics, and
check:testsexpected to finish within five minutes on a warm supported development checkout. - Clarified that the subprocess helper is stdlib-only and test-local, and that implementation tasks are sequential: helper first, suite split and migration second.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-012: Plan/apply semantic parity
Metadata
- Beads feature root:
maison-mol-6w9 - Feature slug:
maison-012-plan-apply-parity - Design path:
docs/src/features/maison-012-plan-apply-parity/design.md - Implemented record:
docs/src/features/maison-012-plan-apply-parity/index.md - Base branch:
dev - Status: reviewed
- Review priority:
P1
Feature Summary
Make maison user plan execute the same semantics as maison user apply, differing only by dry-run execution.
User Intent
user plan always enables forced dotfile behavior while the default apply path does not.
Goals
- Plan and apply share one argument-construction path.
- Dry-run is the only semantic difference for a given flag set unless an option explicitly documents otherwise.
- Dotfile force behavior is visible and consistent: plan and apply default to no forced replacement, and
--force-dotfilesis explicit for both preview and apply.
Non-Goals
- Changing the documented default safety posture for destructive dotfile replacement without tests.
- Changing package-manager dry-run capabilities beyond existing supported flags.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Centralize user-layer argument construction for plan and apply in a testable Python module or function.
maison user plan,maison user apply, aggregatemaison plan, and aggregatemaison applyuse the same force-dotfile flag semantics: default false, explicit--force-dotfilestrue.- Tests prove force-dotfile flags, package/app/tool command steps, lock handling, and preference steps are identical for plan and apply except dry-run execution and explicitly documented command substitutions.
- Docs accurately describe preview, apply, default dotfile safety, and force-dotfile preview/apply behavior.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Move user convergence argument construction into one tested Python module or function. user plan and user apply call
the same builder with an execution mode and the same parsed user flags. The builder emits a structured command plan used
by tests and both task entrypoints. It preserves the existing command order: prepare dotfiles, apply or preview dotfiles,
link or preview mise lockfiles, run package convergence or its supported dry-run substitute, apply or preview remaining
mise user state, and run finalize only for apply.
Default dotfile force behavior is safe and identical: no forced replacement unless --force-dotfiles is supplied.
user plan --force-dotfiles previews the same forced dotfile handoff that user apply --force-dotfiles performs, but
with dry-run execution. Aggregate maison plan --force-dotfiles forwards the same user-layer flag just as aggregate
maison apply --force-dotfiles does.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- User plan/apply command construction is owned by one structured command-plan builder.
- For the same user flags, plan and apply differ only by dry-run execution and documented command substitutions.
- Forced dotfile replacement remains opt-in for both preview and apply.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/operations.md | Update | Document plan/apply parity, default no-force dotfile behavior, and --force-dotfiles preview/apply flow | Implementation tasks |
| docs/recovery.md | Update | Replace force-enabled dry-run wording with explicit user plan --force-dotfiles then user apply --force-dotfiles recovery guidance | Implementation tasks |
| docs/task-reference.md | Update | Document plan/user:plan force-dotfile flag behavior and dry-run-only command differences | Implementation tasks |
docs/src/features/maison-012-plan-apply-parity/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Focused tests comparing generated plan/apply structured command plans for default and
--force-dotfilesmodes. - Behavioral tests for aggregate
planandapplyforce-dotfile forwarding. - Behavioral tests for force-dotfile conflict previews and applies.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-012-plan-apply-parity parity-contract— Add user plan/apply parity tests for structured command plans, force-dotfile defaults, explicit force mode, package/tool/app/lock/preference steps, and aggregate flag forwarding.maison-012-plan-apply-parity parity-implementation— Centralize user convergence argument construction and update task entrypoints/docs to use the shared builder.
Dependencies and Parallelism
This feature follows the Maison review order. Both implementation tasks depend on specification reconciliation. The parity-implementation task depends on the parity-contract task because the tests define the accepted command-plan and flag-forwarding behavior. The implementation tasks are sequential.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Design Changes During Specification Review
- Clarified that plan/apply parity applies for the same user flags and that forced dotfile replacement is opt-in for both preview and apply.
- Added aggregate
maison plan --force-dotfilesflag-forwarding to match aggregate apply semantics. - Scoped the builder to user-convergence command-plan construction rather than replacing package-manager behavior or broad shell orchestration.
- Made implementation tasks sequential: contract tests first, shared builder implementation second.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-013: Exact dotfile backup manifests
Metadata
- Beads feature root:
maison-mol-h8e - Feature slug:
maison-013-dotfile-backup-manifests - Design path:
docs/src/features/maison-013-dotfile-backup-manifests/design.md - Implemented record:
docs/src/features/maison-013-dotfile-backup-manifests/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Feature Summary
Preserve exact filesystem objects in dotfile backups with structured manifests and full restoration tests.
User Intent
cp -RLp dereferences symlinks and can copy content from outside the managed path.
Goals
- Backups preserve whether each original object was a file, directory, or symlink.
- Symlink backups record exact symlink targets without dereferencing.
- Structured manifests drive restoration and audit.
- Restoration tests cover files, directories, symlinks, and failures.
Non-Goals
- Backing up arbitrary unmanaged directories outside refused dotfile targets.
- Replacing application bundle backup archives covered by separate app backup logic.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Replace dereferencing copy behavior for dotfile conflicts.
- Write a manifest containing source path, object type, mode, timestamps where supported, symlink target, backup path, and restore status.
- Refuse or safely handle unsupported special files.
- Docs explain backup inspection and restoration.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Implement dotfile backups through one Python-stdlib manifest module, called by the existing forced-handoff shell adapter. Each backup directory contains an atomically written manifest.json with a version, home-relative source path, object type, mode, supported timestamps, backup-relative payload path, symlink target where applicable, and per-entry restore status. The module snapshots objects using lstat: regular files and directories retain metadata, symlinks are recreated by their target string without traversal, and socket, FIFO, block, and character objects fail before any source is removed.
Expose restoration as maison user restore-dotfiles <backup-directory> --force. It accepts only a backup directory below $HOME/.local/state/maison/backups/dotfiles, validates every manifest path and payload containment before changing targets, and requires --force to replace an existing target. It restores only manifest-recorded pending entries; after each successful entry it atomically records restored status. A failed entry stops the operation and leaves prior statuses durable for a safe retry.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract: manifest data is authoritative for restore and audit; backup preserves
lstatobject identity and never traverses a refused-target symlink.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/recovery.md | Update | Align reader-facing contract with MAISON-013: Exact dotfile backup manifests | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-013: Exact dotfile backup manifests | Implementation tasks |
| docs/task-reference.md | Update | Align reader-facing contract with MAISON-013: Exact dotfile backup manifests | Implementation tasks |
docs/src/features/maison-013-dotfile-backup-manifests/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Backup/restore tests for regular files, directories, symlinks, missing targets, malformed manifests, and unsupported objects.
- Tests proving external symlink targets are not copied into backups and restore cannot traverse outside
$HOMEor the supplied backup directory. - Tests proving restore requires
--force, preserves pending/restored manifest status across a partial failure, and replaces only manifest-recorded targets. mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-013-dotfile-backup-manifests backup-contract— Add failing tests for manifest schema, exact backup/restore identity, rejected paths/types, force requirement, and partial restore status.maison-013-dotfile-backup-manifests backup-implementation— Implement the stdlib manifest module, existing handoff adapter, restore task, reader docs, and contract tests.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation.
backup-contract precedes backup-implementation, because its fixtures and assertions define the manifest and restore
contract. No implementation tasks run in parallel for this feature.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Restoration is exposed as
maison user restore-dotfiles <backup-directory> --force; the explicit force flag is required for any target replacement. - Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-014: Remote convergence restoration
Metadata
- Beads feature root:
maison-mol-vhhy - Feature slug:
maison-014-remote-convergence-restoration - Design path:
docs/src/features/maison-014-remote-convergence-restoration/design.md - Implemented record:
docs/src/features/maison-014-remote-convergence-restoration/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Feature Summary
Restore user state after failed remote convergence by rerunning restricted convergence from the restored prior repository.
User Intent
A failed deployment restores repository source but can leave links and other user state pointing at the failed revision.
Goals
- After remote user convergence failure, restore prior repository source and reconverge user state from that revision.
- Restrict recovery convergence to reversible user state.
- Report irreversible package-side effects separately.
Non-Goals
- Guaranteeing package-manager rollback for completed external package operations.
- Rolling back the Nix system profile outside deploy-rs semantics.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
When maison deploy <host> reaches remote user convergence and that phase fails, Maison first completes and verifies the
repository transaction rollback. Only after the restored repository revision is verified does it run restricted recovery
as the managed user from that restored repository. Recovery writes a JSON diagnostic under
~/.local/state/maison/recovery/ and reports its path. The deployment preserves the original user-convergence exit
status; a rollback failure prevents recovery, and a recovery failure is reported separately with both statuses retained
in the diagnostic.
Requirements
Functional Requirements
- The failed remote user-convergence path completes repository rollback and verifies the restored prior revision before attempting any recovery convergence.
- Recovery runs as the managed user from the restored repository. The deployment account remains responsible only for the privileged transaction recovery and does not run user-owned convergence.
- Restricted recovery repairs dotfile convergence, mise lock links, non-package mise user state, and Maison-owned user
finalization. It honors the deployment’s explicit
--force-dotfileschoice and never enables force implicitly. - Restricted recovery does not run
user:apply,user-apply-packages.sh, package/app bootstrap, application-backup migration, legacy Git migration, system activation, or Nix rollback. - Recovery writes an atomically replaced mode-0600 JSON report at
~/.local/state/maison/recovery/remote-convergence-<failed-revision>.json. The version-one report containsschema_version,kind,failed_revision,restored_revision,initial_convergence(exit code and package-phase status),recovery(status, exit code, steps, and force-dotfiles state), andexternal_side_effects. The latter records package/app convergence as not rolled back and whether follow-up work is required. - Package/app side effects are reported as
not-started,started,completed,failed, orunknownbased on the observed convergence step; the report never claims that an external package manager was rolled back. - If repository rollback fails, recovery is skipped and the command reports the transaction inspection path. If rollback succeeds, the original user-convergence failure remains the deployment’s exit status even when recovery succeeds; recovery failure is included in stderr and the JSON report.
- Docs distinguish repository rollback, restricted user reconvergence, and package/app side effects.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Add a post-rollback recovery phase to the existing remote deployment path. The current full user convergence remains the
first attempt. When it fails, the deployment account invokes the existing repository transaction finalizer with
rollback; the finalizer verifies the active failed revision, restores the prior repository, and removes transaction
state only after the swap is durable. The deployment then invokes recovery over SSH as the managed user from that
restored repository. A failed rollback never runs recovery.
Extend .mise/lib/user_convergence.py with an internal recovery mode instead of creating a second convergence
pipeline. Recovery uses the existing command-plan ownership and ordering, with this explicit safe set:
- dotfile conflict preparation/convergence, limited to the existing explicit force policy and without application archival or legacy Git migration;
- mise lockfile linking;
- non-package mise user state; and
- Maison-owned user finalization.
The recovery plan excludes user-apply-packages.sh, all bootstrap.packages and app convergence, system tasks, and
broad user:apply. The planner records step transitions so the deployment can distinguish whether external package/app
convergence was not started, started, completed, failed, or could not be determined. It writes the report through a
user-owned temporary file and atomic rename; no new privileged transaction namespace is introduced.
The remote wrapper preserves the original convergence status, runs recovery only after successful repository rollback, and emits the report path and recovery status without hiding either failure. The report’s package/app entry is a follow-up diagnostic, not a promise of external rollback.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/deployment.md | Update | Document rollback-before-recovery ordering, managed-user execution, report path, and original/recovery failure semantics | reconverge-implementation |
| docs/recovery.md | Update | Document restricted recovery steps, skipped package/app rollback, report fields, and manual follow-up | reconverge-implementation |
| docs/operations.md | Update | Document the automatic recovery outcome and explicit --force-dotfiles behavior | reconverge-implementation |
| docs/task-reference.md | Update | Reference deploy’s restricted recovery contract and diagnostic artifact | reconverge-implementation |
docs/src/features/maison-014-remote-convergence-restoration/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Mark the roadmap state and retain the Beads root after delivery | Close-out documentation |
docs/src/SUMMARY.md | Update | Register the delivered record while retaining the design link | Close-out documentation |
Validation Strategy
- Add fault-injection tests in
tests/test_remote_convergence.pyfor failed user convergence, rollback-before-recovery, restored-revision execution, force-dotfile forwarding, report contents, skipped package/app convergence, and separate recovery failure. - Run focused tests for
tests.test_remote_convergence,tests.test_deployment_contracts, andtests.test_user_convergence. - Run
python3 scripts/check-docs.pyafter reader-facing documentation changes. - Run
mise run checkfor repository-wide validation.
Implementation Decomposition
maison-014-remote-convergence-restoration reconverge-contract— Createtests/test_remote_convergence.pywith behavioral/fault-injection coverage for the exact recovery sequence, revision boundary, safe-step exclusion, report schema/statuses, force forwarding, and failure semantics. This task owns no production source or reader-facing docs.maison-014-remote-convergence-restoration reconverge-implementation— Update.mise/tasks/deploy,.mise/lib/user_convergence.py, and the narrowly scoped user-prepare adapter needed for recovery; updatedocs/deployment.md,docs/recovery.md,docs/operations.md, anddocs/task-reference.md; run focused and broad validation; preserve the existing transaction and Nix/mise ownership boundaries.
Dependencies and Parallelism
This feature follows the Maison review order. The implementation coordinator and both implementation children depend on specification reconciliation. The contract-test child completes before the implementation child so tests establish the behavior before production changes. The contract-test child owns only its new test file; the implementation child owns production source and the four reader-facing pages, so no sibling edits overlap.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-015: Linux runtime activation verification
Metadata
- Beads feature root:
maison-mol-cg3x - Feature slug:
maison-015-linux-runtime-verification - Design path:
docs/src/features/maison-015-linux-runtime-verification/design.md - Implemented record:
docs/src/features/maison-015-linux-runtime-verification/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Feature Summary
Verify Linux runtime state after activation rather than only writing configuration files.
User Intent
Writing /etc/hostname does not necessarily update the active hostname; writing /etc/timezone does not establish /etc/localtime; SSH reload failures are suppressed.
Goals
- Post-activation checks verify active hostname, timezone, SSH configuration, and service unit state.
- Native system-manager options are used where possible.
- SSH reload failures are surfaced.
Non-Goals
- Replacing system-manager activation as the Linux system layer.
- Supporting Linux hosts without systemd; MAISON-015 supports systemd-backed Linux hosts only.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Linux activation requires systemd as the running init system and fails with an actionable diagnostic when systemd or
systemctlis unavailable; no non-systemd fallback is supported. - Use system-manager/native mechanisms for hostname, timezone, SSH config, and services where available.
- After activation, verify that
hostname --staticequals the configured host name and that the active timezone isAmerica/New_Yorkthrough systemd’s runtime state and/etc/localtime. - Validate the active SSH configuration with
sshd -t, reload or restart the activessh.serviceorsshd.service, and fail activation when the check or reload fails; no SSH reload failure may be suppressed. - Verify that
system-manager.targetand the Maison-managedprefill-authorized-keys.servicereach an active state. - Activation fails or reports a clear degraded state when runtime checks do not match intended configuration.
- Tests cover active hostname,
/etc/localtime, SSH reload failure, and active service unit verification.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Extend nix/modules/linux/system.nix with one systemd-backed activation verification path. A pre-activation assertion
requires a running systemd instance and systemctl; a host without that runtime fails before Maison replaces system
files. Use system-manager’s Nix module mechanisms for the managed service units, source the managed timezone data into
/etc/localtime while retaining the compatibility /etc/timezone file, and keep hostname configuration tied to the
inventory host name.
The existing prefill-authorized-keys.service validates sshd -t and reloads or restarts the active SSH unit without
suppressing failure. A dependent maison-runtime-verification.service runs after that service and verifies the active
static hostname, systemd-reported timezone, /etc/localtime, system-manager.target, and
prefill-authorized-keys.service. It exits non-zero with a field-specific diagnostic for any mismatch. The same
system-manager activation service is used by local activation and deploy-rs remote activation; no separate deployment
or non-systemd verification pipeline is introduced.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the architecture and operations pages named in Documentation Impact so current reader-facing docs match the implemented behavior.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Align reader-facing contract with MAISON-015: Linux runtime activation verification | Implementation tasks |
| docs/operations.md | Update | Align reader-facing contract with MAISON-015: Linux runtime activation verification | Implementation tasks |
| docs/recovery.md | Update | Align reader-facing contract with MAISON-015: Linux runtime activation verification | Implementation tasks |
| docs/deployment.md | Update | Align reader-facing contract with MAISON-015: Linux runtime activation verification | Implementation tasks |
docs/src/features/maison-015-linux-runtime-verification/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
- Nix/system-manager checks for generated activation verification and systemd-only assertions.
- Behavioral tests simulating hostname/timezone/SSH/service mismatch, missing systemd, and reload failure.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-015-linux-runtime-verification runtime-contract— Add Linux runtime verification tests.maison-015-linux-runtime-verification runtime-implementation— Implement Linux post-activation runtime checks.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. Sibling implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-016: Pi TypeScript validation boundary
Metadata
- Beads feature root:
maison-mol-fmud - Feature slug:
maison-016-pi-typescript-validation - Design path:
docs/src/features/maison-016-pi-typescript-validation/design.md - Implemented record:
docs/src/features/maison-016-pi-typescript-validation/index.md - Base branch:
dev - Status: draft
- Review priority:
P1
Historical reconciliation: this design describes the original public Maison delivery. After the Maison/Terroir ownership split, the Pi extension sources and validation workspace moved to private Terroir; the current Maison framework no longer owns the
dotfiles/pi/extensionsworkspace or itscheck:typescripttask.
Feature Summary
Add a real TypeScript workspace, Pi types, typecheck, pure modules, and behavioral tests for Pi extensions.
User Intent
The Pi extensions include a 983-line pager without a pinned TypeScript workspace, typecheck, or behavioral tests.
Goals
- Pi extension TypeScript is typechecked with pinned tooling.
- Pure pager logic is separated from terminal integration.
- Tests cover Unicode, ANSI handling, navigation, timers, and terminal resizing.
Non-Goals
- Rewriting Pi itself.
- Changing visual design beyond behavior needed for tests and type boundaries.
User-Facing Behavior
Operators keep using maison and mise tasks as the command surface. The feature changes the underlying safety,
validation, or documentation contract named above without requiring operators to learn an unrelated tool. When behavior
is unsafe or unsupported, Maison fails with an actionable message instead of silently continuing.
Requirements
Functional Requirements
- Add a pinned TypeScript workspace for
dotfiles/pi/extensionsor an equivalent project-local package. - Use actual Pi types or local checked interface definitions derived from documented Pi extension APIs.
- Run
tsc --noEmitin Maison validation. - Add behavioral tests for pure pager modules.
Quality Requirements
- Preserve Maison’s Nix/Lix system ownership and mise user ownership boundary.
- Prefer tested Python stdlib implementations for stateful behavior. Python task files under
.mise/tasks/are acceptable, includingusagecomments for argument validation and mise templating or sandboxing where useful. - Do not introduce a Rust helper unless a later design proves Python cannot safely satisfy the filesystem or privilege boundary.
- Keep implementation small, reviewable, and covered by behavioral or fault-injection tests before relying on it.
Compatibility and Migration Requirements
- Preserve supported platforms:
aarch64-darwin,aarch64-linux, andx86_64-linux. - Preserve the absence of Home Manager.
- Update existing commands in place rather than introducing parallel legacy paths.
- Public Maison remains generic and public-safe; personal/site configuration belongs in a private overlay where relevant.
Existing Context
Maison currently documents a two-layer architecture where Nix/Lix owns privileged system state and mise owns user state.
The review of commit ded7bbb745f34f1059930fc48eadafe267399ab2 identified this feature as required work. Existing
reader documentation under docs/ describes the target operations and must be reconciled with delivered behavior.
Proposed Design
Create a private project-local Node workspace at dotfiles/pi/extensions. The workspace is source for validation only;
Mise continues to symlink the extension .ts files into the user’s Pi directory, while package.json,
package-lock.json, and tsconfig.json remain repository-owned workspace metadata. The root mise.toml owns Node 24
for repository checks. The workspace lock pins TypeScript, tsx, @types/node, and the Pi packages used by the
extensions (@earendil-works/pi-ai, @earendil-works/pi-coding-agent, and @earendil-works/pi-tui); validation does
not depend on the macOS-only global Pi installation.
Add a check:typescript task that runs npm ci --ignore-scripts in the workspace, then invokes the package scripts
tsc --noEmit and the focused node:test suite through the pinned tsx runner. Include this task in the repository
check task so CI and local validation use the same boundary.
Extract only the pager logic needed for deterministic tests: pager/markdown.ts owns newline normalization, fenced
code, and heading/numbering extraction; pager/keys.ts owns key and printable-input decoding; and
pager/navigation.ts owns scroll, TOC, and code-block selection state transitions. These modules must not import Pi,
TUI, clipboard, process, or terminal APIs. pager.ts remains the thin rendering, clipboard, timer, and Pi-registration
adapter. Timer behavior is tested through an injected scheduler/fake-timer seam rather than wall-clock sleeps. No
visual redesign or wholesale pager rewrite is part of this feature.
Architecture Consistency
Existing Patterns Reused
- Nix/Lix remains the privileged system layer.
- mise remains the user command and package/dotfile layer.
maison checkremains the repository-wide validation entrypoint.- Python is the preferred home for stateful behavior that needs tests, parsing, locks, manifests, or recovery.
Invariants Preserved
- A file, package, service, or preference has exactly one owner within its execution scope. Node is the deliberate scoped exception: the global user runtime serves standalone Pi, while repository Node 24 serves locked validation without changing global convergence.
- Intel macOS remains unsupported.
- Remote deployment remains split between deploy-rs system-profile handling and Maison repository/user handling.
- Public Maison does not contain private infrastructure identity or trusted access material.
New Decisions Introduced
- This feature adopts the concrete behavior described in Proposed Design as the supported Maison contract.
Architecture Documentation Changes
Update the exact reader-facing pages named in Documentation Impact. The architecture page records the scoped Node
runtime boundary; development and reference tooling pages explain workspace installation and validation commands;
dotfiles/pi/AGENTS.md explains the extension editing and test boundary; and dotfiles/opencode/README.md records that
the workspace is scoped to Pi extensions and does not change OpenCode runtime configuration. No operations page requires a
behavior change for this feature.
Operational Considerations
Operators should receive explicit errors, recovery instructions, and validation evidence for this feature’s failure modes. Recovery docs must distinguish Nix generation behavior, repository/source behavior, user convergence behavior, and external package-manager side effects when those concerns apply.
Documentation Impact
| Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|
| docs/architecture.md | Update | Document the scoped repository Node runtime used for Pi validation | Implementation tasks |
| docs/src/development/tooling.md | Update | Align reader-facing contract with MAISON-016: Pi TypeScript validation boundary | Implementation tasks |
| docs/src/reference/tooling.md | Update | Align reader-facing contract with MAISON-016: Pi TypeScript validation boundary | Implementation tasks |
| dotfiles/pi/AGENTS.md | Update | Align reader-facing contract with MAISON-016: Pi TypeScript validation boundary | Implementation tasks |
| dotfiles/opencode/README.md | Update | Align reader-facing contract with MAISON-016: Pi TypeScript validation boundary | Implementation tasks |
docs/src/features/maison-016-pi-typescript-validation/index.md | Create during close-out | Preserve delivery and audit history | Close-out documentation |
docs/src/planned-features.md | Update | Track roadmap status and Beads root | Planning |
docs/src/SUMMARY.md | Update | Register this design and delivered record links | Planning / close-out |
Validation Strategy
mise run check:typescript, which performs locked workspace installation,tsc --noEmit, and the focusednode:testsuite for Pi extensions.- Behavioral tests for Unicode, ANSI handling, navigation, deterministic timers, and terminal resizing.
mise -E dev run checkanduv run scripts/check-docs.py.
Implementation Decomposition
maison-016-pi-typescript-validation typescript-workspace— Own the project-local package metadata, root Node/mise check wiring, Pi type dependencies, typecheck task, and the exact tooling/agent documentation pages.maison-016-pi-typescript-validation pager-tests— After the workspace exists, extract the named pure pager modules and add deterministicnode:testcoverage for Unicode, ANSI, navigation, timers, and terminal resizing.
Dependencies and Parallelism
This feature follows the Maison review order. Its implementation tasks depend on specification reconciliation. Sibling implementation tasks may run in parallel only when they do not edit the same command path, test file, or documentation page.
Rollout and Migration
Roll out by updating tests and docs first, then replacing the existing implementation path. Existing commands keep their names unless a feature explicitly narrows unsafe behavior.
Risks and Tradeoffs
- Safety fixes may temporarily reduce permissive behavior that previously appeared to work.
- Deployment and recovery changes require fault-injection coverage because ordinary happy-path tests are insufficient.
- Keeping shell and mise task compatibility while moving logic into Python may require small adapter changes.
Rejected Alternatives
- Broad rewrite before fixing the review findings.
- Preserving unsafe legacy paths as supported behavior.
- Storing private infrastructure identity in public Maison defaults.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Stateful Maison behavior should move into tested Python implementations.
.mise/tasks/may contain Python task files and may useusagecomments, task templating, and sandboxing. - The default privileged deployment account is
maison-deploy. - Maison is the reusable vehicle; a tracked private overlay repository is the driver for personal/site configuration.
- Fresh setup supports
--overlay <git-url-or-path>, prompts only when interactive, and fails clearly when a required overlay is missing in non-interactive mode. - Public examples should be real workable starter configurations without personal infrastructure identity.
Assumptions
- The reviewed commit and review summary are authoritative planning evidence for this remediation roadmap.
- Feature branches should base on
dev.
Design Changes During Planning
- The private inventory model changed from untracked local TOML to a tracked private overlay repository to preserve Maison’s durability and replication goals.
Source Material
- Maison review summary for commit
ded7bbb745f34f1059930fc48eadafe267399ab2. - Current Maison documentation under
README.mdanddocs/.
Design — MAISON-017: Maison/Terroir repository split
Metadata
- Beads feature root:
maison-mol-jfhs - Feature slug:
maison-017-maison-terroir-repository-split - Design path:
docs/src/features/maison-017-maison-terroir-repository-split/design.md - Implemented record:
docs/src/features/maison-017-maison-terroir-repository-split/index.md - Base branch:
dev - Status: draft
Feature Summary
Create the two repositories that the Maison architecture describes but the existing nix-config repository does not
yet provide: a new public RobertDeRose/maison framework repository and a new private RobertDeRose/terroir
configuration repository. Preserve the existing nix-config checkout and history as a private archived migration source.
User Intent
The user wants Maison to be the reusable public vehicle and Terroir to be the private driver containing actual personal and
site configuration. The new repositories must start with fresh logical commits rather than importing the existing public
history. The public repository should use the dstack workflow and may be initialized through /setup-project; Terroir is a
plain private data/configuration repository. Bitwarden remains the source of truth for secrets and private keys.
The migration source is the current nix-config checkout and its Git history. Migration first generates a candidate
manifest and requires explicit user approval before moving, replacing, or deleting any candidate path.
Goals
- Create a clean public
RobertDeRose/maisonrepository containing reusable framework code, neutral examples, tests, documentation, and dstack project controls. - Create a private
RobertDeRose/terroirrepository containing the approved personal/site configuration in the mirrored overlay layout expected by Maison. - Give each new repository a fresh, reviewable logical commit history with no imported
nix-configcommits or metadata. - Preserve the complete original
nix-confighistory by makingRobertDeRose/nix-configprivate and archiving it only after both new repositories pass validation. - Create separate local checkouts at
../maisonand../terroir, and configure Maison to use Terroir as its overlay. - Make the final split reproducible and auditable through an approved candidate manifest, validation evidence, and documented transition steps.
Non-Goals
- Storing passwords, tokens, secret values, SSH private keys, signing private keys, or other private key material in Terroir; Bitwarden remains the secret store.
- Rewriting or force-pushing the existing
nix-confighistory; that repository becomes the private archive. - Carrying any old commit history, author metadata, Git refs, or Beads interaction history into either new repository.
- Applying a system configuration or changing machine state as part of the repository migration.
- Adding the dstack workflow, Beads, or Copier controls to Terroir.
- Automatically moving files without an approved candidate manifest.
- Deleting the source checkout or deleting either newly created remote repository on a failed migration.
User-Facing Behavior
After delivery, the public workflow is:
git clone https://github.com/RobertDeRose/maison.git
cd maison
./bootstrap.sh --host "$(hostname -s)" --overlay git@github.com:RobertDeRose/terroir.git
Maison stores the selected overlay source using its existing XDG overlay-state contract. The Terroir checkout mirrors
supported owned paths such as inventory.toml, hosts/, config/mise/*.toml, dotfiles/, and non-secret trusted
configuration. Users retrieve secrets and private keys from Bitwarden rather than from either repository.
The old nix-config GitHub repository remains available as a private archived record and is not an active public source.
Requirements
Functional Requirements
- Inspect the current
nix-configworking tree and Git history, classify candidate personal/site paths, and emit a reviewable manifest containing each source path, destination repository/path, action, and rationale. - Require explicit approval of the candidate manifest before any source path is removed, replaced, or copied as private data.
- Create
RobertDeRose/maisonas a new public repository from a fresh dstack/Copier scaffold using/setup-projector its equivalent setup helper. Use the stable dstack template,mainas the default branch, and the project brief fixed by this design. - Create
RobertDeRose/terroiras a new private plain Git repository withmainas the default branch and the documented overlay layout. - Refuse to overwrite an existing repository with either target name; report the conflict for explicit operator handling.
- Populate both repositories from fresh staging checkouts, preserving only approved content and generated dstack controls.
- Commit each new repository in several logical commits without importing source history, source refs, or source commit metadata.
- Verify that the public Maison tree and all reachable public Git objects contain no approved private paths, personal identity, real infrastructure metadata, or secret material.
- Verify that Terroir contains the approved private configuration but no raw secrets or private keys.
- Verify Maison can load and validate Terroir as an overlay without system activation.
- After validation, make
RobertDeRose/nix-configprivate and archive it. Do not archive it before validation succeeds. - Leave local source and migration backups intact until the remote transition is confirmed.
Quality Requirements
- Preserve the existing Nix/Lix system versus mise user ownership boundary and supported platform set.
- Keep the public Maison repository reproducible through Copier metadata, dstack lifecycle controls, committed lockfiles, documentation validation, and repository checks.
- Keep migration actions ordered and failure-safe: manifest approval, staging, validation, publication, then old-repository archival.
- Make privacy checks deterministic and auditable; a failed privacy or remote-state check blocks archival.
- Do not expose remote credentials, Bitwarden values, or private repository contents in logs or committed artifacts.
Compatibility and Migration Requirements
- Preserve Maison’s existing command names, overlay source precedence, inventory schema, mirrored overlay paths, supported platforms, and deployment boundaries.
- Preserve public starter behavior when no overlay is selected, subject to the existing documented overlay-required mode.
- Use
RobertDeRose/maisonandRobertDeRose/terroiras the canonical repository identities. - Keep
nix-configprivate and archived as the historical source of truth after the split; do not treat it as a supported public clone target. - Keep Terroir independent of dstack while documenting its expected Maison overlay layout and secret boundary.
Existing Context
MAISON-004 implemented the public-side overlay contract: --overlay, local XDG source state, cloning/updating, typed
inventory loading, and mirrored overlay paths. It did not create or populate a private overlay repository. The current
repository is still named nix-config, has only its existing public remote, and has no active overlay checkout or saved
overlay source.
The current repository already contains the Maison framework, dstack/Copier project controls, Nix/Lix and mise ownership
boundaries, supported platform contracts, overlay loader, validators, tests, and reader documentation. The current public
history must not be reused for the new public repository because the desired public result is a clean fresh repository.
The Beads implementation remains controlled from the current nix-config repository and its feature worktree; the new
Maison and Terroir repositories are migration outputs, not alternate Beads control planes for this feature.
Proposed Design
Migration phases
-
Freeze and audit
- Treat the current
nix-configcheckout as the immutable migration source. - Scan current files and history for personal/site configuration and private data candidates.
- Produce a manifest with
source,destination_repo,destination_path,action(public,private,replace, orexclude), and rationale. - Present the manifest for explicit approval. No destructive or private-copy action occurs before approval.
- Treat the current
-
Create fresh repository shells
- Create empty local destinations
<maison-checkout>and<terroir-checkout>without reusing the source.gitdirectory. - Run the stable
/setup-project maisonworkflow in the empty Maison destination with these fixed answers:- purpose: reusable public configuration framework for supported macOS and Linux systems;
- users: Maison maintainers and operators managing supported personal or site hosts;
- scope: Nix/Lix system state, mise user state, inventory, local/remote deployment, convergence, recovery, and validation for Apple Silicon macOS and supported Linux;
- boundaries: personal identity, real infrastructure metadata, trusted private material, secrets, and host-specific configuration belong in Terroir or Bitwarden; Intel macOS and Home Manager remain unsupported;
- project kind:
infrastructure; - language profiles:
nix,python, andtypescript; - repository layout:
single-package.
- Treat the generated setup-project scaffold as authoritative for
.copier-answers.yml,AGENTS.md,.beads/,docs/book.toml, feature templates,scripts/setup-tooling.py,mise.toml, hooks, and other project-control files. Source framework files may populate the scaffold only where the manifest assigns ownership; collisions with generated controls stop for review rather than silently overwriting the new project baseline. - Initialize Terroir as a plain Git repository with only a small README, ignore policy, and supported overlay layout.
- Create the GitHub repositories under the authenticated
RobertDeRoseaccount with public/private visibility as specified, refusing conflicts.
- Create empty local destinations
-
Populate and commit
- Copy reusable framework files into Maison while preserving the newly generated dstack/Copier controls and excluding
old
.git,.beadsruntime/history, private candidates, and migration-only source artifacts. - Copy only manifest-approved private paths into Terroir using the existing overlay layout. Replace public paths with neutral starter files where the approved manifest requires a public example; exclude paths with no safe public form.
- Use several fresh logical commits in each repository. The exact commit boundaries are implementation detail, but the histories must not contain source commits or source author metadata.
- Set Maison’s overlay source to
git@github.com:RobertDeRose/terroir.gitin the local migration checkout only after the private remote is available; do not commit machine-local overlay state.
- Copy reusable framework files into Maison while preserving the newly generated dstack/Copier controls and excluding
old
-
Validate and transition
- Run dstack setup verification, Maison documentation checks, shell/data/Nix/Python/TypeScript checks, privacy scans, overlay fixture checks, and non-activating Maison/Terroir inventory validation.
- Verify remote repository visibility, default branches, commit ancestry, and fresh-history boundaries through GitHub and local Git inspection.
- Verify the new local checkouts and overlay state without performing system activation.
- Only after every required check passes, set
RobertDeRose/nix-configprivate and archived, then record the archive state and final repository URLs in the migration record.
-
Document and preserve
- Update public Maison documentation to describe the two-repository model, the Terroir overlay, Bitwarden boundary, source/archive transition, and recovery path.
- Retain the original private archived repository and local source backup until the user confirms the transition.
Repository boundaries
public RobertDeRose/maison
├── reusable Maison code, tests, Nix modules, mise task machinery
├── neutral inventory/examples and public documentation
├── dstack/Copier project controls and validation tooling
└── no personal/site identity or secret material
private RobertDeRose/terroir
├── inventory.toml and hosts/
├── personal/site config/mise declarations
├── personal/site dotfiles and preferences
├── non-secret trusted configuration
└── no passwords, tokens, or private keys; Bitwarden remains authoritative
private archived RobertDeRose/nix-config
└── complete historical migration source and rollback reference
Architecture Consistency
Existing Patterns Reused
- MAISON-004 overlay discovery, state precedence, typed inventory validation, and mirrored data paths.
- The existing Nix/Lix system and mise user ownership boundary.
- dstack/Copier setup and feature lifecycle for the public Maison repository.
- Existing repository mutation, backup, validation, and authoring-checkout safety contracts.
Invariants Preserved
- Public Maison contains no personal infrastructure identity or trusted private material.
- A configuration path has one owning repository after migration: reusable framework in Maison or personal/site data in Terroir.
- Secrets and private keys are not stored in either repository.
- Old source history remains recoverable in the private archived
nix-configrepository. - No remote archive or source cleanup happens before new-repository validation succeeds.
New Decisions Introduced
- The canonical public repository is
RobertDeRose/maison. - The canonical private overlay repository is
RobertDeRose/terroir. nix-configis a private historical archive, not a future public repository.- New repositories start with fresh logical commits and no imported source history.
- Only Maison uses dstack; Terroir remains a plain data/configuration repository.
- Bitwarden owns secrets and private keys.
- Migration uses an explicit approved candidate manifest.
Architecture Documentation Changes
Update the public Maison architecture page to describe the repository pair, source-of-truth ownership, and archived migration source. Keep the overlay state path local and untracked, and document that Terroir is data/configuration rather than a second framework or command surface.
Operational Considerations
- Operators clone Maison and provide Terroir with
--overlayor the existing overlay source mechanisms. - Terroir changes are ordinary Git authoring changes; Maison validates the active overlay before planning or deployment.
- Bitwarden remains required for any secret or private-key material; migration must report excluded secret candidates without copying their values.
- If repository creation, manifest approval, validation, or remote transition fails, leave all source and staging trees
intact and do not archive
nix-config. - If the new remotes are created but later validation fails, mark the transition incomplete and preserve the repositories for explicit repair rather than deleting them automatically.
- Recovery uses the private archived
nix-config, local staging copies, and the approved manifest; it does not rely on public history.
Documentation Impact
| Documentation concern | Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|---|
| Introduction | README.md | Update | Explain Maison/Terroir repository roles and first clone/bootstrap flow | maison-mol-ieqc.4 |
| Introduction | docs/src/introduction/project-overview.md | Update | State the public framework/private overlay split and Bitwarden boundary | maison-mol-ieqc.4 |
| Architecture | docs/architecture.md | Update | Define public Maison, private Terroir, Bitwarden, and archived nix-config ownership | maison-mol-ieqc.4 |
| Usage / Operations | docs/operations.md | Update | Document fresh setup, local checkouts, overlay selection, and transition recovery | maison-mol-ieqc.4 |
| Recovery | docs/recovery.md | Update | Document archived-source recovery and failed split/remote-transition handling | maison-mol-ieqc.4 |
| Deployment | docs/deployment.md | Update | Align deployment source and overlay repository guidance with the new canonical remotes | maison-mol-ieqc.4 |
| Migration contract | docs/migration-contract.md | Update | Record the two-repository migration boundary and archive sequencing | maison-mol-ieqc.4 |
| Inventory reference | docs/src/reference/inventory.md | Update | Clarify Terroir mirrored layout and secret exclusions | maison-mol-ieqc.4 |
| Development | docs/src/development/tooling.md | Update | Document dstack-managed Maison setup and fresh-repository validation commands | maison-mol-ieqc.4 |
| Navigation | docs/src/SUMMARY.md | Update if needed | Register the design and implemented record; register any new durable page | maison-mol-ako9, maison-mol-ryxl |
| Roadmap | docs/src/planned-features.md | Update | Add MAISON-017 with migration sequencing and dependency | maison-mol-ako9 |
| Feature index | docs/src/features/index.md | Update during close-out | Add the delivered migration record | maison-mol-ryxl |
| Implemented record | docs/src/features/maison-017-maison-terroir-repository-split/index.md | Create during close-out | Preserve migration, remote, validation, and archive evidence | maison-mol-ryxl |
| Terroir README | README.md in RobertDeRose/terroir | Create | State overlay layout, no-secret policy, and Maison relationship | maison-mol-ieqc.3, maison-mol-ieqc.4 |
Validation Strategy
- Candidate manifest audit over current files and Git history, including path classification and private-data scan.
- Explicit user approval recorded before migration actions.
/setup-projectverification for Maison: Copier provenance, dstack files,mainbranch, generated docs, Beads, locked tooling, and no legacy migration utilities.- Fresh-history checks for both repositories: no source
.gitreuse, no imported source commits, and only fresh logical commits. - Public privacy checks over Maison working tree and reachable Git objects for private paths, identities, real endpoints, secret patterns, and private-key markers.
- Terroir content checks against the approved manifest and scans rejecting raw secrets/private keys.
uv run scripts/check-docs.pyandmise -E dev run checkin the new Maison repository.- Existing overlay/inventory contract tests against a local Terroir fixture or checkout without system activation.
git diff --check,shellcheck, and relevant dstack/mise/hk checks.- GitHub remote checks for
RobertDeRose/maisonpublic,RobertDeRose/terroirprivate, both defaulting tomain, andRobertDeRose/nix-configprivate plus archived only after successful validation. - Final local checkout and saved-overlay-state verification; no system activation is required or permitted by this feature.
Implementation Decomposition
- Candidate manifest and privacy audit (
maison-mol-ieqc.1) — inspect current tree/history, classify paths, produce the approved migration manifest, and record excluded secret/private-key candidates. - Maison public scaffold (
maison-mol-ieqc.2) — create the new public repository with/setup-project, dstack controls, stable tooling, and fresh local/remote repository state. - Terroir private scaffold (
maison-mol-ieqc.3) — create the private plain repository, overlay directory layout, README, ignore policy, and fresh local/remote repository state. - Content split and fresh commits (
maison-mol-ieqc.4) — populate both repositories from the approved manifest, configure local checkouts, and create fresh logical histories. - Validation and archive transition (
maison-mol-ieqc.5,maison-mol-ieqc.6) — run all privacy/overlay/dstack/remote checks, then make oldnix-configprivate and archived and record final evidence.
Dependencies and Parallelism
- This feature follows MAISON-016 and depends on its delivered public framework state.
- All implementation children depend on specification reconciliation.
maison-mol-ieqc.2andmaison-mol-ieqc.3may proceed in parallel after specification reconciliation, but neither may populate data before the candidate manifest is approved.maison-mol-ieqc.4depends on the approved manifest and both scaffolds.maison-mol-ieqc.5depends on the completed content split and fresh-history checks.maison-mol-ieqc.6depends on successful validation and is the only task allowed to archivenix-config.- Documentation reconciliation and final validation occur after the new public repository’s reader-facing pages are available.
Rollout and Migration
- Freeze the source checkout and create the candidate manifest.
- Obtain explicit approval of the manifest.
- Create both new repository shells and verify ownership/visibility before copying data.
- Populate fresh local histories and push only after local validation.
- Validate public/private boundaries, overlay loading, and dstack checks.
- Make
nix-configprivate and archive it. - Verify final remotes, local checkouts, overlay source, and documentation.
Risks and Tradeoffs
- Automated classification can miss context, so the manifest approval gate is mandatory.
- New fresh histories lose useful public provenance, but eliminate legacy identity and privacy ambiguity.
- Making
nix-configprivate before remote verification reduces public exposure but must not happen before the new public repository is independently usable; the ordered transition prevents both failure modes. - A private Terroir repository still needs normal access control and Bitwarden discipline; GitHub privacy is not a substitute for secret management.
- dstack setup creates a new project control plane, so only approved reusable content and generated controls may replace the scaffold.
Rejected Alternatives
- Reusing or force-rewriting the existing public
nix-confighistory. - Keeping personal configuration in the public Maison repository with only documentation-level separation.
- Calling the private repository
dotfileswhen it also owns inventory, hosts, package policy, and site configuration. - Making Terroir a second dstack project or framework repository.
- Storing secrets or private keys in Terroir because it is private.
- Automatically deleting the source repository or failed staging repositories during migration.
Open Questions
None required for the planned implementation tasks.
Deferred Decisions
- Exact logical commit boundaries may be selected by the implementation coordinator while preserving fresh-history and audit requirements.
- The exact private overlay source syntax (
ssh, HTTPS, or local path during testing) may follow the existing Maison source-precedence contract; the canonical remote isgit@github.com:RobertDeRose/terroir.git.
Planning Record
Questions Asked and Answers
- Private repository name:
terroir. - Public repository name:
maison. - Public owner and private owner:
RobertDeRose. - Source: current
nix-configcheckout and its Git history. - Migration safety: generate a candidate manifest and require explicit approval before changes.
- New histories: fresh logical commits; do not import source history.
- Existing repository: make
RobertDeRose/nix-configprivate and archive it after successful verification. - Public workflow: initialize
maisonthrough/setup-projectand use dstack. - Private workflow: keep
terroiras a plain private data/configuration repository. - Secret storage: Bitwarden owns secrets and private keys; repositories contain no raw secret/private-key material.
- Local layout: preserve the source checkout and create separate
maisonandterroircheckouts.
Assumptions
- The authenticated
ghaccount isRobertDeRoseand has permission to create both repositories and update/archivenix-config. - The existing
nix-configcheckout is the authoritative migration source and implementation control plane until the manifest is approved; its repository-relative implementation path is.. - The current MAISON-004 overlay layout and validation contracts remain the compatibility target.
- Remote repository names are available; conflicts stop the migration rather than triggering overwrite behavior.
Design Changes During Planning
- The first idea was a single private
dotfilesrepository. It was refined into two new repositories: publicmaisonand privateterroir, while the existingnix-configbecomes a private archive. - The private repository name changed from
dotfilestoterroir. - The split changed from history cleanup of the existing public repository to fresh new histories plus private archival of the old repository.
- The private repository was explicitly kept outside dstack and secrets were explicitly assigned to Bitwarden.
Source Material
docs/src/features/maison-004-private-overlay-configuration/design.mddocs/src/features/maison-004-private-overlay-configuration/index.mdREADME.mddocs/architecture.mddocs/operations.mddocs/recovery.mddocs/migration-contract.mddocs/src/reference/inventory.mddocs/src/planned-features.md- Existing
nix-configtree and Git history
Design — Copier-backed overlay setup and bootstrap onboarding
Historical design boundary: the private-overlay runtime and saved alternate-root behavior described below were superseded by the consumer-root architecture. The maintained
overlay_template/remains supported as a setup-time Copier starter for fresh consumers viabootstrap.sh --setup PATH.
Current supported slice
The retained template generates a consumer flake, inventory, mise policy, and host-registration helper. The explicit
--setup PATH bootstrap renders it into a separate destination, registers the first host, pins the consumer lock from
the working-tree path, and stops for review. After the operator creates the first consumer commit, --consumer PATH
performs normal activation. Copier is not a runtime dependency, and Maison does not save, hide, synchronize,
publish, or author an alternate consumer root. The remainder of this design preserves the original overlay workflow as
historical delivery evidence.
Metadata
- Beads feature root:
maison-mol-5s9 - Feature slug:
maison-overlay-copier-bootstrap - Design path:
docs/src/features/maison-overlay-copier-bootstrap/design.md - Implemented record:
docs/src/features/maison-overlay-copier-bootstrap/index.md - Base branch:
main - Status: delivered
Feature Summary
Make private-overlay creation a first-class Maison workflow. The public overlay_template/ directory becomes a
Copier template, bootstrap accepts the canonical MAISON_OVERLAY environment variable or --overlay, and an
interactive first run can either create an overlay now or install only the Maison command and defer configuration to
the documentation.
User Intent
The user should not have to copy a static example or manually discover how to create the private repository. Maison
should bootstrap its reusable command surface first, then either use an explicitly selected existing overlay or guide
the user through Copier-based creation. A generated overlay should be usable immediately and should register the
current macOS or Linux host through Maison’s existing host:add task rather than duplicating inventory mutation logic.
Goals
- Turn
overlay_template/into a maintained Copier template for private overlays. - Accept
--overlay SOURCEandMAISON_OVERLAY=SOURCEas the supported explicit overlay inputs. - Preserve saved overlay state for subsequent runs and retain the legacy
MAISON_OVERLAY_SOURCEinput as a compatibility fallback without documenting it as canonical. - Prompt interactively when no overlay is selected and offer immediate Copier setup.
- On a negative or non-interactive onboarding response, install Maison and its CLI but skip Nix/system/user activation, then print the exact documentation path for completing overlay setup.
- On immediate setup, render a private Git repository, collect identity values, detect the current supported platform,
and invoke
mise run host:addagainst the generated overlay. - Keep platform detection and inventory mutation in Maison rather than reimplementing host schema updates in the template.
Non-Goals
- Creating or publishing a remote private GitHub repository automatically.
- Storing secrets, private keys, or real infrastructure values in the public template.
- Replacing Maison’s existing inventory schema or
host:addtask. - Running system activation without an explicit or newly generated overlay.
- Supporting Intel macOS or platforms outside Maison’s existing supported systems.
- Making Copier a runtime dependency of every Maison user after setup; it is an ephemeral setup tool used through the
locked Maison
uvruntime.
User-Facing Behavior
Overlay selection
Bootstrap resolves sources in this order:
--overlay SOURCEfor the current invocation;MAISON_OVERLAY=SOURCE;- legacy
MAISON_OVERLAY_SOURCE=SOURCEfor compatibility; - the source saved in
${XDG_STATE_HOME:-$HOME/.local/state}/maison/overlay.toml.
An explicit local Git repository is used directly as the active overlay path. A remote Git URL is cloned or fast-forward
updated at ${XDG_DATA_HOME:-$HOME/.local/share}/maison/overlay, preserving the existing owner-only state file.
First-run onboarding
When no source is available:
- An interactive terminal asks whether to set up a private overlay now, with yes as the default.
- Yes installs the minimal Copier runner, renders
overlay_template/into a new local overlay selected by the consumer repository, seeds the inventory username fromid -un, initializes its Git repository, and runs the template task that registers the current host through Maison. Remote sources, including sources restored from saved state, use the same default clone path. - No installs Maison’s verified mise runtime and
maisonCLI, prints the overlay setup documentation URL/path, and exits successfully without installing Nix or applying system/user state. - A non-interactive run behaves like No unless
MAISON_REQUIRE_OVERLAY=true, in which case it fails with an actionable missing-overlay error.
After an overlay is supplied or generated, bootstrap stores it, installs missing Nix/Lix as before, and continues with Maison’s existing system and user convergence flow.
Copier template
The template asks for the private overlay’s inventory user identity and renders a valid inventory with no host entries.
Bootstrap supplies the current effective username with Copier’s --data; manual Copier use can do the same with
--data "username=$(id -un)". Its first-copy task runs only during copier copy, not copier update. The task:
- initializes the destination Git repository when needed;
- uses the current short hostname unless bootstrap supplied
--host; - exports the destination as
MAISON_OVERLAY_PATH; - invokes
mise -C <Maison checkout> run host:addwith the generated inventory user, leaving system detection and validation to Maison.
The template task fails clearly when it was invoked without a Maison checkout path, so manual Copier use documents the
required MAISON_HOME environment variable.
Requirements
Functional Requirements
- Bootstrap help documents
MAISON_OVERLAY,--overlay, the onboarding prompt, and the no-activation path. scripts/maison_overlay.pyexposes the canonical environment precedence and supports direct local Git repositories.- Bootstrap installs the Maison CLI before any optional overlay setup and never runs system/user activation on the no path.
- Immediate setup uses
uvx copierthrough a Maison-manageduvinstallation and trusts only the checked-out local template source. - The template contains
copier.yml, generated inventory content, empty policy stubs, private-dotfile guidance, and a first-copy host-registration task. - Host registration delegates to
mise run host:addand therefore preserves repository mutation locks, schema validation, and platform support checks. - Existing explicit overlays continue to use the existing bootstrap and state persistence path.
Quality Requirements
- Add behavior-first tests for source precedence, prompt/no-prompt branching, no-activation behavior, Copier command construction, username seeding, template rendering, dotfile guidance, and host-task delegation.
- Do not run real Nix activation, package installation, or remote GitHub operations in tests.
- Keep user-facing errors actionable and avoid shell evaluation of untrusted overlay paths or identity values.
- Preserve private-overlay ownership and public-privacy checks.
Compatibility and Migration Requirements
- Existing saved
overlay.tomlfiles remain readable. - Existing
MAISON_OVERLAY_SOURCEusers continue to work, but new docs useMAISON_OVERLAY. - Existing
--overlayremote URLs continue to clone/update into the standard XDG data path. - Existing
host:addbehavior remains unchanged for manually maintained overlays. - The static-copy instructions are replaced by Copier instructions, while generated repositories retain the mirrored overlay layout.
Existing Context
Maison already has a tested overlay state helper in scripts/maison_overlay.py, a shell adapter in
.mise/lib/overlay.sh, a typed inventory validator, and mise run host:add. The current bootstrap installs mise,
Nix/Lix, and the Maison CLI before handing control to the bootstrap task, but it requires an overlay only when
MAISON_REQUIRE_OVERLAY is set and otherwise silently uses public starter data. overlay_template/ is the maintained
Copier source and contains the metadata and first-copy host-generation behavior described below.
MAISON-004 established the overlay source/state contract and MAISON-017 separated public Maison from private Terroir. This feature extends those contracts without moving private data back into Maison.
Proposed Design
Bootstrap phases
Refactor bootstrap.sh into explicit phases while retaining its current prerequisite guards:
- resolve or clone a Maison checkout;
- install verified mise and link the Maison CLI;
- resolve an explicit/saved overlay, or run interactive onboarding;
- if onboarding is declined, print next steps and exit before Nix/system/user work;
- if onboarding succeeds, prepare and persist the overlay;
- install/verify Nix/Lix and execute the existing bootstrap task with the active overlay.
The overlay helper owns source parsing, local-path handling, state writes, and remote clone/update behavior. Bootstrap owns prompts, Copier invocation, CLI installation, and phase selection.
Copier template contract
overlay_template/copier.yml defines user questions for the private inventory identity and a first-copy task. Template
files use Copier’s normal Jinja rendering; inventory.toml.jinja renders a user-only inventory, while empty mise
policies and dotfile guidance remain neutral. The task script is a private-repository file and calls Maison’s public
host:add command with environment-provided MAISON_HOME, MAISON_OVERLAY_PATH, and optional MAISON_HOST.
The bootstrap path invokes Copier with the local template source and environment variables rather than a remote template,
so the setup is tied to the exact Maison checkout the user just selected. It passes the effective user with Copier’s
--data option. uvx copier is run only after mise install uv. The generated repository is initialized before the host
mutation; committing and publishing it remain user decisions.
State and ownership
The overlay state file continues to contain only the source and active path and remains mode 0600. A direct local
source records that path as active; a remote source records the standard clone path. Maison remains the framework owner,
the generated repository owns user/site configuration, and Bitwarden remains the secret source of truth.
Architecture Consistency
Existing Patterns Reused
- Python stdlib state parsing and validation in
scripts/maison_overlay.py. - mise tasks as the public command surface.
host:addas the single inventory mutation implementation.- XDG state/data locations and owner-only state permissions.
- Verified artifact installation before bootstrap handoff.
Invariants Preserved
- A file, package, service, or preference has one owner.
- Public Maison contains no personal identities or secret material.
- System activation is never attempted without an active inventory overlay on first setup.
- Host registration accepts only the supported
aarch64-darwin,aarch64-linux, andx86_64-linuxsystems through Maison’s sharedhost:addvalidation. - Repository mutations are locked, validated, and recoverable.
New Decisions Introduced
MAISON_OVERLAYis the canonical environment variable;MAISON_OVERLAY_SOURCEis compatibility-only.- A declined first-run overlay setup is a successful CLI-only bootstrap, not a failed setup and not an activation of neutral starter state.
overlay_template/is a Copier template, and its first-copy task delegates host creation to Maison.
Architecture Documentation Changes
Update reader-facing architecture/operations content to describe bootstrap phases, source precedence, Copier ownership, and the no-overlay safety boundary.
Operational Considerations
Immediate setup requires network access to install uv/Copier and the user’s normal Git credentials for later remote
publishing. Declined setup is recoverable by following the printed Copier command and rerunning bootstrap with
--overlay or MAISON_OVERLAY. Updating an existing generated overlay uses copier update; it does not rerun host
registration automatically.
Documentation Impact
| Documentation concern | Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|---|
| Introduction / usage | README.md | Update | Replace copy instructions with Copier and explain explicit/interactive setup | maison-mol-xke.3 |
| Operations | docs/operations.md | Update | Document bootstrap phases, prompt outcomes, and recovery | maison-mol-xke.3 |
| Host authoring | docs/add-a-host.md | Update | Explain generated host registration and manual host:add | maison-mol-xke.3 |
| Tooling reference | docs/src/reference/tooling.md | Update | Document Copier/uvx setup and template update rules | maison-mol-xke.3 |
| Template | overlay_template/README.md | Update | Make the template’s Copier contract and private boundary explicit | maison-mol-xke.2 |
| Feature navigation | docs/src/SUMMARY.md | Update | Register this design and its delivered record | maison-mol-xke.3 |
| Roadmap | docs/src/planned-features.md | Update | Add the planned feature and dependency/status entry | maison-mol-xke.3 |
| Implemented Feature Record | docs/src/features/maison-overlay-copier-bootstrap/index.md | Create during close-out | Preserve delivery and audit history | Close-out |
Validation Strategy
- Unit/behavior tests for overlay source precedence, direct-path state, prompt outcomes, no-activation exit, and
bootstrap command handoff using fake
mise,nix,uvx, and Copier executables. - Render the Copier template in temporary directories with deterministic answers and verify inventory parsing.
- Run the generated first-copy task against a fake Maison
miseexecutable and verifyhost:addreceives the current system, hostname, user, and overlay path. - Validate all generated TOML and public privacy boundaries.
- Run
uv run scripts/check-docs.py,mdbook build docs,mise x -- hk check, andmise run check. - Do not perform system activation, publish a remote, or modify the user’s real overlay during tests.
Implementation Decomposition
maison-mol-xke.1— source precedence, prompt branches, CLI-only no path, Copier invocation, and compatibility tests.maison-mol-xke.2— template metadata, generated inventory, policy/dotfile guidance, host detection, and first-copy delegation tests.maison-mol-xke.3— README, operations, host authoring, tooling reference, template guide, roadmap, and navigation.
Dependencies and Parallelism
Template implementation depends on the bootstrap contract for environment names and invocation. Documentation depends on both code slices. Tests remain with the slice that owns the behavior; full validation follows both implementation slices.
Rollout and Migration
Existing explicit overlays continue to work. Users without an overlay see the new prompt on the next bootstrap. Users who decline can create an overlay later with Copier and rerun bootstrap. Existing state files and remote clone locations remain compatible.
Risks and Tradeoffs
- Adding Copier setup increases first-run dependencies, mitigated by using ephemeral
uvxand keeping the no path available. - A generated host mutation can fail after files render, mitigated by running it before the initial commit and printing the destination for repair.
- Direct local overlays alter the old clone-to-XDG behavior, but make
--overlay /path/to/repomatch its documented meaning and preserve remote URL cloning. - Template task execution is intentionally limited to first copy to avoid duplicate-host failures during updates.
Rejected Alternatives
- Keeping manual
cp -Ras the primary setup path: it does not collect identity values or perform host registration. - Duplicating inventory mutation in the template: it would bypass Maison’s validation and transaction safeguards.
- Silently continuing into neutral system activation when no overlay is supplied: it makes first-run ownership ambiguous and can apply unintended public starter state.
- Automatically creating a remote GitHub repository: it would require provider credentials and violate repository-choice boundaries.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- When no overlay is supplied, answering yes runs Copier-based overlay setup immediately; answering no only installs Maison/the CLI and points to the overlay documentation. Confirmed by the user.
Assumptions
- The current hostname is the generated host name unless the bootstrap
--hostoption overrides it. - The later consumer-repository boundary supersedes Copier-created overlay destinations;
MAISON_CONSUMER_ROOTidentifies the authoritative configuration root. MAISON_REQUIRE_CONSUMER=trueis the automation/CI escape hatch for missing consumer input.
Design Changes During Planning
- Replaced the static example-copy model with a Copier template and delegated host registration to the existing Maison task after the user requested automatic platform detection.
Source Material
- Current
bootstrap.sh,scripts/maison_overlay.py,.mise/tasks/host/add, and overlay contract tests. - MAISON-004 private overlay design and MAISON-017 repository split design.
- User request and clarification in the current planning conversation.
Design — Overlay authoring lifecycle and command surface
Metadata
- Beads feature root:
maison-mol-3jb - Feature slug:
maison-overlay-authoring-lifecycle - Design path:
docs/src/features/maison-overlay-authoring-lifecycle/design.md - Implemented record:
docs/src/features/maison-overlay-authoring-lifecycle/index.md - Base branch:
main - Status: delivered
Feature Summary
Make the private overlay a safe, inspectable, and publishable authoring repository. Maison will expose top-level
status and publish commands, refresh the overlay before software mutations, and create focused Git commits for
successful tool, package, and application additions or removals. The existing help output will flatten the workflow
commands without implying a nonexistent workflow command.
User Intent
Maison’s workflow commands are top-level commands, not children of a workflow command. The help output should present
those commands directly while preserving the existing grouped help for github, app, package, tool, host,
system, user, and docs.
The private overlay should also behave like a small authoring repository: the operator can inspect whether it is clean, published, ahead, behind, diverged, or offline; explicitly publish committed changes; and trust add/remove commands to refresh from the remote and commit only their successful declaration changes. The public Maison checkout must never be the fallback target for these personal software mutations.
Goals
- Flatten only the
workflowheading in Maison’s top-level help and addstatusandpublishto the top-level command surface. - Add overlay-only
maison statusandmaison publishcommands using the active saved/environment-selected overlay. - Fetch remote refs for status when possible and clearly report when remote comparison is unavailable or based on the last known tracking ref.
- Refresh the active overlay with a fast-forward-only pull before
tool,package, andappadd/remove mutations. - Automatically commit successful add/remove declarations with focused, predictable messages.
- Preserve unrelated changes, staged state, untracked files, and ignored files without accidentally committing or deleting them.
- Keep publication explicit; add/remove commands never push to a remote.
Non-Goals
- Automatically publishing, creating, or configuring a remote overlay repository.
- Running the full
maison syncworkflow before authoring a mutation; that command still pulls both repositories and applies configuration. - Automatically committing arbitrary overlay edits or changes in the public Maison checkout.
- Automatically rolling back installed packages or applications when Git commit creation fails.
- Changing
host:add, update commands, inventory ownership, or the public starter configuration behavior outside the software add/remove commands covered here. - Adding a separate overlay argument to
statusorpublish; both use the active resolved overlay.
User-Facing Behavior
Help output
The help heading remains:
Available commands and their subcommands:
apply Apply the Nix system layer, then converge the mise user layer
bootstrap Install/repair Nix, apply system configuration, and converge user state
deploy Deploy Linux system state, then transactionally converge its mise user environment
doctor Diagnose the Nix system and mise user layers without changing them
fix Apply deterministic repository fixes
plan Preview the Nix system layer, then the mise user layer
rollback Roll back only the Nix system layer
status Show the active overlay and remote status
sync Pull Maison and your private overlay, then apply both layers
update Update Nix inputs transactionally
github:
auth Configure GitHub authentication for Maison, mise, and Nix
Only the workflow: label is removed. Existing non-workflow groups remain in their current order and format.
maison status
status inspects the active overlay resolved from existing overlay state/environment behavior. It accepts no separate
overlay path. It reports the overlay path, branch/upstream, worktree state, and the relationship to the upstream:
- clean and in sync;
- uncommitted tracked or untracked changes;
- committed changes ahead of the remote;
- remote changes ahead of the checkout;
- diverged history; or
- no configured upstream/remote.
The command fetches the configured remote when possible. If fetching is unavailable because the device is offline or credentials/network access fail, it reports that the remote comparison is unavailable and identifies the last-known tracking state instead of claiming that the repository is in sync. Any inspectable state is reported with exit status zero; no valid active overlay or repository is an error.
maison publish
publish uses the active overlay’s configured upstream branch and never chooses a remote or branch implicitly. It:
- fetches the upstream before changing the worktree;
- fails without stashing if the upstream is unavailable, no upstream is configured, or local and remote history is not a fast-forward-publishable relationship;
- temporarily stashes tracked and untracked changes, but never ignored files;
- pushes already-committed local changes;
- restores the stash after a successful or failed push; and
- leaves the stash intact and returns non-zero if restoration conflicts.
It does not create a commit for arbitrary local edits. A successful no-op when there are no commits to push is allowed and is reported clearly.
Configuration scope
The authoring lifecycle preserves the existing configuration scope. Common tool and package declarations belong in
config/mise/config.toml; platform-specific resolution remains in the corresponding mise lock/config files. The
--macos package option selects config/mise/config.macos-arm64.toml, while application commands use their existing
Apple Silicon application config. Inventory profiles (base, dev, mac, and linux) select Nix modules; Maison does
not add a separate mise profile selector or a profile argument to these authoring commands.
Add/remove mutation lifecycle
tool:add, tool:remove, package:add, package:remove, app:add, and app:remove require an active private overlay
and a Git authoring checkout. They fail instead of falling back to public config/mise files. The target overlay must
have a configured upstream that can be fast-forward-pulled; an unavailable remote, missing upstream, or diverged history
stops the command before the declaration is edited.
After acquiring the existing repository mutation lock, the command stashes tracked and untracked changes (never ignored files), pulls the overlay with fast-forward-only semantics, and restores the stash. A restoration conflict aborts the mutation. Any transaction target file that already contains local changes after restoration is rejected; unrelated dirty files in other paths remain allowed and are not included in the generated commit.
The existing candidate-install/validate/replace transaction remains authoritative. Its journal is completed before Git commit creation so a commit identity, hook, or subprocess failure is a post-transaction error: validated declaration changes remain in place and the command prints manual recovery guidance. Only after the transaction completes does Maison create a Git commit containing the changed declaration and any generated lockfile. Generated subjects use literal backticks and the effective identifier:
added(tool): `github:owner/tool@version`
removed(package): `brew:git`
added(app): `ghostty`
The package category is used for package --macos; the tool/package/app command’s effective identifier is used,
including its manager/cask and requested version where applicable. A Git commit failure leaves the validated declaration
changes in place, reports the manual-commit recovery path, and does not pretend that installation side effects were
rolled back. Add/remove commands never push these commits.
Requirements
Functional Requirements
- The CLI registers top-level
statusandpublishtasks and dispatches their task-specific help normally. - Help renders workflow tasks without a
workflow:heading while retaining all other current groups. - Status uses only the active saved/environment-selected overlay and does not inspect an arbitrary path.
- Status attempts a remote fetch, reports last-known comparison state when the fetch is unavailable, and distinguishes dirty, ahead, behind, diverged, in-sync, and no-upstream states.
- Publish uses the configured upstream, fetches before stashing, refuses non-publishable history, preserves tracked and untracked local changes, leaves ignored files untouched, and reports stash restoration conflicts without dropping the stash.
- Covered software add/remove commands require a private overlay, refresh it before editing, reject target files that
already contain local changes, and never invoke full
maison syncas their refresh path. - Successful covered mutations commit only operation target paths after the existing transaction succeeds, with the
exact
added(scope): \identifier`orremoved(scope): `identifier`` subject contract. - Commit failures preserve the successful file mutation and provide actionable recovery output.
Quality Requirements
- Reuse the existing XDG overlay state, authoring-checkout guard, fail-fast repository lock, mutation journals, and candidate validation instead of creating parallel ownership or transaction systems.
- Use argument arrays or equivalent safe subprocess APIs for Git paths, refs, and commit identifiers; do not evaluate overlay paths, branch names, or package names as shell code.
- Preserve unrelated worktree/index state and never silently delete an untracked or ignored file.
- Keep status/publish and pure overlay Git behavior independently testable without Pi, TUI, Nix activation, package installation, or network access to a real remote.
- Provide actionable diagnostics for missing overlays, missing upstreams, stale/offline status, divergent history, failed pulls/pushes, target-file dirtiness, commit-hook/identity failures, and stash conflicts.
Compatibility and Migration Requirements
- Existing overlay source precedence and saved overlay state remain unchanged.
- Existing
maison synccontinues to pull Maison and the overlay and then apply both layers. - Existing user-facing command names, including
maison user status, remain available. - Public Maison’s neutral fallback files remain available to read-only/apply workflows, but covered software authoring commands no longer mutate them when no private overlay is active.
- Existing transaction rollback/journal behavior remains in force for candidate edits and lockfile replacements.
Existing Context
The CLI in bin/maison discovers executable .mise/tasks and formats tasks into a workflow group plus other
namespace groups. scripts/maison_overlay.py owns overlay source precedence, XDG state, local paths, and remote clone
updates. .mise/lib/overlay.sh exports the active overlay and currently falls back to the public repository when no
overlay is present.
The software authoring tasks already use .mise/lib/transaction.sh, .mise/lib/repository_mutation.py, parser-backed
TOML edits, candidate validation, a repository lock, and mutation journals. tool:add/remove, package:add/remove,
and app:add/remove currently replace files transactionally but do not create Git commits. sync currently performs
full repository pulls followed by apply, so it is not a safe pre-authoring refresh primitive.
The completed overlay bootstrap feature established the private overlay boundary, direct local Git paths, the XDG saved-state contract, and the Copier-generated overlay layout. This feature extends that boundary for authoring without moving private data or secrets into public Maison.
Proposed Design
Shared overlay Git operations
Extend or factor the existing stdlib overlay helper so shell tasks can invoke one consistent, pure/testable implementation for:
- resolving and requiring an active private Git overlay without changing the public fallback used by read-only or convergence workflows;
- fetching/updating status against the configured upstream and distinguishing fresh versus last-known comparison;
- temporary stash/pull/restore handling with tracked plus untracked files and no ignored files;
- fast-forward/upstream relationship checks before any publish stash;
- focused commit creation using an explicit path list and a temporary Git index or equivalent path-limited mechanism that does not commit unrelated staged or worktree changes; and
- returning structured outcomes/errors so shell tasks own only user-facing wording.
All Git subprocesses receive paths and refs as separate arguments. The helper must not invoke full maison sync, and it
must leave the existing repository mutation journal/lock as the transaction boundary for file edits.
Mutation integration
The covered mutation tasks continue to acquire the target repository lock before reading mutable files. Once inside the lock, they require an active overlay, refresh it with the shared helper, verify target paths are clean, and then execute the existing candidate transaction. The task records the exact declaration/lock paths it changed and marks the transaction successful and complete before invoking the focused commit helper. A commit error is a post-transaction failure with preserved files, not a request to reverse external installation effects. The helper receives explicit target paths and the effective identifier so each task does not duplicate Git state handling.
The target cleanliness check is path-specific: unrelated files may remain dirty, but a pre-existing change in a config or lockfile that the operation would commit is rejected. The temporary-index commit preserves unrelated staged files and only records the operation paths.
Command tasks and help
Add .mise/tasks/status and .mise/tasks/publish as top-level tasks. status remains inspection-oriented and does not
change declarations. publish takes the overlay repository lock because stash/push/restore changes repository state;
it does not invoke configuration convergence. Update bin/maison’s workflow formatter to print workflow rows directly,
then leave existing non-workflow grouping logic intact.
Architecture Consistency
Existing Patterns Reused
scripts/maison_overlay.pyand.mise/lib/overlay.shfor active-overlay resolution and XDG state..mise/lib/repository_mutation.pyfor authoring checks, lock ownership, recovery, and private state directories..mise/lib/transaction.shfor candidate replacement, journals, rollback, and signal handling.- Parser-backed
config_edit.pyedits and existing mutation task boundaries. bin/maison’s task discovery and generated task-specific help.
Invariants Preserved
- Public Maison owns reusable framework and neutral policy; private overlay owns personal/site software declarations.
- A covered add/remove command never writes public fallback configuration.
- Successful external installation is followed by a validated declaration and focused Git commit; failed candidate transactions never create the commit.
- Unrelated local work and ignored files are not committed or deleted.
- Publishing is explicit and never occurs as a side effect of mutation.
- System/user activation and the Nix ownership boundary are unchanged.
New Decisions Introduced
maison statusandmaison publishare top-level commands using the active overlay only.- Remote refresh is required before covered add/remove authoring; offline or divergent overlays fail safe.
- Auto-generated mutation subjects are
added(scope): \identifier`andremoved(scope): `identifier``. - Commit failure preserves the already-successful declaration mutation and reports manual recovery.
- Status may fetch; when it cannot, it reports that the comparison is last-known rather than claiming synchronization.
Architecture Documentation Changes
Update architecture and operations/reference pages to describe the overlay authoring boundary, refresh/publish lifecycle, focused commits, no-public-fallback rule, stale/offline status wording, and commit-failure recovery.
Operational Considerations
Operators should run maison status before authoring or maison publish when they need to inspect the overlay. Software
add/remove commands may contact the configured remote and can fail offline by design. Local unrelated work is preserved
through the refresh; target-file edits must be committed or stashed manually before retrying.
maison publish does not publish uncommitted edits. If a push fails, the temporary stash is restored. If restoration
conflicts, the stash remains available for explicit recovery. A failed auto-commit leaves the declaration changes in the
overlay, so operators should inspect maison status and create the documented commit manually.
Documentation Impact
| Documentation concern | Exact page | Create or update | Planned change | Owning Beads task |
|---|---|---|---|---|
| Introduction / command surface | README.md | Update | Add top-level status/publish usage, overlay-only authoring, commit/publish lifecycle, exact help semantics, and the preserved configuration/profile boundary | maison-mol-0tm.3 |
| Architecture | docs/architecture.md | Update | Document overlay authoring ownership, remote refresh, focused commits, and no-public-fallback invariant | maison-mol-0tm.2 |
| Operations | docs/operations.md | Update | Document status states, offline reporting, publish stash/push/restore behavior, and recovery | maison-mol-0tm.1 |
| Reference | docs/task-reference.md | Update | Add status/publish contracts and revised software mutation guarantees | maison-mol-0tm.3 |
| Reference | docs/src/reference/tooling.md | Update | Document overlay status/publish commands, mutation commit behavior, and configuration/profile scope | maison-mol-0tm.3 |
| Package/application authoring | docs/package-policy.md, docs/add-a-tool.md, docs/add-an-app.md | Update | Explain overlay requirement, pre-mutation refresh, target cleanliness, and generated commit subjects | maison-mol-0tm.2 |
| Navigation | docs/src/SUMMARY.md | Update | Register this feature design; no new reader page is required | Planning/close-out |
| Roadmap | docs/src/planned-features.md | Update | Add this feature with dependency/status narrative | maison-mol-0tm.3 |
| Implemented feature record | docs/src/features/maison-overlay-authoring-lifecycle/index.md | Create during close-out | Preserve delivery and audit evidence | Close-out |
No new durable reader-facing page is required; existing operations, architecture, tooling, and authoring pages own these questions.
Validation Strategy
- Add behavior-first tests for CLI help flattening, task discovery, status states, offline/last-known reporting, publish upstream checks, stash preservation, push failure restoration, and restoration-conflict safety.
- Use temporary local Git repositories/bare remotes and controlled subprocesses; do not access a real remote or run real package installation, Nix activation, or system convergence.
- Add mutation tests for missing overlay refusal, pre-mutation fast-forward pull, offline/divergent refusal, target-file dirtiness, preservation of unrelated files/index entries, exact add/remove commit subjects, lockfile inclusion, commit failure preservation, and no commit after candidate/installation failure.
- Run focused Python unittest modules, shell syntax/static checks, documentation validation, mdBook, and the full repository check after implementation and review fixes stabilize.
Implementation Decomposition
- Overlay Git lifecycle and commands — add shared status/publish/refresh/stash/upstream behavior and focused tests.
- Transactional overlay mutation commits — require active overlays, integrate refresh and target checks, and commit successful tool/package/app add/remove operations with focused tests.
- Command surface and reader documentation — flatten help, register top-level commands, update exact reader pages, roadmap, and contract tests.
Dependencies and Parallelism
All implementation tasks depend on specification reconciliation. The mutation task depends on the shared Git lifecycle contract/task because it uses refresh, stash, upstream, and focused-commit primitives. Command-surface documentation and help tests depend on the final command behavior and mutation contract. Full validation follows all three slices.
Rollout and Migration
The feature is additive for status/publish and changes only covered software authoring: users must have an active private overlay and a reachable fast-forwardable upstream before add/remove. Existing public fallback files remain usable for read-only and convergence behavior. Existing manually authored overlay changes are not auto-committed; only future successful covered add/remove operations receive generated commits.
Risks and Tradeoffs
- Requiring remote refresh makes authoring unavailable offline, but prevents new commits from being based on stale history; a future explicit offline mode can be designed separately.
- Temporary stashing is more complex than refusing all dirty worktrees, but permits unrelated work while protecting target files and untracked content.
- Preserving successful file changes after a Git commit failure can leave an uncommitted overlay, but reversing package and application side effects would be less safe and is outside the transaction boundary.
- A temporary-index/path-limited commit needs careful tests to avoid including unrelated staged work; target-file cleanliness is an additional safety precondition.
Rejected Alternatives
- Keeping a visible
workflow:help group when noworkflowcommand exists. - Calling full
maison syncbefore authoring mutations, because it applies configuration and has a broader side effect. - Committing arbitrary dirty overlay changes during
publish. - Silently mutating public Maison when no private overlay is active.
- Proceeding offline or through divergent history before a generated commit.
- Rolling back installed package/application side effects when Git commit creation fails.
Open Questions
None.
Deferred Decisions
None.
Planning Record
Questions Asked and Answers
- Automatic mutation commits may coexist with unrelated local changes, but only operation target paths may be committed.
maison publishpushes existing commits, temporarily stashing tracked and untracked changes and restoring them; ignored files remain untouched.- Covered add/remove operations fast-forward-pull only the active overlay before editing; they do not invoke full
maison sync. - Missing remotes, unreachable remotes, and divergent histories fail safely before authoring edits or publication.
maison statusfetches when possible and reports last-known comparison state when offline/unavailable; inspectable states exit zero.publishuses the configured upstream and refuses implicit remote/branch selection.- Git commit failure preserves the successful mutation and reports recovery rather than rolling back installed effects.
- Only an active private overlay may receive these software mutations; public Maison is never the fallback target.
- Target config/lock files must be clean before a covered mutation; unrelated dirty paths are allowed.
- Commit subjects use literal backticks and the form
added(scope): \identifier`orremoved(scope): `identifier``. - Only the
workflow:help heading is removed; all other groups remain as-is.
Assumptions
- The active overlay is a Git authoring checkout with a configured upstream branch for covered add/remove operations.
- Existing generated overlays contain a usable initial commit and the normal overlay config/lock paths.
- Git’s configured identity and hooks are the operator’s responsibility; hook/identity failures are surfaced as post- transaction commit failures.
- “Effective identifier” means the command’s manager-qualified package/cask/tool argument, including a requested tool version when the command supplied one.
Design Changes During Planning
- The initial authoring behavior allowed public fallback files; the user explicitly changed covered software mutation to require an active private overlay.
- The initial publish concept became a fetch-first, upstream-only push with temporary stash preservation.
- Full
syncwas rejected as the pre-mutation refresh because it also applies system/user state. - Same-file unrelated edits were made a refusal condition after distinguishing path-level commit safety from arbitrary worktree preservation.
Source Material
bin/maisonscripts/maison_overlay.py.mise/lib/overlay.sh.mise/lib/transaction.sh.mise/lib/repository_mutation.py.mise/tasks/tool/{add,remove}.mise/tasks/package/{add,remove}.mise/tasks/app/{add,remove}.mise/tasks/synctests/test_repository_contracts.pytests/test_repository_mutation.pytests/test_transaction_behavior.pyREADME.mddocs/architecture.mddocs/operations.mddocs/task-reference.mddocs/package-policy.mddocs/add-a-tool.mddocs/add-an-app.mddocs/src/reference/tooling.mddocs/src/features/maison-overlay-copier-bootstrap/design.md
Implemented features
This index contains features that have completed implementation, documentation reconciliation, validation, and delivery. Planned or partially implemented work remains in Planned features and Beads.
Feature index
- MAISON-001: Root-owned deployment transaction state
- MAISON-002: Revision-bound commit and rollback
- MAISON-003: Restricted deployment privilege model
- MAISON-004: Private configuration split (superseded)
- MAISON-005: Verified bootstrap artifacts
- MAISON-006: Review-gated dependency updates
- MAISON-007: Parser-backed TOML mutations
- MAISON-008: Repository mutation locking and journals
- MAISON-009: Authoring checkout guard
- MAISON-010: Shared inventory schema validation
- MAISON-011: Bounded deterministic test suite
- MAISON-012: Plan/apply semantic parity
- MAISON-013: Exact dotfile backup manifests
- MAISON-014: Remote convergence restoration
- MAISON-015: Linux runtime activation verification
- MAISON-016: Pi TypeScript validation boundary
- MAISON-017: Maison/Terroir repository split
- Copier fresh consumer bootstrap
- Retired alternate repository command surface
MAISON-001: Root-owned deployment transaction state
Delivery Summary
- Beads feature root:
maison-mol-6y0 - Status: delivered
- Pull request: not used for merge-mode delivery
- Merge commit: fast-forward delivery; final target SHA is recorded in the Beads delivery record
- Design record: design.md
Delivered Capability
Remote repository deployment now keeps privileged transaction state outside the managed user’s home. Maison stages committed repository archives through a stdlib Python transaction manager that creates a root-owned same-filesystem transaction namespace, unpredictable transaction IDs, a single lock, an active state file, a journal, staging storage, and rollback storage.
The public deploy command remains maison deploy <host> / mise run deploy <host>. The shell task streams the Python
helper to the remote host, stages the repository archive, runs user convergence, then commits or rolls back the
repository transaction.
User-Facing Behavior
- Safe default transaction root for
/home/<managed-user>/.maisonis/home/.maison-deploy/transactions/<managed-user>/<repo-hash>/. - Deployment fails clearly if transaction state is under the managed home, crosses filesystems, uses unsafe symlinks, has unexpected ownership, or is group/world writable.
- Deployment fails clearly for unsupported or tampered archives, including missing required repository files, invalid
revision stamps,
.gitdirectories, path traversal, and symlink or special-file archive members. - Failed user convergence rolls back from the root-owned transaction state. If rollback state is missing or unsafe,
Maison leaves the active repository in place and preserves
active.jsonfor operator inspection.
Design Integration
The implementation preserves Maison’s two-layer architecture: deploy-rs still owns Linux system profile deployment and rollback, while Maison owns repository staging and mise user convergence. The privileged repository transaction boundary is now explicit: journals, locks, staging trees, rollback trees, and active state are root-owned state outside the managed user’s writable home.
MAISON-001 intentionally provides the root-owned transaction and journal foundation. Revision-bound startup recovery, fsync-complete non-destructive transitions, and richer inspect/abort command surfaces remain assigned to MAISON-002.
Operational Impact
Operators inspect incomplete repository transactions under the root-owned transaction root rather than next to the managed repository. The durable state files are:
transaction.lockfor serialization;active.jsonfor the current incomplete transaction;<transaction-id>/journal.jsonlfor journal events;<transaction-id>/staging/repositoryfor extracted staged content before install;<transaction-id>/rollback/repositoryfor the previous repository when one existed.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_deploy_transaction— passed for transaction path contract tests.python3 -m unittest -v tests.test_deploy_transaction tests.test_topology.DeploymentContractTest— passed for transaction contract and deployment behavior tests.uv run scripts/check-docs.py— passed after documentation changes.mise -E dev run check— passed after final implementation changes.
Design Reconciliation
Delivered as Designed
- Root-owned, same-filesystem transaction state replaces managed-home sidecars.
- Transaction IDs are unpredictable and path-safe.
- Transaction roots reject managed-home placement, symlinks, wrong owners, unsafe modes, and cross-filesystem roots.
- Repository deployment uses a single lock, journal, active state record, staging tree, and rollback tree.
- Stateful deployment behavior moved from shell into tested Python stdlib code.
Intentional Changes
- The delivered command surface exposes
stageandfinalize;abortandinspect-incompleteremain documented as MAISON-002 follow-up surface because MAISON-001’s reviewed scope is the transaction root and journal foundation. - A validation-blocking tooling scaffold defect was fixed in
maison-0lpvon this branch: unsupported Intel macOS references were removed from tooling docs and locks so the full repository suite could pass.
Deferred Work
- MAISON-002 owns revision-bound startup recovery and stronger non-destructive commit/rollback semantics.
- MAISON-003 owns restricting remote deployment privileges beyond the current root-owned state boundary.
Rejected or Removed Scope
- Legacy
<repo>.next.<pid>,<repo>.previous, and<repo>.deploy-statesidecars are no longer supported behavior. - Maison does not fall back to managed-user-controlled transaction state when root-owned same-filesystem state is unavailable.
Documentation Updated
README.mddocs/architecture.mddocs/deployment.mddocs/migration-contract.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-001-root-owned-deployment-transactions/design.mddocs/src/features/maison-001-root-owned-deployment-transactions/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation:
e1f5c8a(docs(features): reconcile MAISON-001 specification). - Validation blocker chore:
48a781a(removed unsupported Intel macOS scaffold targets, Beadsmaison-0lpv). - Contract and documentation task:
ae664b3(test(deploy): define root-owned transaction contract, Beadsmaison-mol-xjt.1). - Engine implementation task:
b0ccaf7(feat(deploy): use root-owned repository transactions, Beadsmaison-mol-xjt.2). - Implementation coordinator
maison-mol-xjtclosed after both implementation children and final validation passed.
MAISON-002: Revision-bound commit and rollback
Delivery Summary
- Beads feature root:
maison-mol-quq - Status: delivered
- Pull request: not used for merge-mode delivery
- Merge commit: fast-forward delivery; final target SHA is recorded in the Beads delivery record
- Design record: design.md
Delivered Capability
Deployment repository transactions now verify revision expectations during finalize and recovery to make repository state transitions rollback-safe and restart-recoverable.
The root-owned helper now:
- records expected old/new revisions and destination path before finalize,
- verifies those revisions before commit or rollback,
- performs rollback using a temporary rescue path instead of deleting the live repository first,
- and executes startup recovery of incomplete transactions before continuing with user convergence.
User-Facing Behavior
- Each staged repository transaction writes expected old/new revision metadata into
active.jsonand journal events. - Deployment rejects commit when the active repository revision has drifted from the expected prior revision after staging.
- Deployment rejects finalization when the candidate revision does not match the staged revision recorded at stage time.
maison deployrecovers incomplete remote repository transactions before user convergence resumes.- Rollback preserves a recoverable repository path during replacement.
Design Integration
MAISON-002 extends MAISON-001’s root-owned transaction architecture with revision contracts, non-destructive finalize
semantics, and startup recovery. Deploy-rs system rollback behavior and public command surface (maison deploy <host>) are
unchanged.
Reference and Contracts
Operational Impact
Operators inspect root-owned transaction state under
/home/.maison-deploy/transactions/<user>/<repo-hash>/ as needed. The transaction state files are:
transaction.lockfor serialization,active.jsonfor current in-flight transaction metadata,<transaction-id>/journal.jsonlfor durable journal events,<transaction-id>/staging/repositoryfor extracted archive contents,<transaction-id>/rollback/repositoryfor the replacement when a previous repository exists.
Design Reconciliation
Delivered as Designed
- Revision checks are enforced before commit/rollback and before startup recovery decisions.
- Root-owned rollback no longer deletes the active repository before a replacement is proven safe.
- Recovery from incomplete state uses the recorded journal and revision information.
Deferred Work
- Additional command-line inspection surfaces (
inspect,abort, explicitrecover) remain planned for future ergonomics while preserving MAISON-002 scope.
Documentation Updated
docs/deployment.mddocs/recovery.mddocs/operations.mddocs/task-reference.mddocs/src/features/maison-002-revision-bound-commit-rollback/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-0a9. - Revision-contract and engine implementation tasks:
maison-mol-j0h.1,maison-mol-j0h.2. - Validation task:
maison-mol-0kt.
Validation Evidence
python3 -m unittest -v tests.test_deploy_transaction— passed for transaction path contract tests.python3 -m unittest -v tests.test_topology.DeploymentContractTest— passed for revision and recovery contract tests.uv run scripts/check-docs.py— passed after documentation updates.mise -E dev run check— passed.
MAISON-003: Restricted deployment privilege model
Delivery Summary
- Beads feature root:
maison-mol-4v3 - Status: delivered
- Pull request: not used for merge-mode delivery
- Merge commit: fast-forward delivery; final target SHA is recorded in the Beads delivery record
- Design record: design.md
Delivered Capability
Remote Linux deployment now uses a dedicated deployment identity by default instead of direct root deployment or the
managed user account. The default deployment account is maison-deploy; the managed user still owns the remote Maison
repository and runs user convergence.
The Linux system-manager module now creates the deployment account and grants only command-scoped passwordless sudo for reviewed deployment entrypoints:
- preparing the canonical system-manager profile directory,
- running Maison transaction helper
recover,stage, andfinalizeoperations, - and activating the deploy-rs selected system-manager profile.
The previous supported default of broad managed-user NOPASSWD: ALL has been removed.
User-Facing Behavior
Operators keep using the existing command surface:
maison deploy <host>maison deploy <host> --system-onlymaison deploy <host> --force-dotfilesmaison system deploy <host>
Inventory defaults and examples now select deploy.ssh_user = "maison-deploy". Validation rejects configurations where
deploy.ssh_user matches the managed username, while deploy.user_ssh_user must still match that managed username.
Repository transaction and rollback behavior from MAISON-001 and MAISON-002 remains unchanged.
Design Integration
MAISON-003 preserves the established two-layer architecture: Nix/Lix owns privileged system state and mise owns user state. The deployment account is a narrow privileged bridge for remote system activation and root-owned repository transaction helpers; it is not the managed user and does not own the user environment.
Deploy-rs still selects and verifies the system-manager profile. Maison still stages the repository through the root-owned same-filesystem transaction namespace before running user convergence as the managed user.
Operational Impact
Operators must ensure the deployment SSH account can authenticate to deploy-enabled Linux hosts. System-manager manages the account and sudoers policy after the host has enough initial access to apply the system layer.
Recovery procedures still inspect the root-owned transaction namespace under
/home/.maison-deploy/transactions/<user>/<repo-hash>/. Privileged helper recovery runs through the deployment account
with command-scoped sudo instead of through a broadly privileged managed user.
Reference and Contracts
Validation Evidence
python3 .mise/lib/inventory.py --file inventory.toml validate— passed.python3 -m py_compile .mise/lib/inventory.py tests/test_topology.py scripts/maison_deploy_transaction.py— passed.python3 -m unittest -v tests.test_topology.InventoryBehaviorTest tests.test_topology.DeploymentContractTest— passed.uv run scripts/check-docs.py— passed.hk fixandhk check— passed for the shellcheck/shebang housekeeping fix that unblocked hook validation.
Design Reconciliation
Delivered as Designed
maison-deployis the default deployment SSH user in Python and Nix inventory paths.- Deployment and managed user identities are validated separately.
- Remote deployment helper calls and profile preparation use command-scoped sudo when the deployment account is not root.
- Linux sudoers no longer grants the managed user
NOPASSWD: ALLby default. - Reader-facing deployment, recovery, architecture, add-host, and task-reference pages describe the new account boundary.
Intentional Changes
- The sudoers account name comes from the validated deploy inventory user (
host.deploy.sshUser), whose default ismaison-deploy, rather than being hard-coded in the Linux module. This keeps explicit non-managed deployment account overrides aligned with the same restricted privilege contract.
Deferred Work
- Private overlay-managed authorized-key material remains part of the later private overlay feature; public Maison still avoids embedding site-specific trusted access material.
Rejected or Removed Scope
- Broad
NOPASSWD: ALLfor the managed Linux user is no longer supported as the default Maison deployment model. - Direct root deployment remains possible only when explicitly configured; it is no longer the default path.
Documentation Updated
docs/add-a-host.mddocs/architecture.mddocs/deployment.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-003-restricted-deployment-privilege/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-9mi. - Implementation coordinator:
maison-mol-u8y. - Contract-test implementation task:
maison-mol-u8y.1, commit09e4fdfd2b027db782d04eb7a10a20178b272c30. - Restricted privilege implementation task:
maison-mol-u8y.2, commit26a8411b1964b0a54103c6a7fa06584696698d61. - Shellcheck housekeeping commit:
e322902c674c59069cc071d4166de17af7e0d5708. - Validation task:
maison-mol-safe.
MAISON-004: Private overlay configuration split
Historical record: this delivery was superseded by the single consumer-root model. The old source-selection state and command paths described below are not supported; current migration guidance is in Migration Contract.
Delivery Summary
- Beads feature root:
maison-mol-e9t - Status: delivered, later superseded by
maison-hi8 - Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now separates public reusable vehicle code from private site configuration. The public repository keeps neutral starter inventory, host examples, package files, dotfiles, validators, Nix modules, and task machinery. A tracked private overlay repository can own real hosts, usernames, emails, deploy targets, package declarations, dotfile overrides, and trusted key material.
Bootstrap accepts --overlay <git-url-or-path>, records the selected source in local XDG state, and clones or updates
the overlay into a standard XDG data path. When the overlay contains inventory.toml, Maison uses that typed inventory
and its sibling hosts/ tree for shell task lookups, Python validation, Nix evaluation, host mutation, and deployment
targeting.
User-Facing Behavior
Operators keep using the existing Maison command surface. Fresh setup can provide an overlay explicitly:
./bootstrap.sh --host "$(hostname -s)" --overlay GIT-URL-OR-PATH
Overlay source precedence is:
--overlay <git-url-or-path>for the current bootstrap run.MAISON_OVERLAY_SOURCE, including a value supplied by mise secrets.${XDG_STATE_HOME:-$HOME/.local/state}/maison/overlay.toml.
The standard clone/update path is ${XDG_DATA_HOME:-$HOME/.local/share}/maison/overlay. Interactive setup may prompt
when overlay input is required; non-interactive setup fails clearly instead of silently continuing with public starter
data for a real machine.
Design Integration
The implementation preserves Maison’s two-layer architecture. Nix/Lix remains the privileged system owner and mise remains the user-state owner. The overlay is a data source for the same typed inventory and layout contracts; it is not a second command surface or arbitrary schema framework.
Public Maison remains a valid starter repository. Private overlays mirror supported owned data paths such as
inventory.toml, hosts/, config/mise/*.toml, dotfiles/, and trusted key files.
Operational Impact
Operators can recover overlay selection by restoring or recreating the XDG state file, or by rerunning bootstrap with
--overlay. If the overlay clone is corrupt, it can be moved aside and recloned from the recorded source.
maison host add mutates the active inventory repository. With an overlay inventory, new inventory entries and optional
host override stubs are written to the overlay clone rather than to public Maison.
Reference and Contracts
- Architecture
- Operations
- Remote deployment
- Recovery
- Package policy
- Task reference
- Adding a Host
- Adding a Tool
- Adding an App
Validation Evidence
python3 -m py_compile scripts/maison_overlay.py tests/test_topology.py .mise/lib/inventory.py— passed.python3 -m unittest -v tests.test_topology.OverlayContractTest— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Bootstrap supports
--overlayand hands the selected source to the bootstrap task. - Overlay state uses
${XDG_STATE_HOME:-$HOME/.local/state}/maison/overlay.toml. - Overlay clones use
${XDG_DATA_HOME:-$HOME/.local/share}/maison/overlay. --overlaytakes precedence over environment and saved state sources.- The same typed TOML inventory validator handles public examples and private overlay fixtures.
- Python task lookups, Nix evaluation, host overrides, and deployment targeting use the active overlay inventory when present.
- Public starter inventory, dotfiles, and reader examples no longer contain personal infrastructure identity.
Intentional Changes
- The first implementation introduced a small tested Python helper at
scripts/maison_overlay.pyplus a thin shell adapter at.mise/lib/overlay.sh, matching the design preference for Python stdlib state handling while preserving shell task compatibility. - Public starter data remains usable for validation and examples, but real machine convergence is documented around an overlay rather than public defaults.
Deferred Work
- Overlay layering for every possible
config/mise/*.tomlanddotfiles/application path remains limited to the documented mirrored layout and active inventory behavior delivered here. Future features can expand mutation surfaces if needed.
Rejected or Removed Scope
- Public Maison no longer carries personal usernames, hostnames, email addresses, deploy endpoints, or trusted signing keys.
- Untracked-only local inventory is not the durable source of truth; the private overlay repository is tracked.
Documentation Updated
README.mddocs/add-a-host.mddocs/add-a-tool.mddocs/add-an-app.mddocs/architecture.mddocs/deployment.mddocs/operations.mddocs/package-policy.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-004-private-overlay-configuration/design.mddocs/src/features/maison-004-private-overlay-configuration/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-luq, commit4d88caa. - Implementation coordinator:
maison-mol-z6x. - Overlay contract task:
maison-mol-z6x.1, commit96136b0. - Overlay loader task:
maison-mol-z6x.2, commit8ae4710. - Overlay docs/examples task:
maison-mol-z6x.3, commit8d5c240. - Documentation reconciliation task:
maison-mol-5is. - Validation task:
maison-mol-uie.
MAISON-005: Verified bootstrap artifacts
Delivery Summary
- Beads feature root:
maison-mol-pa6 - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison bootstrap no longer executes downloaded installer content before verification. Local bootstrap, shared Lix setup, and the remote deployment mise fallback now download Maison-owned artifacts to disk, verify checked-in SHA-256 metadata, and install or execute only verified local files.
A checked-in manifest at bootstrap/artifacts.toml is the public trust root for supported mise and Lix bootstrap
artifacts on aarch64-darwin, aarch64-linux, and x86_64-linux. Maison runtime inputs required during bootstrap and
command execution are pinned to immutable versions or revisions.
User-Facing Behavior
Operators keep the existing command surface. Fresh setup uses a reviewed local checkout or a downloaded bootstrap file that is verified before execution. When mise or Lix is missing, Maison installs the pinned artifact for the current supported system. Verification failure stops bootstrap with an actionable error rather than continuing with untrusted content.
Remote Linux deployment preserves the existing system/user split. If the managed user lacks mise during repository/user convergence, the staged repository provides the same verified mise install fallback.
Design Integration
The implementation preserves Maison’s architecture boundaries: Nix/Lix remains the privileged system layer and mise remains the user command, package, and dotfile layer. Verification logic is centralized in shared bootstrap shell helpers for task compatibility, with a tested Python verifier for manifest/checksum behavior.
The immutable-input guardrails target Maison bootstrap/runtime inputs only. Ordinary non-bootstrap workstation tools may
still use the repository’s existing latest policy when generated lockfiles capture resolved artifacts.
Operational Impact
Operators can inspect or update reviewed bootstrap metadata in bootstrap/artifacts.toml. A checksum mismatch indicates
that the downloaded artifact differs from the reviewed trust root; recovery is to retry once for partial downloads, then
review the upstream release before changing the manifest or manually installing the same verified artifact.
Reference and Contracts
Validation Evidence
python3 -m py_compile tests/test_topology.py scripts/verify_bootstrap_artifact.py— passed.python3 -m unittest -v tests.test_topology.VerifiedBootstrapContractTest tests.test_topology.OwnershipBoundaryTest.test_repository_tools_are_isolated_from_machine_convergence— passed.uv run scripts/check-docs.py— passed.shellcheck -x bootstrap.sh .mise/lib/bootstrap.sh .mise/tasks/deploy— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Local bootstrap no longer recommends or performs pipe-to-shell installer execution.
- Shared Lix setup downloads a pinned installer artifact, verifies its SHA-256 digest, then executes the local file.
- Remote deployment’s mise fallback installs a verified pinned mise binary from the staged repository metadata.
- Public bootstrap examples avoid pipe-to-shell instructions.
- Source-text guard tests reject unsafe bootstrap patterns and mutable Maison runtime/plugin references.
- Manifest and verifier tests cover required metadata, supported-system selection, checksum success, and checksum mismatch failure.
Intentional Changes
- The checked-in manifest uses per-platform metadata under each artifact so supported systems can use platform-specific URLs and checksums while sharing version and recovery fields.
- The existing
usageMaison runtime tool is pinned to4.0.0; the broader user-tool latest policy remains documented and tested for non-bootstrap tools.
Deferred Work
None.
Rejected or Removed Scope
- Unverified
curl ... | shandcurl ... | bashpaths were removed from Maison-owned bootstrap surfaces. - Private overlay artifact metadata was not introduced as the public default trust root; it remains reserved for future site-specific mirrors or overrides.
Documentation Updated
README.mddocs/deployment.mddocs/operations.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-005-verified-bootstrap-artifacts/design.mddocs/src/features/maison-005-verified-bootstrap-artifacts/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-0od, commit63aa235. - Implementation coordinator:
maison-mol-b2j. - Bootstrap contract task:
maison-mol-b2j.1, commitda8eb56. - Bootstrap implementation task:
maison-mol-b2j.2, commit2a93428. - Documentation reconciliation task:
maison-mol-46k. - Validation task:
maison-mol-bx6.
MAISON-006: Review-gated dependency updates
Delivery Summary
- Beads feature root:
maison-mol-aqu - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison’s cache-refresh automation no longer approves or merges dependency updates. The workflow may refresh flake.lock, build the proposed cache targets, warm Cachix, and open or update the automation/refresh-flake-lock pull request. It leaves dependency acceptance to ordinary pull request review and branch protection.
The repository now has source guards that reject reintroducing dependency automation that invokes gh pr merge, passes --admin, enables auto-merge, or recreates the removed merge approval job.
User-Facing Behavior
Operators and contributors still use the existing update surfaces. Manual maison update behavior is unchanged. Scheduled cache refresh can prepare a reviewed flake-lock PR with warmed cache artifacts, but the PR remains open until a human review and protected-branch merge accepts it.
Design Integration
The implementation keeps Nix input semantics unchanged: flake.lock remains the pinned Nix dependency source and update PRs remain normal repository changes. It narrows only the CI delivery path by removing the administrative merge stage from .github/workflows/cache-refresh.yml.
No new approval service, command surface, or repository ownership layer was introduced. GitHub pull request review and branch protection are the dependency approval mechanism.
Operational Impact
Maintainers should expect cache refresh runs with dependency changes to produce or update an automation PR rather than merge it. The reusable hk and CI workflow runs still execute against automation/refresh-flake-lock when a PR is present, so reviewers can use the warmed cache and validation evidence before deciding whether to merge.
Reference and Contracts
Validation Evidence
python3 -m py_compile tests/test_topology.py— passed.python3 -m unittest -v tests.test_topology.ReviewGatedDependencyUpdateTest— passed.uv run scripts/check-docs.py— passed.mise -E dev exec -- actionlint .github/workflows/cache-refresh.yml— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Removed the automated
merge-lock-prjob from.github/workflows/cache-refresh.yml. - Preserved cache warming and update PR creation/update behavior.
- Added tests that fail if dependency automation uses
gh pr merge,--admin, auto-merge, or an approval merge job. - Updated reader-facing operations, development tooling, and tooling reference docs.
Intentional Changes
- Documentation paths were corrected during specification review from non-existent root-level development/reference pages to the current
docs/src/development/tooling.mdanddocs/src/reference/tooling.mdpages.
Deferred Work
None.
Rejected or Removed Scope
- Administrative PR merge bypass was removed from cache refresh automation.
- A new approval service or Maison command was not introduced.
Documentation Updated
docs/operations.mddocs/src/development/tooling.mddocs/src/reference/tooling.mddocs/src/features/maison-006-review-gated-dependency-updates/design.mddocs/src/features/maison-006-review-gated-dependency-updates/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-642, commit0f4e563. - Implementation coordinator:
maison-mol-73y. - CI contract task:
maison-mol-73y.1, commitb45e97f. - CI implementation task:
maison-mol-73y.2, commitf3682a1. - Documentation reconciliation task:
maison-mol-4jj. - Validation task:
maison-mol-aqw.
MAISON-007: Parser-backed TOML mutations
Delivery Summary
- Beads feature root:
maison-mol-ywg - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now edits TOML through a parser-backed mutation helper instead of hand-written section splicing. Tool, package, app, lockfile, and host-inventory mutations parse the complete target document, apply typed changes, validate the edited TOML, and only then replace the candidate file.
The helper uses a checked-in tomlkit 0.13.3 wheel and verifies its SHA-256 digest before import. This keeps repair
commands available when normal mise project tool resolution is skipped or broken and avoids relying on ambient Python
site-packages.
User-Facing Behavior
Operators continue using the existing maison and mise task commands. Supported comments, quoted keys, table
boundaries, arrays of tables, and CRLF line endings are preserved when Maison edits TOML. Malformed or unsupported TOML
fails with an error before Maison rewrites the file.
host:add uses the same parser-backed mutation layer for the active public or overlay inventory, then continues to
validate the full candidate inventory before committing the change.
Design Integration
The implementation keeps Maison’s existing ownership split: Nix/Lix still owns privileged system state, and mise-owned commands continue to manage user tools, packages, apps, and inventory authoring. Shell task transaction wrappers and command names were preserved; the parser-backed helper replaced only the TOML mutation internals.
The pinned runtime is repository-controlled and verified locally, matching the bootstrap trust model without adding a new package manager, service, or privileged helper.
Operational Impact
Configuration repair paths are safer. tool:add and tool:remove continue to work on the --skip-tools path, even
when invalid tool configuration would prevent normal mise project resolution. Failed parses, unsupported structures,
validation failures, or package-manager failures leave repository files unchanged through the existing transaction
wrappers.
Reference and Contracts
Validation Evidence
python3 -m py_compile .mise/lib/config_edit.py tests/test_config_edit.py tests/test_topology.py— passed.python3 -m unittest -v tests.test_config_edit tests.test_topology.TransactionBehaviorTest— passed.shellcheck -x .mise/tasks/host/add— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Added a regression corpus for trailing table comments, quoted keys, arrays of tables, CRLF input, malformed TOML, lockfile edits, host inventory additions, and unrelated following sections.
- Replaced TOML table and lockfile text splicing in
.mise/lib/config_edit.pywith parser-backed edits. - Added a host inventory mutation operation and routed
host:addthrough it. - Vendored and verified the pinned
tomlkitruntime used by mutation scripts. - Updated reader-facing documentation for the mutation guarantees and contributor runtime.
Intentional Changes
- The specification was corrected during start-feature review to name
docs/src/development/tooling.mdfor contributor tooling documentation while keeping root-level included command pages for task, tool, and app guidance.
Deferred Work
None.
Rejected or Removed Scope
- No new command surface was added.
- Non-TOML authoring artifacts remain outside this feature.
- The feature did not add a Rust helper or a privileged mutation service.
Documentation Updated
docs/src/development/tooling.mddocs/task-reference.mddocs/add-a-tool.mddocs/add-an-app.mddocs/src/features/maison-007-parser-backed-toml-mutations/design.mddocs/src/features/maison-007-parser-backed-toml-mutations/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-nxg, commitef9fde6. - Implementation coordinator:
maison-mol-imq. - Contract-test task:
maison-mol-imq.1, commitda88a62. - Parser implementation task:
maison-mol-imq.2, commitdfb7a7f. - Documentation reconciliation task:
maison-mol-0zx. - Validation task:
maison-mol-oyr.
MAISON-008: Repository mutation locking and journals
Delivery Summary
- Beads feature root:
maison-mol-4ev - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now serializes local repository-writing authoring commands with a shared fail-fast fcntl lock. Tool, package,
app, host, and flake-lock update mutations acquire the target repository lock before reading mutable repository state,
recover incomplete local journals, stage candidate files, validate, and then replace repository files atomically.
The new local journal helper records original files, candidate files, validation state, commit state, and recovery action for repository file mutations. If recovery or rollback fails, Maison returns non-zero and preserves the journal and copied state for diagnostics instead of suppressing the failure.
User-Facing Behavior
Operators continue using the existing maison and mise task names. If another repository mutation is active for the
same public checkout or private overlay, the second command fails fast and identifies the busy repository and journal
location. Read-only plan, status, list, validate, and search commands do not take the mutation lock.
Affected repository-writing commands are tool:add, tool:remove, package:add, package:remove, app:add,
app:remove, host:add, and update.
Design Integration
The implementation keeps the Nix/Lix system and mise user ownership boundary unchanged. Local authoring transactions remain in the user layer and are implemented with Python and shell task adapters. The local mutation journal is explicitly separate from the root-owned remote deployment transaction namespace, so MAISON-001 and MAISON-002 deployment transaction and rollback contracts remain intact.
Journal state is untracked local Maison state keyed by the canonical target repository path. Tests may override the
state root with MAISON_REPOSITORY_MUTATION_STATE_DIR; normal operation uses
${XDG_STATE_HOME:-$HOME/.local/state}/maison/repository-mutations/.
Operational Impact
Concurrent repository writes no longer race silently. Operators should wait for the active mutation to finish before retrying a busy-lock failure. If a mutation is interrupted, rerunning a mutation for the same repository triggers the startup recovery pass before the new command reads state.
Because journals may copy private overlay data, state directories use owner-only permissions. Operators should preserve journals after rollback failures until the active files and copied originals/candidates are understood.
Reference and Contracts
Validation Evidence
python3 -m py_compile tests/test_repository_mutation.py— passed before contract-test commit.python3 -m py_compile .mise/lib/repository_mutation.py tests/test_repository_mutation.py tests/test_topology.py— passed.python3 -m unittest -v tests.test_repository_mutation tests.test_topology.TransactionBehaviorTest— passed.shellcheck -x .mise/lib/transaction.sh .mise/tasks/tool/add .mise/tasks/tool/remove .mise/tasks/package/add .mise/tasks/package/remove .mise/tasks/app/add .mise/tasks/app/remove .mise/tasks/host/add .mise/tasks/update— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Added a shared stdlib
fcntllock and local journal helper in.mise/lib/repository_mutation.py. - Routed tool, package, app, host, and
flake.lockupdate repository mutations through the shared lock. - Added journal helpers in
.mise/lib/transaction.shand used them around multi-file and single-file repository replacements. - Added contract tests for fail-fast locking, journal recovery states, rollback-failure diagnostics, mutating task surfaces, and read-only task exclusions.
- Updated architecture, operations, recovery, and task-reference documentation.
Intentional Changes
- Specification review narrowed scope to repository-writing commands. Installed-state update commands such as
user:update,package:update, andapp:updateremain outside the repository mutation lock because they do not edit checked-in or overlay repository files. - Concurrent mutation semantics were made fail-fast rather than wait/block to keep the first implementation small and predictable.
Deferred Work
None.
Rejected or Removed Scope
- Read-only commands are not serialized.
- The feature does not replace Git as source history.
- The local authoring journal does not reuse or modify the root-owned remote deployment transaction namespace.
Documentation Updated
docs/architecture.mddocs/operations.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-008-repository-mutation-locking/design.mddocs/src/features/maison-008-repository-mutation-locking/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-0pc, commit6b168ba. - Implementation coordinator:
maison-mol-auy. - Contract-test task:
maison-mol-auy.1, commitf9b4151. - Lock/journal implementation task:
maison-mol-auy.2, commit9e6562d. - Documentation reconciliation task:
maison-mol-g1r. - Validation task:
maison-mol-bdn.
MAISON-009: Authoring checkout guard
Delivery Summary
- Beads feature root:
maison-mol-74l - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now rejects repository-writing authoring commands when the target repository is a deployed runtime snapshot. A
snapshot is identified by .maison-revision without .git; it is safe for runtime plan, apply, status, deployment
finalization, and recovery, but not for source edits that the next deployment would overwrite.
User-Facing Behavior
Authoring-only commands keep their existing names: tool:add, tool:remove, package:add, package:remove,
app:add, app:remove, host:add, and update. Before taking the local mutation lock, each command checks the
repository it will write for Git authoring evidence. Both normal .git directories and linked-worktree .git files are
accepted.
When the target is a deployed Maison snapshot, the command exits non-zero and directs the operator to edit from a Git
authoring checkout or private overlay repository. Read-only and runtime commands remain available without .git.
Design Integration
The implementation reuses the MAISON-008 repository mutation helper instead of adding a new runtime. Checkout detection
uses only marker files, so it does not read mutable TOML, lockfiles, or journaled content before local mutation lock and
startup recovery logic run. host:add guards the active inventory repository, preserving MAISON-004 overlay behavior:
a deployed public Maison snapshot may still author hosts in a private overlay clone when that overlay is a Git checkout.
The feature keeps privileged system state in Nix/Lix and user/source authoring state in mise. It does not add .git to
remote deployment archives and does not weaken MAISON-001/002 root-owned remote deployment transaction contracts.
Operational Impact
Operators should run repository-writing commands from the public Maison source checkout or the private overlay checkout that owns the data being changed. Deployed snapshots are runtime trees; rejected authoring commands should be retried from the durable source repository and then applied or deployed again.
Reference and Contracts
Validation Evidence
python3 -m py_compile tests/test_repository_mutation.py— passed before contract-test commit.python3 -m unittest -v tests.test_repository_mutation.AuthoringCheckoutGuardTest tests.test_repository_mutation.RepositoryMutationTaskSurfaceTest tests.test_repository_mutation.MaisonCommandDeployedSnapshotTest— failed as expected before implementation; passed after implementation.python3 -m py_compile .mise/lib/repository_mutation.py tests/test_repository_mutation.py— passed.shellcheck -x .mise/lib/transaction.sh .mise/tasks/tool/add .mise/tasks/tool/remove .mise/tasks/package/add .mise/tasks/package/remove .mise/tasks/app/add .mise/tasks/app/remove .mise/tasks/host/add .mise/tasks/update— passed.python3 -m unittest -v tests.test_repository_mutation tests.test_topology.TransactionBehaviorTest— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Added a shared stdlib Python checkout detector in
.mise/lib/repository_mutation.py. - Added a shell adapter in
.mise/lib/transaction.shand called it from all repository-writing authoring tasks. - Detected deployed snapshots as
.maison-revisionwithout.gitand returned clear source/overlay guidance. - Preserved runtime and read-only command behavior in gitless deployed snapshots.
- Kept
host:addtarget-aware by checking the active inventory root instead of always checking public Maison. - Updated reader-facing architecture, operations, deployment, recovery, and task-reference documentation.
Intentional Changes
- Specification review narrowed the behavior to fail-fast rejection instead of dynamic task hiding.
- Test fixtures for mutating transaction behavior now include
.gitmarkers to model authoring repositories.
Deferred Work
None.
Rejected or Removed Scope
- Deployed snapshots still do not contain
.git. - Runtime plan, apply, status, deployment finalization, and recovery commands are not blocked by the authoring guard.
- The local authoring guard does not reuse or alter the root-owned remote deployment transaction namespace.
Documentation Updated
docs/architecture.mddocs/operations.mddocs/deployment.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-009-authoring-checkout-guard/design.mddocs/src/features/maison-009-authoring-checkout-guard/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-her, commit73d882b. - Implementation coordinator:
maison-mol-kid. - Contract-test task:
maison-mol-kid.1, commit5ccf192. - Checkout-implementation task:
maison-mol-kid.2, commitb44fb98. - Documentation reconciliation task:
maison-mol-xe7. - Validation task:
maison-mol-zki.
MAISON-010: Shared inventory schema validation
Delivery Summary
- Beads feature root:
maison-mol-bvr - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now has a checked-in public inventory schema contract at schemas/inventory.toml. Python and Nix validation both
load that contract for supported systems, profile names, feature keys, deploy keys, and deploy defaults. A shared fixture
corpus under tests/fixtures/inventory/ exercises valid public and overlay shapes plus invalid drift cases so CI fails
when the validators disagree.
User-Facing Behavior
Operators keep using the same maison host validate, planning, deployment, and bootstrap flows. Inventory errors are
now governed by one documented field/default contract rather than separately maintained Python and Nix constants. Unknown
feature keys, unknown deploy fields, duplicate profiles, deploy path escapes, and value type mismatches are rejected
consistently before deployment or authoring changes can proceed.
Design Integration
The implementation keeps Python as the ergonomic typed inventory reader while Nix imports the same schema data with
builtins.fromTOML before constructing outputs. The Nix-packaged maison-inventory app sets MAISON_INVENTORY_SCHEMA
so the Python helper can find the schema from the Nix store. This preserves the existing Nix/Lix system and mise user
ownership boundary while replacing duplicated validation constants with one public contract.
Overlay behavior remains replacement-based rather than merged: when an overlay inventory is active, Maison validates
that inventory and its sibling hosts/ override layout. Host override directories must correspond to inventory hosts and
may contain only system.nix.
Operational Impact
Inventory contract changes now require updating schemas/inventory.toml and the shared fixtures. The new
Inventory Reference documents supported fields, defaults, constraints, and overlay
validation behavior for operators and contributors.
Reference and Contracts
Validation Evidence
python3 -m py_compile tests/test_inventory_schema.py— passed before fixture commit.python3 -m unittest -v tests.test_inventory_schema— failed as expected before implementation because Nix still accepted drift cases; passed after implementation.python3 -m py_compile .mise/lib/inventory.py tests/test_inventory_schema.py tests/test_topology.py— passed.python3 -m unittest -v tests.test_inventory_schema tests.test_topology.InventoryBehaviorTest tests.test_topology.OverlayContractTest tests.test_topology.RepositoryContractTest— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Added
schemas/inventory.tomlas the single public inventory schema contract. - Updated Python inventory validation to load supported systems, profiles, feature defaults, deploy defaults, and regex policy from the schema.
- Updated Nix validation to import the same schema and reject duplicate profiles, unknown features, unknown deploy fields, and deploy value type mismatches.
- Added shared valid and invalid inventory fixtures consumed by both Python and Nix validation tests.
- Added an inventory reference page and updated architecture, host authoring, deployment, tooling, and navigation docs.
Intentional Changes
- Specification review replaced ambiguous overlay merge wording with active overlay inventory and host override layout fixture coverage, matching MAISON-004 behavior.
- The Nix-packaged inventory app exports
MAISON_INVENTORY_SCHEMAso the Python helper remains usable when copied into the Nix store.
Deferred Work
None.
Rejected or Removed Scope
- The feature does not replace TOML as the inventory format.
- The public fixture corpus contains only neutral examples and no private inventory.
- Overlay inventories are not merged with the public inventory.
Documentation Updated
docs/architecture.mddocs/add-a-host.mddocs/deployment.mddocs/src/reference/inventory.mddocs/src/reference/tooling.mddocs/src/features/maison-010-shared-inventory-schema/design.mddocs/src/features/maison-010-shared-inventory-schema/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-41t, commit05ca64e. - Implementation coordinator:
maison-mol-8ey. - Schema-fixtures task:
maison-mol-8ey.1, commitd96b24a. - Schema-implementation task:
maison-mol-8ey.2, commit692bfca. - Documentation reconciliation task:
maison-mol-4np. - Validation task:
maison-mol-50d.
MAISON-011: Bounded deterministic test suite
Delivery Summary
- Beads feature root:
maison-mol-8mr - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison’s Python regression suite is now bounded and organized by subsystem. Tests that execute external commands use a shared stdlib helper with timeouts, process-group cleanup, captured stdout/stderr, and truncated diagnostics. The former monolithic topology test file has been split into focused modules for deployment, inventory, migration, ownership, repository, and transaction behavior.
User-Facing Behavior
Operators keep using the same maison check and mise task surfaces. Contributors get deterministic check:tests
behavior, focused subsystem commands, and clearer diagnostics when subprocess-backed tests fail or time out.
Design Integration
The implementation keeps all changes in the test and documentation layer. It does not change Maison runtime behavior, Nix/Lix system ownership, mise user ownership, supported platforms, deployment privilege boundaries, overlay behavior, or inventory validation semantics. The subprocess helper is Python stdlib-only and test-local.
Operational Impact
mise run check:tests runs Python unittest discovery over tests/test_*.py and is expected to complete within five
minutes on a warm supported development checkout. Test subprocesses default to a 30 second timeout; longer per-call
budgets must be explicit and no longer than 300 seconds.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_process_helper— failed before helper implementation, then passed.python3 -m py_compile tests/support/processes.py tests/test_process_helper.py— passed.mise -E dev run check— passed after helper implementation.python3 -m py_compile tests/*.py tests/support/*.py— passed after subsystem split.python3 -m unittest discover -s tests -p 'test_*.py' -v— passed.- Focused subsystem command covering process helper, inventory, deployment, migration, ownership, repository, mutation, and transaction modules — passed.
uv run scripts/check-docs.py— passed.mise -E dev run check— passed.- Final
mise -E dev run check:testsafter split cleanup — passed.
Design Reconciliation
Delivered as Designed
- Added
tests/support/processes.pywith a 30 second default timeout, 300 second maximum, process-group cleanup where supported, stdout/stderr capture, diagnostic truncation, and temporary-directory support. - Added focused helper tests for success, failure diagnostics, environment/cwd handling, timeout cleanup, and timeout bounds.
- Split topology coverage into subsystem modules:
tests/test_deployment_contracts.pytests/test_inventory_behavior.pytests/test_migration_behavior.pytests/test_ownership_boundary.pytests/test_repository_contracts.pytests/test_transaction_behavior.py
- Moved shared topology test helpers into
tests/support/topology.py. - Migrated test subprocess execution to the shared bounded helper.
- Updated operations, development tooling, and tooling reference docs with the bounded-suite contract.
Intentional Changes
- Specification review corrected documentation impact paths to mdBook source pages under
docs/src/. - The implementation retained historical delivered-feature records that mention former
tests/test_topology.pyevidence because those records are audit history, not current validation instructions.
Deferred Work
None.
Rejected or Removed Scope
- No test framework replacement was introduced.
- No production code was rewritten solely for test organization.
- No deployment, Nix, or inventory coverage was removed.
Documentation Updated
docs/operations.mddocs/src/development/tooling.mddocs/src/reference/tooling.mddocs/src/features/maison-011-deterministic-test-suite/design.mddocs/src/features/maison-011-deterministic-test-suite/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-9hm, commitbe3a910. - Implementation coordinator:
maison-mol-0dm. - Test-helper task:
maison-mol-0dm.1, commit27449b0. - Test-suite-split task:
maison-mol-0dm.2, commit2fd4af3. - Documentation reconciliation task:
maison-mol-524. - Validation task:
maison-mol-4wa.
MAISON-012: Plan/apply semantic parity
Delivery Summary
- Beads feature root:
maison-mol-6w9 - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now builds user convergence from one structured Python command plan. maison user plan and maison user apply
use the same force-dotfile semantics and ordered convergence steps; plan renders the supported dry-run commands without
executing convergence or trust operations.
User-Facing Behavior
Dotfile replacement is opt-in for both preview and apply. By default, maison user plan and maison user apply do not
force replacement of whole-file conflicts. To preview and then apply the same forced handoff, run:
maison user plan --force-dotfiles
maison user apply --force-dotfiles
maison plan --force-dotfiles forwards that user-layer flag after its system preview, matching aggregate apply.
Design Integration
.mise/lib/user_convergence.py owns the ordered user convergence plan: preparation, dotfiles, mise lock links,
packages, remaining mise user state, plus apply-only repository trust and finalization. The shell tasks remain thin
argument adapters. Plan renders dry-run variants for preparation, dotfiles, lock links, packages, and remaining mise user
state without invoking them; apply uses the existing package helper, repository trust, and finalization. These
execution-only substitutions preserve the Nix/Lix system and mise user ownership boundary.
Operational Impact
Use the same --force-dotfiles choice for preview and apply. A forced preview renders the intended command sequence
without modifying files. A forced apply backs up the exact targets before replacement. Existing package-manager dry-run
capabilities are unchanged. The plan renderer no longer invokes those commands, so planning cannot alter mise trust state, dotfiles, lock
links, migration backups, or installed user state. System planning may realize a Nix store derivation for comparison,
but never activates it.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_user_convergence— failed before the shared command-plan module existed, then passed after implementation.python3 -m unittest -v tests.test_user_convergence tests.test_migration_behavior tests.test_ownership_boundary— passed.python3 -m py_compile .mise/lib/user_convergence.py tests/test_user_convergence.py tests/test_ownership_boundary.py— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed.
Design Reconciliation
Delivered as Designed
- Added a tested, stdlib-only user-convergence command-plan builder.
- Centralized user plan/apply command construction and force-dotfile argument handling.
- Added parity coverage for default and explicit force behavior, command substitutions, user task adapters, and aggregate plan forwarding.
- Updated operations, recovery, and task reference pages with default no-force and explicit force preview/apply flow.
Intentional Changes
- Specification review clarified that parity applies for the same flags: default no-force and explicit force are each consistent between plan and apply.
- Package, repository-trust, and finalization differences are documented mode-specific execution substitutions rather than new package-manager dry-run behavior.
Deferred Work
None.
Rejected or Removed Scope
- No package-manager dry-run capabilities were added.
- No production privilege, deployment, inventory, or ownership behavior changed outside user command construction.
Documentation Updated
docs/operations.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-012-plan-apply-parity/design.mddocs/src/features/maison-012-plan-apply-parity/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation task:
maison-mol-88b, commita7564c0. - Implementation coordinator:
maison-mol-bmj. - Parity-contract task:
maison-mol-bmj.1, commitb2e8227. - Parity-implementation task:
maison-mol-bmj.2, commit100dd31. - Documentation reconciliation task:
maison-mol-oov. - Validation task:
maison-mol-5it.
MAISON-013: Exact dotfile backup manifests
Delivery Summary
- Beads feature root:
maison-mol-h8e - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now creates exact, manifest-backed snapshots before an explicitly forced dotfile handoff. Snapshots preserve whether every refused target is a file, directory, or symlink, without dereferencing a symlink target.
User-Facing Behavior
Forced handoff remains opt-in:
maison user plan --force-dotfiles
maison user apply --force-dotfiles
Each handoff writes manifest.json in its timestamped dotfile backup directory. To restore its pending entries, use:
maison user restore-dotfiles ~/.local/state/maison/backups/dotfiles/<timestamp> --force
The explicit force flag is required before a restore can replace a current target. A restore stops at its first failure;
completed entries are recorded as restored, so rerunning restores only remaining pending entries.
Design Integration
.mise/lib/dotfile_backups.py is a Python-stdlib helper called by the existing user preparation adapter and the restore
task. It validates paths under the managed home and Maison backup root, snapshots by lstat object identity, preserves
file and directory metadata, recreates symlinks by their literal target, and refuses sockets, FIFOs, block devices, and
character devices. Nix/Lix remains responsible for system state; mise remains the user-convergence boundary.
Operational Impact
Inspect a backup’s manifest.json before restoring it. Manifest paths and payload paths are containment-validated; a
malformed, escaping, or missing payload is rejected before that entry can replace a target. Existing application-bundle
backup archival and legacy Git configuration migration behavior are unchanged.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_dotfile_backups tests.test_migration_behavior tests.test_repository_contracts.RepositoryContractTest.test_task_and_script_entrypoints_are_executable— passed.python3 -m py_compile .mise/lib/dotfile_backups.py tests/test_dotfile_backups.py tests/test_migration_behavior.py— passed.uv run scripts/check-docs.py— passed.mise -E dev run check— passed (129 tests, shell task validation, and Nix checks).
Design Reconciliation
Delivered as Designed
- Added atomic versioned JSON manifests with source path, object type, mode, timestamps, payload path, symlink target, and restoration status.
- Added exact backup and restore coverage for files, directories, symlinks, unsupported objects, escaping manifests, required force, and partial restoration progress.
- Added force-gated
user:restore-dotfilesand standalone operational recovery instructions.
Intentional Changes
- Specification review selected the explicit
maison user restore-dotfiles <backup-directory> --forceinterface. - The existing shell task retains conflict discovery and delegates stateful snapshot/restore behavior to the tested Python helper.
Deferred Work
None.
Rejected or Removed Scope
- No unmanaged directory backup support was added.
- No application-bundle archive behavior was changed.
- No Nix/Lix system ownership, private inventory, or deployment behavior changed.
Documentation Updated
docs/operations.mddocs/recovery.mddocs/task-reference.mddocs/src/features/maison-013-dotfile-backup-manifests/design.mddocs/src/features/maison-013-dotfile-backup-manifests/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation:
maison-mol-c8i, commit572a77b. - Backup contract:
maison-mol-xg1.1, commit4a98bdf. - Backup implementation:
maison-mol-xg1.2, commitec9f1bb. - Documentation reconciliation:
maison-mol-3kb4. - Validation:
maison-mol-f89l.
MAISON-014: Remote convergence restoration
Delivery Summary
- Beads feature root:
maison-mol-vhhy - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
Maison now restores the repository revision before attempting restricted managed-user recovery when remote user convergence fails. Recovery repairs reversible user state from the restored repository and records package/app side effects separately instead of implying that external package operations were rolled back.
User-Facing Behavior
maison deploy <host> keeps the original user-convergence failure as its exit status. After a verified repository
rollback, it runs recovery as the managed user from the restored repository. Recovery repairs dotfiles, mise lock links,
non-package mise state, and Maison-owned finalization; it does not rerun package/app convergence, application-backup
migration, legacy Git migration, system activation, or Nix rollback.
Recovery preserves the deployment’s explicit --force-dotfiles choice and never enables it implicitly. It writes an
atomically replaced, mode-0600 JSON diagnostic at:
~/.local/state/maison/recovery/remote-convergence-<failed-revision>.json
The report retains the failed and restored revisions, initial and recovery statuses, completed steps, force state, and observed package/app side effects. A rollback failure skips recovery and leaves the transaction inspection path for operator recovery; a separate recovery failure is reported without hiding the original failure.
Design Integration
The implementation extends the existing Python user-convergence planner with an internal restricted recovery mode and reuses the existing remote repository transaction and task adapters. The deployment account performs only privileged rollback and verification; the managed user performs user-owned recovery. Root-owned transaction journals, staging paths, rollback paths, and locks remain outside managed-user control. Nix/Lix retains system ownership and mise retains user convergence ownership.
Operational Impact
Operators should inspect the recovery report after a failed remote deployment and treat package/app entries marked
started, completed, failed, or unknown as follow-up work when indicated. Recovery is deliberately limited to
reversible user state and does not promise package-manager rollback. An explicit --force-dotfiles deployment choice
is honored consistently across the initial convergence and restricted recovery.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_remote_convergence tests.test_user_convergence tests.test_deployment_contracts— passed (39 tests).python3 scripts/check-docs.py— passed.uv run scripts/check-docs.py— passed during close-out.python3 -m py_compile .mise/lib/user_convergence.py tests/test_remote_convergence.py— passed.shellcheck -x .mise/tasks/deploy .mise/tasks/user/recoverandshfmt -d --apply-ignore .mise/tasks/deploy .mise/tasks/user/recover scripts/user-prepare.sh— passed.mise exec -- hk check— passed.mise run check— passed (138 tests plus data, shell, and Nix checks).
Design Reconciliation
Delivered as Designed
- Added rollback-before-recovery ordering with restored-revision verification.
- Added restricted managed-user recovery for reversible user state only.
- Preserved explicit force-dotfile behavior without implicit force.
- Added atomic private diagnostics with separate package/app side-effect states.
- Added behavioral coverage for sequence, safe-step exclusion, revision boundaries, force forwarding, reports, and failure semantics.
Intentional Changes
- Recovery uses a compatibility adapter that uploads the current tested recovery helper when the restored repository does not contain a compatible recovery task, so recovery behavior does not depend on the failed revision’s older source.
- The package/app report may be
unknownwhen the initial convergence did not create its event journal; this avoids claiming that package convergence was not started when the observed state is incomplete. Recovery-only events do not change that classification. - If the restored user environment has no usable mise command or trust step, the uploaded current helper runs directly so the recovery failure still produces the diagnostic report.
- Architecture documentation now states the remote rollback/recovery boundary explicitly.
Deferred Work
- Package/app operations that already ran remain manual follow-up; this feature does not attempt external rollback.
- Pull-request and merge metadata remain pending the delivery action selected after close-out.
Rejected or Removed Scope
- No package/app rollback, system activation, Nix rollback, application-backup migration, or legacy Git migration was added to restricted recovery.
- No new privileged transaction namespace or parallel operator command surface was introduced.
Documentation Updated
docs/architecture.mddocs/deployment.mddocs/recovery.mddocs/operations.mddocs/task-reference.mddocs/src/features/maison-014-remote-convergence-restoration/design.mddocs/src/features/maison-014-remote-convergence-restoration/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation:
maison-mol-ugkm, commit80f33aa1d3318e4cb4513af3cde0ba82f59be8dd. - Contract tests:
maison-mol-75xd.1, commit25183b23a3642e0468ce6886d1f7e36eb14df552. - Implementation:
maison-mol-75xd.2, commit0cbb56cdcb78e9093465095b227d0085094acadc. - Documentation reconciliation:
maison-mol-xelx. - Validation:
maison-mol-6ten. - Holistic delivery review:
maison-mol-7dpa. - Documentation drift review:
maison-mol-nc5u.
MAISON-015: Linux runtime activation verification
Delivery Summary
- Beads feature root:
maison-mol-cg3x - Status: delivered
- Pull request: not created (direct fast-forward merge)
- Merge commit:
410d571182fba2c9835f2f33f08bee351cb73ac0(fast-forward intodev) - Design record: design.md
Delivered Capability
Maison now verifies the active Linux runtime as part of system-manager activation instead of assuming that generated configuration files alone changed the running host. The supported Linux runtime is systemd-backed and failures are reported as actionable activation errors.
User-Facing Behavior
Operators continue using the existing maison and mise command surfaces. Linux activation now:
- requires a running systemd runtime and working
systemctl; - verifies
hostname --staticagainst the inventory host name; - manages and verifies the
America/New_Yorktimezone through systemd runtime state and/etc/localtime; - validates SSH configuration with
sshd -tand reloads or restarts the activessh.serviceorsshd.service; - fails when SSH validation or reload fails rather than suppressing the error; and
- verifies that
system-manager.targetandprefill-authorized-keys.serviceare active.
The same system-manager activation path is used for local activation and deploy-rs remote activation. No non-systemd fallback or parallel deployment verification path was added.
Design Integration
The implementation keeps Nix/Lix as the privileged system layer and mise as the user convergence layer. Native system-manager configuration manages the hostname, timezone files, SSH service, and required service units. A small Python standard-library verifier provides field-specific runtime diagnostics, while the existing authorized-key prefill service owns SSH configuration validation and reload failure propagation.
The ownership boundary, supported platforms, absence of Home Manager, public-safe inventory model, and existing operator command surface remain unchanged.
Operational Impact
Linux hosts must run systemd as the active init system. A missing systemd runtime, hostname or timezone mismatch, invalid SSH configuration, failed SSH reload, or inactive required unit stops activation before user convergence can continue. Operators should use the reported runtime field to correct the host or configuration and retry the existing activation command.
See deployment for system activation, operations for the command workflow, and recovery for failed activation behavior.
Reference and Contracts
Validation Evidence
python3 -m unittest -v tests.test_linux_runtime_verification tests.test_deployment_contracts tests.test_ownership_boundary— passed (48 tests).- Runtime helper smoke checks with fake
systemctl,hostname,timedatectl,readlink, andsshdcommands — passed for matching state and hostname mismatch failure. python3 -m py_compile scripts/verify_linux_runtime.py tests/test_linux_runtime_verification.py— passed.shellcheck -x .mise/tasks/system/applyandnixfmt --check nix/modules/linux/system.nix nix/lib/deployments.nix— passed.uv run scripts/check-docs.py— passed after close-out reconciliation.mise exec -- hk check— passed.mise -E dev run check— passed (152 tests plus data, shell, and Nix checks).
Design Reconciliation
Delivered as Designed
- Added a systemd-only pre-activation assertion with an actionable failure when systemd or
systemctlis unavailable. - Managed
/etc/localtimefrom the pinned timezone data while retaining the compatibility/etc/timezonefile. - Added post-activation verification for hostname, timezone, localtime, required service units, and SSH state.
- Made
sshd -tand active SSH reload failures fail activation without suppression. - Reused the same system-manager activation path for local and deploy-rs activation, with explicit degraded-target rejection in both adapters.
- Added behavioral tests for matching state, missing systemd, hostname/timezone/localtime mismatch, SSH failures, inactive units, persistent service state, pre-existing localtime, and activation adapter failure handling.
Intentional Changes
- The reviewed design’s systemd-only decision was recorded explicitly during feature activation; non-systemd fallback remains unsupported rather than receiving a compatibility path.
- Runtime diagnostics live in
scripts/verify_linux_runtime.pyand are embedded into the system-manager closure as a small Python standard-library helper, keeping stateful behavior testable without adding a separate deployment path. - Local and deploy-rs adapters explicitly verify the target and runtime verifier service after system-manager returns, because the pinned system-manager engine reports service-job failures through activation output.
Deferred Work
- Pull-request and merge metadata remain pending the delivery action selected after close-out.
- Actual systemd-host integration testing is not available in the repository validation environment; contract tests and command smoke checks cover the implemented failure boundaries.
Rejected or Removed Scope
- No non-systemd Linux fallback was added.
- No parallel deploy-rs verifier or alternate activation pipeline was introduced.
- No Home Manager, package ownership, user convergence, or supported-platform boundary changes were made.
Documentation Updated
docs/architecture.mddocs/deployment.mddocs/operations.mddocs/recovery.mddocs/src/features/maison-015-linux-runtime-verification/design.mddocs/src/features/maison-015-linux-runtime-verification/index.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md
Audit Trail
- Specification reconciliation and reviewed design:
maison-mol-08vr, commitca36568866916f0e1f96170ea97a75bf46f365e6. - Runtime contract tests:
maison-mol-ln6g.1, commitf4e384adc165381aba24b3554254545bbd63f511. - Runtime implementation:
maison-mol-ln6g.2, commit88532ab9ed9024d251ea361a9282502eecb8f787. - Close-out reconciliation, activation-gating fixes, and navigation: commit
6701bb8ad1bd3a32b47c849e773551c09d16f81f. - Documentation reconciliation:
maison-mol-176d. - Validation:
maison-mol-xq97. - Holistic delivery review:
maison-mol-9n4b. - Documentation drift review:
maison-mol-1hla. - Delivery action:
maison-mol-miwi, fast-forward merge intodevat410d571182fba2c9835f2f33f08bee351cb73ac0.
MAISON-016: Pi TypeScript validation boundary
Delivery Summary
- Beads feature root:
maison-mol-fmud - Status: delivered
- Pull request: pending delivery action
- Merge commit: pending delivery action
- Design record: design.md
Delivered Capability
At delivery, Maison validated its Pi extensions through a repository-owned TypeScript workspace. The workspace pinned the Pi API packages, TypeScript compiler, test runner, and Node type definitions without changing the files symlinked into the user’s Pi installation. The 983-line pager delegated pure Markdown parsing, key decoding, navigation, timer, and terminal-layout behavior to testable modules.
After the Maison/Terroir ownership split, the Pi extension sources and validation workspace were relocated to the private Terroir repository. Maison now retains only public Pi settings defaults and does not publish or validate personal extensions.
User-Facing Behavior
The historical delivery preserved Pi extension registration, command names, clipboard behavior, and pager rendering.
The relocated Terroir workspace retains direct validation with npm ci --ignore-scripts --no-audit --no-fund,
npm run typecheck, and npm test; those commands are no longer Maison tasks.
Design Integration
The implementation keeps Nix/Lix as the privileged system layer and mise as the repository/user tooling layer. Actual
Pi package declarations supply the extension types, while the project-local npm lockfile supplies reproducible
validation dependencies. pager/markdown.ts, pager/keys.ts, and pager/navigation.ts do not import Pi, TUI,
clipboard, process, or terminal APIs. pager.ts remains the integration adapter for rendering and runtime events.
The scoped Node runtime exception is documented explicitly: global configuration serves standalone Pi, and the repository configuration serves locked validation without replacing global user convergence.
Operational Impact
Terroir contributors should run the pinned npm validation commands after changing Pi extensions and remove
node_modules afterward. Maison’s public checks no longer install or traverse the private Pi workspace. No Pi or
OpenCode runtime configuration is changed by the validation metadata.
Reference and Contracts
- Architecture
- Developer tooling
- Tooling reference
- Pi contributor guidance: private Terroir
dotfiles/pi/AGENTS.md - OpenCode boundary: private Terroir
dotfiles/opencode/README.md - Feature design
Validation Evidence
- Historical delivery evidence:
mise run check:typescriptpassed withtsc --noEmitand 10 focused behavioral tests before the workspace moved to Terroir. - Current Terroir evidence:
mise run checkand its GitHub validation workflow run the same typecheck and 10 focused behavioral tests from the private workspace. uv run scripts/check-docs.py— passed after documentation reconciliation.- Historical delivery evidence:
shellcheck .mise/tasks/check/_default .mise/tasks/check/typescriptpassed before the workspace moved to Terroir. git diff --check— passed.mise -E dev run check— passed: 152 Python tests plus data, shell, and Nix checks.- Pure-module boundary inspection — passed: pager modules contain no Pi, TUI, process, or terminal imports.
Design Reconciliation
Delivered as Designed
- Added a private project-local Node workspace with pinned Pi packages, TypeScript,
tsx, and Node types. - Wired
tsc --noEmitand focusednode:testexecution into the Maison check task. - Extracted pure Markdown, key-decoding, navigation, timer-scheduling, and terminal-layout behavior.
- Added behavioral coverage for Unicode, ANSI handling, fenced code, heading numbering, keyboard protocols, navigation, TOC selection, code blocks, deterministic timers, and terminal resizing.
- Preserved the existing Pi registration adapter, commands, and runtime ownership boundaries.
Intentional Changes
- Repository Node 24 was a scoped validation override while global configuration retained the standalone Pi runtime. The ownership test and architecture documentation described this boundary explicitly.
- The extension sources and validation workspace moved to private Terroir after the repository split; Maison’s public framework no longer owns personal Pi behavior.
- The TypeScript validation task removes its temporary npm dependency tree on exit so repository data and Nix checks do not inspect third-party files.
check:dataskipsnode_modulesdefensively for any local Node workspace, while committed package and lock metadata remain validated normally.
Deferred Work
- Pull-request and merge metadata remain pending the delivery action selected after close-out.
- No interactive Pi host integration environment was available; typechecking and pure behavioral tests cover the delivered validation boundary without launching the full Pi UI.
Rejected or Removed Scope
- No visual pager redesign or wholesale pager rewrite was added.
- No alternate Pi registration path, OpenCode runtime configuration, or package-manager convergence path was introduced.
- No Home Manager, Nix system ownership, supported-platform, or user-command boundary changes were made.
Documentation Updated
docs/architecture.mddocs/src/development/tooling.mddocs/src/features/maison-016-pi-typescript-validation/design.mddocs/src/features/maison-016-pi-typescript-validation/index.mddocs/src/reference/tooling.mddocs/src/features/index.mddocs/src/planned-features.mddocs/src/SUMMARY.md- Terroir
dotfiles/pi/AGENTS.md dotfiles/opencode/README.md
Audit Trail
- Specification reconciliation and reviewed design:
maison-mol-40gy, commit4e79de64c8505f50a58cef918b8981458ff4219f. - Workspace implementation:
maison-mol-1h4n.1, commit6c7328397ff54fb51eea15e4fb7b4f977e3e6a33. - Pager modules and behavioral tests:
maison-mol-1h4n.2, commit65fdd7398a4315ff5ab0db4944d8339a022a84b5. - Implementation coordinator:
maison-mol-1h4n. - Documentation reconciliation:
maison-mol-28r8. - Validation:
maison-mol-dgzm. - Holistic delivery review:
maison-mol-4i8o. - Documentation drift review:
maison-mol-u34v. - Delivery action:
maison-mol-p71j, pending explicit selection.
MAISON-017: Maison/Terroir repository split
Delivery Summary
- Beads feature root:
maison-mol-jfhs - Status: delivered
- Pull request: none; direct fast-forward merge authorized
- Merge commit: none; delivery uses a fast-forward merge into
dev - Design record: design.md
Delivered Capability
Maison is now split into a reusable public framework and a private site overlay:
- RobertDeRose/maison is the public dstack-managed framework.
- RobertDeRose/terroir is the private plain-Git overlay for inventory, hosts, personal/site mise policy, dotfiles, and trusted non-secret configuration.
- RobertDeRose/nix-config is private and archived as the historical source.
- Bitwarden remains the source of truth for secrets and private keys.
Each new repository has a fresh logical history. iTerm2 was explicitly excluded because it is being abandoned; no iTerm2 artifacts were migrated to either new repository.
User-Facing Behavior
Clone Maison and provide Terroir as the overlay for a real host:
git clone https://github.com/RobertDeRose/maison.git
cd maison
./bootstrap.sh --host "$(hostname -s)" --overlay git@github.com:RobertDeRose/terroir.git
Maison stores the selected source in the owner-only local state file
${XDG_STATE_HOME:-$HOME/.local/state}/maison/overlay.toml and clones Terroir under
${XDG_DATA_HOME:-$HOME/.local/share}/maison/overlay. The overlay state is machine-local and is never committed.
Design Integration
The split preserves the existing ownership boundary: Nix/Lix owns privileged operating-system state and mise owns the user layer. Maison retains dstack/Copier/Beads controls, validation, tests, Nix modules, neutral examples, and task machinery. Terroir remains ordinary Git data/configuration rather than a second framework project. Public Maison contains no personal infrastructure identity, private keys, or secret values.
Operational Impact
- Real deployment endpoints, users, host overrides, package policy, and personal dotfiles are loaded from Terroir.
- Public Maison remains usable with its neutral starter inventory when no overlay is supplied.
- The archived source repository and owner-only migration manifest remain recovery references.
- No system activation was performed during migration; systemd-backed host integration remains an operational concern for the target hosts rather than a migration prerequisite.
Reference and Contracts
- Maison overview
- Architecture
- Remote deployment
- Operations
- Recovery
- Migration contract
- Inventory reference
- Development tooling
Validation Evidence
python3 scripts/setup-tooling.py --jsonsucceeded in a fresh Maison remote clone.uv run scripts/check-docs.py,mdbook build docs,mise x -- hk check, and shell validation passed in Maison.- Terroir inventory validation passed against the shared Maison schema.
- Terroir’s private Pi workspace passed
mise run check:tsc --noEmitand 10 focused behavioral tests. - Local and remote overlay preparation passed; saved overlay state is mode
0600andhost:list/host:validateresolve the Terroir inventory. - GitHub checks verified Maison public/unarchived, Terroir private/unarchived, and nix-config private/archived, all with
mainas the default branch. - Fresh-history checks found one root per new repository and no shared ancestry with nix-config.
- Published-object privacy scans found no private-key markers, workstation paths, or abandoned iTerm2 artifacts in Maison; Terroir contains no raw secrets or private keys.
- No system activation or source-history rewrite occurred during delivery. Maison history was later normalized and signed under the repository’s current authorship policy.
Design Reconciliation
Delivered as Designed
- Manifest approval preceded private copying, public replacement, publication, and archival.
- New repositories were created from fresh histories without importing nix-config commits, refs, or Beads metadata.
- Maison remains public and dstack-managed; Terroir remains private and plain Git.
- The original repository was archived only after validation passed.
Intentional Changes
- iTerm2 support, preferences, package declarations, workflows, and export tooling were omitted after the user confirmed iTerm2 would be abandoned.
- Workstation-specific paths were removed from public migration documentation and replaced with neutral placeholders.
Post-delivery Reconciliation
- Ported the persistent
nix-hex-boxbuilder fixes that were mistakenly committed to the archived source after delivery. - Restored the approved historical Linux host and its personal identity in private Terroir.
- Restored automatic validation for the private Pi TypeScript workspace and its focused tests.
- Restored the private OpenCode ownership note without reintroducing excluded terminal-multiplexer scope.
Deferred Work
- No real-host system activation or systemd VM integration was available or required for this repository migration.
- Host rollout and later removal of local recovery backups remain operational follow-up after the new topology is exercised.
Rejected or Removed Scope
- Secrets, passwords, tokens, SSH private keys, and signing private keys remain in Bitwarden rather than either repository.
- Legacy implementation paths, source history, and excluded historical artifacts were not copied into Maison or Terroir.
Documentation Updated
README.mddocs/architecture.mddocs/deployment.mddocs/operations.mddocs/recovery.mddocs/migration-contract.mddocs/src/introduction/project-overview.mddocs/src/reference/inventory.mddocs/src/development/tooling.mddocs/src/planned-features.mddocs/src/features/index.mddocs/src/SUMMARY.md- This implemented feature record
Audit Trail
- Manifest:
${XDG_STATE_HOME:-$HOME/.local/state}/maison/maison-017/manifest.toml, owner-only mode0600, explicitly approved and amended to exclude iTerm2. - Implementation children:
maison-mol-ieqc.1through.6, all closed in dependency order. - Current signed Maison content commits:
8300ed926ae785e05fe4ddd81c213f69edfac98c,12455ed8ea6bea981fdaef561a88e7037b8469e1, and774a602a1efc7e836bf14ba7dbe7d4afa652d4f8. - Current signed Terroir content commits:
bfc6dc9b71b99dea1a0b4d4a5a38c16acb74a5ce,e655386928ec315286fc448bdc4a006c6b5160f3, and97a4b41ebc7d57d04acbb759e4e7e10e999709c8. - Source planning/design commits:
43e5fada1b0b88c3d7e33bd2e82057fa83a7f7aaand4ba83d44a31afdd909635650f4db42e76a980d93.
Copier-backed fresh consumer setup and bootstrap onboarding
The former private-overlay runtime is retired, but the Copier template remains a supported setup-time starter for a fresh consumer repository. See Consumer Repository Reference.
Delivery Summary
- Beads feature root:
maison-mol-5s9 - Status: delivered; retained as the fresh-consumer setup scaffold
- Pull request: pending delivery action
- Merge commit:
14a5e92(fast-forward target) - Design record: design.md
Delivered Capability
Maison retains overlay_template/ as a Copier template that renders a consumer flake, inventory, mise policy, dotfile
guidance, Copier answers, and a first-copy host setup task. Bootstrap seeds the inventory username from the current
effective user; the task delegates host registration and supported-platform detection to Maison’s validated host:add
command.
bootstrap.sh --setup PATH renders the template into a destination separate from Maison, creates the consumer lock, and
stops for review before activation. After the operator creates the first consumer commit, --consumer PATH hands it to
normal bootstrap. The generated repository owns its files and Git history. Copier is not a runtime dependency, and Maison
does not save or manage an alternate configuration root.
User-Facing Behavior
Bootstrap setup:
./bootstrap.sh --setup "$HOME/src/terroir" --host "$(hostname -s)"
Manual setup from a Maison checkout:
mise install uv
MAISON_HOME="$PWD" MAISON_CONSUMER_ROOT="$HOME/src/terroir" \
mise exec --locked uv -- uvx --from copier copier copy --trust \
--data "username=$(id -un)" overlay_template "$HOME/src/terroir"
The generated flake.lock is pinned during bootstrap. Review the generated files and create the consumer’s first Git
commit before applying it. copier update --trust updates the starter files but does not rerun host registration.
Design Integration
The feature keeps Maison’s ownership boundary intact: Maison owns the framework, schema, bootstrap orchestration, and
mutation task; the generated consumer owns its flake, lock, inventory, user configuration, and Git history. Inventory
mutation continues through mise run host:add, preserving repository locks, validation, supported-system checks, and
rollback behavior. Copier is installed ephemerally through Maison-managed uv and is not used by runtime commands.
Operational Impact
Fresh setup needs network access for uvx copier, Nix input locking, and normal Git credentials. The generated
repository is initialized as a Git authoring checkout; reviewing and creating its first commit, then publishing a remote,
remain user decisions. A user who declines interactive setup can rerun bootstrap later with --setup.
Reference and Contracts
Validation Evidence
python3 -m unittest tests.test_copier_template tests.test_inventory_behavior— passed: 18 focused tests.bash -nandshellcheckfor bootstrap and generated host setup scripts — passed.ruff checkandruff format --checkfor changed Python files — passed.uv run scripts/check-docs.py— passed.mdbook build docs— passed.mise x -- hk check— passed.mise run check— passed: 156 Python tests plus data, shell, TypeScript, and Nix checks.- No real system activation, remote repository mutation, or host deployment was performed.
Design Reconciliation
Delivered as Designed
- Replaced static overlay-copy guidance with a Copier template.
- Added
MAISON_OVERLAYand preserved explicit--overlayprecedence and saved-state behavior. - Added interactive immediate/deferred onboarding with a safe no-activation path.
- Delegated generated host setup to Maison’s existing
host:addtask. - Added direct local Git overlay support while retaining remote clone/update behavior.
- Added deterministic template, bootstrap, source-precedence, and no-activation tests.
Intentional Changes
MAISON_OVERLAY_SOURCEis compatibility-only; new documentation usesMAISON_OVERLAY.- Declining first-run setup is a successful CLI-only bootstrap rather than neutral starter activation.
- Local overlay paths are recorded and used directly instead of copied to the standard XDG clone directory.
Deferred Work
- Pull-request and merge metadata remain pending the explicit delivery action selected after close-out.
- Real host activation remains outside the available validation environment.
- Creating or publishing a private remote repository remains a user-controlled operation.
Rejected or Removed Scope
- No automatic GitHub repository creation.
- No secrets, private keys, or real identity values in the public template.
- No duplicate inventory mutation implementation in the Copier template.
- No new supported platforms or changes to Nix/mise ownership boundaries.
Documentation Updated
README.mddocs/operations.mddocs/add-a-host.mddocs/src/reference/tooling.mdoverlay_template/README.mdoverlay_template/dotfiles/README.mddocs/src/SUMMARY.mddocs/src/planned-features.mddocs/src/features/maison-overlay-copier-bootstrap/design.mddocs/src/features/maison-overlay-copier-bootstrap/index.mddocs/src/features/index.md
Audit Trail
- Planning and reviewed design:
maison-mol-5s9, commit8cbd6eb. - Copier template and generated host setup:
maison-mol-xke.2, commite7b2c11. - Bootstrap onboarding and overlay source behavior:
maison-mol-xke.1, commit246aceb. - Reader documentation:
maison-mol-xke.3, commit11f37a8. - Implementation coordinator:
maison-mol-xke. - Documentation reconciliation:
maison-mol-o13. - Validation:
maison-mol-2b5. - Holistic delivery review:
maison-mol-a2x. - Documentation drift review:
maison-mol-ecd. - Delivery action:
maison-mol-e0f, merged intomainat14a5e92.
Overlay authoring lifecycle and command surface
Historical record: this alternate repository command surface is retired. Consumer Git history is now managed explicitly with Git; see Migration Contract.
Delivery Summary
- Beads feature root:
maison-mol-3jb - Status: delivered, later retired by
maison-hi8 - Pull request: not created (direct fast-forward merge)
- Merge commit:
dc385400c78c80f81bd7cd9b31b20c629b86fc6d(fast-forward intomain) - Design record: design.md
Delivered Capability
Maison now treats the active private overlay as a safe, inspectable authoring repository. Top-level maison status
reports worktree and upstream state, including stale or last-known remote comparisons, while maison publish fetches,
checks the configured upstream, pushes existing commits, and restores local tracked and untracked work without touching
ignored files. Neither command changes public Maison fallback files or creates arbitrary commits.
Software add/remove commands for tools, packages, and applications now require a private Git overlay. They refresh it fast-forward-only before editing, reject dirty declaration or lock targets, preserve unrelated work, and create focused commits only after successful declaration transactions. Commit failures preserve validated changes and provide manual recovery guidance.
User-Facing Behavior
maison status
maison publish
maison tool add github:owner/tool latest
maison package add brew:tool --macos
maison app remove ghostty
Help prints workflow commands directly beneath Available commands and their subcommands:; it retains the existing
github, app, package, tool, host, system, user, and docs groups. maison status reports clean, dirty,
ahead, behind, diverged, no-upstream, and offline/last-known states. maison publish never selects an implicit remote or
branch, stashes tracked and untracked files only, and leaves a recoverable stash when restoration conflicts.
Generated mutation subjects use literal backticks and the effective identifier, for example
added(tool): \github:owner/tool@version`orremoved(package): `brew:git`. Common declarations remain in config/mise/config.toml; package –macos` selects the Apple Silicon mise configuration, application commands use the
Apple Silicon application configuration, and inventory profiles select Nix modules rather than a mise profile selector.
Design Integration
The implementation reuses Maison’s saved/environment-selected overlay resolution, private authoring-checkout guard, repository mutation lock, mutation journal, parser-backed TOML edits, candidate validation, and user-convergence fallback behavior. The shared overlay Git helper keeps Git paths and refs argument-safe and independently testable. Transaction journals complete before focused commits are attempted, so Git identity or hook failures are post-transaction errors rather than unsafe rollback requests.
The public repository remains the framework and neutral fallback for read-only and convergence paths. Covered software
mutations never write that fallback, and publication remains an explicit operation separate from maison sync.
Operational Impact
Operators can inspect the overlay before authoring or publishing with maison status. Covered add/remove commands may
contact the configured upstream and fail safely when it is unavailable, missing, or divergent. Unrelated local work is
preserved through refresh and publication; declaration or lock targets must be clean before retrying.
A failed generated commit does not roll back an already-successful package or application installation. Inspect the
validated overlay changes, run maison status, and create the documented focused commit manually. A failed push restores
the temporary stash; a restoration conflict leaves that stash available for explicit recovery.
Reference and Contracts
- Architecture
- Operations
- Task reference
- Tooling reference
- Package policy
- Adding a tool
- Adding an app
- Feature design
Validation Evidence
python3 -m unittest -v tests.test_repository_contracts tests.test_overlay_git tests.test_transaction_behavior— passed (54 focused tests).bash -n bin/maisonandshellcheck -x bin/maison— passed.uv run scripts/check-docs.py— passed.rumdl checkon all changed reader-facing Markdown pages — passed.mise run check— passed: 199 tests plus Nix, documentation, shell, Python, type, formatting, and hook checks.- Temporary local Git repositories covered status, refresh, publication, stash recovery, target cleanliness, focused commits, no-overlay refusal, and commit-failure preservation; no real remote publication or system activation was run.
Design Reconciliation
Delivered as Designed
- Flattened only the nonexistent
workflow:help heading and added top-levelstatusandpublishcommands. - Added active-overlay status with fresh or last-known remote comparison and safe publish stash/push/restore behavior.
- Added fast-forward-only pre-mutation refresh and private-overlay-only software authoring.
- Added path-specific target cleanliness checks and focused
added/removedcommits after transaction completion. - Preserved unrelated work, ignored files, public read-only/convergence fallback, and existing
maison syncsemantics. - Documented configuration scope, inventory profile ownership, recovery, and offline behavior.
Intentional Changes
- Covered software add/remove operations now refuse public fallback mutation when no private Git overlay is active.
- Git commit failures preserve validated declaration changes instead of attempting to reverse external installation effects.
- The command surface exposes status and publish directly while retaining namespace grouping for all other command families.
Deferred Work
- Pull-request creation was not performed; this feature uses the explicitly requested direct fast-forward merge.
- Real remote, package installation, Nix activation, and host deployment remain outside the deterministic validation environment.
- Creating or configuring a private remote repository remains an operator-controlled action.
Rejected or Removed Scope
- No automatic publication, arbitrary-edit commits, implicit remote selection, or full
maison syncrefresh was added. - No public Maison mutation fallback, separate status/publish overlay argument, or mise profile selector was introduced.
- No host authoring, inventory ownership, bootstrap, Nix ownership, or supported-platform boundary was changed.
Documentation Updated
README.mddocs/architecture.mddocs/operations.mddocs/task-reference.mddocs/src/reference/tooling.mddocs/package-policy.mddocs/add-a-tool.mddocs/add-an-app.mddocs/src/planned-features.mddocs/src/features/index.mddocs/src/SUMMARY.mddocs/src/features/maison-overlay-authoring-lifecycle/design.mddocs/src/features/maison-overlay-authoring-lifecycle/index.md
Audit Trail
- Specification reconciliation:
maison-mol-43o, commitdb23f2165d58e830223eb2d497b2d7f084ef294b. - Shared overlay Git lifecycle and commands:
maison-mol-0tm.1, commitd2e7b151a31c0ad6e88a0c8a8632007c944d086a. - Transactional software mutation integration:
maison-mol-0tm.2, commit7c75189f4c7578e82a7b209ddcae67e0404de693. - Command surface and reader documentation:
maison-mol-0tm.3, commit10b467bcc04aea38d6dde41f9a9d54d8eada8cb6. - Implementation coordinator:
maison-mol-0tm. - Documentation reconciliation:
maison-mol-9xx. - Validation:
maison-mol-cm8. - Holistic delivery review:
maison-mol-86a. - Documentation drift review:
maison-mol-70j. - Delivery action:
maison-mol-dv6, direct fast-forward merge intomainatdc385400c78c80f81bd7cd9b31b20c629b86fc6d.